Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
1b45c50
docs(people): add People area handoff docs and prototype
claude Oct 14, 2026
48bd91f
feat(people): add People permissions, restricted access and sample data
claude Oct 14, 2026
7dd9cb4
feat(people): add People navigation, routes and agent-mode API
claude Oct 14, 2026
c12724a
feat(people): add People overview and person record
claude Oct 14, 2026
561f9c7
feat(people): add the member register
claude Oct 14, 2026
13171de
feat(people): add the admins list and admin role actions
claude Oct 14, 2026
8e00f6f
feat(people): add teams, team pages and team membership actions
claude Oct 14, 2026
fb6e158
feat(people): add census campaigns and the responses import wizard
claude Oct 14, 2026
78f5cc9
feat(people): add the linking queue and pending invites
claude Oct 14, 2026
8f4765f
feat(people): add the Capo Admin page
claude Oct 14, 2026
299052f
feat(people): add My area for volunteers and members
claude Oct 14, 2026
efc0238
docs(people): document the People area and finish the handoff notes
claude Oct 14, 2026
0e8578a
Merge branch 'main' into claude/eager-mayer-bsom7v
claude Oct 14, 2026
dde8f3a
fix(people): resolve UI and access audit findings
viganogabriele Oct 14, 2026
feb7f0d
refactor(ui): restrict audit changes to dashboard visuals
viganogabriele Oct 14, 2026
2fd756a
Merge branch 'main' into claude/eager-mayer-bsom7v
Diubii Oct 15, 2026
bd21599
fix(people): close the gaps from the PR security review
claude Oct 15, 2026
7ffc364
test: cover the People access fixes from the security review
Diubii Oct 15, 2026
7c20fa8
fix(people): keep member details behind people:members:read
claude Oct 15, 2026
782d54e
fix(ui): keep record and alert actions off the text on phones
claude Oct 15, 2026
51cfd1c
fix(people): keep the import stepper on one line on phones
claude Oct 15, 2026
075893c
fix(ui): stop narrow cards from breaking values and titles
claude Oct 15, 2026
99dc0ea
fix(people): fit the member register filters on phones
claude Oct 15, 2026
2431dc5
fix(people): wrap Capo Admin request buttons under the request on phones
claude Oct 15, 2026
e1f8bce
fix(people): show the course under every name in team tables
claude Oct 15, 2026
d19af1f
fix(people): stack the census card buttons on phones
claude Oct 15, 2026
b3e1ed4
feat(people): integrate People with the current dashboard and eSign
viganogabriele Oct 17, 2026
9ac2305
fix(people): enforce scoped access and bounded backend inputs
viganogabriele Oct 18, 2026
0ae60cb
refactor(people): align controls and navigation with readable actions
viganogabriele Oct 18, 2026
9407d8b
fix(people): open the permitted account section from the rail
viganogabriele Oct 18, 2026
d902153
fix(people): address the CodeRabbit review
claude Oct 18, 2026
643e70f
Merge remote-tracking branch 'origin/claude/eager-mayer-bsom7v' into …
claude Oct 18, 2026
fcd4fea
fix(people): reject invalid or repeated member admissions
viganogabriele Oct 18, 2026
b270fcf
fix(people): align People with the backend people router
viganogabriele Oct 18, 2026
b4c6883
fix(people): validate invite tokens in agent mode and reactivate form…
claude Oct 18, 2026
1cdb89b
Merge remote-tracking branch 'origin/main' into claude/eager-mayer-bs…
viganogabriele Oct 21, 2026
604fd34
fix(people): hide former team members' contacts from Leads
claude Oct 21, 2026
891a2b9
fix(people): restrict team lead contact visibility
viganogabriele Oct 21, 2026
c4e4382
Merge remote-tracking branch 'origin/claude/eager-mayer-bsom7v' into …
viganogabriele Oct 21, 2026
391ddee
docs(people): complete procedure contract entries
viganogabriele Oct 21, 2026
3716244
Merge remote-tracking branch 'origin/main' into claude/eager-mayer-bs…
lorenzocorallo Oct 22, 2026
489873b
Merge branch 'main' into claude/eager-mayer-bsom7v
lorenzocorallo Oct 22, 2026
0d21a0a
Merge branch 'main' into claude/eager-mayer-bsom7v
lorenzocorallo Oct 22, 2026
3a5b42b
fix(people): show pending census group removals
lorenzocorallo Oct 22, 2026
cc475c9
fix(people): give pending Telegram admin removals their own discrepan…
claude Oct 22, 2026
646c68c
fix(people): mirror pending removal privacy in agent mode
lorenzocorallo Oct 22, 2026
e6c0756
fix(people): integrate the dedicated admin removal discrepancy kind
lorenzocorallo Oct 22, 2026
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
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ What the app is and how to run it: [`README.md`](README.md). UI rules: [`docs/de
`AGENT_MODE`. List groups with `azure:groups:read`; add or remove existing directory users in a group with
`azure:groups:write`; the add picker lists the directory, so it is disabled without `azure:members:read`. Nothing
else about users or groups can be edited from the dashboard.
- **People** (`src/features/people/`): every page reads and writes through `peopleApi(context)`, which parses the
backend's `people.*` responses with `people.contract.ts` and, in agent mode, uses the in-memory fake
(`AGENT_PERMISSIONS`, `AGENT_PERSON` pick the viewer). `/dashboard` admits `people:self` without `admin:access`;
Capo Admin and Team Lead come from the person's own record (`peopleRoles` in the route context) and only open their
sections. Handoff notes, decisions and the plan: `docs/people/`.
- **Shared UI**: `src/components/shell`, `src/components/primitives`, `src/components/ui`.

## Conventions
Expand Down
39 changes: 24 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ group membership management. Directory access requires `azure:members:read`; cre
`azure:members:create`, and changing member IDs `azure:members:write`. Groups require `azure:groups:read`; adding and
removing members `azure:groups:write`.

**People** keeps one record per volunteer and member (Overview, Member register, Admins, Teams, Census, Linking), plus
Telegram › Capo Admin and Account › My area for volunteers themselves. The dashboard talks to a typed `people.*`
contract (`src/features/people/people.contract.ts`, documented in `docs/people/contract.md`); until the backend
implements it, agent mode serves sample data from `people.fake.server.ts`. Permissions: `people:read`, `people:write`,
`people:members:read`, `people:members:write`, `people:appoint`, and `people:self`, which alone admits non-admins to My
area and, by the role on their record, Capo Admin or their team. See `docs/people/README.md`.

Built with TanStack Start (React 19), Vite+, Nitro, Tailwind CSS v4 and shadcn/Base UI. The server is an OIDC BFF:
only it holds the IdP access/refresh/ID tokens. Browsers hold an opaque, HttpOnly session cookie; sessions live in a
**dedicated Redis**, separate from the shared backend Redis. Backend requests carry a user bearer token, and permissions
Expand All @@ -24,21 +31,23 @@ Signing in as a real user needs a development client registered in the IdP.

Set the following runtime variables (the build does not need secrets):

| Variable | Meaning |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `APP_URL` | Dashboard's public origin. Required outside development; HTTPS in production. Development defaults to `http://localhost:$PORT` (3001). Used for exact Origin CSRF validation and callback/logout URLs. |
| `BACKEND_URL` | tRPC backend origin; use the in-cluster service address in production. Defaults to `http://localhost:3000` in development. |
| `IDP_URL` | Public IdP origin; defaults to `https://auth.polinetwork.org`. Issuer is `$IDP_URL/api/auth`. Browser redirects retain this public origin. |
| `IDP_INTERNAL_URL` | Optional internal IdP origin, e.g. `http://auth-service.auth.svc.cluster.local:3000`. Rewrites discovery, token, JWKS and revocation transport only; issuer, JWT assertion audiences (public token/revoke endpoints) and browser URLs remain public. |
| `OIDC_CLIENT_ID` | Registered confidential dashboard client, with authorization code and refresh grants, scopes `openid profile email offline_access backend:admin`, and backend resource link. |
| `OIDC_CLIENT_PRIVATE_JWK` | Private signing JWK JSON with `kid` and `alg` (`EdDSA`/Ed25519 or `ES256`/P-256); inject from this service's Key Vault. Register only its public JWKS in the IdP. |
| `OIDC_CLIENT_SECRET` | Alternative `client_secret_basic` credential; set exactly one client credential mode. |
| `BACKEND_RESOURCE` | Backend OAuth resource/audience; defaults to `https://backend.internal.polinetwork.org`. Sent on every authorize, code exchange and refresh request. |
| `REDIS_URL` | Dedicated dashboard Redis connection. Required outside development; development defaults to `redis://localhost:6379`. Never point to shared backend Redis. |
| `SESSION_SECRET` | Independent random secret of at least 32 characters, encrypting the 10-minute state/nonce/PKCE cookie. Rotation invalidates pending login attempts. |
| `AGENT_MODE` | Fake all-permission user in development only. Backend must be a local fake or local legacy test instance. Production ignores this mode. |
| `PORT` | Local listening port (3001 by default); use a port ≥10000 for agent previews. |
| `TEST_REDIS_URL` | Opt-in disposable Redis for integration tests. Tests create/delete only UUID session keys. |
| Variable | Meaning |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `APP_URL` | Dashboard's public origin. Required outside development; HTTPS in production. Development defaults to `http://localhost:$PORT` (3001). Used for exact Origin CSRF validation and callback/logout URLs. |
| `BACKEND_URL` | tRPC backend origin; use the in-cluster service address in production. Defaults to `http://localhost:3000` in development. |
| `IDP_URL` | Public IdP origin; defaults to `https://auth.polinetwork.org`. Issuer is `$IDP_URL/api/auth`. Browser redirects retain this public origin. |
| `IDP_INTERNAL_URL` | Optional internal IdP origin, e.g. `http://auth-service.auth.svc.cluster.local:3000`. Rewrites discovery, token, JWKS and revocation transport only; issuer, JWT assertion audiences (public token/revoke endpoints) and browser URLs remain public. |
| `OIDC_CLIENT_ID` | Registered confidential dashboard client, with authorization code and refresh grants, scopes `openid profile email offline_access backend:admin`, and backend resource link. |
| `OIDC_CLIENT_PRIVATE_JWK` | Private signing JWK JSON with `kid` and `alg` (`EdDSA`/Ed25519 or `ES256`/P-256); inject from this service's Key Vault. Register only its public JWKS in the IdP. |
| `OIDC_CLIENT_SECRET` | Alternative `client_secret_basic` credential; set exactly one client credential mode. |
| `BACKEND_RESOURCE` | Backend OAuth resource/audience; defaults to `https://backend.internal.polinetwork.org`. Sent on every authorize, code exchange and refresh request. |
| `REDIS_URL` | Dedicated dashboard Redis connection. Required outside development; development defaults to `redis://localhost:6379`. Never point to shared backend Redis. |
| `SESSION_SECRET` | Independent random secret of at least 32 characters, encrypting the 10-minute state/nonce/PKCE cookie. Rotation invalidates pending login attempts. |
| `AGENT_PERMISSIONS` | Development only. Comma-separated permissions for the agent-mode user (default: all). `people:self` alone previews the restricted People view. |
| `AGENT_PERSON` | Development only. The agent-mode user's People record (default `P-0007`, Direttivo). `P-0098` is a Capo Admin, `P-0034` the IT Team Lead, `P-0115` an IT member; `match:P-0115` previews the first sign-in, `none` a sign-in without a record. Sending an invite prints its link in the dev server's console. |
| `AGENT_MODE` | Fake all-permission user in development only. Backend must be a local fake or local legacy test instance. Production ignores this mode. |
| `PORT` | Local listening port (3001 by default); use a port ≥10000 for agent previews. |
| `TEST_REDIS_URL` | Opt-in disposable Redis for integration tests. Tests create/delete only UUID session keys. |

Before review run `pnpm check`, `pnpm typecheck`, `pnpm test` and `pnpm build`. Run real Redis tests with
`TEST_REDIS_URL=redis://127.0.0.1:<test-port> pnpm test`. The OIDC integration test uses a local fake provider with
Expand Down
29 changes: 27 additions & 2 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,9 @@ the `h1`.
**Small screens** (below 760px). Rail and panel leave the layout. A 56px top bar holds the logo (Overview), the
service title ("PoliNetwork Admin" on Overview), Search, the theme toggle and the avatar; the service's sections follow
as horizontally scrolling pills that scroll away with the page. The current pill stays in view after navigation,
font loading and changes to the bar's width, including when it first appears below 760px. A 64px bottom nav lists the six services (current one
accent with a top bar, Reports with the open-reports dot). Section pills and bottom nav are the only navigation; there
font loading and changes to the bar's width, including when it first appears below 760px. A 64px bottom nav lists the visible services (current one
accent with a top bar, Reports with the open-reports dot). The device's bottom safe area adds space below the 64px
bar. Below 360px, WhatsApp uses "WA" so all labels remain readable. Section pills and bottom nav are the only navigation; there
is no sheet.

**Command palette** (`⌘K`/`Ctrl+K`, the rail and top bar Search, the Overview search): sections, "Find “…” in
Expand Down Expand Up @@ -350,6 +351,10 @@ shows the hover line with a text cursor, and gets the normal border and ring whe
`aria-label` in place of the visible label. Use it only for a value edited in place inside a working surface, where a
box around every value would be clutter; forms keep boxed fields.

Calendar dates use a custom picker at every viewport, display the same day–month–year format as read-only dates,
and retain ISO values for the server. Month and year selectors are custom controls; calendars start on Monday.
Grant date/time fields use the calendar and time wheels on touch devices too, with no native date/time fallback.

**Alerts and toasts.** `InlineAlert` for what persists or needs action: load errors, dialog submit errors, partial
saves. It defaults to `tone="danger"` and `role="alert"`, so pass another tone for notices that aren't errors. Toasts
(`appToast.success/info/warning/error`, never raw `toast`) confirm actions; untinted, tone on the icon only, at most
Expand Down Expand Up @@ -575,3 +580,23 @@ Rules that the templates above don't imply.
request.".
- **Account**: permissions are grouped by area (Dashboard, Telegram, WhatsApp, group labels, Microsoft 365, Web) with
the service icon.
- **People**: one record per person whether or not they have an account; Overview, Member register, Admins, Teams,
Census and Linking all open the same record (`/dashboard/people/$personId`). Account status always shows a badge,
Not linked included. Mobile list cards replace unlabelled column values with one line of role chips or status
badges in the name cell. Dialogs that need fields and are destructive (End admin role, End membership, I'm leaving)
use `FormDialog` with `tone="danger"`; plain confirmations stay `ConfirmDialog`.
- **Member register**: status segments scope the table; Number leads on desktop, the name on mobile. Add member from a
record opens the register's dialog via `?add=P-…`. Export confirms before downloading. Register rows open full
records only with `people:read`; `people:members:read` alone reads the register projection. A single row action is
an icon button; menus never repeat a row's record link.
- **Admins**: Groups, Capo Admin of reference, Member and Census never hide; the rest follow the table width. The four
quick filters scroll sideways on phones.
- **Teams**: index cards then all members; a Team Lead without `people:read` sees only their team, as the section
itself. Team-field columns come from `TEAMS` in `people.contract.ts`. Leads of several teams can open each of their own
teams; only `people:appoint` with admin access can end a Lead role.
- **Census**: answer segments become a filter menu on phones. The import wizard skips steps with nothing to settle and
saves nothing before Confirm. Reminders report sent and skipped counts after sending.
- **Linking**: Needs review is a queue (its count sits next to the section like Reports › Open); each review kind has
its own resolutions and a required reason.
- **Capo Admin** (Telegram) and **My area** (Account): self-service pages that open with `people:self` and a role on
the person's own record; no personal emails of others, only phone, Telegram and PoliNetwork addresses.
31 changes: 31 additions & 0 deletions docs/people/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# People area: handoff

Everything an agent needs to build, or continue building, the People area of the dashboard.

| File | What it is |
| ------------------------------ | ---------------------------------------------------------------------------------------- |
| [`brief.md`](brief.md) | The original brief, verbatim |
| [`decisions.md`](decisions.md) | What was settled after the brief: owner decisions, navigation, per-page rules, roles |
| [`contract.md`](contract.md) | The `people.*` backend contract the dashboard codes against |
| [`plan.md`](plan.md) | Phases, checklist and progress log: **start here to know what is done and what is next** |
| [`prototype/`](prototype/) | The clickable prototype's source (structure and copy reference only) |

## Continuing the work

1. Read `AGENTS.md` and `docs/design.md`, then `decisions.md`, `contract.md` and `plan.md` in that order.
2. Check out branch `claude/eager-mayer-bsom7v` and read the open pull request and its latest commits.
3. Pick the first unchecked item in `plan.md`. Commit small and often; tick the box and add a progress-log line in the
commit that finishes an item.
4. Run `PORT=1xxxx AGENT_MODE=true pnpm dev` to see the pages with sample data; set `AGENT_PERMISSIONS` and
`AGENT_PERSON` (once phase 1 adds them) to preview restricted roles.

## The prototype has visual errors

The prototype fixes what each page shows and does, not how it measures. Its spacing, paddings, column widths and some
components are hand-made and wrong in places. Do not copy its CSS or markup: build from the repo's shell and primitives
so spacing and behaviour match the rest of the dashboard, and fix anything that looks off. See "Prototype is a
reference" in `decisions.md` for the known defects.

Prototype canvas (owner only, all pages in desktop/mobile × light/dark): <https://claude.ai/artifact/7rwGssnepqJ2nWuLvGLrjT>

Permission matrix: [`permissions.md`](permissions.md).
Loading
Loading