@bardioc/create-bardioc-app 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +7 -13
  2. package/bin/create.mjs +5 -5
  3. package/package.json +1 -1
  4. package/src/scaffold.js +2 -2
  5. package/templates/_base/.changeset/README.md +3 -3
  6. package/templates/_base/.claude/commands/changeset-app.md +5 -5
  7. package/templates/_base/.claude/commands/refresh-bundle.md +5 -5
  8. package/templates/_base/README.md +9 -9
  9. package/templates/angular/package.json +6 -6
  10. package/templates/nextjs/.env.example +1 -1
  11. package/templates/nextjs/package.json +1 -1
  12. package/templates/preact/package.json +1 -1
  13. package/templates/solid/package.json +1 -1
  14. package/templates/svelte/package.json +1 -1
  15. package/templates/vite/.env.example +1 -1
  16. package/templates/vite/CLAUDE.md +199 -59
  17. package/templates/vite/package.json +5 -1
  18. package/templates/vite/public/app-manifest.json +25 -0
  19. package/templates/vite/src/App.tsx +11 -133
  20. package/templates/vite/src/auth/onboarding.tsx +67 -0
  21. package/templates/vite/src/climate/climate-data.ts +92 -0
  22. package/templates/vite/src/climate/conditions-chart.tsx +38 -0
  23. package/templates/vite/src/climate/location-card.tsx +50 -0
  24. package/templates/vite/src/climate/location-select.tsx +26 -0
  25. package/templates/vite/src/climate/metrics-grid.tsx +22 -0
  26. package/templates/vite/src/climate/overview-view.tsx +52 -0
  27. package/templates/vite/src/climate/stations-view.tsx +149 -0
  28. package/templates/vite/src/climate/types.ts +93 -0
  29. package/templates/vite/src/climate/use-climate.ts +123 -0
  30. package/templates/vite/src/dashboard-provider.tsx +27 -0
  31. package/templates/vite/src/events/live-events-view.tsx +74 -0
  32. package/templates/vite/src/graph/explorer-view.tsx +118 -0
  33. package/templates/vite/src/graph/requests.ts +142 -0
  34. package/templates/vite/src/i18n.tsx +68 -0
  35. package/templates/vite/src/index.css +12 -64
  36. package/templates/vite/src/locales/de.json +141 -0
  37. package/templates/vite/src/locales/en.json +141 -0
  38. package/templates/vite/src/main.tsx +10 -4
  39. package/templates/vite/src/profile/profile-view.tsx +75 -0
  40. package/templates/vite/src/profile/use-profile.ts +52 -0
  41. package/templates/vite/src/shell/about-dialog.tsx +129 -0
  42. package/templates/vite/src/shell/app-shell.tsx +160 -0
  43. package/templates/vite/src/shell/header.tsx +39 -0
  44. package/templates/vite/src/use-app-notify.ts +23 -0
  45. package/templates/vite/src/vite-env.d.ts +5 -5
  46. package/templates/vue/package.json +1 -1
@@ -0,0 +1,118 @@
1
+ import { isInsideIframe } from '@bardioc/app-sdk/dev';
2
+ import { useSdk } from '@bardioc/app-sdk/react';
3
+ import { Badge, Button, Card } from '@bardioc/ui/components';
4
+ import { useState } from 'react';
5
+ import { useTranslation } from '../i18n';
6
+ import { useAppNotify } from '../use-app-notify';
7
+ import { SDK_REQUESTS, type SdkRequest } from './requests';
8
+
9
+ type Result =
10
+ | { status: 'idle' }
11
+ | { status: 'running' }
12
+ | { status: 'done'; live: boolean; ms: number; data: unknown };
13
+
14
+ export function ExplorerView() {
15
+ const { t } = useTranslation();
16
+ const bridge = useSdk();
17
+ const notify = useAppNotify();
18
+ const [activeId, setActiveId] = useState(SDK_REQUESTS[0].id);
19
+ const [result, setResult] = useState<Result>({ status: 'idle' });
20
+
21
+ const active = SDK_REQUESTS.find(request => request.id === activeId) ?? SDK_REQUESTS[0];
22
+
23
+ async function run(request: SdkRequest) {
24
+ setActiveId(request.id);
25
+ setResult({ status: 'running' });
26
+ const started = Date.now();
27
+ try {
28
+ const data = await request.run(bridge);
29
+ const ms = Date.now() - started;
30
+ setResult({ status: 'done', live: true, ms, data });
31
+ notify(t('notify.queryOk', { fallback: 'Request ok ({ms} ms)', params: { ms } }), 'success');
32
+ } catch {
33
+ const ms = Date.now() - started;
34
+ setResult({ status: 'done', live: false, ms, data: request.mock() });
35
+ notify(
36
+ t('notify.queryFailed', { fallback: 'Request failed — showing simulated data' }),
37
+ 'warning'
38
+ );
39
+ }
40
+ }
41
+
42
+ return (
43
+ <div className="flex h-full min-w-0 flex-col gap-3">
44
+ <div>
45
+ <h2 className="text-lg font-semibold">
46
+ {t('explorer.title', { fallback: 'Graph Explorer' })}
47
+ </h2>
48
+ <p className="text-muted-foreground text-xs">
49
+ {t('explorer.description', { fallback: '' })}
50
+ </p>
51
+ </div>
52
+
53
+ <div className="grid min-w-0 gap-3 lg:grid-cols-[minmax(0,240px)_minmax(0,1fr)]">
54
+ <div className="flex min-w-0 flex-col gap-1">
55
+ {SDK_REQUESTS.map(request => (
56
+ <button
57
+ key={request.id}
58
+ onClick={() => setActiveId(request.id)}
59
+ className={`flex min-w-0 items-center justify-between gap-2 rounded border px-2 py-1.5 text-left font-mono text-xs ${
60
+ request.id === activeId
61
+ ? 'border-primary bg-muted'
62
+ : 'border-border hover:bg-muted/50'
63
+ }`}
64
+ >
65
+ <span className="truncate">{request.label}</span>
66
+ <Badge variant={request.category === 'graph' ? 'info' : 'secondary'} numerical>
67
+ {t(`explorer.category${request.category === 'graph' ? 'Graph' : 'Os'}`, {
68
+ fallback: request.category,
69
+ })}
70
+ </Badge>
71
+ </button>
72
+ ))}
73
+ </div>
74
+
75
+ <Card className="min-h-0 min-w-0">
76
+ <div className="flex h-full min-h-0 min-w-0 flex-col gap-2">
77
+ <div className="flex min-w-0 items-center justify-between gap-2">
78
+ <span className="text-muted-foreground truncate font-mono text-xs">
79
+ {active.label}
80
+ </span>
81
+ <Button
82
+ size="sm"
83
+ onClick={() => void run(active)}
84
+ disabled={result.status === 'running'}
85
+ >
86
+ {result.status === 'running'
87
+ ? t('explorer.running', { fallback: 'Running…' })
88
+ : t('explorer.run', { fallback: 'Run request' })}
89
+ </Button>
90
+ </div>
91
+
92
+ {result.status === 'done' && (
93
+ <div className="flex items-center gap-2 text-xs">
94
+ <Badge variant={result.live ? 'success' : 'warning'}>
95
+ {result.live
96
+ ? t('explorer.live', { fallback: 'Live' })
97
+ : t('explorer.mock', { fallback: 'Simulated' })}
98
+ </Badge>
99
+ <span className="text-muted-foreground">
100
+ {t('explorer.durationMs', { fallback: '{ms} ms', params: { ms: result.ms } })}
101
+ </span>
102
+ {!isInsideIframe() && <span className="text-muted-foreground">· standalone</span>}
103
+ </div>
104
+ )}
105
+
106
+ <pre className="bg-muted/40 text-foreground min-h-40 flex-1 overflow-auto rounded p-3 text-[11px] leading-5">
107
+ {result.status === 'done'
108
+ ? JSON.stringify(result.data, null, 2)
109
+ : t('explorer.empty', {
110
+ fallback: 'Select a request and run it to see the response',
111
+ })}
112
+ </pre>
113
+ </div>
114
+ </Card>
115
+ </div>
116
+ </div>
117
+ );
118
+ }
@@ -0,0 +1,142 @@
1
+ import type { HostBridge } from '@bardioc/app-sdk';
2
+
3
+ export type RequestCategory = 'graph' | 'os';
4
+
5
+ export interface SdkRequest {
6
+ id: string;
7
+ category: RequestCategory;
8
+ label: string;
9
+ run: (bridge: HostBridge) => Promise<unknown>;
10
+ mock: () => unknown;
11
+ }
12
+
13
+ interface GraphNode {
14
+ 'ogit/_id'?: string;
15
+ 'ogit/_type'?: string;
16
+ }
17
+
18
+ // graph.query resolves to either a bare array or a { items: [...] } envelope depending on the
19
+ // backend. Normalize so the lookup/traversal presets can pull a real id and type from live data
20
+ // instead of hard-coding ids/types that may not exist in a given instance.
21
+ function toItems(result: unknown): GraphNode[] {
22
+ if (Array.isArray(result)) {
23
+ return result as GraphNode[];
24
+ }
25
+ if (
26
+ result &&
27
+ typeof result === 'object' &&
28
+ Array.isArray((result as { items?: unknown }).items)
29
+ ) {
30
+ return (result as { items: GraphNode[] }).items;
31
+ }
32
+ return [];
33
+ }
34
+
35
+ async function sampleNodes(bridge: HostBridge, limit: number): Promise<GraphNode[]> {
36
+ return toItems(await bridge.transport.graph.query<GraphNode>('*', { limit }));
37
+ }
38
+
39
+ // The full surface a hosted app can reach over the SDK transport. Each `run` issues a real call
40
+ // against the live instance; `mock` returns a shape-representative fallback so the Explorer still
41
+ // works offline. The graph calls mirror what the Graph Explorer app issues: a wildcard query, a
42
+ // query by a type that actually exists, a node lookup, and a Gremlin neighbor traversal — the
43
+ // lookup/traversal presets discover a real node from the wildcard query first, so nothing is
44
+ // hard-coded to a specific instance.
45
+ export const SDK_REQUESTS: SdkRequest[] = [
46
+ {
47
+ id: 'graph.query.all',
48
+ category: 'graph',
49
+ label: "graph.query('*', { limit: 10 })",
50
+ run: bridge => bridge.transport.graph.query('*', { limit: 10 }),
51
+ mock: () => ({
52
+ items: [
53
+ {
54
+ 'ogit/_id': 'cmexample_app-1',
55
+ 'ogit/_type': 'ogit/Auth/Application',
56
+ 'ogit/name': 'Sample application',
57
+ },
58
+ ],
59
+ }),
60
+ },
61
+ {
62
+ id: 'graph.query.type',
63
+ category: 'graph',
64
+ label: String.raw`graph.query('ogit\/_type:"<first result type>"', { limit: 5 })`,
65
+ run: async bridge => {
66
+ const [first] = await sampleNodes(bridge, 1);
67
+ const type = first?.['ogit/_type'];
68
+ if (!type) {
69
+ return [];
70
+ }
71
+ const escapedType = type.replaceAll('/', String.raw`\/`);
72
+ return bridge.transport.graph.query(String.raw`ogit\/_type:"${escapedType}"`, { limit: 5 });
73
+ },
74
+ mock: () => ({
75
+ items: [
76
+ {
77
+ 'ogit/_id': 'cmexample_var-1',
78
+ 'ogit/_type': 'ogit/Automation/Variable',
79
+ 'ogit/name': 'Sample variable',
80
+ },
81
+ ],
82
+ }),
83
+ },
84
+ {
85
+ id: 'graph.get',
86
+ category: 'graph',
87
+ label: 'graph.get(firstNodeId)',
88
+ run: async bridge => {
89
+ const [first] = await sampleNodes(bridge, 1);
90
+ const id = first?.['ogit/_id'];
91
+ if (!id) {
92
+ return null;
93
+ }
94
+ return bridge.transport.graph.get(id);
95
+ },
96
+ mock: () => ({
97
+ 'ogit/_id': 'cmexample_app-1',
98
+ 'ogit/_type': 'ogit/Auth/Application',
99
+ 'ogit/name': 'Sample application',
100
+ }),
101
+ },
102
+ {
103
+ id: 'graph.gremlin',
104
+ category: 'graph',
105
+ label: "graph.gremlin(nodeId, 'both()')",
106
+ // `both()` returns adjacent vertices in either direction. Replace this id with a connected
107
+ // node from your own instance — many nodes (e.g. config vertices) have no traversable edges.
108
+ run: bridge =>
109
+ bridge.transport.graph.gremlin(
110
+ 'cjohdg6y80000ai48sw7swsq0_cmm2b2jz3t6dq0110poeobock',
111
+ 'both()'
112
+ ),
113
+ mock: () => [
114
+ {
115
+ 'ogit/_id': 'cmexample_node-2',
116
+ 'ogit/_type': 'ogit/Automation/Variable',
117
+ edge: 'ogit/uses',
118
+ },
119
+ ],
120
+ },
121
+ {
122
+ id: 'os.profile',
123
+ category: 'os',
124
+ label: 'os.profile.get()',
125
+ run: bridge => bridge.transport.os.profile.get(),
126
+ mock: () => ({ id: 'usr-demo', email: 'operator@bardioc.dev', name: 'Demo Operator' }),
127
+ },
128
+ {
129
+ id: 'os.organization',
130
+ category: 'os',
131
+ label: 'os.organization.getStructure()',
132
+ run: bridge => bridge.transport.os.organization.getStructure(),
133
+ mock: () => ({ id: 'org-demo', name: 'Bardioc Climate Corp', units: [], members: [] }),
134
+ },
135
+ {
136
+ id: 'os.applications',
137
+ category: 'os',
138
+ label: 'os.applications.list()',
139
+ run: bridge => bridge.transport.os.applications.list(),
140
+ mock: () => [{ id: 'app-demo', name: 'Climate Dashboard', version: '0.0.1' }],
141
+ },
142
+ ];
@@ -0,0 +1,68 @@
1
+ import { useOsConfig } from '@bardioc/app-sdk/react';
2
+ import {
3
+ I18nProvider as BaseI18nProvider,
4
+ mergeTranslations,
5
+ useI18n,
6
+ type I18nConfig,
7
+ type TranslationMessages,
8
+ } from '@bardioc/i18n';
9
+ import { deMessages as baseDe, enMessages as baseEn } from '@bardioc/i18n/translations';
10
+ import { SUPPORTED_LOCALES, type Locale } from '@bardioc/types';
11
+ import { useEffect, useMemo, type ReactNode } from 'react';
12
+ import deMessages from './locales/de.json';
13
+ import enMessages from './locales/en.json';
14
+
15
+ // Layer the app's strings over the @bardioc base bundle so built-in @bardioc/ui component text
16
+ // (table pagination, combobox, sidebar, window controls, …) is translated too. App keys win.
17
+ const DEFAULT_MESSAGES: Record<Locale, TranslationMessages> = {
18
+ en: mergeTranslations(baseEn, enMessages),
19
+ de: mergeTranslations(baseDe, deMessages),
20
+ };
21
+
22
+ const STORAGE_KEY = '{{APP_NAME}}.locale';
23
+
24
+ function isSupportedLocale(locale: string | null | undefined): locale is Locale {
25
+ return typeof locale === 'string' && SUPPORTED_LOCALES.includes(locale as Locale);
26
+ }
27
+
28
+ function OsLocaleSync() {
29
+ const osConfig = useOsConfig();
30
+ const { setLocale } = useI18n();
31
+ const osLanguage = osConfig?.language;
32
+
33
+ // React only to OS language changes — depending on the local locale would revert a manual
34
+ // switch made via the in-app language switcher.
35
+ useEffect(() => {
36
+ if (isSupportedLocale(osLanguage)) {
37
+ setLocale(osLanguage);
38
+ }
39
+ }, [osLanguage, setLocale]);
40
+
41
+ return null;
42
+ }
43
+
44
+ interface AppI18nProviderProps {
45
+ children: ReactNode;
46
+ syncWithOs?: boolean;
47
+ }
48
+
49
+ export function AppI18nProvider({ children, syncWithOs = false }: Readonly<AppI18nProviderProps>) {
50
+ const config = useMemo<I18nConfig<Locale>>(
51
+ () => ({
52
+ defaultLocale: 'en',
53
+ supportedLocales: [...SUPPORTED_LOCALES],
54
+ defaultMessages: DEFAULT_MESSAGES,
55
+ storageKey: STORAGE_KEY,
56
+ }),
57
+ []
58
+ );
59
+
60
+ return (
61
+ <BaseI18nProvider config={config}>
62
+ {syncWithOs ? <OsLocaleSync /> : null}
63
+ {children}
64
+ </BaseI18nProvider>
65
+ );
66
+ }
67
+
68
+ export { useI18n, useTranslation } from '@bardioc/i18n';
@@ -1,76 +1,24 @@
1
1
  @import 'tailwindcss';
2
+ @import '@bardioc/ui/styles/theme.css';
3
+ @import '@bardioc/ui/styles/components.css';
4
+
5
+ @source './**/*.{ts,tsx}';
2
6
 
3
7
  * {
4
- box-sizing: border-box;
8
+ box-sizing: border-box;
5
9
  }
6
10
 
7
11
  html,
8
12
  body,
9
13
  #root {
10
- margin: 0;
11
- padding: 0;
12
- height: 100%;
13
- width: 100%;
14
- overflow: hidden;
15
- }
16
-
17
- :root {
18
- --background: oklch(17.88% 0.0075 229.38);
19
- --foreground: oklch(98.69% 0.0093 99.98);
20
- --muted-foreground: oklch(86.03% 0.0056 95.11);
21
- --border: oklch(1 0 0 / 10%);
22
- --primary: oklch(97.02% 0 0);
23
- --primary-foreground: oklch(36.94% 0.0053 236.64);
24
- --destructive: oklch(68.04% 0.1386 21.38);
25
- --ring: oklch(79.43% 0.0084 98.91);
14
+ margin: 0;
15
+ padding: 0;
16
+ height: 100%;
17
+ width: 100%;
18
+ overflow: hidden;
26
19
  }
27
20
 
28
21
  body {
29
- background: var(--background);
30
- color: var(--foreground);
31
- font-family: system-ui, -apple-system, sans-serif;
32
- }
33
-
34
- code {
35
- font-family: inherit;
36
- }
37
-
38
- .bg-background {
39
- background-color: var(--background);
40
- }
41
-
42
- .text-foreground {
43
- color: var(--foreground);
44
- }
45
-
46
- .text-muted-foreground {
47
- color: var(--muted-foreground);
48
- }
49
-
50
- .border-border {
51
- border-color: var(--border);
52
- }
53
-
54
- .bg-primary {
55
- background-color: var(--primary);
56
- }
57
-
58
- .text-primary-foreground {
59
- color: var(--primary-foreground);
60
- }
61
-
62
- .text-destructive {
63
- color: var(--destructive);
64
- }
65
-
66
- .focus\:ring-ring:focus {
67
- --tw-ring-color: var(--ring);
68
- }
69
-
70
- .hover\:bg-primary\/90:hover {
71
- background-color: oklch(from var(--primary) calc(l * 0.9) c h);
72
- }
73
-
74
- .hover\:text-foreground:hover {
75
- color: var(--foreground);
22
+ -webkit-font-smoothing: antialiased;
23
+ -moz-osx-font-smoothing: grayscale;
76
24
  }
@@ -0,0 +1,141 @@
1
+ {
2
+ "app": { "title": "Wetter-Dashboard", "tagline": "Planetare Messwerte in Echtzeit" },
3
+ "nav": {
4
+ "overview": "Übersicht",
5
+ "stations": "Stationen",
6
+ "explorer": "Graph-Explorer",
7
+ "events": "Live-Ereignisse",
8
+ "profile": "Profil"
9
+ },
10
+ "menu": {
11
+ "view": "Ansicht",
12
+ "about": "Über diese App"
13
+ },
14
+ "about": {
15
+ "title": "Über diese App",
16
+ "version": "Version",
17
+ "id": "App-ID",
18
+ "description": "Beschreibung",
19
+ "permissions": "Berechtigungen",
20
+ "error": "app-manifest.json konnte nicht gelesen werden",
21
+ "source": "Live aus public/app-manifest.json gelesen"
22
+ },
23
+ "genericTable": {
24
+ "filterPlaceholder": "Ergebnisse filtern...",
25
+ "itemLabel": "Zeilen",
26
+ "showing": "{from} bis {to} von {total}",
27
+ "columns": "Spalten",
28
+ "noResults": "Keine Ergebnisse gefunden.",
29
+ "goToPreviousPage": "Zur vorherigen Seite",
30
+ "goToNextPage": "Zur nächsten Seite"
31
+ },
32
+ "common": { "button": { "previous": "Zurück", "next": "Weiter" } },
33
+ "header": {
34
+ "search": "Stationen suchen…",
35
+ "sync": "Sync",
36
+ "syncing": "Synchronisiere…",
37
+ "location": "Station",
38
+ "savedAt": "Gespeichert {time}",
39
+ "neverSaved": "Noch nicht gespeichert"
40
+ },
41
+ "auth": {
42
+ "title": "Mit Bardioc OS verbinden",
43
+ "description": "Dieses Dashboard liest Live-Daten über den Bardioc-SDK-Transport. Es läuft in Bardioc OS oder eigenständig mit einer CLI-Dev-Session.",
44
+ "stepsTitle": "Eigenständig mit Dev-Session ausführen:",
45
+ "step1": "Setze VITE_ENABLE_DEV_AUTH=true und DEV_AUTH_HOST_URL=<dein-host> in .env",
46
+ "step2": "Einmal im App-Ordner autorisieren:",
47
+ "step3": "Dev-Server starten — der Session-Key wird automatisch erzeugt und erneuert:",
48
+ "detectedTitle": "Erkannte Umgebung",
49
+ "insideHost": "Im Host-Iframe",
50
+ "devAuth": "Dev-Auth aktiviert",
51
+ "yes": "ja",
52
+ "no": "nein",
53
+ "retry": "Neu laden"
54
+ },
55
+ "overview": {
56
+ "operator": "Betreiber",
57
+ "noOperator": "Betreiber nicht verfügbar",
58
+ "updated": "Aktualisiert {time}",
59
+ "persistence": "In IndexedDB gespeichert",
60
+ "notPersisted": "Vorschau — nicht gespeichert (in Bardioc OS öffnen zum Speichern)",
61
+ "historyOne": "{count} Snapshot im Verlauf",
62
+ "historyOther": "{count} Snapshots im Verlauf",
63
+ "source": "Quelle"
64
+ },
65
+ "conditions": { "sunny": "Sonnig", "cloudy": "Bewölkt", "rainy": "Regnerisch" },
66
+ "source": { "graph": "Bardioc-Graph", "mock": "Simuliert" },
67
+ "metrics": {
68
+ "oxygen": "Sauerstoff",
69
+ "temperature": "Oberflächentemp.",
70
+ "humidity": "Luftfeuchte",
71
+ "windSpeed": "Windgeschw.",
72
+ "vegetation": "Vegetation",
73
+ "oxygenHint": "Atmosphärenmix",
74
+ "temperatureHint": "Bodennähe",
75
+ "humidityHint": "Relativ",
76
+ "windSpeedHint": "Anhaltend",
77
+ "vegetationHint": "Oberflächendeckung"
78
+ },
79
+ "chart": { "title": "7-Tage-Ausblick", "description": "Sonnenstunden nach Wetterlage" },
80
+ "stations": {
81
+ "title": "Messstationen",
82
+ "description": "Alle Stationen dieser Instanz. Suchen, filtern oder auswählen.",
83
+ "search": "Stationen suchen…",
84
+ "filter": "Wetterlage",
85
+ "colName": "Station",
86
+ "colRegion": "Region",
87
+ "colCondition": "Wetterlage",
88
+ "colTemp": "Temp",
89
+ "colHumidity": "Feuchte",
90
+ "colVegetation": "Vegetation",
91
+ "itemLabel": "Stationen",
92
+ "empty": "Keine Stationen passen zu den Filtern",
93
+ "apply": "Anwenden",
94
+ "clear": "Zurücksetzen"
95
+ },
96
+ "explorer": {
97
+ "title": "Graph-Explorer",
98
+ "description": "Führe echte Bardioc-SDK-Transport-Anfragen aus. Ohne Session greift jede Anfrage auf simulierte Daten zurück.",
99
+ "run": "Anfrage ausführen",
100
+ "running": "Läuft…",
101
+ "empty": "Wähle eine Anfrage und führe sie aus, um die Antwort zu sehen",
102
+ "live": "Live",
103
+ "mock": "Simuliert",
104
+ "categoryGraph": "Graph",
105
+ "categoryOs": "OS",
106
+ "request": "Anfrage",
107
+ "durationMs": "{ms} ms"
108
+ },
109
+ "profile": {
110
+ "title": "Betreiberprofil",
111
+ "description": "Live aus dem OS-Transport geladen (os.profile).",
112
+ "name": "Name",
113
+ "displayName": "Anzeigename",
114
+ "firstName": "Vorname",
115
+ "lastName": "Nachname",
116
+ "email": "E-Mail",
117
+ "id": "ID",
118
+ "organization": "Organisation",
119
+ "units": "Einheiten",
120
+ "members": "Mitglieder",
121
+ "raw": "Rohantwort",
122
+ "errorTitle": "Profil konnte nicht geladen werden",
123
+ "errorHint": "Der Transport meldete einen Fehler. Autorisiere eine Session und lade neu.",
124
+ "unknown": "Unbekannt"
125
+ },
126
+ "events": {
127
+ "title": "Live-Ereignisse",
128
+ "description": "Live-Graph-Ereignisse, vom OS über SSE gestreamt (Berechtigung \"events\").",
129
+ "emptyTitle": "Warte auf Ereignisse",
130
+ "emptyHost": "Noch keine Graph-Ereignisse — Aktivität erscheint hier, sobald sie auftritt.",
131
+ "emptyStandalone": "In Bardioc OS (oder einer Dev-Session) öffnen, um Live-Ereignisse zu empfangen."
132
+ },
133
+ "notify": {
134
+ "synced": "{count} Stationen synchronisiert",
135
+ "saved": "In IndexedDB gespeichert",
136
+ "stationSelected": "Aktive Station: {name}",
137
+ "localeChanged": "Sprache gewechselt zu {locale}",
138
+ "queryOk": "Anfrage ok ({ms} ms)",
139
+ "queryFailed": "Anfrage fehlgeschlagen — zeige simulierte Daten"
140
+ }
141
+ }