527 Commits

Author SHA1 Message Date
KoalaDev b450845522 fix(extension): harden reconnect, ping, and offline-queue flush
Address client-side rate-limit edge cases that could trip the server's
per-socket event budget or per-IP connection limit:

- Pace the offline event-queue flush after (re)connect in batches
  (10 per 3s) instead of one synchronous burst, so a reconnect after a
  long outage no longer dumps the whole backlog and gets kicked.
- Wrap socket.send() in try/catch and re-queue on failure (race with a
  server-side disconnect) instead of losing the event.
- Ping liveness: tolerate one missed PONG; only force a reconnect after
  2 consecutive misses (~20s) instead of a single hard 5s timeout.
- Reconnect backoff: tune to ~8 attempts/60s (under the connection limit)
  and add +/-20% jitter to de-synchronize reconnect herds.

All magic numbers extracted into named constants.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v2.4.4
2026-06-23 00:45:29 +02:00
KoalaDev 27023ea58e feat(server): raise event rate limit to 50 and name rate-limit constants
Raise the per-socket event budget from 30 to 50 per 10s to give legitimate
bursts (episode joins, force-sync, ACK flurries) more headroom. Extract the
previously inline connection/event/health/admin windows and limits into named
constants (CONNECTION_RATE_LIMIT, EVENT_RATE_LIMIT, *_WINDOW_MS). Tests now
derive their thresholds from the exported constants instead of hardcoding them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 00:45:19 +02:00
KoalaDev 1a7d23ce93 ci: add PR/push verification workflow and harden release
- Add .github/workflows/ci.yml running `npm run verify` (lint, tests,
  audits, builds) on every push to main and pull request, so regressions
  can't reach main or a release tag unchecked.
- release.yml: use `npm ci` instead of `npm install` for reproducible builds.
- package.json: add `test` script aliasing the verify suite.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 00:45:10 +02:00
KoalaDev d2c8ca1c1e Merge PR #11: extract tab management logic into independent module
3-way merge of Kaia-Alenia's tab-manager refactor (PR #11) onto current
main (v2.4.3). Only the tab-related service-worker listeners move out of
background.js into extension/modules/tab-manager.js via initTabManager({...})
dependency injection; all other recent main changes are preserved untouched.
Behavior-preserving; listeners still register synchronously at top level.

The unrelated website/assets/NewLogoIcon_64.webp change from the PR was
excluded to keep main clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-20 02:06:38 +02:00
Kaia-Alenia 1c404a7f11 refactor(extension): extract tab management logic into independent module 2026-06-19 14:11:47 -06:00
Timo e2bba04efd docs: update README language count to 15 (add zh, uk)
The localization section still listed 13 languages; Ukrainian (uk) and
Chinese Simplified (zh) were added in v2.4.3.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 16:49:34 +02:00
GitHub Action c7b29c277c chore(release): update versions to v2.4.3 [skip ci] 2026-06-19 14:39:06 +00:00
Timo e06965b95c docs: add v2.4.3 entry to CHANGELOG.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v2.4.3
2026-06-19 16:38:49 +02:00
Timo 23d43741ce chore: replace Ko-Fi with Support links, drop uninstall token
Switch the Ko-Fi support links to https://support.koalastuff.net across the
README badge, the extension footer (popup.html/template.html, label
"Support KoalaSync") and remove the now-unused KOFI_URL constant. Stop
generating/sending the anonymous uninstall token: initUninstallURL() now
registers the feedback URL without the `t` parameter or storage write.
CHANGELOG updated to match.

Also guard the client CMD_ACK path: only ACK a command sender that is still
a known peer in the room, so we don't emit ACKs to peers that already left
(which the server now drops as absent-peer ACKs anyway).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 16:26:54 +02:00
Timo 027d2dbde5 fix(sync): coalesce play/pause bursts and quiet absent-peer ACK logs
Media players fire bursts of native play/pause events (source swaps, ABR,
ads, teardown). Each was relayed as a distinct command, spamming peers and
tripping the relay's event rate limit. Add leading+trailing coalescing in
the content script: emit the first event instantly (zero added latency) and,
on a 150ms window, collapse a burst to its final state. Echo-suppression and
seek-flush stay synchronous on event arrival; the trailing send is never
deduped against the leading one (a remote command may change shared state
mid-window — re-sending is the safe, idempotent choice).

Server: split the EVENT_ACK handler's logging so only a genuine different-room
ACK is logged as [SECURITY]; an ACK to a peer that already left is logged
quietly as [ACKDROP] (verbose-only), so real signals are no longer drowned out.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 16:26:31 +02:00
Timo a0c16df664 feat(i18n): add Ukrainian & Chinese, fix zh/uk translation quality
Add two new languages (uk, zh / zh_CN) across the extension and website:
register them in SUPPORTED_LANGUAGES, the Chrome _locales map, the website
build/lang-init/app routing, hreflang/og/schema tags and language selectors.

Fix systematic machine-translation word-sense errors in the new zh/uk
locales (e.g. zh "Status"=地位→状态, "Leave Room"=留出空间→离开房间,
"Play"=玩→播放, "Seek"=寻找→跳转, audio Attack/Release/Knee; uk "Play"=Грати
→Відтворити, "Clear"=ЯСНО→Очистити, "Open"=ВІДЧИНЕНО→Відкрити, peers
"Однолітки"→"Учасники"), translate remaining English leftovers (Room ID,
Custom) and normalise terminology. All 15 locales pass test-locales.cjs.

Note: popup.html and website/template.html also carry the language-selector
additions here; their Ko-Fi→Support label text is part of the branding change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 16:26:20 +02:00
KoalaDev 8fd866f20a feat(website): add canonicals, hreflangs, meta descriptions, and localized schema descriptions 2026-06-19 12:10:08 +02:00
KoalaDev 8f72c238a2 docs: add v2.4.2 entry to CHANGELOG.md 2026-06-19 11:58:06 +02:00
GitHub Action 24ad7c3732 chore(release): update versions to v2.4.2 [skip ci] 2026-06-19 09:54:42 +00:00
KoalaDev 32fd677a31 feat: optimize extension uninstall URL registration with anonymous token and startup hardening v2.4.2 2026-06-19 11:54:07 +02:00
KoalaDev fb22700268 docs: revert completed milestones in ROADMAP.md 2026-06-19 11:39:50 +02:00
KoalaDev 59b675ce17 docs: audit and update script references, supported languages, and completed milestones 2026-06-19 11:35:49 +02:00
KoalaDev 8a9293280b chore: harden version substitution in release workflow and fix supply chain security version in README 2026-06-19 11:12:48 +02:00
KoalaDev 450f715874 ci(release): update actions to Node 24 versions & optimize workflow
- Bump all actions off the deprecated Node 20 runtime: checkout v4->v7,
  upload-artifact v4->v7, setup-node added@v6, action-gh-release v1->v3,
  docker build-push v5->v7, login v3->v4, metadata v5->v6, buildx v3->v4
  (verified latest majors via GitHub API; no breaking input/output changes)
- Add npm dependency caching via setup-node (cache: npm)
- Add GHA layer caching for the multi-arch Docker build (cache-from/to gha)
- Add concurrency group so a release run is never interrupted, only dedupes
  re-pushes of the same tag
- Tighten release-server permissions to contents: read (it never writes to the repo)

Version-injection and commit-back logic unchanged. First exercised on the next v* tag.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 10:59:30 +02:00
GitHub Action b4c00f36a0 chore(release): update versions to v2.4.1 [skip ci] 2026-06-19 08:50:27 +00:00
KoalaDev 36f494fa55 feat(extension): one-click invite button in empty peer list + v2.4.1 changelog
- "No peers yet" state now shows a localized "Copy invite link" button (reuses BTN_COPY_INVITE, present in all 13 locales) so users can share without hunting for the field
- Add v2.4.1 changelog entry covering the onboarding tour improvements, empty-state action, and the mobile landing-page fixes from this session

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
v2.4.1
2026-06-19 10:50:04 +02:00
KoalaDev b5b3885d19 feat(extension): finish onboarding tour with closing step & cleaner welcome
- Add the previously-orphaned 5th onboarding step (ONBOARDING_5) so the tour ends on a closing card instead of stopping at the username field
- Make step 1 a centered welcome card instead of spotlighting the logo; guard querySelector for target-less steps
- Fix stale static placeholders in popup.html (Step 1 of 3 -> 5, progress 33% -> 20%)

Verified ONBOARDING_5/DONE exist across all 13 locales; rendered with the longest (German) copy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 10:40:40 +02:00
KoalaDev 373f2d127a fix(website): mobile comparison cards, hamburger nav, hero symmetry & reveal fallback
- Comparison table: stack into per-feature cards on phones (no more horizontal scroll); bring feature descriptions back
- Re-enable mobile hamburger menu (was hidden via display:none !important); keep logo/language/button uncrowded down to ~320px
- Fix hero asymmetry: extension mockup forced the grid column wider than the container; make it width:100%/max-width:320px and use minmax(0,1fr)
- Add IntersectionObserver guards + <noscript> fallback so reveal-animated content never stays hidden

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 10:27:07 +02:00
KoalaDev 3ebe80aab2 refactor: replace dynamic GitHub API badges with static shields.io badges 2026-06-17 06:39:42 +02:00
Timo e7b59d61ac docs: update all docs for lazy-connect, refactor, and .cjs rename
- ARCHITECTURE.md: startup -> lazy connect, reconnect when in room
- HOW_IT_WORKS.md: on-demand connection, heartbeat while connected
- PRIVACY.md: clarify alarms only during active sessions
- SYNC_GUIDE.md: build-extension.cjs + episode-utils injection
- StoreDescription.md: on-demand relay, no persistent connection
- README.md + extension/README.md: npm run build:extension
- extension/README.md: replace Dual Heartbeat with On-Demand Connection
- website/privacy.html: add lazy-connect privacy note
2026-06-16 14:17:19 +02:00
GitHub Action 71f7ac425a chore(release): update versions to v2.4.0 [skip ci] 2026-06-16 12:00:03 +00:00
Timo 394b9ba474 Merge branch 'refactor/split-monoliths' v2.4.0 2026-06-16 13:58:32 +02:00
Timo 526736b045 fix(ci): update website build path to .cjs after rename 2026-06-16 13:54:57 +02:00
Timo 86112057b4 test: add names generator unit tests (getAvatarForName + generateUsername)
Tests: 20 deterministic avatar assertions (exact match, case insensitive,
longest-match-wins, ZWJ sequences, fallback), 30 randomized username format
checks, and validation that every noun in the list has an emoji mapping.
2026-06-16 13:53:19 +02:00
Timo c56fcfd281 test(server): add 13 WebSocket integration tests covering all server features
Tests: connect, room create/join, password auth, play/pause/seek relay,
force_sync, event_ack, episode_lobby, peer leave, ping/pong, room list,
protocol version check, deduplication, health HTTP endpoint.
All 13 tests pass as part of npm run verify.
2026-06-16 13:48:21 +02:00
Timo 5c43fc6236 docs: add v2.4.0 changelog entries for lazy-connect and refactor 2026-06-16 13:23:53 +02:00
Timo 101761e984 chore: add type:module, fix npm audit, rename CJS files, document module structure
- package.json: add type: module to silence ESM warnings
- server: npm audit fix (0 vulnerabilities, ws vuln resolved)
- Rename 4 CJS files to .cjs: build-extension, test-content-video-finder,
  test-locales, website/build
- Update eslint.config.mjs for .cjs glob patterns
- extension/README.md + server/README.md: document module structure
2026-06-16 13:22:45 +02:00
Timo 3bc68a5713 test: add unit tests for rate-limiter and episode-utils
- test-rate-limiter.mjs: 12 test groups covering all rate-limit
  functions, Maps, cleanup, and double-start guard
- test-episode-utils.mjs: 30+ assertions covering SxxExx patterns,
  6 separator types (dash/dot/slash/colon/comma/space), German/English,
  edge cases (null/undefined/empty), and sameEpisode matching logic
- verify-release.mjs: added 2 test suites + 1 syntax check to pipeline
2026-06-16 13:15:25 +02:00
Timo 6c96dd6344 chore(refactor): remove orphaned comment, add double-start guard to rate-limiter 2026-06-16 13:10:37 +02:00
Timo d7bb8dc97c refactor(extension): extract episode-utils from background.js + content.js
Move extractEpisodeId() and sameEpisode() to shared episode-utils.js.
background.js imports via ES module. content.js (IIFE) receives injected
copy via build-extension.js marker replacement, eliminating duplication.
2026-06-16 12:48:11 +02:00
Timo d38a840114 refactor(server): extract rate-limiter module from index.js
Move 6 rate-limit functions, all rate-limit Maps, cleanup intervals,
and denial counter to server/rate-limiter.js. 149 lines extracted.
Index.js re-exports what tests and ops.js need.
npm run verify passes.
2026-06-16 12:44:43 +02:00
Timo e7d2d76ba3 fix(eslint): add missing browser globals history and location 2026-06-16 10:50:40 +02:00
Timo 5277cd0f21 fix(eslint): add missing browser globals history and location 2026-06-16 10:50:32 +02:00
Timo 4d7cbfe347 Merge branch 'feature/lazy-connect' 2026-06-16 10:47:46 +02:00
Timo 91aa76e842 fix: revert premature isConnecting reset + remove popup disconnected flicker
- background.js: remove isConnecting=false at line 608 (was set before
  WebSocket handshake completed, defeating the guard for the entire
  CONNECTING phase). isConnecting is already properly reset in onclose,
  onopen/40 handler, catch block, and socket guard path.
- background.js: restore isConnecting=false in forceDisconnect() so
  subsequent connect() calls can proceed after intentional disconnect.
- popup.js: remove hardcoded applyConnectionStatus('disconnected') that
  caused a guaranteed red flicker on every popup open. Status is now
  set only by the async GET_STATUS response.
2026-06-16 10:44:57 +02:00
Timo 5f0a189451 fix(popup): trigger CONNECT when popup opens with saved roomId
If user has a room configured but the background is not connected
(e.g. service worker was killed, startup race, or stale state), the
popup now sends CONNECT on open. Previously it only called GET_STATUS
and showed 'disconnected' with 0 logs — the user had no way to know
whether the extension was working.
2026-06-16 10:35:13 +02:00
KoalaDev 312d4f2cf7 chore: update sitemap lastmod dates, automate in release CI
- Update all sitemap <lastmod> entries from 2026-06-09 to 2026-06-16.
- Add sed step in release workflow to auto-update sitemap dates on every
  tagged release, so the sitemap never goes stale.
2026-06-16 06:11:13 +02:00
KoalaDev 884feb982d fix(website): smooth anchor scrolling, language-switch scroll position, lang select UX
- Replace JS scrollIntoView handler with CSS scroll-behavior:smooth for
  native anchor navigation. Fixes #hash-based scroll position surviving
  language switches since the hash stays in the URL.
- Auto-update URL hash via IntersectionObserver as user scrolls through
  sections, so manual scrolling also sets the anchor for language switch.
- Preserve window.location.hash across all language-switch navigations.
- Fix Chrome double-arrow on language select by limiting appearance:base-select
  to ::picker(select) only, keeping the custom SVG chevron.
- Make chevron absolutely positioned over the select with pointer-events:none
  so clicks anywhere on the container open the dropdown.
- Remove max-width constraint on language select so longer names fit.
- Add scroll-margin-top to section[id] and header[id] so anchored sections
  clear the fixed navbar (100px offset).
- Add header[id] to scroll-margin-top for keyboard skip-link accessibility.

Thanks to Taradal from Reddit
2026-06-16 06:05:49 +02:00
KoalaDev b2da17ab62 fix(lazy-connect): harden connection lifecycle against race conditions and edge cases
- Prevent concurrent connect() by removing isConnecting reset from
  forceDisconnect(); the guard in connect() now reliably blocks
  re-entry since no external caller can defeat it.
- Reset isConnecting at end of connect() so subsequent calls can
  proceed without waiting for the async '40' handler.
- Clear connectIntent and cancel reconnect timer on server ERROR
  when no currentRoom exists, preventing infinite reconnect loops
  after failed join attempts (e.g. wrong password via invite link).
- Move connectIntent assignment after !roomId guard in
  WEB_JOIN_REQUEST to avoid clobbering existing intent on invalid
  input.
- Broadcast JOIN_STATUS failure to popup and bridge tabs when
  WEB_JOIN_REQUEST receives an invalid room ID, so the website
  join page receives feedback instead of hanging forever.
- Clear joinBtnTimeout on CONNECTION_STATUS connected/disconnected
  to avoid stale timeout error messages in the popup.
2026-06-16 05:39:17 +02:00
KoalaDev b518685e2c merge: bring main (v2.3.2) changes into lazy-connect 2026-06-16 04:59:13 +02:00
GitHub Action b7a7a14a35 chore(release): update versions to v2.3.2 [skip ci] 2026-06-16 02:29:41 +00:00
KoalaDev 9ee22d985f docs: remove transient _locales fix from changelog (never released) v2.3.2 2026-06-16 04:28:30 +02:00
KoalaDev 2070e701de docs: add v2.3.2 changelog entry (translations, locale fixes, timing leak) 2026-06-16 04:26:07 +02:00
KoalaDev dd8eefe3f9 fix(security): actually prevent timing leak — eagerly evaluate timingSafeEqual
The previous fix had a subtle bug: JavaScript && short-circuits,
so crypto.timingSafeEqual() was skipped when buffer lengths differed
(sameLength was false). The dummy buffer was allocated but never
compared, leaving the original length-based timing leak intact.

Now timingSafeEqual is eagerly assigned to a const before the &&
guard, guaranteeing it runs in constant time on every auth attempt
regardless of whether the length guess was correct.
2026-06-16 04:23:20 +02:00
KoalaDev cc97e0d371 fix(security): prevent admin token length leak via timing side-channel
isAdminMetricsAuthorized() returned early when buffer lengths differed,
allowing an attacker to discover the token length by measuring response
time. Now always calls crypto.timingSafeEqual (using a zeroed dummy
buffer on length mismatch) so every auth attempt takes constant time
regardless of whether the length guess was correct.

Reported-by: Kaia-Alenia
2026-06-16 04:21:15 +02:00