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. +