From 894a13579183d3195ebedc47b37ad0f7136b725e Mon Sep 17 00:00:00 2001 From: lftobs Date: Sun, 12 Jul 2026 01:43:20 +0100 Subject: [PATCH 1/5] Refactor(grafana): overhaul dashboards and logging - Improve migration error handling when adding clear_cache column - Pass projectId to Grafana dashboard creation and load project domains - Overhaul Grafana dashboards to system overview with metrics - Highlight CRITICAL and ERROR logs in deployment logs --- apps/api/src/db/migrate.ts | 3 +- apps/api/src/orchestrator/pipeline.ts | 1 + apps/api/src/utils/grafana.ts | 449 ++++++++++++++++-- .../project/deployments/deployment-logs.tsx | 2 +- .../grafana/dashboards/deployed-apps.json | 430 +++++++++++++++-- 5 files changed, 810 insertions(+), 75 deletions(-) diff --git a/apps/api/src/db/migrate.ts b/apps/api/src/db/migrate.ts index 9b59800..70797f7 100644 --- a/apps/api/src/db/migrate.ts +++ b/apps/api/src/db/migrate.ts @@ -38,7 +38,8 @@ const addClearCacheColumn = async (db: ReturnType) => { db.run(sql`ALTER TABLE deployments ADD COLUMN clear_cache integer NOT NULL DEFAULT 0`); console.log("[Migrate] Added clear_cache column to deployments table"); } catch (err) { - if (err instanceof Error && err.message.includes("duplicate column name")) return; + const cause = err instanceof Error && "cause" in err ? err.cause : err; + if (cause instanceof Error && cause.message.includes("duplicate column name")) return; console.error("[Migrate] Failed to add clear_cache column:", err); throw err; } diff --git a/apps/api/src/orchestrator/pipeline.ts b/apps/api/src/orchestrator/pipeline.ts index 3e75590..6cc4ec8 100644 --- a/apps/api/src/orchestrator/pipeline.ts +++ b/apps/api/src/orchestrator/pipeline.ts @@ -554,6 +554,7 @@ export class PipelineOrchestrator { .slice(0, 63); const containerRegex = `${dashSlug}-.*`; ensureProjectDashboard( + deployment.projectId!, projectName, containerRegex, ).catch((e) => diff --git a/apps/api/src/utils/grafana.ts b/apps/api/src/utils/grafana.ts index 08304bd..7559654 100644 --- a/apps/api/src/utils/grafana.ts +++ b/apps/api/src/utils/grafana.ts @@ -1,4 +1,5 @@ import { config } from "./config"; +import { listDomains } from "../db/repo"; interface GrafanaDashboard { dashboard: { @@ -62,6 +63,7 @@ async function grafanaPost( } export async function ensureProjectDashboard( + projectId: string, projectName: string, containerRegex: string, ): Promise { @@ -71,6 +73,19 @@ export async function ensureProjectDashboard( .replace(/^-+|-+$/g, "") .slice(0, 63); + const domains = [`${slug}.${config.caddyBaseDomain}`]; + try { + const projectDomains = await listDomains(projectId); + const verified = projectDomains.filter(d => d.validationStatus === 'verified'); + for (const d of verified) { + domains.push(d.domain); + } + } catch (e) { + console.warn("[Grafana] Failed to list domains for dashboard query:", e); + } + + const regexEscaped = domains.map(d => d.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\\\$&')).join('|'); + const dashboard: GrafanaDashboard = { dashboard: { title: `Dequel \u2014 ${projectName}`, @@ -79,20 +94,400 @@ export async function ensureProjectDashboard( schemaVersion: 39, version: 1, timezone: "browser", - refresh: "30s", + refresh: "10s", panels: [ { type: "row", - title: "Resource Usage", + title: "Overview", collapsed: false, gridPos: { h: 1, w: 24, x: 0, y: 0 }, }, + { + id: 5, + type: "stat", + title: "Requests (period)", + datasource: { type: "loki", uid: "loki" }, + gridPos: { h: 3, w: 3, x: 0, y: 1 }, + fieldConfig: { + defaults: { + unit: "none", + color: { mode: "fixed" }, + fixedColor: "blue" + } + }, + options: { + graphMode: "area", + reduceOptions: { calcs: ["lastNotNull"] } + }, + targets: [ + { + datasource: { type: "loki", uid: "loki" }, + expr: `sum(count_over_time({container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" [$__range]))`, + refId: "A" + } + ] + }, + { + id: 6, + type: "stat", + title: "% Success", + datasource: { type: "loki", uid: "loki" }, + gridPos: { h: 3, w: 3, x: 3, y: 1 }, + fieldConfig: { + defaults: { + unit: "percent", + color: { mode: "thresholds" }, + thresholds: { + mode: "absolute", + steps: [ + { color: "red", value: null }, + { color: "yellow", value: 90 }, + { color: "green", value: 95 } + ] + } + } + }, + options: { + graphMode: "area", + reduceOptions: { calcs: ["lastNotNull"] } + }, + targets: [ + { + datasource: { type: "loki", uid: "loki" }, + expr: `(sum(count_over_time({container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" | status >= 100 and status < 400 [$__range])) / sum(count_over_time({container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" [$__range])) * 100) or 0`, + refId: "A" + } + ] + }, + { + id: 7, + type: "stat", + title: "Avg Latency", + datasource: { type: "loki", uid: "loki" }, + gridPos: { h: 3, w: 3, x: 6, y: 1 }, + fieldConfig: { + defaults: { + unit: "s", + color: { mode: "thresholds" }, + thresholds: { + mode: "absolute", + steps: [ + { color: "green", value: null }, + { color: "yellow", value: 0.5 }, + { color: "red", value: 2.0 } + ] + } + } + }, + options: { + graphMode: "area", + reduceOptions: { calcs: ["lastNotNull"] } + }, + targets: [ + { + datasource: { type: "loki", uid: "loki" }, + expr: `avg_over_time({container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" | unwrap duration [$__range])`, + refId: "A" + } + ] + }, + { + id: 8, + type: "stat", + title: "Reqs (2m)", + datasource: { type: "loki", uid: "loki" }, + gridPos: { h: 3, w: 3, x: 9, y: 1 }, + fieldConfig: { + defaults: { + unit: "none", + color: { mode: "fixed" }, + fixedColor: "blue" + } + }, + options: { + graphMode: "line", + reduceOptions: { calcs: ["lastNotNull"] } + }, + targets: [ + { + datasource: { type: "loki", uid: "loki" }, + expr: `sum(count_over_time({container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" [2m]))`, + refId: "A" + } + ] + }, + { + id: 9, + type: "stat", + title: "% Success (2m)", + datasource: { type: "loki", uid: "loki" }, + gridPos: { h: 3, w: 3, x: 12, y: 1 }, + fieldConfig: { + defaults: { + unit: "percent", + color: { mode: "thresholds" }, + thresholds: { + mode: "absolute", + steps: [ + { color: "red", value: null }, + { color: "yellow", value: 90 }, + { color: "green", value: 95 } + ] + } + } + }, + options: { + graphMode: "area", + reduceOptions: { calcs: ["lastNotNull"] } + }, + targets: [ + { + datasource: { type: "loki", uid: "loki" }, + expr: `(sum(count_over_time({container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" | status >= 100 and status < 400 [2m])) / sum(count_over_time({container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" [2m])) * 100) or 0`, + refId: "A" + } + ] + }, + { + id: 10, + type: "stat", + title: "HTTP 1/2xx (2m)", + datasource: { type: "loki", uid: "loki" }, + gridPos: { h: 3, w: 2, x: 15, y: 1 }, + fieldConfig: { + defaults: { + unit: "none", + color: { mode: "fixed" }, + fixedColor: "green" + } + }, + options: { + graphMode: "line", + reduceOptions: { calcs: ["lastNotNull"] } + }, + targets: [ + { + datasource: { type: "loki", uid: "loki" }, + expr: `sum(count_over_time({container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" | status >= 100 and status < 300 [2m]))`, + refId: "A" + } + ] + }, + { + id: 11, + type: "stat", + title: "HTTP 3xx (2m)", + datasource: { type: "loki", uid: "loki" }, + gridPos: { h: 3, w: 2, x: 17, y: 1 }, + fieldConfig: { + defaults: { + unit: "none", + color: { mode: "fixed" }, + fixedColor: "orange" + } + }, + options: { + graphMode: "line", + reduceOptions: { calcs: ["lastNotNull"] } + }, + targets: [ + { + datasource: { type: "loki", uid: "loki" }, + expr: `sum(count_over_time({container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" | status >= 300 and status < 400 [2m]))`, + refId: "A" + } + ] + }, + { + id: 12, + type: "stat", + title: "HTTP 4xx (2m)", + datasource: { type: "loki", uid: "loki" }, + gridPos: { h: 3, w: 2.5, x: 19, y: 1 }, + fieldConfig: { + defaults: { + unit: "none", + color: { mode: "fixed" }, + fixedColor: "yellow" + } + }, + options: { + graphMode: "line", + reduceOptions: { calcs: ["lastNotNull"] } + }, + targets: [ + { + datasource: { type: "loki", uid: "loki" }, + expr: `sum(count_over_time({container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" | status >= 400 and status < 500 [2m]))`, + refId: "A" + } + ] + }, + { + id: 13, + type: "stat", + title: "HTTP 5xx (2m)", + datasource: { type: "loki", uid: "loki" }, + gridPos: { h: 3, w: 2.5, x: 21.5, y: 1 }, + fieldConfig: { + defaults: { + unit: "none", + color: { mode: "fixed" }, + fixedColor: "red" + } + }, + options: { + graphMode: "line", + reduceOptions: { calcs: ["lastNotNull"] } + }, + targets: [ + { + datasource: { type: "loki", uid: "loki" }, + expr: `sum(count_over_time({container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" | status >= 500 [2m]))`, + refId: "A" + } + ] + }, + { + type: "row", + title: "HTTP Ingress Performance", + collapsed: false, + gridPos: { h: 1, w: 24, x: 0, y: 4 }, + }, + { + id: 14, + type: "timeseries", + title: "HTTP Requests / Ingress", + datasource: { type: "loki", uid: "loki" }, + gridPos: { h: 8, w: 8, x: 0, y: 5 }, + fieldConfig: { + defaults: { + unit: "reqps", + custom: { fillOpacity: 10, lineWidth: 1.5 } + } + }, + targets: [ + { + datasource: { type: "loki", uid: "loki" }, + expr: `sum(rate({container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" [$__interval])) by (request_host)`, + legendFormat: "{{request_host}}", + refId: "A" + } + ] + }, + { + id: 15, + type: "timeseries", + title: "HTTP Status Codes", + datasource: { type: "loki", uid: "loki" }, + gridPos: { h: 8, w: 8, x: 8, y: 5 }, + fieldConfig: { + defaults: { + unit: "reqps", + custom: { fillOpacity: 10, lineWidth: 1.5 } + } + }, + targets: [ + { + datasource: { type: "loki", uid: "loki" }, + expr: `sum(rate({container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" [$__interval])) by (status)`, + legendFormat: "HTTP {{status}}", + refId: "A" + } + ] + }, + { + id: 16, + type: "timeseries", + title: "Total HTTP Requests", + datasource: { type: "loki", uid: "loki" }, + gridPos: { h: 8, w: 8, x: 16, y: 5 }, + fieldConfig: { + defaults: { + unit: "none", + custom: { fillOpacity: 25, lineWidth: 1 } + } + }, + targets: [ + { + datasource: { type: "loki", uid: "loki" }, + expr: `sum(count_over_time({container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" [$__interval]))`, + legendFormat: "Requests", + refId: "A" + } + ] + }, + { + type: "row", + title: "Latency", + collapsed: false, + gridPos: { h: 1, w: 24, x: 0, y: 13 }, + }, + { + id: 17, + type: "timeseries", + title: "Latency (Average Percentiles)", + datasource: { type: "loki", uid: "loki" }, + gridPos: { h: 8, w: 12, x: 0, y: 14 }, + fieldConfig: { + defaults: { + unit: "s", + custom: { fillOpacity: 10, lineWidth: 1.5 } + } + }, + targets: [ + { + datasource: { type: "loki", uid: "loki" }, + expr: `quantile_over_time(0.99, {container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" | unwrap duration [$__interval])`, + legendFormat: "p99", + refId: "A" + }, + { + datasource: { type: "loki", uid: "loki" }, + expr: `quantile_over_time(0.95, {container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" | unwrap duration [$__interval])`, + legendFormat: "p95", + refId: "B" + }, + { + datasource: { type: "loki", uid: "loki" }, + expr: `quantile_over_time(0.50, {container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" | unwrap duration [$__interval])`, + legendFormat: "p50 (median)", + refId: "C" + }, + { + datasource: { type: "loki", uid: "loki" }, + expr: `avg_over_time({container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" | unwrap duration [$__interval])`, + legendFormat: "Average", + refId: "D" + } + ] + }, + { + id: 18, + type: "heatmap", + title: "Latency Heatmap", + datasource: { type: "loki", uid: "loki" }, + gridPos: { h: 8, w: 12, x: 12, y: 14 }, + targets: [ + { + datasource: { type: "loki", uid: "loki" }, + expr: `log_histogram({container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" | unwrap duration [$__interval])`, + refId: "A" + } + ] + }, + { + type: "row", + title: "Resource Usage", + collapsed: false, + gridPos: { h: 1, w: 24, x: 0, y: 22 }, + }, { id: 1, type: "timeseries", title: "CPU Usage", datasource: { type: "prometheus", uid: "prometheus" }, - gridPos: { h: 9, w: 12, x: 0, y: 1 }, + gridPos: { h: 8, w: 12, x: 0, y: 23 }, fieldConfig: { defaults: { unit: "short", @@ -126,7 +521,7 @@ export async function ensureProjectDashboard( type: "timeseries", title: "Memory Usage", datasource: { type: "prometheus", uid: "prometheus" }, - gridPos: { h: 9, w: 12, x: 12, y: 1 }, + gridPos: { h: 8, w: 12, x: 12, y: 23 }, fieldConfig: { defaults: { unit: "bytes", @@ -157,55 +552,37 @@ export async function ensureProjectDashboard( }, { type: "row", - title: "Request Metrics", + title: "Logs & Troubleshooting", collapsed: false, - gridPos: { h: 1, w: 24, x: 0, y: 10 }, + gridPos: { h: 1, w: 24, x: 0, y: 31 }, }, { - id: 4, - type: "timeseries", - title: "Request Rate", + id: 19, + type: "logs", + title: "HTTP Request Error Logs", datasource: { type: "loki", uid: "loki" }, - gridPos: { h: 9, w: 24, x: 0, y: 11 }, - fieldConfig: { - defaults: { - unit: "reqps", - custom: { - fillOpacity: 30, - lineWidth: 1, - }, - }, - overrides: [], - }, + gridPos: { h: 10, w: 12, x: 0, y: 32 }, options: { - legend: { - displayMode: "table", - placement: "right", - showLegend: true, - }, - tooltip: { mode: "multi" }, + showLabels: true, + showTime: true, + wrapLogMessage: true, + enableLogDetails: true, + dedupStrategy: "none", }, targets: [ { datasource: { type: "loki", uid: "loki" }, - expr: `sum by(host) (count_over_time({container=~"${containerRegex}"} | json [5m]))`, - legendFormat: "{{host}}", + expr: `{container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" | status >= 400`, refId: "A", }, ], }, - { - type: "row", - title: "Logs", - collapsed: false, - gridPos: { h: 1, w: 24, x: 0, y: 20 }, - }, { id: 3, type: "logs", - title: "Container Logs", + title: "Application Container Logs", datasource: { type: "loki", uid: "loki" }, - gridPos: { h: 12, w: 24, x: 0, y: 21 }, + gridPos: { h: 10, w: 12, x: 12, y: 32 }, options: { showLabels: true, showTime: true, diff --git a/apps/web/src/components/project/deployments/deployment-logs.tsx b/apps/web/src/components/project/deployments/deployment-logs.tsx index 1bf9e3e..fe5396a 100644 --- a/apps/web/src/components/project/deployments/deployment-logs.tsx +++ b/apps/web/src/components/project/deployments/deployment-logs.tsx @@ -175,7 +175,7 @@ export function DeploymentLogs({ {logs.map((log, i) => (
[{log.stage} diff --git a/infra/monitoring/grafana/dashboards/deployed-apps.json b/infra/monitoring/grafana/dashboards/deployed-apps.json index dc9fc72..9fd599f 100644 --- a/infra/monitoring/grafana/dashboards/deployed-apps.json +++ b/infra/monitoring/grafana/dashboards/deployed-apps.json @@ -1,11 +1,11 @@ { - "title": "Dequel — Deployed Apps", + "title": "Dequel — System & Apps Overview", "uid": "dequel-deployed-apps", "tags": ["dequel"], "schemaVersion": 39, - "version": 1, + "version": 2, "timezone": "browser", - "refresh": "30s", + "refresh": "10s", "templating": { "list": [ { @@ -36,37 +36,403 @@ "panels": [ { "type": "row", - "title": "Resource Usage", + "title": "Key Metrics", "collapsed": false, "gridPos": { "h": 1, "w": 24, "x": 0, "y": 0 } }, { "id": 1, + "type": "gauge", + "title": "CPU Busy", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { "h": 5, "w": 6, "x": 0, "y": 1 }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "min": 0, + "max": 100, + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 70 }, + { "color": "red", "value": 85 } + ] + } + } + }, + "options": { + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "sum(rate(container_cpu_usage_seconds_total{id=\"/\"}[5m])) * 100 / max(machine_cpu_cores) or sum(rate(container_cpu_usage_seconds_total{name=~\".*\"}[5m])) * 100 / max(machine_cpu_cores)", + "refId": "A" + } + ] + }, + { + "id": 2, + "type": "gauge", + "title": "Used RAM Memory", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { "h": 5, "w": 6, "x": 6, "y": 1 }, + "fieldConfig": { + "defaults": { + "unit": "percent", + "min": 0, + "max": 100, + "color": { + "mode": "thresholds" + }, + "thresholds": { + "mode": "absolute", + "steps": [ + { "color": "green", "value": null }, + { "color": "yellow", "value": 75 }, + { "color": "red", "value": 90 } + ] + } + } + }, + "options": { + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "container_memory_working_set_bytes{id=\"/\"} * 100 / machine_memory_bytes or sum(container_memory_working_set_bytes{name=~\".*\"}) * 100 / max(machine_memory_bytes)", + "refId": "A" + } + ] + }, + { + "id": 3, + "type": "stat", + "title": "Incoming Data (Total)", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { "h": 5, "w": 4, "x": 12, "y": 1 }, + "fieldConfig": { + "defaults": { + "unit": "bytes", + "color": { + "mode": "fixed" + }, + "fixedColor": "green" + } + }, + "options": { + "graphMode": "area", + "reduceOptions": { + "calcs": ["lastNotNull"] + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "sum(container_network_receive_bytes_total)", + "refId": "A" + } + ] + }, + { + "id": 4, + "type": "stat", + "title": "Outgoing Data (Total)", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { "h": 5, "w": 4, "x": 16, "y": 1 }, + "fieldConfig": { + "defaults": { + "unit": "bytes", + "color": { + "mode": "fixed" + }, + "fixedColor": "green" + } + }, + "options": { + "graphMode": "area", + "reduceOptions": { + "calcs": ["lastNotNull"] + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "sum(container_network_transmit_bytes_total)", + "refId": "A" + } + ] + }, + { + "id": 5, + "type": "stat", + "title": "Total Transferred Data", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { "h": 5, "w": 4, "x": 20, "y": 1 }, + "fieldConfig": { + "defaults": { + "unit": "bytes", + "color": { + "mode": "fixed" + }, + "fixedColor": "green" + } + }, + "options": { + "graphMode": "area", + "reduceOptions": { + "calcs": ["lastNotNull"] + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "sum(container_network_receive_bytes_total) + sum(container_network_transmit_bytes_total)", + "refId": "A" + } + ] + }, + { + "type": "row", + "title": "System Statistics / Ingress", + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 6 } + }, + { + "id": 6, + "type": "stat", + "title": "Bitrate Download", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { "h": 4, "w": 6, "x": 0, "y": 7 }, + "fieldConfig": { + "defaults": { + "unit": "bps", + "color": { + "mode": "fixed" + }, + "fixedColor": "blue" + } + }, + "options": { + "graphMode": "line", + "reduceOptions": { + "calcs": ["lastNotNull"] + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "sum(rate(container_network_receive_bytes_total[5m])) * 8", + "refId": "A" + } + ] + }, + { + "id": 7, + "type": "stat", + "title": "Bitrate Upload", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { "h": 4, "w": 6, "x": 6, "y": 7 }, + "fieldConfig": { + "defaults": { + "unit": "bps", + "color": { + "mode": "fixed" + }, + "fixedColor": "blue" + } + }, + "options": { + "graphMode": "line", + "reduceOptions": { + "calcs": ["lastNotNull"] + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "sum(rate(container_network_transmit_bytes_total[5m])) * 8", + "refId": "A" + } + ] + }, + { + "id": 8, + "type": "stat", + "title": "Running Projects", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { "h": 4, "w": 4, "x": 12, "y": 7 }, + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "fixed" + }, + "fixedColor": "orange" + } + }, + "options": { + "graphMode": "area", + "reduceOptions": { + "calcs": ["lastNotNull"] + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "dequel_active_deployments", + "refId": "A" + } + ] + }, + { + "id": 9, + "type": "stat", + "title": "API Uptime", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { "h": 4, "w": 4, "x": 16, "y": 7 }, + "fieldConfig": { + "defaults": { + "unit": "s", + "color": { + "mode": "fixed" + }, + "fixedColor": "blue" + } + }, + "options": { + "graphMode": "none", + "reduceOptions": { + "calcs": ["lastNotNull"] + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "dequel_uptime_seconds", + "refId": "A" + } + ] + }, + { + "id": 10, + "type": "stat", + "title": "Total API Requests", + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { "h": 4, "w": 4, "x": 20, "y": 7 }, + "fieldConfig": { + "defaults": { + "unit": "none", + "color": { + "mode": "fixed" + }, + "fixedColor": "purple" + } + }, + "options": { + "graphMode": "area", + "reduceOptions": { + "calcs": ["lastNotNull"] + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "dequel_requests_total", + "refId": "A" + } + ] + }, + { + "type": "row", + "title": "Resource History", + "collapsed": false, + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 11 } + }, + { + "id": 11, "type": "timeseries", - "title": "CPU Usage", + "title": "CPU Usage (Cores)", "datasource": { "type": "prometheus", "uid": "prometheus" }, - "gridPos": { "h": 9, "w": 12, "x": 0, "y": 1 }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 12 }, "fieldConfig": { "defaults": { "unit": "short", "custom": { - "stacking": { "mode": "normal" }, - "fillOpacity": 30, - "lineWidth": 1 + "fillOpacity": 20, + "lineWidth": 1.5 } - }, - "overrides": [] - }, - "options": { - "legend": { - "displayMode": "table", - "placement": "right", - "showLegend": true - }, - "tooltip": { "mode": "multi" } + } }, "targets": [ { @@ -81,32 +447,22 @@ ] }, { - "id": 2, + "id": 12, "type": "timeseries", "title": "Memory Usage", "datasource": { "type": "prometheus", "uid": "prometheus" }, - "gridPos": { "h": 9, "w": 12, "x": 12, "y": 1 }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 12 }, "fieldConfig": { "defaults": { "unit": "bytes", "custom": { - "stacking": { "mode": "normal" }, - "fillOpacity": 30, - "lineWidth": 1 + "fillOpacity": 20, + "lineWidth": 1.5 } - }, - "overrides": [] - }, - "options": { - "legend": { - "displayMode": "table", - "placement": "right", - "showLegend": true - }, - "tooltip": { "mode": "multi" } + } }, "targets": [ { @@ -124,17 +480,17 @@ "type": "row", "title": "Logs", "collapsed": false, - "gridPos": { "h": 1, "w": 24, "x": 0, "y": 10 } + "gridPos": { "h": 1, "w": 24, "x": 0, "y": 20 } }, { - "id": 3, + "id": 13, "type": "logs", "title": "Container Logs", "datasource": { "type": "loki", "uid": "loki" }, - "gridPos": { "h": 12, "w": 24, "x": 0, "y": 11 }, + "gridPos": { "h": 12, "w": 24, "x": 0, "y": 21 }, "options": { "showLabels": true, "showTime": true, From 0a56270b33d9ea7fb30f78b99d076e9edf62f8ac Mon Sep 17 00:00:00 2001 From: lftobs Date: Sun, 12 Jul 2026 02:00:40 +0100 Subject: [PATCH 2/5] fix(api): fix project deletion --- apps/api/src/api/projects/index.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/apps/api/src/api/projects/index.ts b/apps/api/src/api/projects/index.ts index 0c5045d..66eb821 100644 --- a/apps/api/src/api/projects/index.ts +++ b/apps/api/src/api/projects/index.ts @@ -6,7 +6,7 @@ import { listProjects, getProjectById, updateProject, - deleteProject, + deleteProjectCascade, listDomains, } from "../../db/repo"; import { tryRun, reloadCaddy } from "../../orchestrator/runtime"; @@ -79,7 +79,6 @@ export const projectsRoutes = new Elysia() return { error: "Project not found" }; } - // Docker containers cleanup for (const name of info.deploymentContainerNames) { await tryRun(dockerBin, ['stop', '-t', '5', name]); await tryRun(dockerBin, ['rm', '-f', name]); @@ -89,22 +88,18 @@ export const projectsRoutes = new Elysia() await tryRun(dockerBin, ['rm', '-f', name]); } - // Docker volumes cleanup for (const name of [...info.databaseVolumeNames, ...info.volumeDockerNames]) { await tryRun(dockerBin, ['volume', 'rm', '-f', name]); } - // Docker images cleanup for (const tag of info.deploymentImageTags) { if (tag) await tryRun(dockerBin, ['rmi', '-f', tag]); } - // Remove domains from Caddy route file for (const { domain, projectName } of info.domains) { await removeFromCaddyRoute(domain, id, projectName); } - // Delete the Caddy route file for this project's slug const caddyFilePath = join(config.caddyRoutesDir, `${info.slug}.caddy`); await unlink(caddyFilePath).catch(() => {}); await reloadCaddy().catch(() => {}); @@ -206,14 +201,14 @@ export const projectsRoutes = new Elysia() } const regexEscaped = domains.map(d => d.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\\\$&')).join('|'); - + // Loki metric query for request rate const query = `sum(count_over_time({container="dequel-caddy-1"} | json | request_host =~ "^(${regexEscaped})$" [5m]))`; - + const end = Math.floor(Date.now() / 1000); const start = end - (6 * 60 * 60); // 6 hours ago const step = "60s"; // 1 min resolution - + try { const response = await fetch( `http://loki:3100/loki/api/v1/query_range?query=${encodeURIComponent(query)}&start=${start}&end=${end}&step=${step}` @@ -304,8 +299,8 @@ export const projectsRoutes = new Elysia() ws.onmessage = (event) => { if (closed) return; try { - const dataStr = typeof event.data === "string" - ? event.data + const dataStr = typeof event.data === "string" + ? event.data : new TextDecoder().decode(event.data as any); const data = JSON.parse(dataStr) as any; if (data.streams) { From f87ee8b75443c63d3fe2a748813b4443a6989769 Mon Sep 17 00:00:00 2001 From: lftobs Date: Mon, 13 Jul 2026 03:50:28 +0100 Subject: [PATCH 3/5] refactor(docs): reorder docs menu and polish UI - Add orderedSlugs and sort doc groups to enforce docs order - Polish hero UI: tweak line breaks and button markup - Polish HowItWorks UI: replace arrow with pulse indicator - Remove Next CTA blocks from docs to streamline navigation - Minor whitespace fixes in auth.md and installation.md --- apps/docs/src/components/Hero.astro | 29 ++++-- apps/docs/src/components/HowItWorks.astro | 24 +++-- apps/docs/src/content/docs/auth.md | 1 + apps/docs/src/content/docs/changelog.md | 1 + apps/docs/src/content/docs/configuration.md | 6 -- apps/docs/src/content/docs/databases.md | 6 -- apps/docs/src/content/docs/deployments.md | 6 -- apps/docs/src/content/docs/domains.md | 6 -- apps/docs/src/content/docs/env-vars.md | 6 -- apps/docs/src/content/docs/index.md | 7 +- apps/docs/src/content/docs/installation.md | 1 + apps/docs/src/content/docs/quickstart.md | 6 -- apps/docs/src/content/docs/scaling.md | 6 -- apps/docs/src/content/docs/ssl.md | 5 - apps/docs/src/content/docs/system-config.md | 1 + apps/docs/src/content/docs/volumes.md | 6 -- apps/docs/src/layouts/Layout.astro | 106 ++++++++++++++++++++ 17 files changed, 149 insertions(+), 74 deletions(-) diff --git a/apps/docs/src/components/Hero.astro b/apps/docs/src/components/Hero.astro index 0a7c8d8..359b666 100644 --- a/apps/docs/src/components/Hero.astro +++ b/apps/docs/src/components/Hero.astro @@ -10,7 +10,7 @@ const { tag = "Private Beta", titleNormal = "Deploy anything.", titleItalic = "Scale everything.", - subText = "The self-hosted cloud platform for developers who want Railway-grade deployment workflows on their own infrastructure. Container orchestration, observability, and scaling — all from one dashboard." + subText = "The self-hosted cloud platform for developers who want Railway-grade deployment workflows on their own infrastructure. Container orchestration, observability, and scaling — all from one dashboard.", } = Astro.props; --- @@ -21,7 +21,7 @@ const {

- {titleNormal}
+ {titleNormal}
{titleItalic}

@@ -33,15 +33,28 @@ const { Read the docs - -
Client Request
my-app.com
- -
+ +
+ + +
Git Push
Repo Webhook
-
+
+ +
@@ -79,18 +84,23 @@ Auto SSL / Proxy PORT 80/443 -
+
+ + +
Dequel API Docker Orchestrator -
- +
+
-
+
+ +
diff --git a/apps/docs/src/content/docs/auth.md b/apps/docs/src/content/docs/auth.md index 7bb9fad..704e7fa 100644 --- a/apps/docs/src/content/docs/auth.md +++ b/apps/docs/src/content/docs/auth.md @@ -51,3 +51,4 @@ API keys are scoped to the entire platform with the same permissions as the user | `GET` | `/api/health` | None | Health check | The web dashboard redirects to `/login` when no valid session is detected. + diff --git a/apps/docs/src/content/docs/changelog.md b/apps/docs/src/content/docs/changelog.md index fdc5455..da489ff 100644 --- a/apps/docs/src/content/docs/changelog.md +++ b/apps/docs/src/content/docs/changelog.md @@ -37,3 +37,4 @@ slug: changelog ### Fixed - Railpack build timeout handling and log scrolling + diff --git a/apps/docs/src/content/docs/configuration.md b/apps/docs/src/content/docs/configuration.md index ac38575..26fbc0a 100644 --- a/apps/docs/src/content/docs/configuration.md +++ b/apps/docs/src/content/docs/configuration.md @@ -44,9 +44,3 @@ app.listen(PORT, '0.0.0.0', () => { }); -
diff --git a/apps/docs/src/content/docs/databases.md b/apps/docs/src/content/docs/databases.md index 5c0cd14..c907adb 100644 --- a/apps/docs/src/content/docs/databases.md +++ b/apps/docs/src/content/docs/databases.md @@ -35,9 +35,3 @@ To inject this secret into your container runtime, navigate to **Environment Var DATABASE_URL=postgresql://postgres:random-password@db-my-web-app.dequel.local:5432/postgres - diff --git a/apps/docs/src/content/docs/deployments.md b/apps/docs/src/content/docs/deployments.md index 7d5a4c0..811d51f 100644 --- a/apps/docs/src/content/docs/deployments.md +++ b/apps/docs/src/content/docs/deployments.md @@ -29,9 +29,3 @@ Each deployment creates an isolated, immutable build artifact. - **Build Logs:** View the live compiler steps as Docker pulls layers, executes commands, and builds cache images. - **Zero-Downtime Rollbacks:** Instantly switch traffic back to a previous build artifact by clicking the **Promote** button on any previously successful deployment. - diff --git a/apps/docs/src/content/docs/domains.md b/apps/docs/src/content/docs/domains.md index 1605aee..dd3e3e7 100644 --- a/apps/docs/src/content/docs/domains.md +++ b/apps/docs/src/content/docs/domains.md @@ -29,9 +29,3 @@ In your DNS registrar's control panel (Cloudflare, GoDaddy, Namecheap, etc.), ad - **Verified:** DNS records resolve correctly. The proxy router is configured. - **Failed:** Resolving record targets did not match the required target targets. Check your DNS values. - diff --git a/apps/docs/src/content/docs/env-vars.md b/apps/docs/src/content/docs/env-vars.md index 28422b0..fbb0ce3 100644 --- a/apps/docs/src/content/docs/env-vars.md +++ b/apps/docs/src/content/docs/env-vars.md @@ -26,9 +26,3 @@ Because environment variables are injected during container startup, modifying o Dequel displays a **Redeployment Warning Banner** at the top of the tab whenever variables are mutated. Click the **Redeploy Now** button on the banner to execute a zero-downtime rolling update with the new configurations. - diff --git a/apps/docs/src/content/docs/index.md b/apps/docs/src/content/docs/index.md index c283f8a..47318df 100644 --- a/apps/docs/src/content/docs/index.md +++ b/apps/docs/src/content/docs/index.md @@ -27,9 +27,4 @@ When you connect a repository or upload a folder: 3. The container image is pushed to the local cluster registry and distributed across running nodes. 4. Caddy routing endpoints are automatically updated to direct traffic to the newly created instance ports. - + diff --git a/apps/docs/src/content/docs/installation.md b/apps/docs/src/content/docs/installation.md index 1dc0662..41c2a61 100644 --- a/apps/docs/src/content/docs/installation.md +++ b/apps/docs/src/content/docs/installation.md @@ -162,3 +162,4 @@ dequel update ``` This pulls the latest images from GitHub Container Registry and recreates the services. + diff --git a/apps/docs/src/content/docs/quickstart.md b/apps/docs/src/content/docs/quickstart.md index 00ea75d..201720b 100644 --- a/apps/docs/src/content/docs/quickstart.md +++ b/apps/docs/src/content/docs/quickstart.md @@ -51,9 +51,3 @@ On the deployments page, you can choose between connecting your GitHub repositor Once the deployment changes status from `BUILDING` to `READY`, click the external link button near your custom domain or the auto-generated base domain to see your running web container. - diff --git a/apps/docs/src/content/docs/scaling.md b/apps/docs/src/content/docs/scaling.md index c9b241d..cafb7d8 100644 --- a/apps/docs/src/content/docs/scaling.md +++ b/apps/docs/src/content/docs/scaling.md @@ -28,9 +28,3 @@ Auto-scaling lets you scale compute bounds in response to real-time resource dem To disable auto-scaling policies or switch back to static scaling, click **Delete Policy** in the scaling panel. Confirm the change in the custom Radix confirmation dialog to update the scaling configuration safely. - diff --git a/apps/docs/src/content/docs/ssl.md b/apps/docs/src/content/docs/ssl.md index d79d3ff..07724db 100644 --- a/apps/docs/src/content/docs/ssl.md +++ b/apps/docs/src/content/docs/ssl.md @@ -32,8 +32,3 @@ If your domain status is stuck on `PENDING_SSL`: - Verify that your DNS CNAME/A records have fully propagated. - Ensure that no CDN service (e.g. Cloudflare proxy) is blocking HTTP ACME challenge endpoints (`/.well-known/acme-challenge/`). - diff --git a/apps/docs/src/content/docs/system-config.md b/apps/docs/src/content/docs/system-config.md index 989c4af..5851c4f 100644 --- a/apps/docs/src/content/docs/system-config.md +++ b/apps/docs/src/content/docs/system-config.md @@ -106,3 +106,4 @@ Config file equivalent: "envEncryptionKey": "your-secure-key-here" } ``` + diff --git a/apps/docs/src/content/docs/volumes.md b/apps/docs/src/content/docs/volumes.md index 304c424..7583e1b 100644 --- a/apps/docs/src/content/docs/volumes.md +++ b/apps/docs/src/content/docs/volumes.md @@ -26,9 +26,3 @@ Under the **Volumes** tab, click **Add Volume**: Because data is tied directly to cluster block volumes, data persists even if you update environment variables, scale container replicas, or deploy code rollbacks. - diff --git a/apps/docs/src/layouts/Layout.astro b/apps/docs/src/layouts/Layout.astro index 41f0636..4156d78 100644 --- a/apps/docs/src/layouts/Layout.astro +++ b/apps/docs/src/layouts/Layout.astro @@ -34,6 +34,22 @@ const description = ""; const allDocs = await getCollection("docs"); +const orderedSlugs = [ + "docs", // Introduction + "installation", // Installation + "quickstart", // Quickstart Guide + "configuration", // Configuration + "deployments", // Deployments + "env-vars", // Environment Variables + "scaling", // Scaling Policies + "system-config", // System Configuration + "databases", // Managed Databases + "volumes", // Persistent Volumes + "auth", // Authentication & Access Control + "domains", // Custom Domains + "ssl", // SSL Certificates + "changelog", // Changelog +]; const categoryOrder = [ "Getting Started", "Core Architecture", @@ -51,6 +67,15 @@ for (const entry of allDocs) { slug: entry.data.slug, }); } + +for (const cat in grouped) { + grouped[cat].sort((a, b) => { + const indexA = orderedSlugs.indexOf(a.slug); + const indexB = orderedSlugs.indexOf(b.slug); + return indexA - indexB; + }); +} + const docMenu = categoryOrder .filter((cat) => grouped[cat]) .map((cat) => ({ title: cat, items: grouped[cat] })); @@ -477,6 +502,87 @@ const docMenu = categoryOrder } + + { + (() => { + const currentIndex = orderedSlugs.indexOf(currentSlug); + if (currentIndex === -1) return null; + + const prevSlug = currentIndex > 0 ? orderedSlugs[currentIndex - 1] : null; + const nextSlug = currentIndex < orderedSlugs.length - 1 ? orderedSlugs[currentIndex + 1] : null; + + const prevDoc = prevSlug ? allDocs.find((d) => d.data.slug === prevSlug) : null; + const nextDoc = nextSlug ? allDocs.find((d) => d.data.slug === nextSlug) : null; + + if (!prevDoc && !nextDoc) return null; + + return ( +
+ {prevDoc ? ( + + + + +
+ + Previous + + + {prevDoc.data.title} + +
+
+ ) : ( + From 9ea08480b7792d2f7bcdb71ecd073537559d4a9c Mon Sep 17 00:00:00 2001 From: lftobs Date: Mon, 13 Jul 2026 04:24:41 +0100 Subject: [PATCH 4/5] chore: pending changelog for v0.2.0 --- .tegami/2026-07-13-changes-since-v0.1.1.md | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .tegami/2026-07-13-changes-since-v0.1.1.md diff --git a/.tegami/2026-07-13-changes-since-v0.1.1.md b/.tegami/2026-07-13-changes-since-v0.1.1.md new file mode 100644 index 0000000..34a9b01 --- /dev/null +++ b/.tegami/2026-07-13-changes-since-v0.1.1.md @@ -0,0 +1,27 @@ +--- +packages: + npm:dequel-api: minor + npm:dequel-web: minor + npm:dequel-docs: minor +--- + +## What's Changed + +### New Features + +- feat(api): add PAM auth and token utilities +- feat(deploy): support specific commit deployments and cache clearing +- feat(api): add drizzle migrations and deploy UI +- feat(deployments): add ClearCacheToggle UI + +### Improvements + +- refactor(api): switch PAM auth to HTTP service +- refactor(auth): add timeout and libc fixes +- refactor(grafana): overhaul dashboards and logging +- chore(scripts): improve shell compatibility in installer + +### Bug Fixes + +- fix: robust migration and UI log tweaks +- fix(api): fix project deletion cascade From f5162f98b9c445676b13691001b52be542128bee Mon Sep 17 00:00:00 2001 From: rm -rf <106287048+Lftobs@users.noreply.github.com> Date: Mon, 13 Jul 2026 05:24:48 +0100 Subject: [PATCH 5/5] Delete .tegami/2026-07-13-changes-since-v0.1.1.md --- .tegami/2026-07-13-changes-since-v0.1.1.md | 27 ---------------------- 1 file changed, 27 deletions(-) delete mode 100644 .tegami/2026-07-13-changes-since-v0.1.1.md diff --git a/.tegami/2026-07-13-changes-since-v0.1.1.md b/.tegami/2026-07-13-changes-since-v0.1.1.md deleted file mode 100644 index 34a9b01..0000000 --- a/.tegami/2026-07-13-changes-since-v0.1.1.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -packages: - npm:dequel-api: minor - npm:dequel-web: minor - npm:dequel-docs: minor ---- - -## What's Changed - -### New Features - -- feat(api): add PAM auth and token utilities -- feat(deploy): support specific commit deployments and cache clearing -- feat(api): add drizzle migrations and deploy UI -- feat(deployments): add ClearCacheToggle UI - -### Improvements - -- refactor(api): switch PAM auth to HTTP service -- refactor(auth): add timeout and libc fixes -- refactor(grafana): overhaul dashboards and logging -- chore(scripts): improve shell compatibility in installer - -### Bug Fixes - -- fix: robust migration and UI log tweaks -- fix(api): fix project deletion cascade