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

@@ -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" >
-

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 @@
- Zurück zur Startseite + Back to landing page -

Rechtliches

-

Allgemeine Geschäftsbedingungen (AGB)

-

Stand: {{ lastUpdated }}

+

Legal

+

Terms of Service (TOS)

+

Updated: {{ lastUpdated }}

@@ -21,85 +21,85 @@
-

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 @@
- Zurück zur Startseite + Back to landing page -

Rechtliches

-

Datenschutzerklärung

-

Stand: {{ lastUpdated }}

+

Legal

+

Privacy Notice

+

Updated: {{ lastUpdated }}

-

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 @@