Seseri 3.1.0 — production finalization (security, queue UI, a11y, RTL, release CI)#3
Merged
Conversation
…nt SW - Work around iTunes API serving CDN-cached responses with another origin's Access-Control-Allow-Origin (the "Failed to fetch"/CORS error): route search & lookup through itunesFetch() with a cache-busting param + proxy fallback so every request gets a fresh, correctly-attributed response. - Make service worker path-independent (relative precache/scope) and network-first for navigations so new deploys are picked up; bump cache. - Fix manifest scope/start_url and 404.html redirect to derive base path instead of hard-coding the old /podcast-player/ location.
- Wordmark "Seseri" no longer disappears under its animated sheen: the white highlight band is now a theme-aware token (--wm-shine) that becomes a violet shimmer on the light canvas instead of white, so letters stay legible at every point of the sweep, in every theme. - Hero vignette is theme-aware (--hero-vignette): no more muddy grey corners on the light background. - Darken light-theme --text3 (captions/dates) for AA-level legibility on white.
…tokens Refined Studio direction — make Seseri feel like real audio hardware. - Signature: replace the flat progress bar with a real waveform scrubber. Per-episode amplitude bars are generated from a deterministic seed (mulberry32 hashed from the track id; no audio analysis, which is impossible on cross-origin podcast MP3s). Played region is accent-filled via clip-path; drag to scrub with a time tooltip, a leading-edge playhead, keyboard support, and a subtle per-bar "breathing" + playhead glow only while playing. - Living equalizer: now-playing eq animates only while playing; the currently playing episode row swaps its index for a mini-equalizer (gated by body.is-playing). - Design tokens: add a 4-pt spacing scale (--s-1..8), a type scale (--fs-*), motion-duration tokens (--d-fast/base/slow), a layered elevation ramp, and waveform tokens; route player rhythm/transitions through them. - Motion: now-playing title crossfades on track change; quieter ambient glow. - Floors held: single file, no new deps, all 3 themes, i18n, focus-visible, and full prefers-reduced-motion (waveform/eq/ambient all static).
- Settings jank fix: the drawer stacked three backdrop-filter blurs (overlay blur(6px), panel blur(28px) saturate, sticky header blur(16px)). Animating translateX on a blurred panel forces a full backdrop re-rasterization every frame. Replaced with a solid --surface drawer + plain scrim; added will-change:transform and contain:layout paint so the slide is composited. - Episode rows get content-visibility:auto + contain-intrinsic-size so off-screen rows in long feeds (100+ episodes) are not rendered until scrolled into view. - Waveform "breathing" now animates every other bar instead of all 56, halving active animation layers while keeping the living feel.
- Settings scroll jank: freeze all continuous background motion while the drawer is open (body.settings-open pauses the ambient glow, wordmark/logo, waveform, equalizers, pulse) so the panel scroll gets the full frame budget. - Drop the blur(8px) filter on the hero glow — the radial gradient is already soft, and a continuously animated filter pass steals frames; promote it with will-change so the breathe is purely compositor work. - Scroll containers (settings panel, episode list, results) get overscroll-behavior: contain + -webkit-overflow-scrolling: touch for momentum and no scroll-chaining; panel uses contain: paint.
- Merge README.md + README.tr.md into one bilingual README with in-page language anchors (English / Türkçe); delete README.tr.md. - Update all docs to the seseri deployment: live URL, clone URL, path-independent deploy note; document the waveform feature. - Refresh privacy policy (date, drop the Android mention — pure web app now, add Google Fonts disclosure); SECURITY proxy note. - CHANGELOG 2.1.0: CORS workaround, path-independence, design overhaul, perf. - Trim .gitignore to web-relevant entries (drop Android/Gradle/JetBrains cruft).
Use #lang-en / #lang-tr so the in-page language jump links don't collide with auto-generated heading slugs and work across renderers.
Add og:site_name, og:image:type/width/height/alt and twitter:image:alt so platforms render the share preview without re-fetching or guessing dimensions.
…l guard - Replace render-blocking @import with a <link> stylesheet + preconnects to fonts.googleapis.com / fonts.gstatic.com / itunes.apple.com (faster first paint and first search). - Add referrer policy (strict-origin-when-cross-origin) so deep-link query params (?rss=<url>, ?podcast=<id>) don't leak in the Referer to third parties. - Download: pass the episode URL through httpsOnly() before assigning it to the <a> element (defense-in-depth against non-https / javascript: URLs).
- #4 Replace all emoji/symbol glyphs with one consistent inline-SVG icon set (sprite + <use>): fav/share/settings/play/pause/prev/next/rewind/forward/ download/sort/moon/settings-heading. Strip baked-in glyphs from i18n strings. - #3 Favorited state now shows a filled accent star + accent chrome (not just ☆→★). - #5 Custom <select> chrome (no native arrow) matching the button cluster; moon icon label for the sleep timer. - #8 RTL: skip buttons are icon+number (no misplaced −/+); speed select forced LTR so it reads "1×" not "×1". - #6 Localize aria-labels via new data-i18n-aria; t() now falls back to English. - #7 Waveform slider announces aria-valuetext (time) and handles Home/End. - #9 Sleep timer: active accent state + a transient status message on set/expiry.
When a feed is open but no episode is selected yet, the player now reads as a prompt instead of a loaded track: - hide the "now playing" label + equalizer (it wasn't playing anything); - de-emphasize the waveform (dimmed, grayscale) and make it non-seekable (pointer-events off, removed from tab order, aria-disabled); - disable the play and skip buttons (prev/next were already disabled at idle). Selecting an episode clears the idle state and activates the transport.
Paste a YouTube playlist/channel/video link to listen to shows that publish on YouTube. Episode list comes from YouTube's keyless Atom feed (existing CORS-proxy path); playback uses the official youtube-nocookie IFrame embed — no API key, no third-party stream servers. A shared playback facade routes transport/keyboard/scrubber/Media Session to either <audio> or the YT player, so audio podcasts are unchanged. Download hidden for YT items; CSP tightened (script-src + frame-src), SW cache bumped to seseri-v3.
…rrors The feed fetch waited on one CORS proxy and, on the 25s blanket timeout, returned silently — leaving 'loading episodes' forever. Now all proxies are tried in parallel (first non-empty body wins, each with its own timeout) and a third proxy (codetabs) is added; total failure surfaces a real error instead of an endless spinner. connect-src updated for codetabs.
Two fixes for YouTube playback: - extractYouTube only matched www.youtube.com (dot-prefixed); a bare 'youtube.com/...' link fell through to the RSS path and failed. Guard now accepts youtube.com with/without www/subdomain and youtu.be. - Generic CORS proxies are unreliable; fetch the feed via rss2json (a CORS-enabled RSS->JSON service) first, falling back to the raw Atom XML through the proxies. connect-src updated for api.rss2json.com.
Hide the embed off-screen (kept rendered so audio keeps playing — never display:none) so YouTube feeds look like a normal audio podcast (cover art + waveform). Request suggestedQuality:'small' to reduce data; the official embed still streams video in the background (no audio-only stream exists), but at the lowest rendition.
For playlist links, enumerate the full list (up to ~200) through the IFrame
player's getPlaylist() instead of only the feed's latest ~15. Items beyond the
feed show without a title/date ('Bölüm N') until played, when the title is
filled from the player. Falls back to the feed-only list if enumeration is
blocked or times out (graceful degradation, verified). Drops the '~15' note
when the full list is available.
…ates, download) Resolve YouTube shows through public Piped/Invidious instances to a direct audio URL played by the normal <audio> element — ad-free, background/lock-screen, resume, download, full list (up to ~200) with real dates + durations, lowest bitrate to save data. Several instances raced in parallel. Falls back to the keyless feed + official youtube-nocookie embed when no instance serves the content (then embed limits apply). CSP connect-src updated for the instances.
- ytServiceAudioUrl now picks the highest-bitrate audio stream (was lowest). - On the embed fallback, episodes that only have a 'Bölüm N' placeholder get their real title from noembed (keyless CORS oembed); for shows that put the date in the title this also surfaces the date. CSP connect-src += noembed.com.
Tagged current state as v1-legacy. Screenshots double as store-listing source material later (P6).
- Legacy app moved intact to legacy/ (runnable via npm run legacy)
- Design-token CSS extracted verbatim into src/styles/{tokens,themes,base,components}.css
- Strict tsconfig, ESLint flat config, Prettier, Vitest wiring
- vite-plugin-pwa injectManifest with sw.ts port of legacy sw.js
- Core domain types in src/feeds/types.ts
- public/ carries manifest.webmanifest, icons, privacy policy, 404
- 2,070 lines of legacy JS ported into src/{state,feeds,youtube,player,ui,storage,i18n}
- i18n: 8 languages as typed langs/*.ts (Lang interface — missing key = compile error)
- feeds: unified resolveFeed() replaces loadPodcast/loadRss/loadYouTube duplication;
parseRss is a pure, unit-tested function; proxy chain has a VITE_API_BASE Worker slot
- player: engine.ts transport facade (<audio> + YT embed) emits typed events
- UI: typed h() DOM builder — no innerHTML with remote data, no inline handlers;
toast replaces alert(); visible error states replace empty catches
- Router: pushState history — back button navigates feed → home; legacy
?podcast=/?rss=/?yt= deep links unchanged
- CSP: script-src no longer needs 'unsafe-inline' (verified 0 violations)
- Tests: 34 passing (rss-parser via jsdom, input-parse, i18n completeness)
- Smoke-verified end-to-end: search→play→resume, YouTube video (real audio +
title fill), RSS (mocked proxy, 175 eps), themes, settings, favorites
- storage/db.ts: idb `seseri` v1 (feeds + downloads stores); settings/ progress/subscriptions intentionally stay in localStorage (legacy compat) - player/offline.ts: episode audio in Cache API bucket `seseri-audio`, blob-URL playback (native seek, no SW range handling), storage estimate, persistence request at boot - player screen: stale-while-revalidate feed open (cached copy paints instantly, network refresh preserves the playing episode), offline copy preferred for both direct audio and YouTube episodes, download toggle with legacy file-download fallback for CORS-blocked CDNs - sw.ts fix: activate no longer wipes `seseri-audio` on shell upgrades - storage/opml.ts + tests: OPML export/import incl. iTunes/YouTube link mapping; settings panel gains storage usage, OPML/JSON export, import, clear-downloads (11 new i18n keys × 8 languages) - scripts/smoke-p3-offline.cjs: headless Edge offline smoke — download → offline reload → cached feed renders, downloaded episode plays & seeks (9/9)
… pool - worker/: Hono app on workerd — GET /v1/feed?url= (RSS proxy, SSRF guard, 20 MB cap, 15 min edge cache), /v1/itunes?url= (host-pinned, 1 h cache), /v1/yt/list & /v1/yt/resolve (plan option B: server-side race over a cron-health-checked Piped/Invidious pool stored in KV) - CORS: prod origin allowlist + any localhost origin; KV fixed-window rate limit (~60/min/IP) - 22 handler/unit tests on @cloudflare/vitest-pool-workers (SSRF matrix, validation, proxy happy paths, CORS reflection) - root: worker:* scripts, verify chain now includes worker typecheck+test; .env.development sets VITE_API_BASE=http://127.0.0.1:8787 (client falls back to public proxies when the Worker is down) - scripts/smoke-p4-worker.cjs: e2e proof — The Daily (18 MB RSS, 2907 eps) renders in the browser through the local Worker (3/3) Verified live on wrangler dev: itunes lookup 200, 18 MB feed 200 (cache hit 0.26 s), YT endpoints degrade to 502 when every public instance is down (client then uses its embed fallback). youtubei.js spike (option A) deferred; tracked for the deploy phase.
- theme: 'auto' (default) follows prefers-color-scheme live; theme-color
meta + accent-soft resolve against the painted theme; new theme_auto
i18n key in all 8 languages
- layout: screen switching is now class-driven (body.feed-open) with
#app{display:contents}; ≥900px an open feed shows a library rail
(compact brand, search, subscriptions) beside the episode pane —
the idle home keeps its full-width hero
- mini player: leaving a feed no longer stops playback — a floating
glass transport (art, title, progress hairline, play/pause) appears on
home; tapping it re-enters the loaded feed without reloading
(state/now-playing.ts signal + ui/mini-player.ts)
- back button fix: deep-linked entries used to history.back() out of the
site; router now tracks whether home exists behind us (canGoBack) and
falls back to goHome()
- scripts/smoke-p5-mini.cjs: play → home → mini bar visible & playing →
pause-in-place → reopen feed without reload (5/5); screenshots at
1280/768/390 reviewed
- episode rows: partially-played episodes show an accent hairline at the listened percentage; ≥96% marks the row listened (dimmed, ✓ badge) - feed load errors render a real retry button (new btn_retry key × 8 languages) that re-runs the failed request - settings drawer is a native <dialog>: platform focus trap + Esc, ::backdrop dimming replaces the overlay div, animated close preserved, outside-click detection via the panel box; RTL slide direction kept
- state/queue.ts: session-scoped queue of trackIds with position lookup and end-of-track dequeue - every episode row gets a queue toggle (new ic-queue icon); while queued it shows the 1-based position as an accent chip; second tap removes - on 'ended' the queue is consulted before plain auto-next; opening a different feed clears it - btn_queue / queued i18n keys in all 8 languages; smoke extended to 7/7
Brand: - new icon: single-stroke geometric "S" (two 270° arcs, round caps) on the violet gradient tile; master SVG at public/icons/seseri.svg, all PNG variants (any 192/512, maskable 192/512 with safe zone, monochrome for Android themed icons) generated by scripts/icons.cjs via headless Edge - in-app logo: the S draws itself once on load (pathLength dash animation), then rests; reduced-motion collapses it to the final frame; SVG favicon Store readiness (P6): - manifest completed: id, categories, dir, display_override (window-controls-overlay), launch_handler navigate-existing, maskable + monochrome icons, wide/narrow screenshots (scripts/store-shots.cjs), shortcuts — "?resume=1" actually works (last feed persisted as pp_last_feed and reopened at boot) - .well-known/assetlinks.json template for the Play TWA - docs/STORE.md: full Turkish release guide (Worker deploy → Pages deploy → PWABuilder MSIX for Microsoft Store → TWA for Google Play, update flow, release checklist) Quality (P7): - new suites: lib/format (time/duration) and feeds/proxy-chain (first-win, all-down, abort, per-attempt timeout via stubbed fetch) — 51 frontend + 22 worker tests; docs/ci.yml verify workflow kept local until push - smoke-p3 fixed for the queue button sharing .ep-dl-btn (data-act selector) Housekeeping: - removed root duplicates of public/ files (404.html, privacy-policy.html, icons/); pages.yml now builds with Node and deploys dist/ (reads the VITE_API_BASE repo variable) - README rewritten for the v3 architecture; CHANGELOG 3.0.0 (version was behind the legacy 2.3.0 line); CONTRIBUTING/SECURITY updated; TESTPLAN gained v3 sections (offline, mini player/queue, two-pane, worker)
…proxy
Search by name:
- worker GET /v1/yt/search?q= — Innertube search first, health-checked
Piped pool as fallback; normalized {kind,id,title,author,thumb,extra}
- client: iTunes and YouTube searched in parallel; results render in
"Podcasts" / "YouTube" sections (channels, playlists, videos with
duration/count badges); rows open straight into the yt feed
- ytServiceSearch falls back to public Piped when the Worker is down;
sec_podcasts/sec_youtube i18n keys in all 8 languages
Background / lock-screen playback:
- worker/src/innertube.ts: youtubei.js session (rebound fetch for workerd,
player retrieval); tubeAudio races IOS/TV/ANDROID/MWEB/WEB clients until
one yields a decipherable audio format (SABR-era: some clients only
expose server-ABR); await-based decipher, URL validation
- GET /v1/yt/audio?id= streams the bytes range-aware through the worker
with the minting client's UA (googlevideo rejects foreign UAs and
range-less requests — full-file requests are sent as bytes=0- and
unwrapped back to 200); format URL cached in KV 30 min with 403-retry;
rate-limit exempt (seeking bursts range requests)
- /v1/yt/resolve now hands the client the proxy URL when Innertube
resolves (stream URLs are IP-bound to the worker), else the old pool
path; CSP media-src gains http://127.0.0.1:8787 for local dev
Verified E2E (scripts/smoke-p8-ytsearch.cjs, 3/3): "lex fridman podcast" →
YouTube section renders → channel opens (10 eps) → REAL AUDIO plays through
/v1/yt/audio (background-capable <audio> + Media Session). When YouTube
blocks the proxy (PO-token enforcement varies), the chain degrades to
Piped → official embed exactly as before.
Tests: youtubei.js stubbed in vitest-pool (import-attribute bundling);
+2 validation cases — 51 frontend + 23 worker green.
- innertube: clients reordered (TV/embedded first) and every candidate stream URL now passes a MID-FILE range probe before being accepted — under PO-token enforcement most clients only serve the first chunk, which would truncate playback ~1 min in; the probe guarantees the /v1/yt/audio proxy only activates when it will actually stream - audio chunks fetched via googlevideo's own `range=` query param in 1 MB slices; failed resolutions negative-cached in KV (15 min) so /v1/yt/resolve falls through to the Piped pool quickly - protocol-relative Innertube thumbnails normalized to https - CSP connect-src += https://seseri-api.bozdogancanahmet.workers.dev - scripts/smoke-live.cjs: production smoke — 7/7 against the live site (search via deployed worker, YouTube section, 41-ep feed, real playback, zero CSP violations) - desktop/: Tauri v2 shell (loads the live site; NSIS installer) + STORE.md rewritten: Microsoft Store replaced by direct Windows installer flow Site deployed: github.com/IACBI/seseri @ 0c0d463 → iacbi.github.io/seseri (v3 build with VITE_API_BASE baked in).
Tauri NSIS installer (1.8 MB) built, smoke-launched (app.exe + WebView2 up), published: https://github.com/IACBI/seseri/releases/tag/v3.0.0
The old flow awaited both sources before painting anything, so a cold worker-side YouTube search (15 s+) held the iTunes results hostage — on slow runs the search looked dead. Each source now renders into its own section the moment it settles; the empty/error state only shows when both came back empty. Global search abort raised 10 s → 30 s (podcasts paint long before it fires). Deployed to iacbi.github.io/seseri; live smoke 7/7 against the user's redeployed worker (probe-validated YT audio path live).
…kage metadata - docs/ci.yml -> .github/workflows/ci.yml (verify gate now runs on push/PR) - remove superseded docs/screens-v1/ (no references anywhere) - desktop/package.json: real name/description/author, MIT license, drop nonexistent main and failing default test script
- src/ui/states.ts: single stateBox(loading|empty|error) used by search and player; error variant gets role=alert + .empty-state--error (replaces inline color styles); search errors now offer a retry button - search result rows: role=button, tabindex, Enter/Space activation (previously mouse-only) - results list announces changes via aria-live=polite - settings close button: localized aria-label (new 'close' i18n key, 8 langs) - :focus-visible rings for s-select, s-range, lang-select (outline:none had removed them with no replacement) - light theme --text3 #6e7488 -> #62687c (>=4.5:1 WCAG AA on bg/surface)
At <=520px the label wrapped into 2-3 lines next to the filter input (three lines at 320px). The toggle button still shows the direction.
…lient Every YouTube episode had fallen back to the iframe embed (which mobile browsers pause on screen lock) because stream resolution broke on all fronts: PO-token enforcement caps IOS/MWEB/WEB URLs at the first ~2MB (mid-file ranges 403), the TV/embedded clients now fail playability or need a JS evaluator workerd can't provide, and the public Piped/Invidious pool is dead (verified: all 12 instances 4xx/5xx). Worker changes, each verified against live googlevideo endpoints: - resolve via ANDROID_VR (Meta Quest) — the remaining PO-token-exempt client: direct URLs, full-range (mid-file 1MB chunk 200, ~3MB/s) - generate_session_locally: false — locally generated visitor data now trips the 'Sign in to confirm you're not a bot' wall - retrieve_player: false — no cipher needed for direct URLs; also cuts the CPU spikes that caused intermittent 1102s - drop dead clients; don't cache a failed Innertube.create for the isolate's lifetime Client: one-time toast when the embed fallback is used (region-locked videos), warning that lock-screen playback may stop (new i18n key yt_embed_bg, 8 languages).
Geo-blocks, bot walls and unplayable videos all surface as a bare "no stream" to the client; log the real reason to wrangler tail so the datacenter-IP bot wall is diagnosable in production.
…s from prod CSP - fetchWithTimeout now follows redirects manually (max 3 hops) and runs safeTarget() on every Location target — closes redirect-based SSRF. - Build-only Vite plugin removes http://127.0.0.1:8787 and bare ws: from the shipped CSP meta (fails the build on token drift; dev untouched). - SECURITY.md documents the fix and residual DNS-rebinding risk.
…d keys Adds confirm_*, queue_*, offline_banner, toast_*_exported and lang_select_label; removes unused brand_sub / search_hint.
…ompt Replaces native confirm(); adds missing confirmation on clear-downloads and unsubscribe. Cancel is default-focused; Esc/backdrop cancel. Also success toasts on OPML/JSON export.
Feed open lands focus on the title, back restores the originating row; deep links don't steal focus; language/speed selects labelled; result and episode lists announce aria-busy while loading.
…, safe areas - components.css consumes the existing fs/s/r/d tokens (exact matches only) - one --z-* scale replaces magic z-indexes - direction-relevant physical properties -> logical (Arabic mirrors) - interactive controls extended to >=44px hit areas, visuals unchanged - viewport-fit=cover + safe-area-inset-top on header/topbar/drawer - queue panel + offline banner styles
The queue logic existed with no UI. A sort-bar dropdown now lists queued episodes with keyboard-accessible up/down/remove/clear and a count badge; pure moveInQueue() added to state. Offline banner shows while the network is down. player.ts carries the deferred post-3.1.0 split plan as a note.
…resolve, theme parity Frontend unit tests 51 -> 104; worker 22 -> 26 (redirect guard).
…re docs - all four version fields (root, tauri.conf, Cargo, desktop pkg) -> 3.1.0 - Cargo scaffold metadata replaced (crate seseri, real author/license/repo) - STORE.md: iOS roadmap section, KV-id de-drift, updated checklist
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
…etired main carried the pre-3.0 deployed-site layout (built assets committed at the repo root, pages.yml publishing '.'). The 3.x layout builds from source (pages.yml publishes dist/), so the merge keeps the source tree and drops the stale root artifacts; public/ remains the single origin of manifest/icons/screenshots/404/privacy files.
IACBI
force-pushed
the
redesign/seseri-2.0
branch
from
July 8, 2026 12:44
e7c83bc to
2a86981
Compare
…CI failure The mock stored the original Response, sharing its one-shot body stream across reads; real Cache API semantics copy on put() and mint a fresh Response on match(). Passed on Windows by luck, failed on ubuntu CI.
…ses broke CI jsdom's Blob passed into Node's (undici) Response throws TypeError on the CI Node build, sending downloadOffline into its cors-blocked branch. The suite now stubs Response/fetch/caches with plain fakes so no real fetch classes mix across realms; behavior assertions unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Production-finalization pass on top of the 3.0 rewrite. Everything below is verified by the full `npm run verify` chain (lint + tsc + 104 frontend tests + build + worker tsc + 26 worker tests) plus two headless runtime smokes (mini-player/queue 7/7, offline download/seek 9/9).
Security
Features / UX
Accessibility / i18n
Release engineering
Remaining (credential-blocked, documented)
Windows code signing · Play keystore/assetlinks fingerprint · Apple developer account.
Merging this deploys the web app to https://iacbi.github.io/seseri/ via pages.yml.