From 9577458482d4ac76fbde8498dece5ca1d01b050d Mon Sep 17 00:00:00 2001
From: Remi <73385395+itsrubberduck@users.noreply.github.com>
Date: Sat, 20 Sep 2025 09:46:34 +0200
Subject: [PATCH] Fix remaining German comment
---
app/assets/css/opensquawk-glass.css | 28 +-
app/pages/admin/index.vue | 266 +++---
app/pages/agb.vue | 66 +-
app/pages/api-docs.vue | 74 +-
app/pages/datenschutz.vue | 87 +-
app/pages/impressum.vue | 38 +-
app/pages/index.vue | 823 +++++-------------
app/pages/invite.vue | 26 +-
app/pages/news/[slug].vue | 14 +-
app/pages/news/index.vue | 18 +-
app/pages/pm.vue | 2 +-
server/api/admin/invitations.get.ts | 4 +-
server/api/admin/logs/transmissions.get.ts | 8 +-
server/api/admin/users.get.ts | 2 +-
server/api/admin/users/[id]/role.patch.ts | 6 +-
server/api/atc/ptt.post.ts | 14 +-
server/api/atc/say.post.ts | 12 +-
server/api/auth/invitations.post.ts | 6 +-
server/api/llm/decide.post.ts | 2 +-
.../api/service/auth/forgot-password.post.ts | 14 +-
server/api/service/auth/login.post.ts | 6 +-
server/api/service/auth/register.post.ts | 16 +-
.../api/service/auth/reset-password.post.ts | 8 +-
.../api/service/invitations/bootstrap.post.ts | 4 +-
server/api/service/invitations/manual.post.ts | 4 +-
.../api/service/roadmap-suggestions.post.ts | 20 +-
server/api/service/roadmap.post.ts | 10 +-
server/api/service/updates.post.ts | 14 +-
server/api/service/waitlist.post.ts | 28 +-
server/data/roadmapItems.ts | 40 +-
server/utils/normalize.ts | 36 +-
server/utils/notifications.ts | 4 +-
server/utils/openai.ts | 20 +-
server/utils/runtimeConfig.ts | 2 +-
server/utils/validation.ts | 8 +-
35 files changed, 667 insertions(+), 1063 deletions(-)
diff --git a/app/assets/css/opensquawk-glass.css b/app/assets/css/opensquawk-glass.css
index 9915b1e..76db6dd 100644
--- a/app/assets/css/opensquawk-glass.css
+++ b/app/assets/css/opensquawk-glass.css
@@ -1,6 +1,6 @@
/* === iOS-Style Glassmorphism Add-on === */
:root {
- /* feinere Glas-Parameter */
+ /* refined glass parameters */
--glass-bg: color-mix(in srgb, var(--text) 7%, transparent);
--glass-border: color-mix(in srgb, var(--text) 16%, transparent);
--glass-highlight: rgba(255, 255, 255, .10);
@@ -11,7 +11,7 @@
--glass-noise: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140' viewBox='0 0 140 140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3CfeComponentTransfer%3E%3CfeFuncA type='table' tableValues='0 0 .03 0'/%3E%3C/feComponentTransfer%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
-/* generische Glasfläche */
+/* generic glass surface */
.glass,
.panel,
.card,
@@ -33,7 +33,7 @@
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
}
-/* App Bar (transluzent, sticky) */
+/* App bar (translucent, sticky) */
.hud {
background: linear-gradient(180deg,
color-mix(in srgb, var(--bg) 55%, transparent),
@@ -43,12 +43,12 @@
border-radius: 0 0 14px 14px;
}
-/* Panels etwas luftiger */
+/* Panels with extra breathing room */
.panel {
padding: 14px;
}
-/* Buttons – iOS-Glanz & Lift */
+/* Buttons – iOS shine and lift */
.btn {
border-radius: calc(var(--glass-radius) - 6px);
transition: transform .15s ease, box-shadow .15s ease, background .2s ease;
@@ -92,7 +92,7 @@
}
-/* Chips – feiner Rand, leicht leuchtend */
+/* Chips – subtle border, gentle glow */
.chip {
background: linear-gradient(180deg,
color-mix(in srgb, var(--text) 7%, transparent),
@@ -100,7 +100,7 @@
box-shadow: inset 0 1px 0 rgba(255, 255, 255, .08);
}
-/* Karten/Kacheln – zarter Hover */
+/* Cards/tiles – subtle hover */
.card, .tile, .lesson {
transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease;
}
@@ -110,7 +110,7 @@
border-color: color-mix(in srgb, var(--accent) 28%, transparent);
}
-/* Fortschrittsbalken – Glasfüllung */
+/* Progress bar – glass fill */
.line {
background: linear-gradient(180deg,
color-mix(in srgb, var(--text) 8%, transparent),
@@ -125,7 +125,7 @@
filter: saturate(130%);
}
-/* Textareas / Inputs im Glas */
+/* Textareas / inputs in glass */
.input :deep(textarea),
:deep(.v-field__input) {
background: linear-gradient(180deg,
@@ -141,14 +141,14 @@
display: none;
}
-/* Dialog runder + stärkerer Blur */
+/* Dialog – rounder with stronger blur */
.dialog {
border-radius: 22px;
backdrop-filter: blur(22px) saturate(150%);
-webkit-backdrop-filter: blur(22px) saturate(150%);
}
-/* iOS-Range (Slider) – Track & Thumb */
+/* iOS-style range (slider) – track & thumb */
.chip.inline input[type="range"] {
height: 24px;
-webkit-appearance: none;
@@ -194,7 +194,7 @@
box-shadow: 0 2px 6px rgba(0, 0, 0, .35), inset 0 1px 0 rgba(255, 255, 255, .85);
}
-/* Snackbar – glasiger Toast */
+/* Snackbar – glassy toast */
:deep(.v-snackbar) {
border: 1px solid var(--glass-border);
backdrop-filter: blur(16px) saturate(140%);
@@ -204,14 +204,14 @@
color-mix(in srgb, var(--bg2) 60%, transparent));
}
-/* Fokus-Ring – iOS-Style */
+/* Focus ring – iOS style */
*:focus-visible {
outline: 2px solid color-mix(in srgb, var(--accent) 60%, transparent);
outline-offset: 2px;
border-radius: 12px;
}
-/* Weniger Transparenz, falls gewünscht */
+/* Reduced transparency if requested */
@media (prefers-reduced-transparency: reduce) {
.glass,
.panel,
diff --git a/app/pages/admin/index.vue b/app/pages/admin/index.vue
index 6ab8a02..76188ff 100644
--- a/app/pages/admin/index.vue
+++ b/app/pages/admin/index.vue
@@ -6,14 +6,14 @@
OpenSquawk
Admin Operations Center
- Vollständiger Überblick über Nutzer, Einladungscodes und Funkprotokolle mit Live-Trace.
+ Complete overview of users, invitation codes and radio logs with live tracing.
- Eingeloggt als {{ auth.user?.email }}
+ Signed in as {{ auth.user?.email }}
{{ auth.user?.role?.toUpperCase() }}
@@ -27,7 +27,7 @@
prepend-icon="mdi-refresh"
@click="refreshActiveTab"
>
- Daten aktualisieren
+ Refresh data
@@ -38,11 +38,11 @@
slider-color="cyan"
density="comfortable"
>
-
Übersicht
-
Nutzer
-
Einladungen
-
Warteliste
-
Funkprotokolle
+
Overview
+
Users
+
Invitations
+
Waitlist
+
Transmissions
@@ -50,10 +50,10 @@
-
Systemmetriken
-
Letzte Aktualisierung: {{ formatDateTime(overview?.generatedAt) }}
+
System metrics
+
Last updated: {{ formatDateTime(overview?.generatedAt) }}
-
lädt…
+
loading…
{{ overview.users.devs }}
-
Neu (7 Tage)
+
New (7 days)
{{ overview.users.newLast7Days }}
-
Neueste Accounts
+
Latest accounts
-
Einladungscodes
+ Invitation codes
{{ overview.invitations.total }}
-
Aktiv
+
Active
{{ overview.invitations.active }}
-
Läuft in 7 Tagen ab
+
Expires in 7 days
{{ overview.invitations.expiringSoon }}
-
Letzte Codes
+
Latest codes
- gültig bis {{ formatDateTime(inv.expiresAt) }}
+ Valid until {{ formatDateTime(inv.expiresAt) }}
@@ -152,7 +152,7 @@
-
Funkprotokolle
+ Radio logs
{{ overview.transmissions.total }}
@@ -161,7 +161,7 @@
{{ overview.transmissions.last24h }}
-
Kanäle
+
Channels
-
Neueste Übertragungen
+
Latest transmissions
-
Daten werden geladen…
+
Loading data…
@@ -209,7 +209,7 @@
Admin {{ userRoleStats.admin }}
- Anwenden
+ Apply
@@ -254,7 +254,7 @@
-
Nutzerdaten werden geladen…
+
Loading user data…
@@ -270,15 +270,15 @@
{{ user.email }}
{{ user.name }}
- Einladungen erstellt: {{ user.invitationCodesIssued }}
- Letzter Login: {{ formatRelative(user.lastLoginAt) }}
+ Invitations created: {{ user.invitationCodesIssued }}
+ Last login: {{ formatRelative(user.lastLoginAt) }}
- Aktualisieren
+ Update role
- Keine Nutzer gefunden.
+ No users found.
- Seite {{ userPagination.page }} von {{ userPagination.pages }} · {{ userPagination.total }} Einträge
+ Page {{ userPagination.page }} of {{ userPagination.pages }} · {{ userPagination.total }} entries
- Zurück
+ Back
- Weiter
+ Next
@@ -334,7 +334,7 @@
- Einladungscode erstellen
+ Create invitation code
- Anwenden
+ Apply
@@ -387,7 +387,7 @@
-
Einladungscodes werden geladen…
+
Loading invitation codes…
@@ -417,18 +417,18 @@
{{ invitationStatusLabel(inv) }}
- Gültig bis {{ formatDateTime(inv.expiresAt) }}
- Erstellt von {{ inv.createdBy.email }}
- Verwendet von {{ inv.usedBy.email }}
+ Valid until {{ formatDateTime(inv.expiresAt) }}
+ Created by {{ inv.createdBy.email }}
+ Used by {{ inv.usedBy.email }}
- Keine Einladungen gefunden.
+ No invitations found.
- Seite {{ invitationPagination.page }} von {{ invitationPagination.pages }} · {{ invitationPagination.total }} Codes
+ Page {{ invitationPagination.page }} of {{ invitationPagination.pages }} · {{ invitationPagination.total }} codes
- Zurück
+ Back
- Weiter
+ Next
@@ -457,16 +457,16 @@
-
Warteliste
+
Waitlist
- Gesamt: {{ waitlistStats.total }} · Updates: {{ waitlistStats.updates }} · Aktiviert: {{ waitlistStats.activated }}
+ Total: {{ waitlistStats.total }} · Updates: {{ waitlistStats.updates }} · Activated: {{ waitlistStats.activated }}
- Gesamt: {{ waitlistStats.total }}
+ Total: {{ waitlistStats.total }}
Updates: {{ waitlistStats.updates }}
- Aktiviert: {{ waitlistStats.activated }}
- Wartend: {{ waitlistStats.pending }}
+ Activated: {{ waitlistStats.activated }}
+ Pending: {{ waitlistStats.pending }}
@@ -485,7 +485,7 @@
- Filter anwenden
+ Apply filters
- {{ waitlistPagination.total }} Einträge · Seite {{ waitlistPagination.page }} von {{ waitlistPagination.pages }}
+ {{ waitlistPagination.total }} entries · Page {{ waitlistPagination.page }} of {{ waitlistPagination.pages }}
-
Warteliste wird geladen…
+
Loading waitlist…
- Kontakt
- Beigetreten
- Opt-In
+ Contact
+ Joined
+ Opt-in
Status
@@ -542,7 +542,7 @@
{{ entry.name }}
{{ entry.notes }}
- Quelle: {{ entry.source || 'landing' }}
+ Source: {{ entry.source || 'landing' }}
@@ -551,7 +551,7 @@
- Warteliste
+ Waitlist
- seit {{ formatDateTime(entry.updatesOptedInAt) }}
+ since {{ formatDateTime(entry.updatesOptedInAt) }}
@@ -573,21 +573,21 @@
class="text-xs"
>
- Aktiviert · {{ formatRelative(entry.activatedAt) }}
+ Activated · {{ formatRelative(entry.activatedAt) }}
- Wartet auf Zugang
+ Waiting for access
-
Keine Wartelisten-Einträge gefunden.
+
No waitlist entries found.
- Seite {{ waitlistPagination.page }} von {{ waitlistPagination.pages }} · {{ waitlistPagination.total }} Einträge
+ Page {{ waitlistPagination.page }} of {{ waitlistPagination.pages }} · {{ waitlistPagination.total }} entries
- Zurück
+ Back
- Weiter
+ Next
@@ -618,7 +618,7 @@
- Filter anwenden
+ Apply filters
- {{ logPagination.total }} Einträge · Seite {{ logPagination.page }} von {{ logPagination.pages }}
+ {{ logPagination.total }} entries · Page {{ logPagination.page }} of {{ logPagination.pages }}
@@ -684,7 +684,7 @@
-
Funkprotokolle werden geladen…
+
Loading radio logs…
@@ -709,10 +709,10 @@
Normalized: {{ entry.normalized }}
- Modul {{ entry.metadata.moduleId }}
+ Module {{ entry.metadata.moduleId }}
Lesson {{ entry.metadata.lessonId }}
- Auto Decision: {{ entry.metadata.autoDecide ? 'Ja' : 'Nein' }}
+ Auto Decision: {{ entry.metadata.autoDecide ? 'Yes' : 'No' }}
@@ -723,7 +723,7 @@
prepend-icon="mdi-timeline-text"
@click="toggleLog(entry.id)"
>
- {{ expandedLog === entry.id ? 'Tracer schließen' : 'Tracer öffnen' }}
+ {{ expandedLog === entry.id ? 'Close tracer' : 'Open tracer' }}
@@ -745,10 +745,10 @@
- Off-Schema: {{ entry.metadata.decision.off_schema ? 'Ja' : 'Nein' }}
+ Off-script: {{ entry.metadata.decision.off_schema ? 'Yes' : 'No' }}
- Radio Check: {{ entry.metadata.decision.radio_check ? 'Ja' : 'Nein' }}
+ Radio check: {{ entry.metadata.decision.radio_check ? 'Yes' : 'No' }}
@@ -761,9 +761,9 @@
>
- Schritt: {{ call.stage === 'decision' ? 'Decision' : 'Readback-Check' }}
+ Step: {{ call.stage === 'decision' ? 'Decision' : 'Readback check' }}
- Fehler beim Aufruf
+ Call failed
Request
@@ -774,7 +774,7 @@
{{ formatJson(call.response) }}
- Keine Antwort erhalten.
+ No response received.
Raw Response
@@ -794,11 +794,11 @@
v-if="entry.metadata.decisionTrace.fallback?.used"
class="space-y-1 rounded-xl border border-orange-400/40 bg-orange-500/10 p-3 text-[11px] text-orange-100"
>
-
Fallback aktiviert
+
Fallback activated
- Grund: {{ entry.metadata.decisionTrace.fallback.reason || 'unbekannt' }}
+ Reason: {{ entry.metadata.decisionTrace.fallback.reason || 'unknown' }}
- · Pfad: {{ entry.metadata.decisionTrace.fallback.selected }}
+ · Path: {{ entry.metadata.decisionTrace.fallback.selected }}
@@ -812,11 +812,11 @@
- Keine Funkprotokolle gefunden.
+ No transmissions found.
- Seite {{ logPagination.page }} von {{ logPagination.pages }} · {{ logPagination.total }} Logs
+ Page {{ logPagination.page }} of {{ logPagination.pages }} · {{ logPagination.total }} logs
- Zurück
+ Back
- Weiter
+ Next
@@ -845,10 +845,10 @@
- Einladungscode erstellen
+ Create invitation code
- Der Code wird sofort aktiv und läuft standardmäßig nach 30 Tagen ab. Optional kann ein Label gesetzt werden.
+ The code becomes active immediately and expires after 30 days by default. Adding a label is optional.
- Neuer Code
+ New code
{{ createInviteResult.code }}
- Gültig bis {{ formatDateTime(createInviteResult.expiresAt) }}
+ Valid until {{ formatDateTime(createInviteResult.expiresAt) }}
- Abbrechen
+ Cancel
- Code generieren
+ Generate code
@@ -1053,7 +1053,7 @@ const userSearch = ref('')
const userRoleFilter = ref<'all' | 'user' | 'admin' | 'dev'>('all')
const userRoleStats = reactive({ user: 0, admin: 0, dev: 0 })
const userRoleFilterOptions = [
- { title: 'Alle Rollen', value: 'all' },
+ { title: 'All roles', value: 'all' },
{ title: 'User', value: 'user' },
{ title: 'Developer', value: 'dev' },
{ title: 'Admin', value: 'admin' },
@@ -1074,16 +1074,16 @@ const invitationSearch = ref('')
const invitationStatus = ref<'all' | 'active' | 'used' | 'expired'>('active')
const invitationChannel = ref<'all' | 'user' | 'manual' | 'bootstrap' | 'admin'>('all')
const invitationStatusOptions = [
- { title: 'Alle Status', value: 'all' },
- { title: 'Aktiv', value: 'active' },
- { title: 'Verwendet', value: 'used' },
- { title: 'Abgelaufen', value: 'expired' },
+ { title: 'All statuses', value: 'all' },
+ { title: 'Active', value: 'active' },
+ { title: 'Used', value: 'used' },
+ { title: 'Expired', value: 'expired' },
]
const invitationChannelOptions = [
- { title: 'Alle Kanäle', value: 'all' },
+ { title: 'All channels', value: 'all' },
{ title: 'User', value: 'user' },
{ title: 'Admin', value: 'admin' },
- { title: 'Manuell', value: 'manual' },
+ { title: 'Manual', value: 'manual' },
{ title: 'Bootstrap', value: 'bootstrap' },
]
@@ -1095,14 +1095,14 @@ const waitlistSearch = ref('')
const waitlistSubscription = ref<'all' | 'waitlist' | 'updates'>('all')
const waitlistStatus = ref<'all' | 'pending' | 'activated'>('all')
const waitlistSubscriptionOptions = [
- { title: 'Alle Opt-Ins', value: 'all' },
- { title: 'Nur Warteliste', value: 'waitlist' },
- { title: 'Updates-Opt-in', value: 'updates' },
+ { title: 'All opt-ins', value: 'all' },
+ { title: 'Waitlist only', value: 'waitlist' },
+ { title: 'Product updates', value: 'updates' },
]
const waitlistStatusOptions = [
- { title: 'Alle Status', value: 'all' },
- { title: 'Wartend', value: 'pending' },
- { title: 'Aktiviert', value: 'activated' },
+ { title: 'All statuses', value: 'all' },
+ { title: 'Pending', value: 'pending' },
+ { title: 'Activated', value: 'activated' },
]
const waitlistStats = reactive({ total: 0, updates: 0, activated: 0, pending: 0 })
@@ -1116,26 +1116,26 @@ const logDirection = ref<'all' | 'incoming' | 'outgoing'>('all')
const logRole = ref<'all' | 'pilot' | 'atc'>('all')
const logTimeframe = ref<'24h' | '7d' | '30d' | 'all'>('24h')
const logChannelOptions = [
- { title: 'Alle Kanäle', value: 'all' },
+ { title: 'All channels', value: 'all' },
{ title: 'PTT', value: 'ptt' },
{ title: 'Say', value: 'say' },
{ title: 'Text', value: 'text' },
]
const logDirectionOptions = [
- { title: 'Alle Richtungen', value: 'all' },
+ { title: 'All directions', value: 'all' },
{ title: 'Incoming', value: 'incoming' },
{ title: 'Outgoing', value: 'outgoing' },
]
const logRoleOptions = [
- { title: 'Alle Rollen', value: 'all' },
+ { title: 'All roles', value: 'all' },
{ title: 'Pilot', value: 'pilot' },
{ title: 'ATC', value: 'atc' },
]
const logTimeframeOptions = [
- { title: 'Letzte 24 Stunden', value: '24h' },
- { title: '7 Tage', value: '7d' },
- { title: '30 Tage', value: '30d' },
- { title: 'Alles', value: 'all' },
+ { title: 'Last 24 hours', value: '24h' },
+ { title: '7 days', value: '7d' },
+ { title: '30 days', value: '30d' },
+ { title: 'All time', value: 'all' },
]
const expandedLog = ref(null)
@@ -1170,7 +1170,7 @@ function formatDateTime(value?: string) {
if (!value) return '—'
const date = new Date(value)
if (Number.isNaN(date.valueOf())) return value
- return date.toLocaleString('de-DE', {
+ return date.toLocaleString('en-US', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
@@ -1185,12 +1185,12 @@ function formatRelative(value?: string) {
if (Number.isNaN(date.valueOf())) return value
const diffMs = Date.now() - date.getTime()
const diffMinutes = Math.floor(diffMs / (1000 * 60))
- if (diffMinutes < 1) return 'vor wenigen Sekunden'
- if (diffMinutes < 60) return `vor ${diffMinutes} Min`
+ if (diffMinutes < 1) return 'a few seconds ago'
+ if (diffMinutes < 60) return `${diffMinutes} min ago`
const diffHours = Math.floor(diffMinutes / 60)
- if (diffHours < 24) return `vor ${diffHours} Std`
+ if (diffHours < 24) return `${diffHours} h ago`
const diffDays = Math.floor(diffHours / 24)
- if (diffDays < 30) return `vor ${diffDays} Tagen`
+ if (diffDays < 30) return `${diffDays} days ago`
return formatDateTime(value)
}
@@ -1211,9 +1211,9 @@ function isExpired(expiresAt?: string) {
}
function invitationStatusLabel(inv: InvitationItem) {
- if (inv.usedAt) return `verwendet ${formatRelative(inv.usedAt)}`
- if (inv.expiresAt && isExpired(inv.expiresAt)) return 'abgelaufen'
- return 'aktiv'
+ if (inv.usedAt) return `used ${formatRelative(inv.usedAt)}`
+ if (inv.expiresAt && isExpired(inv.expiresAt)) return 'expired'
+ return 'active'
}
function computeSince(timeframe: '24h' | '7d' | '30d' | 'all') {
@@ -1231,7 +1231,7 @@ async function loadOverview(force = false) {
try {
overview.value = await api.get('/api/admin/overview')
} catch (error) {
- overviewError.value = extractErrorMessage(error, 'Übersicht konnte nicht geladen werden.')
+ overviewError.value = extractErrorMessage(error, 'Could not load overview.')
} finally {
overviewLoading.value = false
}
@@ -1259,7 +1259,7 @@ async function fetchUsers(resetPage = false) {
Object.assign(userRoleStats, response.roles)
usersLoaded.value = true
} catch (error) {
- userError.value = extractErrorMessage(error, 'Nutzerliste konnte nicht geladen werden.')
+ userError.value = extractErrorMessage(error, 'Could not load user list.')
} finally {
userLoading.value = false
}
@@ -1283,7 +1283,7 @@ async function applyRoleChange(userId: string) {
})
await fetchUsers()
} catch (error) {
- userError.value = extractErrorMessage(error, 'Rolle konnte nicht aktualisiert werden.')
+ userError.value = extractErrorMessage(error, 'Could not update role.')
} finally {
userRoleUpdating.value = null
}
@@ -1308,7 +1308,7 @@ async function fetchInvitations(resetPage = false) {
Object.assign(invitationPagination, response.pagination)
invitationsLoaded.value = true
} catch (error) {
- invitationError.value = extractErrorMessage(error, 'Einladungen konnten nicht geladen werden.')
+ invitationError.value = extractErrorMessage(error, 'Could not load invitations.')
} finally {
invitationLoading.value = false
}
@@ -1348,7 +1348,7 @@ async function fetchWaitlist(resetPage = false) {
}
waitlistLoaded.value = true
} catch (error) {
- waitlistError.value = extractErrorMessage(error, 'Warteliste konnte nicht geladen werden.')
+ waitlistError.value = extractErrorMessage(error, 'Could not load waitlist.')
} finally {
waitlistLoading.value = false
}
@@ -1388,7 +1388,7 @@ async function fetchLogs(resetPage = false) {
Object.assign(logPagination, response.pagination)
logsLoaded.value = true
} catch (error) {
- logError.value = extractErrorMessage(error, 'Funkprotokolle konnten nicht geladen werden.')
+ logError.value = extractErrorMessage(error, 'Could not load radio logs.')
} finally {
logLoading.value = false
}
@@ -1419,7 +1419,7 @@ async function submitCreateInvite() {
newInviteLabel.value = ''
await fetchInvitations(true)
} catch (error) {
- createInviteError.value = extractErrorMessage(error, 'Einladungscode konnte nicht erstellt werden.')
+ createInviteError.value = extractErrorMessage(error, 'Could not create invitation code.')
} finally {
createInviteLoading.value = false
}
diff --git a/app/pages/agb.vue b/app/pages/agb.vue
index 1a5d3e9..12120a9 100644
--- a/app/pages/agb.vue
+++ b/app/pages/agb.vue
@@ -3,11 +3,11 @@
- Fragen zu diesen AGB beantworten wir unter info@opensquawk.dev. Für Enterprise- oder Trainingsprogramme erstellen wir individuelle Verträge.
+ Questions about these terms? Email us at info@opensquawk.dev. We provide tailored contracts for enterprise or training programmes on request.
diff --git a/app/pages/datenschutz.vue b/app/pages/datenschutz.vue
index 8558b51..71689e8 100644
--- a/app/pages/datenschutz.vue
+++ b/app/pages/datenschutz.vue
@@ -3,91 +3,80 @@
- 1. Verantwortlicher
+ 1. Controller
- Verantwortlich für die Datenverarbeitung im Sinne der Datenschutz-Grundverordnung (DSGVO) ist die
-
- Faktor Mensch MEDIA UG (haftungsbeschränkt),
-
- Wilhelm-Holzamer-Straße 8
-
- 55129 Mainz,
- Vertreten durch,
-
- Geschäftsführer: Dominik Ziegenhagel, Emanuel Leube,
-
- Telefon: +49 251 981 157 912 0
-
- E-Mail: info@opensquawk.de
+ The data controller under the GDPR is Faktor Mensch MEDIA UG (limited liability), Wilhelm-Holzamer-Strasse 8,
+ 55129 Mainz, Germany. Managing directors: Dominik Ziegenhagel, Emanuel Leube. Telephone: +49 251 981 157 912 0.
+ Email: info@opensquawk.de.
- 2. Verarbeitete Daten
+ 2. Data we process
- Warteliste: Name (optional), E-Mail-Adresse, freiwillige Hinweise, Zeitpunkt des Eintrags, Einwilligungen.
- Feature-Benachrichtigungen: E-Mail-Adresse, optionaler Name, Opt-in-Zeitpunkt, Zustimmung für Produkt-Updates.
- Nutzerkonto: Name, E-Mail-Adresse, Passwort-Hash, Einladungsstatus, Erstellungs- und Login-Zeitpunkte, Einladungscode-Historie.
- Roadmap-Vorschläge: Titel, Beschreibung, optional angegebene Kontaktadresse, Einwilligungen und Zeitstempel.
- Kommunikationsdaten: Sämtliche eingegebenen oder per Push-to-Talk übermittelten Funksprüche (Transkripte, normalisierte Texte, Metaangaben wie Modul, Lesson-ID, Stärke, Entscheidungskontext).
- Technische Protokolle: Geräteinformationen (Browser, OS), Zeitstempel, Request-IDs, Fehlerlogs.
+ Waitlist: Name (optional), email, optional notes, time of signup, consent records.
+ Feature updates: Email, optional name, opt-in timestamp and marketing consent.
+ User accounts: Name, email, password hash, invitation status, creation/login timestamps, invitation history.
+ Roadmap suggestions: Title, description, optional contact address, consent flags and timestamps.
+ Communications: All radio inputs (typed or push-to-talk transcripts, normalized text, metadata such as module, lesson ID, signal strength, decision context).
+ Technical logs: Device details (browser, OS), timestamps, request IDs, error logs.
- Hinweis: Audio-Dateien aus Push-to-Talk werden nur temporär verarbeitet, Transkripte und Kontextdaten speichern wir zur Qualitätsverbesserung.
+ Note: Raw audio from push-to-talk is processed temporarily only. Transcripts and context data are stored to improve quality.
- 3. Zwecke und Rechtsgrundlagen
+ 3. Purpose & legal basis
- Bereitstellung des Dienstes (Art. 6 Abs. 1 lit. b DSGVO): Verwaltung von Warteliste, Accounts und Sessions, Ausgabe von Einladungscodes, Abrechnung zukünftiger Tarife.
- Produktverbesserung & Sicherheit (Art. 6 Abs. 1 lit. f DSGVO): Analyse und Logging sämtlicher Funksprüche, Fehlerüberwachung, Missbrauchsprävention.
- Kommunikation & Community-Feedback (Art. 6 Abs. 1 lit. a DSGVO): Versand von Wartelisten-Updates und Produkt-News nach ausdrücklicher Einwilligung sowie Auswertung eingereichter Roadmap-Vorschläge inkl. optionaler Kontaktaufnahme.
+ Service delivery (Art. 6(1)(b) GDPR): Managing waitlist entries, accounts and sessions, issuing invitation codes, preparing billing for future paid plans.
+ Product improvement & security (Art. 6(1)(f) GDPR): Analysing and logging radio interactions, monitoring errors, preventing abuse.
+ Communication & community feedback (Art. 6(1)(a) GDPR): Sending waitlist or product updates after consent and evaluating roadmap suggestions including optional follow-up.
- 4. Speicherdauer
+ 4. Retention
- Wartelisteneinträge: bis zum Widerruf oder maximal 24 Monate nach letzter Aktivität.
- Feature-Benachrichtigungen: bis zur Abmeldung (Opt-out) oder Widerruf der Einwilligung.
- Roadmap-Vorschläge: bis zur Umsetzung oder spätestens 18 Monate nach Einreichung.
- Accountdaten: solange das Nutzerkonto besteht, anschließend entsprechend gesetzlicher Aufbewahrungspflichten.
- Kommunikationslogs: mindestens 12 Monate zur Qualitätsverbesserung; bei Supportfällen oder Missbrauchsvorwürfen bis zur abschließenden Klärung.
+ Waitlist entries: until withdrawal or 24 months after the last activity.
+ Feature updates: until you unsubscribe or withdraw consent.
+ Roadmap suggestions: until implemented or at most 18 months after submission.
+ Account data: for the lifetime of the account and thereafter according to statutory retention periods.
+ Communication logs: at least 12 months for quality assurance; longer if needed to resolve support cases or investigate abuse.
- 5. Weitergabe & Auftragsverarbeitung
+ 5. Sharing & processors
- Wir hosten OpenSquawk auf europäischen Cloud-Plattformen (derzeit Hetzner Cloud, Deutschland). Kommunikationsdaten werden in unserer MongoDB-Datenbank gespeichert. Externe KI-Dienstleister (z. B. OpenAI) erhalten ausschließlich pseudonymisierte Texte zur Verarbeitung von TTS/LLM-Funktionen. Es gelten entsprechende Auftragsverarbeitungsverträge. Eine Übermittlung in Drittstaaten erfolgt nur unter Nutzung von EU-Standardvertragsklauseln.
+ OpenSquawk runs on European cloud infrastructure (currently Hetzner Cloud, Germany). Communication data resides in our MongoDB database. External AI providers (e.g. OpenAI) receive only pseudonymised text to power TTS/LLM features. Appropriate processing agreements are in place. Transfers to third countries rely on EU Standard Contractual Clauses where necessary.
- Hinweis: Formularübermittlungen (Warteliste, Feature-Benachrichtigungen, Roadmap-Vorschläge) lösen eine interne Benachrichtigungs-E-Mail an info@opensquawk.de aus, versendet über den konfigurierten SMTP- oder Transaktionsmail-Anbieter. Enthalten sind ausschließlich die von dir eingegebenen Angaben zur zügigen Bearbeitung.
+ Note: Form submissions (waitlist, feature updates, roadmap suggestions) trigger an internal notification email to info@opensquawk.de via our SMTP or transactional provider. We only forward the details you submit so we can respond quickly.
- 6. Rechte der Betroffenen
+ 6. Your rights
- Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung (Art. 15–18 DSGVO).
- Datenübertragbarkeit (Art. 20 DSGVO).
- Widerspruch gegen Verarbeitung aus berechtigtem Interesse (Art. 21 DSGVO).
- Widerruf erteilter Einwilligungen mit Wirkung für die Zukunft.
- Beschwerderecht bei einer Datenschutzaufsichtsbehörde, z. B. Berliner Beauftragte für Datenschutz und Informationsfreiheit.
+ Access, rectification, erasure and restriction (Art. 15–18 GDPR).
+ Data portability (Art. 20 GDPR).
+ Objection to processing based on legitimate interest (Art. 21 GDPR).
+ Withdrawal of consent with future effect.
+ Complaint to a supervisory authority, e.g. the Berlin Commissioner for Data Protection and Freedom of Information.
- 7. Kontakt für Datenschutzanfragen
+ 7. Contact
- Bitte richten Sie Auskunfts- oder Löschersuchen an info@opensquawk.de. Zur eindeutigen Zuordnung benötigen wir die bei OpenSquawk registrierte E-Mail-Adresse sowie ggf. weitere Identifikationsmerkmale (z. B. VATSIM-ID).
+ To exercise your rights please email info@opensquawk.de. Provide the email address registered with OpenSquawk and, if applicable, additional identifiers (e.g. VATSIM ID) so we can verify your request.
@@ -95,7 +84,7 @@