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
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,14 @@ jobs:
- name: UI/MCP version audit
if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.mcp == 'true' }}
run: npm run ui:version-audit
# gittensory-ui and gittensory-miner-ui both import compiled output from this shared package
# (packages/gittensory-ui-kit/dist), which is gitignored like every other workspace package's
# dist -- so it does not exist on a fresh checkout until something builds it. Same class of gap
# as "Build engine package" above; runs before UI lint/typecheck/tests/build all in this same
# job, so building it here once covers all four.
- name: Build UI-kit package
if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }}
run: npm run ui:kit:build
- name: UI lint
if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }}
run: npm run ui:lint
Expand Down
20 changes: 20 additions & 0 deletions apps/gittensory-miner-ui/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,24 @@ export default tseslint.config(
},
},
eslintPluginPrettier,
{
files: ["src/routes/**/*.{ts,tsx}"],
rules: {
"no-restricted-syntax": [
"error",
{
selector:
"Literal[value=/(^|\\s)(text-(2xs|xs|sm|base|md|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)|text-\\[\\d|leading-(none|tight|snug|normal|relaxed|loose|3|4|5|6|7|8|9|10)|leading-\\[|rounded-(sm|md|lg|xl|2xl|3xl)|divide-y(\\s|$)|border-(t|b|l|r)(\\s|$))(\\s|$)/]",
message:
"Use design tokens instead: text-token-*, leading-token-*, rounded-token, border-hairline / divide-hairline. See @jsonbored/gittensory-ui-kit/theme.css.",
},
{
selector:
"TemplateElement[value.raw=/(^|\\s)(text-(2xs|xs|sm|base|md|lg|xl|2xl|3xl|4xl|5xl|6xl|7xl|8xl|9xl)|leading-(none|tight|snug|normal|relaxed|loose)|rounded-(sm|md|lg|xl|2xl|3xl))(\\s|$)/]",
message:
"Use design tokens (text-token-*, leading-token-*, rounded-token, border-hairline) in template strings too.",
},
],
},
},
);
2 changes: 1 addition & 1 deletion apps/gittensory-miner-ui/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" class="dark" style="color-scheme: dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
Expand Down
4 changes: 3 additions & 1 deletion apps/gittensory-miner-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@
"format": "prettier --write ."
},
"dependencies": {
"@jsonbored/gittensory-ui-kit": "^0.1.0",
"@tailwindcss/vite": "^4.3.2",
"@tanstack/react-router": "^1.170.17",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"tailwindcss": "^4.3.2",
"tw-animate-css": "^1.4.0",
"vite-tsconfig-paths": "^6.1.1"
},
"devDependencies": {
Expand All @@ -39,7 +41,7 @@
"prettier": "^3.8.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.1",
"vite": "^7.3.1",
"vite": "^8.1.3",
"vitest": "^4.1.9"
}
}
16 changes: 8 additions & 8 deletions apps/gittensory-miner-ui/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,21 @@ export const Route = createRootRoute({

function RootLayout() {
return (
<div className="min-h-screen">
<header className="border-b border-white/10 px-6 py-4">
<div className="min-h-screen bg-background text-foreground">
<header className="border-b-hairline px-6 py-4">
<div className="mx-auto flex max-w-5xl items-center justify-between gap-4">
<div>
<p className="text-sm uppercase tracking-[0.2em] text-emerald-300/80">Gittensory Miner</p>
<h1 className="text-lg font-semibold">Local dashboard shell</h1>
<p className="text-token-xs uppercase tracking-[0.2em] text-primary font-mono">Gittensory Miner</p>
<h1 className="text-token-lg font-display font-semibold">Local dashboard</h1>
</div>
<nav className="flex gap-4 text-sm text-white/70">
<Link to="/" className="hover:text-white">
<nav className="flex gap-4 text-token-sm text-muted-foreground">
<Link to="/" className="hover-surface rounded-token-sm px-2 py-1 hover:text-foreground">
Overview
</Link>
<Link to="/run-history" className="hover:text-white">
<Link to="/run-history" className="hover-surface rounded-token-sm px-2 py-1 hover:text-foreground">
Run history
</Link>
<Link to="/portfolio" className="hover:text-white">
<Link to="/portfolio" className="hover-surface rounded-token-sm px-2 py-1 hover:text-foreground">
Portfolio
</Link>
</nav>
Expand Down
20 changes: 13 additions & 7 deletions apps/gittensory-miner-ui/src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { createFileRoute } from "@tanstack/react-router";

import { Card, CardContent, CardHeader } from "@jsonbored/gittensory-ui-kit/components/card";

export const Route = createFileRoute("/")({
component: IndexPage,
});

export function IndexPage() {
return (
<section className="rounded-xl border border-white/10 bg-white/5 p-6">
<h2 className="text-xl font-semibold">Dashboard shell ready</h2>
<p className="mt-3 max-w-2xl text-sm leading-6 text-white/70">
This package is the empty Phase 6 scaffold for a local, read-only miner dashboard. Run-history and portfolio
views will mount here in follow-up issues once the local data-access layer is wired.
</p>
</section>
<Card>
<CardHeader>
<h2 className="font-display text-token-lg font-semibold">Dashboard shell ready</h2>
</CardHeader>
<CardContent>
<p className="max-w-2xl text-token-sm leading-token-relaxed text-muted-foreground">
This package is the empty Phase 6 scaffold for a local, read-only miner dashboard. Run-history and portfolio
views will mount here in follow-up issues once the local data-access layer is wired.
</p>
</CardContent>
</Card>
);
}
58 changes: 33 additions & 25 deletions apps/gittensory-miner-ui/src/routes/portfolio.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { createFileRoute } from "@tanstack/react-router";
import { useEffect, useState } from "react";

import { Card, CardContent, CardHeader } from "@jsonbored/gittensory-ui-kit/components/card";

import { fetchPortfolioQueue, type PortfolioQueueResult, type QueueStatus } from "../lib/portfolio-queue";

export const Route = createFileRoute("/portfolio")({
Expand All @@ -16,42 +18,46 @@ const STATUS_LABELS: Record<QueueStatus, string> = {
done: "Done",
};

const STATUS_CARD_CLASSES: Record<QueueStatus, string> = {
queued: "border-sky-400/30 bg-sky-500/10 text-sky-100",
in_progress: "border-amber-400/30 bg-amber-500/10 text-amber-100",
done: "border-emerald-400/30 bg-emerald-500/10 text-emerald-100",
// Semantic tone per status, sourced from the shared design system's success/warning
// tokens rather than arbitrary color utilities — kept separate from the accent hue.
const STATUS_TONE: Record<QueueStatus, string> = {
queued: "text-muted-foreground",
in_progress: "text-[var(--warning)]",
done: "text-[var(--success)]",
};

export function PortfolioQueueView({ result }: { result: PortfolioQueueResult | null }) {
if (result === null) {
return <p className="text-sm text-white/60">Loading local portfolio queue…</p>;
return <p className="text-token-sm text-muted-foreground">Loading local portfolio queue…</p>;
}
if (!result.ok) {
return (
<p role="alert" className="text-sm text-rose-300">
<p role="alert" className="text-token-sm text-[var(--danger)]">
Could not read the local portfolio queue: {result.error}
</p>
);
}
const summary = result.summary;
if (summary.total === 0) {
return (
<p className="text-sm text-white/60">
<p className="text-token-sm text-muted-foreground">
No queued work yet — the cards fill in once the miner enqueues its first portfolio item.
</p>
);
}
return (
<div>
<dl className="grid gap-4 sm:grid-cols-3">
{(Object.keys(STATUS_LABELS) as QueueStatus[]).map((status) => (
<div key={status} className={`rounded-xl border p-4 ${STATUS_CARD_CLASSES[status]}`}>
<dt className="text-xs uppercase tracking-wider opacity-80">{STATUS_LABELS[status]}</dt>
<dd className="mt-1 text-3xl font-semibold">{summary.counts[status]}</dd>
</div>
))}
</dl>
</div>
<dl className="grid gap-4 sm:grid-cols-3">
{(Object.keys(STATUS_LABELS) as QueueStatus[]).map((status) => (
<Card key={status}>
<CardContent className="p-4">
<dt className="text-token-2xs uppercase tracking-wider text-muted-foreground">{STATUS_LABELS[status]}</dt>
<dd className={`mt-1 text-token-3xl font-display font-semibold ${STATUS_TONE[status]}`}>
{summary.counts[status]}
</dd>
</CardContent>
</Card>
))}
</dl>
);
}

Expand All @@ -73,14 +79,16 @@ export function PortfolioPage({
}, [loadPortfolioQueue]);

return (
<section className="rounded-xl border border-white/10 bg-white/5 p-6">
<h2 className="text-xl font-semibold">Portfolio queue</h2>
<p className="mt-1 text-sm text-white/60">
Local, read-only summary of the miner&apos;s portfolio queue (`miner_portfolio_queue`).
</p>
<div className="mt-4">
<Card>
<CardHeader>
<h2 className="font-display text-token-lg font-semibold">Portfolio queue</h2>
<p className="text-token-sm text-muted-foreground">
Local, read-only summary of the miner&apos;s portfolio queue (`miner_portfolio_queue`).
</p>
</CardHeader>
<CardContent>
<PortfolioQueueView result={result} />
</div>
</section>
</CardContent>
</Card>
);
}
87 changes: 47 additions & 40 deletions apps/gittensory-miner-ui/src/routes/run-history.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import { createFileRoute } from "@tanstack/react-router";
import { useEffect, useState } from "react";

import { Badge } from "@jsonbored/gittensory-ui-kit/components/badge";
import { Card, CardContent, CardHeader } from "@jsonbored/gittensory-ui-kit/components/card";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@jsonbored/gittensory-ui-kit/components/table";

import { fetchRunStates, type RunHistoryResult, type RunStateRow } from "../lib/run-history";

export const Route = createFileRoute("/run-history")({
Expand All @@ -11,58 +22,52 @@ export const Route = createFileRoute("/run-history")({
// last-updated), served by the dev server's local API. No writes, no new state — a fresh install renders the
// empty state, an unreachable API renders an error message.

const STATE_BADGE_CLASSES: Record<RunStateRow["state"], string> = {
idle: "bg-white/10 text-white/70",
discovering: "bg-sky-500/20 text-sky-200",
planning: "bg-amber-500/20 text-amber-200",
preparing: "bg-emerald-500/20 text-emerald-200",
const STATE_BADGE_VARIANT: Record<RunStateRow["state"], "secondary" | "outline"> = {
idle: "secondary",
discovering: "outline",
planning: "outline",
preparing: "outline",
};

export function RunHistoryView({ result }: { result: RunHistoryResult | null }) {
if (result === null) {
return <p className="text-sm text-white/60">Loading local run state…</p>;
return <p className="text-token-sm text-muted-foreground">Loading local run state…</p>;
}
if (!result.ok) {
return (
<p role="alert" className="text-sm text-rose-300">
<p role="alert" className="text-token-sm text-[var(--danger)]">
Could not read local run state: {result.error}
</p>
);
}
if (result.rows.length === 0) {
return (
<p className="text-sm text-white/60">
<p className="text-token-sm text-muted-foreground">
No local run state yet — the table fills in once the miner records its first repo run.
</p>
);
}
return (
<table className="w-full text-left text-sm">
<thead>
<tr className="border-b border-white/10 text-xs uppercase tracking-wider text-white/50">
<th scope="col" className="py-2 pr-4">
Repository
</th>
<th scope="col" className="py-2 pr-4">
State
</th>
<th scope="col" className="py-2">
Last updated
</th>
</tr>
</thead>
<tbody>
<Table>
<TableHeader>
<TableRow>
<TableHead>Repository</TableHead>
<TableHead>State</TableHead>
<TableHead>Last updated</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{result.rows.map((row) => (
<tr key={row.repoFullName} className="border-b border-white/5">
<td className="py-2 pr-4 font-mono text-white/90">{row.repoFullName}</td>
<td className="py-2 pr-4">
<span className={`rounded-full px-2 py-0.5 text-xs ${STATE_BADGE_CLASSES[row.state]}`}>{row.state}</span>
</td>
<td className="py-2 text-white/70">{row.updatedAt}</td>
</tr>
<TableRow key={row.repoFullName}>
<TableCell className="font-mono text-foreground">{row.repoFullName}</TableCell>
<TableCell>
<Badge variant={STATE_BADGE_VARIANT[row.state]}>{row.state}</Badge>
</TableCell>
<TableCell className="text-muted-foreground">{row.updatedAt}</TableCell>
</TableRow>
))}
</tbody>
</table>
</TableBody>
</Table>
);
}

Expand All @@ -84,14 +89,16 @@ export function RunHistoryPage({
}, [loadRunStates]);

return (
<section className="rounded-xl border border-white/10 bg-white/5 p-6">
<h2 className="text-xl font-semibold">Run history</h2>
<p className="mt-1 text-sm text-white/60">
Local, read-only view over the miner&apos;s per-repo run state (`miner_run_state`).
</p>
<div className="mt-4">
<Card>
<CardHeader>
<h2 className="font-display text-token-lg font-semibold">Run history</h2>
<p className="text-token-sm text-muted-foreground">
Local, read-only view over the miner&apos;s per-repo run state (`miner_run_state`).
</p>
</CardHeader>
<CardContent>
<RunHistoryView result={result} />
</div>
</section>
</CardContent>
</Card>
);
}
25 changes: 13 additions & 12 deletions apps/gittensory-miner-ui/src/styles.css
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
@import "tailwindcss";
@import "tailwindcss" source(none);
@source "../src";
@source "../../../packages/gittensory-ui-kit/src";
@import "tw-animate-css";
@import "@jsonbored/gittensory-ui-kit/theme.css";

:root {
color-scheme: dark;
font-family:
"Segoe UI",
system-ui,
-apple-system,
sans-serif;
background: #0f1117;
color: #f4f7f5;
}
/*
* gittensory-miner-ui renders from the same shared design system as
* gittensory-ui (imported above from @jsonbored/gittensory-ui-kit) — no
* local tokens, colors, or fonts. Dark-only for now (the `.dark` class is
* hardcoded on <html> in index.html), matching gittensory-ui's current
* dark-only deployment — the light tokens ship in the shared theme so a
* toggle can be added later without touching this stylesheet.
*/

body {
margin: 0;
min-height: 100vh;
}
1 change: 1 addition & 0 deletions apps/gittensory-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"format": "prettier --write ."
},
"dependencies": {
"@jsonbored/gittensory-ui-kit": "^0.1.0",
"@radix-ui/react-accordion": "^1.2.15",
"@radix-ui/react-alert-dialog": "^1.1.18",
"@radix-ui/react-aspect-ratio": "^1.1.11",
Expand Down
Loading