style: apply oxfmt to backend + docs
This commit is contained in:
70
DESIGN.md
70
DESIGN.md
@@ -12,27 +12,27 @@ 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 |
|
||||
| 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 |
|
||||
| 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
|
||||
|
||||
@@ -57,38 +57,38 @@ Strength weights used below: **9** strong, **3** medium, **1** weak, blank none.
|
||||
- **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`
|
||||
- **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`
|
||||
- **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 |
|
||||
| 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](./docs/adr/0001-slack-directory-cache.md) |
|
||||
| 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) | — |
|
||||
| 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](./docs/adr/0001-slack-directory-cache.md) |
|
||||
| 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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user