Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
16b0871
feat(ui): console shell with design tokens, navigation and routing
AndyTechnologies Sep 21, 2026
5ea72b1
docs(odd): mark ui-console U01 complete with verification evidence
AndyTechnologies Sep 21, 2026
120c66d
feat(ui): typed API layer mirroring the real backend surface
AndyTechnologies Sep 21, 2026
c5c1c05
docs(odd): mark ui-console U02 complete with verification evidence
AndyTechnologies Sep 21, 2026
a4260c6
feat(ui): live Overview dashboard with honest data states
AndyTechnologies Sep 22, 2026
66b2d67
docs(odd): mark ui-console U03 complete, schedule frontend typecheck …
AndyTechnologies Sep 22, 2026
c40b1c3
feat(ui): workflow library with list, create, run, logs and delete
AndyTechnologies Sep 22, 2026
0e511c1
docs(odd): mark ui-console U04 complete, record real YAML array-form …
AndyTechnologies Sep 22, 2026
a04069e
feat(ui): workflow editor foundation with real YAML round-trip
AndyTechnologies Sep 22, 2026
d17be52
feat(ui): editable editor semantics with validation and canonical save
AndyTechnologies Sep 22, 2026
02ab912
docs(odd): mark ui-console U06 complete with verification evidence
AndyTechnologies Sep 22, 2026
c0464d6
docs(odd): record feature-branch-chain delivery strategy
AndyTechnologies Sep 22, 2026
00f1899
feat(ui): workflow execution with live WS run and per-workflow logs
AndyTechnologies Sep 22, 2026
a903ccb
docs(odd): mark ui-console U07 complete with verification evidence
AndyTechnologies Sep 22, 2026
8a04fba
feat(ui): models registry with real states and honest catalog/downloads
AndyTechnologies Sep 22, 2026
fd11d98
docs(odd): mark ui-console U08 complete with verification evidence
AndyTechnologies Sep 22, 2026
bd80710
feat(ui): providers pages with honest coming-from-backend state
AndyTechnologies Sep 22, 2026
8d858d9
docs(odd): mark ui-console U09 complete with verification evidence
AndyTechnologies Sep 22, 2026
0615954
feat(ui): executions page with bounded per-workflow log aggregation
AndyTechnologies Sep 22, 2026
ef74266
docs(odd): mark ui-console U10 complete with verification evidence
AndyTechnologies Sep 22, 2026
d2e1587
feat(ui): API playground over the real /v1 wire
AndyTechnologies Sep 22, 2026
744e2e9
docs(odd): mark ui-console U11 complete with verification evidence
AndyTechnologies Sep 22, 2026
5408f6d
feat(ui): runtime page with real health, origin, auth and WS probe
AndyTechnologies Sep 22, 2026
738b895
docs(odd): mark ui-console U12 complete with verification evidence
AndyTechnologies Sep 22, 2026
c288f0c
feat(ui): settings and about pages with honest server-owned state
AndyTechnologies Sep 22, 2026
1942d5b
docs(odd): mark ui-console U13 complete with verification evidence
AndyTechnologies Sep 22, 2026
8996603
feat(ui): frontend typecheck coverage and final hardening
AndyTechnologies Sep 23, 2026
f4c59ed
docs(odd): mark ui-console U14 complete — all 14 phases done
AndyTechnologies Sep 23, 2026
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
165 changes: 161 additions & 4 deletions bun.lock

Large diffs are not rendered by default.

115 changes: 115 additions & 0 deletions frontend/src/layouts/AppLayout.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
---
import "../styles/global.css";
import pkg from "../../../package.json";
import Sidebar from "../svelte/navigation/Sidebar.svelte";
import Topbar from "../svelte/navigation/Topbar.svelte";
import StatusBar from "../svelte/navigation/StatusBar.svelte";
import CommandPalette from "../svelte/navigation/CommandPalette.svelte";
import { getApiOrigin } from "../lib/api/config.js";

interface Props {
/** Page title, used as the breadcrumb terminal and document title. */
title: string;
/** Optional full breadcrumb trail; defaults to the page title alone. */
breadcrumb?: string[];
}

const { title, breadcrumb = [title] } = Astro.props;
const origin = getApiOrigin();
const currentPath = Astro.url.pathname;
const documentTitle = `${breadcrumb[breadcrumb.length - 1] ?? title} · WeaveLLM`;
---

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="color-scheme" content="dark" />
<title>{documentTitle}</title>
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>

{/*
Chrome hydrates lazily: these islands render their interactive shell
server-side and hydrate when the browser is idle. Page content islands
keep client:load so their data loads without waiting on idle.
*/}
<Sidebar client:idle current={currentPath} />

<div class="shell">
<Topbar client:idle breadcrumb={breadcrumb} origin={origin} />

<main class="main" id="main" tabindex={-1}>
<slot />
</main>

<StatusBar origin={origin} version={pkg.version} />
</div>

<CommandPalette client:idle />
</body>
</html>

<style>
.skip-link {
position: fixed;
top: var(--space-3);
left: var(--space-3);
z-index: 100;
padding: var(--space-2) var(--space-4);
border: 1px solid var(--border);
border-radius: var(--radius-control);
background: var(--card);
color: var(--text);
transform: translateY(-300%);
transition: transform 150ms ease;
}

.skip-link:focus {
transform: translateY(0);
}

/* Main column clears the fixed sidebar (240px, 64px rail, then drawer). */
.shell {
display: flex;
flex-direction: column;
min-height: 100vh;
margin-left: 240px;
}

.main {
flex: 1;
width: 100%;
max-width: 1152px;
margin-inline: auto;
padding: var(--space-6);
}

@media (min-width: 768px) and (max-width: 1023px) {
.shell {
margin-left: 64px;
}

.main {
padding: var(--space-5);
}
}

@media (max-width: 767px) {
.shell {
margin-left: 0;
}

.main {
padding: var(--space-4);
}
}

@media (prefers-reduced-motion: reduce) {
.skip-link {
transition: none;
}
}
</style>
20 changes: 20 additions & 0 deletions frontend/src/lib/api/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, test } from "bun:test";
import { getApiOrigin, getWsOrigin, wsOriginFrom } from "./config.js";

describe("api origin helpers", () => {
test("ws origin derives from an http origin", () => {
expect(wsOriginFrom("http://127.0.0.1:4317")).toBe("ws://127.0.0.1:4317");
});

test("https origins map to wss", () => {
expect(wsOriginFrom("https://models.example.com")).toBe("wss://models.example.com");
});

test("getApiOrigin returns an http url", () => {
expect(getApiOrigin()).toMatch(/^https?:\/\//);
});

test("getWsOrigin returns a ws url", () => {
expect(getWsOrigin()).toMatch(/^wss?:\/\//);
});
});
31 changes: 31 additions & 0 deletions frontend/src/lib/api/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* API origin resolution for the console shell.
*
* The backend listens on 127.0.0.1:4317 in development. The origin can be
* overridden at build/dev time with `PUBLIC_WEAVELLM_API_ORIGIN` (Astro
* exposes PUBLIC_* environment variables to client code through
* `import.meta.env`). This is the only API-layer module in phase 1; typed
* clients for /api, /v1 and /ws arrive in phase 2.
*/

const DEFAULT_API_ORIGIN = "http://127.0.0.1:4317";

/** Base origin for backend HTTP calls (defaults to the local runtime). */
export function getApiOrigin(): string {
const override: string | undefined = import.meta.env.PUBLIC_WEAVELLM_API_ORIGIN as
| string
| undefined;
return typeof override === "string" && override.length > 0
? override
: DEFAULT_API_ORIGIN;
}

/** Derive a websocket origin from an http(s) origin (http→ws, https→wss). */
export function wsOriginFrom(apiOrigin: string): string {
return apiOrigin.replace(/^http/, "ws");
}

/** Base origin for backend websocket calls (WS /ws). */
export function getWsOrigin(): string {
return wsOriginFrom(getApiOrigin());
}
43 changes: 43 additions & 0 deletions frontend/src/lib/api/health.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* bun:test suite for the /api/health client (getHealth). NO auth gate on the
* backend — the health branch is open. fetchImpl injected; no network.
*/

import { describe, expect, test } from "bun:test";
import { ApiError, type FetchLike } from "./http.js";
import { getHealth } from "./health.js";

function jsonBody(
body: unknown,
status = 200,
): FetchLike {
return async () =>
new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});
}

describe("getHealth()", () => {
test("returns {status:\"ok\"} for a live backend", async () => {
const fetchImpl = jsonBody({ status: "ok" });
const result = await getHealth({ origin: "http://test", fetchImpl });
expect(result).toEqual({ status: "ok" });
});

test("propagates localModels when a hub is wired", async () => {
const fetchImpl = jsonBody({ status: "ok", localModels: ["llama-3.1-8b"] });
const result = await getHealth({ origin: "http://test", fetchImpl });
expect(result.localModels).toEqual(["llama-3.1-8b"]);
});

test("throws ApiError on a non-ok backend status", async () => {
const fetchImpl = jsonBody({ error: "service_unavailable" }, 503);
const caught = await getHealth({ origin: "http://test", fetchImpl }).catch(
(err: unknown) => err,
);
expect(caught).toBeInstanceOf(ApiError);
expect((caught as ApiError).status).toBe(503);
expect((caught as ApiError).code).toBe("service_unavailable");
});
});
21 changes: 21 additions & 0 deletions frontend/src/lib/api/health.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Typed client for the health surface.
*
* GET /api/health → HealthStatus (liveness; NO auth gate — the probe the
* console polls freely at boot and on the status bar).
*/

import { request, type FetchLike } from "./http.js";
import type { HealthStatus } from "./types.js";

export interface HealthOptions {
origin?: string;
fetchImpl?: FetchLike;
signal?: AbortSignal;
timeoutMs?: number;
}

/** GET /api/health → {status:"ok", localModels?}. */
export async function getHealth(options: HealthOptions = {}): Promise<HealthStatus> {
return request<HealthStatus>("/api/health", { method: "GET", ...options });
}
91 changes: 91 additions & 0 deletions frontend/src/lib/api/http.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/**
* bun:test suite for the http transport (request + ApiError). Mirrors the
* config.test.ts style (U01) — bun:test, describe/test, imports "./x.js" —
* and drives request() with an injected fake fetch so no real network or
* backend process is touched.
*/

import { describe, expect, test } from "bun:test";
import { ApiError, request, type FetchLike } from "./http.js";

/** Fake fetch returning a canned Response for a single call. */
function jsonBody(
body: unknown,
status = 200,
headers: Record<string, string> = { "content-type": "application/json" },
): FetchLike {
return async () => new Response(JSON.stringify(body), { status, headers });
}

describe("request()", () => {
test("parses a 200 JSON response body", async () => {
const fetchImpl = jsonBody({ name: "file", version: 3 });
const result = await request<{ name: string; version: number }>("/api/workflows", {
origin: "http://test",
fetchImpl,
});
expect(result).toEqual({ name: "file", version: 3 });
});

test("returns undefined for a 204", async () => {
const fetchImpl = jsonBody(undefined, 204);
const result = await request<void>("/api/workflows/del", {
method: "DELETE",
origin: "http://test",
fetchImpl,
});
expect(result).toBeUndefined();
});

test("throws ApiError wrapping the {error} envelope on non-2xx", async () => {
const fetchImpl = jsonBody(
{ error: "workflow_not_found" },
404,
{ "content-type": "application/json" },
);
const caught = await request<unknown>("/api/workflows/missing", {
origin: "http://test",
fetchImpl,
}).catch((err: unknown) => err);

expect(caught).toBeInstanceOf(ApiError);
const apiError = caught as ApiError;
expect(apiError.status).toBe(404);
expect(apiError.code).toBe("workflow_not_found");
expect(apiError.message).toBe("workflow_not_found");
});

test("throws ApiError(0, network_error) on transport failure", async () => {
const fetchImpl: FetchLike = async () => {
throw new TypeError("fetch failed");
};

const caught = await request<unknown>("/api/health", {
origin: "http://test",
fetchImpl,
}).catch((err: unknown) => err);

expect(caught).toBeInstanceOf(ApiError);
const apiError = caught as ApiError;
expect(apiError.status).toBe(0);
expect(apiError.code).toBe("network_error");
});

test("carries backend envelope errors when present", async () => {
const fetchImpl = jsonBody(
{ error: "invalid_yaml", errors: [{ path: "nodes", message: "missing start" }] },
400,
{ "content-type": "application/json" },
);
const caught = await request<unknown>("/api/workflows/x", {
method: "PUT",
origin: "http://test",
fetchImpl,
}).catch((err: unknown) => err);

const apiError = caught as ApiError;
expect(apiError.status).toBe(400);
expect(apiError.code).toBe("invalid_yaml");
expect(apiError.errors).toEqual([{ path: "nodes", message: "missing start" }]);
});
});
Loading
Loading