The companion served a connect-src self policy while its vocabulary card fetched Wiktionary and MyMemory directly. Allow only those two origins and cover the real installed browser policy in tests.
Turn the pre-viewing connection into its own screen: a titled surface with
the approval steps, the code in its own framed block, and a notice whose
role follows the state. The pure quickConnectView state machine decides
what each of the four states shows — preparing, waiting for approval,
refused, signed out — so a refusal is announced as an alert with a ✕ marker
and its own token, while a refusal reason stays visible above the
replacement code. The recovery action ("Réessayer", "Générer un nouveau
code", "Se connecter") is highlighted by the accent token and receives
focus.
The status bar is now reserved for tracking and stays hidden until viewing
begins; connection messages live on the connection screen. The code tokens
are adjusted (2.5rem, .2em) so six digits hold on one line at 375 px, and
the refusal palette joins the AA-checked palette.
Update the unit and e2e suites, the stylesheet contract, README and
validation report, and rebuild the packaged plugin.
Add an explicit "Plein écran" control in the reading header that paints a
fixed CSS overlay in pure black (--color-oled) instead of calling the
browser Fullscreen API, which iOS Safari does not expose to pages. The
overlay keeps only the principal/help hierarchy: the pure
fullscreenBarVisible rule reveals the minimal bar (exit, resume, status)
when a tap suspends the display, and also when tracking becomes
uncertain, so the indicator stays visible; resuming hides the bar and
restores the live reading. A tall reading scrolls inside the overlay
while the current block stays anchored to the bottom.
Exiting leaves the overlay and releases the best-effort screen wake lock
(createWakeLock, silent on insecure or denied hosts, reacquired when the
page returns) but keeps the requested freeze: resuming stays manual.
Escape is the standard keyboard exit, and the fullscreen choice is never
persisted, so every visit starts in the ordinary shell.
Update the unit and e2e suites, the stylesheet contract, the glossary,
README and rebuild the packaged plugin.
Freeze the displayed history on a tap while tracking carries on in the
background: a frozen snapshot drives historyView, while the live
history, position estimate and track state keep updating underneath. A
second tap, or the named "Reprendre l'affichage" control, re-renders
immediately at the followed position. Suspension is never lifted
automatically - not by visibility, pageshow, online or resume events,
nor by a track or session change.
The pure tracking-status model gains a "suspended" kind that outranks
uncertainty and the resumption check, with a hint that reports stale
tracking without replacing the badge. The status bar shows a compact
bordered badge (pause icon, visible label, distinct token); shared badge
properties are grouped with the synced badge, and the reading zone
advertises the tap with a keyboard-accessible toggle that shares the
tab order.
Update unit and e2e tests, add the freeze/background/uncertain/immediate
resume scenario to tests/shell.test.mjs, and rebuild the packaged plugin.
Reorganize the shell around reading. A persistent status bar (sticky,
opaque) keeps #status visible while the page scrolls and while settings are
open: a compact badge (icon, label, colour, never colour alone) for
synchronized tracking and a full-width bordered banner for uncertain
tracking. The pure tracking-status model (web/src/lib/status.ts) now owns
the kind, message and accessible hint, including the resumption precedence;
estimation, freshness, history and help association are untouched. The
badge's accuracy caveat stays in title/aria-label.
Session selection, details and both subtitle tracks move behind a
one-gesture "Réglages" disclosure (aria-expanded/aria-controls), closable
without ever hiding the status; after connection the keyboard focus goes to
the trigger. Fix [hidden] being overridden by the control display, adopt
the dark surface tokens for native controls, and keep an accessible h1.
Update the e2e suite to open the disclosure and add tests/shell.test.mjs;
rebuild the packaged plugin (dist/).
Turn the history into a dedicated reading zone: the list is a bottom-anchored
flex column, the current block sits at its bottom with a visible "Bloc courant"
marker (aria-current, dominant size, surface panel, accent edge), and older
blocks lose size and colour while staying above AA contrast. Associated help is
grouped under its main block with an indent and a subordinate rule, stacked in
temporal order; a missing association becomes a discreet text marker instead of
the old sentence; source line breaks are preserved and long lines wrap softly.
Move the entry model into web/src/lib/history.ts (HistoryEntry, historyEntries,
entryEmphasis) and sort overlapping help by start time; the store only wires it,
leaving estimation, freshness, history selection and help association untouched.
Unit tests cover the temporal stacking, the absence marker and the recency rule;
the stylesheet contract locks the anchor, the hierarchy, AA contrasts, pre-line
and soft wrapping; the e2e suite checks the rendered hierarchy, the aria-current
entry and the anchored bottom. Rebuild the packaged plugin (dist/).
Reorganize web/src/style.css around @layer tokens: colors, type scale,
line heights, weights and spacing are declared once and referenced with
var(). Raise the reading base from 1rem/1.6 to 1.125rem/1.7 (title and
Quick Connect code follow), keep :focus-visible and add a global
prefers-reduced-motion block for the coming visual tickets.
No selector, Vue file or interface logic changes; the DOM and behaviour
stay those of #14 and the e2e suite is untouched (34/34). A contract
test at web/src/style.test.ts locks the tokens, AA contrasts, focus and
reduced motion; npm run test:unit now includes it (28/28).
Rebuild the packaged plugin (dist/) with the new stylesheet.
Move the interface sources to web/ (Vue 3 single-file components, a
reactive store, framework-free modules) and assemble them with Vite into
fixed asset names under the gitignored src/Compagnon/Web/, embedded in
the single plugin DLL. Templates are precompiled, no router or Pinia is
added and no inline script is emitted, so the script-src 'self' policy
still holds.
Extract the delicate logic - position estimation with its freshness
bounds and seeded window, history selection, help overlap, subtitle
validation and ASS cleanup, saved connection parsing - into modules
tested without a browser by node:test, injecting the clocks.
Build the interface in scripts/package.sh before the C# compile, bump
the plugin to 0.7.0, switch npm run typecheck to vue-tsc and document the
Vite dev proxy flow in the README.
Prove parity with the untouched end-to-end suite: 34 tests on the
disposable server with and without the /jellyfin prefix, stable gap
within 330 ms and seek recovery within 789 ms against the burnt-in
timecode. Record the decision in docs/adr/0003 and the evidence in
docs/validation/issue-14.md.
Extend the displayed position between Jellyfin observations without
counting a second twice, freeze it as explicitly uncertain when the
session observation or the player check-in goes stale, and fetch a
fresh state after a network cut, a suspended page or a restored one
before presenting the display as synchronized again.
Rebuild both tracks and the history when the media or source changed
during an interruption, keep a disappeared session selected without
switching playbacks, and discard in-flight observations computed
before a wake-up.
Validate the packaged companion end to end: network cuts, suspension,
session stop and media change on the installed server, plus a seek
measurement against the burnt-in timecode of the Web witness player
(323 ms worst stable gap, 788 ms seek recovery). Record versions,
conditions and limits in docs/validation/issue-6.md; only Jellyfin Web
is declared validated.
Add the dated lot-level report (versions, protocol, results, mobile and
keyboard checks, verified vs untested) and point the README at it. Prove
the Quick Connect API limitation on the real installed server by refusing
an unknown secret with 404, and check that no credential label remains.
Show at both seams that a token refused while tracking clears the saved
connection, empties the previous account's display (details, pistes,
historique) and returns to Quick Connect without retrying or exchanging
the token, while a network outage or temporary server error keeps the
connection and lets tracking resume without a new approval. The
installed checks revoke the real companion token during playback with a
track selected and hold the Quick Connect check to observe the refusal
message; a second account in the session list proves it never renders.
The playback chooser drops the forgotten account's options instead of
keeping them cached, and a revocation that fails after a newer Quick
Connect attempt began no longer overwrites that attempt's state. Prove
the criteria at both seams: an in-flight subtitle extraction and a held
revocation across the disconnect at the browser boundary, and tracking
stopped, token revoked, other clients kept and display cleared on the
installed server.
On the disposable server, the first Quick Connect status check is answered
as pending, then reported expired; 'Générer un nouveau code' must obtain a
fresh code from the real server that a real authenticated account approves,
after which the companion connects by itself. At the browser boundary, a
tracking observation held across 'Se déconnecter' must not restore the
companion, and the disabled/outage retries are asserted keyboard-focused.
Also folds in two review cleanups: failUnexpected() for the duplicated
failure message, and a SavedConnection typedef.
Cover the criteria that had no evidence: unreadable or incomplete saved
data (including a whitespace-only token, now rejected when read) falls
back to Quick Connect without an authenticated request; the saved object
holds only token, userId and deviceId; and the token leaks into no URL,
message or console line. The installed reopen check now revokes the
companion's own token on the server before reopening the profile, instead
of injecting a 401, so the refused-token path is server-produced.
The initiation secret no longer lives in module state: waitForApproval
receives it as a parameter, so a late response from a superseded attempt
can never redeem a newer secret. Disconnect and rejected-token paths now
share clearConnection(), and the validation report no longer claims the
secret never transits in a URL (Jellyfin's Connect?secret= requires it);
it is still never displayed or persisted.
- Introduced "Writing Great Skills" and "Writing Shape" skills with YAML configurations.
- Created detailed documentation for the "Writing Shape" skill outlining the process of shaping raw material into articles.
- Added agent documentation for issue tracking, triage labels, and domain documentation.
- Developed a comprehensive context document for the subtitle companion, defining key terms and concepts.
- Documented architectural decisions for hosting the subtitle companion within Jellyfin.
- Established guidelines for using GitLab as an issue tracker, including conventions and merge request handling.
- Mapped triage roles to actual label strings used in the issue tracker.
- Conducted research on Jellyfin subtitle plugins, detailing existing capabilities and potential plugin contributions.