/// // A short message the owner broadcasts into the app, for breakage he cannot fix. // // **Why this exists.** AniList disabled their public GraphQL API with no notice // and the family's Anime tab stopped working. The app had nothing to say: the // cause was outside it, no retry would help, and the only honest message was // "this is broken, it is not your television, and it is not ours to fix today". // The same shape covers every outage we do not control — prehraj.to down, a // TorBox key expired, the backend being upgraded this evening — and most of // those the app cannot detect at all. The owner can. This is how he says so. // // **Why read is PUBLIC.** A notice has to survive the case where signing in is // itself what is broken, which is exactly when an explanation is worth most. A // rule of `@request.auth.id != ''` would hide the message from the only people // who need it. The cost is that anyone who finds this backend can read it, so: // // NOTHING SENSITIVE GOES IN A NOTICE. No hostnames, no account names, no // "the TorBox key expired" — that last one names a service the household pays // for and tells a reader what to go looking for. Write what a viewer needs // ("anime is not working today, it is an outage at the provider"), not what an // operator would want. // // Writing stays superuser-only. This is the owner's voice, and a compromised // user account must not be able to put words in front of the family. // // **Why `endsAt` as well as `active`.** The predictable failure is not a wrong // notice, it is a stale one: the outage ends, the message stays, and within a // week everybody has learned to ignore the banner. `active` is the switch the // owner flips; `endsAt` is the one he does not have to remember. The client // enforces both, so a forgotten row expires even if nobody touches it again. // // There is deliberately no severity, no title, no dismissal state and no // targeting. Every one of those is a field to get wrong in exchange for // something a single sentence already does, and dismissal is per-run in the // client precisely so it needs no storage here. migrate( (app) => { const c = new Collection({ type: "base", name: "notices", listRule: "", viewRule: "", createRule: null, updateRule: null, deleteRule: null, fields: [ { name: "message", type: "text", required: true, max: 400, }, { name: "active", type: "bool", required: false, }, { name: "endsAt", type: "date", required: false, }, ], }); app.save(c); }, (app) => { const c = app.findCollectionByNameOrId("notices"); app.delete(c); }, );