AniList disabled their public API with no warning and the family's Anime tab stopped working. The app had nothing to say: the cause was outside it, no retry would have helped, and the honest message was "this is broken, it is not your television, and it is not ours to fix today". Nothing could deliver that. A `notices` collection the owner writes and every app reads, plus a composer in the dashboard's Správa view, which already holds a superuser token. Read is PUBLIC on purpose: a notice has to survive the case where signing in is itself what is broken, which is exactly when an explanation is worth most. The cost is that it is world-readable, so both the migration and the composer say in so many words that nothing sensitive goes in one. Writing stays superuser-only. `endsAt` exists because the predictable failure is not a wrong notice but a stale one: the outage ends, the row stays, and within a week everybody has learned to ignore the banner. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
73 lines
2.8 KiB
JavaScript
73 lines
2.8 KiB
JavaScript
/// <reference path="../pb_data/types.d.ts" />
|
|
|
|
// 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);
|
|
},
|
|
);
|