The tab is still internally named data-tab="tab-dev". It used to be the
dev/diagnostics tab and was deliberately de-emphasized with font-size:
11px and a dimmer color. It was later relabelled to the user-facing
"Status" tab, but the rule stayed, leaving it 3px smaller and paler than
Room/Sync/Settings.
Drop tab-dev from the selector. #devToolsTabBtn, the opt-in Dev tab that
shares the rule, stays de-emphasized on purpose.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Peer usernames are remote-controlled and were interpolated into an
innerHTML string in updateLobbyUI. The server only truncates them to 30
chars, so a peer could inject markup into everyone else's popup: enough
to load a remote image (leaking viewer IPs) or spoof readiness badges.
Inline handlers were already blocked by the MV3 default CSP.
Build the peer items with the DOM API, matching the pattern the sibling
peer list already uses.
Add the two checks that would have caught this before upload:
- eslint no-unsanitized, which reproduces the AMO warning at lint time
- addons-linter on the built XPI in verify-release, with
--warnings-as-errors since it exits 0 on warnings and AMO rejects them
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Feedback round (calmer, less "template/AI"):
- Dial back animated background motion globally (sway/leaf-drift halved,
fewer falling leaves, gentler godrays/fireflies/light-sweep).
- Fire leaf confetti on click of real controls only (was pointerdown
everywhere, which showered mobile users on every scroll).
- Remove hover lift/scale from non-interactive cards, screenshots and
mascots so they no longer read as clickable.
- Fix mobile nav menu colours in the light theme (was a hard-coded dark
panel with low-contrast links).
- Calm how-it-works steps 1-2 (no button pulse/shine/cursor bob); step 3
keeps its lively sync animation. Remove the now-static "copied" toast
from step 1 since it only made sense mid-animation.
UX audit follow-ups:
- Unify vertical rhythm with one section-padding scale; vertically centre
the hero so space is balanced instead of pooling under the CTAs.
- Split the mixed "Why KoalaSync?" grid into use-case scenarios and a
labelled "Features" subsection, and move the #features nav anchor there.
- Demote the hero GitHub CTA to a quiet text link (Chrome > Firefox >
GitHub hierarchy); scale down oversized section mascots and give them a
shared soft shadow.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- thin out bamboo/leaf/firefly decor in tiers (<=1440 / <=1024 / <=768px)
so nothing moves behind the text column and copy stays readable
- center the single-column hero below 1024px (text, koala, CTAs, trust
line) and let the odd CTA button span the full grid row
- remove the periodic wind gusts; keep the constant gentle bamboo sway
- reword the German demo CTA to "Live-Demo ansehen"
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Flag font: ship a committed fontTools subset (43 KB, -45%) of
TwemojiCountryFlags.woff2 with only the 15 flags the site uses, under a
content-hashed name with a preload on every flag-bearing page. The build
validates the subset against a manifest and fails when a new flag
appears without regenerating (npm run subset-flags). JS subsetters
(subset-font/harfbuzzjs) were rejected: their wasm builds silently drop
the COLR/CPAL color tables, rendering all flags invisible.
- Responsive mascots: legal/join/404 pages served 434-500px sources for
175-180px slots; the build now emits 180w/360w variants (AVIF 1x: ~7 KB
vs ~22 KB) and the pages use srcset. Small logo spots (14-42px) now use
the existing 64/128px variants instead of the 256px file.
- injectAvifPictures copies the img sizes attribute onto the AVIF
<source>; without it Chrome mis-selects w-descriptor candidates and can
drop the image entirely.
- AVIF conversion is cached by content hash (.avif-cache.json); the old
mtime check never hit because copyDirSync refreshes mtimes, so every
verify re-encoded all 26 files.
- HTML output is minified (comments + indentation, <pre> preserved,
newlines kept for inline-script safety): index.html 121 KB -> 93 KB.
- unicode-range trimmed to U+1F1E6-1F1FF; tag-sequence flags fall through
to the system emoji font.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test still required 4 foreground film birds, but commit 16cb138
deliberately removed them all; now it guards against remnants instead.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Strip every bird from the demo mockups: both hero video tabs and both
step-3 player scenes lose the background flock (.film-birds) and the
foreground flapping bird (.film-hero-bird), plus all related CSS in
styles/demo.css and the dead style.legacy.css.
Language selector: the <select> was 30px narrower than its pill, so in
appearance:base-select the click/focus highlight covered only the inner
~110px ("half the box"). Move the padding from the container onto the
control so it fills the pill edge-to-edge; mirror the fix in the mobile
legal.css override.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Bamboo layers get their own light-mode recipes (fresh greens, visible
segment rings, sheen) instead of dark-theme gradients faded to 45%
- Language selector pill: frosted white glass, light dropdown/picker
(was hard-coded dark glass in both themes)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Scrolling down now moves all bamboo layers up-screen with the content
(near fastest, far slowest) instead of pushing them down against it.
Vertical overscan flipped from top (-70%) to bottom (-40%) so the
upward drift never exposes a gap at the viewport bottom.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The drift clamp saturated after ~2600px, so nothing moved on long
pages. The vertical drift now maps scroll progress (0..1 over the full
document) onto the stalks' overscan, so the parallax runs end to end.
Turn the background into a descent through the forest: god-ray light
shafts and the canopy fade out while a dusk tint, thicker mist, the
terracotta horizon glow and the fireflies fade in as you scroll down.
Fireflies additionally wander organically (independent translate
property composing with their scale pulse), and on hover-capable
devices the bamboo layers tilt subtly toward the cursor. All layers
animate transform/opacity only, rAF-throttled, reduced-motion safe.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The stalks used border-radius:50%, which renders a tall ellipse whose
ends taper to points, and unbounded parallax drift pushed them off the
bottom of the viewport on long pages. Rounded caps + 20% overscan +
a viewport-relative drift clamp keep them full-height while scrolling.
Sweep the remaining off-palette colors into the nature palette:
content-script overlays (slate grays), amber warnings/badges/star
accents (now terracotta) in popup, background badge and website
illustrations, and the support-heart red (now --danger).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v3.0.0 "The Greens Update" palette: forest-green oklch color system
(accent green + terracotta secondary) replaces the indigo/slate scheme
across the landing page and the extension popup/options. The blob
background becomes an animated bamboo forest overlay (parallax stalks,
falling leaves, canopy, ground mist, fireflies, light sweep, grain)
with prefers-reduced-motion support. Layout, structure and copy are
unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The combined section's third feature tile was Privacy, which is already
covered by the hero trust line and the comparison table. Replace it with
Volume Leveling / Audio Compressor, a genuinely differentiating feature not
shown anywhere else on the page. Shorten its description to match the other
tiles (all 15 locales). The three tiles are now: 1-click invites, volume
leveling, universal HTML5 support.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Trim the long three-sentence blurb to a tight two-liner (platform list plus
the "only sync signals, never the video" reassurance) so it matches the other
tiles in the combined section. Applied to all 15 locales.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The headline column in the old narrow two-column range (769-1023px) was too
tight to fit the full "Add to X" label plus two more buttons, so GitHub wrapped
to a second row. The hero is now single-column below 1024px (headline gets the
full width, demo stacks under it, matching the demo's own breakpoint) and
two-column at >=1024px where the column is wide enough. GitHub button label
shortened to "GitHub" for extra margin. Verified one row at 769/900/1024/1280
and the mobile stack at 375px.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Remove the confusing, space-hungry "Episode Lobby" card from the mockup
- Give the decorative demo mockup a contrast-safe palette: accent shown as
text uses a lighter indigo, and buttons with white text on a colour get
darker shades, so every walkthrough state meets WCAG AA
- Drop the scroll-reveal fade from the above-the-fold hero demo; the fade
briefly rendered its text at partial opacity, which axe caught as failing
contrast on ~1 in 3 audits
- Mark the demo as decorative for assistive tech (aria-hidden + removed from
tab order); mouse interaction is unchanged
- Darken the footer support pill so white text meets contrast
Desktop Lighthouse: 100/100/100/100 (stable across runs). Mobile: 100
a11y / best-practices / SEO; performance ~87 (throttled LCP, separate work).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Hero CTAs: the install buttons are compact (just the browser name) so all
three fit one line; JS detects the visitor's browser and expands that one
to the full "Add to X" call to action. Unknown browsers stay compact.
- Hero demo: caption now sits directly under the two windows (absolutely
positioned to the card cascade) instead of far below the tall scene. The
scene keeps a fixed height that already fits the open popup, so the
vertically centered hero never jumps when the popup toggles.
- The background window now also shows a (non-interactive) extension icon,
making it clear both browsers have KoalaSync installed.
- Escape now also closes the demo popup.
- Added a link from the comparison table to the fuller alternatives/guides
hub, localized in all 15 languages.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Outside-click handling was scoped to the demo scene, so clicks elsewhere
on the page left the popup open and it felt unclosable. A document-level
listener now collapses it from anywhere, like a real extension popup,
and the popup header gained an explicit close button next to the version
link (hidden in the static mobile mockup).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reserving the full 600px open-popup height pushed the H1 down on short
viewports. The scene now stays at 500px (below the left column height,
so it never drives the grid row) and the extension popup is anchored to
the scene bottom, growing upwards on open. Layout stays pixel-stable
when the walkthrough toggles the popup.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scene animated its own height when the walkthrough toggled the
extension popup; with the vertically centered hero grid this re-centered
the left column and shifted everything below on mid-size screens. The
scene now always reserves the 600px open state, so toggling the popup
never reflows the page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New hero headline "Turn any video into a watch party" with animated
gradient highlight, sharper subtitle and quiet trust line under the CTAs
- Changelog badge redesigned as the koala's speech bubble (version and
release date baked in at build time, no version.json fetch anymore)
- Light/dark theme toggle in the header (dark default, pre-paint via
lang-init, full light theme across sections)
- Use cases and "Why KoalaSync" merged into one section with the three
strongest feature tiles; question-mark koala moved to the FAQ
- FAQ and self-hosting setup collapsed by default via shared
section-collapse component (hash deep links auto-open them)
- Compat/juggler section more compact
- og:image switched to the 1280x640 repository OpenGraph card (landing
and join page), generated during build
- "watch party" keyword added to meta title/description in all 15 locales
- All em-dashes removed from user-facing copy
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Change .hero from align-items:center to flex-start so content starts from top
- Increase padding-top in short-viewport media query (<=920px height) to
clamp(7.5rem, 10vh, 9rem) from clamp(3.75rem, 6vh, 5.5rem) so it always
clears the ~96px nav bar
- Fix pre-existing lint errors: remove unused variables demoFinishedByStory,
firstTake; add website/www to eslint ignores
Remove firstTake && !demoFinishedByStory guard from takeOver handler
so clicks outside the extension mockup always collapse the popup,
regardless of whether the automated walkthrough finished.
- Remove data-reveal from hero h1/subtitle/mascot/cta-group/version-badge
(critical above-fold content must never start hidden)
- Change IntersectionObserver rootMargin -150px to -30px, threshold 0.1 to 0.05
(CTAs at ~530px were past the observer zone on laptops)
- Hero layout: align-items flex-start + padding-top 7.5rem for content at top
- Cards 62cqw to 70cqw, -3% offsets for dramatic overlap cascade
- Film animation: replace stick-figure walker with cinematic night scene
(7 twinkling stars, glowing moon with drift, parallax mountains, firefly)
- Seek now restarts all film child animations via .demo-reset class toggle
(was incorrectly resetting parent only — no visible jump)
- Add .demo-seek-sweep overlay + brightness flash during seek
- Video select: placeholder option ('Choose a video tab...') until story picks
- RATE 8 to 1 for realtime playback
- Version badge moved below download buttons
- All 15 locale files: add DEMO_SELECT_PLACEHOLDER key
The v2.5.2 Disney+ page-API integration leaked blob-relative <video>
time into force sync, seeks, and heartbeats when the page-API bridge
had no fresh data, so force sync on Disney+ appeared broken.
- getSyncCurrentTime/getSyncDuration now refuse native values on Disney+
(return null/0) so stale bridge data degrades to a clean no-op instead
of broadcasting garbage to peers. The get_current_time handler and
episode/lobby/hcmIsLive paths are routed through the same accessor.
- Validate FORCE_SYNC_PREPARE/SEEK payloads as finite before relaying;
the internal coercion no longer treats '' as 0.
- Stop double-routing FORCE_SYNC_PREPARE from the popup path (the
generic popup route now covers only play/pause/seek).
- popup force-sync: exclude null/empty peer times from the jump-to-others
median (Number(null)===0 was dragging the target to 0), guard against
NaN end-to-end, clear the dangling reset timer on failure, and retry
without re-injecting when the content script responds but the Disney+
bridge has not yet delivered a finite time.
- hcmIsLive skips the native-duration live signal on Disney+ only,
preserving YouTube/Twitch Infinity-duration live detection.
Disney-specific logic remains strictly gated to disneyplus.com; no
Netflix/YouTube/Twitch/generic path is affected.
Disney+'s <video> is blob-relative (unusable as an absolute clock) and its
scrubber aria-value freezes during playback, so DOM scraping lagged and the
+/-10s button seek could neither reach far targets nor land precisely. The
real player hangs off the <disney-web-player> custom element as
`.mediaPlayer`, exposing seek(ms) and timeline.info (playhead/duration ms).
Since the isolated content world can't read that page object, route it
through the existing MAIN-world page-API bridge (as Netflix already does):
- page-api-seek-overrides.js: register a 'disney' provider.
- background.js installPageApiSeekBridge: seek Disney via mediaPlayer.seek(),
and post the exact playhead/duration (seconds) to the content world every
250ms. Both are gated on provider === 'disney'; Netflix path unchanged.
- content.js: cache the pushed playhead, prefer it in getDisneyPlusTimeline
(DOM scraping stays as fallback), and check the page-API seek first in
seekVideo. Outcome is identical for Netflix and generic sites.
Verified live on Disney+: reported time matches the player exactly and
seek lands within ~1s of the target.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Disney+ has no absolute seek on its blob <video>, so absolute seeks are
translated into repeated clicks of the player's relative skip button. The
click count was capped at 12 (=120s at 10s/step), so any seek further than
two minutes away silently landed short — breaking absolute-position sync
(e.g. syncing to 5:00 from far away never arrived).
Derive the step size from the matched button's own label (10s default,
some UIs use 5/15/30) and issue enough clicks to cover the full delta,
bounded at a sane maximum.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When the control overlay (and its live time readout) is briefly absent,
the old fallback recomputed the timeline from the raw blob-relative
video.currentTime via a cached scale/start. Disney recreates the <video>
element on some play/pause transitions, which resets currentTime, so that
math produced a wildly wrong time ("loses the time" after a pause).
The native clock advances 1:1 with real playback, so instead anchor the
last live UI position to video.currentTime and extrapolate by the elapsed
native delta. If the delta is negative or implausibly large (element
recreated / seek), freeze at the last known position rather than emitting
a garbage time.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Disney+ throttles the scrubber slider's aria-valuenow, so it can lag real
playback by several seconds (and its value snaps back when the control
overlay reappears on pause). The player's "time remaining" indicator,
however, updates live.
getDisneyPlusUiTimeline() now captures any element whose class/testid
mentions "remain", parses its clock text, and — when a reliable duration
is known — derives current = duration - remaining and prefers it over the
laggy slider value. Falls back to the previous behavior when no remaining
indicator is present.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clean up the exploratory Disney+ diagnostics now that the Shadow DOM
timeline/seek integration is in place:
- Remove Proposal 1 (Media Session interceptor): the background.js
interceptor injection and the content.js message listener capturing
__koalaMediaSessionCapture, plus the now-unused mediaSessionPosition
reporting in content.js and popup.js.
- Remove Proposal 3 (Video Event Log): videoEventsLog, logVideoEvent(),
the per-video event logging listeners, and the popup.html/popup.js UI.
- Keep Proposal 2 (DOM timestamp scraper) for ongoing diagnostics.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
## Added
- **WebSocket Protocol Specification** (docs/PROTOCOL.md) - Complete reference
for all 20+ events with payload schemas, rate limits, and edge cases
- **LEAVE_ROOM Rate Limiting** - 10 requests/socket/minute to prevent abuse
- **Vitest Testing Framework** - Modern test setup with coverage reporting
- **Host Control Mode Documentation** - Consolidated EN documentation
(docs/host-control-mode.md) with references to internal implementation
## Changed
- **Graceful Shutdown** - Socket.IO clients properly disconnected during
server shutdown (server/index.js)
- **CI Workflow** - Added test step with continue-on-error for gradual rollout
- **README** - Added protocol documentation link
- **CHANGELOG** - Updated with all new features and improvements
## Moved
- Internal documentation moved to docs/internal/ for better organization
- Host Control Mode docs consolidated from 4 files to 1 comprehensive guide
## Technical Details
- Protocol spec: 495 lines, covers all events from shared/constants.js
- Rate limiter: Follows existing pattern (checkAuthRate, checkEventRate)
- Vitest: 6 tests for rate limiter, all passing
- Documentation: Host Control Mode doc includes edge cases, testing checklist,
architecture decisions
- Remove duplicate local TITLE_PRIVACY_MODES array definition
- Use Object.values(TITLE_PRIVACY_MODES).includes() for validation instead
- Keeps single source of truth in title-privacy.js
Co-Authored-By: Claude <noreply@anthropic.com>
- Manual Connect: split the server picker into its own inset panel,
visually separated from the Room ID / Password fields (they were
welded together in one flat list).
- Custom server URL: stop auto-prepending ws:// — default to wss://
for real hosts, ws:// only for localhost/127.0.0.1 (matches what
the background already enforces; the displayed URL no longer lies).
- Host Control card: replace the raw native checkbox with the proper
.toggle-switch slider used in Options; move the toggle row from a
<label> to a <div> to avoid an illegally nested label.
- Co-host discoverability: owner-only hint in the Host Control card
pointing to the participant list (new NOTICE_COHOST_HINT across all
15 locales). Shown only when actionable: owner + co-host-capable
relay + host-only active.
- Peer row: group right-side badges/actions into one container so they
no longer scatter via space-between or collide with long names;
restyle the give/revoke button (padding, weight, hover fill, tooltip).
- Gate promote/demote (and role badges) on host-only mode being ON,
in addition to the existing owner-privilege check — roles are moot
in 'everyone' mode, so no badge/button noise there.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- server: SET_PEER_ROLE now uses the same per-room 500ms debounce as
SET_CONTROL_MODE (M-4), preventing a buggy/malicious host from
thrashing the room's UI with rapid promote/demote bursts.
- server: PEER_STATUS relay treats explicit null as 'clear' for
tabTitle/mediaTitle/currentTime, so the tab-close heartbeat actually
zeroes out stale state on other peers instead of being silently
preserved by the clamp fallback.
- background: _persistLastSeq is now trailing-debounced (500ms),
cutting storage.session IPC writes from one-per-relayed-event to
one-per-quiet-window in active rooms.
- locales: add missing TOAST_ID_REGENERATED to all 14 non-English
locales (was blocking locale coverage test on main).
- test: H-1 force-sync demote test now waits out the debounce window
to reflect real-world UI timing (a host cannot promote, run a
force-sync, and demote inside 500ms).
Two pre-existing issues surfaced during audit follow-up:
1. popup.js bound elements.regenId but never attached a click handler —
the 'Regenerate Peer ID' button has been dead UI since it was added
(verified across full git history). Wired it up to send REGENERATE_ID;
the background handler already did the right thing (rotate peerId,
persist, force reconnect). The button is functional (regen ID on
duplicate-identity errors), not privacy-theater — username is already
user-changeable in settings, so it stays out of scope.
2. MAX_CONTROLLERS=10 was arbitrary. Payload math (25 controllers ×
16-char IDs ≈ 500 bytes) is well under the 4KB maxHttpBufferSize,
controllers.has() is O(1) on a Set, and 'just use everyone mode' is
not a real answer when a host wants 12 controllers + 3 guests in a
15-person room. Removed the cap entirely; the only real upper bound
is MAX_PEERS_PER_ROOM (25).
Added TOAST_ID_REGENERATED to en.json — the i18n fallback merge
(Object.assign({}, enDict, targetDict)) covers the other 14 locales
automatically with English until they're translated.
Defense-in-depth: the field was always undefined in practice (no code
path ever sets currentRoom.password — verified across the full git
history), and no GET_STATUS caller in the codebase reads it. But if a
future change ever populated currentRoom.password, any caller of
GET_STATUS would receive it in cleartext.
Threat surface today is narrow (externally_connectable is not set, no
content script sends GET_STATUS, only the popup uses it), but the field
has no reason to exist — removing it eliminates the future-leak vector
for free.
Backward compatible: no caller reads this field, so removing it is a no-op
for every existing code path.
8 hex chars (32-bit) gives ~7% collision probability at the relay's
25k-peer capacity and ~1% at 10k peers. A same-room collision silently
triggers the dedup path and kicks the older session with a confusing
'Another session with this ID joined' error.
16 hex chars (64-bit) drops collision probability to ~1e-10 even at one
million peers. The server's JOIN_ROOM sanitizer already clamps peerId to
16 chars, so no server change is needed. Fully backward compatible:
- Existing clients keep their persisted 8-char peerId (only newly
generated IDs change).
- Mixed 8-char + 16-char ID rooms work — both fit the server's 16-char
clamp.
- No protocol change on the wire.
Birthday-paradox reference (computed, not estimated):
32-bit @ 10k peers: 1.16%
32-bit @ 25k peers: 7.02%
64-bit @ 25k peers: ~0.017e-9%
Three drop-in lengths for product listings, launch pages, and directory
submissions where the same pitch gets re-typed repeatedly. Bonus taglines
for hero headlines and social bios. All claims verified against the
codebase and docs/TESTED_SERVICES.md.
Audit fixes (each verified against the actual code path):
H-1 (server): track force-sync initiator on PREPARE; let the demoted
initiator's EXECUTE through the host-only gate so mid-sync demotion no
longer strands the whole room paused. Clear on EXECUTE/peer-leave.
M-1 (background): episode-lobby gate now uses !amController() for parity
with CONTENT_EVENT and server gates — co-hosts can drive the room and
initiate lobbies, not just the owner.
M-2/M-3 (popup/content/background): forceSyncReset respects hcmGuestLocked;
desynced guest skips EPISODE_LOBBY so they don't get frozen in pause after
lobby completion, and checkEpisodeLobbyCompletion excludes desynced peers
from the required count so they don't block the lobby.
M-4 (background): getHostSyncTarget clamps extrapolation to 2x heartbeat
interval so a stale host heartbeat can't snap the guest tens of seconds
past the host's real position.
L-1..L-4 (server/content/background/popup): clarify dedup comment re:
network-blip window, enforce desync invariant on SW-restore, add
forceSyncBtn guest-locked backstop, refresh badge text in place.
Backward compatibility (verified by BC-1..BC-4 regression tests):
- Old client ↔ new server: server adds fields only, never requires; old
heartbeats stripped of desynced; host-only enforced server-side even
when the client has no awareness.
- New client ↔ old server: empty capabilities → host-control UI hidden,
all gates default to everyone, behavior byte-identical to pre-HCM.
- Mixed rooms: every pre-HCM event type relays cleanly in both directions.
- updateHostControlUI takes a state object now; role badge shows Host / Controller
/ Guest; remote-control lock uses amController (a co-host can drive, so their
buttons aren't locked).
- Peer list: Host/Controller badges and, for the owner, a per-peer Give control /
Revoke button (SET_PEER_ROLE). Shown only when the feature is active and the room
is host-only or the viewer is the owner — no role noise for guests in normal rooms.
Hidden entirely against a relay without the co-host capability.
- i18n: BADGE_CONTROLLER, BTN_GIVE_CONTROL, BTN_REVOKE_CONTROL across all 15 locales.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- background: track controllers[] from ROOM_DATA/CONTROL_MODE (restore + reset on
teardown); add amController() (owner or co-host). Both gates now key on controller
membership (sender: !amController(); receiver: senderId not in controllers); desync
invariant uses amController. Thread controllers/amController/coHostSupported through
GET_STATUS/GET_CONTROL_MODE/CONTROL_MODE. New SET_PEER_ROLE message (owner→server).
- content: gate on amController instead of amHost (a controller is not a gated guest);
reconcile/HOST_BLOCKED updated accordingly. Drop now-unused hcmAmHost.
Single-host behavior unchanged (owner is always the sole controller until they promote).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Generalizes host-only from a single host to a controller SET. The owner (creator)
can promote/demote peers; in host-only mode the relay gate allows any controller,
not just the owner. Single-host is now the special case controllers={owner}, so
existing behavior/tests are unchanged.
- shared: CAPABILITIES.CO_HOST enabled; new SET_PEER_ROLE event; CONTROL_MODE
payload carries controllers.
- server: room.controllers Set (always incl. owner), advertised in ROOM_DATA;
gate checks controllers.has(peerId); SET_PEER_ROLE handler (owner-only, target
must be present, MAX_CONTROLLERS=10 cap, re-sync on reject); controlModePayload
helper for consistent emits; owner-leave resets controllers + falls back to
everyone, co-host-leave broadcasts the updated list (dedup/reconnect guarded).
- WS test: promote→drive, plain guest still gated, non-owner can't promote,
demote→re-gated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Move KNOWN_LIMITATIONS.md → docs/KNOWN_LIMITATIONS.md (+ fix SECURITY.md link).
- WS test: in default 'everyone' mode a non-host guest can still drive
play/pause/seek/force-sync/episode-lobby — proves host-control OFF == unchanged
behavior.
- docs/host-control-mode-COHOST-PLAN.md: plan for multi-controller (owner grants
drive rights to several co-hosts), built on the capabilities hook; covers the
gate generalization, roles, backwards-compat, edge cases, and a separate
large-room (510-peer) scaling track.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Records the accepted, out-of-threat-model decisions that reviewers/agents keep
re-reporting as bugs: unauthenticated peerId (host impersonation), non-constant-
time room-password compare, public server token, enumerable room IDs, and
pause-revert-not-prevent. Spells out the threat model (ephemeral, account-less
rooms of invited participants watching a video) so the "NOFIX" rationale is clear,
and distinguishes these from genuinely-deferred items (EC-10 host grace, classifier
tuning). Cross-referenced from SECURITY.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Room-password hashing falls back to a built-in salt that is public in the repo
when SERVER_SALT is not configured. Low risk (the hash never leaves the server),
but operators should set a unique salt — warn at startup, mirroring the existing
ADMIN_METRICS_TOKEN warning.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Findings recovered from the multi-agent audit (verification phase was cut off by a
usage limit; verified inline against the code):
HIGH:
- Host was demoted on peerId dedup (fast reconnect / second tab): the dedup path
removes the old socket and the kicked socket's 'disconnect' both ran the
host-leave fallback before the same peerId re-joined → room silently unlocked on
every host network blip. Now skip the fallback while a join for that peerId is in
flight (peerJoinLocks). Regression test added. (A long real disconnect still
falls back — that's the deferred host-grace EC-10.)
- Episode auto-advance froze a gated guest: in host-only the guest's EPISODE_LOBBY
is dropped server-side, but the guest still self-paused (PAUSE_FOR_LOBBY) waiting
for readies that never came → 60s freeze. A host-only guest now skips creating a
lobby (the host drives episode sync).
LOW / cleanup:
- GET_HCM_STRINGS could return a raw key name ("HCM_DIALOG_TITLE") if the locale
dictionary failed to load (getMessage returns the key on miss) → omit it so
content keeps its English fallback.
- hcmReset now also clears the snap-back cooldown + buffering grace, so a stale
cooldown can't swallow the first snap-back after a room/host change.
- Popup play/pause labels reset on lock too (not just unlock), so a button can't
freeze on "Playing…" when host-only activates mid-click.
- Fix a stale comment (heartbeat now carries 'desynced').
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Audit follow-up on the snap-back paths. The immediate involuntary snap used the
captured target directly; if it was null/invalid (host position not known yet,
e.g. before the first host heartbeat) hcmSnapBackToHost no-ops and the guest is
left silently paused with no dialog and no retry. The deferred and "Stay in sync"
paths already re-query with retry — make the immediate path fall back to the same
when the captured target isn't usable.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the snap-back robustly instead of deferring it to device testing —
since the set of video sites is unbounded, "measure which players misbehave, then
fix" doesn't scale; the snap-back must be safe on any player.
On an involuntary pause/seek, if the player isn't ready to play (readyState<3 or
seeking) we no longer snap immediately (which fights the buffer: seek → re-buffer
→ pause → … = stutter). Instead hcmDeferredSnapBack polls until the player can
play (8s cap) and then snaps ONCE to the host's re-queried current position. A
player can't play while buffering anyway, so waiting is also more correct, not
just safer. Ready players still snap immediately (no regression).
Guards: aborts if the user goes solo or is no longer a gated guest; single
pending poll (no stacking); cleared in hcmReset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the implicit "hostPeerId present ⇒ feature supported" heuristic with an
explicit capabilities list the relay advertises in ROOM_DATA. Cleaner, self-
documenting, and the extensible hook for the planned co-host feature (owner
promotes guests to extra controllers) — a future 'co-host' capability + events
slot in without a protocol bump.
- shared: CAPABILITIES { HOST_CONTROL }.
- server: SERVER_CAPABILITIES advertised in ROOM_DATA.
- background: track serverCapabilities (empty against older relay), serverSupports(),
thread hostControlSupported through GET_STATUS / GET_CONTROL_MODE / CONTROL_MODE.
- popup: gate the host-control card on the explicit capability instead of hostPeerId.
Backwards-compatible both ways: old relay omits the field → feature stays hidden
(no errors); old client ignores the field. WS test asserts ROOM_DATA advertises
the capability. Adds docs/host-control-mode-TESTING.md (beta-server setup, wss
caveat, two-new-clients note, verification checklist).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a separate workflow that builds the server image and pushes it to GHCR under
non-production tags (beta, branch slug, sha-<commit>, and an optional custom tag
via manual dispatch) so the host-control branch can be deployed to a staging /
backup server and used as a custom server — without ever touching ':latest' that
the official relay tracks (flavor: latest=false).
Runs on push to feature/host-control-mode and on workflow_dispatch. Remove once
the feature is merged and released normally.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The card was rendered in every room with a "Guest" badge by default — confusing
in normal rooms, and outright misleading against a relay that doesn't yet support
host control (older server omits hostPeerId, so amHost is always false and nobody
is ever recognized as host).
Now the card shows only when it's meaningful: the host always sees it (to toggle
host-only), a guest sees it only while host-only is active, and it's hidden in a
normal "everyone" room and whenever the server doesn't advertise hostPeerId
(feature unavailable). Thread hostPeerId through updateHostControlUI to detect
server support.
Note: the feature still requires the relay to run the host-control server code;
this just makes the client degrade cleanly instead of showing a stray "Guest".
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The host-control desync dialog and the "watching on your own" badge were
hardcoded English — content.js has no i18n loader. Background does, so it now
resolves the strings (GET_HCM_STRINGS: loadLocale + getMessage) and content
fetches them on init with English fallbacks, re-rendering the badge if it was
already shown. The dialog body is now generic (dropped the paused/jumped verb)
to keep translation clean across languages.
Adds 6 keys (HCM_DIALOG_TITLE/BODY/STAY/SOLO, HCM_BADGE_SOLO/RESYNC) translated
across all 15 popup locales.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two issues surfaced by a holistic audit of the combined desync-persist +
reconcile-on-init changes:
1. Stale desync outside gated-guest state. hcmDesynced is persisted, but neither
ROOM_DATA nor CONTROL_MODE cleared it when the local peer became host or the
room switched to 'everyone'. Combined with reconcile-on-init, a desynced guest
promoted to host (host-leave fallback) while content was reloading could
re-adopt desynced=true — self-labelling "Solo" to all peers and ignoring host
commands in 'everyone' mode. Enforce the invariant centrally
(hcmEnforceDesyncInvariant: desynced ⟹ host-only && !host) on every role/mode
change, and guard the content reconcile to only adopt when actually a gated guest.
2. Solo-badge flicker. The host-side PEER_STATUS handler set peer.desynced
unconditionally, but the background-driven keepAlive heartbeat omits the field —
so every ~30s it clobbered desynced to false, flickering the badge. Only update
when present (matching the sibling fields), and include desynced in the
background heartbeat so both heartbeat sources are consistent.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Resync badge already retried REQUEST_HOST_SYNC when the host's state wasn't
known yet, but the dialog's "Stay in sync" path did a single no-retry request —
so a stay tap right after the host paused (before a heartbeat propagated) could
silently leave the guest paused/out of sync. Extracted the retry loop into a
shared hcmRequestHostSyncWithRetry() and used it in both paths.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes a split-brain where a guest who chose "watch on my own" and then reloaded
the video page would appear stuck as "Solo" to the host while actually being
back in sync.
Root cause: background persists hcmDesynced (survives SW restart) and attaches
it to content-driven heartbeats, but a freshly injected content script started
with hcmDesynced=false and GET_CONTROL_MODE never returned the desync state — so
content re-applied host commands (synced) while background kept relaying
desynced=true to the host.
Fix: GET_CONTROL_MODE now also returns desynced; content adopts it on init
(re-shows the badge) and also adopts hostPeerId so later host-change resets work.
Desync now survives a page reload consistently on both sides, as intended.
Verified the full path: background.js heartbeat (1555 omits / 2018 attaches),
reset paths, CONTENT_BOOT and keepAlive-disconnect (neither reset it), content
sendHeartbeat on inject.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Original code at L1957 already validates sender.tab tabId against
currentTabId before calling processEvent() — added guard was
duplicate. HCM_DESYNC_STATE had no such guard; added with same
sender.tab && currentTabId pattern (popup-safe via undefined
short-circuit).
hcmDesynced now persisted in session storage:
- Restored in ensureState()
- Set on HCM_DESYNC_STATE change
- Reset to false in all 3 teardown storage.sets
Survives SW restarts; cleared on leave/switch/idle-room
- CONTENT_EVENT now rejected from non-selected tabs (popup exempted via sender.tab check)
- HCM_DESYNC_STATE same guard
- hcmDesynced persisted in session storage: restored on SW restart, cleared on teardown
Bis auf peerId-Auth (akzeptiert) alle Audit-Findings behoben:
- Snap-back spielt nicht mehr blind ab, wenn Host-Status unbekannt
- Teardown-Pfade broadcasten jetzt CONTROL_MODE → kein verwaistes Badge/Dialog
- Dialog-Timer bei Ersetzung sauber gecleart
- Server re-synchronisiert Sender bei nicht-Host-Reject
- Popup-Toggle revertiert bei Server-Ablehnung
- Desync-Zweig sendet keine Phantom-ACKs mehr an Host
- Toggle-Debouncing serverseitig (500ms pro Raum)
- Button-Text auf Unlock zurückgesetzt
- Live-Erkennung defensiver, Resync-Retry bei fehlendem Target, Badge-Retry
Neu: Host sieht 'Solo'-Badge für desyncte Gäste via PEER_STATUS-Heartbeat
(16 Sprachen, neuer i18n-Key BADGE_DESYNCED/TOOLTIP_PEER_DESYNCED)
Tests: H-5-Reject-Unicast, M-4-Debounce, Desync-Heartbeat-Relay
hcmHandleBlocked no longer re-checks local control mode before acting. Background
only sends HOST_BLOCKED to a gated guest, so the message itself is authoritative —
adopt host-only/guest role from it. Fixes the join race where a HOST_BLOCKED
arriving before the CONTROL_MODE broadcast was silently ignored.
Also corrects the EC-4 note: "let catch-up re-sync" is invalid (sync is
event-driven, no continuous catch-up); the right fix is a buffer-aware deferred
snap-back.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- popup: lock remote-control buttons (Play/Pause/SYNC) for guests in host-only
mode so clicks don't silently get gated and leave the button stuck; backstop
guards in the handlers and the 2.5s safety reset respects the lock.
- content: rebuild the desync dialog + resync badge with the DOM API inside a
Shadow DOM. innerHTML inline style="" attributes are stripped by strict
style-src CSP (Netflix/YouTube/Disney+); CSSOM .style is CSP-safe and the
shadow root isolates from page CSS.
- content: detect live-DVR in hcmIsLive() via a sliding seekable window
(seekable.start(0) > 1), not just duration === Infinity (EC-15).
Records remaining audit items (snap-back thrash, join race, dialog i18n) in the
edge-case log for the device-testing pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
All five layers implemented; automated checks green. Records what still needs
real-device testing (EC matrix) and what was deferred by decision.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Final wiring of host control mode — the user-facing UI:
- Active-room section gains a Host Control card: a Host/Guest role badge, a
host-only toggle ("Only I can control playback") shown to the host, and a
guest notice when the room is host-only.
- Toggle sends SET_CONTROL_MODE; UI refreshes from the server's CONTROL_MODE
broadcast (with optimistic revert on failure). Card updates on GET_STATUS,
CONTROL_MODE, and reconnect.
- i18n: 6 new keys (LABEL_HOST_CONTROL[_TOOLTIP], LABEL_HOST_ONLY_TOGGLE,
NOTICE_HOST_CONTROLS, BADGE_HOST, BADGE_GUEST) translated across all 15
popup locales.
Completes the feature end to end (server + background + content + popup).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Guest-side handling when a room is in host-only mode:
- Intent classifier (EC-9): tells a deliberate guest pause/seek from an
involuntary one (buffering/seeking/tab-refocus/no recent gesture) via
readyState, video.seeking, a 'waiting' buffering-grace window, the existing
visibility grace, and own-tracked keydown/pointerdown gestures.
- HOST_BLOCKED handler: live streams are left alone (EC-15); involuntary events
snap back silently; deliberate events show a Teleparty-style choice dialog
(Stay in sync / Watch on my own), defaulting to stay after 8s (EC-18).
- Snap-back reuses tryMediaAction + suppress, with a cooldown to avoid
pause/play fight loops (EC-4).
- Desync ("watch on my own") mode: persistent resync badge, ignores host
commands while solo (still ACKs so force-sync doesn't stall), and auto-clears
on episode change (EC-12). Resync pulls the host's extrapolated position.
- background: GET_CONTROL_MODE (init fetch) + REQUEST_HOST_SYNC (resync target).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Background service worker now understands room control mode (no UI yet):
- Track controlMode + hostPeerId + amHost(); adopt from ROOM_DATA, update on
CONTROL_MODE, restore from session storage, reset on leave/room-switch.
- Receiver-side gate: ignore room-moving events from non-host peers in host-only
mode (backstop for old/buggy/modified clients past the server gate).
- Sender-side gate: a guest's play/pause/seek/force-sync is not broadcast;
instead a HOST_BLOCKED message is sent to the content tab with an extrapolated
host sync target (hook for step 4's snap-back + desync dialog).
- broadcastControlMode() notifies popup + content tab; GET_STATUS exposes
controlMode/hostPeerId/amHost; new SET_CONTROL_MODE message lets the host toggle.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Step 2 of host control mode (server-side, fully testable without UI):
- Room now tracks hostPeerId (= first joiner) and controlMode ('everyone' default).
- ROOM_DATA carries hostPeerId + controlMode so clients know their role on join.
- SET_CONTROL_MODE handler: host-only authority check, broadcasts CONTROL_MODE.
- Relay gate: in host-only mode, drop room-moving events (play/pause/seek/
force-sync/episode-lobby) from non-host guests — robust chokepoint that kills
spam regardless of client. Heartbeats/ACKs/episode-ready still pass.
- removePeerFromRoom: if the host leaves, fall back to 'everyone' and reassign
host to the earliest remaining peer (v1: immediate, no grace period).
Extends the WS integration test suite to cover all of the above.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Step 1 of host control mode. Adds the protocol surface only — no behavior yet:
- EVENTS.SET_CONTROL_MODE (client->server: host sets mode)
- EVENTS.CONTROL_MODE (server->client: mode changed)
- CONTROL_MODES { EVERYONE, HOST_ONLY }
Purely additive; old clients ignore the new events. Propagated to
extension/shared via build-extension. Also records the audited design
decisions in the edge-case log.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Planning docs for the optional per-room Host Control Mode (Teleparty-style
host-only playback control) requested via GitHub issue.
- host-control-mode-EDGECASES.md: canonical branch entry-point — goals, scope,
trust model, architecture summary, 21 edge cases, test matrix, open decisions.
- host-control-mode-plan.md: layered implementation plan with concrete code hooks
(server room state, three-layer gate, snap-back, guest desync flow, UI/i18n).
Temporary working docs for this branch; to be folded in or removed before merge.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The old logic only handled en='version.json' and non-en='../version.json',
which broke on /alternatives/ and /de/alternatives/ pages.
Absolute path works for root, /de/, /alternatives/, /de/alternatives/ etc.
- Fix cross-links on teleparty↔screen-sharing pages: use
{{LANG_PREFIX}} so German page links stay German, etc.
- Add og:locale meta tag with correct locale code per language
(e.g. en_US, de_DE, fr_FR, ja_JP, zh_CN, uk_UA)
- Add Organization schema.org JSON-LD block alongside
existing BreadcrumbList on all 3 alternatives templates
- Add 中文 (Chinese) and Українська (Ukrainian) to the language
selector in index, teleparty, and screen-sharing templates
- Previously only 13 of 15 languages were listed in the dropdown
Replaced English fallback with proper Japanese, Korean, and Chinese
translations for all 27 ALT_* website locale keys. All 15 locale
files now fully translated; npm run verify and build pass cleanly.
- Add article-style comparison pages for Teleparty and Screen Sharing
- Replace comparison tables with honest prose sections
- Add 15-language hreflang alternates and sitemap entries
- Add English locale fallback for untranslated languages
- Replace placeholder Draft copy with real English and German text
- Remove old Draft locale keys from all 13 non-EN/DE locales
- Rebuild outputs with zero TODO/Draft/noindex issues
When no other peer is present, heartbeats, force-sync, and episode auto-sync
are now fully suppressed (previously the keepAlive heartbeat, force-sync, and
episode lobby were still broadcast to an empty room):
- keepAlive heartbeat gated on a live peer count
- force-sync is a no-op when solo (no pause-then-timeout freeze, no traffic)
- episode auto-sync skips the lobby entirely and just plays the next episode
The solo state is recomputed live from the current peer list on every event
(never cached), so the instant another peer joins syncing resumes. On that
solo->not-solo transition the background also requests an immediate heartbeat
from the content script so the newcomer sees the current position without
waiting for the next interval.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Room ID, password, and username were resurrected from synced storage on a
fresh install (sync survives an uninstall in the user's Google account), which
made the extension silently auto-connect to a dead room and appear permanently
connected. getSettings() and all settings reads (popup, content, audio-options,
applyAudioSettingsToTab) are now local-only, and legacy keys are actively
purged from sync on install/update/startup. Only onboardingComplete and
dismissedHints remain in sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
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>
- 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>
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>
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>
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>
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>
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>
- 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>
- "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>
- 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>
- 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>
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.
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.
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.
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.
- 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.
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.
- 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.
- 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
- 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.
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.
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
After LEAVE_ROOM or idle-leave, reconnectAttempts was not reset.
GET_STATUS would return 'reconnecting' instead of 'disconnected',
showing wrong status in popup and leaving Retry button visible.
Lazy-connect introduces a new state where the user clicks 'Raum erstellen'
and the WebSocket takes 1-2s to establish (previously always pre-connected).
The old 15s blind timeout would re-enable the button while background was
still connecting, causing silent no-op clicks. Now polls GET_STATUS and
extends the window if still connecting.
- Add connectIntent flag to gate all reconnect attempts
- Only auto-connect on startup if roomId exists in storage
- Close socket + stop reconnect after leaveRoom or idleLeave
- Preserve existing behavior 1:1 when actively in a room
- Guard force-sync state and peer list clearing during transient disconnects
- Guard WEB_JOIN_REQUEST against empty sanitized roomId
Adds application-level ping/pong between extension and relay server.
Extension sends PING every 15s, server echoes PONG. Round-trip time
displayed in Status tab (green/yellow/red color-coded).
Server also forwards PING with target peerId and routes PONG back,
enabling future peer-to-peer ping without server restart.
Extension already responds to incoming peer PINGs.
See CHANGELOG.md for details.
- Fixed episode guard regex to support Sxx:Exx title format (Jellyfin/Emby)
and any non-alphanumeric separator between season and episode numbers
- Migrated username, roomId, password, serverUrl, useCustomServer from
chrome.storage.sync to chrome.storage.local for per-device isolation,
preventing automatic cross-device room joins with duplicate names
- Added one-time migration from sync to local for existing users
Changed condition from lang === 'de' to lang === 'en' so that all
non-English language subdirectories (fr, es, pt-BR, ru, de) correctly
resolve version.json via ../version.json instead of only German.
- Add CODE_OF_CONDUCT.md based on Contributor Covenant 2.1
- Add .github/PULL_REQUEST_TEMPLATE.md with structured sections
- Link CODE_OF_CONDUCT in README.md and CONTRIBUTING.md
- Rewrite bug_report.md with professional structure and Copy Logs requirement
- Rewrite feature_request.md to focus on problem, use cases, and benefits
- Nav Container Padding auf Mobile reduziert (0.75rem statt 2rem)
- Logo-Area Gap auf 0.25rem reduziert
- Icon auf 40x40 verkleinert
- Schriftgröße auf 1.1rem reduziert
- Vertical margin vom Icon entfernt
- Sprachauswahl aus nav-links in neuen nav-right Container verschoben
- nav-right ist auf Mobile rechtsbündig und immer sichtbar
- Hamburger-Menü entfernt (Links sind Same-Page-Anchors)
- Logo-Bereich auf Mobile kompakter (kleineres Icon, weniger Gap)
- Lang-Selector auf Mobile kompakter (kleinere Icons, weniger Padding)
- Gleiches Restructuring für join.html, impressum.html, datenschutz.html
- GET_VIDEO_STATE now returns allVideos[] with summary per video element
- Copy report shows markdown table when multiple videos on page
- Table includes: resolution, muted, paused, readyState, duration, and marks selected target
- Helps diagnose Prime Video scenarios where preview vs main video both exist
- findVideo() now scores all video elements by size + unmuted + duration
- Fixes Prime Video selecting 0x0 placeholder instead of actual player
- Fix history entries showing '?' by using h.action instead of h.event/h.type
- Update Prime Video status in TESTED_SERVICES.md to partial support
- Copy Logs button now generates full Markdown debug report (system, tab, connection, video, history, logs)
- Enhanced Dev tab with 20+ video state fields (network, dimensions, error, shadow DOM, platform detection)
- Platform auto-detection (YouTube, Netflix, Twitch, Prime Video, Disney+, Hulu, HBO, Vimeo, Dailymotion)
- 'No video found' diagnostic mode with hints
- Convert mockup from inline <span lang> to {{MOCK_*}} template vars (all 6 languages)
- New TwoPointZero branding: icons (16/32/48/96/128), favicons (PNG 16/32 + apple-touch + 192)
- Fix logo vertical centering via display:contents on picture wrappers
- Increase popup logo to 48px, website nav logo to 64px
- AVIF quality 70->80, speed 6->4, remove min 3KB threshold
- Add 32+96 icon sizes to manifest
- Remove amazon. from tab blacklist (unblocks Prime Video)
- Update TESTED_SERVICES.md: mark Prime Video as non-functional
- Update CONTRIBUTING.md with Copy Debug Report workflow
- Add currentServerUrl tracking: only disconnect/reconnect when server URL actually
changes, otherwise reuse existing connection and just switch rooms
- Add resolveServerUrl() helper for consistent URL resolution across handlers
- forceDisconnect() now resets currentServerUrl to null
- CONNECT and WEB_JOIN_REQUEST handlers: smart comparison before reconnecting
- Add generateRoomId(): human-readable 'ADJECTIVE-NOUN-Number' format (e.g. HAPPY-KOALA-16)
- Replace Math.random() room IDs with generateRoomId() in joinBtn and handleCreateRoom()
- Expand USERNAME_ADJECTIVES: +16 words (Turbo, Zen, Pixel, Cyber, Solar, etc.)
- Expand USERNAME_NOUNS: +12 words (Yeti, Goblin, Pirate, Ninja, Wizard, Storm, etc.)
- Add emoji mappings for all new words
- WEB_JOIN_REQUEST handler: forceDisconnect+connect instead of reusing old socket
(same bug as CONNECT: JOIN_ROOM was sent to official server instead of custom)
- joinBtn handler: generate random password when roomId is empty and password empty
(previously created rooms without passwords when using the advanced join button)
- Auto-copy invite link when creating room via joinBtn (consistent with Create Room button)
- Fix: CONNECT/RETRY_CONNECT always force-disconnect before reconnecting
(prevents JOIN_ROOM going to official server when custom is selected)
- Add forceDisconnect() helper: clears socket, eventQueue, episodeLobby,
expectedAcksCount, broadcast status, persists cleanup to storage
- Fix: save useCustomServer to storage on Join Room click (no race condition)
- Fix: trigger RETRY_CONNECT on server mode toggle and custom URL change
- Fix: show error when Custom selected but no URL entered (no silent fallback)
- Fix: button label changed to 'Join / Create Room' in all 6 locales
- Rewrite scheduleReconnect() for two-phase strategy:
Phase 1 (aggressive): 500ms-5s backoff, max 20 attempts or 5 minutes
Phase 2 (slow): retry every 5 minutes indefinitely, never give up
- Persist reconnectAttempts/reconnectStartTime/reconnectFailed to session storage
- Remove 'reconnect_failed' status; slow mode shows as 'reconnecting' with retry button
- Add scheduleReconnect() call on offline path to continue retry cycle
- Add sr-only CSS class for screen-reader/crawler-visible elements
- Add hidden <nav> with real <a> links to all 6 language versions in template.html
- Googlebot can now discover /de/, /fr/, /es/, /pt-BR/, /ru/ via HTML links
- No visual changes - existing <select> dropdown remains the user-facing switcher
- Rebuild all www/ output with updated CSS hash
HowTo schema was hardcoded English — now uses {{HOWTO_*}}
placeholders. All 3 rich-result schemas are now locale-aware:
- FAQPage (expandable SERP snippets)
- HowTo (step-by-step SERP snippets)
- SoftwareApplication (entity understanding)
SoftwareApplication and Organization use language-neutral
brand name — no locale keys needed.
- lastmod bumped to 2026-06-01 for all 6 language roots
- xhtml:link hreflang entries added per URL (Google-recommended
belt-and-suspenders approach alongside HTML hreflang tags)
- FAQPage schema now uses {{FAQ_Q1}}..{{FAQ_A6}} placeholders
instead of hardcoded English — each language gets its own
schema for locale-matched Google Rich Snippets
- Q6 updated across all 6 languages: 'What if I don't trust
the official server?' + self-hosting Docker mention
PageSpeed Insights flagged AVIF files as uncached because the
@static matcher only included *.webp. Adding *.avif enables
1-year Cache-Control for all AVIF assets.
Google does not support sameAs for app store links in SoftwareApplication
rich results. sameAs is for identity pages only (Wikipedia, Wikidata, GitHub).
Store pages are independently indexed by Google and need no schema linking.
Google treats SoftwareApplication.sameAs as app listing references
(not social profiles like Organization.sameAs). This connects
KoalaSync's entity graph to both browser stores without
duplication risk.
- Add LCP image preload for hero mascot
- Add HowTo structured data schema (3-step install guide)
- Update SoftwareApplication schema with screenshot + image fields
- Add glassmorphic :focus-visible outlines for keyboard accessibility
- Add content-visibility: auto to heavy offscreen sections for INP
- Expand lang-init.js auto-redirect to all 6 supported languages
- Remove impressum/datenschutz from sitemap.xml (noindex conflict)
- Removed unused html variable from localizeHomeLinks in website/app.js.
- Added website/build.js to ESLint flat config Node files match pattern to define require, process, and __dirname.
- Replaced /impressum.html and /datenschutz.html with clean URLs /impressum and /datenschutz in sitemap.xml.
- Recompiled website to copy clean sitemap to www/ folder.
- Restructured translation guide with clean markdown layout and tables.
- Removed mermaid sequence diagram to improve text-based clarity.
- Audited steps for contributing new language keys.
- Corrected static server root path to website/www/ in Caddyfile.example and website/README.md.
- Updated core roles to reflect multi-language (EN/DE/FR/ES/PT-BR/RU) support.
- Refactored local development and compilation instructions to target build.js and www/ output.
- Added pure Node.js dynamic i18n static site generator (build.js).
- Structured locales for English, German, French, Spanish, Brazilian Portuguese, and Russian.
- Replaced two-state toggle with premium glassmorphic language select dropdown.
- Integrated robust segment-based locale routing with safe dynamic fallbacks for legal and invite pages.
- Audited Core Web Vitals (LCP preloads, CLS dimensions) and SEO structures (robots, sitemap).
- Added dedicated Localization section to README and created contributor TRANSLATION guide.
- Update mockup tabs layout, colors, inputs, cards, and spacing to perfectly mirror popup.html 1-to-1.
- Enable full interactivity for toggle switches in Settings mockup screen.
- Link the top mockup version badge directly to the GitHub repository with smooth CSS hover transitions and cursor pointer.
- Port all hover tooltips (title attributes) from popup.html to the mockup index.html.
- Add mandatory caveman communication protocol to AI_INIT.md onboarding instructions.
- Create shared/names.js as single source of truth for emoji maps, noun
lists, adjectives, and username generation (215 animal emoji entries)
- Remove duplicated inline lists from popup.js and background.js; both
now import getAvatarForName / generateUsername from shared/names.js
- Add names.js to the build script sync list so it stays DRY across
extension builds
- Fix missing cat emoji: CoolCat now correctly resolves to 🐱
- Sort emoji map keys by length at lookup time to prevent substring
false-matches (e.g. 'caterpillar' before 'cat')
The noise filter used naive String.includes() on the full URL, causing
blacklist entry 'x.com' to match netflix.com (since 'netflix.com'
contains 'x.com' as substring). This made Netflix tabs disappear when
the filter was enabled.
Fixed by extracting the URL hostname and matching at domain boundaries:
- Domain entries (x.com): hostname === domain || endsWith('.' + domain)
- Prefix entries (amazon.): startsWith || includes('.' + domain)
- Keyword entries (jira): substring fallback on full URL
Release v1.7.5
The hero subtitle claimed support for 'local MP4s' which makes no sense
for a browser-based synchronization tool (local files can't be synced
across different computers). Changed to accurately reflect actual platform
support: YouTube, Twitch, Netflix, and any website with a video player.
- Tags are permanent once pushed; never reuse or move existing tags
- If a release is missing a fix, increment version and create new tag
- Force pushing (branches or tags) requires explicit user confirmation
- Added to Section 10 (Release Workflow) for AI agents
init() called updatePeerList() directly but never set lastKnownPeers.
On the first PEER_UPDATE message (heartbeat, play/pause, any state
change), detectPeerChanges() saw lastKnownPeers=[] and treated ALL
peers including self as newly joined — triggering 'name joined the room'
toast on every action while the popup was open.
- FORCE_SYNC_TIMEOUT in shared/constants.js was 5000 but all code uses
8500ms. Update constant to 8500 and reference it in background.js
instead of hardcoded values (4 occurrences)
- Add errorToken counter in popup showError() so stale 5s timeout
doesn't clear styling of a newer error that arrived in between
- Fix EVENT_ACK handler indentation in server/index.js to match
surrounding socket.on handlers
- Add 10s timeout to ensureState() so extension doesn't hang forever if
chrome.storage.session.get() never calls back (storage API failure)
- Skip emit(LEAVE_ROOM) in leaveOldRoomIfSwitching when socket is down;
server already cleaned up via disconnect handler, avoid queued no-op
- Persist cleared peers to storage in socket.onclose to prevent stale
peer list restoration on service worker restart
- Store and clean up forceSyncReset setTimeout in popup unload handler
and when force_sync_execute completes
- Store real client IP from x-forwarded-for on socket for use in JOIN_ROOM
auth rate limiting (was using proxy IP, breaking brute-force protection)
- Remove clearEpisodeLobbyState() from socket.onclose to preserve lobby
across brief disconnects; ensureState() recovers lobby + timeout on reconnect
- Lobby is still properly cleared on intentional LEAVE_ROOM and room switch
Two parallel JOIN_ROOM to a non-existent room could race past each
other during bcrypt.hash, causing the second to overwrite the first
room (password hash lost). The lock was read but never written.
- Create lock promise before bcrypt.hash async boundary
- Release in finally to cover all exit paths (success, MAX_ROOMS, error)
- Concurrent waiters now correctly await existing room creation
- Add renderEmpty() helper with icons and hints for peers/history/logs/rooms
- Implement onboarding tour (4 steps) with overlay, dots, skip/next
- Dev tab logs only poll when tab is visible (isDevTabVisible flag)
- Expand username generation from 12/12 to 30/30 adjective-noun pairs
- Update ROADMAP.md to remove implemented features
- popup.js: guard peers/state.acks in updateLastActionUI()
- popup.js: guard state.duration in refreshDebugInfo()
- popup.js: guard elements.lobbyPeerStatus in updateLobbyUI()
- background.js: use Array.isArray() for currentRoom.peers.length checks
Bug Fixes:
- index.html: remove duplicate <title> tags (invalid HTML), use single
title + JS swap; fix og:image relative→absolute URL (logo.png)
- join.html: fix hardcoded lang=de, add language detection IIFE,
add lang toggle to nav, make badge/title/desc/actions bilingual,
add missing </head> closing tag, add noindex meta
- app.js: join status handler was German-only; now fully bilingual
(Success/Error titles, countdown, close/retry button labels)
Improvements:
- Add Firefox 'Add to Firefox' button to hero CTA (placeholder href=#)
- impressum.html / datenschutz.html: add noindex meta to keep legal
pages out of search results
- robots.txt: add Sitemap directive for SEO crawlability
- sitemap.xml: new file listing main page + legal pages with priorities
- style.css: mobile nav hamburger menu (☰) replaces display:none
blackout; nav-links drop down on mobile with backdrop blur
- style.css: cta-group flex-wrap for narrow screens
- Hamburger button added to all 4 page navs
- CI release workflow now extracts version from tag (v1.4.0 → 1.4.0)
and injects it into manifest.base.json, shared/constants.js, and
package.json before building — the tag is the single source of truth
- Build script now copies README.md alongside constants.js and
blacklist.js from /shared → /extension/shared/ (full mirror)
- .gitignore updated: extension/shared/ is fully generated by build
- AI_INIT.md: simplified release workflow, fixed Chrome-only reference
Server Security (S-1 through S-8):
- S-1: Type-check and clamp peerId, protocolVersion, password
- S-2: Validate numeric/boolean/enum fields in relay peerData
- S-3: Construct explicit relay payload (stop spreading raw data)
- S-4: Type-check targetId and actionTimestamp in EVENT_ACK
- S-5: Restrict room IDs to [a-zA-Z0-9-] only
- S-7: Add eventCounts periodic cleanup alongside connectionCounts
- S-8: Guard version parsing against NaN bypass
Documentation (P-1, R-1 through R-6):
- P-1: Fix PRIVACY.md typo, document all in-memory data maps
- R-1/R-5: Fix stale sync-constants.bat references in shared/
- R-2: Fix stale lastTargetState ref in ARCHITECTURE.md
- R-3: Extension README title reflects cross-browser support
- R-6: Document content injection markers in scripts/README.md
- Add language toggle and German translation to website
- Optimize manifest description
- Add robots.txt
- Add open source transparency clause to privacy policy
Streaming players (Emby, Jellyfin, etc.) perform frequent internal seeks
for buffering that are < 1s in magnitude. These were being relayed to
peers, causing brief video freezes every few minutes.
Fix:
- MIN_SEEK_DELTA = 3.0s: ignore seeks smaller than 3 seconds
- 800ms debounce: settle rapid-fire seeks (e.g. scrubbing) before relaying
- Programmatic seek suppression still takes priority via expectedEvents
- lastReportedSeekTime baseline updated on programmatic seeks too
Adds a new toggleable feature that detects episode transitions via
mediaTitle mutation (loadeddata/MutationObserver), pauses the video,
and waits for all room peers to load the same episode before
executing a coordinated Force Sync play at 0:00.
Protocol:
- Add EPISODE_LOBBY and EPISODE_READY events to shared/constants.js
- Add EPISODE_LOBBY_TIMEOUT (60s) constant
- Relay both new events in server/index.js
Content Script (content.js):
- Layered detection: loadeddata + MutationObserver src-change + heartbeat
- Debounced onEpisodeTransition() sends signal ONLY; no eager pause
- PAUSE_FOR_LOBBY handler pauses only after background confirms feature enabled
- startLobbyPoll() polls title match without premature pause for non-initiators
- checkAndReportLobbyReady() pauses and sends EPISODE_READY_LOCAL on match
- CONTENT_BOOT recovery for re-injection after hard navigation
Background (background.js):
- Episode lobby state persisted in chrome.storage.session with recovery
- EPISODE_CHANGED: checks setting, creates lobby, sends PAUSE_FOR_LOBBY to tab
- EPISODE_LOBBY/READY server event handlers with dedup logic
- 60s timeout cancels lobby (Option B) with Chrome failure notification
- Peer departure handled: removes from readyPeers, re-checks completion
- executeEpisodeLobby() reuses existing Force Sync pipeline at targetTime 0.0
- Lobby cleared on LEAVE_ROOM; status exposed in GET_STATUS
Popup:
- Auto-Sync Next Episode toggle in Settings tab (default: off, opt-in)
- Episode Lobby status card in Sync tab with peer readiness display
- LOBBY_UPDATE message handler for real-time UI updates
Bumps APP_VERSION and manifest to 1.2.0
about: Create a report to help us improve KoalaSync
title: ''
labels: bug
assignees: ''
---
> **⚠️ Required:** Before submitting, open the KoalaSync **Status** tab in the extension popup and click **"Copy Logs"**. Paste the full output below — it contains essential system info, connection state, and debug data needed to diagnose your issue.
<details>
<summary><b>📋 Copy Logs Output</b></summary>
<!-- Paste the copied logs here -->
</details>
---
### Describe the Bug
A clear and concise description of what the bug is.
### To Reproduce
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
### Expected Behavior
A clear and concise description of what you expected to happen.
### Actual Behavior
A clear and concise description of what actually happened.
### Screenshots / Screen Recordings
If applicable, add screenshots or recordings to help explain your problem.
### Environment
- **Browser:** (e.g. Chrome 125, Firefox 128)
- **Extension Version:** (visible at the bottom of the Settings tab)
- **OS:** (e.g. Windows 11, macOS 14.5)
- **Self-Hosted Server?:** (yes / no — if yes, provide server version)
Welcome to the KoalaSync project. This file is the primary entry point for any developer or AI agent working on this codebase. It defines the architecture, non-negotiables, and workflows required to maintain the stability and security of the system.
> [!IMPORTANT]
> **Privacy & Data Sovereignty**: KoalaSync follows a strict **Zero-External-Requests Policy**: The extension and website must not make requests to any third-party domains (Google Fonts, CDNs, etc.). All assets (fonts, icons, scripts) must be self-hosted or use system defaults.
> - **Font Stack**: Use a modern system font stack (e.g., -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif) to maintain a premium look without external dependencies. Prohibit the use of `@import` or `<link>` for external font services.
---
## 1. Project Overview
KoalaSync is a specialized tool for **synchronized video playback** across multiple remote peers. It supports YouTube, Twitch, and native HTML5 video elements.
- **Users**: Friends or groups wanting to watch synchronized content together.
- **Workflow**: A user creates a room, shares an invitation link, and all peers in that room are synchronized via a Node.js relay server.
- **Identity**: Users are identified by a unique hex `peerId` combined with a customizable `username`.
## 2. Repository Structure
-`extension/`: Chrome Extension (Manifest V3). Contains background service worker, content scripts, and popup UI.
-`server/`: Node.js Relay Server using Socket.IO (WebSocket-only).
-`website/`: **Landing Page** & Invitation Bridge (Marketing, Tutorials, and Downloads).
-`shared/`: **Single Source of Truth** for protocol constants and event names.
-`docker-compose.yml`: Root-level orchestration for the relay server.
> [!IMPORTANT]
> **Single Source of Truth**: `shared/constants.js` and `shared/blacklist.js` are the master files. They must be synchronized to the `extension/shared/` directory using `.\scripts\sync-constants.bat` or `./scripts/sync-constants.sh`.
> - **Extension Modules** (`background.js`, `popup.js`) import directly from `./shared/constants.js`.
> - **Content Scripts** (`content.js`) use a **manual synchronous mirror** to prevent race conditions during page load. Always verify parity after sync.
## 3. Mandatory Reading
Before touching any code, you MUST read the following documents in order:
1. [ARCHITECTURE.md](ARCHITECTURE.md) – Detailed communication flows, Dual Heartbeat, and two-phase sync protocol.
2. [extension/README.md](extension/README.md) – Extension components, tab structure, and loading process.
3. [SYNC_GUIDE.md](SYNC_GUIDE.md) – Protocol constants and synchronization requirements.
## 4. The "Vanilla JS Mirror" Pattern
To avoid boot-time race conditions in Manifest V3 without a bundler, the following architectural trade-off is enforced:
- **Synchronous Execution**: `content.js` MUST execute synchronously to catch early media events.
- **Manual Mirroring**: `content.js` maintains a manual mirror of the `EVENTS` constants from `shared/constants.js`.
- **Maintenance**: Developers must ensure that any changes to `shared/constants.js` are manually reflected in `content.js` after running the sync scripts.
## 5. Design Guidelines
The popup UI follows a strict design system. Do not modify these variables or the layout structure without explicit approval.
- **Font**: System font stack. **MANDATORY**: No external CDNs or Google Fonts to ensure 100% privacy.
- **Popup Width**: Fixed at `320px`.
- **Tab Structure**: Must maintain the **Room**, **Sync**, **Settings**, and **Dev** tabs.
- **CSS Variables**:
| Variable | Value | Purpose |
| :--- | :--- | :--- |
| `--bg` | `#0f172a` | Main background |
| `--card` | `#1e293b` | Form and info cards |
| `--accent` | `#6366f1` | Primary actions and branding |
- **Deduplication**: If a user joins with a `peerId` that already has an active socket, the server kills the old socket to prevent "Ghost Peers".
## 2. Media Event Synchronization
When a user interacts with a video:
1.**Detection**: `content.js` listens to native events (`play`, `pause`, `seeked`) on the `<video>` element.
2.**Prevention of Loops**: Uses `lastTargetState` to distinguish between user actions and programmatic actions triggered by the extension.
3.**Reporting**: `content.js` sends a `CONTENT_EVENT` to `background.js`.
4.**Relay**: The Server forwards the event to all other peers in the room.
5.**Execution**: Remote peers receive the command and call `video.play()`, `video.pause()`, or `video.currentTime = targetTime`.
## 3. Two-Phase Force Sync
Ensures all peers are frame-perfect and buffered before resuming:
1.**Prepare**: Initiator sends `FORCE_SYNC_PREPARE` with the target timestamp.
2.**Buffer**: Peers seek and pause. Once buffered (`readyState >= 3`), they send a `FORCE_SYNC_ACK`.
3.**Execute**: Once the Initiator collects ACKs (or after a 5s timeout), they send `FORCE_SYNC_EXECUTE`.
4.**Resume**: All peers call `play()` simultaneously.
## 4. Peer Lifecycle & Dual Heartbeat
To maintain a clean room state and eliminate "Ghost Peers":
- **Session Heartbeat (Background)**: Every 30 seconds, `background.js` sends an "I'm alive" signal to the server. This keeps you in the room even if no video is playing.
- **Video Heartbeat (Content)**: Every 15 seconds, `content.js` sends current playback metadata (time, title, state) if a video is found.
- **Server Pruning**: The server runs a "Reaper" every 2 minutes. If a peer has sent **zero** activity (no events and no heartbeats) for 5 minutes, they are forcefully disconnected.
- **Immediate Cleanup**: Rooms are deleted instantly when the last peer leaves or disconnects.
## 5. Security & Stability
- **Service Worker Lifecycle**: Uses `chrome.alarms` to prevent the Manifest V3 service worker from suspending while in an active room.
- **Rate Limiting**: Server-side per-socket and per-IP rate limits to prevent sync-spamming or DoS.
- **Noise Filtering**: Uses a curated blacklist of domains (Search Engines, Social Media) to declutter the "Target Tab" selector in the popup.
- **Diagnostics**: A "Dev" tab provides real-time access to the underlying `<video>` state (`readyState`, `paused`, `currentTime`) for easier troubleshooting.
## 6. Constant Synchronization & Consistency
To maintain a "Single Source of Truth" across the server and extension without using a bundler:
- **Relay Server & Extension Modules**: `background.js` and `popup.js` import constants directly from `shared/constants.js`.
- **Content Scripts**: To ensure zero-latency execution, `content.js` uses a manual mirror of `EVENTS`.
- **Synchronization**: The `./scripts/sync-constants.sh` script ensures that the `shared/` folder within the `extension/` directory is kept up-to-date with the root `shared/` source.
- **Verification**: Any protocol change requires a manual verification sweep across all three constant locations (Shared, Server, and Content Script Mirror).
- **Comments**: Document complex sync logic. The codebase uses inline comments for protocol reasoning.
---
## Version Numbers
> [!CAUTION]
> **Never manually bump version numbers.** The CI pipeline injects the version from the git tag into `manifest.base.json`, `shared/constants.js`, and `package.json` during release builds. Manual bumps cause conflicts.
---
## Open Source Workflow & Pull Requests
If you are new to open-source contributions, follow these steps to propose your changes:
1.**Fork the Repository**: Click the "Fork" button at the top right of this repository to create your own copy of KoalaSync.
2.**Clone your Fork**: `git clone https://github.com/YOUR-USERNAME/KoalaSync.git`
3.**Create a Branch**: `git checkout -b my-new-feature` (e.g. `feature/dark-mode` or `fix/translation-de`)
4.**Make your Changes**: Edit the files, then verify them locally.
- *Extension/Server changes*: Test on Chrome/Firefox and check `npm run lint`.
- *Website/Translation changes*: Run `node website/build.cjs` and check the output in `www/`.
5.**Commit and Push**: `git commit -m "Add my feature"` and `git push origin my-new-feature`
6.**Open a Pull Request (PR)**: Go to the original KoalaSync repository on GitHub and click "New Pull Request".
### PR Code Requirements
- **Lint**: Ensure `npm run lint` passes with zero errors and warnings.
- **Syntax**: Run `node -c` on every modified `.js` file.
- **Protocol changes**: Update relevant documentation in `docs/`.
---
## Bug Report Template
When filing a bug, the easiest way is to use the **Copy Logs** button in the extension's **Status** tab. It copies a fully formatted Markdown report to your clipboard containing:
- System info (version, protocol, peer ID, browser)
- Connection status (server, room, peers, reconnect state)
- Video debug info (playback state, readyState, network state, dimensions, error codes, shadow DOM detection, platform)
- Action history (last 20 events)
- Log entries (last 50)
Simply paste the clipboard contents into your GitHub issue and add:
| Field | Example |
|-------|---------|
| **Steps to Reproduce** | 1. Create room → 2. Join from second browser → 3. Play video |
| **Expected Behavior** | Both peers play simultaneously |
| **Actual Behavior** | Peer B remains paused |
If you cannot access the Status tab, include as much of the following manually:
| Field | Example |
|-------|---------|
| **Browser** | Chrome 125, Firefox 128 |
| **Extension Version** | v1.9.3 (visible at bottom of Settings tab) |
| **Website/Platform** | Netflix, YouTube, Twitch, Jellyfin, etc.
KoalaSync is a premium, lightweight Chrome Extension and Relay Server for synchronized video playback across any website (YouTube, Twitch, Netflix, and custom HTML5 players).
<h1 align="center">KoalaSync</h1>
> [!TIP]
> **New Developers & AI Agents**: Please read [AI_INIT.md](AI_INIT.md) before starting work.
-`shared/`: Protocol constants and domain blacklist.
-`scripts/`: Development utilities for protocol synchronization.
<p align="center"><i>KoalaSync is a lightweight Browser Extension and Relay Server for synchronized video playback on almost any website with a video element—YouTube, Twitch, Netflix, Emby, Jellyfin, and beyond. Built with a focus on <b>Data Sovereignty</b> and <b>Performance</b>.</i></p>
> [!NOTE]
> For deep technical dives, see [ARCHITECTURE.md](ARCHITECTURE.md) and [SYNC_GUIDE.md](SYNC_GUIDE.md).
<p align="center"><a href="docs/CHANGELOG.md"><b>New v2.6.3 Release!</b> — See what's changed</a></p>
### 🌟 Why KoalaSync?
***🛡️ Security-First**: Volatile RAM-based relay with built-in brute-force protection and zero-persistence architecture. We keep no logs of your sessions or synchronizations. *We don't track you. We only track our server* (relying on the [aggregated, anonymous, non-personal metrics](https://syncserver.koalastuff.net/health) provided under `/health`).
***📡 Direct Logic**: Manual Socket.IO wire implementation for reliable synchronization.
***🛠️ Clean Build**: Dependency-free extension runtime with no library overhead.
***🌐 Universal**: Works on any website with a `<video>` tag.
---
### ✨ Key Features
## Key Features
- **Global Synchronization**: Synchronize Play, Pause, and Seeking on any website with a `<video>` tag.
- **Smart Matching**: Automatically highlights and sorts tabs containing matching video titles.
- **Noise Filtering**: Built-in domain blacklist to hide non-video sites from selection.
- **Seamless Invitations**: Smart links that automatically configure server and room credentials for your friends.
- **Smart Audio Compressor**: Tired of constantly riding the volume? Automatically balance out whispering dialogue and deafening explosions with simple presets, or fully customize the audio to your liking.
## Setup Instructions
---
### 1. Relay Server (Docker)
The server runs on Node.js using Socket.IO, containerized for easy deployment.
### 🚀 Quick Start
#### For Users (Installation & Usage)
The easiest and safest way to install KoalaSync is directly through the official browser stores:
*(For manual offline installation: Download the latest `.zip` from the [Releases](https://github.com/Shik3i/KoalaSync/releases) page and load it as an "Unpacked Extension" in Developer Mode).*
**How to use:**
1.**Create a Room:** Click the Koala icon in your browser and hit `+ Create New Room`.
2.**Invite Friends:** Share the auto-copied invite link. Once they click it, they automatically join.
3.**Pick a Video:** Navigate to the Sync Tab, select the tab playing your video, and grab some popcorn! 🍿
---
### 🌐 Localization & Translations
Both the official KoalaSync website and the browser extension feature dynamic localization:
- **Available Languages**: Support is included for 15 languages: English (`en`), German (`de`), French (`fr`), Spanish (`es`), Portuguese (Brazil) (`pt-BR`), Russian (`ru`), Italian (`it`), Polish (`pl`), Turkish (`tr`), Dutch (`nl`), Japanese (`ja`), Korean (`ko`), Chinese (Simplified) (`zh`), Ukrainian (`uk`), and European Portuguese (`pt`).
- **Real-Time Extension Localization**: Inside the extension Settings panel, users can swap languages instantly. The entire interface, notifications, Empty States, and onboarding guides re-translate dynamically in real-time.
- **Contributing**: We welcome community translations for both the website and the extension! Please refer directly to the [TRANSLATION.md](docs/TRANSLATION.md) guide for step-by-step instructions on how to audit, refine, or add new languages.
To build the extension from source and synchronize protocol constants:
```bash
# From the root directory
docker-compose up -d --build
npm install
npm run build:extension
```
The server will be available at `ws://localhost:3000`.
The compiled artifacts will be available in the `dist/` directory.
### 2. Chrome Extension
1.**Synchronize Protocol**: From the root directory, run the sync script to copy the master constants to the extension folder:
```bash
./scripts/sync-constants.sh
```
2. Open Chrome and go to `chrome://extensions/`.
3. Enable **Developer mode** (top right).
4. Click **Load unpacked**.
5. Select the `extension/` folder.
#### For Self-Hosting (Docker)
For local development or a simple private relay, use the root Compose file:
```bash
cp server/.env.example server/.env
# Edit server/.env and set SERVER_SALT to a unique random value:
# openssl rand -base64 32
docker compose up -d --build
```
The local relay will be available at `ws://localhost:3000`.
## Usage
1. Open the extension and go to the **Settings** tab to set your **Username**.
2. Go to the **Room** tab, enter your Server URL (default: `ws://localhost:3000`), and click **Join / Create Room**.
3. In the **Sync** tab, select the tab containing the video you want to sync.
4. Share the **Invite Link** from the Room tab. When your friends click it, they will automatically join your room and server.
5. Use **Force Sync** to perfectly align everyone to your current timestamp.
For production, use the official image and one of the ready-to-edit examples:
- [Docker network compose](examples/docker-compose.caddy.example.yml): for a Caddy or reverse-proxy network. This file does not publish `localhost:3000`; Caddy routes traffic to the container.
- [Static IP compose](examples/docker-compose.ip.example.yml): for a pre-existing Docker network with a fixed container IP.
## Technical Details
- **Manifest V3**: Uses a persistent Service Worker with Alarm-based keep-alive.
- **Manual Socket.IO Protocol**: The extension implements the Socket.IO v4 wire protocol natively for extreme performance and zero dependencies.
- **Dead Peer Pruning**: The server automatically prunes peers after 5 minutes of total inactivity (detected via dual heartbeats).
- **Two-Phase Sync**: Ensures all peers are buffered (`readyState >= 3`) before resuming playback.
In every real deployment, set a unique `SERVER_SALT`. Without it the relay still starts, but it warns that room-password hashes are using the public fallback salt from the repository.
## Security & Privacy
> [!IMPORTANT]
> **Privacy First**: KoalaSync stores no data on disk. All room states exist only in RAM and are purged immediately when empty. There is zero telemetry, tracking, or analytics.
To connect your extension to a self-hosted server, open the popup → **Room** tab → select **Custom Server** → enter your server's WebSocket URL (e.g., `ws://localhost:3000`).
## Troubleshooting
- **Logs**: Check the **Dev** tab in the extension popup for live connection logs and video state diagnostics.
- **Handshake**: Verify you see `Joined Namespace /` in the logs.
- **Permissions**: Ensure the target site hasn't blocked script injection (rare for most video sites).
> **⚠️ Note**: `ws://` only works for `localhost`. If you deploy to a real domain, you **must** use `wss://` (e.g., `wss://sync.yourdomain.com`). This requires a TLS-terminating reverse proxy (e.g., Caddy, Nginx, or Traefik) in front of the relay server. See [Caddyfile.example](examples/Caddyfile.example) for a production-ready template.
To verify your relay is reachable from outside, visit `https://your-domain.com` in a browser — it should return `{"status":"online","service":"KoalaSync Relay"}`.
#### Supply Chain Security (v2.2.2+)
All official release artifacts (Docker images and extension binaries) are published with signed [artifact attestations](https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations) to prove they were built from this repository's source code.
KoalaSync is built on a **zero-persistence, privacy-first** architecture. We take security seriously and appreciate responsible disclosure of vulnerabilities.
---
## Supported Versions
Only the latest stable release receives security patches.
| Version | Supported |
|---------|-----------|
| Latest release | :white_check_mark: Active |
| Older versions | :x: Unsupported |
Users on older versions are encouraged to update. The server enforces a minimum client version via `MIN_VERSION`.
---
## Scope
The following components are within scope for security reports:
| **As needed** | Collaborative discussion for clarification |
| **After patch** | Notification that the fix is deployed |
| **After rollout** | Public acknowledgment in release notes (or anonymity if preferred) |
---
## Architecture & Threat Model
KoalaSync's security is grounded in its architecture:
- **RAM-only relay**: No database, no persistent logs. All session data evaporates on disconnect.
- **Keyed SHA-256 room password hashes**: Plaintext passwords are never stored. Room passwords are held only as in-memory HMAC-SHA256 hashes for the short room lifetime, with brute-force protection: 5 attempts → 15-minute IP lockout.
- **Rate limiting**: Connection rate (IP-based, 60s window), health endpoint rate (10 requests/minute/IP), wrong admin-metrics bearer attempts (5 requests/minute/IP), and event rate (per-socket, 10s window). Health-style JSON responses are cached server-side for 60 seconds and refreshed lazily on request.
- **Reverse proxy boundary**: The relay trusts one proxy hop for client IP detection. In production, keep the Node server reachable only through Caddy or another trusted reverse proxy.
- **URL-hash credential isolation**: Invitation credentials live in the URL fragment (`#join:...`) — never sent to the web server.
KoalaSync uses a "Single Source of Truth" for its communication protocol constants located in the root `shared/` directory. However, Chrome Extensions (Manifest V3) are strictly sandboxed and **cannot load or import files from outside their root directory**.
To ensure that the extension and the relay server are always using the exact same event names and protocol versions, we maintain a mirrored copy of the shared files within the `extension/shared/` folder.
## When should you run the sync script?
You MUST run the synchronization script in any of the following scenarios:
1.**After a fresh `git clone` or `git pull`** (as the synced files are ignored by git).
2.**After modifying**`shared/constants.js`.
3.**After modifying**`shared/blacklist.js`.
4.**Before committing** changes to the repository if any protocol-related files were touched.
5.**Before deploying** the server or releasing the extension.
## How to sync
### On Windows
Run the batch script from the repository root:
```powershell
.\scripts\sync-constants.bat
```
### On macOS / Linux
Run the shell script from the repository root:
```bash
./scripts/sync-constants.sh
```
## What does it do?
The script performs the following actions:
1. Ensures the `extension/shared/` directory exists.
2. Copies `shared/constants.js` to `extension/shared/constants.js`.
3. Copies `shared/blacklist.js` to `extension/shared/blacklist.js`.
## Protocol Versioning
As of v1.0.0-RC5, the system enforces a strict `protocolVersion` check during the `JOIN_ROOM` handshake.
- The version is defined in `shared/constants.js`.
- If the extension and server versions mismatch, the server will reject the connection with an `Incompatible protocol version` error.
- **Always run the sync script** after bumping the version number to ensure both components are updated.
> [!CAUTION]
> **NEVER** edit the files inside `extension/shared/` directly. They will be overwritten the next time the sync script is run. Always edit the files in the root `shared/` directory and then run the sync script.
Ready-to-paste copy for product listings, launch pages, directory submissions, and anywhere else you keep re-typing the same pitch. Three lengths, one consistent message. Pick the one that fits the field limit.
---
## 1. One-Sentence Pitch
> KoalaSync is a privacy-first browser extension that synchronizes video playback across almost any website so you can watch with friends in real time — no accounts, no tracking, and your video never passes through anyone's server but the original site's.
> Private, universal watch parties on any website — no accounts, no tracking, no media proxying.
---
## 2. Three-Sentence Overview
> KoalaSync is a lightweight browser extension that keeps you and your friends perfectly in sync on YouTube, Netflix, Twitch, Prime Video, Jellyfin, Emby, and almost any other site with an HTML5 video player — press play once and everyone stays together. It's built privacy-first: no accounts, no telemetry, and the official relay server runs entirely in volatile RAM with zero persistence, so nothing about your sessions is ever stored. Open source under the MIT license and fully self-hostable with a single Docker Compose command, KoalaSync is a transparent watch-party tool that works everywhere and respects your data sovereignty.
---
## 3. Full Description
### Watch together — on any site, on your terms.
Counting down "3, 2, 1, play" over voice chat doesn't scale past two people. KoalaSync fixes that with a tiny browser extension that synchronizes play, pause, and seeking across everyone in the room, on almost any website with a `<video>` element. Create a room, share a link, press play — that's it.
### What makes KoalaSync different
Most watch-party tools fall into one of two traps: they only work on a short allowlist of sites (site-specific extensions that need a separate build for every platform), or they route your video through their own player and servers. KoalaSync was built around three principles that break that mold.
**Universal by design.** If the site has an HTML5 `<video>` element, KoalaSync can usually sync it. YouTube, Netflix, Twitch, Prime Video, Disney+, Jellyfin, Emby, and countless niche sites work out of the box — no per-site integration to wait for, no extension swap when your friends want to switch services.
**Your video never touches our servers.** KoalaSync only relays tiny timing messages — play, pause, seek position, readiness — over a hand-rolled WebSocket protocol. The actual video keeps streaming directly from the original site to each viewer's browser. KoalaSync never proxies, transcodes, uploads, or redistributes a single frame, which also means there is no legal gray zone around redistribution.
**Privacy is the default, not an upgrade.** No accounts, no emails, no telemetry, no analytics, no behavior profiling. The official relay server runs entirely in volatile RAM and keeps zero persistent state — when the room closes, the data is gone. Pick a nickname or let KoalaSync generate one for you and you're in.
### Built for people who actually want to read the code
KoalaSync is MIT-licensed open source, built by a solo developer. Audit it, fork it, change it. The extension is dependency-free with a direct Socket.IO wire implementation — no opaque libraries, no framework bloat, no surprise third-party SDKs. Want full sovereignty? Set a unique `SERVER_SALT` in the supplied Compose file, then start your own relay with a single Docker Compose command and keep all watch-party coordination traffic inside your own infrastructure. The official public relay is there when you don't care, self-hosting is there when you do.
### Little touches you'll notice
- **Episode Auto-Sync** pauses the room when someone loads the next episode and resumes only when everyone is ready — no spoilers, no one left behind on the previous cliffhanger.
- **Smart Audio Compressor** tames the modern "whisper dialogue, deafening explosion" mix with one click. Three presets or full manual control over threshold, ratio, attack, and release.
- **One-click invite links** auto-configure the server and room for your friends — they just click the link and they're in. No fumbling with server URLs or room IDs.
- **Dual-heartbeat architecture** kills ghost rooms and stale connections before they desync your session.
- **15 languages** fully translated and switchable in real time from the settings panel — English, German, French, Spanish, Portuguese (Brazil + European), Russian, Italian, Polish, Turkish, Dutch, Japanese, Korean, Chinese, Ukrainian.
### Install and start in under a minute
Install KoalaSync from the Chrome Web Store or Firefox Add-ons, click "Create Room," share the invite link, and pick a video. The official relay is ready out of the box — no setup required unless you want to self-host.
- Website: https://sync.koalastuff.net
- GitHub: https://github.com/Shik3i/KoalaSync
---
## Bonus: Taglines (for hero headlines, social bios, meta descriptions)
- Watch together. Anywhere. Privately.
- The watch-party tool that works on every site and tracks none of them.
- Sync play, pause, and seek on any video — no accounts, no logs, no lock-in.
- Self-hostable, open-source watch parties for the post-"3, 2, 1, play" era.
Tired of counting down "3, 2, 1, Play" over voice chat? KoalaSync keeps you and your friends perfectly in sync. Whether you are streaming from your own self-hosted media server like Emby, Jellyfin or Plex, or watching on a major platform like Netflix, Prime Video or YouTube, KoalaSync is designed for smooth, browser-based watch parties.
✨ CORE FEATURES
No account required. No tracking. Just create a room, invite your friends, and start watching together.
• Real-Time Video Sync: Play, pause, seek, and watch together with fast synchronized playback across everyone in your room.
• No Account Needed: Create a room and share the invite link. No emails, no passwords, no sign-ups. Pick a nickname or let KoalaSync generate one for you.
• Works Almost Everywhere: If the website uses a standard HTML5 video player, KoalaSync can usually sync it. Perfect for streaming sites, self-hosted media servers, and other websites.
• Smart Binge-Watching: When a new episode loads, KoalaSync automatically pauses the lobby until everyone is ready. No spoilers, no one left behind.
• Smart Audio Compressor: Tired of quiet dialogue and suddenly loud action scenes? Balance whispering, explosions, and music with a single click while you watch.
• One-Click Invites: Send a smart invite link to your friends. When they open it, KoalaSync automatically configures the room so they can join instantly.
• 15 Languages: Enjoy a native experience in English, German, French, Spanish, Brazilian and European Portuguese, Russian, Italian, Polish, Turkish, Dutch, Japanese, Korean, Simplified Chinese, and Ukrainian.
🛡️ PRIVACY & SECURITY
KoalaSync is built for private watch parties without unnecessary data collection.
• No Tracking: Zero analytics, zero telemetry, and absolutely no behavior profiling.
• Anonymous by Design: No accounts needed. Rooms can be joined with a simple nickname.
• Ready Out of the Box: Install KoalaSync and start watching immediately using the official public relay server. No technical setup required.
• RAM-Only Public Server: The official relay server operates entirely in volatile RAM. No databases, no stored watch history, no persistent room data. Room data exists only temporarily and disappears when the room closes.
• Self-Hostable: Want full control? You can run your own private KoalaSync relay server via Docker in seconds. Self-hosting is optional and never required.
🚀 HOW IT WORKS
1. Install KoalaSync.
2. Click "Create Room" to start a private watch party.
3. Share the invite link with your friends.
4. Open your favorite streaming site or media server.
5. Select the active video tab.
6. Press play. Everyone stays perfectly in sync.
⚙️ UNDER THE HOOD
KoalaSync is lightweight, transparent, and built with privacy in mind.
• On-Demand Relay: Playback state is synchronized through a custom WebSocket-based relay server. There is no permanent background connection. The relay connection is maintained only while you're in a room and closes when you leave or automatically after two hours without a selected-video heartbeat.
• No Media Streaming: KoalaSync does not stream, proxy, upload, download, or redistribute any video content. Everyone watches from their own browser on the original website.
• Temporary Room State Only: The relay server only coordinates room state such as play, pause, seek position, active target, nickname, and readiness status.
• Docker Self-Hosting: The relay server can be self-hosted with Docker if you prefer to run your own private instance.
• Open Architecture: The project is designed to be inspectable, forkable, and easy to review.
💻 OPEN SOURCE
KoalaSync was built by a solo developer who needed a fast, secure way to watch movies with friends. The code is fully transparent under the MIT license: audit it, fork it, improve it, or self-host your own relay server.
Found a bug or have a feature idea? Open an issue on GitHub. Contributions and code reviews are always welcome.
Welcome to the KoalaSync project. This file is the primary entry point for any developer or AI agent working on this codebase. It defines the architecture, non-negotiables, and workflows required to maintain the stability and security of the system.
> [!IMPORTANT]
> **Privacy & Data Sovereignty**: KoalaSync follows a strict **Zero-External-Requests Policy**: The extension and website must not make requests to any third-party domains (Google Fonts, CDNs, etc.). All assets (fonts, icons, scripts) must be self-hosted or use system defaults.
> - **Font Stack**: Use a modern system font stack (e.g., -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif) to maintain a premium look without external dependencies. Prohibit the use of `@import` or `<link>` for external font services.
>
> [!IMPORTANT]
> **Communication Standard**: Be concise, concrete, and professional. Match the user's language when practical, explain risky changes before making them, and report exactly what was changed and verified. Do not use joke protocols or intentionally broken language in contributor-facing work.
---
## 1. Project Overview
KoalaSync is a specialized tool for **synchronized video playback** across multiple remote peers. It supports YouTube, Twitch, and native HTML5 video elements.
- **Users**: Friends or groups wanting to watch synchronized content together.
- **Workflow**: A user creates a room, shares an invitation link, and all peers in that room are synchronized via a Node.js relay server.
- **Identity**: Users are identified by a unique hex `peerId` combined with a customizable `username`.
## 2. Repository Structure
-`extension/`: Browser Extension (Chrome & Firefox, Manifest V3). Contains background service worker, content scripts, and popup UI.
-`server/`: Node.js Relay Server using Socket.IO (WebSocket-only).
-`website/`: **Landing Page** & Invitation Bridge (Marketing, Tutorials, and Downloads).
- **`build.cjs`**: Static site compiler using the repository's build dependencies. Translates `template.html` + `locales/*.json` → `www/`, generates the sitemap, and minifies HTML/CSS/JS.
- **`www/` is auto-generated**: Never edit files in `www/` directly. Always edit source files (`template.html`, `style.css`, `styles/*.css`, `app.js`, `lang-init.js`, `locales/*.json`) and run `node website/build.cjs` to regenerate. `style.css` is the development manifest; `style.legacy.css` is an unloaded, byte-identical migration reference. Production creates page-specific and render-priority CSS bundles from `styles/*.css`; CSS/JS are output as `.min.*` files and stale generated assets are removed on each build.
-`shared/`: **Single Source of Truth** for protocol constants, event names, blacklist data, and generated usernames.
-`scripts/`: Development utilities (e.g., `build-extension.cjs`).
-`docker-compose.yml`: Root-level orchestration for the relay server.
> [!IMPORTANT]
> **Single Source of Truth**: `shared/constants.js`, `shared/blacklist.js`, and `shared/names.js` are the master shared files. They must be synchronized to the `extension/shared/` directory using `node scripts/build-extension.cjs`.
> - **Extension Modules** (`background.js`, `popup.js`) import directly from `./shared/constants.js`.
> - **Content Scripts** (`content.js`) use a **marker-injected synchronous copy** of the constants. The build script automatically replaces the marked blocks — no manual mirroring needed.
## 3. Mandatory Reading
Before touching any code, you MUST read the following documents in order:
1. [../README.md](../README.md) – User, developer, and self-hosting overview.
2. [README.md](README.md) – Documentation map by role.
3. [ARCHITECTURE.md](ARCHITECTURE.md) – Detailed communication flows, Dual Heartbeat, and two-phase sync protocol.
4. [../extension/README.md](../extension/README.md) – Extension components, tab structure, and loading process.
5. [SYNC_GUIDE.md](SYNC_GUIDE.md) – Protocol constants and synchronization requirements.
## 4. The "Vanilla JS Mirror" Pattern
To avoid boot-time race conditions in Manifest V3 without a bundler, the following architectural trade-off is enforced:
- **Synchronous Execution**: `content.js` MUST execute synchronously to catch early media events.
- **Automated Injection**: The build script (`node scripts/build-extension.cjs`) automatically injects `EVENTS`, `HEARTBEAT_INTERVAL`, and episode utility functions into `content.js` using marker-based replacement (see `../scripts/README.md` for marker details).
- **Maintenance**: After modifying any root `shared/` file, run the build script. No manual mirroring is required.
The popup UI follows a strict design system. Do not modify these variables or the layout structure without explicit approval.
- **Font**: System font stack. **MANDATORY**: No external CDNs or Google Fonts to ensure 100% privacy.
- **Popup Width**: Fixed at `320px`.
- **Tab Structure**: Must maintain the visible **Room**, **Sync**, **Settings**, and **Status** tabs. The hidden **Dev** tab is available only for developer diagnostics.
- **Appearance modes**: Preserve `system`, `light`, and `dark` behavior and the Eucalyptus, Cyber, and Graphite palettes.
- **CSS variables**: Use the semantic tokens defined in `popup.html` (`--bg`, `--card`, `--accent`, `--text`, `--text-muted`, `--success`, `--warning`, and `--error`). Do not hard-code the retired indigo/slate palette or bypass palette-specific token overrides.
## 7. Non-Negotiables (Core Logic)
The following features are critical and must not be removed or fundamentally altered:
- **Two-Phase Force Sync**: The `Prepare` → `ACK` → `Execute` flow ensures all peers are buffered before playback resumes.
- **Episode Auto-Sync**: Ensures series binges stay perfectly synced. A lobby initiates during title transitions, freezing peers until everyone is ready.
- **Host Control Mode**: In `host-only` rooms, only the host and promoted controllers may initiate room-moving playback events. Both client-side UX and server-side gates must remain intact.
- **Dual Heartbeat**:
- **Background Heartbeat (1m)**: Ensures session persistence even without a video element.
- **Content Heartbeat (15s)**: Transmits current video metadata (time, title).
- **Dead Peer Pruning**: Server "Reaper" disconnects peers after 5 minutes of total silence (no heartbeats or events).
- **Deduplication**: Server kills old sockets if a user re-joins with the same `peerId` to prevent ghosts.
- **Platform Specifics**: Specialized click-logic for YouTube (`.ytp-play-button`) and Twitch.
- **pollSeekReady()**: Polling mechanism that checks `video.readyState` before acknowledging sync.
- **SW Keep-alive**: Use of `chrome.alarms` to prevent the Manifest V3 Service Worker from suspending.
- **Diagnostics**: The visible **Status** tab provides real-time access to connection state, ping, logs, history, and underlying `<video>` state for troubleshooting.
- **Persistence**: `peerId` and `username` must be stored to remain stable across sessions.
- **Title Privacy**: Do not accidentally reintroduce tab/media title sharing when users disabled or reduced it.
- **Room ID Format**: Room IDs are restricted to `[a-zA-Z0-9-]` only (alphanumeric + hyphens). This is enforced server-side.
## 8. Technical Constraints
- **No Bundler**: The extension uses plain ES Modules. Do not introduce build steps or npm packages into the `extension/` folder.
- **Manual Protocol**: `background.js` implements a subset of the Socket.IO wire protocol natively.
- **Server Transport**: Restricted to `websocket` only. Polling is disabled.
- **Docker Context**: The Docker build must run from the **Repo Root**.
- **Content Script Scope**: `bridge.js` is the only static content script in the manifest and runs on `https://sync.koalastuff.net/*` at `document_start`. Video control scripts are injected only into the selected tab via `chrome.scripting`; do not add broad persistent video content-script matches.
- **Self-Hosting Salt**: Production/self-hosted relay examples must include a unique `SERVER_SALT` so room-password hashes are not derived with the public fallback salt.
- **Strict Backward & Forward Compatibility (Store Delay Rule)**: Browser extensions are distributed through stores (e.g., Chrome Web Store, Firefox Add-ons) which can take up to 2 weeks to approve updates. Therefore, the server MUST NOT reject older extension clients unless a critical protocol version bump is explicitly authorized, and new extension versions MUST remain fully operational when connected to older servers (e.g., by silently falling back if a new feature is not supported). This is a core architectural requirement.
## 9. Security & Deployment
- **Tokens**: Security tokens are intentionally managed via `shared/constants.js` and server `.env`.
- **Environment**: `.env` is excluded via `.gitignore`. Only `.env.example` should be committed.
- **Revocation**: `MIN_VERSION` check on the server is used to deprecate old extension versions.
- **Invitation Links**: Correctly propagate server URLs, Room IDs, and Passwords via the URL hash to the bridge.
## 10. Common Workflows
## CRITICAL: Git & Release Rules
- **NEVER** push, commit, tag, or release without explicit user instruction.
- **NEVER** retag or force-push without explicit instruction.
- **NEVER** create tags for documentation-only or README changes.
- **NEVER** run `git push`, `git tag`, `git commit` unless the user says "push", "commit", or "tag".
- Only the user decides when to commit, push, tag, or release.
- Ask before any git write operation.
### ⚠️ Pre-Session Git Sync (MANDATORY)
Before starting any task, committing, or pushing, you **MUST** run `git pull --rebase` to ensure your local branch is up-to-date with `origin/main`. CI pipelines and other agents may push commits concurrently. Skipping this step will cause merge conflicts and rejected pushes.
### Releasing a New Version (CRITICAL WORKFLOW FOR AI AGENTS)
> [!CAUTION]
> **AI AGENTS MUST FOLLOW THIS EXACT SEQUENCE WHEN RELEASING A NEW VERSION OR TAGGING.**
>
> **🚫 NO MANUAL VERSION BUMPING**: You MUST **NEVER** manually modify the version strings in `package.json`, `extension/manifest.base.json`, or `website/version.json`. The GitHub Actions CI pipeline automatically extracts the version from the git tag (e.g. `v2.0.5` -> `2.0.5`), injects it into all target files, and commits the updates back to `main` with `[skip ci]`. Manual bumps will cause merge conflicts and build failures.
> - **Website Versioning**: **NEVER** manually modify generated version strings in `website/www/`. The website build injects version data from `website/version.json` into generated output.
1.**MANDATORY SYNTAX & LINT CHECKS**: Before staging, committing, or pushing any changes, you **MUST** run both checks on every modified JavaScript file:
- **Syntax Validation**: Run `node -c` on every single modified JavaScript file (e.g., `node -c extension/background.js` and `node -c extension/content.js`). **NEVER** commit or push code that fails this check.
- **ESLint Validation**: Run `npm run lint` (or `npx eslint .`). The output must show **zero errors and zero warnings**. ESLint is configured to catch undefined variables, unused vars, unreachable code, and other semantic issues. **NEVER** commit or push code that fails this check.
2. Commit all verified code changes and push to `main`.
3. Create and push a new tag. **MANDATORY**: Tags MUST start with a `v` (e.g., `v1.4.0`). The GitHub Actions release workflow is strictly configured to ignore any tags without the `v` prefix.
- **🚫 TAG IMMUTABILITY**: Once a tag is pushed to `origin`, it is **PERMANENT**. You MUST **NEVER** reuse, move, or force-push an existing tag — not even to "fix" a mistake. If a release is missing a fix, increment the version and create a **new** tag (e.g., `v1.7.0` → `v1.7.1`). Tags are immutable identifiers; moving them breaks CI pipelines, corrupts the release history, and causes unreproducible builds.
- **🚫 WHEN NOT TO TAG**: Do NOT create a release tag for changes that do NOT affect the shipped extension or server artifacts. Website text changes, documentation updates (`.md` files), and landing page content do NOT require a version tag. Tags trigger the full CI pipeline (Docker build, extension packaging, GitHub Release) — running this for a typo fix wastes CI resources and creates meaningless releases. Only tag when extension code (`extension/`), server code (`server/`), or shared protocol constants (`shared/`) have changed.
4. The CI will extract the version from the tag (e.g., `v1.4.0` → `1.4.0`), inject it into all source files, build the extension artifacts, publish the Docker image, and create a GitHub Release.
5. Verify the release builds on GitHub Actions.
### 🚫 Force Push Policy
> [!CAUTION]
> **Force pushing (`git push --force` or `git push -f`) is FORBIDDEN without explicit user confirmation.**
> - If a push is rejected due to a non-fast-forward conflict, you **MUST** run `git pull --rebase` first.
> - If a force push is absolutely required (e.g., squashed history, amended commits), you **MUST** ask the user for explicit permission with a clear explanation of why it's necessary. Never force-push autonomously.
> - This applies to both branches (`main`) and **tags** (see Tag Immutability above). Force-pushing tags is doubly destructive. Never do it.
### Adding a Protocol Event
1. Add the event name to `shared/constants.js`.
2. Run the build script (`node scripts/build-extension.cjs`).
3. Implement the handler in `server/index.js` and `background.js`.
### Making Website Changes
1. Edit source files in `website/` (`template.html`, `style.css`, `styles/*.css`, `app.js`, `lang-init.js`, or `locales/*.json`).
2. Run the compiler: `node website/build.cjs`. This generates the multilingual pages in `www/` and minifies CSS/JS.
3. Verify the sources and generated contract: `node --check website/app.js`, `node --check website/lang-init.js`, `node scripts/test-website-locales.mjs`, and `node scripts/test-website-theme.mjs`.
4. Test locally: `npx serve website/www` or `python3 -m http.server 8080 -d website/www`.
5. Commit the source changes only. `website/www/` is generated and gitignored.
### Testing Locally
1. Run the build script: `node scripts/build-extension.cjs`.
2. Load `dist/chrome/` as an "Unpacked Extension" in Chrome (or `dist/firefox/` in Firefox).
3. Start the server from the root: `docker compose up --build`.
4. Use **different browser profiles** or vendors to test multi-peer logic.
5. Use the **Status tab** to verify real-time connection state, logs, and video element metadata.
### Locking Old Versions
1. Update `MIN_VERSION` in the server's `.env` file to the minimum acceptable version.
2. Restart the server. Older extensions will be rejected with a "Version too old" error.
This document describes the communication flows and internal logic of the KoalaSync system.
## 1. Extension Connection (Lazy Connect)
- **Initialization**: On startup, `background.js` reads settings (Server URL, Username, Last Room) from `chrome.storage.sync`. No WebSocket connection is established at this point.
- **On-Demand Connection**: The extension only connects when needed — either the user opens the popup with saved room credentials, or when actively in a room. When not in a room, no connection exists. This improves privacy (IP not exposed while idle) and reduces battery/network usage.
- **WebSocket Handshake (when connecting)**:
1. Background creates a `new WebSocket` to `/socket.io/?EIO=4&transport=websocket&version=1.0.0`.
2. Server performs security checks:
- **IP Rate Limit**: Checks if the IP has exceeded connection limits.
- **Protocol Version**: Client must match the server's protocol (currently `1.0.0`).
3. Server responds with Engine.IO handshake (`0`) and the client joins the namespace (`40`).
- **Deduplication**: If a user joins with a `peerId` that already has an active socket, the server kills the old socket to prevent "Ghost Peers". Deduplication re-validates after acquiring the room creation lock to avoid kicking the wrong socket during concurrent joins.
## 2. Media Event Synchronization
When a user interacts with a video:
1.**Detection**: `content.js` listens to native events (`play`, `pause`, `seeked`) on the `<video>` element, including videos inside Shadow DOM (YouTube, Netflix, etc.).
2.**Prevention of Loops**: Uses an `expectedEvents` Set to distinguish between user actions and programmatic actions. Expected events are consumed on match and expire via timeout. Timeout IDs are cleaned up immediately to prevent memory leaks.
3.**Reporting**: `content.js` sends a `CONTENT_EVENT` to `background.js`.
4.**Relay**: The Server forwards the event to all other peers in the room.
5.**Execution**: Remote peers receive the command via `SERVER_COMMAND` (which includes the original `senderId` for correct ACK routing) and call `video.play()`, `video.pause()`, or `video.currentTime = targetTime`.
6.**ACK Routing**: `content.js` echoes the `commandSenderId` back in `CMD_ACK`, ensuring the `EVENT_ACK` is routed to the correct initiating peer even when multiple commands arrive concurrently.
## 3. Two-Phase Force Sync
Ensures all peers are buffered and synchronized before resuming:
1.**Prepare**: Initiator sends `FORCE_SYNC_PREPARE` with the target timestamp.
2.**Buffer**: Peers seek and pause. Once buffered (`readyState >= 3`), they send a `FORCE_SYNC_ACK`. (Note: `content.js` limits polling to 8000ms).
3.**Execute**: Once the Initiator collects ACKs (or after an 8.5s timeout), they send `FORCE_SYNC_EXECUTE`.
> [!IMPORTANT]
> **Network Transit Buffer Rule**: The orchestrator (`background.js`) must always use a timeout at least 500ms longer than the worker (`content.js`) to account for IPC and network transit time. Never align them exactly 1:1, as this will introduce a race condition on slow connections.
4.**Resume**: All peers call `play()` simultaneously.
## 4. Episode Auto-Sync
Maintains continuous synchronized viewing when watching series:
1.**Detection**: `content.js` monitors the Media Session API for title changes.
2.**Lobby Creation**: When a new title is detected, the peer initiates an `EPISODE_LOBBY` and broadcasts the new title.
3.**Wait State**: All peers freeze their video until they have also loaded the exact same title.
4.**Mid-Lobby Joins**: If a new user joins the room during an active lobby, the lobby initiator broadcasts the active lobby state so the newcomer can sync up.
5.**Resume**: Once all peers report `EPISODE_READY`, the lobby is resolved and playback resumes perfectly.
## 5. Peer Lifecycle & Dual Heartbeat
To maintain a clean room state and eliminate "Ghost Peers":
- **Session Heartbeat (Background)**: Every 30 seconds, `background.js` sends an "I'm alive" signal to the server. This keeps you in the room even if no video is playing.
- **Video Heartbeat (Content)**: Every 15 seconds, `content.js` sends current playback metadata (time, title, state) if a video is found.
- **Server Pruning**: The server runs a "Reaper" every 2 minutes. If a peer has sent **zero** activity (no events and no heartbeats) for 5 minutes, they are forcefully disconnected.
- **Immediate Cleanup**: Rooms are deleted instantly when the last peer leaves or disconnects.
- **Reconnect Strategy (while in room)**: Aggressive backoff — 500ms base, 1.5x multiplier, capped at 5s. Max 20 attempts before marking as failed. Events are queued during disconnect and flushed after namespace rejoin. When not in a room, no reconnection occurs.
> [!CAUTION]
> **Identity Rule**: Differentiate between `peerId` and `socket.id`. Use `socket.id` exclusively for ephemeral transport routing on the server. Use `peerId` exclusively for identity, state management, and room tracking across the stack.
## 6. Broadcast Protocol & Routing
KoalaSync uses a megaphone routing approach to minimize server logic:
- **`emit()` Broadcast Behavior**: Any `emit()` from the extension client is unconditionally broadcast to **all other peers in the room**. It is not a direct message.
- **Storm Prevention**: When dispatching state updates in response to a new user joining (e.g., an active lobby state), ensure ONLY the initiator (or a designated leader) calls `emit()` to prevent $O(N)$ broadcast storms.
## 7. Security & Stability
- **Service Worker Lifecycle**: Uses `chrome.alarms` (30s interval) to prevent the Manifest V3 service worker from suspending while in an active room. On wake, runtime state is restored from `chrome.storage.session` via `ensureState()`.
- **Reconnect Visualization**: Badge shows "..." (orange) during reconnect. Popup displays "Reconnecting..." with attempt counter.
- **Rate Limiting**: Server-side per-socket and per-IP rate limits to prevent sync-spamming or simple DoS. Public health endpoints are limited to 10 requests/minute/IP and cached server-side for 60 seconds, wrong admin-metrics bearer attempts to 5 requests/minute/IP, and room discovery to one request every 10 seconds per socket. Real client IP is taken from the trusted reverse proxy hop, so the Node port must stay private behind Caddy or another trusted proxy.
- **Room Creation Lock**: Per-room mutex prevents race conditions when multiple peers join a new room simultaneously.
- **CORS**: Allows `chrome-extension://` origins for WebSocket fallback compatibility.
- **Message Buffer**: `maxHttpBufferSize` set to 4KB to accommodate large `JOIN_ROOM` payloads.
- **Process Guards**: `uncaughtException` and `unhandledRejection` handlers prevent silent server crashes.
- **Noise Filtering**: Uses a curated blacklist of domains (Search Engines, Social Media) to declutter the "Target Tab" selector in the popup.
- **Diagnostics**: A "Dev" tab provides real-time access to the underlying `<video>` state (`readyState`, `paused`, `currentTime`) for easier troubleshooting.
## 8. Constant Synchronization & Consistency
To maintain a "Single Source of Truth" across the server and extension without using a bundler:
- **Relay Server & Extension Modules**: `background.js` and `popup.js` import constants directly from `shared/constants.js`.
- **Content Scripts**: To ensure zero-latency execution, `content.js` uses a synchronized copy of `EVENTS` and constants.
- **Automation**: The `npm run build:extension` script automatically injects `EVENTS`, `HEARTBEAT_INTERVAL`, and `episode-utils.js` functions into `content.js` during the build process, eliminating the risk of manual mirror mismatch.
- **Verification**: Any protocol change is automatically propagated across the stack by running the build script.
All notable changes to the KoalaSync browser extension and relay server.
---
## [v2.6.3] — 2026-07-15
### Fixed
- **Extension: Host-access recovery races** — Prevents stale permission grants, reinjection retries, closed tabs, and rapid target changes from reactivating or clearing the wrong video tab.
- **Extension: Cross-browser host patterns** — Uses Firefox-compatible match patterns for local servers while preserving exact Chromium port scopes.
- **Extension: Force-sync target integrity** — Keeps sampled playback time and acknowledgements bound to the selected tab during recovery and target changes.
## [v2.6.2] — 2026-07-15
### Added
- **Website: Site-access recovery guide** — Added an English help page and a localized banner across every landing-page language.
### Fixed
- **Extension: Withheld website access recovery** — Detects browser-withheld host access, shows a localized allow-access action, requests only the selected website origin, and resumes the selected tab after access is granted.
- **Extension: Target-tab reliability** — Hardened permission recovery against rapid tab changes, navigation, closed tabs, service-worker restoration, and stale pending state.
- **Browser compatibility** — Uses Chrome's toolbar access request only when available, with a direct user-gesture permission fallback for Firefox and older Chromium browsers.
---
## [v2.6.1] — 2026-07-15
### Fixed
- **Extension: Episode Lobby peer list** — Peer names in the lobby are now rendered as text instead of markup. A peer could previously put HTML in their username and have it rendered in everyone else's popup, which allowed loading remote images (leaking viewer IP addresses) and spoofing the readiness badges. Scripts were already blocked by the extension's content security policy.
### Changed
- **Release checks: AMO validation** — `npm run verify` now runs Mozilla's `addons-linter` against the built Firefox artifact with `--warnings-as-errors`, and ESLint enforces `no-unsanitized`, so upload-blocking issues surface locally instead of at submission time.
---
## [v2.6.0] — 2026-07-15
### Added
- **Extension: Appearance controls** — Added localized system, light, and dark theme options with an early theme initializer to avoid flashes during popup startup.
### Changed
- **Extension: Popup and settings redesign** — Unified controls, status surfaces, colors, icons, badges, and accessibility behavior; settings are now organized into mutually exclusive accordion groups.
### Fixed
- **Extension: Episode Lobby reliability** — Prevented the lobby from remaining stuck in a loading state and stopped episode transitions from triggering on non-episodic media.
- **Extension: Tab-title normalization** — Notification counters such as `(14)`, `[7]`, and `(99+)` are removed reliably without stripping legitimate large numeric titles.
---
## [v2.5.4] — 2026-07-08
### Changed
- **Extension: Refined clutter blacklist** — Removed `localhost` and cloud storage providers (`drive.google.com`, `dropbox.com`, `onedrive.live.com`, `icloud.com`) from the domains blacklist so that local web development servers and tabs hosting video files in cloud storage are no longer hidden under "Hide Clutter Tabs".
---
## [v2.5.3] — 2026-07-02
### Fixed
- **Extension: Disney+ force sync and seeking** — Fixed force sync on Disney+ failing or jumping to wrong positions. The extension now relies solely on accurate player-API time and fails cleanly when it isn't available yet, instead of falling back to unreliable raw video data.
- **Extension: Force-sync accuracy** — When syncing to the group, peers whose current position isn't known yet (e.g. a Disney+ peer that just loaded) no longer pull the sync target toward the start of the video.
- **Extension: Disney+ Host Control Mode** — Regular Disney+ content is no longer misclassified as a live stream, which had silently disabled the desync dialog and snap-back for guests in host-controlled rooms. YouTube and Twitch live detection is unchanged.
- **Extension: Disney+ episode auto-sync** — Episode transitions and the "waiting for peers" lobby flow now work reliably on Disney+ again.
---
## [v2.5.2] — 2026-07-02
### Added
- **Extension: Privacy title controls** - Advanced users can now disable sending browser tab titles separately from media titles. Media titles can still be sent in full, reduced to detected episode identifiers such as `S01E04`, or hidden entirely. Defaults remain full titles for backwards compatibility.
- **Relay: Cleaner restart handling** — Connected clients are now disconnected explicitly during relay shutdown so reconnects recover more predictably.
- **Relay: Stronger abuse protection** — Rapid room-leave spam is now rate-limited.
- **Extension: Hidden remote seek diagnostics** — KoalaDev can use the hidden Dev tab to simulate remote seeks and inspect precise native/page-API timing while debugging playback integrations.
### Changed
- **Extension: Shared page-API seek bridge** — Netflix and Disney+ now use a common page-level seek bridge so private player APIs can be invoked from the page context while the default HTML5 path stays unchanged.
- **Build: Release build timestamp** — Extension builds now inject a build timestamp into the hidden Dev tab for easier local package verification.
### Fixed
- **Extension: Disney+ precise sync** — Disney+ now reads time and seeks through the real page media-player API, and the temporary DOM timeline/button scraping fallback has been removed.
- **Extension: Netflix seek reliability** — Netflix seeking keeps using the page player API with a safer session lookup path.
- **Extension: Tab-title counter cleanup** — Leading browser notification counters such as `(14)` or `[7]` are removed from shared tab titles and matching logic without changing the existing privacy controls.
- **Extension: Tab navigation reinjection** — Reinjecting the content script after selected-tab navigation now uses the same page-API-aware injection path.
---
## [v2.5.0] — 2026-06-29
### Added
- **Extension + Relay: Host Control Mode** — Room owners can now switch a room between open playback control and host-controlled playback. In host-only mode, guests stay synchronized but their local play, pause, and seek actions are not rebroadcast to the room.
- **Backward-compatible Host Control rollout** — The extension only shows Host Control when the connected relay supports it, so users on older self-hosted servers do not see controls that cannot work yet.
- **Extension: Clear host and guest states** — The popup shows the current control mode, host status, peer roles, and localized guest guidance so participants understand when playback is controlled by the host.
- **Website: FAQ clarification for streaming access** — The landing page and FAQ structured data now state clearly that KoalaSync does not stream, host, share, or bypass access to video content. Every participant watches locally and needs their own access to services such as Netflix.
### Changed
- **Playback sync now follows the room's control setting** — When Host Control is enabled, only the host can drive room-wide playback changes; guests can still watch in sync without accidentally changing playback for everyone.
---
## [v2.4.6] — 2026-06-23
### Fixed
- **Room and settings are no longer stored in `chrome.storage.sync`** — Room ID, password, and username were being resurrected from synced storage on a fresh install (sync survives an uninstall in the user's Google account), which made the extension silently auto-connect to a dead room and appear permanently connected. `getSettings()` and all settings reads are now local-only, and legacy keys are actively purged from sync on install/update/startup. Only `onboardingComplete` and `dismissedHints` remain in sync.
- **No server traffic while alone in a room** — When you are the only peer, heartbeats, force-sync, and episode auto-sync are now fully suppressed (previously the keepAlive heartbeat, force-sync, and episode lobby were still broadcast to an empty room). The solo state is re-evaluated live on every event — never cached — so the instant another peer joins, syncing resumes immediately, including an instant state push so the newcomer sees your current position without waiting for the next heartbeat.
## [v2.4.4] — 2026-06-23
### Changed
- **Server: Event rate limit raised 30 → 50 per 10s**, and all connection/event/health rate-limit thresholds and windows extracted into named constants.
- **Extension: Reconnect backoff tuned and jittered** — capped at ~8 attempts/60s (under the per-IP connection limit) with ±20% jitter to de-synchronize reconnect herds after a server blip.
- **CI: Added a verification workflow** running lint, tests, audits, and builds on every push/PR; the release build now uses `npm ci`.
### Fixed
- **Extension: Offline event-queue flush is now paced** (small batches instead of one synchronous burst) so a reconnect after a long outage no longer trips the server event limit and gets disconnected on rejoin.
- **Extension: Ping liveness tolerates one missed PONG** — a reconnect is forced only after 2 consecutive misses (~20s) instead of a single 5s timeout, avoiding spurious drops under transient load.
- **Extension: `socket.send()` failures are caught and re-queued** instead of losing the event on a disconnect race.
## [v2.4.3] — 2026-06-19
### Added
- **Two new languages: Ukrainian (`uk`) and Chinese (`zh`, Simplified)** — added across the extension (UI strings + Chrome `_locales`) and the website (localized pages, hreflang/Open Graph/schema tags, language selector), bringing the total to 15 languages.
### Changed
- **Play/pause sync coalescing** — The content script now collapses rapid bursts of native play/pause events (source swaps, ABR/quality switches, ad transitions, page teardown) into a single relayed command: the first event is sent instantly and a short 150ms window absorbs the rest. This cuts redundant relay traffic and stops bursts from tripping the server's per-socket event rate limit.
### Fixed
- **zh/uk translation quality** — Corrected systematic machine-translation word-sense errors in the two new locales (e.g. "Play", "Status", "Leave Room", "Clear", "Open", "peers", and audio compressor terms) and translated the remaining English leftovers.
- **Relay logging** — An `EVENT_ACK` aimed at a peer that already left is now logged quietly instead of as a `[SECURITY]` cross-room event, so genuine cross-room attempts stand out in the logs.
## [v2.4.2] — 2026-06-19
### Changed
- **Extension: Optimized uninstall URL registration** — Extracted registration into a reusable, race-condition-protected `initUninstallURL()` helper. It registers the uninstall feedback URL with browser context on both extension installation/update and browser startup to prevent state loss, without storing or sending an installation token.
## [v2.4.1] — 2026-06-19
### Added
- **Extension: Onboarding tour now has a closing step** — The first-run tour ends on a dedicated "You're all set!" card (the `ONBOARDING_5` copy that already existed in all 13 locales but was never shown). The tour no longer stops abruptly on the username step.
- **Extension: One-click invite from the empty peer list** — The "No peers yet" state now shows a **📋 Invite Link** button that copies the invite link to the clipboard, so users can share it without hunting for the field.
### Changed
- **Extension: Cleaner onboarding welcome** — Step 1 is now a centered welcome card instead of spotlighting the logo title. Added a guard so target-less tour steps center cleanly.
- **Website: Mobile comparison table** — The KoalaSync vs Teleparty table stacks into per-feature cards on phones instead of forcing horizontal scrolling; feature descriptions are shown again on mobile.
### Fixed
- **Extension: Onboarding step counter/progress placeholders** — Static `Step 1 of 3` / 33% fallbacks in `popup.html` corrected to match the actual 5-step tour (`Step 1 of 5` / 20%).
- **Website: Mobile navigation restored** — The header hamburger menu was hidden by a `display:none !important` rule, leaving the nav links unreachable on phones. Re-enabled, with spacing kept comfortable down to ~320px.
- **Website: Hero alignment on mobile** — A fixed-width extension mockup forced the hero grid column wider than the container, shifting all hero content off-center (larger left margin than right). The mockup is now responsive (`width:100%/max-width` + `minmax(0,1fr)` grid track).
- **Website: Reveal-animation fallback** — Added a `<noscript>` style fallback and `IntersectionObserver` feature guards so scroll-revealed content can never stay invisible if JavaScript is disabled or unsupported.
## [v2.4.0] — 2026-06-16
### Added
- **Extension: Lazy WebSocket connection** — The extension no longer maintains a permanent WebSocket connection to the relay server. Instead, the connection is established only when actively in a room or when the popup is opened with a saved room configuration. This improves privacy (IP is not exposed while idle), reduces battery/network usage, and prevents the server from tracking online status of inactive users. Automatic reconnect is guaranteed while in a room — zero behavior change during active sync sessions. See `connectIntent` flag in `background.js`.
- **Extension: Episode title regex unification** — `extractEpisodeId()` had inconsistent regex patterns between `background.js` and `content.js`. The content script correctly matched Crunchyroll-style separators (`S01/E01`) while the service worker's stricter pattern (`[\s\-\.]*`) silently rejected them, causing episode lobby sync failures. Now unified to `[^a-zA-Z0-9]*` via shared `episode-utils.js`.
- **Unit tests: `rate-limiter` and `episode-utils`** — 12 test groups for rate-limit functions and 30+ assertions for episode title parsing, covering all 6 separator types (dash, dot, slash, colon, comma, space). Run automatically via `npm run verify`.
### Changed
- **Server: Rate limiter extracted to `rate-limiter.js`** — 6 rate-limit functions, all rate-limit Maps, and cleanup intervals moved from `index.js` (149 lines). `index.js` now imports via facade pattern with re-exports for backward compatibility.
- **Extension: Episode utilities extracted to `episode-utils.js`** — `extractEpisodeId()` and `sameEpisode()` deduplicated from `background.js` and `content.js`. The shared module is imported as an ES module by the service worker and injected into the content script IIFE by the build script.
- **Build: `"type": "module"` in root `package.json`** — All scripts standardized to ESM (`.mjs`) or explicitly CommonJS (`.cjs`). Eliminated Node.js `MODULE_TYPELESS_PACKAGE_JSON` warnings.
- **Server: npm audit resolved** — `ws` package vulnerability (CVE-2024-37890) fixed. Zero vulnerabilities in production dependencies.
- **Pop-up: Connection status flicker fixed** — Removed hardcoded `disconnected` state on every pop-up open. Status now reflects actual background state from the first frame.
- **Pop-up: Join button timeout improved** — No longer blindly re-enables after 15s. Polls connection status and extends window if still connecting.
- **Pop-up: Validation failure state cleanup** — Custom server URL validation errors now properly reset `isProcessingConnection` and `joinBtnTimeout`.
- **Extension: `WEB_JOIN_REQUEST` channel leak fixed** — Missing `sendResponse()` call when already in the target room.
- **Extension: `LEAVE_ROOM` now clears `roomId` from storage** — Prevents phantom auto-reconnect on browser restart after explicit leave.
- **Extension: Reconnect attempt counters reset on leave** — Prevents stale `reconnecting` status display after intentional disconnect.
## [v2.3.2] — 2026-06-16
### Changed
- **Extension: Refined Spanish, Italian, and Portuguese translations**: Complete manual review and improvement of all Spanish (`es`), Italian (`it`), Portuguese — Brazil (`pt-BR`), and Portuguese — Portugal (`pt`) locale files for both the extension UI and the landing website. Thanks to [@Kaia-Alenia](https://github.com/Kaia-Alenia) for the native-quality translations.
### Fixed
- **Extension: Locale typos and corrupted characters fixed**: Repaired a Korean refresh button label (`Refreschi` → `새로고침`), a corrupted Korean connection status string (`연kel` → `연결`), a Korean character contaminating a Japanese string (`의` → `の`), and a Dutch typo (`cmmuniceren` → `communiceren`).
- **Server: Admin token length leak fixed (timing side-channel)**: `isAdminMetricsAuthorized()` returned early when the provided buffer had a different length than the expected token, leaking the token length via response timing. Now `crypto.timingSafeEqual` runs in constant time on every attempt regardless of length match. Reported by [@Kaia-Alenia](https://github.com/Kaia-Alenia).
- **Server: Smart unhandled rejection handling (exits after 5/min instead of 1)**
- **Server: Optimized admin health metrics allocation**
---
## [v2.3.0] — 2026-06-14
### Added
- **Extension: New Interactive Onboarding Tour**: A fully redesigned, interactive step-by-step onboarding experience.
- **Extension: Auto-Switch to Sync Tab**: The UI now intelligently switches to the Sync tab when you join a room to guide video selection.
- **Extension: Uninstall URL Integration**: Prepared an uninstall URL setup that works natively across Chrome and Firefox, cleanly attaching browser context for analytics.
### Fixed
- **Extension: Infinite Seek Loop Prevention**: Replaced the fragile time-based seek suppression with an exact target-time verification mechanism, entirely eliminating infinite seek loops on slow buffers.
- **Extension: Zombie Connections Resolved**: Implemented a forced disconnect upon ping timeouts, ensuring the extension reliably auto-reconnects when the WebSocket hangs in a half-open state.
- **Extension: Room Switching Architecture**: Joining a new room while already connected now explicitly severs the old connection first, preventing state cross-contamination.
- **Extension: Join/Leave Race Conditions**: Added UI locks to prevent users from accidentally sending conflicting connection commands via rapid double-clicking.
- **Extension: Same-Room Invite Bypass**: Clicking an invite link for the room you are currently in no longer triggers a redundant reconnect, instead instantly confirming the join.
- **Extension: Audio settings now propagate immediately to video tabs**: Changes made in the audio options page are now instantly applied to the active video tab. Previously, settings saved to `chrome.storage.local` were not picked up by the background listener, which only watched `chrome.storage.sync`.
- **Extension: Audio compressor now logs enable/disable state and resume failures**: The compressor reports when it is activated or bypassed, and warns if the `AudioContext` cannot be resumed (e.g. browser autoplay policy requires a user gesture on the page first).
- **Extension: Video heartbeat no longer sent when alone in a room**: The full media metadata `PEER_STATUS` is now only emitted when other peers are present. The session keepalive (background heartbeat) continues to run unaffected, preventing the server reaper from disconnecting idle peers.
- **Server: Increased `failedAuthAttempts` eviction threshold from 50k to 200k**: Reduces frequency of expensive batch evictions under high auth-failure volumes, smoothing heap usage.
---
## [v2.2.4] — 2026-06-10
### Fixed
- **Extension: Error notifications now respect `browserNotifications` setting**: Server error events (e.g. "Server is restarting") no longer trigger a browser notification when the user has disabled notifications in the extension settings.
- **Server: Misleading reconnect message corrected**: The graceful shutdown message no longer tells users to manually reconnect — the extension handles this automatically.
---
## [v2.2.3] — 2026-06-10
### Added
- **Artifact Attestations (Supply Chain Security)**: All release artifacts (Docker images, extension ZIPs) are now published with signed [SLSA provenance attestations](https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations) via `actions/attest@v4`. Anyone can verify that an artifact was built from this repository using `gh attestation verify`.
- **Admin health: `rateLimits.denied` counters**: New rolling counters track actual rate-limit denials (429 responses), separate from `rateLimits.trackedClients` which reports unique IPs in the tracking window.
- **Docker HEALTHCHECK**: Container health is now checked every 30s via `GET /health`.
- **`npm start` script**: Server can now be started with `npm start`.
### Fixed
- **Server: `activeLobby` no longer silently overwritten**: If a second peer sends `EPISODE_LOBBY` while a lobby is already active, the request is now ignored instead of destroying the first peer's lobby.
- **CORS log sanitization**: Rejected origin headers are sanitized (`\r\n` stripped) to prevent log injection.
- **Extension: pagehide resource leak**: `keepAlivePort`, `lobbyPollTimer`, heartbeats, and `MutationObserver` are now properly cleaned up when a tab is hidden or enters bfcache.
- **Extension: unhandled storage rejections**: `chrome.storage.session.set()` calls in the disconnect handler now have `.catch(() => {})`.
- **`'Pixel'` duplicate in name generator**: Second occurrence replaced with `'Nitro'` for better name diversity.
- **`'opposum'` typo**: Corrected to `'opossum'` in the emoji map and added `'Opossum'` to `USERNAME_NOUNS`.
- **Test reliability**: `test-server-routes.mjs` now sets `ADMIN_METRICS_TOKEN` before importing the server module, fixing standalone test execution.
- **`MAX_PEERS_PER_ROOM` konsistent**: `.env` auf 25 gesetzt (wie `.env.example`).
- **Admin health: `rateLimitEntries` renamed to `rateLimits.trackedClients`**: The field now accurately describes that it tracks unique clients in the rate-limit window, not denial counts. Update your json_exporter/Grafana config accordingly.
- **README restructured**: Sections reordered by progressive technical depth. New "Supply Chain Security" subsection under "For Developers & Self-Hosters" with verification commands.
---
## [v2.2.2] — 2026-06-09
### Added
- **Chrome Web Store i18n Support**: Added `default_locale: "en"` to manifest and created `_locales/*/messages.json` for all 13 supported languages. This unlocks the language selection dropdown in the Chrome Web Store dashboard, allowing translated store listings (title, description) per locale. The extension's own UI translations (`locales/*.json` + `i18n.js`) remain unchanged.
- **Locale test coverage**: Extended `scripts/test-locales.js` to validate all `_locales/*/messages.json` files (correct format, required keys, no duplicates) and verify `default_locale` is set in the manifest.
### Fixed
- **Copy Logs button alignment**: Removed stray `margin-top: 8px` inherited from `.secondary` class that pushed the button 8px down in the connection status row.
---
## [v2.2.1] — 2026-06-09
### Added
- **Server Ping Display**: Measures round-trip latency to the relay server via application-level ping/pong events. The extension sends `PING { t }` every 15 seconds; the server responds with `PONG { t }`. Round-trip time is calculated client-side and displayed in the Status tab, color-coded (<50ms green, 50–150ms yellow, >150ms red). No ping value is shown when disconnected or if the server does not respond within 5 seconds.
- **Peer Ping Response (Future-Proof)**: The extension can now respond to incoming `PING { t, sender }` events from other peers by sending back `PONG { t, target: sender }`. The relay server forwards `PING` to the target peer and routes `PONG` back to the original sender. Both client and server validate that peers are in the same room before forwarding/routing. Peer-to-peer ping initiation will be activated in a future extension update without requiring a server restart.
---
## [v2.2.0] — 2026-06-08
### Added
- **Web Audio API Compressor**: Built-in audio dynamic range compression with four presets (Recommended, Dynamic Range, Vocal Enhancement, Smooth) and fully customizable sliders (threshold, ratio, knee, attack, release). Uses dry/wet crossfade (40ms linear ramp) to avoid clicks. Configured via the new Audio Options page accessible from the Settings tab.
- **Audio Options Page** (`audio-options.html`): Dedicated settings page with master toggle, compressor preset selector, real-time custom sliders, and equalizer placeholder. Dark theme matching the popup design.
- **Feature Hint System**: Generic `dismissedHints` array in sync storage for announcing new features. First hint highlights the Audio Options entry in Settings. Extensible for future features.
### Changed
- **Support Links**: Static footer badges on the Settings and Status tabs linking to the developer's support page. README and website footer updated with a Support KoalaSync badge.
### Fixed
- **Portuguese (PT) locale**: Removed Italian contamination — "sincronizzazione" → "sincronização", "tempo reale" → "tempo real", "Link di Invito" → "Link de Convite", "Sair della Sala" → "Sair da Sala".
- **Korean locale**: Fixed broken character in `HOWTO_STEP_2_TEXT` (`클rip보드` → `클립보드`).
- **Website COMP_FEAT_6_KOALA**: Normalized from inconsistent "6 Languages" to "13 Languages" across all locale files (en, de, es, fr, pt-BR, ru).
- **Debug report showing wrong logs**: Fixed `logs.slice(-50)` and `history.slice(-20)` in the "Copy Debug Report" feature. Since `addLog()` and `addToHistory()` use `unshift` (inserting entries at index 0), the arrays are ordered newest-first. `slice(-N)` took the N **oldest** entries instead of the N **newest**. Changed to `slice(0, N).reverse()` to correctly include the most recent logs and display them chronologically.
---
## [v2.1.2] — 2026-06-06
### Fixed
- **Episode guard regex**: Fixed `isDifferentEpisode()` not detecting episode changes when the MediaSession title uses `Sxx:Exx` format (colon separator, as used by Jellyfin/Emby). The regex character class `[\s\-\.]` was replaced with `[^a-zA-Z0-9]` to match **any** non-alphanumeric separator between season and episode numbers, preventing play/pause/seek commands from a different episode leaking through and incorrectly manipulating a peer's playback.
- **Per-device storage isolation**: Migrated `username`, `roomId`, `password`, `serverUrl`, and `useCustomServer` from `chrome.storage.sync` (synced across Google account) to `chrome.storage.local` (per-device). This prevents the extension from automatically joining the same room with the same name on multiple devices. Existing user data is migrated silently on first run; all preferences (`filterNoise`, `autoSyncNextEpisode`, etc.) remain synced.
### Changed
- Added one-time migration fallback in `getSettings()` and popup `init()` to copy existing user settings from `storage.sync` to `storage.local` on first launch after the update.
---
## [v2.1.0] — 2026-06-04
### Added
- Added full translation support for 7 new languages to both the browser extension popup settings and landing website: Italian (`it`), Polish (`pl`), Turkish (`tr`), Dutch (`nl`), Japanese (`ja`), Korean (`ko`), and European Portuguese (`pt`).
- Implemented robust, centralized browser system language detection mapping `pt-BR` to Brazilian Portuguese and other `pt` locales (like `pt-PT`) automatically to European Portuguese.
- Added flag emojis to language selector dropdowns in both the extension popup and landing/utility web pages for quicker visual identification.
- Added 181 translation keys parity validation suite checks for the new languages.
### Fixed & Hardened (Extension Audit)
- Guarded all website `localStorage` interactions to prevent initialization/join flow script failures on privacy-hardened or cookie-blocked browser configurations.
- Added robust validation null-guards to `chrome.runtime.onMessage` listeners across all extension scripts (`bridge.js`, `content.js`, `background.js`, `popup.js`) to reject unexpected runtime messages.
- Guarded CustomEvent payload destructuring in `bridge.js` to ensure stability when receiving third-party page events.
- Wrapped `video.currentTime` seeking adjustments during forced sync in content scripts with exception handling to absorb uninitialized video state DOMExceptions.
- Added payload validation guards on incoming Socket.IO events within the background script's event handlers to secure against malformed server updates.
- Prevented noisy browser console exceptions from context invalidation in target tabs by catching promise rejections on extension message dispatches.
### Performance
- Implemented in-memory language dictionary caching in the background script to completely avoid redundant extension package filesystem reads during translations.
---
## [v2.0.8] — 2026-06-03
### Fixed
- Fixed a bug where switching language inside the extension popup overwrote dynamic fields (such as active room ID, connection status, active server details, and video debug info) with default localized placeholder texts.
- Fixed a version reporting mismatch where the copied logs (debug reports) and connection handshake parameters incorrectly reported the hardcoded `1.9.0` version instead of the actual installed manifest version.
---
## [v2.0.7] — 2026-06-03
### Added
- Added a `DEBUG_LOGGING` environment variable to the relay server (defaulting to `"0"` / disabled) to prevent console spam from verbose connection (`CONN`), room activity (`ROOM`, `DEDUPE`), and `CORS` events under load. Critical logs like `SERVER`, `SECURITY`, `AUTH`, and `ERROR` remain enabled at all times.
---
## [v2.0.6] — 2026-06-03
### Performance & Security Hardening
- Optimized failed authentication attempts cache eviction algorithm to $O(1)$ by exploiting Javascript `Map` insertion-order properties. This completely removes the previous array copying and sorting bottleneck, neutralizing a potential main-thread blocking DoS vector under heavy brute-force password traffic.
---
## [v2.0.5] — 2026-06-03
### Security & Hardening
- Hardened extension room idle auto-leave detection to correctly recognize when the target tab's video heartbeat goes stale (e.g., after tab navigation or media closure).
- Exported cleaner graceful shutdown and lifecycle methods (`stopServerForTests`) from the relay server to prevent socket leaks and port-binding conflicts during verify checks.
### Added
- Added a validation step in `test-locales.js` to ensure the supported language list in `extension/i18n.js` is perfectly synchronized with the actual JSON translation files in the locales directory.
- Added a robust route verification test suite (`scripts/test-server-routes.mjs`) covering rate limit throttling, caching headers, and admin metrics access control.
---
## [v2.0.4] — 2026-06-03
### Security & Hardening
- Hardened relay health endpoints against simple flood traffic: `GET /` and `GET /health` are now limited to 10 requests per minute per client IP.
- Added lazy 60-second server-side caching for `GET /`, basic `/health`, and admin `/health` JSON responses to reduce repeated health-check work under noisy polling.
- Added startup warning for short `ADMIN_METRICS_TOKEN` values and documented that production Node ports must stay private behind Caddy or another trusted reverse proxy.
- Lowered the default maximum peers per room to 25.
### Added
- Optional privacy-preserving admin metrics on `/health` when `ADMIN_METRICS_TOKEN` is configured and a valid bearer token is supplied. Metrics are aggregate-only and exclude room IDs, peer IDs, usernames, IP addresses, media titles, passwords, and other user-level data.
### Changed
- Removed `bcryptjs`; temporary room passwords continue to use keyed SHA-256/HMAC hashing as documented.
- Public room discovery is now rate-limited server-side to one refresh every 10 seconds per socket, with the extension refresh button locked for 11 seconds.
### Fixed
- Improved Shadow DOM video detection so real embedded players are not hidden by smaller light-DOM preview or placeholder videos.
- Fixed join-button timeout cleanup after join status responses.
---
## [v2.0.2] — 2026-06-02
### Fixed
- Peer identity spoofing in relay server: client-supplied `peerId` could be used to impersonate other peers in PEER_STATUS events. Server now always stamps `peerId` with the authenticated sender's identity.
- Amazon domain detection: replaced broad `includes('amazon.')` substring check with boundary-safe regex that correctly matches all Amazon storefronts (`amazon.com`, `amazon.de`, `amazon.co.uk`, etc.) while rejecting lookalike domains.
---
## [v2.0.1] — 2026-06-01
### Fixed
- Video detection on Prime Video: `findVideo()` now scores all video elements by size, duration, and mute state instead of picking the first one. Fixes 0×0 placeholder being selected over the actual player.
- History entries in debug report showing `?` instead of action names.
- Prime Video status in compatibility matrix updated to reflect partial support.
### Added
- Multi-video overview table in Copy Debug Report when a page has more than one `<video>` element. Shows resolution, mute state, playback state, readyState, duration, and marks the currently targeted video.
---
## [v2.0.0] — 2026-06-01
### 🌍 Multi-Language Extension (Biggest Feature!)
- **6-Language UI**: The browser extension is now fully translated into **English, German, French, Spanish, Portuguese (Brazilian), and Russian**. Switch languages instantly in Settings without reload.
- **Real-Time i18n**: Every label, button, tooltip, toast notification, empty state, and onboarding guide updates dynamically when the language changes.
### New Features
- **Copy Debug Report (Markdown)**: The *Copy Logs* button in the Status tab now copies a fully formatted Markdown debug report — system info, connection status, video diagnostics, action history, and logs. One click, paste into a GitHub issue, all debugging data ready.
- **Platform Auto-Detection**: The Dev tab now identifies streaming platforms (YouTube, Netflix, Twitch, Prime Video, Disney+, HBO Max, Vimeo, Dailymotion) and displays the detected platform.
- **Enhanced Video Debug Info**: 20+ new fields in the Status tab including network state, buffered ranges, dimensions (with 0×0 warning), media error codes, shadow DOM status, seeking/ended/loop flags, volume, playback speed, and data attributes.
- **No-Video Diagnostic Mode**: When no video is found, the Status tab shows platform, page title, video count, shadow DOM presence, and MediaSession data to help troubleshoot.
- **Larger Popup Logo**: Extension popup icon increased to 48px.
- **Prime Video Unblocked**: Removed `amazon.` from the tab blacklist so Amazon/Prime Video tabs appear in the video selector.
- **Improved Debug Report**: Full User-Agent string for accurate browser identification, UTC timestamp, connection details including server URL and room info.
- **Smart Disconnect**: Improved disconnect handling when leaving rooms.
- **Human-Readable Room IDs**: Expanded word lists for friendlier room names.
- **Custom Server Support**: WEB_JOIN_REQUEST and join button for custom server invite flows.
- **Reconnection Strategy**: Custom server reconnection improvements.
- **Episode-Aware Sync**: Command sequencing with smarter episode transition detection and echo suppression for smoother series binges.
- **Sync Status Refinements**: YouTube and Twitch sync behavior improved.
- **No External Dependencies**: Extension remains dependency-free with no library overhead.
### Fixed
- Hardcoded strings, missing translation keys, and Service Worker notification race conditions.
---
## Versioning Policy
- **MAJOR** (x.0.0): Breaking protocol changes, architecture rewrites, or major feature milestones.
- **MINOR** (0.x.0): New features, significant enhancements, new translations, or UI redesigns.
- **PATCH** (0.0.x): Bug fixes, minor improvements, and documentation updates. PATCH releases may not receive individual changelog entries if bundled with a MINOR release.
This guide walks through the complete user flow of KoalaSync, from creating a room to synchronized playback. It is designed for **store reviewers**, **end-users**, and **manual testers** to understand exactly what happens at each step, what data is sent, and where it goes.
---
## Step 1: Installing the Extension
1. Download the extension from the [Releases](https://github.com/Shik3i/KoalaSync/releases) page (or install from the Chrome Web Store / Firefox Add-ons).
2. The extension adds a small icon to your browser toolbar.
3. On first install, a unique 8-character **Peer ID** is generated locally and stored in `chrome.storage.local`. This ID is never sent to any external service — it only travels to the relay server when you join a room.
> **What's stored locally**: `peerId` (8-char hex), `username` (customizable, defaults to a readable adjective-noun pair), `serverUrl`, `filterNoise` preference. All stored via `chrome.storage.sync` and `chrome.storage.local`.
---
## Step 2: Connecting to the Relay Server
When you open the extension popup (with saved room credentials) or when a saved room configuration exists from a previous session, the background service worker connects to the relay server:
1.**WebSocket Handshake** (on demand): `background.js` opens a WebSocket to `wss://syncserver.koalastuff.net/socket.io/?EIO=4&transport=websocket` only when needed (popup opened or active room).
2.**Security Checks** (server-side):
- The server checks the client's **IP rate limit** (max 10 connections per 60 seconds).
- The server validates the **authentication token** (hardcoded in `shared/constants.js`) to verify this is a legitimate KoalaSync client.
- The server checks the **extension version** against `MIN_VERSION` to reject outdated clients.
3.**Connection Established**: The server responds with an Engine.IO handshake (`0{...}`), followed by a Socket.IO namespace join (`40`). The connection status dot in the popup turns green.
> **Data sent to server**: `token` (authentication), `version` (e.g., `1.3.1`). No personal data is transmitted during connection.
---
## Step 3: Creating a Room
Click **"Create Room"** in the popup's Room tab:
1. The extension generates a random Room ID (e.g., `happy-koala-42`) and a random 6-character password.
2. Room IDs are restricted to `[a-zA-Z0-9-]` (alphanumeric + hyphens only).
3. The extension emits a `JOIN_ROOM` event to the server.
> **Data sent in `JOIN_ROOM`**:
> ```json
> {
> "roomId": "happy-koala-42",
> "password": "x7k2m9",
> "peerId": "a1b2c3d4",
> "username": "MyName",
> "tabTitle": "YouTube - My Video",
> "protocolVersion": "1.0.0"
> }
> ```
4.**Server-side processing**:
- All fields are **sanitized**: `roomId` is stripped of invalid characters and clamped to 64 chars; `peerId` clamped to 16 chars; `password` clamped to 128 chars; `username` clamped to 30 chars.
- The server **hashes the password** with a keyed SHA-256 HMAC and stores only that hash in RAM (the plaintext is never stored).
- A new room object is created in memory with the peer's data.
- The server responds with `ROOM_DATA` containing the list of peers in the room.
5.**Popup updates**: The Room tab switches to the "Active Room" view, showing your Room ID and an invitation link.
---
## Step 4: Sharing an Invitation Link
Click the **📋 Copy** button next to the invite link:
- `serverFlag`: `0` for official server, `1` for custom server.
- `encodedServerUrl`: Only populated if using a custom server.
2. **Important**: The room credentials are in the **URL hash** (`#`), which means they are **never sent to the web server** — the hash fragment stays entirely in the browser. The landing page server never sees your room ID or password.
3. Send this link to your friend via any messaging app.
---
## Step 5: Your Friend Opens the Invitation Link
When your friend opens the link in their browser:
1. **`join.html` loads** on `sync.koalastuff.net`. The page displays "INVITATION DETECTED" with the Room ID.
2. **Extension detection**: The page checks for `document.documentElement.dataset.koalasyncInstalled`, which is set by `bridge.js` (a content script injected only on `sync.koalastuff.net`).
3. **If the extension IS installed**:
- The page shows "Joining room automatically..."
- After 500ms, the page dispatches a `KOALASYNC_JOIN_REQUEST` custom DOM event with `{ roomId, password, useCustomServer, serverUrl }`.
- `bridge.js` catches this event and forwards it to `background.js` via `chrome.runtime.sendMessage`.
- `background.js` stores the credentials in `chrome.storage.sync` and emits `JOIN_ROOM` to the server.
- The server validates the password against the stored keyed SHA-256 HMAC hash.
- On success, the server responds with `ROOM_DATA` and broadcasts `PEER_STATUS { status: 'joined' }` to all existing peers.
- The join page updates to show "✅ Successfully joined!".
4. **If the extension is NOT installed**:
- The page shows download links (Chrome Web Store / GitHub).
- The user installs the extension, returns to the link, and the flow continues from step 3.
---
## Step 6: Selecting a Video Tab
Both users now need to select which browser tab contains the video to sync:
1. Open a video on any website (YouTube, Twitch, Netflix, etc.).
2. In the extension popup → **Sync** tab → use the **"Target Tab"** dropdown.
3. The dropdown lists all open tabs, filtered to exclude noise (search engines, social media — configurable via Settings).
4. Tabs with a **matching video title** are highlighted with a ⭐ prefix for easy identification.
5. Selecting a tab causes `background.js` to set `currentTabId` and inject `content.js` into that tab via `chrome.scripting.executeScript`.
> **What `content.js` does on injection**: Finds the first `<video>` element on the page and attaches event listeners for `play`, `pause`, `seeked`, and `loadeddata`. (Time and volume state are tracked via a 15-second heartbeat interval, not continuous event listeners). It uses an `expectedEvents` Set to distinguish between user actions and programmatic actions (loop prevention).
---
## Step 7: Synchronized Playback
When User A presses **Play** on their video:
1. `content.js` detects the native `play` event on the `<video>` element.
2. It checks the `expectedEvents` Set — if this event was expected (caused by a remote command), it's consumed silently. If not, it's a **user action**.
3. For user actions, `content.js` sends `{ type: 'CONTENT_EVENT', action: 'play', payload: { currentTime, ... } }` to `background.js`.
4. `background.js` adds an `actionTimestamp` and emits the `PLAY` event to the server.
5. **Server relay**: The server sanitizes all fields (strings clamped, numbers validated, booleans type-checked) and constructs a clean `relayPayload` with `senderId` set to User A's `peerId`. The raw client data is never forwarded directly.
6. The server broadcasts the sanitized payload to all other peers in the room.
7. User B's `background.js` receives the `PLAY` event and calls `routeToContent()`, which sends a `SERVER_COMMAND` message to User B's `content.js`.
8. User B's `content.js` adds `'playing'` to its `expectedEvents` Set (so it won't echo the event back), then calls `video.play()`.
> **The same flow applies to Pause and Seek**, with Seek additionally sending `targetTime` for the time position.
---
## Step 8: Force Sync (Two-Phase Protocol)
If videos drift out of sync, either user can click **"Force Sync"**:
### Phase 1 — Prepare
1. The initiator's `content.js` captures the current `video.currentTime` as the `targetTime`.
2. `background.js` emits `FORCE_SYNC_PREPARE` with `{ targetTime }` to all peers.
3. All peers (including the initiator) **pause** their video and **seek** to `targetTime`.
4. Each peer's `content.js` polls `video.readyState` until it reaches `≥ 3` (buffered enough to play), with an 8-second timeout.
5. Once buffered, each peer sends `FORCE_SYNC_ACK` back.
### Phase 2 — Execute
6. Once all ACKs are received (or after 8.5 seconds), the initiator emits `FORCE_SYNC_EXECUTE`.
7. All peers call `video.play()` simultaneously, achieving synchronized playback.
> **Why two phases?** Without buffering confirmation, peers with slower connections would start playing before they've loaded the target timestamp, causing immediate desync.
---
## Step 9: Heartbeat & Peer Health
While in a room, two heartbeats keep the session alive:
| Heartbeat | Interval | Source | Purpose |
|:----------|:---------|:-------|:--------|
| **Background** | 30 seconds | `background.js` | While connected, signals "I'm still connected" and triggers automatic reconnect (500ms base, max 5s). No heartbeats fire when idle (lazy connect). |
- **Server Reaper**: Every 2 minutes, the server checks for peers with no activity for 5+ minutes and disconnects them ("dead peer pruning").
- **Room Cleanup**: Empty rooms are deleted immediately. Inactive rooms are pruned after 2 hours.
---
## Step 10: Leaving a Room
When a user clicks **"Leave"** or closes their browser:
1. `background.js` emits `LEAVE_ROOM` (or the WebSocket `disconnect` fires automatically).
2. The server calls `removePeerFromRoom()`, which:
- Removes the peer from the room's `peers` Set, `peerIds` Map, and `peerData` Map.
- Removes the socket from the global `socketToRoom` and `peerToSocket` maps.
- Broadcasts `PEER_STATUS { status: 'left' }` to remaining peers.
- If the room is now empty, **deletes the room entirely** — no data persists.
3. The event rate-limit counter for that socket is also cleaned up.
> **After disconnect, zero data about the user remains on the server.** There is no database, no log file, no analytics record. The session existed only in RAM and is now gone.
---
## Episode Auto-Sync Flow
When watching a series and an episode ends:
1. `content.js` monitors the [Media Session API](https://developer.mozilla.org/en-US/docs/Web/API/Media_Session_API) for title changes.
2. When a new title is detected, the peer broadcasts `EPISODE_LOBBY` with the expected title after applying the local media title privacy setting. In episode-only mode this is an identifier such as `S01E04`; when media titles are not sent, the client does not create a new episode lobby.
3. All peers' videos freeze. The UI shows an "Episode Lobby" card with peer readiness status.
4. Each peer's `content.js` polls for the new title to appear in the `<video>` element's metadata.
5. Once a peer detects the matching title, they send `EPISODE_READY`.
6. When all peers report ready, the lobby resolves and playback resumes simultaneously.
│ Extension │ ←─────────────────→│ Relay Server │←──────────────────→│ Extension │
│ (User A) │ JOIN_ROOM │ (RAM only) │ JOIN_ROOM │ (User B) │
│ │ PLAY/PAUSE/SEEK │ │ PLAY/PAUSE/SEEK │ │
│ │ FORCE_SYNC_* │ Sanitizes & │ FORCE_SYNC_* │ │
│ │ PEER_STATUS │ relays only │ PEER_STATUS │ │
│ │ EPISODE_* │ │ EPISODE_* │ │
└──────┬──────┘ └───────────────┘ └──────┬──────┘
│ │
┌────┴─────┐ ┌─────┴────┐
│ content │ Listens to <video> events │ content │
│ .js │ Controls playback │ .js │
└──────────┘ └──────────┘
```
> **The relay server is a pure message forwarder.** It never interprets video content, accesses URLs, or stores session history. All media control happens locally inside each user's browser via the `<video>` DOM API.
**KoalaSync does not collect, store, or sell any personal data.**
*We don't track you. We only track our server* (relying exclusively on aggregated, anonymous, and non-personal system metrics to monitor performance and stability).
KoalaSync is designed with a **Security-First & Volatile** architecture. This means we prioritize keeping your data out of persistent storage, though certain technical data must be processed temporarily to ensure service stability and security.
## 1. Data Processing (In-Memory Only)
KoalaSync does not use a database. All active session data exists only in the server's RAM and is purged immediately when no longer needed.
- **Session Data**: To synchronize playback, the server must temporarily hold your `peerId` and `username`. By default, KoalaSync also shares the selected tab title and media title with the room so peers can identify matching videos and coordinate episode transitions. Privacy Settings let you disable sending the tab title separately, and choose whether media titles are sent in full, reduced to a detected episode identifier (for example `S01E04`), or not sent. Playback metadata (`playbackState`, `currentTime`, `volume`, `muted`) is held per peer for the duration of the session. All of this is deleted as soon as you leave the room.
- **Room Passwords**: If you set a room password, it is stored only as an in-memory **keyed SHA-256 HMAC hash**. The server receives the plaintext password only during join validation, never stores it, and keeps only the hash for the short room lifetime.
- **Routing Maps**: The server maintains ephemeral lookup tables (`socketToRoom`, `peerToSocket`) to route messages between peers. These contain only transport identifiers and are purged on disconnect.
### Data Retention
| Data Type | Maximum Retention | Trigger for Deletion |
To prevent abuse and brute-force attacks, the following data is processed:
- **Brute-Force Protection**: If multiple failed password attempts are detected, the server stores the `IP address` and `Room ID` in a temporary RAM-based lockout list for a maximum of 15 minutes.
- **Connection Rate Limiting**: IP addresses are tracked for 60 seconds to prevent connection-flooding (DoS) attacks.
- **Event Rate Limiting**: Per-socket event counters are tracked for 10-second windows to prevent event-spamming. These are keyed by ephemeral socket IDs and cleaned up periodically.
- **Console Logging**: The official relay server (`syncserver.koalastuff.net`) outputs connection events (including IP addresses) to the server console for real-time monitoring. These logs are ephemeral and are not archived, sold, or linked to any persistent user identity.
## 3. Extension Permissions
The browser extension requires the following permissions:
-`storage`: To remember your local preferences (username, server URL, room settings).
-`tabs` & `scripting`: To detect and control video elements on the pages you choose to sync.
-`<all_urls>` (host permission): Required to detect `<video>` elements on any website the user chooses to synchronize. The extension only activates on the specific tab the user has actively selected — it does not scan, monitor, or interact with any other tabs or pages.
-`alarms`: To keep the background service worker alive during active sync sessions (the extension only stays connected while you are in a room).
-`notifications`: To display sync status updates (e.g., "Peer joined", "Force Sync initiated").
- **No History Access**: We do not read, store, or transmit your browsing history. We only interact with the specific tab you have actively selected for synchronization.
## 4. Zero Third-Party Requests
KoalaSync is completely self-contained:
- **No CDNs or External Libraries**: All scripts and styles are self-hosted.
- **No Analytics**: We do not use Google Analytics, tracking pixels, or any third-party telemetry.
- **No External Fonts**: We use system font stacks to prevent tracking via font services.
## 5. Self-Hosted Instances
This privacy policy applies to the **official KoalaSync relay server** at `syncserver.koalastuff.net`. If you choose to self-host a relay server using our open-source Docker image, the data handling practices of that instance are the responsibility of the server operator.
---
**Auditable & Open Source**: Because KoalaSync is open source, you can verify these claims by reviewing the [Server Source Code](https://github.com/Shik3i/KoalaSync/blob/main/server/index.js) and the [Extension Logic](https://github.com/Shik3i/KoalaSync/blob/main/extension/content.js).
This directory contains deep-dives into the KoalaSync protocol, architecture, roadmap, and operational guidelines.
## Start Here by Role
- **Users and reviewers**: Start with [HOW_IT_WORKS.md](HOW_IT_WORKS.md), then [PRIVACY.md](PRIVACY.md) and [TESTED_SERVICES.md](TESTED_SERVICES.md).
- **Self-hosters**: Start with the root [README.md](../README.md), then [devops.md](devops.md), [PROTOCOL.md](PROTOCOL.md), and the examples in `../examples/`.
- **Contributors**: Start with [../CONTRIBUTING.md](../CONTRIBUTING.md), then [ARCHITECTURE.md](ARCHITECTURE.md), [SYNC_GUIDE.md](SYNC_GUIDE.md), and the README for the subdirectory you are editing.
- **AI agents**: Start with [AI_INIT.md](AI_INIT.md), then read the relevant subdirectory README before changing files.
- **Security reviewers**: Start with [SECURITY.md](../SECURITY.md), [KNOWN_LIMITATIONS.md](KNOWN_LIMITATIONS.md), [PRIVACY.md](PRIVACY.md), and [PROTOCOL.md](PROTOCOL.md).
## 🏗️ Core Architecture & Design
- **[ARCHITECTURE.md](ARCHITECTURE.md)**: Overview of the communication flows, Dual Heartbeat architecture, and synchronization logic.
- **[HOW_IT_WORKS.md](HOW_IT_WORKS.md)**: Step-by-step walkthrough of every user flow, from room creation to synchronized playback. Ideal for store reviewers and manual testers.
- **[host-control-mode.md](host-control-mode.md)**: Design, requirements, and edge cases of the Host Control feature.
- **[AI_INIT.md](AI_INIT.md)**: Maintainer and AI-agent onboarding: non-negotiables, workflow order, and safety checks.
## 📡 Protocol & Synchronization
- **[PROTOCOL.md](PROTOCOL.md)**: Low-level message format and payload descriptions for the KoalaSync sync protocol.
- **[SYNC_GUIDE.md](SYNC_GUIDE.md)**: Guide on keeping protocol constants synchronized across the workspace.
## 📋 Compatibility, Roadmap & Contribution
- **[TESTED_SERVICES.md](TESTED_SERVICES.md)**: Status of compatibility with major streaming services and contribution guidelines for testing new platforms.
- **[KNOWN_LIMITATIONS.md](KNOWN_LIMITATIONS.md)**: Threat model and accepted design limitations (NOFIX entries) for security audits.
- **[PRIVACY.md](PRIVACY.md)**: Privacy model and data-handling policy for users, reviewers, and contributors.
- **[ROADMAP.md](ROADMAP.md)**: Planned features, backlog items, and rejected proposals.
- **[TRANSLATION.md](TRANSLATION.md)**: Guide for native speakers to contribute and audit dynamic extension/website translations.
## 🚀 DevOps & Releases
- **[devops.md](devops.md)**: Guide on the automated tag-based release pipeline.
- **[CHANGELOG.md](CHANGELOG.md)**: Detailed history of releases and changes.
---
*For high-level project information and developer setup instructions, refer to the root [README.md](../README.md).*
> Feature priorities, planned work, backlog, and rejected ideas for KoalaSync.
---
## Status Legend
| Badge | Meaning |
|---|---|
| 🚧 In Progress | Currently being developed |
| 📋 Planned | Prioritized for an upcoming phase |
| 💡 Backlog | Under evaluation, not yet prioritized |
| ❌ Rejected | Declined (with rationale) |
| ✅ Completed | Shipped |
---
## 🚧 In Progress
*Currently being worked on.*
| Feature | Priority | Area |
|---|---|---|
| *(none yet)* | | |
---
## 📋 Planned
*Prioritized for upcoming phases.*
### 2. Invite link with target URL for auto-redirect
- **Priority:** P2
- **Category:** UX / Ease of Sharing
- **Background:** The invite link currently only contains the room ID. The invited person has to manually open the page. Ideally, the link would include the shared tab's URL so the invitee gets redirected to the right page and the tab is auto-selected (auto-matching via tab title already exists).
- **Known challenges:**
- Many streaming sites (e.g., Emby, Jellyfin) don't have unique URLs per content — once inside the player, the URL stays the same.
- Dozens of such edge cases exist; a generic solution is difficult.
- Would likely need site-specific extractor logic (similar to the existing sync service adapters).
- **Possible approaches:**
- Fallback: if no unique URL can be determined, only pass the tab title.
- Site-specific URL extraction for known services.
---
## 💡 Backlog
*Ideas and feature requests under evaluation.*
### In-room chat overlay (like TeleParty)
- **Priority:** P3
- **Category:** Social / Communication
- **Background:** A collapsible chat panel to the right of the video (or as an overlay) allowing text-based communication with everyone in the room.
- **Why backlog (still uncertain):**
- **Use case:** No strong personal need — with chat, latency matters less than with voice; async communication tolerates a few seconds of delay.
- **Legal/moderation:** Unclear what moderation requirements would apply if users can exchange chat messages. Could be relevant depending on jurisdiction.
- **Status:** Under evaluation, may come later.
### Cross-frame video detection and control
- **Priority:** P3
- **Category:** Compatibility / Embedded Players
- **Background:** KoalaSync currently injects on demand into the selected tab's top frame. This works for normal top-frame players, including current Emby/Jellyfin usage, but does not cover cases where the real `<video>` lives inside a cross-origin iframe or an `about:blank`/`srcdoc` player frame.
- **Possible approach:** Add an opt-in frame bridge where child frames announce detected videos to the top frame, and the top frame routes remote play/pause/seek commands to the active child video.
- **Status:** Future compatibility work, not needed for current Emby behavior.
### Local extension E2E smoke tests
- **Priority:** P2
- **Category:** Testing / Release Confidence
- **Background:** The release verification covers unit tests, server integration, syntax, lint, audits, and builds, but it does not currently run a real browser extension flow. A small local E2E smoke suite would catch regressions in content-script injection, tab navigation reinjection, remote seek handling, and iframe player support.
- **Possible approach:** Add a separate local-only Playwright smoke command that loads the unpacked extension, opens two controlled video pages, and verifies play/pause/seek through the actual extension path. Keep it outside `npm run verify` until it is stable enough for CI.
- **Status:** Backlog, recommended before larger content-script or frame-bridge changes.
---
## ❌ Rejected
*Declined features with rationale — keeps decisions documented so they don't get re-debated.*
KoalaSync uses a "Single Source of Truth" for its communication protocol constants located in the root `shared/` directory. However, Browser Extensions (Manifest V3) are strictly sandboxed and **cannot load or import files from outside their root directory**.
To ensure that the extension and the relay server are always using the exact same event names and protocol versions, we maintain a mirrored copy of the shared files within the `extension/shared/` folder.
## When should you run the build script?
You MUST run the build script in any of the following scenarios:
1.**After a fresh `git clone` or `git pull`** (as the synced files are ignored by git).
2.**After modifying**`shared/constants.js`.
3.**After modifying**`shared/blacklist.js`.
4.**After modifying**`shared/names.js`.
5.**Before committing** changes to the repository if any shared protocol or extension-mirrored files were touched.
6.**Before deploying** the server or releasing the extension.
## How to sync
Run the Node.js build script from the repository root:
```bash
node scripts/build-extension.cjs
# or simply:
npm run build:extension
```
## What does it do?
The build script performs the following actions:
1. Synchronizes shared files by copying `shared/constants.js`, `shared/blacklist.js`, `shared/names.js`, and `shared/README.md` into `extension/shared/`.
2. Injects `EVENTS`, `HEARTBEAT_INTERVAL`, and `episode-utils.js` functions (`extractEpisodeId`, `sameEpisode`) into `content.js` via marker-based replacement.
3. Injects browser-specific uninstall URL constants into `background.js` and a build timestamp into `popup.html`.
4. Compiles browser-specific manifest files.
5. Packages the final ready-to-publish extension artifacts for Chrome and Firefox into the `dist/` directory.
## Protocol Versioning
The system enforces a strict `protocolVersion` check during the `JOIN_ROOM` handshake.
- The version is defined in `shared/constants.js`.
- If the extension and server versions mismatch, the server will reject the connection with an `Incompatible protocol version` error.
- **Never manually bump version numbers**. The CI pipeline automatically injects the version from the git tag into `manifest.base.json`, `shared/constants.js`, and `package.json` during release builds. Run the build script to synchronize other constant updates.
> [!CAUTION]
> **NEVER** edit the files inside `extension/shared/` directly. They will be overwritten the next time the build script is run. Always edit the files in the root `shared/` directory and then run the build script.
This document tracks which streaming platforms and media servers are supported by the KoalaSync extension.
> [!TIP]
> **Contributions are highly welcome!** 🤝 Anyone can easily update this list. If you have tested a streaming service (whether it works, has issues, or is not yet listed), please help the project by submitting a quick Pull Request. See the [How to Contribute](#how-to-contribute) guide below!
---
## Compatibility Matrix
| Service | Sync Works | Media Title | Episode Auto-Sync | Last Tested | Tested By | Extension Version | Notes |
| **Netflix** | ✅ Full | ❌ | ❌ | — | — | — | No media title exposed. |
| **Emby** | ✅ Full | ✅ Full | ✅ Full | — | — | — | Best-in-class support. |
| **Jellyfin** | ✅ Full | ✅ Full | ✅ Full | — | — | — | — |
| **Plex** | Not tested | Not tested | Not tested | — | — | — | — |
| **Disney+** | ✅ Full | ⚠️ Partial | ❌ | — | — | — | Series title only (e.g. "The Simpsons"), no episode info. |
| **Prime Video** | ✅ Full | ✅ Full | ❌ | — | — | — | — |
| **HBO Max / Max** | Not tested | Not tested | Not tested | — | — | — | — |
| **Crunchyroll** | Not tested | Not tested | Not tested | — | — | — | — |
| **Vimeo** | Not tested | Not tested | Not tested | — | — | — | — |
| **Dailymotion** | Not tested | Not tested | Not tested | — | — | — | — |
| **ARD / ZDF Mediathek** | Not tested | Not tested | Not tested | — | — | — | — |
| **Vix** | ✅ Full | ✅ Full | ✅ Full | — | — | — | Everything works correctly. |
### Legend
| Symbol | Meaning |
| :---: | :--- |
| ✅ Full | Works without limitations. |
| ⚠️ Partial | Works with caveats (see Notes). |
| ❌ | Not supported / does not work. |
| ❌ N/A | Not applicable (feature does not exist on the platform). |
| **Not tested** | Has not been tested yet. |
---
## How to Contribute
Updating this compatibility list is quick and easy! You don't need deep coding skills to contribute:
1.**Fork the Repository**: Click the **Fork** button at the top of the [KoalaSync GitHub Repository](https://github.com/Shik3i/KoalaSync).
2.**Edit this File**: Open [docs/TESTED_SERVICES.md](TESTED_SERVICES.md) in your fork's browser editor (or clone it locally) and update the table with your testing details.
3.**Commit & Push**: Commit your changes with a clear message (e.g., `docs: update Netflix compatibility status`).
4.**Create a Pull Request**: Submit the Pull Request (PR) from your fork to our `main` branch.
> [!NOTE]
> **Reporting Problems:** If you notice a bug or partial support on a service, please open a [GitHub Issue](https://github.com/Shik3i/KoalaSync/issues) describing the problem, and link it in the **Notes** column of the table.
>
> _If you are unsure how to create/link an issue, don't worry! Simply submit the PR anyway, and the maintainers will gladly create and link the issue for you._
---
## Technical Background
KoalaSync works on any website with a **standard HTML5 `<video>` element** that allows script injection.
Limited functionality on certain platforms is typically caused by:
- **DRM/Copy Protection** (e.g., Widevine on Netflix) which restricts access to media metadata like title and playback state
- **Shadow DOM encapsulation** that hides video elements from content scripts
- **Strict Content Security Policies** (CSP) that block script injection
Websites with heavily obfuscated custom players (e.g., complex Shadow DOM, iframe isolation) may require platform-specific workarounds in `content.js`.
Welcome to the **KoalaSync** translation guide. We rely on the open-source community to make KoalaSync accessible to users worldwide.
KoalaSync is split into two independent translation areas. You can translate either one, or both:
1.**The Browser Extension** (`extension/locales/`): The core product that users interact with daily.
2.**The Website** (`website/locales/`): The landing page and invitation bridge.
---
## Supported Languages Dashboard
We divide supported languages into two tiers: **Core Languages** (fully hand-crafted and audited by native speakers) and **Extended Languages** (auto-generated using translation models to expand initial coverage).
> [!TIP]
> **Help Us Improve!**
> We welcome community contributions to audit `Auto-Generated` translations and elevate them to `100% Manually Verified` status.
| Language Code | Language Name | Verification Status | Rationale / Context |
| :--- | :--- | :--- | :--- |
| `en` | **English** | `100% Manually Verified` | Global default language (verified by developer) |
> Any newly contributed languages must be marked as `Auto-Generated` in this table until fully reviewed and signed off by a native speaker in a pull request.
---
## How to Translate KoalaSync
Here is the exact step-by-step process for contributing translations.
### Step 1: Fork and Clone the Repository
If you are an external contributor, start with the standard open-source workflow:
1. Click the "Fork" button on GitHub to create your own copy of the repository.
2. Clone your fork locally: `git clone https://github.com/YOUR-USERNAME/KoalaSync.git`
3. Create a branch: `git checkout -b translation/my-language`
### Step 2: Translate the Extension
The browser extension handles real-time syncing, settings, and popups.
1. Navigate to `extension/locales/`.
2. Edit an existing `[lang].json` or copy `en.json` to create a new one (for example, `it.json`).
3. Translate all string values. **Do not change the JSON keys.**
### Step 3: Translate the Website
The website hosts the landing page and invitation bridge.
1. Navigate to `website/locales/`.
2. Edit an existing `[lang].json` or copy `en.json` to create a new one.
3. Translate all string values. **Do not change the JSON keys.**
4. If creating a brand new language, configure the metadata at the top of your JSON file:
```json
{
"LANG_CODE":"it",
"HTML_CLASS":"lang-it",
"CANONICAL_PATH":"it/",
"LANG_TOGGLE_URL":"../",
"LANG_TOGGLE_TEXT":"EN"
}
```
5. If creating a brand new language, register it in `website/build.cjs` by adding it to the `languages` array.
### Step 4: Verify Locally
Ensure your JSON files are valid and all keys match the English baseline. Open your terminal in the KoalaSync root folder and run:
```bash
# Tests the extension locales for missing keys or syntax errors
node scripts/test-locales.cjs
# Tests the website locales for missing keys or syntax errors
node scripts/test-website-locales.mjs
# Builds the website with your new translations
node website/build.cjs
```
If you receive any errors about missing keys or placeholder strings, fix them before submitting.
### Step 5: Commit and Pull Request
1. Open this `TRANSLATION.md` file and add or update your language in the **Supported Languages Dashboard** above. Mark it as `100% Manually Verified` only if it has been reviewed by a native speaker.
2. Commit your changes: `git commit -m "Update Italian translations"`
3. Push to your fork: `git push origin translation/my-language`
4. Open a pull request on the main KoalaSync repository on GitHub.
---
## Strict Legal Exclusion Rule
Our legal pages have strict constraints to protect user privacy and avoid regulatory liabilities.
> [!IMPORTANT]
> **Do Not Translate Legal Documents**
> The legal notice (`website/impressum.html`) and privacy policy (`website/datenschutz.html`) **MUST remain exclusively in English and German**.
>
> **Rationale:** Legal compliance under the European Union General Data Protection Regulation (GDPR) and the German Digital Services Act (DDG). Offering automated translations of legally binding notices introduces compliance risks due to potential mistranslations of liability limits.
>
> **Technical fallback:** Our system automatically falls back to English for legal pages if a user visits them in an unsupported language, so you do not need to translate them.
This document describes the deployment and release process for KoalaSync.
## Tag-Based Releases
KoalaSync uses a fully automated release pipeline triggered by Git tags.
> [!IMPORTANT]
> **DO NOT** manually bump the version numbers in any files (such as `package.json`, `manifest.base.json`, `shared/constants.js`, etc.) before creating a release.
> Bumping versions manually is redundant, leads to conflicts, and is completely handled by the CI/CD pipeline.
### How it Works
When you push a Git tag matching `v*` (e.g., `v2.5.1`), the GitHub Actions release workflow (`.github/workflows/release.yml`) is triggered. The workflow performs the following actions:
1.**Extracts the version** from the tag (e.g., `2.5.1` from `v2.5.1`).
2.**Injects the version** automatically into the following files:
-`README.md` (updates badge and announcement banner)
-`website/sitemap.xml` (updates `lastmod` dates)
3.**Commits and pushes** these version updates back to the `main` branch automatically with the commit message `chore(release): update versions to vX.X.X [skip ci]`.
4.**Builds the extension** for both Chrome and Firefox and publishes the zipped archives.
5.**Builds the website** and uploads website artifacts.
6.**Builds and publishes** the Docker image for the relay server to the GitHub Container Registry (`ghcr.io`).
---
## Steps to Deploy a New Release
To release a new version (e.g., `v2.5.1`), follow these steps:
1. Make sure your local repository is synced on `main`:
```bash
git checkout main
git pull origin main
```
2. Create a local Git tag:
```bash
git tag v2.5.1
```
3. Push the tag to GitHub:
```bash
git push origin v2.5.1
```
The release pipeline will take care of the rest! You can monitor the progress under the **Actions** tab of the GitHub repository.
A Manifest V3 Chrome Extension for synchronized video playback across any website.
This directory contains the Manifest V3 browser extension for Chrome and Firefox. It owns the popup UI, background service worker, content-script video control, invitation bridge, audio processing, and all browser-local settings.
## Where You Are
-`manifest.base.json` is the source manifest. The build script creates browser-specific `manifest.json` files in `dist/chrome/` and `dist/firefox/`.
-`background.js` is the long-lived coordinator: WebSocket client, room state, host-control authority, heartbeat, reconnects, tab selection, and content-script injection.
-`content.js` runs in the selected video tab. It detects video state, applies remote play/pause/seek, handles episode transitions, and applies local audio processing.
-`popup.html` and `popup.js` implement the visible extension UI.
-`extension/shared/` is generated by `npm run build:extension` from the root `shared/` directory. Do not edit it directly.
## Key Features
- **Manifest V3**: Optimized Service Worker architecture with session persistence.
- **Pure Vanilla JS**: No external dependencies or heavy libraries.
- **Smart Peer IDs**: Hexadecimal IDs combined with customizable Usernames for easy identification.
- **Dual Heartbeat**: Advanced session tracking (Background) and video synchronization (Content) to prevent ghost sessions.
- **Live Diagnostics**: Built-in "Dev" tab for real-time video state debugging (ReadyState, CurrentTime, etc.).
- **Manifest V3**: Service-worker architecture with session persistence and explicit keep-alive handling.
- **Pure Vanilla JS**: No extension runtime dependencies and no bundler inside `extension/`.
- **On-Demand Connection**: The service worker connects only while the user intends to be in a room.
- **Host Control & Co-Hosts**: Hosts can switch a room into `host-only` mode and grant controller rights to trusted peers.
- **Episode Auto-Sync**: Title/episode changes can open a lobby so peers resume together once everyone is ready.
- **Smart Matching & Title Privacy**: Matching video tabs are highlighted, while tab/media title sharing can be reduced or disabled.
- **Audio Processing**: Optional local compressor settings live in the dedicated audio options page.
- **Status Diagnostics**: The Status tab exposes connection state, ping, video debug data, action history, and copyable logs.
- **Dynamic i18n**: 15 languages are supported: `en`, `de`, `fr`, `es`, `it`, `nl`, `pl`, `pt`, `pt-BR`, `tr`, `ru`, `ja`, `ko`, `zh`, and `uk`.
## Tab Overview
1.**Room**: Manage connections, view active peers, and share invitation links.
2.**Sync**: Control video playback (Play/Pause/Force Sync) and view recent activity.
3.**Settings**: Customize your Username and toggle domain-based Noise Filtering.
4.**Dev**: Monitor connection status and view real-time video element metadata for debugging.
1.**Room**: Select official/custom server, create or join rooms, view peers, share invite links, and manage Host Control when supported by the relay.
2.**Sync**: Select the video tab, send play/pause/seek/force-sync actions, and view episode lobby state.
3.**Settings**: Configure username, title sharing, noise filtering, auto episode sync, notifications, language, and audio options.
4.**Status**: Inspect connection state, latency, video debug info, history, and logs for bug reports.
5.**Dev**: Hidden developer-only controls shown for the `KoalaDev` username.
## Privacy & Permissions
KoalaSync requires `<all_urls>` permission to detect and interact with video elements (`<video>`) on websites.
- **No Browsing History**: We do not track or store your browsing history.
- **State Management**: Sensitive data (Room Passwords) is stored locally using `chrome.storage`.
-**Zero Telemetry**: No analytics or external tracking scripts.
KoalaSync requires `<all_urls>` host permission so it can detect and control `<video>` elements on arbitrary sites.
-No browsing history is collected or uploaded.
- Room credentials and user settings are stored locally with `chrome.storage`.
- No analytics, external scripts, external fonts, or tracking libraries are used by the extension.
- Audio processing is local to the selected tab.
- Title privacy controls decide whether tab/media titles are shared with room peers.
## Installation
1.**Sync Protocol**: Run `./scripts/sync-constants.sh` (macOS/Linux) or `scripts\sync-constants.bat` (Windows) from the root.
2. Open Chrome and go to `chrome://extensions/`.
3. Enable **Developer mode** (top right).
4. Click **Load unpacked** and select the `extension` folder.
From the repository root:
```bash
npm install
npm run build:extension
```
Then load the generated bundle:
- Chrome/Chromium: open `chrome://extensions/`, enable Developer Mode, and load `dist/chrome`.
- Firefox: open `about:debugging`, choose **This Firefox**, and load `dist/firefox/manifest.json`.
## Development
If you modify `shared/constants.js`, you must synchronize the changes across the extension and server:
- **Windows**: Run `scripts\sync-constants.bat`
- **Linux/macOS**: Run `scripts/sync-constants.sh`
Run the build whenever shared protocol files or extension packaging inputs change:
```bash
npm run build:extension
```
The build copies `shared/constants.js`, `shared/blacklist.js`, `shared/names.js`, and `shared/README.md` into `extension/shared/`, injects synchronous constants into `content.js`, generates browser manifests, and creates zip artifacts in `dist/`.
Useful focused checks from the repository root:
```bash
node -c extension/background.js
node -c extension/content.js
node -c extension/popup.js
node scripts/test-episode-utils.mjs
node scripts/test-title-privacy.mjs
node scripts/test-audio-settings.mjs
node scripts/test-locales.cjs
```
For the full suite, run:
```bash
npm run verify
```
## Do Not Break
- Keep `content.js` synchronous and IIFE-based; it cannot import ES modules directly.
- Keep the injection markers used by `scripts/build-extension.cjs`.
- Keep protocol names in `shared/constants.js` as the source of truth.
- Keep extension runtime dependencies at zero unless the project explicitly decides to introduce a bundler.
"ERR_IDENTITY_NOT_LOADED":"Identität noch nicht geladen. Warte kurz und versuche es erneut.",
"ERR_NO_PEERS_TIME":"Keine anderen Teilnehmer mit bekannter Zeit. Wechsle zu 'Zu mir springen'.",
"ERR_NO_VIDEO_TAB":"Verbindung zum Video-Tab fehlgeschlagen.",
"SITE_ACCESS_REQUIRED":"Dein Browser blockiert KoalaSync auf {host}. Klicke unten auf „Zugriff erlauben“. Falls kein Dialog erscheint, erlaube KoalaSync in den Erweiterungs- oder Websiteberechtigungen für diese Seite.",
"BTN_RETRY_ACCESS":"Zugriff erlauben",
"ERR_SELECT_VIDEO":"Bitte wähle zuerst ein Video aus!",
"TOAST_INVITE_COPIED":"Einladungslink kopiert!",
"TOAST_COPY_FAILED":"Kopieren in Zwischenablage fehlgeschlagen",
"TOAST_LOBBY_SKIP_FAILED":"Überspringen der Lobby fehlgeschlagen.",
"TOAST_LOGS_COPIED":"Kopiert!",
"TOAST_PEER_JOINED":"{name} ist dem Raum beigetreten",
"TOAST_PEER_LEFT":"{name} hat den Raum verlassen",
"TOAST_PEER_ACTION":"{name} hat {action}",
"STATUS_CONNECTED":"Verbunden",
"STATUS_RECONNECTING":"Verbinde erneut...",
"STATUS_CONNECTING":"Verbinde...",
"STATUS_FAILED":"Fehlgeschlagen",
"STATUS_DISCONNECTED":"Getrennt",
"STATUS_IDLE":"Bereit zum Verbinden",
"STATUS_IDLE_TOOLTIP":"KoalaSync ist bereit. Tritt einem Raum bei oder erstelle einen, um die Verbindung herzustellen und die Synchronisierung zu starten.",
"BTN_STATE_JOINING":"🚀 Trete bei...",
"BTN_STATE_RECONNECTING":"🔄 Verbinde erneut...",
"BTN_STATE_PLAYING":"▶ Spiele ab...",
"BTN_STATE_PAUSING":"⏸ Pausiere...",
"BTN_STATE_SYNCING_GROUP":"Synce zur Gruppe ({time})...",
"ERR_INVALID_SERVER_URL":"Invalid Server URL format.",
"ERR_IDENTITY_NOT_LOADED":"Identity not yet loaded. Wait a moment and try again.",
"ERR_NO_PEERS_TIME":"No other peers with a known time. Switch to 'Jump to Me'.",
"ERR_NO_VIDEO_TAB":"Could not connect to video tab.",
"SITE_ACCESS_REQUIRED":"Your browser is blocking KoalaSync on {host}. Click \"Allow access\" below. If no dialog appears, allow KoalaSync on this site in your extension or site permissions.",
"BTN_RETRY_ACCESS":"Allow access",
"ERR_SELECT_VIDEO":"Please select a video first!",
"TOAST_INVITE_COPIED":"Invite link copied!",
"TOAST_COPY_FAILED":"Failed to copy to clipboard",
"TOAST_LOBBY_SKIPPED":"Episode Lobby skipped.",
"TOAST_LOBBY_SKIP_FAILED":"Failed to skip lobby.",
"TOAST_LOGS_COPIED":"Copied!",
"TOAST_PEER_JOINED":"{name} joined the room",
"TOAST_PEER_LEFT":"{name} left the room",
"TOAST_PEER_ACTION":"{name} {action}",
"STATUS_CONNECTED":"Connected",
"STATUS_RECONNECTING":"Reconnecting...",
"STATUS_CONNECTING":"Connecting...",
"STATUS_FAILED":"Failed",
"STATUS_DISCONNECTED":"Disconnected",
"STATUS_IDLE":"Ready to connect",
"STATUS_IDLE_TOOLTIP":"KoalaSync is ready. Join or create a room to connect and start syncing.",
"BTN_STATE_JOINING":"🚀 Joining...",
"BTN_STATE_RECONNECTING":"🔄 Reconnecting...",
"BTN_STATE_PLAYING":"▶ Playing...",
"BTN_STATE_PAUSING":"⏸ Pausing...",
"BTN_STATE_SYNCING_GROUP":"Syncing to group ({time})...",
"BTN_STATE_SYNCING":"Syncing...",
"BTN_STATE_SYNCED":"✅ Synced!",
"NOTIF_PLAY":"started playback",
"NOTIF_PAUSE":"paused playback",
"NOTIF_SEEK":"seeked the video",
"NOTIF_FORCE_PREPARE":"started force sync",
"NOTIF_FORCE_EXECUTE":"synchronized everyone",
"DEBUG_NO_TAB":"No target tab selected.",
"DEBUG_COMM_FAIL":"Could not communicate with tab video.",
"EMPTY_PEERS_TITLE":"No peers yet",
"EMPTY_PEERS_HINT":"Share your invite link to get started",
"EMPTY_HISTORY_TITLE":"No activity yet",
"EMPTY_HISTORY_HINT":"Play, pause, or seek to see history",
"EMPTY_LOGS_TITLE":"No logs",
"EMPTY_LOGS_HINT":"Connection events will appear here",
"EMPTY_ROOMS_TITLE":"No active rooms",
"EMPTY_ROOMS_HINT":"Create a room or refresh to find public ones",
"LABEL_CONN_STATUS_TOOLTIP":"Estado actual de la conexión WebSocket",
"CONN_STATUS_DISCONNECTED":"Desconectado",
"BTN_RETRY":"REINTENTAR",
"BTN_RETRY_TOOLTIP":"Intentar volver a conectarse al servidor",
"BTN_COPY_LOGS":"Copiar registros",
"BTN_COPY_LOGS_TOOLTIP":"Copiar registros al portapapeles para compartir",
"LABEL_VIDEO_DEBUG":"Información de depuración de video",
"LABEL_VIDEO_DEBUG_TOOLTIP":"Detalles técnicos sobre el elemento de video seleccionado actualmente",
"VIDEO_DEBUG_EMPTY":"No hay pestaña seleccionada o no se detectó ningún video.",
"LABEL_HISTORY":"Historial completo",
"LABEL_HISTORY_TOOLTIP":"Registro cronológico de todos los comandos de sincronización en la sala",
"HISTORY_EMPTY":"Sin actividad aún",
"LABEL_LOGS":"Logs (Últimos 50)",
"LABEL_LOGS_TOOLTIP":"Registros técnicos de conexión para depuración",
"BTN_CLEAR":"LIMPIAR",
"BTN_CLEAR_TOOLTIP":"Limpiar salida de registros",
"LABEL_GITHUB":"Repositorio de GitHub",
"BTN_ONBOARDING_SKIP":"Omitir",
"BTN_ONBOARDING_SKIP_TOOLTIP":"Omitir el tutorial",
"BTN_ONBOARDING_NEXT":"Siguiente",
"BTN_ONBOARDING_NEXT_TOOLTIP":"Ir al siguiente paso",
"ONBOARDING_1_TITLE":"¡Bienvenido a KoalaSync!",
"ONBOARDING_1_TEXT":"Mira videos junto a tus amigos en perfecta sincronización, sin importar dónde estén. ¡Hagamos un recorrido rápido!",
"ONBOARDING_2_TITLE":"1. Crear una sala",
"ONBOARDING_2_TEXT":"Comienza aquí. Crea una sala y comparte el enlace de invitación con tus amigos.",
"ONBOARDING_3_TITLE":"2. Seleccionar video",
"ONBOARDING_3_TEXT":"Navega aquí para seleccionar el video que deseas sincronizar. Reproduce, pausa y busca: todos permanecen sincronizados.",
"ONBOARDING_4_TITLE":"3. Personalizar",
"ONBOARDING_4_TEXT":"Elige un nombre de usuario divertido para que tus amigos sepan quién eres.",
"ONBOARDING_5_TITLE":"¡Todo listo!",
"ONBOARDING_5_TEXT":"Hora de preparar las palomitas. ¡Disfruta viendo con tus amigos!",
"ERR_CONN_TIMEOUT":"Tiempo de conexión agotado. Inténtalo de nuevo.",
"ERR_INVALID_SERVER_URL":"Formato de URL de servidor no válido.",
"ERR_IDENTITY_NOT_LOADED":"Identidad no cargada aún. Espera un momento e inténtalo de nuevo.",
"ERR_NO_PEERS_TIME":"No hay otros participantes con posición conocida. Cambia a 'Traerlos a mi posición'.",
"ERR_NO_VIDEO_TAB":"No se pudo conectar a la pestaña de video.",
"SITE_ACCESS_REQUIRED":"Tu navegador está bloqueando KoalaSync en {host}. Haz clic en «Permitir acceso». Si no aparece ningún diálogo, permite KoalaSync en este sitio desde los permisos de extensiones o del sitio.",
"BTN_RETRY_ACCESS":"Permitir acceso",
"ERR_SELECT_VIDEO":"¡Selecciona un video primero!",
"TOAST_INVITE_COPIED":"¡Enlace de invitación copiado!",
"TOAST_COPY_FAILED":"Error al copiar al portapapeles",
"TOAST_LOBBY_SKIPPED":"Sala de espera de episodio omitida.",
"TOAST_LOBBY_SKIP_FAILED":"Error al omitir la sala de espera.",
"TOAST_LOGS_COPIED":"¡Copiado!",
"TOAST_PEER_JOINED":"{name} se ha unido a la sala",
"TOAST_PEER_LEFT":"{name} ha salido de la sala",
"TOAST_PEER_ACTION":"{name} ha {action}",
"STATUS_CONNECTED":"Conectado",
"STATUS_RECONNECTING":"Reconectando...",
"STATUS_CONNECTING":"Conectando...",
"STATUS_FAILED":"Error",
"STATUS_DISCONNECTED":"Desconectado",
"STATUS_IDLE":"Listo para conectar",
"STATUS_IDLE_TOOLTIP":"KoalaSync está listo. Únete o crea una sala para conectarte y empezar a sincronizar.",
"BTN_STATE_JOINING":"🚀 Uniéndose...",
"BTN_STATE_RECONNECTING":"🔄 Reconectando...",
"BTN_STATE_PLAYING":"▶ Reproduciendo...",
"BTN_STATE_PAUSING":"⏸ Pausando...",
"BTN_STATE_SYNCING_GROUP":"Sincronizando al grupo ({time})...",
"BTN_STATE_SYNCING":"Sincronizando...",
"BTN_STATE_SYNCED":"✅ ¡Sincronizado!",
"NOTIF_PLAY":"ha iniciado la reproducción",
"NOTIF_PAUSE":"ha pausado la reproducción",
"NOTIF_SEEK":"ha cambiado la posición del video",
"NOTIF_FORCE_PREPARE":"ha iniciado una sincronización forzada",
"NOTIF_FORCE_EXECUTE":"ha sincronizado a todos",
"DEBUG_NO_TAB":"No hay pestaña objetivo seleccionada.",
"DEBUG_COMM_FAIL":"No se pudo comunicar con el video de la pestaña.",
"EMPTY_PEERS_TITLE":"Sin participantes aún",
"EMPTY_PEERS_HINT":"Comparte tu enlace de invitación para comenzar",
"EMPTY_HISTORY_TITLE":"Sin actividad aún",
"EMPTY_HISTORY_HINT":"Reproduce, pausa o salta en el tiempo para ver el historial",
"EMPTY_LOGS_TITLE":"Sin registros",
"EMPTY_LOGS_HINT":"Los eventos de conexión aparecerán aquí",
"EMPTY_ROOMS_TITLE":"Sin salas activas",
"EMPTY_ROOMS_HINT":"Crea una sala o actualiza para buscar salas públicas",
"LABEL_YOU":"Tú",
"ONBOARDING_DONE":"¡Hecho!",
"LABEL_LOBBY_PEER_READY":"Listo",
"LABEL_LOBBY_PEER_LOADING":"Cargando...",
"LABEL_PASSWORD_PROTECTED":"Protegido con contraseña",
"LABEL_PEERS_COUNT":"{count} participantes",
"LABEL_CUSTOM_SERVER":"Servidor personalizado",
"BTN_STATE_CREATING":"🚀 Creando sala...",
"NOTIF_LOBBY_CANCEL_TITLE":"KoalaSync — Error al sincronizar episodio",
"NOTIF_LOBBY_CANCEL_MSG":"Sincronización automática cancelada: {reason}. Es posible que debas sincronizar manualmente.",
"LOBBY_CANCEL_TIMEOUT":"Tiempo de espera agotado",
"LOBBY_CANCEL_TIMEOUT_RECOVERED":"Tiempo de espera agotado (recuperado)",
"LOBBY_CANCEL_PEERS_LEFT":"Todos los demás participantes se han ido",
"LOBBY_CANCEL_TIMEOUT_PEERS_LOAD":"Tiempo de espera agotado: no todos los participantes cargaron el episodio",
"LOBBY_CANCEL_USER":"Cancelado por el usuario",
"NOTIF_ERROR_TITLE":"Error de KoalaSync",
"FOOTER_SUPPORT":"Support KoalaSync",
"FOOTER_REVIEW":"★ Valorar",
"FOOTER_SUPPORT_PROMPT":"¿Te gusta KoalaSync? ¡Deja una reseña!",
"LABEL_AUDIO_PROCESSING":"Procesamiento de audio",
"LABEL_AUDIO_PROCESSING_TOOLTIP":"Aplica efectos de audio como compresión a la reproducción de video",
"AUDIO_OPEN_SETTINGS":"Abrir",
"NEW_FEATURE_AUDIO":"Nuevo: Procesamiento de audio — ¡prueba el compresor!",
"NEW_FEATURE_THEME":"Nuevo: Temas — cambia al aspecto clásico o al grafito minimalista.",
"AUDIO_BACK":"← Volver",
"AUDIO_PAGE_TITLE":"Configuración de audio",
"AUDIO_MASTER_TOGGLE":"Procesamiento de audio",
"AUDIO_COMPRESSOR":"Compresor",
"AUDIO_COMPRESSOR_ENABLE":"Activado",
"AUDIO_PRESET":"Preajuste",
"AUDIO_PRESET_RECOMMENDED":"Recomendado",
"AUDIO_PRESET_DYNAMIC_RANGE":"Rango dinámico",
"AUDIO_PRESET_VOCAL_ENHANCEMENT":"Mejora de voz",
"AUDIO_PRESET_SMOOTH":"Suave",
"AUDIO_PRESET_CUSTOM":"Personalizado",
"AUDIO_PARAM_THRESHOLD":"Umbral (Threshold)",
"AUDIO_PARAM_KNEE":"Codo (Knee)",
"AUDIO_PARAM_RATIO":"Relación (Ratio)",
"AUDIO_PARAM_ATTACK":"Ataque (Attack)",
"AUDIO_PARAM_RELEASE":"Liberación (Release)",
"AUDIO_EQUALIZER":"Ecualizador",
"AUDIO_COMING_SOON":"Próximamente",
"BTN_RESTART_TOUR":"Reiniciar tutorial",
"BTN_RESTART_TOUR_TOOLTIP":"Reiniciar el tutorial de inicio",
"HINT_SELECT_VIDEO":"¡Selecciona tu video aquí!",
"OPTION_TITLE_PRIVACY_FULL":"Enviar título completo",
"OPTION_TITLE_PRIVACY_EPISODE":"Enviar ID de episodio",
"OPTION_TITLE_PRIVACY_HIDDEN":"No enviar",
"LABEL_PRIVACY_SETTINGS":"Ajustes de privacidad",
"LABEL_PRIVACY_SETTINGS_TOOLTIP":"Elige qué títulos de pestaña y multimedia se envían a la sala.",
"LABEL_SEND_TAB_TITLE":"Enviar título de pestaña",
"LABEL_SEND_TAB_TITLE_TOOLTIP":"Envía a la sala el título de la pestaña del navegador seleccionada.",
"LABEL_MEDIA_TITLE_PRIVACY":"Título multimedia",
"LABEL_MEDIA_TITLE_PRIVACY_TOOLTIP":"Elige cómo se envían los títulos multimedia a la sala.",
"LABEL_SETTINGS_GROUP_PROFILE":"Perfil y Apariencia",
"LABEL_SETTINGS_GROUP_PROFILE_TOOLTIP":"Cambia tu nombre de usuario, tema y preferencias de idioma.",
"LABEL_SETTINGS_GROUP_SYNC":"Reproducción y Sincronización",
"LABEL_SETTINGS_GROUP_SYNC_TOOLTIP":"Personaliza la reproducción, las notificaciones y las preferencias de audio."
}
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.