LogTide
v1.1.0
Released
Feature

1.1.0: Inbound Webhook Receivers & Project Soft-Delete

A feature release. External systems can now push events into LogTide through tokenized inbound webhook receivers (GitHub, Uptime, generic JSON), and deleting a project moves it to a recoverable 30-day trash instead of wiping it. Plus five dashboard and log-count correctness fixes.

  • Inbound webhook receivers: GitHub, Uptime Robot / Better Stack and generic-JSON senders can push events in via per-receiver tokenized URLs
  • Received events flow through the full pipeline — PII masking, quotas, Sigma detection, metering and live tail all apply
  • Project soft-delete: deletion moves a project to a recoverable 30-day trash (still queryable, read-only) with a Restore action
  • Log total is now exact below 50k rows on TimescaleDB instead of an unreliable planner estimate (#271)
  • Activity Overview chart tracks live data again instead of lagging 1–2h behind (#274)
  • Dashboard editing works over plain HTTP on a LAN IP again (#272)

A feature release. The two headline additions are inbound webhook receivers — a tokenized ingestion path so external systems (CI/CD, uptime monitors, arbitrary tools) can push events into LogTide — and project soft-delete, which turns project deletion into a recoverable 30-day trash instead of an immediate wipe. Alongside them, five dashboard and log-count correctness fixes.

Added

  • Inbound webhook receivers (#155): external systems can now push events into LogTide through per-receiver tokenized endpoints (POST /api/v1/receivers/:id/:token). Tokens are lr_-prefixed, stored as SHA-256 hashes and compared timing-safe — the URL is the credential, since senders like GitHub and Uptime Robot can’t add custom headers. Three adapters normalize payloads into log entries: GitHub (workflow_run / deployment_status with conclusion-to-level mapping; ping and unsupported events marked skipped), Uptime (shape-detects Uptime Robot and Better Stack incident payloads), and Generic JSON (optional dot-path field mapping with levelMap and defaults; the full payload is always preserved in metadata). The endpoint answers 202 and processing is asynchronous via a new receiver-events queue job, which runs the adapter, validates against logSchema and ingests through ingestionService.ingestLogs — so PII masking (fail-closed), usage quotas, Sigma detection, pipelines, metering and live tail all apply automatically. Each outcome (processed/skipped/failed, normalized output, error) is recorded on a receiver_events row pruned to the last 100 per receiver. Management is project-scoped CRUD + a recent-events API (session auth, audit-logged), a new receivers.max capability limit enforced with the canonical withLimitLock pattern, and a “Webhook Receivers” section in project settings (adapter picker, one-time URL reveal with copy, enable/disable, recent-events viewer). Migration 052
  • Soft-delete for projects with a 30-day grace window: deleting a project now moves it to a recoverable “trash” state (deleted_at) instead of removing it immediately. Its logs, traces and metrics stay queryable and the project stays viewable read-only during the window — the detail view shows a “deleted (read-only)” banner with the permanent-deletion date and hides the Settings tab — and a Restore action brings it back. A daily purge worker (3 AM) hard-deletes projects past the window, calling reservoir.purgeProject() to clear logs/spans/metrics from every storage engine before deleting the row. Name and slug uniqueness moved to partial unique indexes (active projects only), so a deleted project’s name/slug can be reused; restoring into a name/slug a new project has since taken is rejected with a 409 instead of a raw constraint error. Soft-deleted projects are excluded from listings, data-availability widgets and API-key verification (ingestion is refused immediately). New POST /api/v1/projects/:id/restore and GET /api/v1/projects?includeDeleted=true. Migrations 050 (soft-delete column + partial indexes) and 051 (drops ON DELETE CASCADE from logs/spans/metrics so an out-of-band project delete can’t bulk-wipe data — the purge worker clears reservoir data explicitly first)

Fixed

  • Log total no longer disagrees with the visible logs on TimescaleDB (#271): the result count came from the Postgres query planner’s row estimate, which is unreliable at small scale (stale ANALYZE stats, few rows per chunk) and could be wrong in either direction. countEstimate now falls back to an exact COUNT below a 50k-row estimate and keeps the fast planner estimate above it, so small and moderate deployments see an accurate total while large datasets stay performant
  • Activity Overview chart stuck 1–2h behind live data (#274): on TimescaleDB the panel read its logs/spans/detections series entirely from continuous aggregates, whose refresh policy never materializes the most recent 1–2 buckets, so the chart’s latest edge showed empty while logs were still arriving. The panel now reads the aggregate only up to a bucket-aligned cutoff two buckets back and backfills the recent tail live from the raw tables. ClickHouse/MongoDB were unaffected
  • Hosts missing from the filter dropdown (#273): getDistinctHostnames clamped its lookup to 6 hours regardless of the selected range, so hosts that had only logged 6–24h ago were dropped from the dropdown even though their logs were still visible. The window now honors the requested range (default 24h), and hostnames stay cached for 5 minutes to bound the JSONB distinct cost
  • Dashboard editing over plain HTTP on a LAN IP (#272): adding a panel called crypto.randomUUID(), which only exists in a secure context (HTTPS or localhost), so it threw when LogTide was accessed over plain HTTP by IP. Panel IDs now go through a uuid() helper that falls back to crypto.getRandomValues() in non-secure contexts
  • Top Services table window was unlabeled (#275): the “Top Services by Volume” widget covers a fixed 7-day window, but sat next to the 24h-framed activity chart with no label, so its list and total read like stale data. The title now states its window (“Last 7 Days”); the 7-day window itself is intentional and unchanged