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
55 changes: 52 additions & 3 deletions dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,20 @@ npm install
npm run dev
```

Open [http://localhost:5173](http://localhost:5173) to view the dashboard.
Open [http://localhost:5173](http://localhost:5173) to view the engineer dashboard.

**Team Brain admin review inbox** (local only — [#69](https://github.com/Hrithik-Gavankar/brainstack/issues/69)):

```bash
# Optional: pre-fill Supabase project (same vars as CLI / supabase/project.public.env)
export VITE_TEAM_BRAIN_SUPABASE_URL=https://xxxx.supabase.co
export VITE_TEAM_BRAIN_SUPABASE_ANON_KEY=eyJ...

npm run dev
# → http://localhost:5173/admin/review
```

Connect with a crew **admin** `tb_…` API key (session storage only). Approve/reject calls the same RPCs as `pending list|approve|reject` in `core/scripts/team-brain-api.sh`. Never deploy real keys to GitHub Pages.

```bash
npm run build # production build → dist/
Expand Down Expand Up @@ -81,21 +94,57 @@ npm run preview

Public demo tip (Vercel): set **Root Directory** to `dashboard`, build command `npm run build`, output `dist`.

## Team Brain admin review queue

Admin-only inbox for pending memory overrides (#67 / #69). Workshop-friendly alternative to `pending list` JSON in the terminal.

| View | Source |
|------|--------|
| Pending queue | `list_pending_memories` RPC (`status = pending`, optional `p_jira_key`) |
| Diff panel | Proposed `body_preview` vs live capture via `list_recent` + `target_capture_id` |
| Metadata | `conflict_reason`, author, `match_metadata.matches`, timestamps |
| Actions | `approve_pending_memory` · `reject_pending_memory` (admin role required) |

**Auth:** Same trust model as CLI — `tb_…` api_key in `sessionStorage`, Supabase anon key for REST RPC calls. No anon `SELECT` on `memory_pending_submissions`; RPC-only.

**Privacy:** Team-scoped like `list_members`. Never surfaces personal `BRAIN.md`.

**Route:** `/admin/review` (pathname switch in `Root.tsx`; no extra router dependency).

```
dashboard/src/
├── admin/ # Team Brain review inbox (#69)
│ ├── AdminReviewApp.tsx
│ ├── AdminCredentialsForm.tsx
│ ├── PendingSubmissionCard.tsx
│ └── MemoryDiffPanel.tsx
├── team-brain/
│ ├── rpc.ts # Supabase REST RPC client
│ ├── session.ts # sessionStorage credentials
│ └── types.ts
├── Root.tsx # /admin/review vs engineer dashboard
```

See [docs/team-brain-memory.md §7c](../docs/team-brain-memory.md#7c-redundant-memory-feedback--admin-approval-queue-67).

## Architecture

```
dashboard/
├── public/
│ └── favicon.svg
├── src/
│ ├── components/ # React presentation components
│ ├── admin/ # Team Brain review inbox (local admin only)
│ ├── team-brain/ # RPC client + types
│ ├── components/ # Engineer dashboard presentation
│ ├── data/
│ │ ├── loadDashboardData.ts # Stable data port
│ │ ├── sampleData.ts # Demo adapter payload
│ │ └── brainAdapter.ts # Stub for BRAIN.md parser
│ ├── colors.ts # Chart colors (UI layer)
│ ├── types.ts # DashboardData + brain-spec types
│ ├── App.tsx
│ ├── App.tsx # Engineer dashboard (sample data)
│ ├── Root.tsx # Route: /admin/review → AdminReviewApp
│ ├── main.tsx
│ └── index.css
├── package.json
Expand Down
8 changes: 4 additions & 4 deletions dashboard/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions dashboard/src/Root.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import App from "./App";
import { AdminReviewApp } from "./admin/AdminReviewApp";

const isAdminReviewRoute = (): boolean => {
const base = import.meta.env.BASE_URL.replace(/\/$/, "");
const path = window.location.pathname.replace(/\/$/, "");
const suffix = base ? `${base}/admin/review` : "/admin/review";
return path === suffix || path.endsWith("/admin/review");
};

export const Root = () => {
if (isAdminReviewRoute()) {
return <AdminReviewApp />;
}
return <App />;
};

export default Root;
137 changes: 137 additions & 0 deletions dashboard/src/admin/AdminCredentialsForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { useCallback, useState } from "react";
import { KeyRound, ShieldCheck } from "lucide-react";
import type { TeamBrainSession } from "../team-brain/types";
import {
hasSupabaseDefaults,
readDefaultAnonKey,
readDefaultSupabaseUrl,
} from "../team-brain/session";

interface AdminCredentialsFormProps {
onConnect: (session: TeamBrainSession) => void;
}

export const AdminCredentialsForm = ({ onConnect }: AdminCredentialsFormProps) => {
const defaultsAvailable = hasSupabaseDefaults();
const [apiKey, setApiKey] = useState("");
const [supabaseUrl, setSupabaseUrl] = useState(readDefaultSupabaseUrl());
const [anonKey, setAnonKey] = useState(readDefaultAnonKey());
const [error, setError] = useState<string | null>(null);

const handleSubmit = useCallback(
(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setError(null);

const trimmedApiKey = apiKey.trim();
const trimmedUrl = supabaseUrl.trim();
const trimmedAnon = anonKey.trim();

if (!trimmedApiKey.startsWith("tb_")) {
setError("API key must start with tb_ (crew member key from register/join).");
return;
}
if (!trimmedUrl || !trimmedAnon) {
setError("Supabase URL and anon key are required.");
return;
}

onConnect({
apiKey: trimmedApiKey,
supabaseUrl: trimmedUrl,
anonKey: trimmedAnon,
});
},
[anonKey, apiKey, onConnect, supabaseUrl],
);

return (
<div className="min-h-screen bg-surface flex items-center justify-center px-6">
<div className="w-full max-w-lg rounded-2xl border border-border-dim bg-surface-card p-6 space-y-5">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-brain-600/20 flex items-center justify-center">
<ShieldCheck className="w-5 h-5 text-brain-400" aria-hidden="true" />
</div>
<div>
<h1 className="text-lg font-semibold text-text-primary">Team Brain — Review queue</h1>
<p className="text-sm text-text-secondary">Admin-only inbox for pending memory overrides</p>
</div>
</div>

<p className="text-sm text-text-secondary leading-relaxed">
Uses the same RPC trust model as the CLI: your crew <code className="text-brain-400">tb_…</code> key
in session storage only. Never deploy real keys to GitHub Pages.
</p>

<form className="space-y-4" onSubmit={handleSubmit}>
<label className="block space-y-1.5">
<span className="text-sm font-medium text-text-primary">Admin API key</span>
<div className="relative">
<KeyRound
className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-muted"
aria-hidden="true"
/>
<input
type="password"
value={apiKey}
onChange={(event) => setApiKey(event.target.value)}
autoComplete="off"
spellCheck={false}
aria-label="Team Brain admin API key"
placeholder="tb_…"
className="w-full rounded-lg border border-border-dim bg-surface pl-10 pr-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-brain-600/40"
/>
</div>
</label>

{!defaultsAvailable && (
<>
<label className="block space-y-1.5">
<span className="text-sm font-medium text-text-primary">Supabase URL</span>
<input
type="url"
value={supabaseUrl}
onChange={(event) => setSupabaseUrl(event.target.value)}
aria-label="Supabase project URL"
placeholder="https://xxxx.supabase.co"
className="w-full rounded-lg border border-border-dim bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-brain-600/40"
/>
</label>

<label className="block space-y-1.5">
<span className="text-sm font-medium text-text-primary">Supabase anon key</span>
<input
type="password"
value={anonKey}
onChange={(event) => setAnonKey(event.target.value)}
autoComplete="off"
aria-label="Supabase anon key"
className="w-full rounded-lg border border-border-dim bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:ring-2 focus:ring-brain-600/40"
/>
</label>
</>
)}

{defaultsAvailable && (
<p className="text-xs text-text-muted">
Supabase URL/anon loaded from <code>VITE_TEAM_BRAIN_*</code> env vars.
</p>
)}

{error && (
<p role="alert" className="text-sm text-red-accent">
{error}
</p>
)}

<button
type="submit"
className="w-full rounded-lg bg-brain-600/20 text-brain-400 text-sm font-medium py-2.5 hover:bg-brain-600/30 transition-colors"
>
Connect
</button>
</form>
</div>
</div>
);
};
Loading
Loading