# YSP Web Project Notes

## Runtime flow

Browseren gar till `index.php`.

`index.php` laddar `index.html`, raknar fram ett versionsnummer fran viktiga filer och ersatter `__APP_VERSION__`.

`index.html` ar bara HTML-skalet. Den laddar:
- `assets/css/app.css`
- `assets/js/app.js`

`assets/js/app.js` pratar med backend via `api.php?action=...`.

## Important files

- `index.php` - entrypoint for the web app and cache/version wrapper.
- `index.html` - page markup only.
- `assets/css/app.css` - all app styling.
- `assets/js/app.js` - all frontend logic and screen rendering.
- `api.php` - API router, account/session logic, state handling, teams, manager mode, transfers and match simulation.
- Keep `code/.htaccess` as the only dot file under `code/`. Internal PHP, catalogs and runtime data live under `code/private/`.
- `private/lib/app_version.php` - shared app-version helper used by both `index.php` and `api.php`.
- `private/lib/http.php` - JSON responses, request body parsing and CSRF/request checks.
- `private/lib/manager_progression.php` - promotion/relegation/playoff helper logic.
- `private/legacy-player-names.json` - player-name source. Official club squad pages are the primary source for names/positions; old game data and third-party sites are only fallback sources.
- `private/player-nationalities.json` - player nationality source for manager squad flags. Regenerate with `php tools/import_player_nationalities.php`.

## Runtime-generated files

These should not be copied into a clean build:

- `private/data/state.json` - users, chosen teams, league state and current app state.
- `private/data/state.json.bak` - backup of state.
- `private/data/state.lock` - write lock for state.
- `private/manager-saves/` - manager save snapshots.
- `private/manager.log` - runtime log.

The clean `project` copy starts without accounts because these files are absent.

The webserver must be able to write in `private/`, `private/data/`, `private/manager-saves/`, `private/manager-save-backups/`, `uploads/manager-chat/` and `reports/`. Release zips include these as empty writable runtime directories.

Only build a release zip when explicitly asked. When asked, create the zip from the cleaned root layout (`index.php` plus `code/`) and put it in the requested tmp folder.

## Assets

- `assets/flags/` - team flag images.
- `assets/icons/` - favicon and app icons.
- `sounds/` - crowd, kick, whistle and crit sounds.

## Recent manager-mode notes

- Player squads must follow each club's official website first. If an official club squad page lists different players than Transfermarkt, FMInside, Sofascore, Wikipedia, or older imported data, the official club page wins. Use third-party sources only to fill missing fields such as nationality, age, position detail, or to verify a name that the official page omits.
- Official-squad data cleanup order is:
  1. National League
  2. League Two
  3. League One
  4. Championship
  5. Premier League
  Work through one league at a time and do not batch-update later leagues before earlier leagues have been checked against official club websites.
- Scout unlocks live on the user's current manager finance. `manager_sync_current_user_scout_finance_to_participants()` copies only scout-related fields into matching human participant snapshots for the current save chain, so old snapshots cannot revert a hired scout to `null` without copying balance, squad or broader finance data across saves.

- Touchlines live view has saved match speed controls at 25/50/75/100/125/150/175/200 percent. The value is stored in browser localStorage under `yspMatchSpeedPct`.
- Club logo images open the shared logo modal from every visible placement that uses `img.team-logo` or `.team-logo-line img`, including the top bar, match rows and league tables.
- Manager menu options hidden by access/state must use both `hidden` and `.hide`; the redesigned manager menu has high-specificity grid button styles, so keep the explicit `#managerActionList button[hidden]` CSS guard.
- Cross-save turn alerts live above the manager menu options. When there are no alerts, `#managerTurnAlerts` must be both `hidden` and `.hide` so the fixed-grid manager menu does not leave an empty row above LEAGUE TABLE.
- Manager menu order starts with LEAGUE TABLE, then ECONOMY. Keep economy near the top because it is a frequent season-management action.
- The manager menu label for `managerStatsInfoOption` is `INFO`; keep the internal action/id as `statsInfo`.
- In Touchlines post-match/report-open layout, the grid rows are `head`, `pressure`, `pitch`, then controls/stats/report. The large `minmax(...dvh)` row must stay on the pitch row, not the pressure row, or a black gap appears above the pitch after full time.
- Match reports now expose explicit `goalScorers`. The frontend uses that list before guessing scorers from report player rows, and scripted live goals rewrite the displayed scorer to the player actually found on the scoring side.
- Cup summaries use public cup rows from `manager_public_cups()`. If a related human save still has the same cup ongoing, winner/runner-up are hidden and the cups page shows ONGOING.

## Current split status

Frontend is split into separate HTML, CSS and JS files.

PHP has a first shared lib split for app versioning and HTTP/request helpers. `api.php` is still the large file and should be split later into smaller modules:
- state/session
- users/auth
- team/player data
- manager league
- fixtures/results
- transfers
- match engine/reporting

## Manager form rule

Daily form dots are player-bound, not slot-bound. The backend locks the boost/drop players from the next fixture's starting XI the first time the fixture form is shown or used. After that, swapping a player between starting XI and bench moves the dot with that player until the fixture is played.

If a locked daily form points at a player who no longer exists in either STARTING or BENCH, the backend may repair that slot to another valid squad player. Do not repair just because the player moved to the bench; the dot follows the player until the fixture is played.

Squad players keep a natural role and a separate slot role. `role`/`naturalRole` is the player's real position; `slotRole` is the position he is currently placed in. The formation board shows the slot role in the white role badge, and only marks a player red when the natural role and slot role are incompatible. Best Lineup assigns the whole XI globally, not slot-by-slot greedily, so it does not steal the only RB/LB for an earlier CB/CM slot when a better total lineup exists.

On narrow mobile squad views, formation X coordinates are compressed slightly and player cards are narrower so 4-4-2 wide roles fit inside the fixed pitch. Do not widen the mobile pitch cards without checking LB/LM/RB/RM edge positions at about 350px viewport width.

When a club is in league formtop, daily form changes from one +10% player and one -10% player to two different +10% players and no -10% player.

League form is streak-based. Three straight league wins gives the club a green up arrow in the league table and +3% match power. Three straight league losses gives a red down arrow and -3% match power. Draws reset the streak and cup matches do not count.

Human sim results are briefly masked from other users while the local sim playback is running. The fixture is shown as LIVE with a progressive live score, and the public table is calculated without those hidden league results until the reveal delay has passed. Do not hide the whole league table during a live sim; show the table without the masked result instead.

Masked LIVE fixtures include their matching report inside `managerSeason.liveSimulations` so other managers in the same season can click the LIVE card or LIVE fixture row and open the Touchlines spectator view. Do not rely on `managerSeason.matchReports` for this, because that list is still viewer-owned report history.

The main `START GAME` button targets the shown NEXT fixture, even if CPU-only rounds or a finished-round continue step sits in front of it. The backend can fast-forward those safe blockers, but it must not auto-continue while the viewer's result is still hidden by the reveal delay.

Future NEXT fixture requests may arrive with only the target round. The backend resolves that to the viewer's own pending fixture in that round, so a stale continue blocker does not trap the user before the next match.

START GAME requests can also arrive with an old fixture index when a phone has rendered an older manager state. `managerNextRound` normalizes a stale request to the viewer's fresh pending fixture in the active round before returning `staleRound`; otherwise the user has to press START GAME/READY twice. The frontend also retries once after a `staleRound` response if the refreshed state shows a pending viewer fixture.

The live 2D engine blocks ping-pong passing: after the same two players have passed between each other four times in a row, that pair is not selected again until another teammate is used. A player also cannot keep carrying the ball after five match minutes; the next decision must be a shot or a pass/release.

Watched manager matches with a prebuilt report must not inject report goals directly into the scoreboard. Report goals are converted into visible scripted attacks and forced shots, so the score changes only after the ball is shown going toward goal. This keeps the 2D view and the reported result in the same world.

After any visible live-view goal, the 2D engine keeps the ball inside the drawn goal net briefly before kickoff. Do not call kickoff immediately after incrementing the score, because that makes mobile/desktop viewers see the ball back at midfield while the event log says goal.

Touchlines live view shows a compact goals strip under the pressure bar and above Events. It is driven by actual `GOAL - scorer | A: assist` engine events, not by the final report alone, so scorer/assist appears only when the visible goal has happened.

The Touchlines goals strip must own its own vertical expansion/scrolling. Do not put a small max-height on the inner scorer list; otherwise later goals are clipped and only the first scorer appears. Keep the visible scorer box tall enough for several goals before scrolling; it should not feel like a one-line ticker.

Touchlines also shows a short pitch overlay for live goals. When `tlProRecordGoalEvent` records a new `GOAL - ...` event, `touchlinesGoalFlash` displays `GOAL!! player` over the pitch for about 2 seconds, then disappears.

Touchlines live layout order is match header, pitch, pressure bar, stat bar, goals strip, then Events/Reporter. Keep POS/SHOTS/PASSES/TACKLES/ENGINE below the pressure bar so the pitch is not pushed down by stat boxes above it.

Touchlines Pro has a real half-time break. First half stoppage time is `45+0` through `45+3`; the whistle happens at `45 + firstHalfAdded`, not always exactly 45. The break lasts 2 real seconds, players visually swap sides, the clock resets to 45 for the second half, and attacking direction/goalkeeper home side flips with the mirrored formation.

In Squad Setup, injured squad players must be visible in a separate list under the bench tray, including role, current STARTING/BENCH group, injury reason and matches remaining. The same injury data still drives badges on the pitch/player cards and match-start blocking.

When a manager presses START GAME/READY for a multiplayer human fixture, that click should be enough. If the user is waiting for another human, the client remembers the fixture and automatically opens the live playback as soon as the poll sees the match report created. Polling must also reconstruct that pending auto-start from `simVote.mineReady`, because a user already shown as waiting is already ready and must not need a second START GAME click after the final manager becomes ready.

The START GAME button may show the green pressed/ready style only for the current viewer's own ready vote. Other managers being ready belongs in the waiting card/title only; it must not make the viewer's button look already pressed.

START GAME must stay visually neutral for normal start, CPU-finish and round-continue fallback actions. The green/pressed style is reserved for an actual local READY vote while waiting for other managers.

`updateReadyButton()` must not overwrite a rendered `data-waiting="1"` START GAME button unless it is the viewer's own locally armed READY vote. Otherwise the league page can draw `WAITING`, then immediately relabel the disabled button as START GAME after returning from a match.

Squad action buttons must check active waiting state before previewing a future fixture. A user who has already played their match in the current round may be listed as ready by the backend, but that is not a fresh local START GAME vote; show a neutral WAITING button, not green READY/START GAME. Do not style `data-waiting="1"` green by itself.

The pending START GAME auto-start is stored in browser `sessionStorage` per user and season, but only as a short-lived record from a fresh local click. Old records without `createdAt`, or records older than 10 minutes, must be discarded. Backend `simVote.mineReady` alone is not enough to make START GAME look pressed, because stale server votes can otherwise make the user feel the button is no longer under their control.

When START GAME creates a waiting vote, keep the user on the page they clicked from. Squad START GAME should redraw Squad with WAITING state, not force navigation back to the league table. The league home should redraw in place.

START GAME/READY matching depends on both `roundIndex` and `fixtureIndex`. `managerRoundMeta`, `managerSimFixture`, and stored auto-start metadata must preserve `fixtureIndex`; otherwise Squad can send the vote but fail to redraw the green READY/WAITING button.

While a pending auto-start is armed, the UI must not briefly show a clickable `START GAME` button before the live view opens. The interim state can show `MATCH STARTING...`, then go straight into the match as soon as the report/details are available.

Auto-start memory is only for pending/watching/simulating fixtures. A fixture that is already `played` must never be reconstructed from `simVote.mineReady`, because that can reopen the same human-vs-human report after full time. The client also marks an auto-opened fixture as consumed for the browser session and does not run auto-start while the live match page (`managerSimPage`) is still active.

If a stale browser auto-start record points at a fixture that is still `pending` but there is no active waiting vote to show, the UI should fall back to a clickable `START GAME` instead of a permanent `MATCH STARTING...` state. This gives the user a way out of old sessionStorage state without editing the save.

The START GAME ready/pressed style is only valid for a locally armed auto-start record on an active pending/watching/simulating fixture. When a Touchlines view opens, that fixture's auto-start key is consumed. A played fixture must never keep the green ready state when the user returns to the league page.

After a played fixture, `simVote.mineContinuePending` blocks START GAME rendering. Do not render a future preview fixture's START GAME button before checking this flag; otherwise the home/squad page can show a stale or pressed-looking START GAME while the previous round is still closing.

The client auto-closes a completed round by calling `managerRoundContinue` when `simVote.mineContinuePending` is true. This auto-close must not auto-start the next match; it only advances the round so the normal START GAME button can return once the next fixture is actually available.

Round continue state can self-repair: when a human manager's fixture is already `played` and the result is no longer hidden for that manager, backend setup/start/continue paths may mark that manager continued automatically. This prevents the next START GAME from being trapped behind an invisible old continue wait.

When `managerNextRound` creates a completed watched/headless fixture, it should run the same auto-continue repair before returning public season data. Otherwise the client can briefly redraw the league page with stale `mineContinuePending` and make START GAME look pressed after a match.

Continue-wait after a completed round has a separate auto-start memory. If a user has pressed CONTINUE/READY and is waiting for another manager, the client stores that round. When polling later sees the viewer has a pending fixture in the currently active round, it calls START GAME automatically instead of showing a fresh START GAME button. This same-round allowance matters because one human can trigger their own fixture before the other human's pending fixture has moved to a new round index. Do not auto-start a future preview fixture while an earlier active round is still waiting; that only loops `Loading game...` while the backend correctly blocks the request.

Passive round-close waits after returning from a match must be displayed as `ROUND WAITING` / `DONE` / `Still to finish`, not as a READY/START GAME waiting button. Auto-close is not a fresh user START GAME click and must not write after-continue auto-start memory.

2026-07-05: `public_manager_sim_vote` exposes `passiveRoundWait`/`mineRoundPlayed`, and the client also infers this from the viewer's played fixture. Passive wait is per viewer: a round-level continue lock alone must not hide `START GAME` for another manager who still has a pending match. Passive waits clear stale after-continue sessionStorage and must not apply the green pressed `START GAME` style.

Transfers includes hireable staff managers/Booster Trainers. They are stored in `managerFinance.hiredManager`, cost 500,000 for +0.5% team boost up to 5,000,000 for +5.0%, and upgrades only cost the difference. Hiring one runs the same best-lineup formation logic as the BEST button, then applies the staff boost as `matchStatMultiplier` on public/match squad data. Staff boost stacks with daily form. A hired Booster Trainer also auto-runs the best-lineup logic whenever manager/squad data is loaded and before match start/sim season. It can move injured starters and daily-form red-dot players out if the squad has at least 11 fit players; otherwise the normal injury block remains.

Transfers also includes hireable scouts. `managerFinance.hiredScout` stores the scout tier, `scoutSearch` stores the user's filter, and `scoutSpent` tracks scout auto-buy fees. Level 1 searches only the open transfer market, level 2 can also search Browse/CPU clubs, and level 3 can auto-buy one matching affordable player when the user presses `RUN SCOUT`. Do not run scout auto-buy from polling or page refresh; buying must stay tied to an explicit user action.

Chief Scout auto-training uses the single `managerFinance.scoutSearch.autoTrainBench` flag. The scout page checkbox and the Squad page `AUTO TRAIN` button must toggle that same flag; do not add a second squad-only setting.

The Squad page action menu is one shared row for every save type: `STATS`, `AUTO TRAIN`, `BEST`, then the current match action such as `START GAME`, waiting state, or no-match fallback. Keep these controls in `manager-squad-match-actions`, not split between old solo and online layouts.

Manual Squad swaps set `manager.manualLineupOverrideAt`, so a hired Booster Trainer does not immediately undo the user's player-position swap on the next squad/team load. Pressing BEST or changing formation clears the override. If an injured starter blocks match start, the Booster Trainer may still rebuild the XI to fix the injury.

On the Squad page, clicking a bench player opens a small action row under that bench card. `SWAP IN` marks that bench player and then the user clicks a player on the pitch to replace; the bench must stay visible under the pitch while this is happening. `SELL` opens the transfer price modal and sends `listOnly` so the player is listed on the market after price approval instead of being sold instantly.

Transfers page order matters: show the transfer action buttons (sell, market, browse, deal offers) before the Booster Trainer/staff list. The staff list can be tall and must not hide the normal buy/sell/browse entry points on mobile. On narrow mobile, the transfer action grid is intentionally forced to four 72px rows so the staff box cannot overlap the browse/deal buttons.

Deal Offers must open its own manager page (`managerDealsPage`). Do not render active offers inline below the transfer action buttons; on mobile that leaves the actual deal hidden under the buttons and makes the blinking alert hard to use.

Economy loans live on `managerFinance.loan`. A club can have one active bank loan at a time, with a maximum borrowed amount of three times that save's finance `startBalance`. Loan terms are 1-5 years; one year is 46 match payments. Payments are deducted automatically from balance in `add_manager_finance_for_result`, after match income and wages are booked.

Match economy has a slightly higher win bonus than the original baseline, while draw money, goal bonus, gate income and cup income stay separate. Cup/playoff matches give both teams a participation cup-income payment and give the winner a doubled cup-winner payment, including matches decided by penalties. Keep future win-bonus changes modest so transfer prices and wages do not inflate too fast.

Manager Chatroom messages are stored on the active manager season (`season.chatMessages`) and are only returned to users in that season's `humanManagers` list. A solo save therefore has a solo chat, a two-player save has only those two managers, and a three-player save has only those three.

Transfer market is topped up, not reset. `ensure_manager_market` preserves existing human listings and CPU/free-agent listings, then fills the market to the configured target count.

CPU clubs do not all use 4-4-2. Their formation is chosen deterministically from the club and division, then existing CPU squads are migrated into that formation on load. CPU season/cup creation uses a fast formation assignment so starting a new manager season does not run the heavier human Best Lineup algorithm hundreds of times.

The CPU formation migration is guarded by `managerCpuFormationVersion` in state, so login/state requests do not repeatedly rescan and rebuild old CPU squads.

CPU bids on human-listed market players are based on the user's asking price. Overpricing lowers the chance of getting a bid and makes CPU clubs haggle harder, but the offer is no longer rounded in a way that effectively sticks to the base sell value.

Starting a new solo manager season or a new online invite is a full active-game reset. Accounts, selected teams and `private/manager-saves/` files are preserved, but active manager seasons, live matches, manager market/offers, trophies, squads, finances and manager divisions are cleared before the new season/invite is created. This makes the new game start from the base division with clean clubs, without destroying older saves that can still be loaded later.

The main-menu solo game buttons (`START SOLO GAME` before saves exist, `NEW SOLO GAME` when saves exist) open a team-only picker for a solo manager game. Online games are started separately from `INVITE USER`, which keeps the user invite picker and unique-team validation.

The `INVITE USER` picker should normally show only usernames. Team dropdowns are only shown for selected users when two selected users have the same team, or when a selected user is missing a team choice and needs correction.

Incoming invite cards follow the same rule: the invited user's team is shown as static text unless that user has a duplicate/missing team and must choose another club before joining.

`NEW SOLO GAME` and `INVITE USER` use the lightweight `managerNewGameSetup` API action to load fresh team/user setup data. Keep it separate from `managerSetup`, which is heavier and tied to the active manager season.

## Manager saves

Manager saves are standalone snapshots in `private/manager-saves/`. They are outside the active `private/data/state.json` game flow: starting `NEW SOLO GAME` only resets the active manager game, while `CONTINUE` restores one of those snapshots.

`RESET EVERYTHING` clears all users and marks stale sessions as logged out. Client API handling must force the UI back to login when a response has `loggedOut`, `not_logged_in`, or a `state` response with no `user` while the client still thinks someone is logged in.

The bottom `#managerMsg` beige/orange message strip is intentionally hidden. Manager loading uses the existing load card/global loader instead; do not re-enable that bottom status strip for manager progress text.

Autosave must never prune or delete manager saves. Older code limited saves to two per owner/group through `manager_prune_saves`, which could remove a multiplayer save when a new solo autosave was created. That function is now intentionally a no-op. The only normal way to remove a save is the explicit delete button, and the admin-only `RESET EVERYTHING` remains the hard reset that also clears manager save files.

Every manager save is also backed up outside `private/manager-saves/`. Writes update `private/manager-save-backupslive/` and create timestamped copies in `private/manager-save-backupsarchive/`; manual delete, user delete and reset-all copy the save before unlinking the live file. On 2026-07-05 a manual one-time backup of the existing save set was created under `private/manager-save-backupsmanual-20260705-051742/`.

New save filenames include the season id, for example `jdc.s87926a11f8.20260703-063402.json`. The save metadata includes mode (`solo` or `online`), users, saved-by, division, season year, season id, manager game id, latest match and score. `managerGameId` is stable across continued seasons in the same league/save chain.

Save lists and "has any save" checks should use `private/manager-saves/meta/*.meta.json` sidecars via `manager_save_cached_metadata`. This avoids decoding every multi-megabyte save file just to render the load screen or decide if a user can continue.

The save list is available from the main-menu `CONTINUE` button. Save lists show only the latest save per manager game id available to the viewer; older autosaves and older seasons for the same league stay on disk and can still become visible if the latest visible save is deleted.

The visible save list first keeps the latest save per `slotKey + managerGameId`, not managerGameId alone. This matters when an online league later splits into one-human division saves; a solo-looking branch must not hide the original online save. It also collapses duplicate-looking leagues with the same mode, users, division and season year, keeping the newest one. This prevents two rows that both look like the same save, while the older files still remain on disk.

The visible save list is membership-only for every account, including `jdc`. Admin delete/reset permissions stay separate, but `CONTINUE` must only show and load saves where the current user exists in the save metadata `users`.

2026-07-05: Autosaves for a split online manager game include every non-aborted season in the same `managerGameId`, not only the division that just played. `managerCurrentByUser` is also resolved by season year, highest division reached in that year, status and update score so an old finished season or a stale lower-division branch cannot hide the real active one. This keeps Divisions and future load snapshots aware of users who have promoted/relegated into separate leagues.

2026-07-05: Continuing a finished season must reuse an existing `continuedFrom` season for that user even if the loaded user division is stale. A stale older save had allowed `jdc` to create both a correct League Two continuation and an incorrect National League continuation from the same previous season. Save-list grouping now prefers the highest division reached in the same season year/game before raw saved-at time, so that bad lower-division branch does not hide the real progression branch.

2026-07-05: Loading an online save with multiple users expands the snapshot with each user's latest save branch in the same `managerGameId`. This is required after users promote/relegate into different divisions, because older autosaves only wrote the division that had just played. Loading `jdc+bodin` must therefore restore `jdc`'s latest branch and `bodin`'s latest branch together, while also removing stale active seasons for those users from `private/data/state.json`.

Loading a manager save must restore the save snapshot as the active manager context. User squads/finances are loaded from the save, and manager market, browse offers, transfer offers and browse-sold state are replaced from the snapshot so players and pending deals do not leak between different saves/leagues.

Team selection is not globally unique across every account. Separate test accounts and separate saves may choose the same club. The uniqueness rule belongs to a manager season: two human managers cannot start or accept the same online league with the same team.

Numeric-looking usernames, for example `111`, must be returned to the frontend as strings in manager user/invite payloads. PHP can turn numeric JSON object keys into integer array keys, and strict JS comparisons then fail if the API does not cast names back to strings.

Admin user deletion is restricted to `jdc`. Deleting a user removes the account, active matches involving that user, invites involving that user, active manager seasons containing that user, transfer references, team trophies for that user's team and manager saves where that user is included. The `jdc` account is protected from deletion.

## Manager economy

Negative balance discipline is checked at season rollover after season awards have been paid. If a human manager is still below 0 at the end of a season, the next season starts with a finance warning. If the balance is not back above 0 by the end of that warned season, the following season starts with a -10 point deduction in the league table. The warning follows the user across promotion/relegation and save reloads.

League win match bonuses are intentionally stronger than draw/loss income, but the multipliers are modest and division-based so match income does not inflate too quickly. If a human manager has an active loan, league match economy is topped up only as needed toward a controlled net result after wages and loan payment: wins around +25,000, draws around break-even, and losses around -5,000. The latest match finance is stored in `managerFinance.lastMatchFinance`, while new seasons store full income and expense snapshots in `startingFinance` so the Economy page can show latest-round and season totals separately.

## Premier League Europe

The game uses a simplified Premier League European qualification model: places 1-5 qualify for UEFA Champions League, places 6-7 for UEFA Europa League, and place 8 for UEFA Conference League. Domestic cup winners do not move these European league slots in the manager simulation.

Season awards include fixed promotion bonuses based on the division being left: National League to League Two pays 500,000; League Two to League One pays 1,000,000; League One to Championship pays 2,000,000; Championship to Premier League pays 4,000,000.

Player value drops with age at 42, 43 and 44+. Wages also drop for those older players, but only by half of the value discount so old signings are cheaper without becoming free to run.

MERCH replaces the old shirt-sales wording. `managerFinance.merchLevel` is upgraded from Economy in 5 levels and increases match merch income; `merchSpent` is counted in estimated start-balance calculations so new-game/reset economy stays clean.

Open transfer market prices are buyer-sensitive by division. If the buyer is below the player's selling/source division, the visible asking price gets a premium and direct CPU-market buys can be rejected even when the asking price is met. One division gap is only a small bump; two divisions is noticeably harder; three or more divisions should feel close to impossible without a major overpay. Human-listed market players use the same higher asking/base price, but the human seller still makes the accept/counter/reject decision.

Human manager benches are auto-filled to 9 with weak emergency loan players. These players have no transfer value or wage, cannot be sold or included in deals, and are removed from the bench first when a real player is bought or received.

## Manager performance

`managerSetup` is the heavy manager endpoint because it returns the public season, league table, fixtures, market, squad context, divisions and transfer offers. Keep the response public and slim: do not expose full CPU squads through `managerSeason.participants`; team/player details should be loaded through the dedicated team/squad endpoints when needed.

The manager screen poll interval is intentionally slower than live match polling. Match polling still needs fast updates, but manager home polling should not request the full manager payload every 500 ms.

Frontend app-version reloads are checked frequently and may auto-apply on safe screens, including manager pages when no match is actively running. Code edits still get detected during live matches, but hard reload is deferred until the server match or Touchlines playback is no longer active.

When a saved route returns to manager mode on browser reload, startup should show the manager panel loading state immediately instead of sitting on the first generic loading screen while the initial state request is running.

Season reports include a Finance section for the viewer when the season is finished. New seasons store `startingFinance` counter snapshots so `TOTAL MONEY IN` is a per-season earned total: match income plus season awards plus transfer sales. Loan cash is excluded because it is debt, not earned income.

2026-07-04: `managerSetup` returned empty 500 responses when a hired Booster Trainer auto-fixed lineups for a squad with many players. The old "best formation" search used bitmask layers and could exhaust PHP memory. It now uses a Hungarian assignment pass, keeping the best slot/player matching without exponential memory growth.

## Manager trophies

Team trophies carry a visual key: `cup`, `medal`, or `medal_silver`. Public output infers this for older saves from the trophy `kind`, so old trophies still render with a cup or medal icon.

The manager trophy page filters team trophies to the current season chain. Trophies stored in an older save remain in state for that save, but they do not leak into a newly started solo season for the same club.

Finished seasons also award a league Golden Boot trophy to every tied top league scorer. This is separate from cup goals and uses `statTrophiesApplied` so older finished seasons can be backfilled without duplicating team trophies.

The Top Scorers page also shows league Top Assists below the scorers list, so users can scroll down to see both stat tables in one page. Manager Menu no longer has a separate Top Assists action; that slot opens the Divisions browser instead, and the division table has quick buttons for switching between divisions.

2026-07-05: The Top Scorers page uses a sports-site leaderboard layout: league/cup tabs, a Golden Boot leader panel, ranked player rows with club flags, Top Assists below, and My Team goal/assist stats at the bottom.

The Divisions browser is not a static skill list. It should show real league-table rows when a current season exists in the same `managerGameId`, even if human managers have split into different divisions after promotion/relegation. Keep the same columns, promotion/playoff/relegation colors and league-form arrows as the main league table. Static division rows are only a fallback for divisions with no active/current season in that save chain.

When online managers split into different divisions, season rollover must still wait for every active human manager in the same `managerGameId` to finish that season year. Do not rely only on `continuedFrom` root ids, because older branch parents may be missing from the active state after save restore.

2026-07-05: Human-owned teams are reserved across every active/playoff season in the same `managerGameId`, not only inside the current division. This prevents a promoted/relegated human club from being inserted as a CPU opponent in another human manager's league schedule. The state migration also repairs old `cpu:*` fixture refs for human-owned teams and keeps cup-only replacements in `cupParticipants` so they do not leak into league tables.

2026-07-05: Finished manager seasons can generate optional job offers for the viewer. League champions have the highest chance of offers from the division above, direct promotion has a medium chance, playoff promotion has a lower chance, and staying in the same division can sometimes create same-division offers. Offers are stored on the finished season per user; accepting one changes the user's club/squad for the next season while the finished season keeps using its participant snapshot for old tables/reports.

2026-07-05: Each active division payload includes its own league `topScorers` and `topAssists`. The Divisions browser has a `TOP SCORERS` action for the currently viewed division, while the Manager Menu Top Scorers action still resets to the viewer's own league/cup stats.

2026-07-05: Leaving a live Touchlines manager match for another Manager Menu page pauses the frontend engine and keeps `touchlinesView` in memory. Choosing `LEAGUE TABLE` or pressing `START GAME` while that match is unfinished resumes the same match state instead of recreating the engine or revealing the already-simulated backend result.

2026-07-05: Divisions with no active human/current season are now shown as simulated shadow seasons instead of zero-match static lists. The shadow table uses deterministic fixtures from the save's `managerGameId` and season year, includes top scorers/top assists, and still excludes clubs already controlled by humans in the same save chain.

2026-07-05: Squad placement no longer applies a -10% wrong-position OVR penalty or red wrong-position styling. Best Lineup still prefers natural roles, but manual placement is allowed. Starter slot roles are normalized from the active formation position for internal placement, while the Squad UI keeps showing the player's own natural/display role so an RB moved into a CB slot still reads as RB.

## Promotion zones

Premier League marks the bottom three as relegated. Championship uses top two promoted, places 3-6 playoffs, and bottom three relegated. League One uses top two promoted, places 3-6 playoffs, and bottom four relegated. League Two uses three direct promotion places, playoffs for places 4-7, and two relegation places. Direct promotion rows use the same green as the National League champion, playoff rows use the same playoff color as the National League playoff places, and relegation rows are red. Human managers finishing in those red places are moved down one division for the next season.

## Player IDs

Player IDs are stored on each player, not in a separate registry file. This keeps saves, transfers, injuries and reports in one state tree and avoids an extra file that can drift out of sync.

Generated squad players use deterministic IDs based on club and original squad number, for example `p_fc-halifax-town_01`. `shirtNumber` is separate and is only used for the visible number in matches.

Transfers keep the same player ID when the player moves club. A random `T...` ID is only generated if the target squad already has that ID.

Players have an age profile stored on the player. New profiles seed age 17-44, but existing `jdc`/`bodin` human squads cap seeded age at 27 to avoid suddenly old squads. At season rollover players age by one year; at 45 they finish that season, retire from the current squad, and are reborn as 17-year-olds with their original base stats in their origin club.

## Save backups

Manager save JSON files are backed up before destructive delete actions and after every save write. `code/private/manager-save-backups/live/` is a same-filename mirror of the latest copy, while `code/private/manager-save-backups/archive/` keeps timestamped immutable copies and is not pruned by the old three-save display rule.

2026-07-05: A one-time manual backup of the existing save folder was created at `code/private/manager-save-backups/manual-20260705-051742/` before changing the backup policy.

2026-07-05: Manager Menu has cross-save turn alerts. `managerSetup`/`state` return `managerTurnAlerts`; the menu button pulses when another active save in a different `managerGameId` is waiting for the current user, and the menu renders one `YOUR TURN IN ...` button per save. Those buttons use `managerSwitchTurnAlert` to switch to the active season first and only fall back to loading a save file if the active season is gone. Keep `managerCurrentByUser` as the explicit current-save selector so loading a solo save does not get overridden by the newest/highest online season.

2026-07-05: Touchlines live match header follows the same side switch as the pressure meter after half time. The larger team names and visual score are shown in pitch-side order, while match data remains stored home-away.

2026-07-05: Touchlines live match header team names include a small `HOME TEAM` / `AWAY TEAM` label under the name. The label belongs to the real fixture side and moves with that team when the visual header swaps at half time.

2026-07-05: Premier League table rows use European qualification colours instead of promotion colours: places 1-5 are marked dark blue for UEFA Champions League and places 6-7 purple for UEFA Europa League. Bottom three remain relegation red. UEFA Conference League also exists, but no colour has been assigned yet.

2026-07-05: Fixed a cross-save promotion leak where `apply_manager_progression()` and season continuation used `users.*.managerDivisionLevel` from the currently loaded/global user row instead of the finished season's own `divisionLevel`. The National League season `s9f048f1a1a` had promoted `jdc` and `adam`, but `jdc` was incorrectly continued into Championship because his global level was stale. `manager_continue_target_level()` now derives the next level from the finished season outcome, new continued seasons pin human participants to the target division, and refreshes preserve the season division for league participants. The active save was repaired by removing the unplayed `sfa2ec7ee26` Championship branch, putting `jdc` and `adam` together in League Two season `s2681787e68`, and moving the wrong autosave into `code/private/manager-save-backups/manual-20260705-fix-jdc-adam-promotion/`.

2026-07-05: The top marquee user list must show every human manager in the active save chain, not only the current division. `managerSaveUsers` / `managerSeason.saveUsers` is built from all non-aborted seasons with the same `managerGameId`, so split-division saves still show users like `jdc`, `adam`, and `elias` everywhere.

2026-07-05: Human transfer-market listings are only valid while the seller still owns the listed `sellerPlayerId`, and new listings store `marketManagerGameId` / `marketHumanManagers`. Cleanup removes stale rows and pending offers, so a player from one save cannot be bid on from another save and old sold/missing players do not appear as active manager deals.

2026-07-05: Formtop daily form is recalculated after every played league fixture. The match after three straight league wins can still use the +3% formtop boost, but if that match is a draw or loss then future locked daily-form entries for that team are cleared so the next match does not keep two +10% players.

2026-07-05: Live simulation rows stay visible to other users for the fixture's playback window even after the match owner has completed playback. The owner still stops seeing their own live row immediately after completion, which prevents old finished matches from lingering as live.

2026-07-05: Touchlines Pro keeps the penalty flag from match reports in the live view. Penalty goals now show a `PENALTY` event before the kick, `GOAL (PEN)` in the events list, `PENALTY GOAL!!` in the flash overlay, and a `PEN` tag in the goals strip.

2026-07-05: Chatroom now has a separate private-message layer in `managerPrivateChats`. League chat still uses the manager save/season chat, while PMs are global between registered users and carry server-side unread alerts so Manager Menu, Chatroom, and the `USERS` button can blink for new private messages.

2026-07-05: Chatroom `USERS` must always use the global registered account list, not the users in the current league/save. The frontend opens `managerRegisteredUsers` when the button is pressed and normal state polling also carries `managerUsers`, so newly registered or admin-deleted users sync into the chat user picker.

2026-07-06: Official club squad pages are the primary source for player names and squad membership. Use third-party sites only as fallback when the club site is missing or clearly stale. Cleanup order is National League, then League Two, League One, Championship, and Premier League.

2026-07-06: Team player data can live per club in `code/private/team-players/<team_slug>.json`. The API checks that file before the old shared `private/legacy-player-names.json`, so future squads can be updated one club at a time without loading or rewriting the whole catalog.

2026-07-06: Matchday bench remains capped by `MANAGER_MAX_BENCH_PLAYERS` (9). Full squads can carry up to `MANAGER_MAX_SQUAD_RESERVES` reserves so real club squad lists do not make live matches heavier. When a club squad page is grouped by position, the generator builds the full squad first and then selects a balanced default 4-4-2 starting XI.

2026-07-06: AFC Fylde has been moved to `code/private/team-players/afc_fylde.json` using the official AFC Fylde team/signing pages as first-pass sources.

2026-07-06: All 116 playable teams now have per-team player files. AFC Fylde uses the official-club first pass; the rest were batch-filled from Transfermarkt 26/27 squad pages where available, with legacy catalog supplements only when a source list was too thin. `tools/import_transfermarkt_team_players.php` can rerun the Transfermarkt batch, and `tools/audit_team_players.php` now audits all five playable divisions.

2026-07-06: Squad size now follows the real player file count. The generator no longer forces short source lists up to a 20-player squad with generic names. Matchday bench remains capped at 9, but clubs with fewer real reserves simply use a shorter real bench until their official data is expanded.

## Quick checks

Run from this folder:

```bash
php -l index.php
php -l api.php
php -l private/lib/app_version.php
php -l private/lib/http.php
php -l private/lib/manager_progression.php
node --check assets/js/app.js
```

Open:

`https://auth.jdc.se/project/`

## Saved test reports

- `reports/manager-online-stress-2026-07-02.md` - manager online stress notes for 3-user/3-season and 15-user split testing.
