Skip to content
Open
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
19 changes: 19 additions & 0 deletions packages/web-readonly-status/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover"
/>
<meta
name="description"
content="Read-only local adapter status page for the approved summary endpoint."
/>
<title>OpenCode Read-Only Status</title>
<script type="module" src="/src/main.tsx"></script>
</head>
<body>
<div id="root"></div>
</body>
</html>
23 changes: 23 additions & 0 deletions packages/web-readonly-status/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "@openchamber/web-readonly-status",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"type-check": "tsc --noEmit",
"lint": "eslint \"./src/**/*.{ts,tsx}\" --config ../../eslint.config.js"
},
"dependencies": {
"react": "^19.1.0",
"react-dom": "^19.1.0"
},
"devDependencies": {
"@types/react": "^19.1.10",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^5.0.0",
"typescript": "~5.9.3",
"vite": "^7.1.2"
}
}
170 changes: 170 additions & 0 deletions packages/web-readonly-status/src/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { useEffect, useState } from "react";
import {
REAL_ADAPTER_SUMMARY_URL,
assertRealAdapterSummaryUrl,
} from "./adapter/realAdapter";
import {
type RealAdapterSummary,
fetchRealAdapterSummary,
} from "./adapter/realAdapterClient";

type LoadState = "idle" | "loading" | "ready" | "error";

function formatBoolean(value: boolean | null): string {
if (value === true) {
return "true";
}
if (value === false) {
return "false";
}
return "unknown";
}

function formatStatusCode(value: number | null): string {
return value === null ? "unknown" : String(value);
}

function formatTagCount(value: number | null): string {
return value === null ? "unknown" : String(value);
}

function formatTimestamp(value: string | null): string {
if (!value) {
return "Not refreshed yet";
}
const date = new Date(value);
return Number.isNaN(date.valueOf()) ? value : date.toLocaleString();
}

function App() {
const [loadState, setLoadState] = useState<LoadState>("idle");
const [summary, setSummary] = useState<RealAdapterSummary | null>(null);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [lastRefreshedAt, setLastRefreshedAt] = useState<string | null>(null);

async function refreshSummary(): Promise<void> {
try {
setLoadState("loading");
setErrorMessage(null);
assertRealAdapterSummaryUrl();
const nextSummary = await fetchRealAdapterSummary();
setSummary(nextSummary);
setLastRefreshedAt(new Date().toISOString());
setLoadState("ready");
} catch (error) {
setErrorMessage(error instanceof Error ? error.message : "Unknown error");
setLastRefreshedAt(new Date().toISOString());
setLoadState("error");
}
}

useEffect(() => {
void refreshSummary();
}, []);

return (
<main className="shell">
<section className="hero">
<p className="eyebrow">Read-only local adapter view</p>
<h1>OpenCode Read-Only Status</h1>
<p className="lede">
This page is limited to one approved browser request and displays
status only.
</p>
<div className="boundary">
<span className="boundary-label">Allowed endpoint</span>
<code>{REAL_ADAPTER_SUMMARY_URL}</code>
</div>
<div className="toolbar">
<button
className="refreshButton"
type="button"
onClick={() => {
void refreshSummary();
}}
disabled={loadState === "loading"}
>
{loadState === "loading" ? "Refreshing..." : "Refresh status"}
</button>
<p className="timestamp">
Last refreshed: {formatTimestamp(lastRefreshedAt)}
</p>
</div>
</section>

<section className="grid" aria-label="Read-only adapter summary">
<article className="card accent">
<header>
<h2>Adapter</h2>
<span className={`pill pill-${loadState}`}>{loadState}</span>
</header>
<dl>
<div>
<dt>Status</dt>
<dd>{summary?.adapterStatus ?? "unknown"}</dd>
</div>
<div>
<dt>OpenCode reachable</dt>
<dd>{summary?.opencodeReachable ? "true" : "false"}</dd>
</div>
<div>
<dt>OpenCode HTTP status</dt>
<dd>{formatStatusCode(summary?.opencodeStatus ?? null)}</dd>
</div>
</dl>
</article>

<article className="card">
<header>
<h2>Repository</h2>
<span className="subtle">Display only</span>
</header>
<dl>
<div>
<dt>Branch</dt>
<dd>{summary?.repoBranch ?? "unknown"}</dd>
</div>
<div>
<dt>HEAD</dt>
<dd className="mono">{summary?.repoHead ?? "unknown"}</dd>
</div>
<div>
<dt>Clean</dt>
<dd>{formatBoolean(summary?.repoClean ?? null)}</dd>
</div>
<div>
<dt>Stage 3 tag count</dt>
<dd>{formatTagCount(summary?.repoStage3TagCount ?? null)}</dd>
</div>
</dl>
</article>

<article className="card warning">
<header>
<h2>Safety boundary</h2>
<span className="subtle">Hard-coded</span>
</header>
<ul className="guardList">
<li>No direct 4096 browser egress</li>
<li>No chat, agent, terminal, git, or file actions</li>
<li>No provider setup, auth, cookies, or embedded credentials</li>
<li>No POST, PUT, PATCH, or DELETE routes</li>
</ul>
</article>
</section>

{errorMessage ? (
<section className="errorPanel" role="alert">
<h2>Adapter request failed</h2>
<p>{errorMessage}</p>
<p>
This page should fail closed and must not fall back to any other
endpoint.
</p>
</section>
) : null}
</main>
);
}

export default App;
31 changes: 31 additions & 0 deletions packages/web-readonly-status/src/adapter/realAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/**
* Web Spike 3P: read-only browser client with one fixed summary endpoint.
*
* This package must never expand into chat, terminal, git mutation, provider,
* filesystem, auth, or direct 4096 access without a separate approved review.
*/
export const REAL_ADAPTER_READONLY_MODE = true;

export const REAL_ADAPTER_SUMMARY_URL =
"http://192.168.50.202:8790/api/local/v0/summary";

export function assertRealAdapterSummaryUrl(
summaryUrl: string = REAL_ADAPTER_SUMMARY_URL,
): void {
const url = new URL(summaryUrl);
if (url.protocol !== "http:") {
throw new Error(`real adapter refuses non-http protocol: ${url.protocol}`);
}
if (url.username || url.password) {
throw new Error("real adapter refuses embedded credentials");
}
if (url.hostname !== "192.168.50.202") {
throw new Error(`real adapter refuses unexpected host: ${url.hostname}`);
}
if (url.port !== "8790") {
throw new Error(`real adapter refuses unexpected port: ${url.port}`);
}
if (url.pathname !== "/api/local/v0/summary") {
throw new Error(`real adapter refuses unexpected path: ${url.pathname}`);
}
}
70 changes: 70 additions & 0 deletions packages/web-readonly-status/src/adapter/realAdapterClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {
REAL_ADAPTER_READONLY_MODE,
REAL_ADAPTER_SUMMARY_URL,
assertRealAdapterSummaryUrl,
} from "./realAdapter";

interface AdapterSummaryResponse {
adapter?: {
status?: unknown;
};
opencode?: {
reachable?: unknown;
status?: unknown;
};
repo?: {
branch?: unknown;
head?: unknown;
clean?: unknown;
stage3TagCount?: unknown;
};
}

export interface RealAdapterSummary {
adapterStatus: string;
opencodeReachable: boolean;
opencodeStatus: number | null;
repoBranch: string;
repoHead: string;
repoClean: boolean | null;
repoStage3TagCount: number | null;
}

function guard(): void {
if (!REAL_ADAPTER_READONLY_MODE) {
throw new Error("real adapter read-only web app is disabled");
}
assertRealAdapterSummaryUrl(REAL_ADAPTER_SUMMARY_URL);
}

function toNumber(value: unknown): number | null {
return typeof value === "number" && Number.isFinite(value) ? value : null;
}

export async function fetchRealAdapterSummary(): Promise<RealAdapterSummary> {
guard();
const response = await fetch(REAL_ADAPTER_SUMMARY_URL, {
method: "GET",
headers: {
Accept: "application/json",
},
credentials: "omit",
});

if (!response.ok) {
throw new Error(`real adapter GET /summary -> ${response.status}`);
}

const data = (await response.json()) as AdapterSummaryResponse;
return {
adapterStatus:
typeof data.adapter?.status === "string" ? data.adapter.status : "unknown",
opencodeReachable: data.opencode?.reachable === true,
opencodeStatus: toNumber(data.opencode?.status),
repoBranch:
typeof data.repo?.branch === "string" ? data.repo.branch : "unknown",
repoHead: typeof data.repo?.head === "string" ? data.repo.head : "unknown",
repoClean: typeof data.repo?.clean === "boolean" ? data.repo.clean : null,
repoStage3TagCount: toNumber(data.repo?.stage3TagCount),
};
}
10 changes: 10 additions & 0 deletions packages/web-readonly-status/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./styles.css";

ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);
Loading