From c8a293b8f62a379f507e7f0edb44ff8664e7f58a Mon Sep 17 00:00:00 2001 From: wtclaude Date: Tue, 1 Sep 2026 08:11:10 -0500 Subject: [PATCH] docs(admin): the engagement rules screen, and the privacy inventory an engagement mailer changes Engagement Phase 12a. The site had pages for where a message goes (Notifications and email) and what it says (Message templates), and nothing at all for what makes one get sent -- the four Engagement screens the workstream built. New page: Engagement rules. Rules, Audiences, the trigger catalog and the send log on one page, sitting between the two it joins up. Templates already has its own page and Suppressions is in Troubleshooting, so neither is repeated here. Two things it exists to state plainly: * Every rule ships disabled, including the ones a module brings. "Installed" is not "on", and an upgrade whose Team mail went quiet is the same fact. * The ceiling is a TREE, not a ladder. The tempting reading -- a staff-only event could obviously also go to one person -- is wrong, and the example is the argument: "one person" for cheat detection is the player it was detected on. Troubleshooting gains the symptom that page answers ("nothing is sent for one particular event"): the rule is off, the rule is dormant, its own cooldown held it, or the audience is empty. Privacy: two rows the engagement work makes necessary, and one sentence it made false. * app-content claimed "Nothing is cached for offline use". Phase 8 shipped a DataStore snapshot of the inbox, so it was untrue -- and that row feeds the generated Play Data Safety answers, which is a store-review matter rather than a doc nit. The snapshot now has its own row and its own Play mapping (Messages / Other in-app messages; not collected by us, stored on the device), and app-content's claim is narrowed to everything else. * deploy-engagement, for the deployment scope: an address is now used for more than getting into an account, there is a delivery log holding a one-way hash of it, and there is a suppression list. Its retention line says what is true rather than what a reader assumes -- none of these tables has a retention sweep. PLAY_DATA_SAFETY.md regenerated from the inventory; legal.lastUpdated moved with the page it dates. Verified: the whole `verify` chain green -- checkSidebar (plannedSidebar moved with the live tree), checkFacts 19/19, checkQuickstart 59, checkReference 22, checkLinks 2605, checkA11y, checkCsp, playDataSafety --check, 42 + 7 tests. Read in a browser as well, in the served build. AI-assisted: written with Claude Code. Co-Authored-By: Claude --- PLAY_DATA_SAFETY.md | 16 +- src/config/sidebar.mjs | 2 + .../docs/administration/engagement-rules.mdx | 175 ++++++++++++++++++ .../docs/administration/message-templates.mdx | 3 +- .../notifications-and-email.mdx | 7 + .../docs/docs/administration/teams.mdx | 5 +- .../docs/administration/troubleshooting.mdx | 16 ++ src/data/collection.mjs | 69 ++++++- src/data/legal.mjs | 2 +- 9 files changed, 286 insertions(+), 9 deletions(-) create mode 100644 src/content/docs/docs/administration/engagement-rules.mdx diff --git a/PLAY_DATA_SAFETY.md b/PLAY_DATA_SAFETY.md index ca50a70..89b0767 100644 --- a/PLAY_DATA_SAFETY.md +++ b/PLAY_DATA_SAFETY.md @@ -32,6 +32,7 @@ Where the console offers free text about security practices, two things are wort | App info and performance | Other app data | No | No | Not collected by us. Stored on the device only. | | Messages | Other in-app messages | No | No | Not collected by us. Declare the relay hop in the console’s free-text security section if it asks. | | Messages | Other user-generated content | No | No | Not collected by us. | +| Messages | Other in-app messages | No | No | Not collected by us. Stored on the device only. | | Device or other IDs | Device or other IDs | No | No | Not collected. | ## Each answer, and why it is the truthful one @@ -83,12 +84,23 @@ Push is off until you enable it. When you do, the app mints a random, unguessabl **Messages → Other user-generated content.** Not collected by us. -Forum posts, Team activity, character and shard information, notification preferences: all of it is a live read or write against the deployment. Nothing is cached for offline use and nothing is duplicated anywhere else — the app with no signal is an app with no content, which is a limitation and also an accurate description of where the data lives. +Forum posts, Team activity, character and shard information, notification preferences: all of it is a live read or write against the deployment. Apart from the notification snapshot described in the next entry, nothing is cached for offline use and nothing is duplicated anywhere else — the app with no signal is an app with almost no content, which is a limitation and also an accurate description of where the data lives. - **Why that answer:** Content is written to the community’s own installation. We have no copy, no access and no way to obtain one. - **Retention:** Held by the deployment, under its operator’s policy - **Read from:** `PLAN.md §9 section 2` +### A snapshot of your notifications, so the inbox opens without a signal + +**Messages → Other in-app messages.** Not collected by us. Stored on the device only. + +The app keeps the most recent notifications it has already fetched — at most thirty, and only the first page — on the device, so opening the inbox shows you what you had rather than a spinner. It is a copy of what the deployment already sent you and it is refreshed from there; nothing is written here that was not read from your own account. It is scoped to the account that fetched it, so a second person signing in on the same phone is never shown the first one’s messages. + +- **Why that answer:** The snapshot is written on the phone from data the deployment had already delivered. It is not uploaded anywhere, and no server we operate is on either end of it. +- **Retention:** Until you sign out, or the thirty are pushed out by newer ones +- **In detail:** Signing out deletes the snapshot outright. It lives in the app’s ordinary preference store rather than the encrypted one — sign-in tokens are the thing that store is for — which is worth stating plainly: on a device where someone has root, these are readable, and they are notification bodies rather than credentials. +- **Read from:** `core/inbox/DataStoreInboxCache.kt, data/repository/AuthRepository.kt` + ### No analytics, no crash reporting, no advertising **Device or other IDs → Device or other IDs.** Not collected. @@ -116,4 +128,4 @@ Not part of the Data Safety form — that form is about the app — but a review - **Your browser’s user-agent string, truncated** — With the row; blanked on removal. - **The web server’s access log** — Short-term operational retention, then rotated away. -Last generated from data dated 2026-08-24. Regenerate with `npm run play:datasafety` after any change to what the app stores. +Last generated from data dated 2026-09-01. Regenerate with `npm run play:datasafety` after any change to what the app stores. diff --git a/src/config/sidebar.mjs b/src/config/sidebar.mjs index 27d069d..e91818e 100644 --- a/src/config/sidebar.mjs +++ b/src/config/sidebar.mjs @@ -38,6 +38,7 @@ export const docsSidebar = [ { label: 'Teams', slug: 'docs/administration/teams' }, { label: 'Moderation', slug: 'docs/administration/moderation' }, { label: 'Notifications and email', slug: 'docs/administration/notifications-and-email' }, + { label: 'Engagement rules', slug: 'docs/administration/engagement-rules' }, { label: 'Message templates', slug: 'docs/administration/message-templates' }, { label: 'Managing modules', slug: 'docs/administration/managing-modules' }, { label: 'The shard connection', slug: 'docs/administration/the-shard-connection' }, @@ -114,6 +115,7 @@ export const plannedSidebar = { 'Teams', 'Moderation', 'Notifications and email', + 'Engagement rules', 'Message templates', 'Managing modules', 'The shard connection', diff --git a/src/content/docs/docs/administration/engagement-rules.mdx b/src/content/docs/docs/administration/engagement-rules.mdx new file mode 100644 index 0000000..f03dd33 --- /dev/null +++ b/src/content/docs/docs/administration/engagement-rules.mdx @@ -0,0 +1,175 @@ +--- +title: Engagement rules +description: Decide what your site mails and shows people — the rule editor, saved audiences, the trigger catalog and the send log that answers "did they actually get it". +--- + +import { Aside } from '@astrojs/starlight/components'; + +[Notifications and email](/docs/administration/notifications-and-email/) is about where a +message goes. [Message templates](/docs/administration/message-templates/) is about what it +says. This page is the part in between: **what makes one get sent at all.** + +A **rule** is four decisions — *when* (a trigger), *to whom* (an audience), *by what* +(channels), and *how often* (timing). **Admin → Engagement → Rules.** + +## Nothing sends until you turn it on + +Every rule arrives switched **off**. That is true of the ones you make, and it is true of +the ones your modules ship with them: install a game module and you get a shelf of ready +rules, all dark, none of them mailing anybody. Turning one on is a deliberate, separate +act. + +The same caution runs through the rest of the screen. Every rule carries a **hard ceiling +on sends per hour** — you cannot save one without a number — because the failure mode of an +automated mailer is not a wrong message, it is ten thousand of them at four in the morning. + + + +## What a rule is made of + +**The trigger** is the event that fires it: a house falling into disrepair, a post being +published, a login failing. Pick it from what is registered — see +[the catalog](#the-trigger-catalog) below. A rule's trigger is **fixed once the rule +exists**: its cooldowns, its pending messages and its whole send history are about one +event, so changing it would silently be a different rule wearing the same name. Make a new +one instead. + +**The audience** is who hears about it. Some are built in — the person the event is about, +everyone subscribed to it, staff. Others come from your modules and are named in their own +vocabulary. You can also point a rule at a **saved audience** you composed yourself; see +[Audiences](#audiences). + +**The channels** are how it reaches them: on the site, by email, by push. A rule can name +more than one, and each channel picks its own template — the same event can be a sentence +in the inbox and a properly laid-out letter in the mail. + +**The timing** is the part worth reading twice. + +- A **delay** holds the message before it goes, so a situation that resolves itself never + produces a message at all. +- **Cancel on** names the events that call it back. A warning that a house is about to + collapse waits fifteen minutes and is cancelled outright if the owner turns up and + repairs it — nobody is told their house was in danger after it stopped being in danger. +- A **cooldown** is the "not again for a while" limit, counted **per person, per subject + and per channel**. Per subject, so a cooldown about one house says nothing about another. + Per channel, so "one a day about this house" means one email *and* one inbox item, which + is what an operator setting that limit means. + +## Audiences + +**Admin → Engagement → Audiences** is where you build a named set of people out of the ones +your modules declare — *members of this Team*, *the sitting governors* — and combine them: +all of these, any of these, none of these. + +One rule governs the whole screen: **composition narrows and never widens.** + +- The ceiling of a saved audience is **derived** from the tightest thing in it, never + chosen. That is true of "any of" too, where the intuitive answer — the widest of the two — + is the wrong one. A ceiling says what an expression is *allowed* to reach, not what it + happens to resolve to today. +- **"None of" is only offered inside an "all of" group.** Alone it would have to mean + "everybody except these", which is a broadcast built out of a short list, and it is not + offered anywhere it would mean that. +- Two audiences with no relationship between them — staff and "the person this is about", + say — have no honest combined ceiling, so the save is refused rather than guessing which + side to take. + +Before you save a rule, the editor shows you a **reach preview**: a number, never a list of +names. It will also tell you when a number is a floor rather than an answer, and when an +audience resolves to nobody at all and why. + +## The ceiling, and why a rule will not offer the audience you expected + +Every trigger declares the **widest audience a rule may ever give it**. It is the security +boundary of the whole system, and it is set in code by whoever declared the event, not in +the admin panel. Staff-only events cannot be widened into public ones by anybody, including +you. + +Seven values, and they are a **tree, not a ladder**: + +| Ceiling | Who that is | +| --- | --- | +| `everyone` | Everyone, including signed-out visitors | +| `authenticated` | Any signed-in user | +| `subscribers` | Signed-in users subscribed to this event | +| `members` | Members of a module-declared list | +| `staff` | Staff only — admins, editors and moderators | +| `admin` | Administrators only | +| `owner` | Only the user the event is about | + + + +So `staff` does not permit `owner`, `members` does not permit `subscribers`, and the editor +simply does not offer you the audiences the trigger forbids. The one exception proves the +rule: `admin` sits under `staff`, because every administrator really is staff. + +## The trigger catalog + +**Admin → Engagement → Triggers** lists every event a rule can be built on, and it is +read-only on purpose — **there is no table behind it**. A trigger is declared in code, by +the site or by an installed module, so what you are looking at is whatever registered on +this boot. Uninstall a module and its triggers stop appearing; nothing was deleted. + +Two things it shows that are invisible everywhere else: + +- **The variables** each event carries, with an example of each. This is the list a template + is allowed to reference — when a message comes out with a hole in it, this is the screen + that says why. +- **The ceiling**, so when the rule editor offers you a narrower set of audiences than you + expected, you can see the number it is obeying. + +### Dormant rules + +A rule can be switched on and still be unable to fire — most often because the module that +declared its trigger, or the audience it points at, is no longer installed. Those are +badged **dormant** in the list, with the reason, because "this rule cannot fire" is a +different fact from "this rule is off" and you need both. The on/off switch keeps working +on a dormant rule, deliberately: a rule whose module has gone is exactly the rule you most +want to be able to stop. + +## The send log + +**Admin → Engagement → Send Log** answers one question: *did that person get that message, +and if not, why not?* Every attempt is a row — when, what fired it, which user, which +channel, and the result. Filter by result to go straight to what failed. + +| Result | What it means | +| --- | --- | +| **Sent** | Handed to the channel successfully | +| **Failed** | The attempt errored — the reason is on the row, not hidden in a tooltip | +| **Not sent** | Suppressed before it was attempted: unsubscribed, unverified, or on the [suppression list](/docs/administration/troubleshooting/) | +| **Bounced** | The receiving server rejected it after the fact | +| **Marked as spam** | The recipient reported it | + +Test sends from the template editor land here too, labelled as such, so you can confirm +your own test arrived before turning a rule on for real. + + + +## What a game module brings + +A module declares its own triggers and its own audiences, in its own vocabulary, and it may +ship rules and message bodies to go with them. The Ultima Online module ships a large family +of them — houses falling to ruin, vendors running out of gold, a governor being seated, a +guild's fortunes — written in the voice of an in-world office rather than a system alert. + +All of them arrive **disabled**, like every other seeded rule. Read the list in +**Admin → Engagement → Triggers**, turn on the ones your shard should send, and check the +send log the first time each one fires. diff --git a/src/content/docs/docs/administration/message-templates.mdx b/src/content/docs/docs/administration/message-templates.mdx index 88b9c39..05112bd 100644 --- a/src/content/docs/docs/administration/message-templates.mdx +++ b/src/content/docs/docs/administration/message-templates.mdx @@ -129,4 +129,5 @@ rejected sender address, a bad password, a relay that will not accept your domai The log does not store anybody's email address. It keeps a one-way fingerprint instead, so that a bounce can be matched back to a delivery without the log itself becoming a second -copy of your members' addresses. +copy of your members' addresses. The rest of that screen — and the rules that decide a +message is sent at all — is [Engagement rules](/docs/administration/engagement-rules/). diff --git a/src/content/docs/docs/administration/notifications-and-email.mdx b/src/content/docs/docs/administration/notifications-and-email.mdx index 6a081df..7adb945 100644 --- a/src/content/docs/docs/administration/notifications-and-email.mdx +++ b/src/content/docs/docs/administration/notifications-and-email.mdx @@ -9,6 +9,9 @@ Four separate delivery paths, each optional, each off until you configure it. A configures none of them still works — it just never reaches anyone who is not looking at it. +This page is about the paths themselves. What decides that a particular message gets sent +down one of them is a rule — see [Engagement rules](/docs/administration/engagement-rules/). + ## Email **Admin → Settings → Email delivery.** The site sends contact-form messages, invitations, @@ -154,6 +157,10 @@ member sets their own. The defaults are not symmetrical, and the asymmetry is de - **Muting a Team silences all three for that Team**, whatever the grid says, without touching any of their other Teams. +The operator's side of the same question — which events exist, and how wide an audience each +one may ever be given — is [Engagement rules](/docs/administration/engagement-rules/). When +a message went nowhere and you want to know why, the send log there is the screen that says. +