Company details on the web and in the app: @zirospot/shared/company (Pulitore s. r. o., IČO, address, e-mail, phone) rendered by the new SiteFooter on the landing and document pages, as §11 of the terms, in the privacy policy's contact section and under the legal links in the app's Profile tab (EU DSA trader details, store verification).
v0.33.02026-10-12
Added
Account deletion (store requirement — App Store guideline 5.1.1(v), Google Play account deletion policy): DELETE /api/account removes the signed-in user, their venue and everything scoped to it (spots + files, variants, stations, schedule, settings, play log) after re-checking the current password; admins are refused. Profile tab on the web and in the app got a "Delete account" section (password + confirm); the app signs out afterwards. The privacy policy (sk/cs/en) names the self-service path.
docs/store-checklist.md: what App Store Connect and Play Console need (accounts, D-U-N-S, DSA trader status, assets, forms, review notes) with links and where each item goes.
Changed
Mobile: ITSAppUsesNonExemptEncryption: false in app.json (HTTPS only, skips the export-compliance prompt per build); eas.json has a submit profile (Android service-account key path, internal track; the key file is gitignored).
v0.32.02026-10-02
Added
Station catalogue: more of Europe and the Americas — 36 further European countries plus US, CA, MX, BR, AR, CL, CO and PE, the 50 most popular https streams of each that actually played (import-catalog.mjs --limit=50). The country row lists the home markets (SK, CZ) first, then the rest by code; on the web it scrolls sideways like in the app.
Web + app: the catalogue's filters combine — country chip (or the new "All"), genre chip and the name search narrow the results together, in our catalogue and in the Radio Browser directory alike (GET /api/stations/catalog?country=&q=&genre=, the search route's q / tag / country). Picking a chip no longer clears the others.
Privacy policy (sk / cs / en), version 2: Vercel Web Analytics listed among the processors.
v0.31.02026-10-02
Added
Web + app: the directory search is back, inside the Katalóg tab. Typing a name (3+ letters) searches our catalogue across all countries and the Radio Browser directory at once — catalogue matches first, then "more stations from the Radio Browser directory" (streams already shown are not repeated). The genre chips (Pop, Rock, Jazz…) search the directory by tag. A station added from the directory joins the catalogue like any venue-added stream.
v0.30.02026-10-02
Added
Web: the Stations tab's Katalóg is our own station catalogue, the twin of the app's — search across countries, country chips with flags and counts, one card per station with preview, one-click add and the ad-free badge (components/StationCatalog.tsx).
Web + app: the three dots shown while a station connects are animated (.loading-dots on the web, LoadingDots in the app's ui.tsx).
Web: Vercel Web Analytics (@vercel/analytics, <Analytics /> in the app layout) — cookieless page views of the public pages and the console. The Payload admin is not tracked. The native app has no analytics: Vercel offers no React Native SDK and custom events are not part of the free plan.
Removed
Web: the Radio Browser search UI (components/StationSearch.tsx). GET /api/stations/search stays for app builds older than 0.7.0.
v0.29.02026-10-02
Mobile app 0.7.0: our own station catalogue, music from the phone, auto volume, a landscape layout, help moved into Profile and the square logo back in a simpler form. The UI changes are in the native app only; the server parts (catalogue, loudness) serve both clients.
Added
Station catalogue — our own database of radio streams grouped by country (Europe; seeded with Slovakia and Czechia). Payload collection catalog-stations (admin-only in /admin: mark ad-free, hide, fix names, set the order), read by the app through GET /api/stations/catalog (?country=SK or ?q=name; ad-free first, then by popularity). scripts/import-catalog.mjs seeds it from Radio Browser: https streams only, duplicates and talk-only stations dropped, and every stream is actually played with ffmpeg before it is stored — with its measured loudness. A stream a signed-in venue adds on its own is written to the catalogue immediately (source venue) and is visible to every venue; the anonymous demo cannot write to it.
App: the Stations tab's Katalóg is this catalogue — country chips with flags and counts, name search across countries, preview, one-tap add, an "ad-free" badge. The custom-stream form gained a country. The Radio Browser search stays in the web console.
App: music from the phone ("Z telefónu", third source in the Stations tab) — audio files picked from the device are copied into the app and play as a looping playlist instead of a radio stream, with no internet needed. Breaks work as with the radio; a track resumes where it was faded out. Engine: playPlaylist / updatePlaylist / skipTrack, the onTrack event, prev/next switch tracks. The list and the chosen source are device settings (src/music-store.ts).
Auto volume — a spot plays as loud as the music it interrupts. The server measures programme loudness (EBU R128, LUFS) with ffmpeg (lib/loudness.ts, new dependency ffmpeg-static): every new spot, TTS edit and language variant is normalised to −14 LUFS and stored as MP3 (uploads in any format included — a webm recording now plays on iOS too); a station takes the catalogue's value or is measured after it is added. Spot.loudness, SpotVariant.loudness, Station.loudness and Settings.autoVolume (default on) travel in the DTOs; planLoudness() in @zirospot/shared/loudness turns them into volume factors, which the mobile engine multiplies into its fades (setMusicGain, BreakSpot.gain). On/off switch in Spoty → Rozvrh. scripts/backfill-loudness.mjs measured the existing rows.
App: landscape layout — the tab bar becomes a side rail, the DJ screen puts the play ring on the left and the controls on the right (nothing below the fold on a phone), stations / catalogue / tracks use two columns, spots show the form next to the list, the schedule its two halves side by side, Profile two columns (src/layout.ts).
App: Help section in Profile — every "how it works" explainer as a collapsible topic, closed by default.
The ZiroSpot promo (sk / cs / en) and the demo's showcase spot with its language versions were regenerated with the ElevenLabs voice and normalised like any spot; the old Edge-voice files are gone. Promo URLs carry PROMO_VERSION (now 2) so devices re-download a regenerated file. scripts/generate-platform-audio.mjs repeats this with an ElevenLabs key at hand.
Changed
Brand: the square logo is back, simplified — a square frame with a Z split into two congruent halves (cream = the music, orange = the spot taking over), flat colours, no gradients. The lowercase Outfit wordmark, the tagline and the palette stay. brand/generate.mjs regenerated the SVGs, the mobile icons / splash, the in-app mark and the web icons and social card.
App: the explainers above the forms (spots, AI voice, recorder, schedule, station list, catalogue) are gone from the working screens — they live in Profile → Help.
GET /api/stations and POST /api/stations return StationDTO (with loudness) instead of raw rows.
App: the spot cache directory is created on first use (the Expo web export crashed at import time).
Removed
App: the Radio Browser search screen (StationSearch.tsx), replaced by the catalogue.
App: the ready-made ad texts (template categories) in the AI-voice form — no longer needed. They remain in the web console.
v0.28.02026-09-25
Mobile app 0.6.0: the web console gets the Profile tab the app already had, and making an AI spot no longer means typing things twice.
Added
Web: Profile tab in the console (fourth tab, ProfilePanel) — name, venue name, e-mail and password (changes confirmed with the current password via Payload REST), console language, the feedback form, diagnostics (a browser-side event buffer, lib/diag.ts, fed by the player/scheduler/engine; copy as text or send to Sentry with a note and the log attached), legal links, sign-out and the version. The demo shows the same tab without the account sections (register / sign in links instead). The floating "?" feedback button left the consoles (/app, /demo) and the logout button left the console masthead — both live in Profile; the "?" stays on the public pages.
Spot language picker (SK / CZ / EN) in the AI-voice form on the web and in the app: the voice's language is chosen per spot instead of following the app language (/api/tts and POST /api/spots accept locale; the app language stays the fallback). Templates are offered in the chosen language.
spotNameFromText() in @zirospot/shared/spots: an AI spot is named after the first words of its text; the name stays editable.
resolveSession() in lib/tenant.ts — user + venue in one auth call (resolveVenue delegates to it).
TTS logs which engine produced a spot (tts.engine), and a signed-in request without ELEVENLABS_API_KEY warns once (tts.eleven_unconfigured) instead of silently using Edge — how the missing production key went unnoticed for four days.
Changed
AI-voice form (web + app): the separate "Spot name" field is gone for AI spots (it remains for uploads and recordings).
Spot editor (web + app): language versions are edited in one text box switched by SK / CZ / EN chips — the base language edits the spot's own text, the other languages generate or delete their variant. A missing variant starts empty instead of being pre-filled with the base text (which read as a broken "translation" window).
Dictionaries: mobile.profile and the mobile.diag* keys moved to top-level profile / diag sections shared by both consoles; LOCALE_LABELS moved to @zirospot/shared/i18n.
v0.27.02026-09-21
Mobile app 0.5.0: ElevenLabs voices, a Profile tab, ready-made ad texts for the first Prague venues and a calmer brand.
Added
AI voice by ElevenLabs (lib/tts.ts): with ELEVENLABS_API_KEY set, signed-in venues get their spots, edits and language variants synthesized by ElevenLabs (Flash v2.5 with language enforcement for sk/cs/en, default premade voices so the free plan works; model and voices overridable via env). The public demo stays on Edge TTS so it cannot burn the credit quota, and Edge TTS remains the automatic fallback when ElevenLabs fails or the quota runs out (tts.eleven_fallback warning). No new dependency — plain fetch.
Ad templates (@zirospot/shared/spot-templates): eleven ready-made texts in cs/sk/en for corner shops, Asian bistros, baguette shops and small retail, picked by category in the AI-voice form on both the web and the mobile app; a template fills the text field for editing.
Mobile: Profile tab — name, venue name, e-mail and password (e-mail/password changes are confirmed with the current password), language, plan badge, feedback form, diagnostics, legal links, changelog, sign out and the app version.
Demo: spots added by visitors live for 24 hours (lib/demo-cleanup.ts). Expired spots disappear from the demo immediately (query filter) and are removed for good — row and files — after the response, at most every ten minutes; builtin showcase spots never expire. The demo's spot list says so.
Changed
Rebrand — "the departing spot". A calmer logo replaces the framed sharp Z: a flat warm disc with a Z drawn as one soft line, and a small orange dot that has left the circle along the Z's bottom line; lowercase "zirospot" wordmark in Outfit, lowercase tagline. The accent moves from #f97316 to the softer #f08a4b on the web and in the app (no gradients). Regenerated from brand/generate.mjs: brand SVGs (new on-light variants, the swoosh alternative is gone), mobile icons/splash, web favicon, Apple touch icon and social card.
Mobile: the tab bar is half again as tall and has four tabs; the "?" feedback button left the bar and the account footer left the Stations tab — both live in Profile now.
/api/tts resolves the request scope (an invalid token is a 401, as everywhere else).
Privacy policy and terms name ElevenLabs as a voice provider.
v0.26.02026-09-13
Mobile app 0.4.0: the player keeps working without internet, heals a dropped stream on its own and tells the venue how it is doing. Follows the static review of the 0.3.0 APK and the memory-leak audit.
Added
Mobile: on-device spot cache (src/spot-cache.ts). Every spot, language variant and the per-locale promo is downloaded into the cache directory after bootstrap and whenever the spot list changes; breaks play the local file and fall back to the URL only when it is missing. An internet outage stops the radio at worst, never the ads.
Mobile: offline start. The last successful /api/bootstrap answer is kept in AsyncStorage; when the server is unreachable the app starts on that snapshot (status "last known"), retries every minute and on foregrounding, and swaps in fresh data silently.
Mobile: stream self-healing in the engine. A failed or dropped stream no longer surfaces as an error to act on — the engine reconnects with exponential back-off (2 s … 60 s), the scheduler's clock keeps running on a near-silent keepalive player (the same trick the web uses), so breaks still fire while the radio is down and the radio comes back on its own. The keepalive also bridges every break, which gives the new spot watchdog a background-safe clock: a spot that never starts or never ends is failed instead of hanging the break forever.
Mobile: health strip under the player controls — stream (playing / reconnecting, attempt n / off), spots stored offline (n of m), internet (from the last API call), settings (up to date / last known) and the time of the last spot — plus, on Android, a battery-optimisation hint with a button that opens the system setting (Samsung/Xiaomi/Honor kill background apps under their default policy) and a "already set" dismiss.
PlayLog outcomes: PlayLog.status (played | skipped | failed) and reason. Both players now log once per spot at its terminal event — never ahead of time — so "played" means the shop heard it to the end, "skipped" that the venue cut it, "failed" why it did not play. Success rate = played / (played + failed); GET /api/log returns the fields. Migration play_log_status.
Changed
Mobile build hygiene: SYSTEM_ALERT_WINDOW (pulled in by React Native), USE_BIOMETRIC / USE_FINGERPRINT (androidx.biometric via expo-secure-store) are blocked in the manifest, allowBackup is off, orientation is unlocked (tablets lying flat), and the preview EAS profile builds an arm64-only APK for testers (~35 MB instead of the 82 MB universal APK; Play gets the AAB).
Mobile: the login hint no longer sends the venue to the web console for spots and schedules (the app has had them since 0.2.0).
HintBox: the whole card toggles on mobile (one press target with hit slop); on the web the <summary> line is full-width and finger-sized.
Web engine: destroy() detaches its own listeners and unloads the keepalive; onSpotEnd(spot, how) / onSpotFailed(spot, reason) events.
Fixed
Mobile memory/resource leaks from the audit: a failed stream no longer logs a breadcrumb and re-renders the player four times a second; the keep-awake lock is released when the player unmounts mid-broadcast; recordings are deleted after upload/discard/abandon; a TTS preview generated after the form was left is not written; the schedule's "saved" timer is cleared on unmount.
v0.25.12026-09-13
First fixes from Sentry (mobile app 0.3.1).
Fixed
Mobile: uploading a spot (file, recording or AI voice) failed with a network error on every attempt — Expo SDK 57 replaces the global fetch with expo/fetch, which builds multipart bodies in JS and rejects React Native's {uri} file parts ("Unsupported FormDataPart implementation", Sentry ZIRO-NATIVE-1). uploadSpot now streams the file through expo-file-system's native upload task with an explicit MIME type.
Mobile: leaving the microphone form (switching to the file/AI source or another tab) while a recording was running could crash the app on Android. useAudioRecorder released the native recorder before the component's own cleanup touched it; the recorder is now owned by a ref, created on the first Record, stopped and only then released on unmount, and the audio session is always handed back to the broadcast.
Web: a Neon-terminated Postgres connection ("terminating connection due to administrator command", Sentry ZIRO-NEXT-2) became an uncaught exception and killed the serverless function. Every pg client of the Payload pool now has an error listener (onInit), and the Prisma adapter logs pool/connection errors through lib/log.ts instead of throwing.
v0.25.02026-09-10
Added
Observability: Sentry on both apps. Mobile (@sentry/react-native, app 0.3.0): crashes and errors, every engine/scheduler/API/app-state transition as a breadcrumb, and a Diagnostics screen (account footer) where a tester reads the last 300 events, shares them as text or sends them to Sentry with a note. Web (@sentry/nextjs): server, edge and browser runtimes, unhandled request errors, a global error page, and a structured JSON logger (lib/log.ts) used by the spot, TTS and station search routes — visible in Vercel logs, warnings/errors mirrored to Sentry. Everything is off until the DSN env vars are set.
Brand: the client's logo (September 2026) as vector sources in /brand — framed-Z mark (square, sharp corners), mono variant, swoosh variant, Archivo wordmark and the full lockup with the tagline, text converted to outlines. Generated from brand/generate.mjs, which also writes the mobile app icons (iOS, Android adaptive + monochrome, splash, favicon) and the in-app SVG components (src/brand.tsx); the app's wordmark is now the real lockup. The web favicon, Apple touch icon and social card use the same mark.
v0.24.12026-09-10
Fixed
Mobile (first Android device test, app 0.2.1): ad breaks now fire with the app in the background / screen off. React Native pauses JS timers there, so the engine's fades and the stream-connect timeout are now also stepped from the players' native status updates instead of setTimeout alone — a fade used to stall the whole break until the app was reopened, after which the queued spots played back to back.
Mobile: Stop always returns the player to idle, even if releasing a native player throws; the countdown no longer keeps running after Stop.
Mobile: station/spot/TTS previews pause before release (they stayed audible on Android) and stop whenever the app leaves the foreground — a preview could keep playing after the app was closed.
Mobile: multiline text fields (AI spot text, variants, feedback) lost their border and text colour — the caller's style replaced the base style instead of extending it, so typed text was invisible.
v0.24.02026-09-09
Added
Mobile app (Phase 4b, console parity): the Spoty tab with the same Spoty ⇄ Rozvrh switch as the web. Spots: create with AI voice (preview via /api/tts), from a file (native picker) or the microphone (expo-audio recorder), preview, edit (name; TTS text, voice, tone and language variants for AI spots), enable/disable, reorder with up/down arrows, delete with confirmation; the builtin showcase spot is protected exactly like on the web. Schedule: interval, fade, spots per break and fixed-time breaks (time, weekdays, pinned spot). Console chrome: in-app language switch (sk/cs/en, persisted), first-run venue creation, the "?" feedback sheet, links to registration, changelog and the legal pages. Mobile app version 0.2.0.
Web: privacy policy gains a "Mobile app" section (microphone, background playback, files, local storage) in all three languages.
Changed
Spot constants (MIME whitelist, 25 MB cap, TTS text limit, voice/tone keys, HH:MM format, size formatting) moved to @zirospot/shared/spots; the web API routes and components use them, so both clients validate against the same values.
Mobile: the microphone permission is now declared (expo-audio plugin) — required for recording spots.
v0.23.02026-09-09
Added
Mobile player: the Stations tab is now 1:1 with the web console — "Moje stanice" (pick, remove, add a custom stream) and "Katalóg" (Radio Browser search from 3 characters, genre chips, stream preview, one-tap add). A freshly added station becomes the active one, exactly like on the web.
Web: a proper /login page for venues (e-mail + password, remembers the ?next= target, admins land in the Payload panel).
Fixed
Web: signing in from the landing page, the register page and an unauthenticated /app visit led to the Payload admin panel instead of the venue console. The Payload panel is now admin-only; venues always end up in /app.
v0.22.02026-08-26
Added
Mobile player, phases 3–4: the crossfade engine ported onto expo-audio (two native players, elapsed-time fades, no keepalive track — the audio background mode keeps the app alive), a full DJ screen with countdown ring, start/stop, pause, "spot now", skip, volume, prev/next station, interval and fixed-time breaks, rotation, the free-tier promo, play logging, lock-screen card with play/pause, and keep-awake while on air. Awaits a real-device test (development build).
Changed
Scheduling rules (rotation, promo cadence, fixed-time matching, language-variant pick) moved to @zirospot/shared/scheduler; the web Player now uses the same functions as the mobile app — behaviour is unchanged.
v0.21.02026-08-26
Added
Native mobile player, phase 2 of the mobile-app plan: new Expo (React Native) app in apps/mobile — sign in with the web-console account (token kept in the device keychain), one-request bootstrap, station picker synced with the console's active station, sk/cs/en from the shared dictionaries, brand-dark UI. Playback arrives in the next phase.
Changed
The repository is now an npm-workspaces monorepo: the web app moved to apps/web, DTO types and i18n dictionaries to packages/shared (@zirospot/shared, consumed by both apps). No user-facing web changes; Vercel's Root Directory must point at apps/web.
v0.20.02026-08-26
Added
Backend groundwork for the native mobile player (mobile-app plan, phase 1): every app API route accepts Authorization: JWT <token> from POST /payload-api/users/login alongside the browser cookie; a present-but-invalid token returns 401 instead of silently falling back to the demo scope; login tokens live 30 days.
GET /api/bootstrap — venue, plan, locale, settings, stations, spots and schedules in one request (BootstrapDTO).
CORS headers on /api/* and /payload-api/* (wildcard by default, CORS_ORIGIN env narrows it).
Changed
/app and /demo load their initial data through the shared loadScopeData() used by the bootstrap endpoint.
v0.19.22026-07-26
Changed
The masthead tag ("demo · vX.Y.Z") now ellipsizes on one line when it doesn't fit next to the logo instead of wrapping under it; the wordmark itself still never breaks.
The landing-page feature carousel caps at the shell width (1180 px) on wide monitors instead of running edge to edge across the whole viewport.
The floating "?" feedback button and the section badge icons hug the 720 px console column on wide screens (shared --fab-inset token) instead of drifting to the viewport corners.
v0.19.12026-07-26
Fixed
The masthead no longer falls apart on narrow phones: the ZiroSpot wordmark never line-breaks anymore, and when the page tag ("demo · vX.Y.Z") doesn't fit next to it, it moves onto its own line under the logo (new .masthead-brand wrapper on all pages).
v0.19.02026-07-25
Added
Background playback hardening for mobile (locked screen): the whole broadcast — radio, countdown, interval breaks and fixed-time schedules — now keeps working while the phone's screen is off. - The break countdown and the fixed-time scheduler are driven by clock deadlines instead of 1 s counter ticks, so throttled background timers can't stretch the interval; a late tick fires an overdue break immediately, and fixed-time breaks missed by up to 10 minutes are caught up (older ones stay missed on purpose). - The scheduler additionally ticks from the playing audio's timeupdate events, which mobile browsers keep firing with the screen locked while audio plays. - A near-silent looping keepalive track plays for the whole broadcast and bridges the gaps between radio and spots, so the page never stops producing audio — that's what keeps iOS from suspending the tab mid-break. - Volume fades compute their progress from elapsed time, not tick counts — a background-throttled fade still finishes on schedule.
Media Session integration: the lock screen shows the station (or the spot on air) with the ZiroSpot artwork, and its media controls work — play/pause, stop, and previous/next switch stations.
Fixed
Pausing the radio now freezes the break countdown exactly; resuming restores the remaining time (previously the counter simply stopped being decremented, which drifted with throttled timers).
Fixed
The ON AIR pill ("Vypnuté") is pixel-aligned with the "?" button on every screen: it no longer floats with the DJ console's vertical space distribution but is pinned to the same shared top offset (--fab-top) as the "?" button and the section badges.
Changed
The floating row ("?" button, section badges, ON AIR pill) moved up — less dead space between the masthead and the content; the stations and spots sections start correspondingly higher.
v0.18.52026-07-24
Changed
The section badge is a bare 40 px icon (no circle), matching the overall size of the "?" button.
v0.18.42026-07-24
Changed
The showcase spot cannot be switched off either — the API rejects the rotation toggle for builtin spots (only reordering remains) and the UI hides the button.
v0.18.32026-07-24
Fixed
The section badge and the "?" button are pixel-aligned on load on every device: both anchor to the same viewport-top offset (the badge absolutely positioned in the document, so it still scrolls away with the content).
v0.18.22026-07-24
Changed
The section badge lives in the content flow (scrolls away) and the floating "?" is aligned to the same line as the badge.
v0.18.12026-07-24
Changed
Stations/Spots views show a section badge (radio / speaker icon) on the left, mirroring the "?" button's circle on the right.
The logo rise-up is slower (2.6 s) and more three-dimensional: tighter perspective, depth offset and a brightness sweep as it lifts.
v0.18.02026-07-24
Added
Protected showcase spot: Spot.builtin (migration spot_builtin) marks platform demo spots — the API returns 403 for delete, rename, TTS edits and variant changes (rotation toggle and reordering still work) and the UI hides Edit/Delete. The demo's "Ukážkový spot" is marked builtin.
Default spot artwork: every spot row shows a cover tile — sparkle on an accent gradient for AI spots, a music note on a neutral one for uploaded/recorded spots.
Changed
The logo rise-up animation is slower (1.5 s).
v0.17.52026-07-24
Added
Logo entrance animation: on page load the SPOT part of the wordmark lies flat along the z-axis and rises up into place with a slight overshoot (pure CSS, honours reduced motion).
v0.17.42026-07-24
Changed
The feedback "?" button sits on the right under the header; the registration/create-venue content and the console tab sections got extra top spacing so nothing sits underneath it.
v0.17.32026-07-24
Changed
Masthead order unified across pages: action icons (register/log-in or logout) first, the language flag always at the far right; auth icons grew to 26 px. The landing footer is left-aligned on mobile.
v0.17.22026-07-24
Changed
Landing: register and log-in moved into the masthead as always-visible icons; the hero keeps a single prominent "Try the demo" pill with an oversized play icon.
v0.17.12026-07-24
Changed
The landing feature carousel is full-bleed — it runs from browser edge to edge instead of stopping at the content column.
The landing footer stacks vertically on mobile.
The "How it works" chevron sits after the label (the left edge belongs to headings).
v0.17.02026-07-24
Added
Landing feature carousel: the feature boxes are an infinite auto-scrolling carousel (pure CSS marquee, pauses on hover, honours reduced motion) with icons, extended to six cards (crossfade, catalog, instant spots, fixed times, language versions, no install).
Changed
The hero play circle scales with the device height — up to 290 px on tall phones, down to 120 px on short ones (clamp over 100dvh); it can never overflow the screen.
The feedback "?" button sits under the sticky header on the left.
The "How it works" collapse chevron is bigger and accent-colored.
The public demo's Slovak-only "AI spot" was replaced with a multilingual "Ukážkový spot" (sk base + cs/en variants), so switching the app language switches the played ad too.
v0.16.12026-07-24
Changed
Fluid DJ layout: the player column now fills exactly the visible height between the sticky masthead and the tab bar (min-height: 100dvh − chrome) and distributes leftover space evenly between its rows — no dead gap above the tab bar and no residual scrolling on iPhones.
v0.16.02026-07-24
Added
Language versions of spots: a spot can now carry per-language variants (new SpotVariant model, migration spot_language_variants; base audio language stored in Spot.locale). Playback follows the app language — the Player picks the variant matching the current locale and falls back to the base audio. In the AI-spot edit form each other language gets a text field + Generate/Delete (server-side Edge TTS via PUT/DELETE /api/spots/[id]/variants).
Locale-native TTS voices: the female/male voice picks map to native neural voices per language (sk Viktória/Lukáš, cs Vlasta/Antonín, en Jenny/Guy) — Czech and English spots no longer speak with a Slovak accent. Applies to /api/tts (UI language) and variant generation.
Changed
Landing page buttons are player-styled pills with icons (play for the demo, add-user for sign-up, log-in arrow).
v0.15.02026-07-24
Added
Drag & drop spot reordering: hold the new handle on a spot row and drag to change the rotation order. Persisted via PUT /api/spots/reorder (rewrites sortOrder in one transaction). Works with touch (the handle blocks scrolling while dragging).
Changed
The feedback "?" button moved to the bottom-left, level with the tab bar, so it no longer floats over content.
Registration and create-venue forms lost their card chrome too.
The DJ tab hero now sizes itself from the dynamic viewport height (100dvh) where supported — no more tiny scroll on iOS Safari with its collapsing toolbar.
v0.14.12026-07-24
Changed
Sticky masthead: the top bar (logo, version, language, register) stays pinned with a blurred glass background on every page.
The player always fits the viewport: the hero play button (and its countdown ring) shrinks on short screens via height media queries, so the DJ tab shows 100% of the player between the sticky masthead and the bottom tab bar with no scrolling.
Masthead never overflows on narrow phones: the register link is icon-only below 640 px, the version tag truncates with an ellipsis, and gaps/logo tighten below 400 px.
v0.14.02026-07-24
Added
AI spot text editing: TTS-generated spots now store their source (new Spot.ttsText/ttsVoice/ttsTone columns, migration 20260724103855_spot_tts_source). The Upraviť button opens name + text + voice + tone for AI spots; changing the source regenerates the MP3 server-side (lib/tts.ts, shared with /api/tts) and replaces the stored file. Uploaded/recorded spots keep name-only editing.
Collapsible section explainers: the "how it works" paragraphs moved under their section headings into <details open> boxes (HintBox) — visible by default, one tap to collapse.
Changed
Language switcher is a dropdown in the masthead — collapsed it shows only the flag of the active language.
Spoty/Rozvrh switch is a prominent full-width segmented control, clearly distinct from the small source pills below it.
Console sections lost their card chrome — stations, spots and schedule now sit flat on the page background (mobile gains width).
Action-strip icons (Pauza / Spot teraz / Preskočiť) are orange.
v0.13.02026-07-24
Changed
Spots tab: one card with a segmented Spoty/Rozvrh switch instead of two stacked cards (both parts stay mounted so a running preview survives switching).
Schedule pane restructured into two labelled blocks: "Interval spotov" (interval → spots per break → fade → save) and, behind a divider, "Pevné časy" (time → days → spot → add).
Feedback dialog is a real modal now: fullscreen on mobile with the submit button anchored at the bottom, a centered card over a dimmed backdrop on desktop; Escape or a backdrop click closes it.
Floating fields no longer stretch inside tall flex parents (.field-float lost its row-era flex: 1).
v0.12.12026-07-24
Changed
Forms stack vertically on every screen size: the side-by-side .form-row field grid is gone, inputs and action buttons stretch to the full column width (mobile-app look on desktop too).
v0.12.02026-07-24
Added
Spot editing: an Edit button on every spot row renames it inline (PATCH /api/spots/[id], which already supported name).
Payload admin logout with a label: the icon-only logout button in the admin nav now shows an icon + "Logout" text (payload/components/LogoutButton.tsx via admin.components.logout.Button).
scripts/generate-promo.mjs — regenerates the per-locale promo MP3s with Edge TTS when the promo text changes.
Changed
Free-tier promo is a standalone slot every 4th played spot instead of opening every ad break (cadence persists in localStorage as zirospot.promoCount.<scope>). The promo text is much shorter now ("Táto reklama vám hrá vďaka ZiroSpotu."), regenerated for sk/cs/en, and the terms promo clause was updated to match.
A spot pinned to an active fixed-time schedule leaves the normal rotation — it plays only at its scheduled time and the spot list shows it as "iba v rozvrhu". This fixes the closing-announcement spot playing inside regular rotation blocks.
Dark Spotify-style UI everywhere: the management panes (spots, schedule, stations), auth cards and the feedback dialog switched from light panes to dark cards; form fields lost their filled background (transparent, outline-only, floating labels stay) and native widgets (selects, time picker) render dark via color-scheme: dark. Light panes remain only on document pages (privacy, terms, changelog).
v0.11.02026-07-24
Added
Release notes page (/changelog): renders this changelog with localized page chrome (sk/cs/en); the vX.Y.Z version tag in every masthead now links to it. Parser in lib/changelog.ts; the file ships to Vercel via outputFileTracingIncludes.
Floating-label inputs: text, number, time, select and textarea fields across the app (registration, venue creation, spot name, AI voice, schedule, custom stream, feedback) use a native-style floating label that rests inside the control and floats up on focus or fill. New .field-float primitive; the static .field stays for file inputs and button groups.
Changed
Styling migrated to Tailwind CSS v4 (two steps: a visual 1:1 migration, then the redesign on top). Design tokens live in @theme, shared design-system primitives stay as semantic classes in @layer components, one-off layout is utility classes in JSX. .grid → .tab-grid and svg .ring → .hero-ring renames avoid Tailwind utility collisions.
New brand palette — orange & black: near-black warm background with an ember glow, orange accent (#f97316) with dark ink text on orange surfaces (contrast), light management panes warmed to match. Lamp red stays reserved for ON AIR / recording; the break glow now shifts orange → red.
Feedback dialog restyled: rounded sheet with a pop-in animation and floating-label fields.
v0.10.02026-07-22
Added
Free/premium plan with a non-skippable promo spot: venues now have a plan (Payload venues.plan, admin-only editable, default free). On the free plan every ad break opens with a short ZiroSpot promo spot (static per-locale MP3 in public/promo/, generated with Edge TTS) that can be neither skipped nor stopped — the hero stop button is disabled while it plays and the engine refuses to interrupt it. The venue's own spots gained a Skip button in the action strip, and stopping the broadcast mid-break now actually cancels the rest of the break (the engine previously kept playing queued spots after stop). The demo always runs as free. Terms of use gained a free-tier/promo clause.
Fixed-time scheduling: new ScheduledBreak model + /api/schedules routes + a "Pevné časy" section in the Rozvrh panel. A schedule is a time (HH:MM, device-local), optional weekdays (empty = daily), and an optional pinned spot (default: normal rotation block). The Player checks schedules every second while on air and fires the break in the matching minute — e.g. lunch menu at 11:55 or a closing announcement at 21:55.
Localization (sk/cs/en): hand-rolled dictionaries in lib/i18n/ (no library). The locale resolves per request: zirospot.locale cookie (set by the new bottom-left language switcher) → Vercel geo country (SK→sk, CZ→cs, other→en) → Accept-Language → en. Server pages use getDict(), client components the I18nProvider context, and API routes return localized { error } messages; the audio engine now emits error codes instead of Slovak strings. Privacy policy and terms of use exist as full per-locale documents, metadata and <html lang> follow the locale, and the free-tier promo spot plays in the UI language.
v0.9.02026-07-20
Added
Invites: admins can invite new admins (or venues) via one-time links. New Payload collection invites (admin-only) generates a shareable link with a role; /register?invite=<token> shows the invite, assigns the role through /api/invites/redeem and burns the invite (7-day expiry). No email sending yet — the admin shares the link manually (an email adapter, e.g. Resend, can be added later).
v0.8.02026-07-20
Added
Privacy policy (/privacy) and Terms of use (/terms) tailored to the service (account/venue data, uploaded spots, TTS via Microsoft, Radio Browser, public demo caveats, SOZA/Slovgram responsibility of the venue). Linked from the landing footer and the registration form.
Feedback widget: floating "?" button on all app pages (demo included) for reporting bugs and requesting features. Submissions land in the Payload collection feedback — visible only to admins at /admin (Vercel has no feedback inbox; this is our own).
v0.7.02026-07-20
Added
Multi-tenant venue console at `/app`: authenticated users get their own space — stations, spots, schedule and play logs scoped to their venue (venueId columns + per-venue settings rows). First login walks the user through creating a venue; masthead shows the venue name and a logout button. Anonymous visitors keep using the public demo data.
Data isolation is enforced server-side in every API route via lib/tenant.ts (Payload cookie → user → venue); logged-in users opening /demo are redirected to their console.
Changed
Unified design across screen sizes: the desktop now uses the same app-like look as mobile — one centered column with the bottom icon tab bar (DJ / Stanice / Spoty) — just slightly roomier (larger fonts, wider column). The two-column desktop grid is gone.
v0.6.02026-07-20
Moving from prototype toward the production app.
Added
Payload 3 admin + auth: admin panel at /admin, REST at /payload-api, collections Users (admin/venue roles, first registrant becomes admin) and Venues (future tenant unit). Payload lives in its own Postgres schema payload on the same Neon DB. New required env var PAYLOAD_SECRET.
Self-registration at /register (Slovak form → Payload REST → auto-login → admin).
Public landing page at /; the full console now runs as a no-login demo at `/demo`.
Changed
Dependency refresh: Next.js 15 → 16.2, React 19.2, Prisma 6 → 7 (driver adapter @prisma/adapter-pg, prisma.config.ts, generated client in lib/generated/prisma), package is now ESM ("type": "module"). TypeScript stays on 5.9 — Next.js does not support TS 7 yet.
v0.5.12026-07-20
Changed
The masthead shows the current app version (from package.json) instead of the static "prototyp" label.
v0.5.02026-07-20
Changed
Directory search is live-as-you-type: fires automatically (debounced 400 ms) once at least 3 characters are entered, no confirmation needed. Out-of-order responses are dropped so fast typing can't show stale results.
v0.4.02026-07-20
Added
Mobile app layout: a fixed bottom tab bar with icons (DJ / Stanice / Spoty) on screens ≤900px; content above stays scrollable. Tabs hide sections with CSS only, so the player keeps playing across switches.
Changed
Fresh installs start with no stations: the seed no longer creates default stations, and with an empty list the app opens on the stations tab with the directory (Katalóg) ready, so the venue configures its radio at first page load. Built-in seeded stations were removed from the production DB (user-added ones kept).
v0.3.02026-07-20
Changed
Full UI redesign modeled on the Daily Tunes app: midnight-navy gradient, cobalt accent, glowing waveform behind a central circular play/stop control with the countdown ring, large light station name with prev/next arrows, icon action strip (pause, spot now) and full-width volume slider.
Station browser is now a light list pane (app-style): "Moje stanice" / "Katalóg" tabs, rows with station logos, animated EQ indicator on the active station, icon buttons for preview/add/delete. Replaces the old <select> picker (StationPicker → StationPanel).
Spots and schedule panels restyled to matching light panes.
Switching stations (arrows or list click) while on air re-tunes the stream live instead of waiting for the next broadcast start.
Fixed
Horizontal overflow on narrow screens: the single-column grid track now uses minmax(0, 1fr) so panels shrink below their content minimum.
v0.2.02026-07-20
Added
Radio station directory search backed by the free Radio Browser API (api.radio-browser.info): search by station name or genre chips, preview a stream before adding it, one-click add to the station list. Results are filtered to plain https streams (no HLS, no mixed content).
This changelog; package.json version is now maintained per release.
Changed
POST /api/stations deduplicates: re-adding a station with a known directory uuid or an existing stream URL returns the existing row instead of creating a duplicate.
v0.1.02026-07-19
Initial prototype: internet radio playback with DJ-style crossfade to uploaded ad spots on an interval schedule, manual break trigger, spot upload/recording/TTS generation, station picker with custom streams, play logging, Vercel deployment (Neon Postgres + Vercel Blob).