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
10 changes: 10 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# At the repository root because that is the only place git reads it from.
#
# The migration to a mono repo moved the Android runner into a subdirectory and
# its .gitmodules moved with it, where git never looks — so the gitlink at
# runner-android/third_party/llama.cpp survived with no URL registered against
# it, and `git submodule update --init` in a fresh clone silently found nothing
# to do. The path below is the only line that had to change.
[submodule "runner-android/third_party/llama.cpp"]
path = runner-android/third_party/llama.cpp
url = https://github.com/ggml-org/llama.cpp
6 changes: 6 additions & 0 deletions collector/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,12 @@ so it never touches a real fleet's history.
| `thermal` | device | The same benchmark run back to back for a quarter hour, so the answer is a curve: does the cold number survive the device getting warm |
| `cold-start` | host | Launch the installed build from cold, warm and hot; p50 and p95 per state, per device |
| `drain` / `soak` | host | Battery curve under a replayed GPX track; and whether a runner is still alive hours later |
| `build` | machine | Check out a ref, build it, publish the product as an artifact the next `install` can name |
| `speech-eval` / `embed-eval` | device | Word error rate and real-time factor for on-device transcription; recall-at-k and throughput for on-device embeddings |
| `vantage` | device, machine | DNS, connect, TLS and TTFB to your own sites from wherever each agent is, so "the site is slow" and "my wifi is slow" stop being the same answer |
| `locale-shots` | host | A screenshot flow under every locale, including RTL, bundled as a store-ready contact sheet |
| `app-soak` / `a11y-audit` | host | Memory, jank and crashes over hours; and the accessibility tree diffed against a baseline at the largest dynamic type |
| `self-check` | host, machine | The fleet inspects its own hosts: disk, tool versions, clock drift, agents actually loaded |
| `archive` / `digest` | host | Pull store reviews and Search Console data; then have the shelf summarize its own reviews using its own models |

Full reference, including every parameter and the failure modes worth knowing:
Expand Down
6 changes: 5 additions & 1 deletion collector/dash/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,11 @@ export type Job = {
job_id: string;
workload: string;
executor: "device" | "host";
status: "queued" | "claimed" | "done" | "failed" | "cancelled";
status: "waiting" | "queued" | "claimed" | "done" | "failed" | "cancelled";
/** Job ids this one waits for; it stays `waiting` until they are all done. */
depends_on?: string[] | null;
/** Whether a higher-priority job may ask this one to checkpoint and step aside. */
preemptible?: boolean;
claimed_by: string | null;
attempts: number;
max_attempts: number;
Expand Down
23 changes: 17 additions & 6 deletions collector/dash/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useKeyboard, SHORTCUTS } from "./keys.js";
import { TokenBanner } from "./TokenBanner.js";
import { useLiveState } from "./live.js";
import { match, useRoute } from "./router.js";
import { Glyph } from "./icons.js";
import { Glyph, Icon, NAV_ICON } from "./icons.js";
import { Link, Panel } from "./ui.js";
import { Compose } from "./pages/Compose.js";
import { DeviceDetail } from "./pages/DeviceDetail.js";
Expand All @@ -14,6 +14,7 @@ import { Overview } from "./pages/Overview.js";
import { Schedules } from "./pages/Schedules.js";
import { AlertBanner, Alerts } from "./pages/Alerts.js";
import { Artifacts } from "./pages/Artifacts.js";
import { Evals, EvalSetPage } from "./pages/Evals.js";
import { Events } from "./pages/Events.js";
import { Results } from "./pages/Results.js";
import { Visual } from "./pages/Visual.js";
Expand All @@ -25,6 +26,7 @@ const NAV = [
["/devices", "Devices"],
["/jobs", "Jobs"],
["/results", "Results"],
["/evals", "Evals"],
["/visual", "Visual"],
["/schedules", "Schedules"],
["/artifacts", "Artifacts"],
Expand Down Expand Up @@ -82,6 +84,7 @@ function Router() {
// Before /jobs/:id, or "new" would be read as a job id.
if (match("/jobs/new", route)) return <Compose />;
if (match("/results", route)) return <Results />;
if (match("/evals", route)) return <Evals />;
if (match("/visual", route)) return <Visual />;
if (match("/artifacts", route)) return <Artifacts />;
if (match("/events", route)) return <Events />;
Expand All @@ -93,6 +96,10 @@ function Router() {
if (device) return <DeviceDetail key={device.id} id={device.id} />;
const job = match("/jobs/:id", route);
if (job) return <JobDetail key={job.id} id={job.id} />;
// After the literal /evals above, for the same reason /devices/new is before
// /devices/:id — a literal segment must never be read as an id.
const evalSet = match("/evals/:sha", route);
if (evalSet) return <EvalSetPage key={evalSet.sha} sha={evalSet.sha} />;


return <NotFound route={route} />;
Expand Down Expand Up @@ -134,11 +141,15 @@ export function App() {
</span>
</span>
<nav class="nav">
{NAV.map(([to, label]) => (
<Link key={to} to={to}>
{label}
</Link>
))}
{NAV.map(([to, label]) => {
const icon = NAV_ICON[to];
return (
<Link key={to} to={to}>
{icon && <Icon name={icon} />}
{label}
</Link>
);
})}
</nav>
<LiveDot />
</header>
Expand Down
127 changes: 127 additions & 0 deletions collector/dash/src/art.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Empty-state drawings.
//
// A panel with nothing in it is a state worth drawing, not a gap to apologise
// for — and the four that matter are different states, not one "no data":
// the fleet being idle is good news, no results yet is a beginning, and no
// device matching a filter is a filter problem. Each gets its own picture and
// keeps the sentence the dashboard already printed underneath it.
//
// Same drawing language as icons.tsx: stroke on a flat ground, theme colours
// through CSS classes (never a hard-coded fill except the brand amber, which
// is a brand colour rather than a theme one), and no illustration bigger than
// the paragraph it introduces.
import type { ComponentChildren } from "preact";

function Art({ children, caption }: { children: ComponentChildren; caption: ComponentChildren }) {
return (
<div class="empty-art">
{children}
<p>{caption}</p>
</div>
);
}

/**
* Nothing claimed — the shelf is powered and waiting.
*
* The middle phone's pulse draws every few seconds. Idle is not the same as
* dead, and a completely still drawing would say the wrong one.
*/
export function ArtIdle({ caption }: { caption: ComponentChildren }) {
return (
<Art caption={caption}>
<svg viewBox="0 0 260 150" role="img" aria-label="Three phones on a shelf, waiting for work">
<path class="art-shelf" d="M24 126h212" />
<rect class="art-body" x="52" y="52" width="40" height="72" rx="6" />
<rect class="art-screen" x="58" y="59" width="28" height="52" rx="2" />
<path class="art-flat" d="M60 85h24" />
<rect class="art-body" x="110" y="44" width="40" height="80" rx="6" />
<rect class="art-screen" x="116" y="51" width="28" height="60" rx="2" />
<path class="art-flat" d="M118 81h26" />
<path class="art-pulse blip" d="M118 81h6l3-8 5 16 3-8h9" />
<rect class="art-body" x="168" y="58" width="40" height="66" rx="6" />
<rect class="art-screen" x="174" y="65" width="28" height="46" rx="2" />
<path class="art-flat" d="M176 88h24" />
</svg>
</Art>
);
}

/**
* No device here — either the shelf is empty or the filters exclude everything.
* The enrolment QR pointing at an outline is the actual fix in both cases.
*/
export function ArtNoDevices({ caption }: { caption: ComponentChildren }) {
return (
<Art caption={caption}>
<svg viewBox="0 0 260 150" role="img" aria-label="An enrolment code beside an empty device outline">
<path class="art-shelf" d="M24 126h212" />
<rect class="art-body ghost" x="150" y="46" width="42" height="78" rx="6" />
<path class="art-flat" d="M165 118h12" stroke-dasharray="4 3" />
<g>
<rect x="62" y="62" width="52" height="52" rx="4" fill="#f7f8fa" />
<g fill="#1c2025">
<rect x="68" y="68" width="12" height="12" />
<rect x="96" y="68" width="12" height="12" />
<rect x="68" y="96" width="12" height="12" />
<rect x="84" y="70" width="3" height="3" />
<rect x="89" y="74" width="3" height="3" />
<rect x="84" y="80" width="3" height="3" />
<rect x="86" y="86" width="3" height="3" />
<rect x="92" y="88" width="3" height="3" />
<rect x="98" y="86" width="3" height="3" />
<rect x="103" y="92" width="3" height="3" />
<rect x="96" y="98" width="3" height="3" />
<rect x="100" y="104" width="3" height="3" />
<rect x="90" y="100" width="3" height="3" />
<rect x="86" y="106" width="3" height="3" />
<rect x="72" y="86" width="3" height="3" />
<rect x="78" y="90" width="3" height="3" />
</g>
<g fill="#f7f8fa">
<rect x="71" y="71" width="6" height="6" />
<rect x="99" y="71" width="6" height="6" />
<rect x="71" y="99" width="6" height="6" />
</g>
</g>
<path class="art-flat" d="M122 88h20" />
<path class="art-flat" d="M138 83l5 5-5 5" />
</svg>
</Art>
);
}

/**
* Nothing failed. A flat trace running into a tick — the same pulse the mark
* uses, ending well.
*/
export function ArtAllClear({ caption }: { caption: ComponentChildren }) {
return (
<Art caption={caption}>
<svg viewBox="0 0 260 150" role="img" aria-label="A steady trace ending in a tick">
<path class="art-pulse" d="M28 86h58l10-26 16 52 10-26h34" />
<circle cx="186" cy="86" r="22" class="art-ok" />
<path class="art-ok check" d="M176 86.5l7 7 13-14" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</Art>
);
}

/**
* No results yet. Dotted bars in the accent: the chart that will be here, not
* a chart of nothing — a solid empty chart would read as a measurement of zero.
*/
export function ArtNoResults({ caption }: { caption: ComponentChildren }) {
return (
<Art caption={caption}>
<svg viewBox="0 0 260 150" role="img" aria-label="Outlined bars waiting to be filled by results">
<path class="art-flat" d="M40 122h180" />
<path class="art-flat" d="M40 92h180M40 62h180" stroke-dasharray="2 4" stroke-width="1" />
<rect class="art-bar" x="64" y="70" width="26" height="52" rx="3" />
<rect class="art-bar" x="104" y="52" width="26" height="70" rx="3" />
<rect class="art-bar" x="144" y="84" width="26" height="38" rx="3" />
<rect class="art-bar" x="184" y="66" width="26" height="56" rx="3" />
</svg>
</Art>
);
}
Loading
Loading