fix(usage): keep the settings tab usable on a bad collector URL, and report layouts and CSS accurately

Three items, one of which explains an error seen in the app.

"The operation could not be completed" could come from a config typo.
status() called collectorUrl() bare, and that throws on a bare hostname,
a path, a query, or http in production. The settings page renders one
generic failure when its status query errors, so a misconfigured
USAGE_COLLECTOR_URL replaced the whole tab with that sentence — no
cause, and no way to read the status or withdraw, because every control
there sits behind that call. The URL is now reported as
collector_error: 'INVALID_COLLECTOR_URL' beside the real state, the tab
says what is wrong and how to fix it, and the links are only rendered
when there is somewhere to point them.

gallery_layouts reported grid for every preset-themed install.
color_theme holds either a theme object or the NAME of a preset — the
theme picker stores names, and eventTypeService seeds them
(`theme_preset: 'corporateTimeline'`). Only reading value.galleryLayout
made masonry, timeline, mosaic and the two gallery presets invisible.
Names now resolve, and an event with no theme of its own resolves
through the global one instead of being counted as grid. Only the
name -> layout mapping is duplicated, not the presets;
frontend/src/types/theme.types.ts stays the source of truth, and an
unknown name reports `other` so a preset added later degrades to
"something else" rather than quietly inflating the grid count.

custom_css missed CSS applied through a template. An enabled
css_templates row applied via events.css_template_id is gallery styling
by the same definition as the settings fields — the Custom CSS tab is
where both are authored — but neither the snapshot nor the middleware
saw it, so those installs reported custom_css entirely false. Existence
only; template contents are never read.

Eleven tests. Reverting each fix in turn fails 3, 1 and 3 of them.

Refs #1110
This commit is contained in:
Paul Nothaft
2026-09-05 22:33:25 +02:00
parent 9785b636a9
commit bb76ca5375
6 changed files with 246 additions and 17 deletions
@@ -155,6 +155,14 @@ export default function ProductUsageTab() {
{data.last_report_date && (
<p>{t('productUsage.lastReport', { date: data.last_report_date })}</p>
)}
{data.collector_error === 'INVALID_COLLECTOR_URL' && (
<p role="alert" className="text-amber-700 dark:text-amber-300">
{/* Shown alongside the real controls, not instead of them: with a
bad URL the operator still needs to read their status and
still needs to be able to withdraw. */}
{t('productUsage.invalidCollectorUrl')}
</p>
)}
{data.last_error && (
<p role="status">
{/* Retrying cannot fix an unreadable signing key, and neither can
@@ -210,14 +218,16 @@ export default function ProductUsageTab() {
</Button>
</>
)}
<a
className="text-sm text-primary-600 dark:text-primary-400 hover:underline self-center"
href={`${data.collector_url}/transparency`}
target="_blank"
rel="noreferrer"
>
{t('productUsage.transparency')}
</a>
{data.collector_url && (
<a
className="text-sm text-primary-600 dark:text-primary-400 hover:underline self-center"
href={`${data.collector_url}/transparency`}
target="_blank"
rel="noreferrer"
>
{t('productUsage.transparency')}
</a>
)}
</div>
</Card>
{active && (
@@ -449,7 +459,7 @@ export default function ProductUsageTab() {
{message && <p role="status">{message}</p>}
{consent && (
<ConsentDialog
collector={data.collector_url}
collector={data.collector_url ?? ''}
busy={busy}
close={() => setConsent(false)}
enable={() =>
+1
View File
@@ -26,6 +26,7 @@
"hash": "Dein vertraulicher Abfrage-Hash",
"lastReport": "Zuletzt angenommener Bericht: {{date}} (UTC)",
"deliveryProblem": "Die Übertragung benötigt Aufmerksamkeit. Bei Löschung oder Identitätskonflikt ist die Erfassung gestoppt. Versuche es erneut oder deaktiviere die Teilnahme, um die Daten zu löschen.",
"invalidCollectorUrl": "Die konfigurierte Collector-URL ist ungültig, daher kann die Teilnahme weder gestartet noch übermittelt werden. Setzen Sie USAGE_COLLECTOR_URL auf einen https-Origin ohne Pfad, Query oder Zugangsdaten (oder lassen Sie sie leer, um den Standard zu verwenden).",
"signingKeyUnreadable": "Der Signaturschlüssel für die Nutzungsdaten kann nicht gelesen werden. Meist wurde USAGE_ENCRYPTION_KEY — oder das als Rückfallwert genutzte JWT_SECRET — geändert. Berichte können nicht gesendet und auch die Löschanfrage kann nicht signiert werden. Stellen Sie das ursprüngliche Schlüsselmaterial wieder her, um die Löschung abzuschließen; erneutes Senden oder Deaktivieren allein behebt dies nicht.",
"inspect": "Genau sehen, was geteilt wird",
"preview": "Nächsten Bericht ansehen",
+1
View File
@@ -26,6 +26,7 @@
"hash": "Your private lookup hash",
"lastReport": "Last accepted report: {{date}} (UTC)",
"deliveryProblem": "Delivery needs attention. Collection stops during deletion or an identity conflict. Use retry, or disable participation to delete its data.",
"invalidCollectorUrl": "The configured usage collector URL is not valid, so participation cannot be started or delivered. Set USAGE_COLLECTOR_URL to an https origin with no path, query or credentials (or leave it unset to use the default).",
"signingKeyUnreadable": "The usage signing key cannot be read, which usually means USAGE_ENCRYPTION_KEY — or JWT_SECRET, which it falls back to — was changed. Reports cannot be sent and the deletion request cannot be signed either. Restore the original encryption material to finish deletion; retrying or disabling will not resolve it on its own.",
"inspect": "See exactly what is shared",
"preview": "Preview next report",
@@ -9,7 +9,8 @@ export interface UsageStatus {
| 'identity_conflict';
notice_dismissed: boolean;
installation_id: string | null;
collector_url: string;
collector_url: string | null;
collector_error?: 'INVALID_COLLECTOR_URL' | null;
schema_version: string;
last_report_date: string | null;
last_error: string | null;