mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
Hygiene: name the FHIR immunization gap, cache the digest offline, freeze the no-FK pair, document the pin bump
Four small debts from the audit tail. The FHIR bundle's missing Immunization resource gets the same written-decision paragraph the visits exclusion carries — deliberate follow-up, not oversight. The service worker's offline allowlist gains /api/daily/digest (both copies, policy module and sw.js, plus the test) so the Today rail renders on an offline reopen. The two Telegram plumbing tables that link to an account by a bare userId string are frozen as a closed NO_FK_BY_DESIGN set in the wipe guard — a third such table now fails the test instead of passing rule 1 silently. And deploy.md documents the manual digest-pin bump end to end: imagetools inspect, the env update, and the three-probe /api/version verification with the too-fast-deploy warning.
This commit is contained in:
1 parent
9d64eb6aa2
commit
bcabd43ca7
6 files changed
+79
No files matched your search
@@ -131,6 +131,34 @@ without touching the file:
|
||||
- `HEALTHLOG_IMAGE_REF=:v1.16.5` — pin to a release tag (still mutable).
|
||||
- `HEALTHLOG_IMAGE_REF=@sha256:<digest>` — pin to the immutable digest.
|
||||
|
||||
#### Bumping a digest pin by hand
|
||||
|
||||
A digest-pinned instance never follows a release on its own — an image
|
||||
promote does not touch the pin, so moving it is part of shipping. The
|
||||
mechanic, end to end:
|
||||
|
||||
1. Resolve the new release's digest from the registry (the manifest-list
|
||||
digest, which covers both architectures):
|
||||
|
||||
```sh
|
||||
docker buildx imagetools inspect ghcr.io/mbombeck/healthlog:v1.37.20 \
|
||||
| grep -m1 Digest
|
||||
```
|
||||
|
||||
2. Set `HEALTHLOG_IMAGE_REF=@sha256:<that digest>` where the compose
|
||||
environment lives — the `.env` next to the compose file, or the
|
||||
deployment platform's env vars for the app service.
|
||||
|
||||
3. Deploy as usual.
|
||||
|
||||
4. Verify `/api/version` on the target — and verify it **three times a
|
||||
few seconds apart**, not once: during the container swap a single
|
||||
probe can be answered by the outgoing container (or a cache) and read
|
||||
as success while the old release keeps running. Three stable answers
|
||||
reporting the shipped `version` + `buildSha` are the signal; a deploy
|
||||
that finishes suspiciously fast (under ~30 s) is a warning that
|
||||
nothing was actually pulled.
|
||||
|
||||
> **A pin is sticky, and a stale pin is silent.** Once set, every later deploy
|
||||
> ships that reference until someone changes it. The deploy still reports
|
||||
> success, the tag still builds, the image still publishes, and the running
|
||||
|
||||
@@ -79,6 +79,9 @@ const MAX_DATA_ENTRIES = 60;
|
||||
// and the version probe are eligible.
|
||||
const API_READ_ALLOWLIST = [
|
||||
"/api/dashboard/snapshot",
|
||||
// v1.37.20 — daily digest: the Today rail's offline read. Keep in
|
||||
// lockstep with src/lib/pwa/sw-cache-policy.ts.
|
||||
"/api/daily/digest",
|
||||
"/api/dashboard/widgets",
|
||||
"/api/measurements",
|
||||
"/api/medications",
|
||||
|
||||
@@ -280,4 +280,36 @@ describe("data-wipe completeness", () => {
|
||||
OR: [{ recordUserId: "u1" }, { recipientUserId: "u1" }],
|
||||
});
|
||||
});
|
||||
|
||||
it("freezes the set of user-linked tables that carry no foreign key", () => {
|
||||
// v1.37.20 (A6-19) — NO_FK_BY_DESIGN, as an assertion instead of a pair
|
||||
// of FK migrations. The two Telegram plumbing tables link to an account
|
||||
// by a bare `userId` string on purpose: both hold a bounded, short-lived
|
||||
// window of chat state (scheduled message deletions, prompt contexts)
|
||||
// that a cron already reaps, and a cascade would buy nothing the wipe
|
||||
// plan does not already do — rule 1 catches them because the column NAME
|
||||
// matches the convention. That is the exact fragility this test freezes:
|
||||
// a THIRD such table would also pass rule 1 silently, without anyone
|
||||
// deciding it should. So the set is closed here; growing it is a
|
||||
// decision made in this file, with a reason, or by adding the FK.
|
||||
const NO_FK_BY_DESIGN = new Set([
|
||||
"TelegramScheduledDeletion",
|
||||
"TelegramPromptContext",
|
||||
]);
|
||||
const bareUserIdModels = [...models.entries()]
|
||||
.filter(
|
||||
([name, shape]) =>
|
||||
name !== "User" &&
|
||||
shape.scalars.includes("userId") &&
|
||||
!shape.relations.some((rel) => rel.target === "User"),
|
||||
)
|
||||
.map(([name]) => name)
|
||||
.sort();
|
||||
expect(bareUserIdModels).toEqual([...NO_FK_BY_DESIGN].sort());
|
||||
// And both stay in the wipe plan — the assertion above is about schema
|
||||
// shape, this one about the plan actually reaching them.
|
||||
for (const model of NO_FK_BY_DESIGN) {
|
||||
expect(WIPE_MODELS).toContain(model);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -269,6 +269,14 @@ export function buildFhirDocumentBundle(
|
||||
// no route module lies. The PDF carries the visits; the Bundle does not, and
|
||||
// a reader who notices the gap should find this comment rather than a bug.
|
||||
//
|
||||
// No vaccinations either, on the same terms (v1.37.20, A6-21). FHIR has
|
||||
// `Immunization` and the model would map cleanly, but a resource type in
|
||||
// the Bundle without its REST route module and capability entry is the
|
||||
// exact half-shipped state the visits paragraph refuses; emitting it is a
|
||||
// deliberate follow-up, not an oversight. The Impfpass PDF and the backup
|
||||
// both carry the doses; the Bundle does not, and this comment is where a
|
||||
// reader who notices should land.
|
||||
//
|
||||
// held an episode (the aggregator gates `data.illnessEpisodes`). Each
|
||||
// episode emits a patient-reported Condition (generic SNOMED root, label on
|
||||
// `code.text`) plus a bounding Encounter that references it.
|
||||
|
||||
@@ -14,6 +14,9 @@ describe("isAllowlistedApiRead — offline read boundary", () => {
|
||||
it("caches the curated safe GET read endpoints", () => {
|
||||
expect(isAllowlistedApiRead("/api/dashboard/snapshot")).toBe(true);
|
||||
expect(isAllowlistedApiRead("/api/dashboard/widgets")).toBe(true);
|
||||
// v1.37.20 — the Today rail's offline read. Watched red before the
|
||||
// allowlist entry landed.
|
||||
expect(isAllowlistedApiRead("/api/daily/digest")).toBe(true);
|
||||
expect(isAllowlistedApiRead("/api/measurements")).toBe(true);
|
||||
expect(
|
||||
isAllowlistedApiRead("/api/measurements/series-batch?types=WEIGHT"),
|
||||
|
||||
@@ -27,6 +27,11 @@
|
||||
export const API_READ_ALLOWLIST = [
|
||||
"/api/dashboard/snapshot",
|
||||
"/api/dashboard/widgets",
|
||||
// v1.37.20 — the daily digest read: the Today rail is the first thing an
|
||||
// offline reopen renders, and the digest is an idempotent GET with no
|
||||
// secret in the body. The deny guard and the body-level refusal still
|
||||
// apply on top, like every entry here.
|
||||
"/api/daily/digest",
|
||||
"/api/measurements",
|
||||
"/api/medications",
|
||||
"/api/version",
|
||||
|
||||
Reference in new issue
Block a user