Files
photofetch/DESIGN.md
2026-06-26 16:25:08 +01:00

10 KiB

photofetch — Design (QFD)

How photofetch turns "pick a Project, see its team as faces" into engineering functions and components. Vocabulary is defined in CONTEXT.md; the Slack-matching decision is recorded in ADR-0001. This is the tree-only QFD variant (cascade + budget + tradeoffs) — the goal/function counts don't justify the upkeep of full QFD matrices.

Strength weights used below: 9 strong, 3 medium, 1 weak, blank none.


1. Goals — the WHATs

ID Goal Weight Source
G1 Pick a Project and immediately see who's on it, as faces 10 user request
G2 Each Member shows the correct name + Role 8 user request
G3 Find a specific Project fast, even among hundreds 6 [Q3]
G4 Stay useful when Slack is down or a Member has no Slack 6 [Q6]
G5 Keep the roster private to the team 7 [Q7]
G6 Select several Projects, see their teams grouped, and export the photos 8 user request

2. Functions — the HOWs

ID Function Dir Target (now) Target (future)
F1 Resolve a Project's Members from Napta (real + active only) correct set
F2 Render a Project's grid end-to-end ≤ 2 s p95 (warm cache) ≤ 1 s
F3 Match Members → Slack Avatars in-memory vs cached directory
F4 Authenticate to Napta (Auth0 M2M) cached JWT, refresh on 401/expiry
F5 Keep the Slack directory cache fresh 30-day TTL + manual Refresh
F6 Filter/search the Project picker (multi-select) instant on ≤ few hundred (client-side) server-side if thousands
F7 Gate access to the whole site Basic Auth at nginx SSO
F8 Build the Export zip (folder per Project Group) ≤ ~15 s for a few hundred photos stream + progress

3. Cascade — Goals → Functions → How → Components

  • G1 Pick a Project, see its team (W10)
    • F1 Resolve Members from Napta
      • How: GET /user_project?filter[project_id] (drop simulated) → resolve user_ids → GET /user (drop inactive) → resolve user_position_id → Role
        • Component: backend/src/napta.ts (client + token + queries)
    • F4 Authenticate to Napta
      • How: client-credentials → JWT at auth.napta.io/oauth/token (aud backend), cache until expiry, refresh on 401
        • Component: backend/src/napta.ts (token cache)
    • F2 Render the grid
      • How: one backend call GET /api/projects/:id/members returns ready-to-render Members; SPA shows loading skeleton → grid
        • Component: src/App.vue, src/components/MemberGrid.vue, MemberCard.vue
  • G2 Correct name + Role (W8)
    • F1 (Role = Napta Position) — see above
    • F3 Match Members → Avatars
      • How: users.list sweep → email → {imageUrl, slackId} directory in SQLite; match each Member's email in memory; unmatched → initials Avatar
        • Component: backend/src/slack.ts, backend/src/db.ts (directory table)
  • G3 Find a Project fast (W6)
    • F6 Client-side searchable picker
      • How: fetch all non-archived Projects once; typeahead filters by name + client
        • Component: src/components/ProjectPicker.vue
  • G4 Useful when Slack is down / no match (W6)
    • F7→degrade: if the directory sweep fails, return Members anyway with slackMatched:false + a slackError flag; UI shows initials + dismissible warning
      • Component: backend/src/slack.ts, MemberGrid.vue
  • G5 Private to the team (W7)
    • F7 Basic Auth at the edge
      • How: nginx auth_basic over the whole site incl. /api; htpasswd generated at container start from BASIC_AUTH_USER/BASIC_AUTH_PASSWORD; no-op when unset (local dev)
        • Component: nginx.conf, web image entrypoint
  • G6 Multi-select + grouped view + photo Export (W8)
    • F6 Multi-select searchable picker; chosen Projects render as Project Groups (a Person on several appears in each)
      • Component: src/components/ProjectPicker.vue, src/App.vue, ProjectGroup.vue
    • F8 Export zip
      • How: POST /api/export {projectIds[]} → per Project Group fetch Members, fetch each Avatar's bytes (dedup within the export, bounded concurrency), write <project>/<lastname-firstname>.<ext>; unmatched Members get a generated initials SVG; stream the zip (fflate)
        • Component: backend/src/export.ts, backend/src/avatar.ts (initials SVG), backend/src/index.ts

7. Critical performance budget

Rank Function Target Watched on If we miss it
1 F2 grid render ≤ 2 s p95 (warm) backend request logs parallelize Napta calls; cache Positions table; batch user fetch by id
2 F5 directory sweep ≤ ~10 s for full workspace sweep duration log paginate + serve stale snapshot while refreshing in background
3 F1 Member resolution correct set, not slow path spot-check vs Napta UI add staffed_days > 0 filter if "assigned but never staffed" noise appears
4 F8 Export build ≤ ~15 s, a few hundred photos export duration log bounded-concurrency avatar fetch; dedup repeated avatars; cap selection size with a warning

8. Tradeoffs — Got / Paid / ADR

ID Tradeoff Got Paid ADR
T1 users.list whole-workspace sweep over per-email lookup few Slack calls, rate-limit-safe, in-memory matching fetch entire directory; up to 30-day staleness (mitigated by Refresh) ADR-0001
T2 Napta Auth0 M2M cached JWT over a static token matches Napta's real auth; survives token expiry token-exchange code + refresh-on-401
T3 HTTP Basic Auth over SSO strangers kept out with ~zero build one shared credential; no per-user identity/audit; easily swapped later
T4 Email as the only Napta↔Slack join key one reliable key, no fuzzy matching Members whose Slack email differs go unmatched (shown with initials)
T5 Display-only grid for v1 ships the core ask fastest no click-to-Slack / copy-email yet
T6 Initials Avatars exported as SVG, not rasterized PNG zero native deps, clean Alpine image, scalable mixed extensions in folders; SVG unsuitable where only raster embeds
T7 Server-side zip (fflate), built in memory no Slack-CDN CORS, has avatar bytes; simple whole zip held in memory; large Selections need streaming (F8 future)

Tensions being watched (unresolved by design)

  • Project count. Client-side search assumes ≤ a few hundred active Projects. Trigger to revisit: the all-Projects fetch gets slow or Napta paginates it past one page → move search server-side (F6 future).

9. Inconsistencies spotted and fixed

  • Static token assumption. Scaffold used NAPTA_API_TOKEN; Napta actually uses Auth0 M2M client-credentials. → Config becomes NAPTA_CLIENT_ID + NAPTA_CLIENT_SECRET with a token-exchange step.
  • Wrong cache shape. Scaffold db.ts modeled a per-email avatar cache (email → url, null = no-match), which fits lookupByEmail, not the chosen users.list sweep. → Reworked to a directory snapshot table + a refreshed_at timestamp.
  • Host port publishing. docker-compose.yml published 80/8000, which collided on the Coolify host and failed the first deploy. → Switched to expose: (already fixed and deployed).

How to keep this honest

  • New ADR lands → reference it from §8 and (if matrices ever added) §6.
  • Spike/measurement returns numbers → update §7 Target / Watched on.
  • WHATs (§1) change rarely; HOWs (§2) change per release; revisit §3 cascade when either side moves.
  • Delete any section that becomes empty — empty sections lie.