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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ dashboard view (`g`), as plain text, and as `/dispatch graph html`, a self conta
opens from disk (still no server and no port) with the topology drawn Node-RED style
([`docs/graph.md`](docs/graph.md)).

![The trigger and flow graph as /dispatch graph html draws it: cron and forge triggers wired to their flows, an observed chain edge carrying its run count, a potential mention, cron re-arm loops with their schedules, an orphan skill dimmed, per-trigger run counts with outcomes, and the legend stating the chain caps](docs/images/graph-view.png?v=0.9.0)
![The trigger and flow graph as /dispatch graph html draws it: cron and forge triggers wired to their flows, an observed chain edge carrying its run count, a potential mention, a skill with its prose loop grouped inside it, cron re-arm loops with their schedules, an orphan skill dimmed, the forge group naming the repos its runs hit, and the legend stating the chain caps](docs/images/graph-view.png?v=0.9.1)

```bash
pi install npm:@edgehero/pi-dispatch-admin # then, in pi: /dispatch
Expand Down
3 changes: 2 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,8 @@ Stated openly rather than discovered later:
- **The graph export writes one static HTML file, on your keystroke, to a temp path it names.** What
crosses into it: trigger configuration you authored, skill names and frontmatter from the repos you
service, and the PII-free run-record fields the panel already shows. What never does: raw job log
bytes, issue or task text, session material, or a host path beyond a folder's basename. Nothing
bytes, issue or task text, session material, or a host path beyond a folder's basename unless you
pass `--full-paths` yourself (your own reviewed `run.folder` config, on your keystroke). Nothing
listens and nothing serves the file — opening it is a local browser reading local bytes, and the
page makes no network request of any kind. The browser spawn is best-effort, skipped and announced
over SSH or without a display; the printed `file://` URL is the contract.
Expand Down
2 changes: 1 addition & 1 deletion admin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ One command puts a live TUI over the whole deployment:
- **The topology, as a graph.** The GRAPH view (`g`) and `/dispatch graph` show what triggers what, what chained to what in the recorded runs, and what a skill's own text says it might chain to, with orphan skills and dangling triggers flagged. `/dispatch graph html` draws the same picture Node-RED style in your browser, from one self contained file on disk: no server, no port, and the page reloads itself when you regenerate it.

<p align="center">
<img src="https://raw.githubusercontent.com/edgehero/pi-dispatch/main/docs/images/graph-view.png?v=0.9.0" alt="The trigger and flow graph: triggers wired to their flows, an observed chain edge with its count, a potential mention, cron re-arm loops, an orphan skill dimmed, and the legend stating the chain caps" width="820">
<img src="https://raw.githubusercontent.com/edgehero/pi-dispatch/main/docs/images/graph-view.png?v=0.9.1" alt="The trigger and flow graph: triggers wired to their flows, an observed chain edge with its count, a potential mention, a skill with its prose loop grouped inside it, cron re-arm loops, an orphan skill dimmed, the forge group naming the repos its runs hit, and the legend stating the chain caps" width="820">
</p>

- **Triggers, editable live.** cron, label, comment and pull_request triggers with colored drill-ins showing what fires each one, what it runs, and its trust model. Added, edited and deleted without a restart. Triggers that run third-party code or a custom image are badged; opting in or out of either stays an edit to the reviewed `triggers.json`, which neither the console nor a model-callable tool will make for you.
Expand Down
9 changes: 7 additions & 2 deletions admin/src/dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { windowEndAt } from "@edgehero/pi-dispatch/pause-windows";
// injected seams (`fetchCosts`/`listPricedModels`/`whatIf`), never the façade, so tests stay fully
// canned and the one worker/pricing coupling sits beside the queue and redis this module already owns.
import * as pricing from "@edgehero/pi-dispatch/pricing";
import { listRuns, readSettingsView, mapSchedulers, readTriggers, readPauseWindows, readStagedPackages, readSubscriptions, scanRunRecords, GRAPH_LIMITS, cronRunStats, joinRunsToTriggers, observedChainEdges, collectGraphInputs } from "./read-model.mjs";
import { listRuns, readSettingsView, mapSchedulers, readTriggers, readPauseWindows, readStagedPackages, readSubscriptions, scanRunRecords, GRAPH_LIMITS, cronRunStats, joinRunsToTriggers, observedChainEdges, collectGraphInputs, forgeRepoTargets } from "./read-model.mjs";
import { renderStatus, renderBudget, renderTriggers, renderSettingsView, renderGraph } from "./render.mjs";
import { buildGraphModel } from "./graph-model.mjs";
import { matchesKey } from "./keys.mjs";
Expand Down Expand Up @@ -159,6 +159,7 @@ export function createDashboardDeps(paths: any) {
cronStats: cronRunStats({ records: recs, schedulerIds: triggerList.filter((t) => t.type === "cron" && typeof t.id === "string").map((t) => t.id) }),
runJoin: joinRunsToTriggers({ records: recs, triggerCount: triggersView?.count, triggerTypes: Object.fromEntries(triggerList.map((t) => [t.index, t.type])) }),
chainEdges: observedChainEdges({ records: recs }),
forgeRepos: forgeRepoTargets({ records: recs }),
caps: { chainDepthMax: paths.chainDepthMax, chainMaxPerJob: paths.chainMaxPerJob, windowDays: GRAPH_LIMITS.windowDays },
nowMs,
});
Expand Down Expand Up @@ -1636,8 +1637,11 @@ function graphRowLine(row: any, cursor: boolean, inner: number, styler: any): st
const g = row.group;
const fold = styler.fg("accent", row.folded ? G.foldClosed : G.foldOpen);
const label = g.kind === "forge" ? `forge ${g.label}` : `folder ${g.path ?? g.label}`;
// Record-derived scope for a forge group: which repos its runs actually hit in the window,
// because a forge trigger's config names none and "github" alone answers nothing.
const seen = g.kind === "forge" && Array.isArray(g.repos) && g.repos.length > 0 ? styler.fg("dim", ` · ran against ${g.repos.join(", ")}`) : "";
const state = g.unreachable ? styler.fg("warning", ` · ${g.kind === "forge" ? "skills unverifiable from this host" : g.unreachable}`) : g.head ? styler.fg("dim", ` · HEAD ${String(g.head).slice(0, 7)}`) : "";
return fitLine(pre + fold + " " + styler.fg("accent", label) + state, inner, styler);
return fitLine(pre + fold + " " + styler.fg("accent", label) + seen + state, inner, styler);
}
if (row.kind === "gtrigger") {
const t = row.node;
Expand All @@ -1657,6 +1661,7 @@ function graphRowLine(row: any, cursor: boolean, inner: number, styler: any): st
if (missing) marks.push(styler.fg("error", "[missing at HEAD]"));
if (s.isSub) marks.push(styler.fg("dim", `[sub of ${s.group}]`));
if (s.aiTrigger) marks.push(styler.fg("success", "[chainable]"));
for (const loop of Array.isArray(s.loops) ? s.loops : []) marks.push(styler.fg("accent", `${G.rearm} "${loop.hint}"`));
const badges = graphBadges(row.flags, styler);
return fitLine(pre + styler.fg("dim", "skill ") + name + (marks.length ? " " + marks.join(" ") : "") + badges, inner, styler);
}
Expand Down
Loading
Loading