Skip to content

Fix the entity lists loaded for another episode, and various improvements - #2195

Merged
NicoPennec merged 26 commits into
cgwire:mainfrom
NicoPennec:main
Sep 4, 2026
Merged

Fix the entity lists loaded for another episode, and various improvements#2195
NicoPennec merged 26 commits into
cgwire:mainfrom
NicoPennec:main

Conversation

@NicoPennec

Copy link
Copy Markdown
Member

Problem

  • An entity list mounted after an episode switch keeps displaying the cache of the episode left behind. The topbar sets the current episode before the page instance exists, so the currentEpisode watcher cannot fire, and the loaded rows say nothing about the scope they belong to.
  • A pinned task type header in the episode, sequence, and shot lists points at the wrong route section, so an episode switch from there lands on the production homepage.
  • The task type failure message of the playlist modal points at a key that does not exist, and renders raw.
  • A remote task update on a task type page rebuilds the search index twice, including for tasks of a task type that the page does not display.
  • A new comment rebuilds both done-task indexes from data the mutation never touched.
  • chart.js and the datepicker ship in the entry preload, although a few screens draw a chart or pick a date.
  • Several locales carry untranslated CSV import keys, mismatched terminology, and broken sentence assembly.
  • The unit suite pays a Node bootstrap per spec file and re-walks the same heavy dependency graphs.
  • CI runs superseded jobs to completion and rebuilds the Vitest prebundle on every run.
  • Some npm dependencies are outdated.

Solution

  • Record the production and episode a dataset was loaded for, and let each page compare it with the scope it displays to decide whether to reload on mount.
  • Point each list at its own section.
  • Use the existing playlists.change_task_type_error key.
  • Debounce the refresh into a single rebuild, and skip updates that target another task type.
  • Drop the two dead rebuilds.
  • Register the chart tags as async components, import the datepicker in DateField, and isolate the oxc runtime helpers in their own chunk so they stop pulling the charts chunk into the entry graph.
  • Translate the missing keys and unify the terminology across locales.
  • Run the suite on worker threads, enable the Vitest dependency optimizer, and drop jsdom for the DOM-free specs.
  • Cancel superseded runs on the same branch, and cache the prebundle between runs.
  • Bump npm dependencies.

Note
The helpers chunk group in vite.config.js matches a rolldown-internal virtual module ID. If that format changes, the group silently stops matching, and the entry graph reverts to its previous shape, which is a size regression rather than a crash.

Spreading k sequence arrays as call arguments risks a RangeError past the
engine argument limit, which varies with the remaining stack depth. flat()
has the same complexity without going through the argument stack.
Four chained concat calls allocated four arrays per asset where one
literal with spreads does the same job. Word order is unchanged, so the
resulting index is identical.
Registering vue-date-picker globally pinned @vuepic/vue-datepicker and its CSS
to the entry payload for every page, while DateField is the only component that
renders one. Importing it locally drops the entry preload by 69.4 kB gzipped.
chartjs-adapter-date-fns pulled date-fns into the charts chunk, and the oxc
lowering helpers that chunk shares with the entry pinned it to the initial
payload: chart.js was downloaded on the login screen, before authentication.

Isolating those helpers in their own group and registering the chartkick tags
with defineAsyncComponent drops the entry preload by 88.0 kB gzipped, from
488.7 to 400.7 kB.
The guard forces an empty search field, so the nextTick onSearchChange falls
into resetTasks(), which calls resetTaskIndex() on the same data one microtask
after the explicit call did. Nothing reads taskIndex in between, so the first
rebuild is dead work: 99 ms at 2000 entities, 277 ms at 5000.

The handler also fired on tasks belonging to another task type, which getTasks
filters out anyway, so the rebuild could not change anything on screen.
NEW_TASK_COMMENT_END only touches an entry of state.todos / state.personTasks.
The done lists come from a separate fetch (is_done=True) and share no object
with them, so rebuilding doneIndex and personDoneTasksIndex re-indexed
untouched data on every posted comment.
The topbar commits the new current episode before the page instance exists
(Main.vue renders it ahead of the router view, so its $route watcher is a
pre-flush job of lower uid). Assets.vue's currentEpisode watcher, the only
trigger reloading on a scope change, therefore cannot fire when the route
lands from another page: coming back from a task type on another episode kept
displaying the previous episode's assets until a manual reload.

mounted() only tested the shape of the cache (map size, validation columns,
validations of the first asset), never the scope it was loaded for. The store
now records that scope, the way the shots store already does, and the page
compares it. Assets cast in from other episodes are legitimate rows of an
episode load, so the check cannot be based on the rows themselves.

Two other paths to the same stale list: loadAssets() handed its in-flight
promise to any caller regardless of the episode requested, and reset() bailed
out on isAssetsLoading, dropping an episode switch made during a load.
Same staleness as the asset list: the topbar commits the new current episode
before the page instance exists, so the currentEpisode watcher cannot fire when
the route lands from another page, and mounted() only tested the shape of the
cache, never the scope it was loaded for.

The load already computed that scope to queue concurrent loads, but kept it in
the non reactive module cache, where a getter would have been pinned to its
first value. It moves to the state, records 'all' as itself so a page can
compare it with the episode it displays, and the chained reload now waits for
the loading flag to be down, which a response discarded on a production switch
would otherwise leave up forever.

The section watcher went through this.edits, which the page never mapped: it
threw a TypeError on every section change instead of reloading.
Same staleness as the asset and edit lists: the topbar commits the new current
episode before the page instance exists, so the currentEpisode watcher cannot
fire when the route lands from another page.

mounted() compared the production of the first cached sequence, which holds
just as well for an episode of the same show, and the section watcher compared
the episode of the first displayed row, which an empty search result or a
production-wide dataset defeats. The load now records the scope it fetched,
'all' included, and the page compares it.
initTaskType resolved without loading as soon as the entity map held two rows,
whatever episode had filled it. Reaching a task type page on another episode
(a bookmark, a browser back, a topbar switch) therefore kept the map of the
previous one, and the page, which filters on the current episode, showed an
empty list.

The asset, edit, sequence and shot stores now all record the scope they
fetched, so the cache can be compared with the episode being displayed instead
of being trusted on its size alone.
loadEditsData compared the production of the first cached edit, which holds
just as well for another episode of the same show, while loadShotsData right
above it already compared the episode. It now compares the scope the edits
store records, which also tells the all pseudo-episode from a real one.
The validation headers of a list are rendered twice, once for the pinned
columns and once for the scrolling ones, and the pinned block was a copy that
kept the type of the list it was copied from: shots offered 'assets',
sequences and episodes offered 'editor'.

Nothing reads that segment of the task type URL except the topbar, which
derives the current section from it. An unknown value resolves to no section
at all, and switching episode from a task type reached through a pinned column
lands on the production homepage instead of the list left behind.

The added test compares both blocks of every list, since the defect comes from
the duplication rather than from any one value.
Without `enabled`, Vitest disables optimizeDeps and the `include` list was
dead config. sanitize-html stays out: pre-bundled for the client it pulls
postcss and its fs/path/url access, which Vite externalizes with a warning
per spec file.
- Add sequences/episodes/edits.fields.time_spent: the lists and the CSV
  exports already displayed them, so the raw key was showing up.
- Point the task type modal at playlists.change_task_type_error, the key it
  asked for lived under a playlist namespace that does not exist.
- Restore the leading space of notifications.and_change_status and drop the
  preposition that duplicated to_status in ten locales.
- Leave people.role.undefined empty everywhere, as en.js does.
- Add the final period on the sentences that lacked it and tighten the
  markdown lists of the quota explanations.
@NicoPennec
NicoPennec merged commit 5f59286 into cgwire:main Sep 4, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant