feat(usage): distinguish real edits and template delivery with v5 consent (#1339)

* feat(usage): distinguish real edits and template delivery with v5 consent

* fix(usage): exclude queued test messages and count reorders as edits

- queueEmail carries usageEligible: false into email_data and the queue
  processor passes it on, so the dev tools' send-test-email no longer
  records email_template_delivery once the worker sends it.
- event-types/reorder and categories/reorder-global compare the persisted
  order before and after and record the v5 edit markers only when it
  changed, matching the display_order edit already counted on PUT.
- normalized() builds arrays with Array.from so a row array from the sqlite
  binding compares equal under Jest's separate realm.

* fix(usage): cover per-gallery category order and workflow test runs

- categories/reorder records category_editing when an event's override
  changes; reorder/:eventId records it when an override was actually
  removed.
- send_email and the collections handoff pass usageEligible: false for a
  workflow test run (engine.testRun sets __test), so a non-dry test send is
  not counted as template delivery.

---------

Co-authored-by: Paul Nothaft <[email protected]>
This commit is contained in:
Paul Nothaft
2026-09-07 20:03:15 +02:00
committed by GitHub
co-authored by Paul Nothaft
parent 69754f8a2c
commit 5c1e38d921
31 changed files with 5398 additions and 292 deletions
@@ -1,6 +1,6 @@
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import catalog from './usageFeatures.v4.json';
import catalog from './usageFeatures.v5.json';
/** Local, static disclosure: opening it never contacts the collector. */
export function UsageCatalog() {
@@ -29,7 +29,7 @@ export function UsageCatalog() {
<section key={key} className="border-t border-theme pt-2">
<h4 className="font-semibold">{t(`productUsage.catalog.${key}.name`)}</h4>
<p className="text-xs"><code>{key}</code> · {definition.since}</p>
<p className="text-sm">{t('productUsage.configuredLabel')}: {t(`productUsage.catalog.${key}.configured`)}</p>
<p className="text-sm">{t(definition.configuration === 'builtin' ? 'productUsage.builtinLabel' : definition.configuration === 'flag' || definition.configuration === 'capability' ? 'productUsage.enabledLabel' : 'productUsage.configuredLabel')}: {t(`productUsage.catalog.${key}.configured`)}</p>
<p className="text-sm">{definition.used
? `${t('productUsage.usedLabel')}: ${t(`productUsage.catalog.${key}.used`)}`
: t('productUsage.configurationOnly')}</p>
@@ -69,10 +69,10 @@ beforeEach(() => {
};
});
afterEach(cleanup);
it('shows every v4 signal locally before participation, without collector calls', async () => {
it('shows every v5 signal locally before participation, without collector calls', async () => {
mount();
await screen.findByText('productUsage.catalogTitle');
expect(screen.getAllByRole('heading', { level: 4, hidden: true })).toHaveLength(87);
expect(screen.getAllByRole('heading', { level: 4, hidden: true })).toHaveLength(88);
expect(service.enable).not.toHaveBeenCalled();
expect(service.preview).not.toHaveBeenCalled();
expect(service.upgradeConsent).not.toHaveBeenCalled();
File diff suppressed because it is too large Load Diff
+44 -7
View File
@@ -1,17 +1,17 @@
{
"productUsage": {
"fields": "usage.v4-Berichte enthalten einen Installationsfingerabdruck, die PicPeak-Version, UTC-Berichtsdatum und Erstellungszeit, Schema- und Signaturmetadaten, die Galerie-Layouts aus einer festen Liste, 86 Funktionssignale (63 Paare aus Konfiguriert und Genutzt, 23 reine Konfigurationswerte) sowie zwei Gesamtzahlen der Installation: gespeicherte Galerien und Fotoeinträge ohne Videos. Entwürfe, archivierte Galerien und deren erhaltene Fotoeinträge zählen mit. Der Katalog unten erklärt jedes Feld. Keine Aktionszähler, keine Beobachtung von Besuchern.",
"catalogTitle": "Vollständiger Katalog: 86 Funktionssignale und 2 Bestandszahlen (usage.v4)",
"fields": "usage.v5-Berichte enthalten einen Installationsfingerabdruck, die PicPeak-Version, UTC-Berichtsdatum und Erstellungszeit, Schema- und Signaturmetadaten, die Galerie-Layouts aus einer festen Liste, 87 Funktionssignale (64 Paare aus Konfiguriert und Genutzt, 23 reine Konfigurationswerte) sowie zwei Gesamtzahlen der Installation: gespeicherte Galerien und Fotoeinträge ohne Videos. Entwürfe, archivierte Galerien und deren erhaltene Fotoeinträge zählen mit. Der Katalog unten erklärt jedes Feld. Keine Aktionszähler, keine Beobachtung von Besuchern.",
"catalogTitle": "Vollständiger Katalog: 87 Funktionssignale und 2 Bestandszahlen (usage.v5)",
"catalogExplanation": "„Konfiguriert“ beschreibt, ob eine Funktion technisch verfügbar oder eingerichtet ist. „Integriert“ heißt: immer verfügbar, ohne Nutzungsnachweis. „Genutzt“ ist ein einzelnes Ja/Nein für die ganze Installation seit der Zustimmung zu diesem Schema; ein angenommener Auftrag zählt als gestartet, nicht als abgeschlossen. Reine Konfigurationssignale haben kein Genutzt-Feld. Der Bestand enthält nur die aktuellen Gesamtzahlen der Galerie- und Fotoeinträge, ohne Aufschlüsselung nach Galerien. Nutzungsmarker speichern keine Personen, Objektkennungen, Zeitpunkte oder Häufigkeiten.",
"catalogSearch": "Funktionsname oder Schlüssel suchen",
"catalogEmpty": "Keine passenden Funktionen.",
"configuredLabel": "Konfiguriert",
"configuredLabel": "Verfügbarkeit / Konfiguration",
"usedLabel": "Genutzt",
"configurationOnly": "Nur Konfiguration, die tatsächliche Nutzung wird nicht erfasst.",
"versionDisclosure": "Diese Zustimmung gilt für usage.v4 / usage-consent.v4. Sie ersetzt die Frage „Erlaubt mindestens eine Galerie Downloads?“ durch „Hat mindestens eine Galerie Downloads abgeschaltet?“. Bestehende v1-, v2- und v3-Teilnahmen behalten ihren bisherigen Umfang, bis du sie ausdrücklich erweiterst. Identität und Rohhistorie bleiben erhalten, wartende Pakete werden vor dem Upgrade unverändert zugestellt. Lokale Nutzungsmarker beginnen erst nach der Bestätigung durch den Collector neu. Der erste v4-Bericht kann am nächsten aktiven UTC-Tag folgen. Das neue Signal wird vor der Bestätigung nicht erfasst.",
"versionDisclosure": "Diese Zustimmung gilt für usage.v5 / usage-consent.v5: 87 Funktionen und dieselben zwei Bestandszahlen. Neue Ja/Nein-Werte unterscheiden echte CMS-, Vorlagen-, Branding-, SEO-, Kategorie- und Ereignistyp-Änderungen sowie die Annahme echter Vorlagen-E-Mails durch den Mailtransport (auch im Hintergrund) von Standards, unverändertem Speichern, Vorschauen und Tests. Inhalte, Empfänger, Vorlagenkennungen, Aktionszeitpunkte und Häufigkeiten werden nicht erfasst. Die Beobachtung beginnt erst nach bestätigter Zustimmung; Nein bedeutet nicht, dass noch Standards verwendet werden. Frühere Versionen behalten bis zum Upgrade Bedeutung und Umfang. Wartende Pakete bleiben unverändert; Markierungen starten nach Bestätigung neu.",
"currentSchema": "Aktuelles Berichtsschema: {{schema}}",
"reviewUpgrade": "Erweiterten Umfang von usage.v4 ansehen",
"upgrade": "usage.v4 ausdrücklich zustimmen",
"reviewUpgrade": "Erweiterten Umfang von usage.v5 ansehen",
"upgrade": "usage.v5 ausdrücklich zustimmen",
"upgradeExplanation": "Deine bestehende Teilnahme behält ihren bisherigen Umfang. Sieh dir den erweiterten Katalog und die beiden Bestandszahlen an, bevor du dich entscheidest. Ablehnen beendet die Teilnahme nicht.",
"upgradePending": "Die signierte Erweiterung wartet auf die Bestätigung des Collectors. Bis dahin wird nur der bisher bestätigte Umfang erfasst. Versuche es erneut, wenn der Collector erreichbar ist, oder deaktiviere die Teilnahme, um zu stoppen und zu löschen.",
"catalog": {
@@ -425,6 +425,41 @@
"gallery_downloads_restricted": {
"name": "Galerie-Downloads eingeschränkt",
"configured": "Mindestens eine Galerie hat Downloads abgeschaltet; nur installationsweite Existenz, niemals Galeriekennungen oder Anzahlen."
},
"cms_content_editing": {
"name": "CMS-Inhalte bearbeiten",
"configured": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis.",
"used": "Ein Admin hat seit Zustimmung den Titel oder Inhalt einer internen CMS-Seite tatsächlich geändert. Unverändertes Speichern, externe Links, Logos, Standardseiten und Seitenaufrufe zählen nicht. Ob jemand die Seite gelesen hat, wird nicht gemessen."
},
"email_template_editing": {
"name": "E-Mail-Vorlagen anpassen",
"configured": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis.",
"used": "Ein Admin hat seit Zustimmung eine nicht leere Vorlage erstellt oder Betreff/Inhalt tatsächlich geändert. Standardvorlagen, unverändertes Speichern, Vorschauen und Versand zählen nicht. Frühere Anpassungen lassen sich daraus nicht ableiten."
},
"branding_editing": {
"name": "Branding anpassen",
"configured": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis.",
"used": "Ein Admin hat seit Zustimmung Branding-Einstellungen, ein Theme oder Logo geändert. Lesen und unverändertes Speichern zählen nicht. Auch das Wiederherstellen eines Standards kann eine Änderung sein; dies beschreibt nicht das aktuelle Design."
},
"seo_editing": {
"name": "SEO anpassen",
"configured": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis.",
"used": "Ein Admin hat seit Zustimmung eine festgelegte SEO-Einstellung tatsächlich geändert. Standards, Lesen und unverändertes Speichern zählen nicht; Regeln, Pfade und Suchmaschinenaktivität werden nicht erfasst."
},
"event_type_editing": {
"name": "Ereignistypen anpassen",
"configured": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis.",
"used": "Ein Admin hat seit Zustimmung einen Ereignistyp erstellt, geändert oder gelöscht. Standardvorlagen, Lesen und unverändertes Speichern zählen nicht. Namen, Vorlagen und Kennungen werden nicht gespeichert."
},
"category_editing": {
"name": "Kategorien anpassen",
"configured": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis.",
"used": "Ein Admin hat seit Zustimmung eine Kategorie erstellt, geändert oder gelöscht. Standardkategorien, Lesen und unverändertes Speichern zählen nicht. Namen, Zuordnungen und Kennungen werden nicht gespeichert."
},
"email_template_delivery": {
"name": "E-Mails mit Vorlagen versendet",
"configured": "Integrierte Funktion ist verfügbar; dies ist kein Nutzungsnachweis.",
"used": "Seit Zustimmung wurde mindestens eine echte Vorlagen-E-Mail vom SMTP-Transport oder Mail-Webhook angenommen, auch bei Hintergrundversand. Vorschauen, Testmails und Nachrichten ohne Vorlage zählen nicht. Dies belegt weder Empfang noch Lesen. Vorlagenkennung, Empfänger, Inhalte, Nachrichtenkennung, Versandzeit und Anzahl werden nicht in Nutzungsmarkierungen gespeichert."
}
},
"auditTitle": "Export- und Löschquittungen",
@@ -522,7 +557,9 @@
"name": "Gespeicherte Fotoeinträge",
"description": "Aktuelle Anzahl der Fotoeinträge ohne Videos, einschließlich RAW, Gast-Uploads und Einträgen archivierter Galerien. Eine Gesamtzahl der Installation; keine eindeutigen Dateien, Vorschaubilder, Verarbeitungserfolge oder Fotoinhalte. Gelöschte Einträge zählen nicht."
}
}
},
"builtinLabel": "Fest integriert — Verfügbarkeit bedeutet keine Nutzung",
"enabledLabel": "Aktiviert — kann ein Standard sein"
},
"userManagement": {
"title": "Benutzerverwaltung",
+44 -7
View File
@@ -1,17 +1,17 @@
{
"productUsage": {
"fields": "usage.v4 reports contain an installation fingerprint, PicPeak version, UTC report date and generation time, schema/signing metadata, controlled gallery layouts, 86 fixed capability signals (63 configured/used pairs and 23 configuration-only booleans), and two installation totals: stored galleries and photo records excluding videos. Drafts and archived galleries and their retained photo records are included. The catalog below defines every field. There are no action counts or visitor observations.",
"catalogTitle": "Full catalog: 86 capability signals and 2 inventory totals (usage.v4)",
"fields": "usage.v5 reports contain an installation fingerprint, PicPeak version, UTC report date and generation time, schema/signing metadata, controlled gallery layouts, 87 fixed capability signals (64 configured/used pairs and 23 configuration-only booleans), and two installation totals: stored galleries and photo records excluding videos. Drafts and archived galleries and their retained photo records are included. The catalog below defines every field. There are no action counts or visitor observations.",
"catalogTitle": "Full catalog: 87 capability signals and 2 inventory totals (usage.v5)",
"catalogExplanation": "Configured describes current technical availability or configuration. Built-in means available, not used. Used is one installation-wide yes/no bit since consent to the reporting schema; accepted jobs mean initiated, not necessarily completed. Configuration-only capabilities omit used. Inventory contains only current gallery/photo record totals, with no per-gallery breakdown. No actor, entity identifier, action time or frequency is stored in usage markers.",
"catalogSearch": "Search capability name or key",
"catalogEmpty": "No matching capabilities.",
"configuredLabel": "Configured",
"configuredLabel": "Availability / configuration",
"usedLabel": "Used",
"configurationOnly": "Configuration only — actual use is not collected.",
"versionDisclosure": "This consent covers usage.v4 / usage-consent.v4. It replaces the question “does any gallery allow downloads?” with “does any gallery have downloads switched off?”. Existing v1/v2/v3 participants keep their exact previous scope until they explicitly upgrade. Identity and raw history remain; pending packets are delivered unchanged before an upgrade. Local usage markers restart only after confirmation. The first v4 report may be on the next active UTC day. The new signal is never collected before confirmation.",
"versionDisclosure": "This consent covers usage.v5 / usage-consent.v5: 87 capabilities and the same two inventory totals. New booleans distinguish real CMS/template/branding/SEO/category/event-type edits and actual template-email transport acceptance, including background sends, from defaults, unchanged saves, previews and tests. No content, recipient, template key, action timestamp or frequency is collected. Observations begin only after accepted consent; false does not mean an installation still uses defaults. Previous versions keep their meanings and scope until upgrade; queued packets stay unchanged and markers restart after confirmation.",
"currentSchema": "Current reporting schema: {{schema}}",
"reviewUpgrade": "Review expanded usage.v4 scope",
"upgrade": "Explicitly agree to usage.v4",
"reviewUpgrade": "Review expanded usage.v5 scope",
"upgrade": "Explicitly agree to usage.v5",
"upgradeExplanation": "Your existing participation keeps its current scope. Review the expanded catalog and the two inventory totals before deciding whether to upgrade. Declining does not end participation.",
"upgradePending": "The signed consent upgrade is pending confirmation. Only the previously accepted scope is collected. Retry when the collector is available, or disable participation to stop and delete.",
"catalog": {
@@ -425,6 +425,41 @@
"gallery_downloads_restricted": {
"name": "Gallery downloads restricted",
"configured": "At least one gallery has downloads switched off; only existence across the installation, never gallery IDs or counts."
},
"cms_content_editing": {
"name": "CMS content editing",
"configured": "Built-in capability is available; this is not evidence of use.",
"used": "An admin saved a real change to an internal CMS page title or body since consent. Unchanged saves, external links, logos, seeded pages and page views do not count. This does not measure whether anyone read the page."
},
"email_template_editing": {
"name": "Email template customization",
"configured": "Built-in capability is available; this is not evidence of use.",
"used": "An admin created a nonempty template or saved a real subject/body change since consent. Defaults, unchanged saves, previews and sending are excluded. This does not establish the current customization of templates edited before consent."
},
"branding_editing": {
"name": "Branding customization",
"configured": "Built-in capability is available; this is not evidence of use.",
"used": "An admin changed branding settings, a theme or a logo since consent. Reading settings and unchanged saves do not count. A change can also restore a default; this is not a claim about the current design."
},
"seo_editing": {
"name": "SEO customization",
"configured": "Built-in capability is available; this is not evidence of use.",
"used": "An admin saved a real change to an allowlisted SEO setting since consent. Defaults, reading and unchanged saves do not count; no rules, paths or search-engine activity are collected."
},
"event_type_editing": {
"name": "Event type customization",
"configured": "Built-in capability is available; this is not evidence of use.",
"used": "An admin created, changed or deleted an event type since consent. Seeded presets, reading and unchanged saves do not count. No names, presets or identifiers are retained."
},
"category_editing": {
"name": "Category customization",
"configured": "Built-in capability is available; this is not evidence of use.",
"used": "An admin created, changed or deleted a category since consent. Seeded categories, reading and unchanged saves do not count. No names, memberships or identifiers are retained."
},
"email_template_delivery": {
"name": "Emails sent using templates",
"configured": "Built-in capability is available; this is not evidence of use.",
"used": "At least one real template email was accepted by SMTP or the configured mail webhook since consent, including background sends. Previews, test messages and template-free messages are excluded. Acceptance does not prove receipt or reading. No template key, recipient, contents, message identifier, send time or count is stored in usage markers."
}
},
"auditTitle": "Export and deletion receipts",
@@ -522,7 +557,9 @@
"name": "Stored photo records",
"description": "Current number of non-video photo records, including RAW, guest uploads and records of archived galleries. One total for the installation; not unique files, thumbnails, processing success or photo contents. Deleted records are excluded."
}
}
},
"builtinLabel": "Built in — availability is not use",
"enabledLabel": "Enabled — may be a default"
},
"userManagement": {
"title": "User Management",
@@ -49,12 +49,12 @@ export const productUsageService = {
async enable(): Promise<UsageStatus> {
return (
await api.post('/admin/usage/enable', {
consent_version: 'usage-consent.v4'
consent_version: 'usage-consent.v5'
})
).data;
},
async upgradeConsent(): Promise<{ delivered: boolean; queued: boolean; state: UsageStatus }> {
return (await api.post('/admin/usage/consent', { consent_version: 'usage-consent.v4' })).data;
return (await api.post('/admin/usage/consent', { consent_version: 'usage-consent.v5' })).data;
},
async disable(): Promise<UsageStatus> {
return (await api.post('/admin/usage/disable')).data;