# Project Manager — web Vue 3 + TypeScript + Vite PWA. Talks to the REST API in the parent directory. ## Develop ```bash npm install npm run dev # http://localhost:5173 ``` The dev server proxies `/api` to `http://localhost:8080` (the Dockerised API — run `docker compose up -d` in the parent directory first). Override the target with `VITE_PROXY_TARGET`, or point the app at a different API entirely with `VITE_API_BASE_URL` (see [.env.example](.env.example)). ## Build ```bash npm run build # type-checks, then emits dist/ npm run preview ``` The parent `Dockerfile` runs this build in a Node stage and copies `dist/` into the PHP image's `public/`, so the `app` container serves the compiled SPA at `/`. There is no separate frontend container — a production image is `docker compose build app` from the parent directory. ## Layout ``` src/main.ts App bootstrap; resolves the stored session before mount src/router/index.ts Routes + guard (redirects to /login when unauthenticated) src/stores/auth.ts Pinia store: token in localStorage, magic-link + fetchMe src/stores/projects.ts Pinia store: the user's projects (fetch + create) src/stores/cards.ts Pinia store: one project's cards (CRUD; no reordering -- see lib/cardOrder.ts) src/stores/inbox.ts Pinia store: the caller's global inbox (fetch + create) src/lib/api.ts fetch wrapper, bearer token, typed ApiError src/lib/cardOrder.ts reorderColumn() -- PUT /api/cards/order, shared by the sidebar and kanban board src/lib/webauthn.ts base64url <-> ArrayBuffer + the register/login passkey ceremonies src/components/AppSidebar.vue left nav: Dashboard link, project dropdown, Inbox + form src/components/CardRow.vue editable text + status chip + delete, one card src/components/KanbanCard.vue small draggable card for the board columns and the inbox src/components/PasskeyNotice.vue dismissible "add a passkey" banner across the top of the page src/views/ DashboardView, ProjectView, LoginView, ProfileView, VerifyEmailView ``` Signed-in "app" routes (`meta.requiresAuth`) render inside a persistent shell: the top bar, then a left **sidebar** (`AppSidebar.vue`) beside the routed view. The sidebar stays mounted across navigation — it holds a **Dashboard** link, a divider, a project `