style: apply oxfmt to backend + docs

This commit is contained in:
Julien Calixte
2026-06-26 16:25:08 +01:00
parent de98bdeef7
commit 09238df7e3
7 changed files with 53 additions and 66 deletions

View File

@@ -13,7 +13,7 @@ Strength weights used below: **9** strong, **3** medium, **1** weak, blank none.
## 1. Goals — the WHATs ## 1. Goals — the WHATs
| ID | Goal | Weight | Source | | ID | Goal | Weight | Source |
|----|------|:------:|--------| | --- | ----------------------------------------------------------------------- | :----: | ------------ |
| G1 | Pick a Project and immediately see who's on it, as faces | 10 | user request | | 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 | | G2 | Each Member shows the correct name + Role | 8 | user request |
| G3 | Find a specific Project fast, even among hundreds | 6 | [Q3] | | G3 | Find a specific Project fast, even among hundreds | 6 | [Q3] |
@@ -24,7 +24,7 @@ Strength weights used below: **9** strong, **3** medium, **1** weak, blank none.
## 2. Functions — the HOWs ## 2. Functions — the HOWs
| ID | Function | Dir | Target (now) | Target (future) | | ID | Function | Dir | Target (now) | Target (future) |
|----|----------|:---:|--------------|-----------------| | --- | ----------------------------------------------------------- | :-: | -------------------------------------- | ------------------------ |
| F1 | Resolve a Project's Members from Napta (real + active only) | → | correct set | — | | 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 | | 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 | — | | F3 | Match Members → Slack Avatars | ↑ | in-memory vs cached directory | — |
@@ -72,7 +72,7 @@ Strength weights used below: **9** strong, **3** medium, **1** weak, blank none.
## 7. Critical performance budget ## 7. Critical performance budget
| Rank | Function | Target | Watched on | If we miss it | | 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 | | 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 | | 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 | | 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 |
@@ -81,7 +81,7 @@ Strength weights used below: **9** strong, **3** medium, **1** weak, blank none.
## 8. Tradeoffs — Got / Paid / ADR ## 8. Tradeoffs — Got / Paid / ADR
| ID | Tradeoff | 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) | | 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 | — | | 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 | — | | T3 | HTTP Basic Auth over SSO | strangers kept out with ~zero build | one shared credential; no per-user identity/audit; easily swapped later | — |

View File

@@ -41,7 +41,7 @@ cd backend && pnpm typecheck
Set on the Coolify app (or in `backend/.env` locally — see `backend/.env.example`): Set on the Coolify app (or in `backend/.env` locally — see `backend/.env.example`):
| Var | Purpose | | Var | Purpose |
|---|---| | ----------------- | --------------------------------------------------------- |
| `NAPTA_API_TOKEN` | Napta API token (lists projects + their people) | | `NAPTA_API_TOKEN` | Napta API token (lists projects + their people) |
| `NAPTA_BASE_URL` | Napta API base (default `https://app.napta.io/api/v1`) | | `NAPTA_BASE_URL` | Napta API base (default `https://app.napta.io/api/v1`) |
| `SLACK_BOT_TOKEN` | Slack bot token, scopes `users:read` + `users:read.email` | | `SLACK_BOT_TOKEN` | Slack bot token, scopes `users:read` + `users:read.email` |

View File

@@ -29,9 +29,9 @@ export interface DirectoryEntry {
} }
export function getDirectoryRefreshedAt(): number | null { export function getDirectoryRefreshedAt(): number | null {
const row = db const row = db.prepare("SELECT refreshed_at FROM slack_directory_meta WHERE id = 1").get() as
.prepare("SELECT refreshed_at FROM slack_directory_meta WHERE id = 1") | { refreshed_at: number }
.get() as { refreshed_at: number } | undefined | undefined
return row ? row.refreshed_at : null return row ? row.refreshed_at : null
} }
@@ -56,9 +56,11 @@ export function replaceDirectory(entries: DirectoryEntry[]): void {
} }
export function getDirectoryMap(): Map<string, DirectoryEntry> { export function getDirectoryMap(): Map<string, DirectoryEntry> {
const rows = db const rows = db.prepare("SELECT email, image_url, slack_id FROM slack_directory").all() as {
.prepare("SELECT email, image_url, slack_id FROM slack_directory") email: string
.all() as { email: string; image_url: string | null; slack_id: string }[] image_url: string | null
slack_id: string
}[]
const map = new Map<string, DirectoryEntry>() const map = new Map<string, DirectoryEntry>()
for (const r of rows) { for (const r of rows) {
map.set(r.email, { email: r.email, imageUrl: r.image_url, slackId: r.slack_id }) map.set(r.email, { email: r.email, imageUrl: r.image_url, slackId: r.slack_id })

View File

@@ -10,12 +10,7 @@ export const fixtureProjects: Project[] = [
{ id: "demo-zephyr", name: "Zephyr Mobile App", clientName: "Globex" }, { id: "demo-zephyr", name: "Zephyr Mobile App", clientName: "Globex" },
] ]
function member( function member(id: string, firstName: string, lastName: string, role: string): Member {
id: string,
firstName: string,
lastName: string,
role: string,
): Member {
return { return {
id, id,
firstName, firstName,

View File

@@ -35,9 +35,7 @@ app.get("/api/projects/:id/members", async (c) => {
return c.json({ source: "fixture", slack: "unconfigured", members: fixtureMembers(id) }) return c.json({ source: "fixture", slack: "unconfigured", members: fixtureMembers(id) })
} }
try { try {
const { members, slack } = await enrichWithSlackAvatars( const { members, slack } = await enrichWithSlackAvatars(await fetchNaptaProjectMembers(id))
await fetchNaptaProjectMembers(id),
)
return c.json({ source: "napta", slack, members }) return c.json({ source: "napta", slack, members })
} catch (err) { } catch (err) {
console.error("fetchNaptaProjectMembers failed:", err) console.error("fetchNaptaProjectMembers failed:", err)
@@ -57,7 +55,5 @@ app.post("/api/slack/refresh", async (c) => {
} }
}) })
console.log( console.log(`photofetch backend listening on :${config.port} (napta: ${hasNaptaCredentials})`)
`photofetch backend listening on :${config.port} (napta: ${hasNaptaCredentials})`,
)
serve({ fetch: app.fetch, port: config.port }) serve({ fetch: app.fetch, port: config.port })

View File

@@ -65,11 +65,7 @@ interface QueryOpts {
pageSize?: number pageSize?: number
} }
async function naptaGetPage( async function naptaGetPage(path: string, opts: QueryOpts, page: number): Promise<JsonApiResponse> {
path: string,
opts: QueryOpts,
page: number,
): Promise<JsonApiResponse> {
const token = await getAccessToken() const token = await getAccessToken()
const url = new URL(`${config.naptaBaseUrl}${path}`) const url = new URL(`${config.naptaBaseUrl}${path}`)
// flask-rest-jsonapi: ?filter=[{"name","op","val"}], page[size], page[number], include. // flask-rest-jsonapi: ?filter=[{"name","op","val"}], page[size], page[number], include.
@@ -198,7 +194,6 @@ export async function fetchNaptaProjectMembers(projectId: string): Promise<Membe
} }
return [...byUserId.values()].sort( return [...byUserId.values()].sort(
(a, b) => (a, b) => a.lastName.localeCompare(b.lastName) || a.firstName.localeCompare(b.firstName),
a.lastName.localeCompare(b.lastName) || a.firstName.localeCompare(b.firstName),
) )
} }

View File

@@ -52,8 +52,7 @@ export async function refreshDirectory(): Promise<number> {
if (u.deleted || u.is_bot || u.id === "USLACKBOT") continue if (u.deleted || u.is_bot || u.id === "USLACKBOT") continue
const email = u.profile?.email const email = u.profile?.email
if (!email) continue if (!email) continue
const imageUrl = const imageUrl = u.profile?.image_512 || u.profile?.image_192 || u.profile?.image_72 || null
u.profile?.image_512 || u.profile?.image_192 || u.profile?.image_72 || null
entries.push({ email, imageUrl, slackId: u.id }) entries.push({ email, imageUrl, slackId: u.id })
} }
cursor = page.response_metadata?.next_cursor || undefined cursor = page.response_metadata?.next_cursor || undefined