# Skin optimization — plan

Started 2026-10-12. **Status 2026-10-13 (night): everything built so far is released in plugin 2.1.69 / skin 1.3.56**
(B5 rows prepared by the service, D context menus from the service, E bytecode and quick actions without a plugin
start, F the user first). Nothing is committed to git. The user will say when to measure on the weak box.

## Handover: where we are and what comes next

An agent taking this over reads `AGENTS.md`, this section, then the part of the step it takes.

**Released (2.1.63):** phase 1 = M, B0–B4 below; Pokračovat and TMDb / ČSFD hidden by default, forced once on every
install (flag `MilionarDefaultsV9`); the temporary measuring code (`wcs/skin/perflog.py`, its start in
`wcs/skin/service.py`, the wrapper and the `milionar.perf.delay` switch in `main.py`) on purpose, until the end.

**Released 2026-10-13:** B5 (2.1.67 / 1.3.54, "B5" in part B), part D (2.1.68 / 1.3.55), parts E and F
(2.1.69 / 1.3.56; the ZIP carries 199 bytecode files, 7.3 MB).
Every Home row of every section is a plugin list whose items the service prepared; the plugin call only repeats
them (`wcs/skin/journal.py`). Pokračovat lost its own static mechanism and is a section like the others.

**Measured (Mac, `scripts/skin_harness/measure.py`, before and after alternating, a fresh Kodi for every repetition,
medians; "slow" = Kodi started with `taskpolicy -b`, efficiency cores only, a stand-in for a weak box):**

| | default 6 sections: before → after | slow: before → after | all 10 sections: before → after |
|---|---|---|---|
| Kodi start → every section loaded | 11.3 s → **2.8 s** | 29.0 s → **11.0 s** | 17.4 s → **5.8 s** |
| CPU time Kodi used until then | 6.1 s → 3.9 s | 27.8 s → 14.3 s | 9.9 s → 5.2 s |
| change → the rows on screen | 0.26 s → 0.25 s | 1.45 s → 1.14 s | 0.29 s → 0.26 s |
| change → every section | 4.7 s → 2.0 s | 8.5 s → 5.1 s | 4.5 s → 1.8 s |
| going to the last section before it is loaded: all its rows | 0.46 s → 0.12 s | 2.76 s → 2.38 s | 0.24 s → 0.08 s |
| a section that is loaded: key → rows | 0 → 0 | 0 → 0 | 0 → 0 |

Repetitions: 3 / 2 / 2 per state; the runs of one state differ by less than 0.5 s (slow: 1.4 s). Raw data:
`scripts/skin_harness/out/perf/{B,A}-{default,slow,all}.json` (B = before, A = after). Not improved: on the slow
machine a section visited before it is loaded still needs about 2.3 s (Kodi starting the plugin calls, not the
rows). The memory figure of the tool (resident size at the end) varies between 580 and 800 MB from run to run in
both states and says nothing.

**Next, in this order (each ends with the user's own test):**

1. Nothing waits for a release. `python3 scripts/release.py publish` needs `python3.11` on the PATH (bytecode).
2. **Measure on the weak Android TV box** (the user has one). `measure.py` drives a Kodi on this Mac only; on the box
   read the `[DEBUG][Milionář Perf]` lines of its `kodi.log` (`row … served in` / `built in`, `cards <sec> k/n`,
   `warm … end after`). Decide there whether `warm.BATCH` = 3 is right (each batch shows three sections' card
   images at once).
3. **C2: remove the Pokračovat section** itself (menu item, Home group 24000, settings, strings); its special
   mechanism is already gone.
4. **C3**, then part A (A2 first: it is the one that helps Kodi).
5. Last: remove the measuring code, update `docs/features/012-skin-milionar.md`.

**Open / known:**

- A single failed row still has no "could not load" text: a plugin list cannot tell "failed" from "empty". With B5 a
  failed build keeps the journal the row had, so the old cards stay; a row that never had any stays hidden.
- First start after an update: no journal matches the new code yet, the rows are built by the plugin calls as before
  while the service prepares them (once).
- Changing a slot's limit / sort / style or the Pro tebe mode asks for a URL that has no journal yet: built by the
  plugin call that once, prepared in the background for the next time (`journal.KEEP` = 6 journals per slot).
- Kodi sometimes hangs on quit on the Mac ("service.py: script didn't stop in 5 seconds"), seen before this work
  too (crash report 18:22 on 2026-10-12). Not investigated.
- Tests that failed before this work and still fail, unrelated: the ones expecting 6 slots per section
  (`80 != 63`, `77 != 60`, `my_movies.4`) and the context-menu label tests in `scripts/library_harness/test_menu.py`.
- `test_refresh_hooks` and `test_refresh_service` must be run as separate processes (one publication test fails
  only when both run in one process).

**Goal:** `skin.milionar` and the addon must be fast on **weak Android TV devices**. Smaller and simpler code
comes second. **Look and functions never change**, except the three removals the user ordered (part C).

## Decisions (user, 2026-10-12)

1. Target = weak Android TV boxes. Every step is judged by what it does there.
2. Scope = everything: skin and addon (Python).
3. Home shows only Milionář's own items, forever. Estuary's Home leftovers go away (part C).
4. The section **Pokračovat is removed** from the skin and the addon; **Pro tebe** replaces it (part C).
5. **TMDb / ČSFD is hidden by default** on Home.
6. A hidden section never loads data and never costs anything.
7. One loading principle for every section: always cache, always a visible progress, nothing ever stays
   empty, no section or row blocks another, every change shows at once (part B).
8. Talk to the user very briefly; ask non-technical questions with a recommendation; decide technical detail
   yourself.

9. Order: fixes B1–B4 → measurement → one principle (B5) → remove Pokračovat (C2). Between the phases the user
   tests it himself.
10. Measure on the Mac Kodi first (the agent measures in its own test); the weak box comes later.
11. Timing lines stay in `kodi.log` until the whole optimization is finished, then they are removed.
12. TMDb / ČSFD is hidden for **every** user through the existing forced-defaults mechanism
    (`wcs/skin/defaults.py`, flag `MilionarDefaultsV<n>`); do not build a second one, simplify it if possible.
13. Removing Pokračovat keeps each user's own row settings of Pro tebe.
14. B5 = rows pushed by the service (as Pokračovat does today); 20 cards per row at most.
15. Pause after phase 1; release it with Pokračovat and TMDb / ČSFD hidden by default, forced for every user.

## Rules for every step

1. One step at a time. Tick it only when done and checked.
2. Generated files (`do not edit` header) change only through their generator in `scripts/skin_harness/`.
3. Save the old output first (scratchpad); prove the new one behaves the same (see the step's "Check").
4. Run only the tests of the touched files. No full skin baseline unless the user asks.
5. `scripts/skin_harness/sync_dev.sh` copies the skin to the installed copy.
6. Another session shares the same Kodi: message it before keys or live runs.
7. Hand over with a short numbered test list. No deploy, no commit unless the user asks.
8. Write "Notes" in the step so the next agent can continue.

## Checklist

- [x] **M. Measure first** (Mac done 2026-10-12; the box later)
- [x] **B0. Change events reach the service** (bug found while measuring)
- [x] **B1. Pro tebe keeps its rows** (bug)
- [x] **B2. Hidden sections load nothing**
- [x] **B3. Loading indicator in every row**
- [x] **B4. Sections do not block each other** (as far as plugin rows allow; the rest is B5)
- [x] **User test of phase 1** ("funguje", 2026-10-12)
- [x] **C1. TMDb / ČSFD hidden by default** (and Pokračovat; released 2.1.63)
- [x] **Release 2.1.63 / skin 1.3.50** — PAUSE here
- [x] **B5. One loading principle** (built and measured 2026-10-13: rows prepared by the service, 20 cards)
- [x] **Release 2.1.67 / skin 1.3.54** (B5)
- [x] **D. Context menus and their quick actions without a plugin start** (built and measured 2026-10-13)
- [x] **Release 2.1.68 / skin 1.3.55** (D)
- [x] **E. Bytecode; clicks, buttons and menu entries without a plugin start** (built and measured 2026-10-13)
- [x] **User test of E** ("funguje to", 2026-10-13)
- [x] **F. The user first: background work steps aside** (built and measured 2026-10-13)
- [x] **Release 2.1.69 / skin 1.3.56** (E + F)
- [ ] **M2. Measure on the weak box**
- [ ] **C2. Remove Pokračovat** (hidden by default since 2.1.63; its static mechanism removed with B5)
- [ ] **C3. Remove Estuary's Home leftovers**
- [ ] **A0. Fix the sidebar doc**
- [ ] **A1. Shorter conditions on disk**
- [ ] **A2. Smaller hero-button logic** (the real win of part A)
- [ ] **A3. Remove dead generated names**
- [ ] **A4. Release check: false "missing image" warnings** (separate, not the skin)
- [ ] ~~One include for the Home sections~~ — dropped unless the user asks (maintenance only)

---

## M. Measure first

Numbers before any change, on the weak box and on the Mac: skin load, Home load, time from focusing a section to
its rows, time from "watched" to the row changing, frame rate while moving along a row. Rows log their build time
at LOGDEBUG (`widget_row <row> -> N items in X ms`, `warm <sec> end after X s`).

Known so far (Mac, `kodi.log`): skin load 0.08–0.16 s, Home load at most ~0.4 s. The box is unmeasured.

Notes (2026-10-12):
- Tools, all temporary until the plan's last step: `wcs/skin/perflog.py` (lines `[DEBUG][Milionář Perf]` at LOGINFO:
  every row build, section focus → rows ready, change event, publication, background loading), its thread started in
  `wcs/skin/service.py`, the wrapper around `widget_row` in `main.py`, and
  `Window(Home).Property(milionar.perf.delay)` = ms added to every row (imitates a weak box on the Mac).
- `python3 scripts/skin_harness/measure.py run LABEL [--reps N] [--slow] [--all-sections]` and
  `measure.py compare BEFORE AFTER` (rewritten 2026-10-13). Every repetition starts a fresh Kodi. It reads only
  the probe's `cards <sec> <rows with cards>/<rows>` lines, so the numbers do not depend on how rows are loaded:
  per section the time from Kodi's start to its first and to all its rows, the CPU time used, a jump to the last
  section right after the first screen, a walk through the loaded menu, and a change of the movie rows. The probe
  watches every section only while the file `perf_watch` exists in the addon's profile (the script writes and
  removes it). To compare two states of the code, alternate them (the session of 2026-10-13 swapped the changed files
  from two snapshots between runs); do not compare numbers of different days.
- The harness' menu focus must be `Control.SetFocus(9000,<n>,absolute)`: without `absolute` the position is relative
  to the list's view and lands elsewhere once the menu scrolls.
- Restarting Kodi from the harness brings Kodi to the front: keys the user is typing elsewhere land in Kodi (an `h`
  opens the PVR dialog, which also blocks the background loading). Close it with `Dialog.Close(all,true)`.

| Mac | before (`baseline`) | after phase 1 (`after-fixes`) |
|-----|--------------------|-------------------------------|
| back to Pro tebe | 0 of 6 rows, stays empty | 6 of 6 rows, 52 ms |
| change → row on screen | waits for the 5 s poll | 0.3 s |
| change → other sections | 3 s for 1 section (Pro tebe never) | 5 s for 2 sections |
| a section after the background pass | ~50 ms | ~50 ms |

With 1000 ms added per row (`slow-box-1000ms`): a section not loaded yet showed its first row after 7 s, because its
row calls queued behind 21 calls of other sections already handed to Kodi. That is the limit of plugin rows → B5.

## Part B — loading (how it works today, what is wrong)

Two principles exist side by side:

- **Pokračovat (`cont`)**: static items drawn from skin strings that the service writes (`wcs/skin/startrows.py`,
  `gen_start.py`). No plugin run; on screen in the first frame.
- **Every other section, also Pro tebe**: each row is a `plugin://…widget_row` call (`gen_slots.py` `CONTENT`).
  A row reloads when its revision skin string (`mil.<sec>.<n>.rev`) changes. Until
  `Window(Home).Property(milionar.go.<sec>)` is set or the menu item is focused, the URL is an empty playlist.
  `wcs/skin/warm.py` opens the gates and loads sections off-screen, one after another.

Found by reading the code (2026-10-12):

- **B1.** `warm.SECTIONS` has no `foryou`, so `milionar.go.foryou` is never set. Leaving the Pro tebe menu item
  switches its 7 rows to the empty playlist; coming back runs 7 plugin calls again. This is the "section is gone
  when I come back" bug. Changes to Pro tebe rows are never loaded off-screen either.
- **B2.** `refresher.active_rows()` reads the slots of every section, hidden ones too, so hidden TMDb / Kraska
  still fetch from the network. `refresh.publish` also walks hidden sections.
- **B3.** A row has no loading indicator (`shown` = has items or is updating, nothing is drawn while updating).
  An empty or failed row looks the same as a loading one.
- **B4.** The warm thread loads one section at a time: at least ~2.2 s each (dwell + stable polls + quiet hide),
  at most 20 s. One slow section holds all the others. A watched change marks ~10 row types dirty, so several
  sections queue up. `milionar.go.all` (the thread's failure fallback) is not read by the skin.
- **B5.** Every update is a full row reload through a plugin call, in every slot that shows that row type. That
  is the delay between "watched" and the screen on a weak box.

Target (decision 7): one principle in all sections. Two candidates, to be chosen by the numbers of step M:

- **Service-pushed rows** (what Pokračovat does): no plugin call, first frame instant, the service updates only
  what changed. Cost: many values to write, a fixed number of cards per row (12 today).
- **Plugin rows, repaired**: gates stay open, the last snapshot shows at once and is replaced when the fresh one
  is ready, rows load in parallel, indicator always visible.

B1–B4 are needed in both cases and come first.

**B5. Built 2026-10-13: rows prepared by the service, handed over by the plugin call.**

What was decided with the user was "the service fills the rows, no plugin call per row". Two ways of doing exactly
that were tried first and are **not** used; the reasons matter for whoever touches this again:

- **Binding the items into the Home lists from the service** (`xbmcgui.Window(10000).getControl(id).addItems()` into a
  list without `<content>`): works, looks identical, every row of Home was filled 2 s after Kodi's start. But Kodi
  runs a list's `<onclick>` (and its own click) **only for a list that has content of its own**
  (`CGUIBaseContainer::OnClick` checks `m_listProvider`); Select on a bound list does nothing. It can only be worked
  around with the Home keymap (key → Home property → skin timer → the card's action; tried, it works), and a keymap
  does not see Select from JSON-RPC remotes (`Input.Select`), touch, or controllers without a mapping, it loses
  Kodi's click sound, and the keymap file lives in userdata and would also act in other skins. That changes
  functions, so it was dropped. (Also: the Python window object must outlive every call on a control it handed out,
  or Kodi crashes.) The attempt is in the session's scratchpad only, not in the tree.
- **Static items for every section** (what Pokračovat did, `startrows.py` / `gen_start.py`): a static item's
  labels are refreshed once a second, every skin string / Home property name is registered by a linear search
  (20 cards × ~35 fields × 36 rows ≈ 25 000 names, quadratic at every skin load), and the skin settings file grows by
  megabytes. Not built.

**What is built (one principle for all 11 sections, Pokračovat included):**

- A Home row stays a plugin list, so click, context menu, mouse, touch, sounds and remotes are Kodi's own.
- `wcs/skin/journal.py`: the service builds every row exactly as its plugin listing does (the real entry point
  `wcs.library2.rows.run` with the parameters of the row's URL; `xbmcgui.ListItem` writes down every `set…` / `add…`
  call while the building thread runs) and keeps that journal in a Home property and in
  `addon_data/plugin.video.milionar/row_journal/<sec>.<slot>.<hash>.json`.
- `main.py`: `action=widget_row` first calls `journal.serve()`: it repeats the calls of the journal that belongs to
  exactly those URL parameters, the current revision of the row's data and this version of the code (the time main.py
  was installed) and returns. No module of the addon is imported, nothing is computed (0–50 ms instead of
  80–500 ms on the Mac). Without such a journal the call builds the row as before, so a row is never empty and never
  older than its data, also when the service is busy or not running.
- After a restart of Kodi the journals on disk answer the first screen before the service has started.
- `wcs/skin/refresh.publish`: for a changed row type the service prepares the journal first and only then changes the
  row's revision skin string, so the reload Kodi starts is answered at once; the section on screen comes first.
- `journal.run` (service thread): any row the Home screen can ask for that has no journal yet (after an update, a
  new slot, another limit, sort or Pro tebe mode) gets one in the background.
- `wcs/skin/warm.py`: the other sections are loaded off-screen in batches of `BATCH` = 3 (per-section properties
  `milionar.warm.<menu id>` / `milionar.warm.quiet.<menu id>` in `Home.xml` and `Includes_Animations.xml`) instead of
  one after another, with short waits, because a row call is cheap now.
- At most 20 cards per Home row (`journal.normalize`, both for prepared and for self-built rows); the widget
  manager still offers 30 / 50, they act as 20.
- Pokračovat is a section like the others: `STATIC` kind, `gen_start.py`, `wcs/skin/startrows.py`,
  `Includes_MilionarStart.xml` and their tests are removed (the first half of C2). Old `mil.st.*` skin strings stay in
  the skin settings of existing installs; nothing reads them.
- Tests: `scripts/skin_harness/test_journal.py` (new), `test_warm.py` (rewritten), `test_cinema.py`,
  `test_refresh_service.py`, `test_refresh_hooks.py`, `scripts/library_harness/test_hero_buttons.py` adjusted.

## Part D — context menus (2026-10-13)

The user: on a slow machine a context menu sometimes needs 10 seconds, worst on the rows of the Home sections.

**What was wrong.** Every menu started the plugin (`RunPlugin(…?action=skin_context)` from the Home keymap,
`skin_context_native` from `DialogContextMenu.xml`), and so did every entry chosen in it. Building the menu takes
about 20 ms in a process that has the code loaded; the rest was the plugin run. Kodi's Python writes no bytecode
(`sys.dont_write_bytecode` is true; the `__pycache__` directories in the tree come from offline tests), so every run
compiles the modules it imports again, and when the reused plugin interpreters are busy with rows the run waits or
starts a cold one.

**What is built.**

- `wcs/skin/commands.py`: the keymap (`resources/keymaps/milionar-home.xml`, six bindings) and
  `DialogContextMenu.xml` send `NotifyAll(plugin.video.milionar,skin_context | skin_context_native)`; a service thread
  with its own `xbmc.Monitor` gets it and runs `wcs.skin.context.home_context` / `native_context` in a new thread.
  Kodi hands a notification to Python only in the thread that made the monitor and only while it waits in
  `waitForAbort` (steps of 100 ms), so that thread does nothing else and waits in steps of 20 ms (250 ms while
  something plays). The modules a menu needs are imported once after the start. The plugin actions of the same names
  stay in `main.py`.
- `wcs.skin.context.run_chosen` / `run_local`: `ctx_mark`, `ctx_mark_toggle`, `ctx_hide_continue`, `ctx_list`,
  `ctx_list_toggle`, `ctx_next`, `mplay_req` and (in our skin) `show_wcs_details` are done in the process that shows
  the menu, exactly as `main.py` does them; a failure there falls back to the builtin. Every other entry is the
  builtin it was. This also covers the menus of the addon's own dialogs (`dialog_context`), which already ran in
  their dialog's process.
- Same entries, same order, same Kodi context dialog; the menu manager, the widget manager on a menu item and the
  hero button editor are reached as before (checked live).
- Tests: `scripts/skin_harness/test_commands.py`.

**Measured** (Mac, `measure.py context LABEL [--slow] [--busy]`, before and after alternating, fresh Kodi per run;
time from the key to the moment the menu is handed to Kodi's dialog, median of 5–6):

| | normal | slow | slow, rows reloading at that moment |
|---|---|---|---|
| menu on a film card | 0.06 → 0.06 s | 0.31 → 0.22 s | 0.81 → **0.20 s** |
| menu on a Pro tebe card | 0.07 → 0.06 s | 0.36 → 0.40 s | 0.86 → **0.15 s** |
| menu on a genre tile | 0.05 → 0.07 s | 0.35 → 0.21 s | 0.47 → **0.11 s** |
| a menu action (mark) → done | 0.07 → 0.01 s | 0.25 → 0.10 s | 0.42 → **0.08 s** |

Worst single menu while rows reload: 2.36 s before, 0.30 s after. On an idle Mac a plugin start is cheap, so opening
the menu is the same there; the gain is where the plugin start is expensive or has to wait. **The weak box is not
measured**; its log now has `context menu open` and `action … done` lines.

## Part E — bytecode, and quick actions without a plugin start (2026-10-13)

Decided by the user after part D: ship compiled code, and make clicks, buttons and all menu entries as fast and as
consistent as the context menu.

**What was measured first (Mac; "slow, busy" = Kodi on the efficiency cores while rows reload):**

- A plugin start that does nothing costs 0.05–0.08 s idle and 0.4 s slow and busy: the script start itself, before
  any import. All Python in Kodi shares one interpreter lock, so a start competes with the rows being prepared.
- Kodi's Python writes no bytecode and runs with `-O`: it looks for `<module>.cpython-311.opt-1.pyc`. Compiling this
  addon's 198 modules takes 227 ms on the Mac, loading them from bytecode 7 ms; `import requests` (with urllib3,
  chardet, idna, certifi) 130 ms without and 43 ms with bytecode.

**Built.**

- **Bytecode in the release** (`scripts/build_zip.sh`): every importable module of the ZIP gets
  `__pycache__/<module>.cpython-311.opt-1.pyc`, made from the bytes the ZIP ships by `python3.11` (the Python of
  Kodi 21), hash-checked: Python compares each with its source on every import and ignores one that does not belong
  to it, so stale bytecode cannot run. The build verifies header, hash and that each file loads. ZIP 5.6 → 7.3 MB.
  `main.py`, `service.py`, `context.py` are scripts: Python caches nothing for them.
- **Bytecode made on the device** (`wcs/skin/bytecode.py`, service thread, 90 s after the start, never while
  something plays, once per version of the addon and of each library; marker
  `addon_data/plugin.video.milionar/bytecode.json`): the same hash-checked files for whatever Python and
  optimization level the device runs, for the addon when the shipped ones do not fit, and for the `script.module.*`
  libraries the addon depends on (requests, urllib3, certifi, chardet, idna, unidecode), written into their own
  `__pycache__` folders. A folder that cannot be written is left alone.
- **One place for the addon's quick actions** (`wcs/skin/local.py`): `ctx_*`, `mplay_req`, `show_wcs_details` (our
  skin), `skin_detail_*`, `skin_slots`, `skin_wm`, `skin_menu_item`, `skin_hero`, `skin_context_edit`,
  `skin_channel_edit`, `skin_classic`, `skin_accent`, `channel_play`, `foryou_*`, `vinyl_*` and the like. `main.py`
  calls it for a plugin run (its own copies of these branches are gone), the service for a request of the skin,
  a context menu for the chosen entry. Listings, resolvers, and what opens the addon's big dialogs or the game
  (through `wcs/router.py`) are not in it and stay plugin runs.
- **The skin sends every builtin of a card, a hero button and of its own buttons to the service**:
  `NotifyAll(plugin.video.milionar,run:<builtin>)` (the builtin goes as it is, nested parentheses and commas
  included; checked). `wcs/skin/commands.py` hands it to `wcs.skin.context.run_chosen`: a quick action in a service
  thread, anything else as the builtin it is. While the listener runs, `Window(Home).Property(milionar.service)` is
  set; each such place in the skin has a second line without it, so the skin runs the builtin itself when the
  service is not there (before it is up, or without it): a click is never lost. Generated by
  `gen_slots.run_lines` / `click_variable` (rows and hero buttons in `gen_cinema.py`, Estuary-widget rows in
  `gen_slots.py`, the Home detail in `gen_detail.py`); the hand-written files (`Custom_1123_MilionarRows.xml` 20,
  `SkinSettings.xml` 13, `Home.xml` 2, `Custom_1108_MilionarSettingsMenu.xml` 1) carry the same two lines.
- Tests: `scripts/skin_harness/test_commands.py`, `test_bytecode.py`, `test_cinema.py`.

**Measured** (Mac, before and after alternating, fresh Kodi per run, medians of 5):

| | idle | slow, rows reloading |
|---|---|---|
| hero button "Více informací" → the detail is open (`measure.py clicks`) | 0.18 → 0.13 s | 1.40 → **0.59 s** |
| a quick action sent by the skin (mark) → done | 0.07 → 0.06 s | — |
| the MyTV dialog through a plugin start, without → with bytecode | 0.31 → 0.18 s | 3.45 → 2.75 s |

With the service "not there" (property cleared) the same click takes 0.16 s through the plugin and works.

**The addon's big dialogs** (MyTV, similar titles, source search, the game) still start the plugin, on purpose:
they are large and long-running, and in the service a fault of theirs would take the rows with it. What their start
costs and what was done about it is part F.

## Part F — the user first (2026-10-13)

The user: speed up the big dialogs too, without breaking anything.

**Measured** (MyTV dialog through a plugin start, with bytecode): idle 0.18 s; on the slow Mac 0.8 s when nothing else
runs and 2.7 s while rows are being prepared. In that last case `from wcs import router` alone took 1.7 s, the same
import that takes 0.06 s idle: all Python in Kodi shares one interpreter lock, so the user's plugin run was waiting for
the service's background work, not doing more work. Importing less in `wcs/router.py` would save at most those 0.06 s
(idle) and means touching 1700 lines of old code; not done.

**Built** (`wcs/skin/userfirst.py`): `mark()` at the very start of every plugin run that is not a Home row
(`main.py`) and for every request the skin sends to the service (`wcs/skin/commands.py`); for `HOLD` = 2 s after it
the background steps aside: the journal thread before each row, `refresh.publish` before each row of a section that
is not on screen (the one on screen is not delayed), the warm thread before a batch, the cache builders before a
job. Nothing is cancelled, only delayed, never longer than a few seconds.
`Window(Home).Property(milionar.perf.nouserfirst)` switches it off (temporary, for measuring).
Tests: `scripts/skin_harness/test_userfirst.py`.

**Measured** (slow Mac, rows reloading, the same Kodi run, switch alternating, 6 each): MyTV dialog 2.27 → **1.25 s**.
Altogether for that case: 3.45 s (2.1.68) → 2.75 s (bytecode) → 1.25 s.

Longer jobs that are not loops of the service step aside too: `userfirst.breathe()` between the phases of the Pro
tebe rebuild (`wcs/foryou/engine.py`), between the seasons of a show's metadata refresh (`wcs/library2/tmdb.py`) and
between the files of the bytecode pass. It waits only in a thread that called `userfirst.background_thread()` (the
service's cache builders and the Pro tebe thread), so a rebuild the user starts himself never waits for itself.

**Left:** the `ctx_open` context item of other skins still starts the plugin.

## Part C — removals ordered by the user

- **C1. Done, released 2.1.63.** `HomeMenuNoMilTMDbButton` and `HomeMenuNoMilContinueButton` are set in the
  shipped defaults, flag `MilionarDefaultsV8` → `V9`, so every install applies them once on its next Home load.
  To force defaults again later: set the wanted state in the maintainer's Kodi, raise the flag to V10 in
  `wcs/skin/defaults.py` and `Home.xml`, release with `--snapshot-defaults`. `wcs/skin/startrows.py` builds nothing
  while Pokračovat is hidden.
- **C2.** Remove `cont`: Home group 24000 and menu item `mil_continue`, `gen_start.py`, `wcs/skin/startrows.py`,
  the `STATIC` kind in `gen_cinema.py`, its settings, strings (cs/sk/en), tests and docs. `warm.FIRST` becomes Pro
  tebe. Do it only after B5 is decided: today `cont` is the only screen that is there in the first frame.
- **C3.** The Home menu already lists only `mil_*` items. Remove what is left of Estuary's Home: unused includes
  in `Includes_Home.xml` (1081 lines), the `HomeMenuNo*Button` switches for Kodi's own items in
  `SkinSettings.xml`, their strings. Kodi's other windows (player, settings, dialogs) stay. Update
  `skin-milionar/030-fork.md` §4 and the `difftree` list.

Notes: —

## Part A — code size

Measured 2026-10-12 (nothing was changed; step A1 was tried in memory only):

- `Includes_MilionarCinema.xml` is 1.87 MB; 1.19 MB are expressions.
- Kodi expands every `$EXP[...]` when the skin loads. After expansion it holds **24.9 MB** of condition text;
  22.8 MB are 30 expressions (`HasB1`–`HasB4`, `B1Play` in six sections), ~0.76 MB each.

**A0. Sidebar doc.** `skin-milionar/230-sidebar.md`: collapsed edge 95 (not 110), shift 367, content width 1825
(not 1810), backdrop zoom 125.17 %; the right-edge items sit 367 px in and slide with the content. Verified
against `Includes_MilionarSidebar.xml`.

**A1. Shorter conditions on disk.** `gen_cinema.py` `any_of()`: use `$EXP[MilCineIs_{sec}_{n}]` instead of writing
`source(sec, n)` out. Result: 1.87 → 1.05 MB. Check: expand every `$EXP` in old and new output; the texts must be
identical (they are). `test_cinema`, `test_detail`, `test_navigation` pass. **Kodi gains nothing** from this step
(same text after expansion); it only makes the file readable.

**A2. Smaller hero-button logic.** Each button state repeats "nothing is lent" (`!MilCineLent`, 42 row pairs) in
all 7 branches and "slot n feeds the hero" 42 + 7 times. Regroup in the generator: take `!Lent` out once, group
the lent cases by source slot. Estimated 25 MB → 1–2 MB after expansion (not tried yet). Check: the text is no
longer identical, so prove by machine that old and new formulas give the same result for every combination of
their inputs. Then measure Home load on the box.

**A3. Dead generated names** (verified by a name search over skin XML and Python): variables
`MilCinePlay_<sec>`, `MilCineDetail_<sec>` (7 each), `MilCineLeft_vinyl`, `MilCineRating_vinyl`; expressions
`MilCineHasCaption_vinyl`, `MilCineHasRating_vinyl`, `MilCineResumable_vinyl`; include `MilCineDetailNavigation`.
Stop generating them. Check: search again before removing; no new "unknown" lines in the Kodi log after a reload.

**A4. Release warnings.** Six images are reported as missing though `wcs/ui/assets.py` (`NATIVE_MAP`) maps them
to Estuary textures. The warning is printed during `release.py publish` by `scripts/build_zip.sh`. Never touch the dev backup
rule (100 % copy).

**Later, only after A2 and M:** let the service keep `milionar.row.<sec>` on a valid row, so each hero condition
is one comparison. Risk: the hero waits for the service.

Notes: —
