Translate news posts and remove banners

This commit is contained in:
Remi
2025-09-18 23:48:20 +02:00
committed by itsrubberduck
parent a2ad53c51d
commit 641042ac75
5 changed files with 39 additions and 119 deletions

View File

@@ -6,14 +6,6 @@
</NuxtLink>
<article v-if="post" class="card space-y-6">
<NuxtImg
v-if="bannerSrc"
:src="bannerSrc"
:alt="`${post.title} Beitragsbild`"
class="news-banner"
sizes="sm:100vw md:900px lg:1100px"
loading="lazy"
/>
<header class="space-y-2">
<span class="chip text-[10px] uppercase tracking-[0.3em]">{{ formatNewsDate(post.publishedAt) }}</span>
<h1 class="text-3xl md:text-4xl font-semibold">{{ post.title }}</h1>
@@ -39,8 +31,6 @@ import { getNewsBySlug } from '~~/shared/utils/news'
const route = useRoute()
const slug = computed(() => String(route.params.slug ?? ''))
const post = computed(() => getNewsBySlug(slug.value))
const bannerSrc = computed(() => (post.value ? `/img/news/${post.value.bannerFile}` : ''))
useHead(() => {
if (!post.value) {
return {
@@ -72,7 +62,6 @@ const formatNewsDate = (iso: string) => new Date(iso).toLocaleDateString('de-DE'
.btn { @apply inline-flex items-center justify-center gap-2 rounded-xl px-5 py-3 font-medium transition; }
.btn-primary { @apply bg-cyan-500 text-white hover:bg-cyan-400 shadow-[0_0_40px_rgba(34,211,238,.25)]; }
.chip { @apply inline-flex items-center gap-2 rounded-full bg-white/10 border border-white/15 text-white px-3 py-1 text-xs; }
.news-banner { @apply w-full rounded-xl object-cover; max-height: 420px; }
.news-body :deep(h2) { @apply text-2xl md:text-3xl font-semibold mt-6 mb-3 text-white; }
.news-body :deep(h3) { @apply text-xl font-semibold mt-5 mb-2 text-white; }
.news-body :deep(p) { @apply text-white/80 leading-relaxed mb-4; }

View File

@@ -1,34 +1,33 @@
---
title: "Alpha-Prototyp bereit für frühe Tests"
title: "Alpha prototype ready for early testing"
date: "2025-09-15"
summary: "Unser erster OpenSquawk Alpha-Build läuft lokal für MSFS und zeigt, wohin die Reise Richtung community-getriebener AI-ATC geht."
bannerFile: "alpha-prototype.svg"
summary: "Our first OpenSquawk alpha build runs locally for MSFS and shows where community-driven AI ATC is heading."
---
Wir haben den Alpha-Prototyp von OpenSquawk intern stabilisiert und geben ihn jetzt an neugierige Mitentwickler:innen raus. Der Build kombiniert eine Nuxt-Oberfläche mit Node-Services für Speech-to-Text, Entscheidungslogik und TTS. Alles lässt sich lokal betreiben, solange du ein wenig Terminal-Erfahrung mitbringst.
We have stabilised the OpenSquawk alpha prototype internally and are now handing it to curious co-developers. The build combines a Nuxt interface with Node services for speech-to-text, decision logic and TTS. Everything runs locally as long as you bring a bit of terminal experience.
## Was dich erwartet
## What to expect
- Self-host Setup: `yarn install`, `.env` befüllen, danach `yarn dev` oder `docker compose up` für das volle Paket.
- Simulator-Fokus: Microsoft Flight Simulator (MSFS) zuerst, X-Plane steht bereits in der Konzeptphase.
- Zielbild: Community-driven Features, damit Trainingsrichtung VATSIM realitätsnah bleibt und Hosting langfristig bezahlbar wird.
- Self-host setup: `yarn install`, fill your `.env`, then `yarn dev` or `docker compose up` for the full stack.
- Simulator focus: Microsoft Flight Simulator (MSFS) first; X-Plane is already in the concept phase.
- Target image: community-driven features so VATSIM training stays realistic and hosting remains affordable.
## Startvoraussetzungen
## Prerequisites
- Grundkenntnisse in Node/Nuxt (Logs lesen, Pakete aktualisieren, `.env` konfigurieren).
- Zugriff auf MSFS (PC) für die ersten Funkexperimente.
- Bereitschaft, Issues zu dokumentieren und kleine Tweaks per Pull Request zu liefern.
- Basic knowledge of Node/Nuxt (reading logs, updating packages, configuring `.env`).
- Access to MSFS (PC) for the first radio experiments.
- Willingness to document issues and ship small tweaks via pull request.
## Wie du helfen kannst
## How you can help
- Teste den Alpha-Build und melde Findings als Issues am besten mit Logs oder kurzen Screenshots.
- Schau in die Issues labeled `help-wanted`, dort liegen konkrete Aufgaben für Node/Nuxt Devs, ATC SMEs, Tester:innen und Infra-/Kosten-Benchmarking.
- Teile eigene Feature-Ideen direkt in der Roadmap oder schreib uns via [info@opensquawk.de](mailto:info@opensquawk.de).
- Test the alpha build and report findings as issues ideally with logs or quick screenshots.
- Check the `help-wanted` issues for concrete tasks aimed at Node/Nuxt devs, ATC SMEs, testers and infra/cost benchmarking.
- Share feature ideas directly in the roadmap or email us at [info@opensquawk.de](mailto:info@opensquawk.de).
## Nächste Schritte
## Next steps
- X-Plane Plugin als nächstes Milestone.
- Lernpfad-Module iterieren (Ground → Departure → Arrival → VATSIM).
- Hosting-Kosten transparent benchmarken und im Blog teilen.
- X-Plane plug-in as the next milestone.
- Iterate learning modules (Ground → Departure → Arrival → VATSIM).
- Benchmark hosting costs transparently and share them on the blog.
Danke für jedes Feedback gemeinsam bringen wir OpenSquawk Richtung „Open-source, low-cost AI ATC für Flugsimulatoren".
Thanks for every piece of feedback together we can turn OpenSquawk into open-source, low-cost AI ATC for flight simulators.

View File

@@ -1,36 +1,36 @@
---
title: "Runtime-Härtung & sichere Eingaben"
title: "Runtime hardening & safer inputs"
date: "2025-09-18"
summary: "Neue Konfigurationszentrale, sichere Audio-Uploads und strengere Passwörter machen OpenSquawk robuster."
readingTime: "3 Min Lesezeit"
summary: "A new configuration hub, secure audio uploads and stronger passwords make OpenSquawk more robust."
readingTime: "3 min read"
---
Wir haben uns heute quer durch Backend, Runtime-Config und Onboarding gearbeitet, um OpenSquawk widerstandsfähiger zu machen. Drei Baustellen standen im Fokus: reproduzierbare Konfiguration, saubere Audio-Eingaben für den Funk-Workflow und ein härterer Schutz der Benutzerkonten.
We worked across the backend, runtime config and onboarding today to make OpenSquawk more resilient. Three workstreams were in focus: reproducible configuration, clean audio inputs for the radio workflow and tighter protection for user accounts.
## Highlights
- **Zentrale Runtime-Konfiguration:** Alle sensiblen Schlüssel (OpenAI, TTS, Piper & Speaches) laufen jetzt über eine gemeinsame Helper-Funktion. Fehlkonfigurationen fliegen sofort auf, inklusive klarer Fehlermeldung.
- **Sicherere Funk-Eingaben:** Die PTT-API akzeptiert nur noch validiertes Base64-Audio bis 2MB und standardisiert unbekannte Formate auf WAV, bevor Whisper loslegt.
- **Stärkere Passwörter:** Registrierung prüft E-Mail-Format und Passwort-Qualität (Länge, Buchstaben/Zahlen, Sonderzeichen), damit Alpha-Zugänge nicht mit Trivialpasswörtern angelegt werden.
- **Central runtime configuration:** All sensitive keys (OpenAI, TTS, Piper & Speaches) now flow through a shared helper. Misconfigurations show up instantly with clear error messages.
- **Safer radio inputs:** The PTT API now accepts validated base64 audio up to 2 MB and standardises unknown formats to WAV before Whisper takes over.
- **Stronger passwords:** Registration checks email format and password quality (length, letters/numbers, special characters) so alpha access is not created with trivial credentials.
## Details
### Runtime-Konfiguration aufgeräumt
### Runtime configuration tidied up
Ein neues Utility (`server/utils/runtimeConfig.ts`) kapselt sämtliche Laufzeit-Variablen. `OpenAI`- und TTS-Clients ziehen daraus konsistent Schlüssel, Modelle, Voice-Defaults sowie Piper/Speaches-Schalter. Der Server stoppt mit einem erklärenden Fehler, falls `OPENAI_API_KEY` fehlt besser früh scheitern als stumm 500er produzieren.
A new utility (`server/utils/runtimeConfig.ts`) encapsulates every runtime variable. The `OpenAI` and TTS clients consistently pull keys, models, voice defaults as well as Piper/Speaches switches from there. The server now stops with an explanatory error if `OPENAI_API_KEY` is missing better to fail early than silently throw 500s.
### Audio-Endpunkte härter gemacht
### Audio endpoints hardened
Die Push-to-Talk-Route prüft Base64-Eingaben, begrenzt Dateigrößen auf praxisnahe 2MB (~60s Funk) und erzwingt bekannte Audioformate. Das reduziert Risiko von Speicherausreißern und sorgt dafür, dass FFmpeg nur bei echten Konvertierungen anspringt. Gleichzeitig lesen TTS-Endpunkte ihre Einstellungen jetzt aus der Runtime-Config und respektieren lokale Piper-Ports sowie Speaches-Basis-URLs.
The push-to-talk route validates base64 input, caps file size at a practical 2 MB (~60 seconds of radio) and enforces known audio formats. That reduces the risk of memory spikes and ensures FFmpeg only kicks in for real conversions. At the same time, TTS endpoints now read their settings from the runtime config and respect local Piper ports and Speaches base URLs.
### Onboarding abgesichert
### Onboarding secured
Die Registrierung verweigert invaliden Input ab sofort sofort: ungültige E-Mails, kurze Passwörter oder fehlende Sonderzeichen liefern verständliche Fehlermeldungen. So bleiben Test-Accounts verwaltbar und Sicherheitsstandards steigen, ohne den Flow zu bremsen.
Registration immediately rejects invalid input: wrong emails, short passwords or missing special characters now return clear error messages. Test accounts stay manageable and security standards rise without slowing the flow.
## Ausblick
## Outlook
- Hotjar/Analytics-Opt-in an die neue Config-Logik anbinden.
- Für den Login dieselben Passwort-Guidelines rückspiegeln (Feedback-UI).
- Größere Audiodateien optional asynchron verarbeiten, falls Langform-Transkripte spannend werden.
- Connect the Hotjar/analytics opt-in to the new config logic.
- Mirror the same password guidelines for login (including feedback UI).
- Consider processing larger audio files asynchronously if long-form transcripts become interesting.
Wenn euch weitere Hardenings einfallen: gerne Issues aufmachen oder direkt PRs schicken!
If you have more hardening ideas, feel free to open issues or send PRs directly!

View File

@@ -1,56 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1600" height="720" viewBox="0 0 1600 720" preserveAspectRatio="xMidYMid slice">
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#041026" />
<stop offset="50%" stop-color="#0a223f" />
<stop offset="100%" stop-color="#134b5f" />
</linearGradient>
<radialGradient id="glow" cx="75%" cy="25%" r="70%">
<stop offset="0%" stop-color="#2dd4bf" stop-opacity="0.85" />
<stop offset="55%" stop-color="#22d3ee" stop-opacity="0.25" />
<stop offset="100%" stop-color="#0b1020" stop-opacity="0" />
</radialGradient>
<radialGradient id="glow2" cx="20%" cy="80%" r="65%">
<stop offset="0%" stop-color="#38bdf8" stop-opacity="0.75" />
<stop offset="60%" stop-color="#0ea5e9" stop-opacity="0.18" />
<stop offset="100%" stop-color="#0a0f1c" stop-opacity="0" />
</radialGradient>
<linearGradient id="grid" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#22d3ee" stop-opacity="0.1" />
<stop offset="100%" stop-color="#22d3ee" stop-opacity="0" />
</linearGradient>
<filter id="soft" x="-10%" y="-10%" width="120%" height="120%">
<feGaussianBlur in="SourceGraphic" stdDeviation="3" />
</filter>
</defs>
<rect width="1600" height="720" fill="url(#bg)" />
<circle cx="1150" cy="180" r="520" fill="url(#glow)" />
<circle cx="360" cy="620" r="460" fill="url(#glow2)" />
<g stroke="url(#grid)" stroke-width="1" fill="none" opacity="0.5">
<path d="M-200 640 L1800 120" />
<path d="M-120 700 L1760 180" />
<path d="M-60 760 L1700 240" />
<path d="M40 760 L1620 320" />
</g>
<g opacity="0.45" filter="url(#soft)">
<circle cx="230" cy="210" r="90" fill="#38bdf8" />
<circle cx="290" cy="210" r="60" fill="#22d3ee" />
<circle cx="350" cy="210" r="30" fill="#0ea5e9" />
</g>
<g transform="translate(180 500) rotate(-6)" opacity="0.3">
<rect x="0" y="0" width="460" height="110" rx="28" ry="28" fill="#0f172a" stroke="#1f2937" stroke-width="2" />
<text x="230" y="68" font-family="'Space Grotesk', 'Segoe UI', sans-serif" font-size="48" fill="#e0f2fe" text-anchor="middle">Alpha Prototype</text>
</g>
<g opacity="0.65">
<text x="150" y="180" font-family="'Space Grotesk', 'Segoe UI', sans-serif" font-size="120" fill="#e0f2fe" letter-spacing="8">OPEN</text>
<text x="150" y="300" font-family="'Space Grotesk', 'Segoe UI', sans-serif" font-size="140" fill="#22d3ee" letter-spacing="10">SQUAWK</text>
</g>
<g transform="translate(980 470)" opacity="0.55">
<circle cx="0" cy="0" r="8" fill="#22d3ee" />
<circle cx="60" cy="-40" r="6" fill="#38bdf8" />
<circle cx="120" cy="-10" r="5" fill="#0ea5e9" />
<circle cx="180" cy="-60" r="4" fill="#38bdf8" />
<circle cx="240" cy="-20" r="6" fill="#22d3ee" />
<polyline points="0,0 60,-40 120,-10 180,-60 240,-20" fill="none" stroke="#bae6fd" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -6,7 +6,6 @@ export interface NewsPost {
body: string
html: string
readingTime: string
bannerFile: string
}
interface FrontMatter {
@@ -16,7 +15,6 @@ interface FrontMatter {
excerpt?: string
description?: string
readingTime?: string
banner?: string
}
const rawNewsModules = import.meta.glob('~~/content/news/*.md', {
@@ -53,7 +51,6 @@ function parseNewsFile(path: string, raw: string): NewsPost | null {
const excerpt = truncateText(excerptSource || firstParagraph(cleanBody), 220)
const html = markdownToHtml(cleanBody)
const readingTime = attributes.readingTime || formatReadingTime(cleanBody)
const bannerFile = normalizeBannerFile(attributes.banner, slug)
return {
slug,
@@ -63,7 +60,6 @@ function parseNewsFile(path: string, raw: string): NewsPost | null {
body: cleanBody,
html,
readingTime,
bannerFile,
}
}
@@ -147,15 +143,7 @@ function truncateText(text: string, maxLength: number): string {
function formatReadingTime(content: string): string {
const words = content.split(/\s+/).filter(Boolean)
const minutes = Math.max(1, Math.round(words.length / 180))
return `${minutes} Min Lesezeit`
}
function normalizeBannerFile(banner: string | undefined, slug: string): string {
const value = (banner || '').trim()
if (!value) {
return `${slug}.svg`
}
return value.includes('.') ? value : `${value}.svg`
return `${minutes} min read`
}
function markdownToHtml(markdown: string): string {