diff --git a/AGENTS.md b/AGENTS.md index c82ffae..d4ed9d3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ Performance Review Manager — a local, single-user ("solo EM") pnpm monorepo. S ### Data / running - Local data lives in `/workspace/data` (gitignored): SQLite DB, encrypted vault, uploaded files. Delete it to reset to a clean state (stop the API first, since it holds the DB handle). -- First app load shows "Create & seed demo team" (seeds an 8-person org). After init it shows "Unlock". Default seed password: `workbench`. +- First app load offers **Empty workspace** or **Demo team** (seeds an 8-person org). After init it shows "Unlock". Default demo password: `workbench`. - Locking the workspace ("Lock & encrypt") seals the SQLite DB + files to `.enc` at rest; unlocking decrypts them. - `.session` stores the auth token only (not the AES key). After an API process restart you must unlock again even if the UI still has a token. diff --git a/README.md b/README.md index ec74a41..e156a18 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ pnpm dev | API | http://127.0.0.1:8787 | | Seed password | `workbench` | -First load: **Create & seed demo team** (8-person org), then unlock. Between sessions use **Lock & encrypt**. +First load: choose **Empty workspace** or **Demo team** (8-person org, password default `workbench`), then unlock later sessions with your password. Between sessions use **Lock & encrypt**. Requires **pnpm 10.33.3** (see [`AGENTS.md`](./AGENTS.md) if Corepack hands you pnpm 11). @@ -85,7 +85,7 @@ Regenerate with `pnpm screenshots` (UI + API running; default password `workbenc **Intent:** Keep the workspace sealed when you walk away. The unlock gate is the trust boundary for local data. -**How it works:** On first run you create a workspace password and can seed a demo org. Later sessions decrypt the vault with that password. **Lock & encrypt** re-seals SQLite and files so a stolen laptop disk is not a readable review archive. +**How it works:** On first run you create a workspace password and choose an empty vault or a seeded demo org. Later sessions decrypt the vault with that password. **Lock & encrypt** re-seals SQLite and files so a stolen laptop disk is not a readable review archive. ![Unlock workbench](./docs/screenshots/01-unlock.png) @@ -199,7 +199,7 @@ Regenerate with `pnpm screenshots` (UI + API running; default password `workbenc **Intent:** Configure providers, privacy gates, appearance, and vault maintenance in one place. -**How it works:** Set AI provider and models (Anthropic, OpenAI, Cursor gateway, Ollama), toggle egress for private notes / meetings / document extracts, manage Jira and Bitbucket credentials, run encrypted backup / restore, verify the activity log, and switch dark / light theme. +**How it works:** Set AI provider and models (Anthropic, OpenAI, Cursor gateway, Ollama), toggle egress for private notes / meetings / document extracts, manage Jira and Bitbucket credentials, run encrypted backup / restore, verify the activity log, and choose appearance (system / dark / light plus Teal, Ocean, Forest, or Slate accent). ![Settings](./docs/screenshots/14-settings.png) diff --git a/apps/ui/index.html b/apps/ui/index.html index 2e61709..740b6e3 100644 --- a/apps/ui/index.html +++ b/apps/ui/index.html @@ -1,26 +1,44 @@ - + - + Performance Review Manager - +
diff --git a/apps/ui/src/App.tsx b/apps/ui/src/App.tsx index e84d86a..02bb262 100644 --- a/apps/ui/src/App.tsx +++ b/apps/ui/src/App.tsx @@ -1,4 +1,4 @@ -import { NavLink, Navigate, Route, Routes } from "react-router-dom"; +import { NavLink, Navigate, Route, Routes, useLocation } from "react-router-dom"; import { useEffect, useState } from "react"; import type { WorkspaceStatus } from "@prm/shared"; import { api, getToken, setToken, waitForApi } from "./lib/api"; @@ -20,10 +20,60 @@ import { SprintPulsePage } from "./pages/SprintPulsePage"; import { SearchPage } from "./pages/SearchPage"; import { BackfillPage } from "./pages/BackfillPage"; +const NAV = [ + { + label: "Work", + links: [ + { to: "/", end: true, label: "Home" }, + { to: "/team", label: "Team" }, + { to: "/roles", label: "Roles" }, + { to: "/cycles", label: "Cycles" }, + { to: "/promotions", label: "Promotions" }, + ], + }, + { + label: "Review", + links: [ + { to: "/consistency", label: "Consistency" }, + { to: "/team-performance", label: "Team performance" }, + { to: "/sprint-pulse", label: "Sprint pulse" }, + { to: "/search", label: "Search" }, + { to: "/backfill", label: "Backfill" }, + ], + }, + { + label: "Tools", + links: [ + { to: "/chat", label: "AI Chat" }, + { to: "/settings", label: "Settings" }, + ], + }, +] as const; + export function App() { const [status, setStatus] = useState(null); const [error, setError] = useState(null); - const { theme, toggle } = useTheme(); + const [navOpen, setNavOpen] = useState(false); + const { resolved, toggle } = useTheme(); + const location = useLocation(); + + useEffect(() => { + setNavOpen(false); + }, [location.pathname]); + + useEffect(() => { + if (!navOpen) return; + function onKey(e: KeyboardEvent) { + if (e.key === "Escape") setNavOpen(false); + } + const prev = document.body.style.overflow; + document.body.style.overflow = "hidden"; + window.addEventListener("keydown", onKey); + return () => { + document.body.style.overflow = prev; + window.removeEventListener("keydown", onKey); + }; + }, [navOpen]); async function refresh() { try { @@ -45,10 +95,10 @@ export function App() { type="button" className="btn ghost theme-toggle" onClick={toggle} - aria-label={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"} - title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"} + aria-label={resolved === "dark" ? "Switch to light mode" : "Switch to dark mode"} + title={resolved === "dark" ? "Switch to light mode" : "Switch to dark mode"} > - {theme === "dark" ? "Light mode" : "Dark mode"} + {resolved === "dark" ? "Light mode" : "Dark mode"} ); @@ -57,7 +107,6 @@ export function App() { const logPath = typeof window !== "undefined" ? window.__PRM_DESKTOP_LOG__ : undefined; const apiBase = typeof window !== "undefined" ? window.__PRM_API_BASE_URL__ : undefined; async function retryDesktop() { - // Prefer same-origin UI served by the sidecar once it is healthy. const base = (apiBase || "http://127.0.0.1:8787").replace(/\/$/, ""); try { const res = await fetch(`${base}/api/health`); @@ -90,7 +139,7 @@ export function App() { )}

{error}

-
{themeToggle}
+
{themeToggle}
); @@ -110,26 +159,68 @@ export function App() { } return ( -
-