@crewhaus/hangar-ui 0.5.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -8
- package/assets/hangar.css +205 -0
- package/assets/js/api.js +2 -0
- package/assets/js/app.js +10 -1
- package/assets/js/dom.js +30 -0
- package/assets/js/router.js +16 -0
- package/assets/js/routes.js +80 -2
- package/assets/js/supervision.js +70 -1
- package/assets/js/util.js +1 -0
- package/assets/js/views/advisor.js +631 -0
- package/assets/js/views/library.js +139 -11
- package/assets/js/views/models.js +430 -0
- package/dist/embed-suite.d.ts +1 -0
- package/dist/embed-suite.js +289 -0
- package/dist/index.js +4 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -285,16 +285,60 @@ origin — never part of this one. The asset-hygiene suite pins the exception
|
|
|
285
285
|
to that single module, and `paneSandbox` strips `allow-same-origin` even if a
|
|
286
286
|
payload asks for it.
|
|
287
287
|
|
|
288
|
+
## M5 — the Advisor
|
|
289
|
+
|
|
290
|
+
`views/advisor.js` draws one more harness tab (`#/h/<id>/advisor`, right
|
|
291
|
+
after Overview — the strip order is the triage order) and one more fleet
|
|
292
|
+
screen (`#/advisor`):
|
|
293
|
+
|
|
294
|
+
- **The feed** — every alert, suggestion and optimization signal the manager
|
|
295
|
+
can derive about the harness, in one severity-ranked list. Every item
|
|
296
|
+
carries a hover TOOLTIP (`withTip`/`tipIcon` in `dom.js`: a `data-tip`
|
|
297
|
+
attribute the stylesheet renders via `attr()` — text only, so the
|
|
298
|
+
injection ban holds) explaining why it matters, the fact it was derived
|
|
299
|
+
from, guidance on what to do, and a quick action: an executable button for
|
|
300
|
+
a queued CLI verb (with its CLI twin beside it) or a deep link into the
|
|
301
|
+
tab that owns the fix. Acting opens an inline confirm where a COMMENT can
|
|
302
|
+
be attached — it is recorded in the harness-local decisions ledger next to
|
|
303
|
+
the queued job. Dismissing REQUIRES a reason (the server refuses without
|
|
304
|
+
one), dismissed items keep their recorded reasoning in a collapsible fold,
|
|
305
|
+
and every dismissal can be reopened. Zero open items renders as a
|
|
306
|
+
first-class "running optimally" state, never as an empty screen.
|
|
307
|
+
- **The trend** — "is it improving?": the eval pass-rate bars, the decision
|
|
308
|
+
counts, and a one-line verdict, all folded from durable sources.
|
|
309
|
+
- **Reports** — generate model-usage / costs / usefulness / optimization
|
|
310
|
+
reports (each button tooltipped with what it measures); saved reports are
|
|
311
|
+
listed and re-readable in place.
|
|
312
|
+
- **Issues** — describe a problem and it is tuned into an update ready to
|
|
313
|
+
run: the kind selector (tooltipped per kind) picks what gets queued, with
|
|
314
|
+
`optimize` — the eval→patch loop whose artifact is a reviewable spec
|
|
315
|
+
patch — as the default.
|
|
316
|
+
- **The fleet board** (`#/advisor`) — every harness's rollup, worst first,
|
|
317
|
+
each row deep-linking its Advisor tab.
|
|
318
|
+
|
|
319
|
+
The Library also becomes curatable (M5): harness rows carry a `hidden` flag
|
|
320
|
+
(the row editor's Visibility checkbox), hidden rows fold out of every view
|
|
321
|
+
except an explicit **Hidden** rail bucket, and a **Find harnesses…** panel
|
|
322
|
+
lists the unregistered harnesses under the scan roots (via
|
|
323
|
+
`GET /api/registry/discover`) with per-candidate Add buttons — discovery
|
|
324
|
+
without registration, so the Library only shows what you add. The blanket
|
|
325
|
+
Scan (which registers everything) stays as the labeled bulk fallback.
|
|
326
|
+
|
|
288
327
|
## Testing
|
|
289
328
|
|
|
290
|
-
`bun test
|
|
291
|
-
for the pure browser modules
|
|
292
|
-
`
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
bun
|
|
296
|
-
|
|
297
|
-
|
|
329
|
+
`bun run test` — two `bun test` invocations, DELIBERATELY two processes:
|
|
330
|
+
`bun test src` runs the unit tests for the pure browser modules
|
|
331
|
+
(`util.js`, `supervision.js`, `markdown.js`, `router.js`, `routes.js`,
|
|
332
|
+
`shapes.js`, the view decisions) and the `api.js` client under a stubbed
|
|
333
|
+
`fetch` (every wrapper asserted against the route map's method/path/body);
|
|
334
|
+
`bun test ./src/embed-suite.ts` then runs the embed-map
|
|
335
|
+
completeness/hygiene suite alone. The split is load-bearing, not taste:
|
|
336
|
+
the embed suite imports every asset as TEXT while the view tests import
|
|
337
|
+
the same files as ES modules, and bun's module registry keys a module by
|
|
338
|
+
path alone — in one process whichever load lands first poisons the other.
|
|
339
|
+
All DOM-free at import time and imported directly by bun. Deterministic:
|
|
340
|
+
clocks are injected, no network, no subprocesses. The server side of the
|
|
341
|
+
same contract runs in `@crewhaus/hangar-server`'s `contract.test.ts`.
|
|
298
342
|
|
|
299
343
|
The M2 screens stay testable without a browser by keeping the decisions —
|
|
300
344
|
state → row, envelope → disabled-with-reason, SSE frame → feed item,
|
package/assets/hangar.css
CHANGED
|
@@ -786,6 +786,36 @@ table.fleet {
|
|
|
786
786
|
font-size: 10.5px;
|
|
787
787
|
}
|
|
788
788
|
|
|
789
|
+
/* 0.6.0 — the Models tab's share meter and its chip row. A share is always
|
|
790
|
+
rendered NEXT TO its number, never instead of one: the bar is the shape of
|
|
791
|
+
the split, the figure is the claim. */
|
|
792
|
+
.meter {
|
|
793
|
+
min-width: 72px;
|
|
794
|
+
height: 8px;
|
|
795
|
+
border-radius: 999px;
|
|
796
|
+
background: var(--panel-2);
|
|
797
|
+
border: 1px solid var(--rule-2);
|
|
798
|
+
overflow: hidden;
|
|
799
|
+
}
|
|
800
|
+
.meter-fill {
|
|
801
|
+
height: 100%;
|
|
802
|
+
background: var(--accent);
|
|
803
|
+
opacity: 0.85;
|
|
804
|
+
}
|
|
805
|
+
.chips {
|
|
806
|
+
display: flex;
|
|
807
|
+
flex-wrap: wrap;
|
|
808
|
+
gap: 2px;
|
|
809
|
+
margin-bottom: 8px;
|
|
810
|
+
}
|
|
811
|
+
.chips .chip {
|
|
812
|
+
cursor: pointer;
|
|
813
|
+
font-family: var(--mono);
|
|
814
|
+
}
|
|
815
|
+
tr.row-best td {
|
|
816
|
+
background: var(--accent-ghost);
|
|
817
|
+
}
|
|
818
|
+
|
|
789
819
|
/* preflight + folds */
|
|
790
820
|
.fold {
|
|
791
821
|
border: 1px solid var(--rule);
|
|
@@ -1724,3 +1754,178 @@ tr.deflated td {
|
|
|
1724
1754
|
grid-column: 2 / 3;
|
|
1725
1755
|
}
|
|
1726
1756
|
}
|
|
1757
|
+
|
|
1758
|
+
/* ── tooltips (M5): data-tip rendered via attr() — text only, never markup */
|
|
1759
|
+
|
|
1760
|
+
.has-tip {
|
|
1761
|
+
position: relative;
|
|
1762
|
+
cursor: help;
|
|
1763
|
+
}
|
|
1764
|
+
.has-tip::after {
|
|
1765
|
+
content: attr(data-tip);
|
|
1766
|
+
position: absolute;
|
|
1767
|
+
left: 0;
|
|
1768
|
+
bottom: calc(100% + 7px);
|
|
1769
|
+
z-index: 60;
|
|
1770
|
+
width: max-content;
|
|
1771
|
+
max-width: min(34rem, 78vw);
|
|
1772
|
+
padding: 7px 10px;
|
|
1773
|
+
border: 1px solid var(--rule-2);
|
|
1774
|
+
border-radius: var(--radius-sm);
|
|
1775
|
+
background: var(--panel-3);
|
|
1776
|
+
color: var(--ink);
|
|
1777
|
+
font-size: 12.5px;
|
|
1778
|
+
font-weight: 400;
|
|
1779
|
+
font-family: var(--sans);
|
|
1780
|
+
line-height: 1.45;
|
|
1781
|
+
white-space: normal;
|
|
1782
|
+
text-align: left;
|
|
1783
|
+
opacity: 0;
|
|
1784
|
+
pointer-events: none;
|
|
1785
|
+
transition: opacity 120ms ease;
|
|
1786
|
+
box-shadow: 0 6px 22px rgba(0, 0, 0, 0.35);
|
|
1787
|
+
}
|
|
1788
|
+
.has-tip:hover::after,
|
|
1789
|
+
.has-tip:focus-visible::after {
|
|
1790
|
+
opacity: 1;
|
|
1791
|
+
}
|
|
1792
|
+
.tip-icon {
|
|
1793
|
+
display: inline-flex;
|
|
1794
|
+
align-items: center;
|
|
1795
|
+
justify-content: center;
|
|
1796
|
+
width: 15px;
|
|
1797
|
+
height: 15px;
|
|
1798
|
+
border: 1px solid var(--rule-2);
|
|
1799
|
+
border-radius: 50%;
|
|
1800
|
+
font-size: 10.5px;
|
|
1801
|
+
color: var(--ink-3);
|
|
1802
|
+
background: var(--panel-2);
|
|
1803
|
+
vertical-align: middle;
|
|
1804
|
+
margin-left: 5px;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
/* ── advisor (M5): the unified alert/suggestion feed ───────────────────── */
|
|
1808
|
+
|
|
1809
|
+
.adv-item {
|
|
1810
|
+
display: grid;
|
|
1811
|
+
grid-template-columns: 96px minmax(0, 1fr) auto;
|
|
1812
|
+
gap: 8px 12px;
|
|
1813
|
+
align-items: start;
|
|
1814
|
+
padding: 10px 12px;
|
|
1815
|
+
border: 1px solid var(--rule);
|
|
1816
|
+
border-left-width: 3px;
|
|
1817
|
+
border-radius: var(--radius-sm);
|
|
1818
|
+
margin: 8px 0;
|
|
1819
|
+
background: var(--panel-2);
|
|
1820
|
+
}
|
|
1821
|
+
.adv-critical {
|
|
1822
|
+
border-left-color: var(--bad);
|
|
1823
|
+
}
|
|
1824
|
+
.adv-warn {
|
|
1825
|
+
border-left-color: var(--warn);
|
|
1826
|
+
}
|
|
1827
|
+
.adv-suggestion {
|
|
1828
|
+
border-left-color: var(--info);
|
|
1829
|
+
}
|
|
1830
|
+
.adv-main {
|
|
1831
|
+
min-width: 0;
|
|
1832
|
+
}
|
|
1833
|
+
.adv-title {
|
|
1834
|
+
font-weight: 550;
|
|
1835
|
+
}
|
|
1836
|
+
.adv-detail {
|
|
1837
|
+
margin-top: 2px;
|
|
1838
|
+
font-size: 12.5px;
|
|
1839
|
+
}
|
|
1840
|
+
.adv-guidance {
|
|
1841
|
+
margin-top: 4px;
|
|
1842
|
+
font-size: 12.5px;
|
|
1843
|
+
color: var(--ink-2);
|
|
1844
|
+
}
|
|
1845
|
+
.adv-actions {
|
|
1846
|
+
display: flex;
|
|
1847
|
+
flex-direction: column;
|
|
1848
|
+
align-items: flex-end;
|
|
1849
|
+
gap: 6px;
|
|
1850
|
+
}
|
|
1851
|
+
.adv-cli {
|
|
1852
|
+
font-size: 11px;
|
|
1853
|
+
color: var(--ink-3);
|
|
1854
|
+
font-family: var(--mono);
|
|
1855
|
+
max-width: 24rem;
|
|
1856
|
+
overflow: hidden;
|
|
1857
|
+
text-overflow: ellipsis;
|
|
1858
|
+
white-space: nowrap;
|
|
1859
|
+
}
|
|
1860
|
+
.adv-form {
|
|
1861
|
+
grid-column: 1 / -1;
|
|
1862
|
+
display: flex;
|
|
1863
|
+
gap: 8px;
|
|
1864
|
+
align-items: center;
|
|
1865
|
+
flex-wrap: wrap;
|
|
1866
|
+
}
|
|
1867
|
+
.adv-form .input {
|
|
1868
|
+
flex: 1 1 16rem;
|
|
1869
|
+
}
|
|
1870
|
+
.adv-optimal {
|
|
1871
|
+
border: 1px solid var(--ok);
|
|
1872
|
+
border-radius: var(--radius);
|
|
1873
|
+
background: var(--accent-ghost);
|
|
1874
|
+
padding: 16px 18px;
|
|
1875
|
+
margin: 10px 0;
|
|
1876
|
+
}
|
|
1877
|
+
.adv-note {
|
|
1878
|
+
font-size: 12.5px;
|
|
1879
|
+
color: var(--ink-2);
|
|
1880
|
+
}
|
|
1881
|
+
.adv-decision {
|
|
1882
|
+
font-size: 12px;
|
|
1883
|
+
color: var(--ink-3);
|
|
1884
|
+
}
|
|
1885
|
+
.adv-board-row {
|
|
1886
|
+
display: grid;
|
|
1887
|
+
grid-template-columns: minmax(10rem, 1fr) auto auto minmax(0, 2fr);
|
|
1888
|
+
gap: 8px 14px;
|
|
1889
|
+
align-items: center;
|
|
1890
|
+
padding: 9px 12px;
|
|
1891
|
+
border-bottom: 1px solid var(--rule);
|
|
1892
|
+
}
|
|
1893
|
+
.trend-bars {
|
|
1894
|
+
display: flex;
|
|
1895
|
+
align-items: flex-end;
|
|
1896
|
+
gap: 3px;
|
|
1897
|
+
height: 44px;
|
|
1898
|
+
margin: 6px 0;
|
|
1899
|
+
}
|
|
1900
|
+
.trend-bar {
|
|
1901
|
+
width: 14px;
|
|
1902
|
+
min-height: 2px;
|
|
1903
|
+
background: var(--accent-ghost);
|
|
1904
|
+
border-top: 2px solid var(--accent);
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
/* ── find harnesses (M5): discovery without registration ───────────────── */
|
|
1908
|
+
|
|
1909
|
+
.find-row {
|
|
1910
|
+
display: flex;
|
|
1911
|
+
gap: 10px;
|
|
1912
|
+
align-items: center;
|
|
1913
|
+
padding: 7px 10px;
|
|
1914
|
+
border-bottom: 1px solid var(--rule);
|
|
1915
|
+
}
|
|
1916
|
+
.find-row .mono {
|
|
1917
|
+
flex: 1 1 auto;
|
|
1918
|
+
min-width: 0;
|
|
1919
|
+
overflow: hidden;
|
|
1920
|
+
text-overflow: ellipsis;
|
|
1921
|
+
white-space: nowrap;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
@media (max-width: 720px) {
|
|
1925
|
+
.adv-item {
|
|
1926
|
+
grid-template-columns: minmax(0, 1fr);
|
|
1927
|
+
}
|
|
1928
|
+
.adv-actions {
|
|
1929
|
+
align-items: flex-start;
|
|
1930
|
+
}
|
|
1931
|
+
}
|
package/assets/js/api.js
CHANGED
|
@@ -262,7 +262,9 @@ export const api = {
|
|
|
262
262
|
groupProc: (name, verb, body = {}) => call(ROUTES.groupProc, { name, verb }, body),
|
|
263
263
|
setTags: (id, tags) => call(ROUTES.setTags, { id }, { tags }),
|
|
264
264
|
setPin: (id, pinned) => call(ROUTES.setPin, { id }, { pinned }),
|
|
265
|
+
setHidden: (id, hidden) => call(ROUTES.setHidden, { id }, { hidden }),
|
|
265
266
|
setNotes: (id, notes) => call(ROUTES.setNotes, { id }, { notes }),
|
|
267
|
+
discover: () => call(ROUTES.discover, {}),
|
|
266
268
|
relocateHarness: (id, newDir) => call(ROUTES.relocate, { id }, { newDir }),
|
|
267
269
|
removeHarness: (id) => call(ROUTES.removeHarness, { id }),
|
|
268
270
|
|
package/assets/js/app.js
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
import { shapeAccent, shapeLabel } from "./shapes.js";
|
|
38
38
|
import { procRow } from "./supervision.js";
|
|
39
39
|
import { renderActivity } from "./views/activity.js";
|
|
40
|
+
import { renderAdvisor, renderAdvisorBoard } from "./views/advisor.js";
|
|
40
41
|
import { renderApprovals } from "./views/approvals.js";
|
|
41
42
|
import { renderChannels } from "./views/channels.js";
|
|
42
43
|
import { renderCosts } from "./views/costs.js";
|
|
@@ -51,6 +52,7 @@ import { renderInspect } from "./views/inspect.js";
|
|
|
51
52
|
import { renderLibrary } from "./views/library.js";
|
|
52
53
|
import { renderMemoryFabric } from "./views/memory-fabric.js";
|
|
53
54
|
import { renderMemory } from "./views/memory.js";
|
|
55
|
+
import { renderModels } from "./views/models.js";
|
|
54
56
|
import { renderOverview } from "./views/overview.js";
|
|
55
57
|
import { renderPanes } from "./views/panes.js";
|
|
56
58
|
import { renderReview } from "./views/review.js";
|
|
@@ -68,6 +70,7 @@ import { renderTokenScreen } from "./views/token.js";
|
|
|
68
70
|
const THEME_KEY = "hangar.theme";
|
|
69
71
|
const TAB_LABELS = {
|
|
70
72
|
overview: "Overview",
|
|
73
|
+
advisor: "Advisor",
|
|
71
74
|
spec: "Spec",
|
|
72
75
|
runs: "Runs",
|
|
73
76
|
schedulers: "Schedulers",
|
|
@@ -77,6 +80,7 @@ const TAB_LABELS = {
|
|
|
77
80
|
data: "Datasets",
|
|
78
81
|
feedback: "Feedback",
|
|
79
82
|
costs: "Costs",
|
|
83
|
+
models: "Models",
|
|
80
84
|
creds: "Credentials",
|
|
81
85
|
channels: "Channels",
|
|
82
86
|
security: "Security",
|
|
@@ -94,6 +98,7 @@ const NAV = [
|
|
|
94
98
|
{ view: "review", label: "Review" },
|
|
95
99
|
{ view: "activity", label: "Activity" },
|
|
96
100
|
{ view: "health", label: "Health" },
|
|
101
|
+
{ view: "advisor", label: "Advisor" },
|
|
97
102
|
{ view: "credentials", label: "Credentials" },
|
|
98
103
|
{ view: "feedback", label: "Feedback" },
|
|
99
104
|
{ view: "thredz", label: "Thredz" },
|
|
@@ -352,6 +357,8 @@ async function dispatch(route) {
|
|
|
352
357
|
await renderThredzGlobal(viewRoot);
|
|
353
358
|
} else if (route.view === "health") {
|
|
354
359
|
await renderHealthBoard(viewRoot);
|
|
360
|
+
} else if (route.view === "advisor") {
|
|
361
|
+
await renderAdvisorBoard(viewRoot);
|
|
355
362
|
} else if (route.view === "settings") {
|
|
356
363
|
await renderSettings(viewRoot);
|
|
357
364
|
} else {
|
|
@@ -423,7 +430,8 @@ async function renderHarnessPage(root, route) {
|
|
|
423
430
|
if (route.tab === "spec") {
|
|
424
431
|
await renderSpec(tabRoot, ctx);
|
|
425
432
|
await renderSpecEdit(section(tabRoot), ctx);
|
|
426
|
-
} else if (route.tab === "
|
|
433
|
+
} else if (route.tab === "advisor") await renderAdvisor(tabRoot, ctx);
|
|
434
|
+
else if (route.tab === "runs") await renderRuns(tabRoot, ctx);
|
|
427
435
|
else if (route.tab === "schedulers") await renderSchedulers(tabRoot, ctx);
|
|
428
436
|
else if (route.tab === "sessions") await renderSessions(tabRoot, ctx);
|
|
429
437
|
else if (route.tab === "evals") {
|
|
@@ -435,6 +443,7 @@ async function renderHarnessPage(root, route) {
|
|
|
435
443
|
} else if (route.tab === "data") await renderData(tabRoot, ctx);
|
|
436
444
|
else if (route.tab === "feedback") await renderFeedback(tabRoot, ctx);
|
|
437
445
|
else if (route.tab === "costs") await renderCosts(tabRoot, ctx);
|
|
446
|
+
else if (route.tab === "models") await renderModels(tabRoot, ctx);
|
|
438
447
|
else if (route.tab === "creds") await renderCreds(tabRoot, ctx);
|
|
439
448
|
else if (route.tab === "channels") await renderChannels(tabRoot, ctx);
|
|
440
449
|
else if (route.tab === "security") await renderSecurity(tabRoot, ctx);
|
package/assets/js/dom.js
CHANGED
|
@@ -161,6 +161,36 @@ export function copyBtn(value, label = "copy") {
|
|
|
161
161
|
return btn;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Attach a rich hover tooltip. The text rides a `data-tip` attribute the
|
|
166
|
+
* stylesheet renders via `attr()` — always text, never markup, so the
|
|
167
|
+
* injection ban holds — with the native `title` kept as the fallback for
|
|
168
|
+
* assistive tech and anything that never hovers. Returns the node.
|
|
169
|
+
*/
|
|
170
|
+
export function withTip(node, tipText) {
|
|
171
|
+
const value = String(tipText ?? "");
|
|
172
|
+
if (value === "") return node;
|
|
173
|
+
node.classList.add("has-tip");
|
|
174
|
+
node.dataset.tip = value;
|
|
175
|
+
if (!node.getAttribute("title")) node.setAttribute("title", value);
|
|
176
|
+
return node;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** A small "?" affordance carrying only its tooltip — guidance on rows where
|
|
180
|
+
* a whole sentence inline would drown the content. Keyboard-focusable, so
|
|
181
|
+
* the tip is reachable without a pointer. */
|
|
182
|
+
export function tipIcon(tipText) {
|
|
183
|
+
return withTip(
|
|
184
|
+
el("span", {
|
|
185
|
+
class: "tip-icon",
|
|
186
|
+
tabindex: "0",
|
|
187
|
+
"aria-label": String(tipText ?? ""),
|
|
188
|
+
text: "?",
|
|
189
|
+
}),
|
|
190
|
+
tipText,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
164
194
|
/** A native collapsible (<details>) card. */
|
|
165
195
|
export function collapsible(summaryChildren, bodyChildren, open = false) {
|
|
166
196
|
return el("details", { class: "fold", open: open || null }, [
|
package/assets/js/router.js
CHANGED
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
|
|
41
41
|
export const HARNESS_TABS = [
|
|
42
42
|
"overview",
|
|
43
|
+
// M5: the unified alert/suggestion feed — right after Overview because it
|
|
44
|
+
// is the "what needs doing" screen; the strip order is the triage order.
|
|
45
|
+
"advisor",
|
|
43
46
|
"spec",
|
|
44
47
|
"runs",
|
|
45
48
|
"schedulers",
|
|
@@ -49,6 +52,9 @@ export const HARNESS_TABS = [
|
|
|
49
52
|
"data",
|
|
50
53
|
"feedback",
|
|
51
54
|
"costs",
|
|
55
|
+
// 0.6.0 (design §8.3): the models tab sits after Costs because it answers
|
|
56
|
+
// the question Costs raises — WHICH model, under which settings, spent it.
|
|
57
|
+
"models",
|
|
52
58
|
"creds",
|
|
53
59
|
"channels",
|
|
54
60
|
"security",
|
|
@@ -70,6 +76,10 @@ export const HARNESS_TABS = [
|
|
|
70
76
|
* state that tab renders is a real answer and a shared URL must not 404. */
|
|
71
77
|
export const M3_TABS = [
|
|
72
78
|
"data",
|
|
79
|
+
// 0.6.0: `models` takes trailing segments generically too — a route
|
|
80
|
+
// timeline is `#/h/<id>/models/routes/<sess>`, and naming that sub-screen
|
|
81
|
+
// is the view's business, not the router's.
|
|
82
|
+
"models",
|
|
73
83
|
"feedback",
|
|
74
84
|
"creds",
|
|
75
85
|
"channels",
|
|
@@ -77,6 +87,9 @@ export const M3_TABS = [
|
|
|
77
87
|
"thredz",
|
|
78
88
|
"inspect",
|
|
79
89
|
"dev",
|
|
90
|
+
// M5: the advisor's sub-screens (a saved report, say) are the view's to
|
|
91
|
+
// name — the same generic `rest` capture the M3 tabs use.
|
|
92
|
+
"advisor",
|
|
80
93
|
];
|
|
81
94
|
|
|
82
95
|
/** The global (fleet-wide) screens, in nav order. `#/` is the Library. */
|
|
@@ -92,6 +105,9 @@ export const GLOBAL_VIEWS = [
|
|
|
92
105
|
// notification rules, read-only mode, the plugin inventory).
|
|
93
106
|
"health",
|
|
94
107
|
"settings",
|
|
108
|
+
// M5: the fleet advisor board — every harness's open alerts/suggestions,
|
|
109
|
+
// worst first.
|
|
110
|
+
"advisor",
|
|
95
111
|
];
|
|
96
112
|
|
|
97
113
|
/**
|
package/assets/js/routes.js
CHANGED
|
@@ -56,7 +56,15 @@ export const ROUTES = {
|
|
|
56
56
|
body: "GroupProc", // {force?, acknowledge?, parallel?}
|
|
57
57
|
},
|
|
58
58
|
addScanRoot: { method: "POST", path: "/api/registry/scan-roots", body: "ScanRootCreate" }, // {dir}
|
|
59
|
+
// Discovery WITHOUT registration: the harnesses under the scan roots that
|
|
60
|
+
// are NOT yet registered, each one an Add away — the "find harnesses on
|
|
61
|
+
// this machine" flow. Adding stays the default gesture; a blanket scan
|
|
62
|
+
// (which registers everything) is the bulk fallback, not the front door.
|
|
63
|
+
discover: { method: "GET", path: "/api/registry/discover" },
|
|
59
64
|
removeHarness: { method: "DELETE", path: "/api/h/:id" },
|
|
65
|
+
// Curation, not removal: hidden entries stay registered (state, groups,
|
|
66
|
+
// history intact) but the Library's default view omits them.
|
|
67
|
+
setHidden: { method: "PUT", path: "/api/h/:id/visibility", body: "SetVisibility" }, // {hidden}
|
|
60
68
|
relocate: { method: "POST", path: "/api/h/:id/relocate", body: "Relocate" }, // {newDir}
|
|
61
69
|
setGroups: { method: "PUT", path: "/api/h/:id/groups", body: "SetGroups" }, // {groups}
|
|
62
70
|
// One member's BOOT position inside a group; `order: null` clears it.
|
|
@@ -741,6 +749,73 @@ export const ROUTES = {
|
|
|
741
749
|
group: "inspect",
|
|
742
750
|
},
|
|
743
751
|
|
|
752
|
+
// ---- group "advisor": the unified alert/suggestion feed + its loops ----
|
|
753
|
+
// M5. Every alert, suggestion and optimization signal the manager can read
|
|
754
|
+
// about a harness, in ONE feed: each item carries its severity, the fact it
|
|
755
|
+
// came from, a tooltip-grade explanation, guidance, the screen that owns
|
|
756
|
+
// the fix, and a quick action (a queued CLI verb from a closed vocabulary,
|
|
757
|
+
// or a deep link — a suggestion is never an application). Acting takes an
|
|
758
|
+
// optional operator comment; dismissing REQUIRES a reason; both append to
|
|
759
|
+
// a harness-local ledger and reopen supersedes rather than deletes.
|
|
760
|
+
advisor: { method: "GET", path: "/api/h/:id/advisor", group: "advisor" },
|
|
761
|
+
advisorAct: {
|
|
762
|
+
method: "POST",
|
|
763
|
+
path: "/api/h/:id/advisor/:itemId/act",
|
|
764
|
+
body: "AdvisorAct", // {comment?}
|
|
765
|
+
group: "advisor",
|
|
766
|
+
},
|
|
767
|
+
advisorDismiss: {
|
|
768
|
+
method: "POST",
|
|
769
|
+
path: "/api/h/:id/advisor/:itemId/dismiss",
|
|
770
|
+
body: "AdvisorDismiss", // {reason}
|
|
771
|
+
group: "advisor",
|
|
772
|
+
},
|
|
773
|
+
advisorReopen: {
|
|
774
|
+
method: "POST",
|
|
775
|
+
path: "/api/h/:id/advisor/:itemId/reopen",
|
|
776
|
+
body: "Empty", // {}
|
|
777
|
+
group: "advisor",
|
|
778
|
+
},
|
|
779
|
+
// Improvement over time, folded from durable sources only (the eval index,
|
|
780
|
+
// the cost ledger, the decisions ledger) — a read never stores a snapshot.
|
|
781
|
+
advisorTrend: { method: "GET", path: "/api/h/:id/advisor/trend", group: "advisor" },
|
|
782
|
+
advisorReports: { method: "GET", path: "/api/h/:id/advisor/reports", group: "advisor" },
|
|
783
|
+
advisorReportRun: {
|
|
784
|
+
method: "POST",
|
|
785
|
+
path: "/api/h/:id/advisor/reports",
|
|
786
|
+
body: "AdvisorReportRun", // {kind: model-usage|costs|usefulness|optimization|routing|hybrid}
|
|
787
|
+
group: "advisor",
|
|
788
|
+
},
|
|
789
|
+
advisorReport: {
|
|
790
|
+
method: "GET",
|
|
791
|
+
path: "/api/h/:id/advisor/reports/:reportId",
|
|
792
|
+
group: "advisor",
|
|
793
|
+
},
|
|
794
|
+
// The issue inbox: a complaint submitted here is tuned into an update
|
|
795
|
+
// ready to run — `optimize` queues the eval→patch loop whose artifact is a
|
|
796
|
+
// reviewable spec patch; `note` records without queueing anything.
|
|
797
|
+
advisorIssues: { method: "GET", path: "/api/h/:id/advisor/issues", group: "advisor" },
|
|
798
|
+
advisorIssueSubmit: {
|
|
799
|
+
method: "POST",
|
|
800
|
+
path: "/api/h/:id/advisor/issues",
|
|
801
|
+
body: "AdvisorIssue", // {title, detail?, kind?}
|
|
802
|
+
group: "advisor",
|
|
803
|
+
},
|
|
804
|
+
advisorFleet: { method: "GET", path: "/api/advisor", group: "advisor" },
|
|
805
|
+
|
|
806
|
+
// ---- group "models": per-model settings, hybrid routing, the scoreboard -
|
|
807
|
+
// 0.6.0 (design §8.3). READ-ONLY, all four: the console renders the
|
|
808
|
+
// `models:` registry, the declared pool, per-role/per-profile spend, the
|
|
809
|
+
// learned arms and their leaderboard, and one run's route timeline — and
|
|
810
|
+
// it writes none of them. Hangar never touches an arm or a prior; the
|
|
811
|
+
// roster, the rules and the floor stay human-owned behind `propose`.
|
|
812
|
+
models: { method: "GET", path: "/api/h/:id/models", group: "models" },
|
|
813
|
+
// The route timeline for ONE session, read from the session JSONL (§8.1:
|
|
814
|
+
// Hangar reads the durable log, never the bus).
|
|
815
|
+
modelRoutes: { method: "GET", path: "/api/h/:id/models/routes/:sess", group: "models" },
|
|
816
|
+
modelArms: { method: "GET", path: "/api/h/:id/models/arms", group: "models" },
|
|
817
|
+
modelLeaderboard: { method: "GET", path: "/api/h/:id/models/leaderboard", group: "models" },
|
|
818
|
+
|
|
744
819
|
// ---- group "runtime": the mcp-server + dev run classes -----------------
|
|
745
820
|
// Both are PROCESSES: they get runfiles, ledger rows and ledger-claimed
|
|
746
821
|
// ports, and their live output rides the ONE existing SSE feed
|
|
@@ -763,8 +838,9 @@ export const ROUTES = {
|
|
|
763
838
|
devStop: { method: "POST", path: "/api/h/:id/dev/stop", body: "Empty", group: "runtime" },
|
|
764
839
|
};
|
|
765
840
|
|
|
766
|
-
/** The
|
|
767
|
-
* owns it on the server and the view
|
|
841
|
+
/** The grouped areas, in tab order (the M3 areas plus M5's advisor). A
|
|
842
|
+
* route's `group` names the module that owns it on the server and the view
|
|
843
|
+
* that renders it in the console. */
|
|
768
844
|
export const M3_GROUPS = [
|
|
769
845
|
"spec",
|
|
770
846
|
"memory",
|
|
@@ -777,6 +853,8 @@ export const M3_GROUPS = [
|
|
|
777
853
|
"thredz",
|
|
778
854
|
"inspect",
|
|
779
855
|
"runtime",
|
|
856
|
+
"advisor",
|
|
857
|
+
"models",
|
|
780
858
|
];
|
|
781
859
|
|
|
782
860
|
/** Every route key belonging to `group`, in map order. The views render
|
package/assets/js/supervision.js
CHANGED
|
@@ -700,6 +700,15 @@ function shortJson(value, max = 120) {
|
|
|
700
700
|
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
701
701
|
}
|
|
702
702
|
|
|
703
|
+
/** `model` first, then the optional facets, then the router's reason — joined with dots. */
|
|
704
|
+
function routeDetail(e, parts) {
|
|
705
|
+
const head = parts
|
|
706
|
+
.filter((part) => part !== null && part !== undefined && part !== "")
|
|
707
|
+
.map(String);
|
|
708
|
+
const reason = typeof e.reason === "string" && e.reason !== "" ? e.reason : null;
|
|
709
|
+
return [...head, ...(reason ? [reason] : [])].join(" · ");
|
|
710
|
+
}
|
|
711
|
+
|
|
703
712
|
/**
|
|
704
713
|
* One TraceEvent → its feed card. Known kinds get a written line; unknown
|
|
705
714
|
* kinds still render (tolerant-reader contract) with a clipped payload
|
|
@@ -763,8 +772,68 @@ export function traceCard(event) {
|
|
|
763
772
|
remediation: typeof e.remediation === "string" ? e.remediation : null,
|
|
764
773
|
};
|
|
765
774
|
case "model_route":
|
|
775
|
+
// 0.6.0 (design §8.3) — the event carries `model` (wire id) and
|
|
776
|
+
// `routeKey`, never `modelId`/`band`; the 0.5.x card read the latter
|
|
777
|
+
// and rendered an empty detail. Profile, policy and reason follow when
|
|
778
|
+
// present so a hybrid decision reads as a sentence.
|
|
779
|
+
return {
|
|
780
|
+
...base,
|
|
781
|
+
title: `model route${e.stage ? ` · ${String(e.stage)}` : ""}`,
|
|
782
|
+
detail: routeDetail(e, [
|
|
783
|
+
e.model,
|
|
784
|
+
e.profile ? `profile=${String(e.profile)}` : null,
|
|
785
|
+
e.routeKey ? `band=${String(e.routeKey)}` : null,
|
|
786
|
+
e.policy
|
|
787
|
+
? `policy=${String(e.policy)}${e.explored === true ? " (exploring)" : ""}`
|
|
788
|
+
: null,
|
|
789
|
+
e.ruleId ? `rule=${String(e.ruleId)}` : null,
|
|
790
|
+
]),
|
|
791
|
+
};
|
|
766
792
|
case "model_tier_route":
|
|
767
|
-
return {
|
|
793
|
+
return {
|
|
794
|
+
...base,
|
|
795
|
+
title: "model tier",
|
|
796
|
+
detail: routeDetail(e, [
|
|
797
|
+
e.model,
|
|
798
|
+
e.tier ? `tier=${String(e.tier)}${e.escalated === true ? " (escalated)" : ""}` : null,
|
|
799
|
+
]),
|
|
800
|
+
};
|
|
801
|
+
case "model_failover":
|
|
802
|
+
return {
|
|
803
|
+
...base,
|
|
804
|
+
title: "model failover",
|
|
805
|
+
detail: `${String(e.from ?? "?")} → ${String(e.to ?? "?")}${
|
|
806
|
+
e.reason ? ` · ${String(e.reason)}` : ""
|
|
807
|
+
}`,
|
|
808
|
+
};
|
|
809
|
+
case "model_stage":
|
|
810
|
+
return {
|
|
811
|
+
...base,
|
|
812
|
+
dot: e.outcome === "failed" ? "warn" : base.dot,
|
|
813
|
+
title: `stage ${String(e.stage ?? "?")} ${String(e.outcome ?? "")}`.trim(),
|
|
814
|
+
detail: [
|
|
815
|
+
e.model ? String(e.model) : null,
|
|
816
|
+
e.profile ? `profile=${String(e.profile)}` : null,
|
|
817
|
+
e.role ? `role=${String(e.role)}` : null,
|
|
818
|
+
e.strategy ? `strategy=${String(e.strategy)}` : null,
|
|
819
|
+
e.cause ? `cause=${String(e.cause)}` : null,
|
|
820
|
+
]
|
|
821
|
+
.filter((part) => part !== null)
|
|
822
|
+
.join(" · "),
|
|
823
|
+
};
|
|
824
|
+
case "model_directive":
|
|
825
|
+
return {
|
|
826
|
+
...base,
|
|
827
|
+
dot: e.accepted === false ? "warn" : base.dot,
|
|
828
|
+
title: `/model ${String(e.requested ?? "?")} ${e.accepted === false ? "refused" : "accepted"}`,
|
|
829
|
+
detail: [
|
|
830
|
+
e.resolved ? `→ ${String(e.resolved)}` : null,
|
|
831
|
+
e.source ? `source=${String(e.source)}` : null,
|
|
832
|
+
e.reason ? String(e.reason) : null,
|
|
833
|
+
]
|
|
834
|
+
.filter((part) => part !== null)
|
|
835
|
+
.join(" · "),
|
|
836
|
+
};
|
|
768
837
|
case "janitor_action":
|
|
769
838
|
return { ...base, title: "janitor", detail: String(e.action ?? "") };
|
|
770
839
|
default:
|
package/assets/js/util.js
CHANGED
|
@@ -180,6 +180,7 @@ export function normalizeRows(feed) {
|
|
|
180
180
|
groups: Array.isArray(src.groups) ? src.groups.map(String) : [],
|
|
181
181
|
tags: Array.isArray(src.tags) ? src.tags.map(String) : [],
|
|
182
182
|
pinned: src.pinned === true,
|
|
183
|
+
hidden: src.hidden === true,
|
|
183
184
|
notes: typeof src.notes === "string" ? src.notes : "",
|
|
184
185
|
missingSince: typeof src.missingSince === "string" ? src.missingSince : null,
|
|
185
186
|
lastSeen: typeof src.lastSeen === "string" ? src.lastSeen : null,
|