feat: dashboard widgets, auth fix, profile names, dynamic groups

- Add VehicleDashboardCard: self-contained widget modelled after
  AtemschutzDashboardCard, shows einsatzbereit ratio and inspection
  warnings inline; replaces StatsCard + InspectionAlerts in Dashboard

- Add EquipmentDashboardCard: consolidated equipment status widget
  showing only aggregated counts (no per-item listing); replaces
  EquipmentAlerts component in Dashboard

- Fix auth race condition: add authInitialized flag to api.ts so 401
  responses during initial token validation no longer trigger a
  spurious redirect to /login; save intended destination before login
  redirect and restore it after successful auth callback

- Fix profile firstname/lastname: add extractNames() helper to
  auth.controller.ts that falls back to splitting userinfo.name when
  Authentik does not provide separate given_name/family_name fields;
  applied on both create and update paths

- Dynamic groups endpoint: replace hardcoded KNOWN_GROUPS array in
  events.controller.ts with a DB query (SELECT DISTINCT unnest
  (authentik_groups) FROM users); known slugs get German labels via
  lookup map, unknown slugs are humanized automatically

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthias Hochmeister
2026-03-03 10:28:31 +01:00
parent 831927ae90
commit 2306741c4d
11 changed files with 470 additions and 110 deletions

View File

@@ -430,6 +430,48 @@ class EventsService {
return { token, subscribeUrl };
}
// -------------------------------------------------------------------------
// GROUPS
// -------------------------------------------------------------------------
/**
* Returns distinct group slugs from active users as { id, label } pairs.
* The label is derived from a known-translations map or humanized from the slug.
*/
async getAvailableGroups(): Promise<Array<{ id: string; label: string }>> {
const knownLabels: Record<string, string> = {
'dashboard_admin': 'Administratoren',
'dashboard_mitglied': 'Mitglieder',
'dashboard_fahrmeister': 'Fahrmeister',
'dashboard_zeugmeister': 'Zeugmeister',
'dashboard_atemschutz': 'Atemschutzwart',
'dashboard_jugend': 'Feuerwehrjugend',
'dashboard_kommandant': 'Kommandanten',
'dashboard_moderator': 'Moderatoren',
'feuerwehr-admin': 'Feuerwehr Admin',
'feuerwehr-kommandant': 'Feuerwehr Kommandant',
};
const humanizeGroupName = (slug: string): string => {
if (knownLabels[slug]) return knownLabels[slug];
const stripped = slug.startsWith('dashboard_') ? slug.slice('dashboard_'.length) : slug;
const spaced = stripped.replace(/-/g, ' ');
return spaced.charAt(0).toUpperCase() + spaced.slice(1);
};
const result = await pool.query(
`SELECT DISTINCT unnest(authentik_groups) AS group_name
FROM users
WHERE is_active = true
ORDER BY group_name`
);
return result.rows.map((row) => ({
id: row.group_name as string,
label: humanizeGroupName(row.group_name as string),
}));
}
// -------------------------------------------------------------------------
// ICAL EXPORT
// -------------------------------------------------------------------------