getJwtContext returns nil when missing/wrong type; requireJwtContext
aborts with 500 at every non-optional call site so handlers fail closed
without panic. NewCoalesceOutbound returns (*CoalesceOutbound, error)
instead of panicking on nil writeAsync. Tracks the cleanup design doc.
This program implements **all** recommended operations from a prior architecture review. openfsd’s layout is settled (`pkg/*`, `internal/*`, single binary `cmd/openfsd`). Sweatbox, airport editor, and `pkg/twrfiles` have landed. Remaining cleanup is operational debt that still taxes everyday changes:
1.**Hygiene enforcement is broken** (script exits 0 on panic hits) and **two real panics** remain in non-test library code.
2.**CI does not run** hygiene, import-graph, or gofmt despite AGENTS.md §7.
3.**Classic FSD** (`handleConn` / `eventLoop` / accept loop) is still a dual path gated by `Deps.Listen != nil || ForceClassicFSD`, even though production and `StartTestServer` use gnet.
4.**Design docs** for sweatbox and airport editor still say **Draft** with stale “current state” tables.
5.**Public datafeed** invents pilot/military ratings and QNH, and never exports flight plans that sessions already hold.
The work is split into independently mergeable PRs. Classic FSD removal is the riskiest slice and is isolated after hygiene/CI are green so regressions are easier to diagnose.
---
## Goals & Non-Goals
### Goals
1. Make `scripts/check-hygiene.sh`**exit non-zero** when it prints violations; wire it into CI.
2.**Eliminate all `panic(`** in non-test `pkg/` and `internal/` code (today: `getJwtContext`, `NewCoalesceOutbound`) **and** ensure JWT handlers fail closed without nil-deref panics (all non-optional call sites use `requireJwtContext` in the same PR).
3. Add **import-graph** and **gofmt** gates to CI so AGENTS.md §7 matches reality.
4.**Remove classic FSD as a production dual-path tax**: one I/O model (gnet) for everyday development and default tests; no `Listen != nil ⇒ classic` coupling.
5.**Close out** sweatbox + apt/air design docs (status, current-state, PR plan truth) without deleting design history.
6.**Populate datafeed honestly** via extended service-HTTP online-users fields (web must not invent FSD state). Remaining zeros documented.
- Adding a weather / QNH model, military ratings, or multi-line ATIS persistence as a hard requirement of this program (honest zeros / empty arrays are acceptable when no source exists).
- Reintroducing browser automation (Playwright etc.) for design-doc closeout.
- Splitting the single binary or reopening package layout.
- Performance A/B of gnet vs classic as a long-term product feature (verifyperf is optional historical tooling).
---
## Problem Statement (grounded)
### A. Hygiene script subshell bug
`scripts/check-hygiene.sh` sets `failed=0` in the parent shell, then pipes file lists into `scan_files`:
```bash
printf'%s\n'"$files"| scan_files '\bpanic\s*\('
```
`scan_files` assigns `failed=1` when it finds hits. Under bash, the right-hand side of a pipeline runs in a **subshell**, so the parent’s `failed` stays `0`. The script prints panic locations (e.g. the two real sites below) and still exits 0:
```bash
if[["$failed" -ne 0]];then
exit1
fi
```
That is why hygiene can “fail” visually while CI/local automation thinks it passed.
**Real panic sites** (non-test; confirmed by workspace grep):
| Location | Behavior |
|----------|----------|
| `internal/web/auth.go:321–325``getJwtContext` | `panic("attempted to load non-existent jwt context")` when gin key missing |
-`QnhIHg: 29.92`, `QnhMb: 1013` (standard atmosphere, not measured)
-`FlightPlan` never set (always omitted)
-`TextATIS: []string{}` (empty — honest only if no ATIS source)
Service HTTP `GET /online_users` (`handleGetOnlineUsers` in `http_service.go`) populates position/identity/synthetic but **does not** export `FlightPlan`, pilot rating, assigned beacon, or ATIS lines.
Import graph constraint: `internal/web` → `serviceapi` OK; web must **not** import `server` / `session`.
---
## Proposed Design
### Part A — Hygiene, panics, CI gates
#### A.1 Fix `scripts/check-hygiene.sh`
**Root cause:** pipeline subshell mutates a copy of `failed`.
**Normative fix:** convert **every**`scan_files` invocation that today uses a pipeline (`printf … | scan_files` or equivalent). The script currently has **five** such call sites:
1.`panic(` scan over `pkg/` + `internal/`
2.`reflect` scan under `pkg/protocol`
3.`fmt.Print*` scan
4.`log.Print*` scan
5.`log.Fatal*` / `log.Panic*` scan
Leaving any `… | scan_files` intact reintroduces the same subshell bug for that pattern class (e.g. a future `fmt.Print` would print and still exit 0).
**Recommended form (process substitution)** — apply the same shape to all five:
```bash
if scan_files '\bpanic\s*\(' < <(printf'%s\n'"$files");then
echo" OK"
fi
```
Under bash, process substitution feeds stdin **without** putting the function in a pipeline subshell, so `failed=1` sticks in the parent.
**Alternatives** (also acceptable if applied to all sites): temp file / path array with parent-owned iteration; `scan_files` only echoes hits and returns 1 while the parent sets `failed`.
Keep `set -euo pipefail` and the existing `if scan_files …; then` pattern so a non-zero return does not abort early before other pattern classes run (or until the final `failed` check).
**Manual self-check (once after the fix, not a permanent poison file):** introduce a temporary non-test `panic(` under `internal/`, run the script, expect exit 1 and a printed hit; revert the poison. Comment in the script may point maintainers at this check.
Do **not** weaken patterns to ignore real panics.
#### A.2 `getJwtContext` — fail closed without panic
**File:**`internal/web/auth.go` + **every** consumer under `internal/web/`
panic("attempted to load non-existent jwt context")
}
claims=val.(*auth.CustomClaims)
return
}
```
**Binding requirement for PR1:** removing the `panic(` token alone is **not** sufficient. Call sites that do `claims := getJwtContext(c)` then `claims.NetworkRating` / `claims.CID` would become **nil pointer panics** if middleware is mis-wired. Hygiene greps only for `panic(`, so CI would be green while handlers remain unsafe. **All non-optional call sites must be updated in PR1** — no follow-up PR for “the rest of the call sites.”
**Recommended API:**
```go
// getJwtContext returns session/bearer claims set by requireSessionHTML /
// jwtBearerMiddleware. Returns nil if missing or wrong type (never panics).
| API handlers that already check `claims == nil` | Keep explicit check **or** migrate to `requireJwtContext` for one style |
**Grep-driven checklist (touch every site in PR1):**`rg 'getJwtContext' internal/web` — expect ~24 call sites across `auth.go` (`requireMinRatingHTML`), `pages_*.go`, `config.go`, `user.go`, `api_*.go`, `fsdconn.go`, etc. After PR1, every non-optional site uses `requireJwtContext` (or equivalent abort + bool). No bare `getJwtContext` followed by unconditional field access.
**Tests (PR1, required):**
1. Empty gin context → `getJwtContext` returns nil (no panic).
2. Handler/middleware path without claims set → 500/abort **without** panic (table or direct `requireJwtContext` / thin handler).
3. Existing PE/API tests with middleware stay green.
Everyday development and default CI already exercise **gnet only** via `StartTestServer`. Classic survives for:
1. One bootstrap inject test (`TestRunServiceHTTPAndListen`)
2. Optional verifyperf A/B (`ForceClassicFSD`)
Keeping a “narrow classic” forever still means every login/lifecycle change risks classic/gnet drift (`handleConn` vs `finishLogin`). Full removal is lower long-term risk if the two consumers are rewritten/deleted carefully.
**Do not** remove shared helpers that gnet + sweatbox need.
| Classic arm of verifyperf | `io_ab_verify_test.go` | See below |
| Stale classic godoc | `server.go`, `config.go`, `gnet_fsd.go`, kick/sweatbox comments | e.g. `FsdNumEventLoop` “only when Deps.Listen is nil” in `config.go` |
#### What to keep (possibly rename/move for clarity)
| Symbol | Consumers |
|--------|-----------|
| `parseLoginPackets` (`login_parse.go`) | gnet `finishLogin`, unit tests |
| `attemptAuthentication`, `enforcePilotPPLRequirement` | gnet `attemptAuthGnet`, unit tests |
| `gnetLoginConn` / `attemptAuthGnet` | gnet only — keep |
| `session.SenderWorker` | **sweatbox synthetics** + any channel-path tests |
| `session.NewCoalesceOutbound` | gnet only |
| `Deps.HTTPListen`, `Deps.FSDBound` | tests + service HTTP inject |
Optional cleanup (same PR or follow-up): rename `conn.go` → `login_lifecycle.go` (or split auth/broadcast) so the file is not named for a deleted path. Not required for correctness.
#### Docs / comments
- Update `Server` godoc in `server.go` (“classic when Listen injected…” → gnet-only description).
- Update `internal/server/config.go` godoc that still says event-loop count applies “only when Deps.Listen is nil” (or similar) — gnet is always on.
- Update comments in `gnet_fsd.go`, `http_service.go` kick, sweatbox that say “handleConn defer” to “gnet disconnect / synthetic cleanup”.
- AGENTS.md does not mandate classic path; no package table change required.
- Keep `HTTPListen` pre-bound listener inject (unchanged; service HTTP is independent of classic)
- **Readiness:** `select` on `FSDBound` receive, `Run` error channel, and a timeout — same style as `StartTestServer` (including any `0.0.0.0` → `127.0.0.1` rewrite used there). **Forbidden:** sole readiness gate of `time.Sleep(50 * time.Millisecond)` before dial.
- Dial bound FSD addr to prove accept path; cancel context; wait for `Run` return
**`StartTestServer`:** already gnet — no change required.
**`io_ab_verify_test.go` (build tag `verifyperf`):**
Options (pick one in PR description; recommend **1**):
1.**Delete classic arm** — rename to gnet-only perf smoke (`TestIO_GnetBaseline`) or delete the file if the A/B has served its purpose.
2.**Delete entire verifyperf test** if maintainers no longer run it.
3.**Keep file but only gnet** — remove `ForceClassicFSD` parameter from helper.
Do **not** keep classic solely for verifyperf after production dual-path is gone.
**Handler / security tests** that call `attemptAuthentication` / `broadcast*` directly: unchanged (no TCP classic loop).
1.**PR B0:**`useClassic := d.ForceClassicFSD` only (`Listen != nil` no longer forces classic). Document that `Listen` is unused/ignored unless `ForceClassicFSD`. Update bootstrap test to use gnet + `FSDBound` (or set `ForceClassicFSD: true` temporarily).
Prefer **single full-removal PR** after A is green if the diff stays reviewable (mostly deletions + one bootstrap rewrite).
#### Risk controls for Part B
- Race-clean `go test -race ./internal/server/...` including e2e.
- Manual smoke: `openfsd -fsd` accepts pilot login (gnet).
- Confirm sweatbox e2e still registers synthetics + `SenderWorker` drain.
- Confirm `Deps.Listen` removal does not break external forks (in-tree only consumer is tests).
---
### Part C — Design-doc closeout
Do **not** delete design history. Edit headers, add an **Implementation status** section near the top (after Overview or after the metadata table), and retcon “current state” tables.
| **Status** | `Implemented` (note ship date or “landed P0+P1; see Implementation status”) |
| **Implementation status** | New section: pure engine in `internal/sweatbox`; host/HTTP in `internal/server/sweatbox_*.go`; web instructor UI + PE; Synthetic sessions; service HTTP control plane; coverage floor ≥95% on sweatbox; e2e present |
| **Background “current state”** | Prefixed as historical pre-implementation snapshot, **or** updated to match tree (prefer short “As of implementation” table + leave long motivation) |
| **PR Plan** | Mark each planned PR **Done** / **Cancelled** with one-line notes (P0a–c, P1 pattern, etc.) |
| **Implementation status** | `pkg/twrfiles` Parse+Format+fixtures; sweatbox aliases/wrappers; `/airport-editor` MPA + Leaflet PE; `webjs/` Node tests + `scripts/check-webjs.sh` in CI; validate API via `pkg/twrfiles`; no durable server persistence; Playwright cancelled |
| **Background current-state table** | Replace stale rows: Format **present** (`format_apt.go` / `format_air.go`); parsers live in `pkg/twrfiles` (not only sweatbox); JS tests under `webjs/` |
| **Open Questions** | Mark Supervisor access / undo as deferred v1.1 or product-default (Admin-only shipped) |
No code changes required for Part C unless a doc references a wrong path that confuses implementers — fix those paths in the same PR.
---
### Part D — Datafeed honest population
#### Design principle
**FSD process is source of truth.** Web builds the public datafeed from `GET /online_users` (+ local config for server ident). Web must not invent ratings, QNH, or flight plans.
#### D.1 Extend `internal/serviceapi` DTOs
**File:**`internal/serviceapi/online_users.go`
```go
typeOnlineUserPilotstruct{
OnlineUserGeneralData
Altitudeint`json:"altitude"`
Groundspeedint`json:"groundspeed"`
Headingint`json:"heading"`
Transponderstring`json:"transponder"`
Syntheticbool`json:"synthetic,omitempty"`
// PilotRating is the VATSIM pilot rating wire ID from the certificate
// at login (0,1,3,7,15,31,63). 0 if unknown (e.g. synthetic without DB).
PilotRatingint`json:"pilot_rating"`
// FlightPlan is the session info-section string (no $FP source/dest),
// empty if none filed. Same layout as session.FlightPlan / encodeFlightPlanInfo.
FlightPlanstring`json:"flight_plan,omitempty"`
// AssignedBeaconCode is the ATC-assigned squawk if set; may be empty.
// TextATIS is multi-line controller ATIS when the server stores it.
// Empty when not available (openfsd does not persist NEWINFO by default).
TextATIS[]string`json:"text_atis,omitempty"`
}
```
Omitempty on empty flight plan / ATIS keeps dashboard/online_users payloads compact. `pilot_rating` should always be present as a number (0 is valid P0).
`DatafeedPilot` / `DatafeedATC` today **embed**`serviceapi.OnlineUserPilot` / `OnlineUserATC` and declare outer fields with the **same JSON tags** as the embed (`pilot_rating`, `flight_plan`, `text_atis`). Go’s `encoding/json` prefers the **outer** field for a given name; the embedded value is ignored for that tag.
Verified footguns if left careless:
| Outer field left at zero-value | Resulting JSON |
serviceapi.OnlineUserATC// promotes general + text_atis from serviceapi
// Do NOT redeclare TextATIS — embed owns json:"text_atis".
Serverstring`json:"server"`
}
```
**Reviewer reject rule:** any PR that “simplifies” by only embedding and forgetting to map `FlightPlan`, or that reintroduces an outer `PilotRating` / outer `TextATIS` that can zero-out honest embed values, is incorrect.
**Alternative (also acceptable, more verbose):** stop embedding; copy general/position fields explicitly into `DatafeedPilot` so service-HTTP string vs datafeed object never share a tag path. Prefer the embed + intentional `FlightPlan` shadow above (less churn).
#### D.2 Session: carry pilot rating after login
**Problem:**`User.PilotRating` is loaded in `attemptAuthentication` for the PPL gate but not stored on the session.
**Change:**
1. Add `PilotRating int` to `session.LoginData` (immutable after login; document in field-ownership comment).
2. In `attemptAuthentication` (both password and JWT branches after user load), set `client.PilotRating = user.PilotRating` (validate with `protocol.IsValidPilotRating`; if invalid, store `0`).
3. Sweatbox `buildSession`: leave `0` (or document synthetic as P0) — no DB user for default sweatbox CID.
4.`handleGetOnlineUsers`: copy `client.PilotRating`, `client.FlightPlan.Load()`, `client.AssignedBeaconCode.Load()` into DTO.
No new DB query on the online_users hot path.
#### D.3 Flight plan → `DatafeedFlightplan` mapping (web)
**`formatHHMM` contract (normative):** sweatbox/`encodeFlightPlanInfo` emits single-digit `"0"` for hours/minutes, not `"00"`. Do **not** string-concatenate raw fields.
```go
// parseNonNegIntField: empty or non-numeric → 0; negative → 0.
// formatHHMM(hoursField, minutesField) → fmt.Sprintf("%02d%02d", h, m)
```
| hours field | minutes field | `formatHHMM` result |
- If serviceapi `FlightPlan == ""`, set outer `DatafeedPilot.FlightPlan`**nil** (omitempty) — intentional shadow means the string is **not** marshaled as `flight_plan`.
- If non-empty but fewer fields than expected, map what is present; do not invent routes/airports.
- Mapping is **best-effort** for VATSIM-shaped consumers; openfsd does not claim full VATSIM datafeed parity.
Place parser in **web** (pure helper + table tests). Keep `serviceapi` DTO-only.
#### D.4 QNH, military rating, TextATIS
| Field | Source today | Honest value |
|-------|--------------|--------------|
| `QnhIHg` / `QnhMb` | None (no weather model on session) | **0** / **0** — stop shipping 29.92/1013 as if real |
| `MilitaryRating` | Not stored anywhere | **0** |
| `PilotRating` | DB at login → session → online_users | Real ID |
| `TextATIS` | Not stored (NEWINFO forward-only) | **`[]string{}`** or omit empty |
Optional **follow-up** (not required for this program’s honesty goal): store last NEWINFO payload lines on ATC `Session` when handling `$CQ` NEWINFO, export via `OnlineUserATC.TextATIS`. That is a product enhancement with protocol semantics; out of scope unless PO expands scope.
#### D.5 `generateDatafeed` changes
Resolve server ident once (same source as servers.json — already in-tree via `getFsdServerInfo` / `ConfigFsdServerIdent`). Fallback `"OPENFSD"` only on config error so datafeed is not the outlier.
```go
serverIdent,_,_,err:=s.getFsdServerInfo()
iferr!=nil||serverIdent==""{
serverIdent="OPENFSD"
}
for_,pilot:=rangeonlineUsers.Pilots{
dp:=DatafeedPilot{
OnlineUserPilot:pilot,// pilot_rating comes from embed — do not set outer PilotRating
OnlineUserATC:atc,// text_atis from embed when present
Server:serverIdent,
// no outer TextATIS
})
}
```
Remove comments that say “INOP placeholder” once fields are honest (replace with “always 0: no model” where applicable).
#### D.6 Tests
| Layer | Cases |
|-------|--------|
| `session` / server auth unit | After successful auth, `client.PilotRating` matches user |
| `handleGetOnlineUsers` unit | Snapshot includes flight plan **string** + pilot rating + beacon |
| `web` pure map helper | Table-driven info section → `DatafeedFlightplan`; empty → nil; `formatHHMM` cases in D.3 |
| `web`**JSON marshal** of `DatafeedPilot` | Build from `OnlineUserPilot` with non-zero `PilotRating` + non-empty plan string; assert marshaled JSON has real `"pilot_rating"` and object `"flight_plan"` (not omitted, not a JSON string). Also assert no outer zero-rating when embed is set. |
| `web` generateDatafeed | Stubbed online users: no fabricated QNH 29.92; `server` matches config ident when available |
| e2e (optional but valuable) | File `$FP`, poll online_users or datafeed path if test harness allows |
Dashboard consumers of online_users should ignore unknown JSON fields; adding fields is backward compatible.
---
## Alternatives Considered
### Hygiene failed flag
| Alternative | Trade-off |
|-------------|-----------|
| Process substitution (chosen) | Minimal diff; keeps scan_files |
| Rewrite without pipeline | Clearer; more churn |
| `PIPESTATUS` after pipe | Fragile with `set -o pipefail` + multiple greps inside |
| Panic in `Must*` only | Still need safe path for library |
### Classic FSD
| Alternative | Trade-off |
|-------------|-----------|
| **Full removal** (chosen) | One I/O model; deletes dual maintenance; rewires one test + verifyperf |
| Narrow: `ForceClassicFSD` only | Smaller first PR; still pays dual tax until B1 |
| Keep classic forever for inject Listen | Continuous drift risk; gnet already has `FSDBound` for `:0` |
### Datafeed flight plans via DB
| Alternative | Trade-off |
|-------------|-----------|
| **Session → service HTTP → web** (chosen) | Live wire state; ATC `$AM` already updates session |
| Web re-queries DB | No FP in DB; violates “don’t invent FSD state” |
| New dedicated `/datafeed_snapshot` HTTP | Extra surface; online_users already exists |
### TextATIS persistence
| Alternative | Trade-off |
|-------------|-----------|
| Empty until future PR (chosen for honesty now) | No protocol inventing |
| Parse NEWINFO into session in this program | Larger server change; defer |
---
## Key Decisions
| # | Decision | Rationale |
|---|----------|-----------|
| 1 | Fix hygiene via **process substitution on all five `scan_files` call sites** (or equivalent non-pipeline parent mutation) | Parent must see `failed`; partial conversion reintroduces the bug |
| 2 | **`getJwtContext` returns nil**; **`requireJwtContext` at every non-optional call site in PR1** (not a follow-up) | Avoid replacing `panic(` with nil-deref panics; AGENTS.md §10 |
| 3 | **`NewCoalesceOutbound` → `(*CoalesceOutbound, error)`** | Loud in tests, no production panic |
| 8 | verifyperf **drops classic arm** (or whole file) | No reason to keep classic for A/B alone |
| 9 | Design docs → **Implemented** + Implementation status; history retained | Closeout without amnesia |
| 10 | Datafeed fields from **extended online_users**, not web invention | Import graph + SoT |
| 11 | Store **`PilotRating` on LoginData** at auth | Avoid online_users DB hits; already loaded |
| 12 | Export **FlightPlan info section** + map in web | Session already stores it |
| 13 | **QNH / military = 0**; **TextATIS empty** unless stored later | Honesty over fake standards |
| 14 | Stop hardcoding **PilotRating/MilitaryRating = 1** | Those were false signals |
| 15 | Classic removal is its **own PR** after hygiene/CI | Isolate highest-risk change |
| 16 | No Playwright / no SPA / no package layout change | House rules |
| 17 | **Datafeed embed:** drop outer `PilotRating` and outer `TextATIS`; keep intentional outer `FlightPlan *DatafeedFlightplan` shadow; marshal unit test required | `encoding/json` outer-field preference otherwise zeros/omits honest embed values |
| 18 | Datafeed `server` field from **`ConfigFsdServerIdent`** via `getFsdServerInfo` (fallback `"OPENFSD"`) in PR5 | Match servers.json honesty; stop hardcoding |
---
## Open Questions
None that block implementation. Product defaults already taken:
- Classic path: full removal (this doc).
- Datafeed synthetics: remain included when sweatbox is enabled (sweatbox design decision).
- TextATIS persistence: deferred; empty is honest.
- Datafeed `server` field: **`ConfigFsdServerIdent`** in PR5 (fallback `"OPENFSD"`) — resolved; not polish-deferred.
---
## Risks
| Risk | Severity | Mitigation |
|------|----------|------------|
| Hygiene fix suddenly fails CI on residual violations | Low | Only two panics; fix panics in PR1, then CI in PR2 |
| Missed nil check after `getJwtContext` change → nil deref panics | **High** | PR1 **must** migrate every non-optional call site to `requireJwtContext`; grep checklist; empty-context + no-middleware abort tests |
| gnet-only bootstrap test flakes on bind race | Med | Select on `FSDBound` / Run error / timeout (like `StartTestServer`); **forbid sleep-only readiness** |
- Tests: server online_users; web mapper + **JSON marshal** of `DatafeedPilot`; auth rating sticky
- Sweatbox host: explicit PilotRating 0 on synthetic LoginData if field added
**PR5 reviewer reject:** outer `PilotRating` or outer `TextATIS` reintroduced; outer `FlightPlan` not mapped when serviceapi string non-empty; hardcoded `"OPENFSD"` when config ident is available.
---
## PR Plan
Independently mergeable; order matters where noted. Each PR: `gofmt`, `go test -race ./...` (or package-scoped + full before merge), coverage floors when touching measured packages.
| **Description** | `getJwtContext` returns nil; **`requireJwtContext` at every non-optional call site in this PR** (success criterion is fail-closed without any panic, not merely deleting the `panic(` token). `NewCoalesceOutbound` returns `(*CoalesceOutbound, error)`. Grep checklist: no bare `getJwtContext` + unconditional field access. Tests: empty context → nil; missing claims → 500/abort without panic; existing middleware paths green. Zero `panic(` hits under non-test pkg/internal after this PR. |
| **Description** | Status → Implemented; Implementation status section; fix stale current-state tables; mark PR plans Done/Cancelled; retain design history. No code. |
### PR5 — Datafeed honesty via online_users extension
| | |
|--|--|
| **Title** | `datafeed: export pilot rating and flight plan via online_users; stop fabricating QNH` |
| **Depends on** | — (independent of classic removal; can parallel PR3/PR4) |
| **Files** | `internal/session/session.go`, `internal/server/conn.go` (auth set rating), `internal/server/http_service.go`, `internal/serviceapi/online_users.go`, `internal/web/data.go` (embed shape D.1b, `formatHHMM`, generateDatafeed), tests under server/web; sweatbox `buildSession` zero rating |
| **Description** | Extend serviceapi pilot/ATC DTOs; populate from session snapshot. Datafeed: **drop outer PilotRating / TextATIS**; intentional `FlightPlan *object` shadow; MilitaryRating/QNH 0; `Server` from `ConfigFsdServerIdent` (fallback OPENFSD). Unit tests for mapper, `formatHHMM`, and **JSON marshal** (real pilot_rating + object flight_plan). Remove fake defaults of 1 and 29.92/1013. |
| **Files** | session field or atomic, `handler_query.go`, online_users population, tests |
| **Description** | Only if product wants non-empty `text_atis` on the **serviceapi** DTO (embed path — still no outer DatafeedATC field). Out of core program unless PO prioritizes. |
---
## Success criteria
1.`bash scripts/check-hygiene.sh` exits **1** if a non-test `panic(` is introduced; exits **0** on clean tree; **all** pattern classes honor `failed` in the parent shell.
2. Zero `panic(` under non-test `pkg/` + `internal/`; JWT handlers fail closed without nil-deref panics.
3. CI fails on gofmt drift, hygiene hits, or forbidden imports.
4. No classic FSD code path; no dead `Server.listen` / `useClassicFSD` / `Deps.Listen`; `Server.Run` always gnet; tests green under `-race`; bootstrap readiness is not sleep-only.
5. Design docs Status **Implemented** with accurate implementation status.
6. Datafeed JSON: real `pilot_rating` when known (via embed, not outer zero); `flight_plan`**object** when session has FP; QNH/military not falsely non-zero; `server` from config ident when available; web still does not import `server`/`session`.
@@ -185,7 +190,10 @@ func (s *Server) createUser(c *gin.Context) {
return
}
claims:=getJwtContext(c)
claims,ok:=requireJwtContext(c)
if!ok{
return
}
// Create is full mutation: Supervisor+ only.
if!canFullMutateUsers(claims.NetworkRating)||
reqBody.NetworkRating>int(claims.NetworkRating){
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.