mirror of
https://github.com/FoggedLens/deflock.git
synced 2026-08-18 08:27:13 +02:00
59 lines
3.8 KiB
Markdown
59 lines
3.8 KiB
Markdown
# api/
|
|
|
|
Fastify server run directly on Bun (no build step — Bun executes the TypeScript as-is).
|
|
Deployed as a long-running process on a VPS via systemd (see `.github/workflows/`), **not**
|
|
serverless. That's a load-bearing fact for how background work is structured below.
|
|
|
|
Handles everything for DeFlock that isn't OpenStreetMap map data: geocoding proxy
|
|
(Nominatim), GitHub sponsors, and the contact form.
|
|
|
|
## Conventions
|
|
|
|
- No ORM/DB. Every external service gets its own thin client class in `services/` that talks
|
|
to that service's HTTP API directly via native `fetch` (or, where the service already has an
|
|
official SDK in use elsewhere in this org, that SDK — e.g. `AiScreeningClient` uses the
|
|
`openai` package). See `ZammadClient`, `NominatimClient`, `GithubClient`, `TurnstileClient`.
|
|
- Config/data that isn't a secret is checked into git and read at startup, not hardcoded and
|
|
not fetched at runtime — e.g. `data/zipcodes-us.json`, `prompts/contact-screening.md`, and
|
|
`kb/*.md`. Editing these requires a server restart to take effect (loaded once at module
|
|
init).
|
|
- OpenTelemetry (`telemetry.ts`) exports logs/metrics to Grafana Cloud via a local otelcol
|
|
sidecar. Errors are bucketed by a substring-matching `classifyErrorMessage()` in `server.ts`
|
|
— new upstream integrations should extend that rather than inventing a separate error-typing
|
|
scheme.
|
|
- Tests use `bun:test`. Prefer dependency injection (pass a stub client/SDK instance into the
|
|
constructor or function) over mocking `global.fetch` when the thing being tested isn't
|
|
itself an HTTP client — see `ContactScreeningService.test.ts` vs `ZammadClient.test.ts`.
|
|
|
|
## Contact form + AI screening
|
|
|
|
**Intent**: the volunteer support team gets more contact-form messages than they can
|
|
individually triage. Every submission that passes Cloudflare Turnstile gets a first pass from
|
|
an AI classifier before a human sees it — unless the sender explicitly opts out via a
|
|
checkbox on the form. The AI never sends anything to a customer directly; it only drafts,
|
|
tags, and prioritizes for a human to review in Zammad.
|
|
|
|
**Ordering matters**: the Zammad ticket is always created synchronously and the user gets
|
|
their success response immediately. AI screening then runs as fire-and-forget background work
|
|
*after* the response is sent — it relies on this process staying alive to finish, which is
|
|
only safe because this is a persistent server, not a serverless function. A screening failure
|
|
(OpenAI down, bad response, Zammad write failure) never blocks ticket creation or crashes the
|
|
process — it just gets tagged/logged and a human handles that ticket without AI assistance,
|
|
same as before this feature existed.
|
|
|
|
**Where the classification logic lives**: the taxonomy, tone, and per-category instructions
|
|
are entirely in `prompts/contact-screening.md` (checked into git, not in TypeScript) so
|
|
non-engineers can review/iterate on classifier behavior like any other reviewed change. Code
|
|
(`ContactScreeningService.ts`) only translates the AI's structured output into Zammad actions
|
|
(tags, priority, group, shared draft vs. internal note, scheduled close) — it deliberately
|
|
does *not* contain classification judgment calls itself, except for hard safety backstops
|
|
(e.g. force-tagging) that shouldn't depend on the model remembering a rule. When product
|
|
behavior changes (new category, new disposition), expect to change the prompt first and the
|
|
plan/orchestration code second — they're meant to stay in sync but are two different kinds of
|
|
change (policy vs. mechanism).
|
|
|
|
**Dry-run endpoint**: `POST /contact/message/dry-run` runs the same classifier + planning
|
|
logic with no Turnstile check and no Zammad writes, for testing prompt/taxonomy changes
|
|
quickly. It has no auth — fine for local dev, but don't expose it publicly without adding
|
|
some, since it's a free OpenAI-call relay otherwise.
|