Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions .cursor/debug.log
Original file line number Diff line number Diff line change
Expand Up @@ -397,3 +397,42 @@
{"data":{"method":"GET","status":200,"url":"https://athena-db.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1772171281746}
{"data":{"method":"GET","status":200,"url":"https://mirror2.athena-db.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1772171341737}
{"data":{"method":"GET","status":200,"url":"https://athena-db.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1772171341739}
{"data":{"value":null},"hypothesisId":"A","location":"metrics.rs:initialize","message":"env OTEL_METRICS_EXPORTER","runId":"pre-fix","sessionId":"debug-session","timestamp":1777342896251}
{"data":{},"hypothesisId":"B","location":"metrics.rs:initialize","message":"no exporter configured","runId":"pre-fix","sessionId":"debug-session","timestamp":1777342896256}
{"data":{"has_traces":true},"hypothesisId":"D","location":"tracing.rs:create_tracer","message":"tracer initialized","runId":"pre-fix","sessionId":"debug-session","timestamp":1777342896268}
{"data":{},"hypothesisId":"C","location":"metrics.rs:new","message":"meter created","runId":"pre-fix","sessionId":"debug-session","timestamp":1777342896301}
{"data":{"value":null},"hypothesisId":"A","location":"metrics.rs:initialize","message":"env OTEL_METRICS_EXPORTER","runId":"pre-fix","sessionId":"debug-session","timestamp":1777343044064}
{"data":{},"hypothesisId":"B","location":"metrics.rs:initialize","message":"no exporter configured","runId":"pre-fix","sessionId":"debug-session","timestamp":1777343044065}
{"data":{"has_traces":true},"hypothesisId":"D","location":"tracing.rs:create_tracer","message":"tracer initialized","runId":"pre-fix","sessionId":"debug-session","timestamp":1777343044065}
{"data":{},"hypothesisId":"C","location":"metrics.rs:new","message":"meter created","runId":"pre-fix","sessionId":"debug-session","timestamp":1777343044068}
{"data":{"value":null},"hypothesisId":"A","location":"metrics.rs:initialize","message":"env OTEL_METRICS_EXPORTER","runId":"pre-fix","sessionId":"debug-session","timestamp":1777368522702}
{"data":{},"hypothesisId":"B","location":"metrics.rs:initialize","message":"no exporter configured","runId":"pre-fix","sessionId":"debug-session","timestamp":1777368522708}
{"data":{"has_traces":true},"hypothesisId":"D","location":"tracing.rs:create_tracer","message":"tracer initialized","runId":"pre-fix","sessionId":"debug-session","timestamp":1777368522721}
{"data":{},"hypothesisId":"C","location":"metrics.rs:new","message":"meter created","runId":"pre-fix","sessionId":"debug-session","timestamp":1777368522756}
{"data":{"value":null},"hypothesisId":"A","location":"metrics.rs:initialize","message":"env OTEL_METRICS_EXPORTER","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369026430}
{"data":{},"hypothesisId":"B","location":"metrics.rs:initialize","message":"no exporter configured","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369026430}
{"data":{"has_traces":true},"hypothesisId":"D","location":"tracing.rs:create_tracer","message":"tracer initialized","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369026430}
{"data":{},"hypothesisId":"C","location":"metrics.rs:new","message":"meter created","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369026434}
{"data":{"method":"GET","status":200,"url":"https://athena-cluster.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369026567}
{"data":{"method":"GET","status":200,"url":"https://athena-cluster.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369086546}
{"data":{"method":"GET","status":200,"url":"https://athena-cluster.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369146544}
{"data":{"value":null},"hypothesisId":"A","location":"metrics.rs:initialize","message":"env OTEL_METRICS_EXPORTER","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369186162}
{"data":{},"hypothesisId":"B","location":"metrics.rs:initialize","message":"no exporter configured","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369186162}
{"data":{"has_traces":true},"hypothesisId":"D","location":"tracing.rs:create_tracer","message":"tracer initialized","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369186163}
{"data":{},"hypothesisId":"C","location":"metrics.rs:new","message":"meter created","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369186166}
{"data":{"method":"GET","status":200,"url":"https://mirror3.athena-db.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369186234}
{"data":{"method":"GET","status":200,"url":"https://athena-cluster.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369186270}
{"data":{"method":"GET","status":200,"url":"https://mirror3.athena-db.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369188390}
{"data":{"method":"GET","status":200,"url":"https://mirror3.athena-db.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369239608}
{"data":{"method":"GET","status":200,"url":"https://mirror3.athena-db.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369246221}
{"data":{"method":"GET","status":200,"url":"https://athena-cluster.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369246272}
{"data":{"method":"GET","status":200,"url":"https://mirror3.athena-db.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369246683}
{"data":{"method":"GET","status":200,"url":"https://athena-cluster.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369261460}
{"data":{"method":"GET","status":200,"url":"https://mirror3.athena-db.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369306222}
{"data":{"method":"GET","status":200,"url":"https://athena-cluster.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369306272}
{"data":{"method":"GET","status":200,"url":"https://mirror3.athena-db.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369366233}
{"data":{"method":"GET","status":200,"url":"https://athena-cluster.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369366300}
{"data":{"method":"GET","status":200,"url":"https://mirror3.athena-db.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369426224}
{"data":{"method":"GET","status":200,"url":"https://athena-cluster.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369426270}
{"data":{"method":"GET","status":200,"url":"https://mirror3.athena-db.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369486226}
{"data":{"method":"GET","status":200,"url":"https://athena-cluster.com"},"hypothesisId":"E","location":"http_probe.rs:call_endpoint","message":"response received","runId":"pre-fix","sessionId":"debug-session","timestamp":1777369486267}
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,13 @@ See `.env.example.github` for detailed documentation of all GitHub workflow envi
- Pressing the restart button sends a POST to `/api/restart` and executes the shell command in `XBP_RESTART_CMD`.
- **Building the dashboard**: before running `cargo run`, go to the `dashboard/` directory, install dependencies (`npm install`), and run `npm run build`. That produces the static files in `dashboard/out/`, which the Rust server serves at `/dashboard`.

### Dashboard local development (Next + Rust)

- Rust backend serves API endpoints on `http://127.0.0.1:3000`.
- Dashboard dev server runs on `http://127.0.0.1:3001/dashboard`.
- Next rewrites proxy `/api/*`, `/probes/*`, and `/stories/*` from port `3001` to Rust on port `3000`.
- Start backend first, then run dashboard dev in another terminal to avoid API 404 HTML responses.

## Config and YAML

- Deserialize config with `serde_yaml`; top-level shape is `Config { probes, stories }`.
Expand Down
164 changes: 164 additions & 0 deletions dashboard/app/config/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
"use client";

import { useCallback, useEffect, useState } from "react";
import { Button, Input, Label, TextField, Spinner } from "@heroui/react";
import { Icon } from "@iconify/react";
import { useApiUrl } from "@/lib/useApiUrl";
import { useToast } from "@/components/ToastProvider";
import { restartServer } from "@/lib/api";

export default function ConfigPage() {
const { toast } = useToast();
const { url: apiUrl, setUrl: setApiUrl, mounted } = useApiUrl();
const [yaml, setYaml] = useState("");
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [restarting, setRestarting] = useState(false);
const [tempApiUrl, setTempApiUrl] = useState("");

const loadConfig = useCallback(async () => {
try {
const res = await fetch("/api/config");
if (!res.ok) throw new Error(await res.text());
setYaml(await res.text());
} catch (err) {
toast(String(err), { variant: "danger" });
} finally {
setLoading(false);
}
}, [toast]);

useEffect(() => {
loadConfig();
}, [loadConfig]);

useEffect(() => {
if (mounted) setTempApiUrl(apiUrl);
}, [apiUrl, mounted]);

async function handleSave() {
setSaving(true);
try {
const res = await fetch("/api/config", {
method: "PUT",
headers: { "Content-Type": "text/yaml" },
body: yaml,
});
if (!res.ok) throw new Error(await res.text());
toast("Config saved. Restart to apply changes.", { variant: "success" });
} catch (err) {
toast(String(err), { variant: "danger" });
} finally {
setSaving(false);
}
}

async function handleRestart() {
if (!confirm("Restart the monitoring service?")) return;
setRestarting(true);
try {
await restartServer();
toast("Restart signal sent.", { variant: "success" });
} catch (err) {
toast(String(err), { variant: "danger" });
} finally {
setRestarting(false);
}
}

function handleSaveApiUrl() {
setApiUrl(tempApiUrl);
toast("API base URL saved.", { variant: "success" });
}

function resetApiUrl() {
setTempApiUrl(apiUrl);
toast("Changes discarded.", { variant: "warning" });
}

return (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold">Config</h1>
<p className="text-sm text-muted mt-0.5">
Raw YAML configuration — edits here require a restart
</p>
</div>
<div className="flex gap-2">
<Button
variant="ghost"
onPress={loadConfig}
isPending={loading}
>
<Icon icon="gravity-ui:arrow-rotate-right" className="size-4" />
Reload
</Button>
<Button
variant="primary"
onPress={handleSave}
isPending={saving}
>
<Icon icon="gravity-ui:floppy-disk" className="size-4" />
Save
</Button>
<Button
variant="danger"
onPress={handleRestart}
isPending={restarting}
>
<Icon icon="gravity-ui:power-switch" className="size-4" />
Restart
</Button>
</div>
</div>

{mounted && (
<div className="rounded-xl border border-default-200 bg-white p-5 shadow-sm">
<div className="mb-4">
<h2 className="text-sm font-semibold text-default-900">API Settings</h2>
<p className="text-xs text-default-500 mt-1">
Configure where the dashboard sends API requests. Leave empty to use the same origin (production).
</p>
</div>
<div className="space-y-3">
<TextField className="max-w-md" name="api-base-url">
<Label>API Base URL</Label>
<Input
placeholder="http://127.0.0.1:3000"
value={tempApiUrl}
onChange={(e) => setTempApiUrl(e.target.value)}
/>
<div className="text-xs text-default-500 mt-1">
e.g., http://127.0.0.1:3000 for local dev, empty for production
</div>
</TextField>
<div className="flex gap-2">
<Button size="sm" onPress={handleSaveApiUrl}>Save URL</Button>
<Button size="sm" variant="ghost" onPress={resetApiUrl}>Discard</Button>
</div>
</div>
</div>
)}

{loading ? (
<div className="flex justify-center py-16">
<Spinner size="lg" />
</div>
) : (
<div className="bg-content1 border border-divider rounded-xl overflow-hidden">
<div className="bg-content2 px-4 py-2 flex items-center gap-2 border-b border-divider">
<Icon icon="gravity-ui:code" className="size-4 text-muted" />
<span className="text-xs text-muted font-mono">xbp.yml</span>
</div>
<textarea
className="w-full min-h-[60vh] bg-transparent p-4 font-mono text-sm resize-y outline-none text-foreground"
value={yaml}
onChange={(e) => setYaml(e.target.value)}
spellCheck={false}
/>
</div>
)}
</div>
);
}
Loading
Loading