Ants Projects Hub
← Local Web Server Manager

Changelog

Local Web Server Manager

Every release, newest first — 2 in total.

v0.3.0

0.3.0 — The full state model

Theme: The full state model

Added

  • A starting project shows how long it has been starting, and says when it is slow. (LWSM-1402) The row reads starting (12s), then starting (slow — 42s) past 30 seconds. Nothing is ever marked failed for being slow. A screen reader hears starting, then starting (slow) once, not a count every second.

  • The app can ask a running site whether it works. (LWSM-1034) Right-click a project and choose "Check that the site answers". While it runs, the app asks it for a page every 10 seconds and shows the answer under its port, with a plain word under the code: "HTTP 200 OK", "HTTP 302 redirects", "HTTP 404 not found", "HTTP 500 server error" or "no response". So a site that is running but broken no longer looks fine, and one that only sends you on to another page no longer looks broken. "Change the page it checks…" picks a page other than the front page. Both choices are saved with the project, and an imported profile cannot turn checking on or change the page.

  • The app remembers the port it saw each project running on. (LWSM-1038) Once a project has run, its row says "confirmed", and the app checks, starts and opens it on the port it really used instead of a guess read from the project's files. A row still guessing says "detected". A project with no port in its files gets one the first time the app starts it and sees it open a single port.

  • Every row says exactly what its server is doing (LWSM-1011) A project now reads running, running (wrong port), running (foreign), port blocked, failed, starting or stopped, each with its own word, symbol and colour. A login-started server that belongs to the project reads running; one you started yourself reads running (foreign); something unrelated on the port reads port blocked.

  • Stop a server you started yourself in a terminal (LWSM-1301) Stop now works on a server started by hand. The app lists every process it would stop, with each one's ID, start time, program and command in its own column, and asks first; Cancel is the default. If the list changes while you decide, it asks again.

  • The release check warns when a checking tool's pinned version falls behind (LWSM-1347)

  • You can take back a "yes, run it" answer for a project (LWSM-1319) Right-click a project and choose "Ask before starting again". The next Start shows the confirmation, with the exact command, as it did the first time.

  • A project's row warns when its files disagree about its port (LWSM-1385) The port cell reads "port 3000 (sources differ)" when two files name different ports, and its tooltip says which file each came from. The app now remembers this in projects.json, so the warning is still there after a restart. Run Rescan once to fill it in for existing projects.

  • Projects run by the system's service manager get their port from the app (LWSM-1028) Starting such a project from the app passes it the app's port and tells it the app is in charge. Stopping it from the app takes that away again, so when it starts by itself at login it behaves as before.

  • The scan finds a port in .env files, docker-compose and the README (LWSM-1121) When the start script names no port, these are checked too. When two of them disagree, the most trustworthy one is used and the scan notes say which others disagreed. A file naming two different ports is not guessed from.

  • The first start finds your projects and asks before saving them. (LWSM-1008) With no project list yet, the app scans straight away and lists every project it found, each with a tick box. Save keeps the ticked ones; Not now saves nothing and the next start asks again. Folders it skipped are listed below, each with its reason. If none of the folders it scans exists, it asks you to choose one first and remembers it.

  • The app keeps a backup of your project list, and offers it if the list ever cannot be read. (LWSM-1039) The first save of each run copies the list as it was at startup to projects.json.bak. If the list later fails to load, the app asks before restoring the backup, and keeps the damaged file beside it rather than deleting it.

Changed

  • When systemd refuses to start a project, the app removes the port setting it wrote for that start. (LWSM-1407) Otherwise the service's next start at logon would use the app's port. A start that only timed out keeps the setting, because systemd may still be starting it.

  • The tests run in a different random order every time, and each one is checked for leaving a mess behind (LWSM-1374) A test that only passes because of the one before it now gets caught. A test that leaves a changed folder, setting or running program behind fails by name.

  • PySide6 moves from 6.11.1 to 6.12.0, ruff from 0.16.1 to 0.16.10 and setuptools from 83.0.0 to 84.0.0 (LWSM-1394) The app now runs on Qt 6.12; every automated check passes on it.

  • uv is moved from 0.12.2 to 0.12.23, both on this machine and in CI (LWSM-1386) CI's setup-uv step moves from 9.0.0 to 10.2.0 alongside it.

  • Status colours are easier to tell apart (LWSM-1338) Some status colours were nearly identical, such as wrong port and unknown. Every pair now differs clearly in every colour theme, with the same meaning for each colour as before.

  • The first-run "Not added" list shows real folders only (LWSM-1383) Plain files and hidden dot-folders are left out of the dialog, so the folders that matter are not buried. Both still reach the log.

  • The project's rulebooks now follow the author's shared standards, and keep only this project's differences. (LWSM-1326) The seven project-owned copies in docs/standards/ were replaced by short *-overrides.md files, each holding one rule per difference with the reason for it. docs/standards/README.md explains where the shared standards live.

  • The release check now refuses a release-candidate version such as 0.2.0-rc.1, and says why. (LWSM-1355) It used to accept one that the rest of the release tooling cannot read, so such a release could never pass.

Fixed

  • install-desktop-entry.sh no longer hides the app from the launcher when its folder name holds a backslash, and refuses a folder or a path with a newline. (LWSM-1411) A backslash in the install path was written raw into the entry's TryExec line, where \s reads as a space, so the launcher looked for a file that did not exist and hid a working entry. A folder given in place of the program was accepted and published as an entry that could not start, and a newline in the path split the entry's lines.

  • A browser you set up with "open with a custom command" can now be picked for a project. (LWSM-1410) The desktop keeps such a browser out of its menus, and the picker used to leave it out too. It is offered when your link settings name it. A browser's hidden second menu entry, such as Google Chrome's, is still not listed twice.

  • A browser whose launch line puts the link inside an option now opens the page. (LWSM-1410) A launch line such as --url=%u used to get the text %u and then the link as an extra argument.

  • A browser whose program is present but cannot be run is no longer offered. (LWSM-1410) Picking it used to fail only when you clicked Open.

  • Preferences that were saved are no longer reported as not saved. (LWSM-1410) When the save worked but the computer could not confirm it would survive a crash, the message now says exactly that.

  • Opening the app a second time no longer un-maximises the running window. (LWSM-1410) A minimised window comes back as it was, maximised or not, and the size saved on close stays right.

  • Two copies of the app can no longer both start in a rare race after a crash. (LWSM-1410) A copy recovering from a crashed one now checks again before it runs, and defers to a copy that started at the same moment.

  • Pressing Enter again while a project is starting no longer cancels the start. (LWSM-1409) Enter starts or stops the project you are on. During a slow start the Stop button still cancels it when you click it.

  • Browser names in the browser picker are no longer cut off by a few letters. (LWSM-1409) "Firefox" showed as "Firefo". The default entry now reads "Auto", which fits; its tooltip still says "Default browser". A name too long for the column always has a tooltip with the full name.

  • A greyed-out Restart button no longer tells a screen reader it will ask before acting. (LWSM-1409)

  • An exported profile, text size or theme that was saved is no longer reported as not saved. (LWSM-1409) When the save worked but the computer could not confirm it would survive a crash, the message now says exactly that.

  • A text size that cannot be applied no longer stays ticked in the menu. (LWSM-1409)

  • A project name holding a line break or other control character can no longer split the message banner into a fake second line. (LWSM-1409)

  • A port the app confirmed while a rescan was running is no longer forgotten when the rescan finishes. (LWSM-1409)

  • Some messages now follow your language and your number style. (LWSM-1409) "Cannot stop" can now be translated. Counts such as "Saved 3 of 4 projects" use your own digits. An unknown user id shows as "?" rather than "None".

  • Confirmation dialogs are freed once answered, instead of staying in memory until the window closes. (LWSM-1409)

  • Stopping a server you started by hand no longer reports a failure when the server quit just before the app forced it to. (LWSM-1408) The app now checks again at the end, so a server that is gone is reported as stopped.

  • The app will not offer to stop a server if it cannot tell which processes are its own. (LWSM-1408) Before, it could have included the terminal the app was started from in the set it stopped.

  • Stopping a systemd service while port status is unavailable no longer leaves the row on "stopping" for ever. (LWSM-1408) The row shows "unknown" instead, as a stop of a project the app started already did.

  • Stop or Restart during a port status outage now says the port table cannot be read. (LWSM-1408) It used to say the server had changed while you were deciding, which nobody had seen.

  • A project whose port changed after a rescan shows "unknown" until the new port is checked. (LWSM-1408) It used to keep showing "running" from the old port.

  • A site that answers very slowly can no longer stop the health check working for other projects. (LWSM-1408) Each check now gives up after 5 seconds in total.

  • A server listening on an unusual loopback address such as 127.0.0.2 no longer shows as running. (LWSM-1408) localhost only reaches 127.0.0.1 and ::1, so Open would have pointed at a page that never answers.

  • Starting a systemd-run project always gives systemd the port, even after an earlier start failed part-way. (LWSM-1407) It also checks systemd actually read the setting, and says so if it did not, instead of starting the project on its own port.

  • A port held by another user's service is never mistaken for one of your own services. (LWSM-1407)

  • An X11 session remembers where its window was, even with a stray Wayland setting in its environment. (LWSM-1407)

  • A service-run project keeps its systemd setup when a rescan cannot reach systemd. (LWSM-1406) A single slow answer from systemd made the rescan record the project as a plain launcher, so Start would have run a second copy of a server systemd already runs.

  • A typo like ~typo/projects in the scan-roots file no longer stops the app opening. (LWSM-1406)

  • The scan finds ports set with the shell default form, such as ${SERVER_PORT:-8080}. (LWSM-1406)

  • The scan finds compose ports listed at the same indent as ports:, and no longer takes Flask's port from an example in a docstring. (LWSM-1406)

  • One unusable scan folder no longer fails the whole scan, and a folder name that cannot be saved is refused when you add it. (LWSM-1406)

  • The first-run check boxes have a visible outline, and a thick focus ring. (LWSM-1405) On the dark themes a ticked box showed as a bare tick with no box around it, and the keyboard focus was a faint thin line.

  • The first-run dialog's count uses your language's digits, and its project lines follow a language change. (LWSM-1405)

  • A project folder whose name is not valid text no longer stops the project list saving. (LWSM-1404) A rescan that found one added it anyway, and then every save failed, on every run. Such a project is now left out, with a note saying why.

  • Importing a profile no longer brings in settings this version cannot read. (LWSM-1404) A newer version could later have acted on them.

  • A hand-edited setting that an import never uses no longer blocks exporting a profile. (LWSM-1404)

  • Restoring the project list from its backup says when the system could not confirm the file is safely on disk. (LWSM-1404)

  • A slow system no longer freezes the window once a second. (LWSM-1401) The once-a-second check released exited projects, rotated logs, walked every process on the machine and asked systemd about port holders on the window's own thread. All of it now runs in the background.

  • A project's rotated log is capped at the log limit, and a failed rotation keeps the previous one. (LWSM-1401) The backup used to take the whole log, however large, and was emptied before the copy had succeeded.

  • Closing a window just after it resized or opened no longer leaves a task that fails in the background (LWSM-1399) A re-measure or a placement queued by the window now goes away with it, instead of running afterwards against parts that no longer exist.

  • The window could, rarely, fail while building its menus after rows had been removed. (LWSM-1395) Reading the order of the project rows left behind bookkeeping that outlived the rows; a new menu could later be mistaken for it.

  • A login-started server no longer keeps the app's port after you quit the app (LWSM-1387) The app now removes its port setting as soon as such a server has started. The running server keeps its port, and the next login start uses the server's own port. One catch: if the system restarts a crashed server by itself, it comes back on its own port.

  • A row's buttons stay inside a 600 px magnifier view again (LWSM-1393) The word after a port (confirmed, detected, sources differ) now sits on a second line under the port, so a long row no longer pushes its buttons out of view with a wider font. A screen reader still hears "port 5005 (confirmed)". Long project names lose one more character before the "...".

  • The local checks measure text in the same font as CI (LWSM-1392) A row width that passed on this machine had failed on GitHub, because the two drew text in different fonts.

  • A project whose start script hands off to a slow server shows starting, not stopped (LWSM-1389) A start script that launches the real server and exits no longer makes the row read stopped while that server is still coming up.

  • A server can be stopped while it is still starting (LWSM-1372) Stop is offered during starting for a server this app launched, and a stop on a machine where port information cannot be read ends on unknown instead of showing stopping for ever.

  • Stop and Restart act only on a server's own login service (LWSM-1012) A server started from a terminal inside another login-started app (an editor, say) could have had Stop shut down that whole app. The app now checks that a service belongs to the project before driving it, and refuses with a reason when it does not. Stop now also works for a login service whose process this user cannot inspect.

  • The first-run "Not added" list no longer loses real folders behind hidden files (LWSM-1384)

  • On X11 desktops, a window remembered at the bottom of the screen no longer comes back with its edge hanging off it. (LWSM-1354) The window's position is now fitted to the screen including its title bar and border, not just the inside of the window.

  • A slow or very large applications folder can no longer hold up the window at startup. (LWSM-1340) The browser list now stops after two seconds or 5,000 desktop entries, whichever comes first, and keeps what it found. If a project's chosen browser was not reached, Open says the list was cut short instead of saying the browser is not installed.

  • A browser installed in a sub-folder of the applications directory is now offered. (LWSM-1339) The browser list only looked at the top of each applications folder. It now looks inside sub-folders too, and names such an entry the way the desktop does, so a removal in mimeapps.list and a user's own copy of the entry both apply to it. Linked folders are not followed, so a link loop cannot repeat or stall the list.

  • A rescan now clears a port saved from an older, wrong detection once it can read the project and finds no port. (LWSM-1309)

  • Stopping or restarting your own server no longer warns that someone else started it when the app briefly cannot see which programs hold which ports. (LWSM-1295)

v0.1.0

0.1.0 — Looks finished

Theme: Looks finished

Added

  • The checks before every push now include a type checker over the app's source. (LWSM-1066) pyright runs over src/ at its standard level, as part of ./scripts/local-ci.sh and therefore of CI. The 41 errors it found were typing gaps rather than behaviour bugs; each was fixed where it arose, with no blanket ignores.

  • Opening the app while it is already running shows the running copy instead of starting a second one. (LWSM-1065) Two copies could scramble each other's log file and fight over the same project list. The second launch now asks the first to come to the front and exits. Under Wayland whether the window actually comes forward is the desktop's decision, since apps may not take focus unasked; not yet checked on KDE. A copy that crashed does not block the next launch.

  • Acting on a server the app did not start now shows you what is holding the port first (LWSM-1154) The program, who it runs as, its full command, when it started, and its service name if it has one. Nothing happens until you agree, and the default is no.

  • Stop, Restart and Open now work for servers started at logon, not just ones the app launched (LWSM-1370) A server systemd starts at login is driven through systemd, so stopping it from here does not stop it coming back at the next logon. Starting it again uses the same service rather than launching a second copy.

  • A pressed button now shows it, in every theme (LWSM-1298) Holding a button down gave no feedback at all on some desktops, because the app left pressed rendering entirely to the platform. Every theme now paints a pressed button in its accent, and the pair is checked against the same contrast floor as the rest of the palette.

  • Follow system, a theme that tracks the desktop (LWSM-1244) Picks a light or dark palette from the desktop's own setting, and an assistive one where the desktop asks for higher contrast — so someone who has already said they need contrast does not have to say it again here. Light and dark are tracked live; the contrast preference is read once at startup, because reading it costs a subprocess and the window must not wait on one every time dark mode toggles.

  • Pick which browser each project opens in (LWSM-1187) Every project's row has a browser dropdown beside its port. Leave it on "Default" and Open behaves as before; pick Firefox for one project and Chrome for another and each opens where you chose. The list is the browsers already installed on your machine, so there is no command to type. If you later uninstall a browser you had picked, that project quietly falls back to your default and remembers the choice in case you install it again.

  • The title bar now tells you which version you are running (LWSM-1186) The window is titled "Local Web Server Manager 0.0.0" rather than just the name, so the running version is visible without opening a menu. The number is part of the translatable text, so a translation can move it rather than having it welded to the end.

  • Hide a project you do not use, and show it again from the View menu (LWSM-1185) Right-click a project (or press the Menu key on it) for "Hide this project". Hidden projects leave the list and the choice is saved. View → Show hidden projects brings them back, each marked "(hidden)" so you can tell which and unhide it. Nothing becomes unrecoverable without editing the file by hand.

  • Enter in the filter box jumps to the first matching project (LWSM-1156) Type /, type part of a project's name, press Enter — you land on the first project that matches, instead of having to press Tab first. Enter on a project still starts or stops it, as before.

  • The window reopens at the size you left it, and a Centre on screen action (LWSM-1033) Size and maximised state are remembered between runs, and View → Centre on screen puts the window back in the middle. Position is remembered too on X11. Under Wayland the desktop never tells an application where its own window is, so a position cannot be recorded there — the size still is, and a position already in the file is still restored. The values are plain numbers in settings.json, so a window you cannot reach can be fixed by editing the file.

  • Save and reload a project-settings profile (LWSM-1148) File → Export profile writes your project list and your settings to a file you name; Import profile reads one back. An import is a merge, not a replacement: your own names, port overrides and notes come back from the profile, while what this machine detected — the launcher, the declared port — stays as this machine found it. A project the profile does not mention is left alone. A profile that cannot be read in full is refused with a reason rather than restored in part.

  • A Preferences dialog for the folders to scan, how often to check, and how much log to keep (LWSM-1018) Reachable from Settings → Preferences. All three take effect immediately — no restart. The folder list is still the scan-roots file you could already edit by hand, and saving keeps any comments you wrote at the top of it.

  • Make the text bigger from inside the app — Settings > Text size, 100 % to 200 %, remembered between runs (LWSM-1032) Five steps, applied to the desktop's own font rather than replacing it, so a machine already set to large text stays large and gets larger. The choice is stored beside the theme in settings.json.

  • Drive the whole list from the keyboard, and filter it down to what you are looking for (LWSM-1040) Press / to jump to the filter box and type part of a project's name — upper or lower case, anywhere in the name — and the list narrows to what matches. Escape clears it and brings everything back. Number keys 1 to 9 jump straight to a project, counting the ones you can actually see rather than the ones behind the filter, and Enter starts the project you are on, or stops it if it is already running. The filter box shares the row with Rescan rather than taking a line of its own, so the list keeps its height.

  • A settings.json beside projects.json, holding the theme choice so it survives a restart (LWSM-1031) A settings file that cannot be read never costs you a window: the app falls back to the defaults and says what it ignored.

  • Six colour themes plus high contrast in light and dark, switchable from Settings > Theme without a restart (LWSM-1031) The six are adopted from finbreak. Every colour is checked by arithmetic against every background it can land on, and the two high-contrast palettes are held to a stricter standard than the rest, so adding a theme that is hard to read is a failing build.

  • A menu bar, so there is somewhere obvious to find settings. (LWSM-1146) The window was one Rescan button with nowhere to put anything else. It now opens with a File menu (Rescan, Quit) and a Settings menu whose Preferences entry is where the settings dialog will attach. Both menus are reachable from the keyboard, and both follow a language change.

  • A runnable release preflight, so a release stops on this machine rather than half-way through. (LWSM-1151) scripts/local-release.sh reports whether a release would stop, and where, without changing anything — the recipe, the version lockstep, the tag, the dated changelog section, and whether the roadmap agrees the cited work shipped. Nothing on GitHub checks a release, so this is the only gate one gets.

  • Release machinery: one place to change the version, and a check that all four files agree (LWSM-1067) The version number is written in four files by hand. There is now a release recipe that bumps all four together and a check that fails the build if they ever disagree — so a release cannot ship with the version updated in some places and not others. Nothing user-visible changes yet; this is the groundwork for the first tagged release.

  • An Open button that opens the running site in your browser (LWSM-1016) Opens the port the server is actually listening on, read at the moment you click rather than remembered from earlier — so it still opens the right thing after a project moves ports. Available for any server that is running, including one you started yourself outside the app.

  • Start, Stop and Restart buttons on every project — the app can now actually run your servers (LWSM-1010) The buttons respond the instant you click them: a project shows "starting" or "stopping" straight away rather than waiting up to a second for the next check, and the real status takes over as soon as it is known. The first time you start a project the app shows you exactly what it is about to run — the full path and the exact command — and asks; it asks again if that script changes. Stopping a project stops the helper processes it started too.

  • A Rescan button that finds your projects and folds them into the list without losing anything you changed (LWSM-1131) Rescan looks through your projects folder, adds anything new, updates what it can see, and tells you in one line what changed — "Rescan: 2 new, 1 changed". Names you have typed, ports you have overridden and notes you have written are never overwritten. A project it can no longer find is flagged rather than deleted, and a port it could not read this time keeps the value it had rather than being blanked. The scan runs in the background, so the window stays responsive while it works.

  • The project list can now be saved, and it remembers how each project is started (LWSM-1007) projects.json now holds the launcher command, the launcher kind, a systemd unit name, per-project notes, and flags for hiding a project or starting it at login — as well as the name and port it already held. Saving is atomic, so a crash or a full disk can never leave a half-written list, and the app refuses to write over a file it could not fully read: a typo you can still fix by hand is never overwritten by a fresh one.

  • Start and stop actually work: the Supervisor spawns each project in its own process group and reaps the whole tree (LWSM-1009) Stopping a project now stops the helper processes its start script spawned, so the port is genuinely released rather than left held by something nobody can see. Launched projects are given only the handful of environment variables they need — never your SSH agent, API keys or cloud credentials (LWSM-1048) — and a project's start script is refused outright if it points outside the project or if another account on the machine can rewrite it (LWSM-1046, core half). Each project's output goes to its own log file, capped at 5 MB with one rotation.

  • Four scanner behaviours the spec calls load-bearing now have tests that fail when they break (LWSM-1125) Each was correct in the shipping code and protected by nothing: the timeout signal not being confusable with a file error (LWSM-1125), the package.json dependency block staying out of the port rules (LWSM-1126), a start script without the run permission falling through to the next rule (LWSM-1127), and the filename a port was read from being cleaned before it is shown (LWSM-1129). Every one was watched failing against a deliberate break.

  • Detect projects, their launchers and their declared ports (LWSM-1006) Point the app at a folder of projects and it works out, for each one, how that project starts and which port it wants — reading the launcher the project already has rather than asking you to write anything down. Where nothing in the project says which port, it says unknown instead of guessing, and every port it does find is labelled with the rule and the file it came from.

  • A window that shows each project's live status (LWSM-1005) lwsm now opens a window listing the projects named in ~/.config/localwebservermanager/projects.json, written by hand for now. Each row states — as a word, a glyph and a colour — whether anything is listening on that project's port: running, stopped, or unknown where the project names no port. The label follows the live socket table within 2 seconds of a server starting or stopping, and is re-derived on every start rather than remembered.

    Rows are keyboard-focusable and carry an accessible name built from what is on screen, so a screen reader announces "running, project-a, port 5005". The glyph is decorative and left out of it. Colours come from theme tokens rather than being written into the widgets.

    A malformed projects.json is refused with a named reason rather than half-read; a single bad record is skipped and reported in the status bar while the rest still load; and a mistyped port loses the port, not the project. The socket table is read on a background worker, so a slow lookup cannot freeze the window.

  • The lwsm console script and python -m lwsm, with a --version flag. No interface yet — it configures logging, reports where it is logging to, and exits 0. CI asserts the entry point actually resolves, because a console script can name a module that does not exist while the build stays green.

  • Build tooling: uv + exact pins, ruff, pytest + pytest-qt, and a GitHub Actions workflow that calls scripts/local-ci.sh rather than restating its steps, so the local gate and CI cannot drift.

  • Application log at $XDG_STATE_HOME/localwebservermanager/app.log, INFO by default, rotating at 1 MB with 5 kept (LWSM-1026).

  • docs/standards/dependencies.md — version policy: latest by default, with an exception register that makes a held-back pin retestable instead of permanent.

Changed

  • The three settings files are now read by one shared reader (LWSM-1357) The project list, the preferences and the list of approved launchers each had their own copy of the same reading steps, and the copies had drifted. The approved-launchers file now refuses NaN and Infinity values like the other two, which means it trusts nothing and asks again, the safe direction.

  • Messages from the menus now appear in a banner above the project list (LWSM-1345) Text size, theme, profile and rescan messages used to go to a status bar at the bottom edge of the window, out of a magnifier's view. They now show above the list and stay until you press Dismiss or a new message replaces them.

  • A desktop font size change applies the next time the app starts (LWSM-1346) The bundled Qt sends a running app no notice of the change (measured on KDE Plasma 6.7). The View menu's text size works immediately.

  • The "run this project's start script?" question is asked once per launcher, not once per session. (LWSM-1046) Your answer is kept in trust.json beside settings.json. It is asked again if the script or its command changes. A damaged file only means being asked again.

  • Stop and Restart are greyed out for a server this app did not start (LWSM-1197) Open was already restricted to servers the manager started. Stop and Restart were offered on any project whose port was busy, and could only fail: a stop was refused because there is no handle to signal, and a restart quietly became a start that the port check then refused. The buttons now say that up front.

  • Split docs/design.md — the look-and-feel and accessibility contracts are their own documents (LWSM-1157) The design document had reached 1223 lines and its review gate kept capping without settling. docs/design-look-and-feel.md and docs/design-accessibility.md now hold the theme/token contract and the accessibility contract; design.md keeps everything else and is 912 lines. Content moved verbatim — no rule changed. Both headings stay behind as pointers so older citations still land.

  • When an action fails, the message appears under the project it is about instead of in the status bar (LWSM-1032) A message in the corner of the window is invisible to someone whose magnifier is on a button. It clears itself as soon as the project's state moves on, so it can never contradict what the row says.

  • The app now starts dark (LWSM-1147) Following the desktop's own light/dark setting would open light on a light desktop, which is not what was asked for, so it is a choice rather than the starting state.

  • The window opens at a size that fits the list, and the list scrolls (LWSM-1149) The opening size is measured from the content and clamped to the screen — eight rows before the list starts scrolling, three as the floor — rather than left to Qt's ~790x520 default with the rows crushed against the window chrome. The row list now sits in a scroll area, so twenty projects no longer force a window taller than the screen that cannot be shrunk. Rescan moved off the full-width strip into a right-aligned control of its own, and the margins and gaps come from the text metric.

  • Groundwork for translation, and stricter internal checks (LWSM-1080, LWSM-1081) The words in the window can now be translated, which none of them could before. No translations ship yet — this is about not making that harder later. A type-checking tool also found four small mistakes that nothing in the build was looking for; those are fixed.

  • Colours meet the readability standard the project set itself (LWSM-1075, LWSM-1077) The colour used for "unknown" was very slightly too faint against the window to meet the contrast standard, in the palette a first run gets. It has been darkened, and every colour is now checked by arithmetic rather than by eye, so a future palette that fails is a failing build. The colour rules also moved into the one place meant to own them.

  • scripts/local-ci.sh parses all its arguments, and honours NO_COLOR Only $1 was examined, so --fst ran the full gate silently. Unknown arguments exit 2 with usage, --help works, a failing step is named rather than exiting bare, and escape codes are suppressed when stdout is not a terminal.

  • CI fails when a check could not run, and pins the toolchain it runs with The workflow sets LWSM_REQUIRE_ALL_TOOLS=1, so a SKIP is fatal on the machine that is supposed to hold every tool; locally it stays a warning. setup-uv is pinned rather than resolving whatever uv is newest at run time, .python-version is committed so both machines resolve the same interpreter, yamllint is installed rather than assumed from the runner image, and the job has a 15-minute timeout in place of the 6-hour default.

Fixed

  • A screen reader now hears the project rows as items in a list (LWSM-1348) Each row used to present itself as a plain panel, so nothing said "list" or where in the list you were. The rows are now list items in a list, and what each one says is unchanged.

  • A project's name can no longer make a screen reader announce a false status (LWSM-1342) A row is now read as its status and port first, then "named" and the project's name, so a name like "x, running, port 80" cannot pass for the row's own status.

  • Outlines round buttons, fields, lists and rows are now visible in every theme (LWSM-1337) They used to blend into the background, most of all in the high-contrast dark theme. Every outline now stands out at least 3:1 from what is behind it, and a disabled control's outline fades with its label.

  • The keyboard focus ring is thick on every control, not just on project rows (LWSM-1349) Buttons, the filter box, the browser picker, number boxes and lists now get the same thick ring as a project row, and it grows with the text size. The controls keep their usual shading.

  • A broken script in package.json no longer makes Start silently do nothing (LWSM-1367) A lone surrogate character in the script crashed the start with no message.

  • install-desktop-entry.sh writes an absolute path when given a relative one, and handles a % in the install path.

  • "Centre on screen" no longer silently does nothing after a previous placement was interrupted, and no longer reports a failure for a window that did move.

  • "Centre on screen" is disabled, with a reason, on Wayland desktops that are not KDE, where it could only fail.

  • The Rescan button meets the minimum click-target size at small system font sizes.

  • A failure shown under a project's row is now read out by a screen reader.

  • The note that a server was not started by this app is now available to keyboard and screen-reader users, not only on mouse hover.

  • A project whose launcher crashes before its server starts no longer sits at "starting" with every button disabled.

  • A systemd service that fails right after it was started no longer leaves its row at "starting"; the row says the service did not stay up.

  • lwsm --version and lwsm --help no longer print a crash traceback.

  • A browser you removed by hiding its desktop entry in your own applications folder is no longer offered, and desktop files saved with a byte-order mark are read correctly.

  • Stopping or restarting a server this app did not start is refused if a different server took the port while the confirmation was open.

  • A server that runs several worker processes is recognised as one server

  • Logs of servers left running from an earlier session are kept to the size limit too

  • A stop or start that fails part-way no longer loses track of the running server

  • A system service is never mistaken for one of your own services

  • A line like parseInt(process.env.PORT, 10) || 3000 is read as port 3000, not 10 The port finder took the first number after port =. It now reads the fallback value and skips number bases and list indexes.

  • Projects whose start script names a locked folder or a very long argument stay in the list Only the port is unknown now; before, the whole project vanished from the list.

  • The port finder looks next to the file doing the importing, not at the project's top folder

  • A start script that passes a folder after the program, like node server.js public, still finds the port

  • Why a rescan skipped a folder is now written to the app log

  • A settings file with no version line can be saved again It was read correctly but then refused every save, including the window position on close. The note about the missing line is still shown.

  • One unusual value in the project list or settings no longer blocks every later save A huge number such as 1e999 or a broken text escape is now refused when the file is read, with a message naming the field, instead of making every save fail.

  • Settings a newer version wrote at the top of the project list survive a save

  • Importing a profile warns when it gives two projects the same port

  • A server started through a wrapper script that exits is still recognised as yours Stop and Open no longer show the "this manager did not start it" warning for a project the app started itself.

  • Pressing Enter on a row's Open or Restart button, or its browser picker, no longer stops the project Those controls ignore Enter, so the key reached the row, which treats Enter as Start or Stop. The row now acts on Enter only when it has focus itself.

  • Keyboard and screen-reader users keep their place, and status messages are read out. (LWSM-1323) Pressing Start or Stop from the keyboard used to throw focus somewhere else in the window as the button greyed out; it now stays on that project's row. Messages shown at the bottom of the window are now also announced to a screen reader. Also: counts in the rescan summary use your language's digits, and a restart can no longer begin before its own stop has finished being handled.

  • The project list is read and saved more carefully, and a save that did happen is no longer reported as failed. (LWSM-1322) When the disk accepted the new project list but could not confirm it would survive a crash, the app said "not saved" and then behaved as if nothing had been written. It now says the list was saved and why it may not survive a crash. Also: a hand-edited list with a repeated setting, or a value JSON does not allow such as NaN, is reported rather than silently accepted; a list over 1000 projects loads the first 1000 and is never written over; a project's name is shown and read aloud without control characters and at a bounded length; an existing log file is made private to you.

  • Starting a project again no longer launches a second server beside part of the old one that refused to die. (LWSM-1321) A process can outlive a forced kill while it waits on slow disk I/O. The app used to forget it and let Start launch a second copy; Start now refuses, naming the process, until it is gone. Also: a server is still counted as running when the system hides its details, launchers are read whole before being fingerprinted, and a project started from a session with no PATH still finds its tools.

  • The Graphite theme's keyboard focus outline is now clearly visible (LWSM-1238) The outline around a focused button was too faint against Graphite's dark buttons. Graphite's accent colour is now a lighter blue of the same hue.

  • A usage example in a Python docstring is no longer detected as the project's port (LWSM-1308) A project whose help text showed PORT=5000 python3 serve.py was detected on 5000 instead of its real default. That invented a port conflict, and another project's Start was refused. Rescan to pick up the corrected port.

  • A desktop entry that fails validation is no longer left in the launcher (LWSM-1267) install-desktop-entry.sh checked the entry after writing it into ~/.local/share, so a malformed one had already appeared in the application menu and the script then exited leaving it there. It is now written beside the target, validated, and only then renamed into place — so it appears whole or not at all.

  • Jumping to a project with a number key now scrolls it into view (LWSM-1259) The highlight could land on a row below the bottom of the window, so pressing Enter acted on a project you could not see. Enter in the filter box had the same problem.

  • Choosing a bigger text size now actually enlarges the text on every desktop (LWSM-1256) On a desktop whose font is sized in pixels the setting did nothing at all, while the menu ticked your choice and saved it. Where the size genuinely cannot be changed the app now says so instead of pretending.

  • Starting, stopping or restarting a server now says so (LWSM-1302) A restart left no trace: the row reads "running" before and after, so there was nothing to see and no message either. The app now confirms what happened along the bottom of the window.

  • The "do you trust this launcher?" box can no longer appear with nothing in it (LWSM-1261) In one case the box showed no launcher at all, and saying yes still approved it. It now shows the launcher the app knows about, and refuses to ask at all when there is nothing to show you.

  • Disabled controls now dim under every theme, so a button that does not apply looks like one (LWSM-1300) Applying a theme overwrote the platform's own dimming, so a control the app had correctly switched off rendered exactly like a live one. Start stayed bright on a running server. The palette now sets its disabled colours explicitly, for buttons, labels and text fields alike.

  • The browser list now honours the desktop's own mimeapps.list (LWSM-1248) A browser removed from the http handlers is no longer offered, and one added there is now offered even if its desktop entry declares no MimeType. The list previously read each entry's own claim, which was both too wide and too narrow.

  • A browser whose desktop entry cannot be read is no longer reported as not installed (LWSM-1250) An unreadable desktop entry was skipped in silence, so a browser that is installed looked exactly like one that is absent — and the app told the user to reinstall it. Entries that cannot be read are now recorded and logged, and the message says the entry could not be read rather than that the browser is missing.

  • A browser's name from a desktop file is now bounded and stripped of control characters (LWSM-1249) Desktop entries are not written by this app, and their name went straight to the browser picker, its tooltip and its accessible name — unbounded and with control characters intact. A very long name also made the app spend real time measuring it on every text-size change. The name is now clipped and sanitised by the same rule a project name already gets.

  • The theme-colour derivation tool no longer prints a failed colour as if it had passed (LWSM-1246) When no shade cleared the contrast floor, the tool printed its closest miss in exactly the format of a success, counted no shortfall, ended with "0 shortfall(s)" and exited 0 — output meant to be pasted straight into the source. A miss is now labelled, counted, not printed in pasteable form, and the run exits nonzero.

  • A window placement that half-fails no longer leaves a registration inside the window manager (LWSM-1243) Placing the window loads a small script into KWin, runs it and unloads it. If the middle step failed, the unload was skipped while the script file was still deleted, leaving KWin holding a registration that pointed at nothing. The unload is now attempted on every path, which costs nothing: unloading a name that was never registered succeeds.

  • Centre on screen now respects panels on Wayland instead of centring on the whole display (LWSM-1241) Wayland gives an application no way to learn the usable area, so the window was centred on the full screen and sat low by half the height of the panel. The window manager is now asked for the usable area and picks the spot. X11 was already correct and is unchanged. Measured on a 3840x2160 display with a 46-pixel panel: a window that landed at y=816 now lands at y=793.

  • A wedged window manager can no longer freeze startup for nine seconds (LWSM-1240) Placing the window asks KWin three times, and the three-second deadline applied to each call rather than to the exchange. The three calls now share one deadline, so the wait is bounded by three seconds however many calls it takes.

  • A Wayland session with no XDG_SESSION_TYPE is no longer mistaken for X11 (LWSM-1239) Window placement picked its strategy from one environment variable that is routinely absent — under a systemd user unit, a scrubbed environment or a session manager setting it to "tty". Absent read as X11, so the app moved the window, Wayland discarded the move, and the app reported success. A non-empty WAYLAND_DISPLAY now names a Wayland session too.

  • Stop, Restart and Open now say why they are disabled on a running project this manager did not start (LWSM-1297) The three buttons are gated on the server being one this app started, a security rule from ADR-0004. Nothing said so, so clicking Open on a running project did nothing at all and looked broken. Each now carries a tooltip naming the reason — the one channel that still reaches a disabled widget, where a click does not.

  • MEDIUM: the settings error clip removes the cause rather than the hostile input. (LWSM-1237) When saving preferences fails, the message now says why. It was cut to a fixed length that the file path alone nearly filled, so the part naming the actual cause — disk full, permission denied — was always the part removed.

  • MEDIUM: the config size cap is a caller obligation that two of four callers do not meet. (LWSM-1236) The 1 MiB limit on config files is now enforced by the writer rather than asked of each caller, two of which did not check. A long comment block in the scan-roots file could otherwise produce a file too large to read back — and, because the app refuses to overwrite a file it cannot read, one that could never be repaired from inside the app.

  • MEDIUM: a settings schema mismatch permanently blocks every future write. (LWSM-1235) A preferences file whose version line is missing, older or malformed is now read as far as it can be and stays writable, instead of losing every value and blocking every future save. Only a file written by a NEWER version is refused, because overwriting that would destroy settings this version cannot read.

  • MEDIUM: design.md's overlay rule contradicts its own next sentence. (LWSM-1234) Documentation only, no behaviour change. The design document gave two different answers about when the start/stop indicator disappears, and one of them would have made a slow start flicker. It now states the rule the app actually follows, and the three other things that end the indicator.

  • MEDIUM: abandon_pool retains the pool but not the signaller the abandoned task still holds. (LWSM-1233) A background check the app gave up on can no longer take the app down with it as it quits. The object such a worker reports on is now released along with the worker, instead of being destroyed underneath it. Applies to both places that give up on a worker — the port poll and the project rescan.

  • MEDIUM: the port snapshot discards the listening address, so any interface counts as bound. (LWSM-1232) A program listening only on your network address no longer makes an unrelated project read as running, and Open no longer offers a link to a port nothing answers on. Starting a server still refuses a port taken on any address, which is a different question and is now asked separately. Where two processes hold one port on different loopback addresses, the app says it does not know who owns it rather than picking one.

  • MEDIUM: the managed flag survives a probe outage, so Open can point at a stranger's server. (LWSM-1231) When the app loses sight of which programs hold which ports, it no longer claims a running server is one it started, and the row repaints so the buttons follow. Previously the claim survived the outage, and Open would still offer to visit a port a stranger could have taken meanwhile.

  • A booby-trapped file where a rotated log goes could freeze status updates (LWSM-1229) The app caps each project's log and keeps one older copy. The place that older copy is written was opened less carefully than the log itself, so a special file planted there could make the app wait for it forever, silently, and stop every project's log being capped. It now refuses such a file — and checks before emptying anything, so a file that is not ours is no longer blanked on the way to being rejected.

  • The browser picker could be too narrow to click, and cut its text with no way to read it (LWSM-1253) On a machine with no browser installed the dropdown shrank below the minimum size everything clickable is meant to have. Its text was also cut short on every row — the default entry is longer than the column — with no way to see the whole name. Hovering now shows it.

  • Stop says so when it could not signal part of a server (LWSM-1224) If the system refused to let the app stop one of a server's processes, that went only into the log file and Stop still reported a clean success. The result now names what it could not signal. A process that had already exited on its own is not reported — that is the outcome Stop was after.

  • A large launcher script can no longer hold a scan past its time limit (LWSM-1223) While working through a start-up script looking for the file that sets the port, the scan never checked its own time limit — it only looked again when it moved on to the next project. A very large script could run hundreds of times over the limit and still report that it had finished in time.

  • Scanning is no longer slowed down by long awkward lines (LWSM-1222) Deciding whether a line really declares a port meant re-reading everything before it, once for every candidate on the line. A single long line could take twenty milliseconds on its own. The check now looks only at the word immediately before, which is all it ever used, and the cost stops growing with the length of the line.

  • One unreadable folder no longer makes a whole project disappear (LWSM-1221) If a project's start-up file referred to something inside a folder this app is not allowed to open, the entire project vanished from the list — even though the project folder and its start-up file were both perfectly readable. It stays in the list now, with its port shown as unknown and the reason recorded.

  • A scan that runs out of time says so instead of guessing (LWSM-1220) When the time budget ran out while a scan was following a project's imports to find its port, the project was listed with the port shown as unknown — and the scan still reported that it had finished normally. It now stops and says the scan was cut short, which is what every other part of it already did.

  • An older version of the app no longer deletes settings it does not recognise (LWSM-1218) Anything in the project file that a given build did not know about was dropped the next time that build saved. Running an older version once was enough to erase every per-project browser choice, and the same happened to a profile passed through it. Unrecognised entries are now kept and written back untouched.

  • An imported profile no longer brings a launch command with it (LWSM-1216) A profile carries your own choices — names, hidden flags, port overrides. For a project this machine had never scanned, it was also bringing across what the other machine had worked out for itself, including the command used to start the server. Those are rediscovered by a scan here instead.

  • A profile is not exported when one of your own settings could not be read (LWSM-1215) A typo in a hand-edited setting — a port override written as text, say — made the app drop just that setting. Exporting a profile then saved it as empty, and importing that profile elsewhere quietly wiped the working value on those machines. Export now refuses and names the setting. Settings the app works out for itself are unaffected: a rescan recreates those.

  • Clearing every scan folder behaves the same before and after a restart (LWSM-1213) Removing all the folders from the Settings dialog stopped the app scanning anything for the rest of the session, then quietly went back to the default folder next time it opened — re-adding the very projects that had been cleared away. An empty list has always meant "use the default" when the file is read, and now means the same the moment you save it.

  • A preferences save that fails no longer throws away your folder list too (LWSM-1212) The Settings dialog writes two separate files. They shared one attempt, so if the preferences file could not be written — which a single stray comma in it is enough to cause — the folder list was silently skipped and gone by the next launch, while the message mentioned only preferences. Both are written independently now, and anything that fails is named.

  • One failure while quitting no longer skips the rest of the cleanup (LWSM-1211) Quitting runs three cleanup steps. They shared one block, so if the first went wrong the other two were skipped — and one of those is what stops a background worker outliving the app, which is the very thing that block exists to prevent. Each step now runs on its own, and a failure is written to the log rather than passing unnoticed.

  • On a machine with no home folder, Rescan is no longer offered (LWSM-1210) The app already opens and explains itself when it cannot find a home directory. It was still showing Rescan and the profile import and export entries, none of which has anywhere to write, so using one would have failed. They are hidden now, like everything else that cannot work there.

  • The launcher entry works when the app lives in a folder with a space in its name (LWSM-1209) Installing from somewhere like "My Projects" produced a menu entry that appeared and then would not start, because the path was written without quotes and the launcher split it at the space. A folder containing "&" or "|" corrupted the entry outright. Both are fixed, and the installer now has tests that run it against those names.

  • CONTRIBUTING now describes the checks that actually run (LWSM-1208) Step 5 said a green run on your own machine means a green run in CI, and that any documentation change skips the checks. Neither is true: running the script by hand deliberately tolerates a missing tool, and three kinds of documentation are tested against and always run the checks. It also never mentioned the one-off command that installs the hook doing all this for you.

  • Highlighted text is readable in every colour theme (LWSM-1207) Selecting text in the Ledger, Parchment, Mint and Graphite themes drew it in a combination too faint to meet the readability standard the project holds itself to. Those four accent colours are now a shade deeper — same hue, enough contrast — and the tool that checks the palettes now looks at this pairing, which it never did.

  • Two projects set to the same port: the second one is refused, and says why (LWSM-1205) The port belongs to whichever project was registered first. That was worked out and written into the rescan report, but Start never saw it, so both projects would launch and the second failed later with an unrelated message. Start now refuses the later claimant and names the project holding the port.

  • Every control in the Settings dialog is now big enough to click (LWSM-1206) The minimum size that keeps a control comfortably clickable was applied to the Add and Remove buttons only. On a machine with a smaller default font the two number boxes and the OK and Cancel buttons came out under it — measured at 19 pixels tall for OK and Cancel against a floor of 24.

  • Stop no longer leaves behind a process that appeared while it was working (LWSM-1204) Stop listed the server's processes once, at the start. Anything that appeared afterwards — a shutdown handler that restarts something, a watcher, a worker being replaced — was on no list, was never asked to quit, kept the port, and Stop still reported success. The list is taken again before the forceful step and once more at the end, and anything still running is now named in the result.

  • The app now says when it can no longer see which ports are busy (LWSM-1203) If reading the system's port list started failing — a hardened kernel, a container without /proc — every row kept showing its last answer, which looks entirely normal and is no longer true. The only record was the log file. The status bar now says so when it happens, and says so again when it starts working. Repeats stay quiet, so a permanent failure does not flood it.

  • Starting a second server no longer makes the first look stopped (LWSM-1202) The "starting" label could only be shown for one project at a time, so pressing Start on a second one took it away from the first while that server was still coming up — leaving it reading "stopped" with its Start button live, which only earned a refusal. A project we hold a running process for, whose port nothing has claimed yet, now reads as starting on its own.

  • A project that ignores its port override is no longer reported as stopped (LWSM-1201) Setting an override made the app watch only the new port. A project that pays no attention to it stays on the port it was written with, so the app saw nothing on the override, said "stopped", and Start would have launched a second copy alongside the one already running. Both ports are checked now, which costs nothing — they come from the same reading.

  • Larger text no longer pushes the rows out of reach (LWSM-1200) The smallest size the window would allow was worked out once, when it opened. Turning the text size up made the rows wider than that, and since the list does not scroll sideways the extra width simply could not be reached. The window now re-measures its own limit whenever the text size or the language changes.

  • Hiding a project during a rescan no longer loses the change (LWSM-1199) A rescan works from the project list as it was when the scan started. Hiding a project, or choosing its browser, while one was running left the finished scan writing the old value back over it — after the status bar had already said the choice was saved. The scan now keeps its own findings and takes your choices as they stand when it lands.

  • "Centre on screen" now says why it is greyed out (LWSM-1198) On a desktop that will not let an application place its own window the entry is disabled, and the explanation for that was written into a tooltip Qt never drew — menus hide tooltips unless asked. The tooltip is shown now, and the entry also says "unavailable on this desktop" in its own label, so the reason does not depend on hovering.

  • Start no longer comes back before the stop it is waiting on has finished (LWSM-1191) On a project whose port the app cannot detect, Start went live again within a second of pressing Stop, while the shutdown was still running — and pressing it only produced an error, because the app correctly refuses to start something it is still stopping. Start now stays unavailable until the stop actually finishes, and becomes available again the moment it does.

  • A scan folder the app cannot store is now refused instead of quietly changed (LWSM-1179) The list of folders to scan is stored one per line, so a folder whose name ends in a space or contains a line break could not be written down faithfully — it came back pointing somewhere else, or split into two folders that do not exist, and nothing said so. Preferences now tells you it cannot store that folder and leaves your existing list alone. Ordinary names with spaces in them, like "my projects", are unaffected.

  • A scan-folders file the app cannot read is no longer overwritten with the default (LWSM-1178) If the file listing which folders to scan could not be read — one stray byte in it was enough — the app quietly fell back to a single default folder, Preferences showed that as though it were your list, and clicking OK wrote it back over the real one. Your folders and your own comments at the top of the file both went. Saving now refuses and tells you why, so the file you can still repair is left alone. A file that simply does not exist yet is unaffected and still saves.

  • The Rescan button now changes language with the rest of the window (LWSM-1177) Switching language left the Rescan button showing its old label while File → Rescan projects beside it changed — and a screen reader read out the old label too, since the button's spoken name comes from what is written on it. Both now change together.

  • A faulty translation of a settings save-failure message no longer takes the window down (LWSM-1176) If saving your theme or text size failed, the app showed the reason in the status bar. Under a translation that had renamed the slot the reason goes into, showing that message raised an error of its own — turning a handled failure into a broken window. The two messages now use the placeholder Qt substitutes without being able to fail, so a bad translation loses the reason and nothing else.

  • Changing the theme or the language no longer deletes a project's failure message. (LWSM-1175) When a project failed to start, the message shown beside it vanished the moment you switched theme or language — most painfully for someone switching to a high-contrast theme in order to read that very message. The message is cleared because the project's state moved on, and re-drawing a row is not the state moving on. It still clears the instant the project actually starts or stops.

  • A hostile scan-roots file can no longer hang the app on startup. (LWSM-1173) The file that says where to look for projects was read without any of the protections the other config files get, and it is read before the window exists — so a booby-trapped one made the app block forever with no window, no error and nothing in the log. It now goes through the same bounded reader, which also caps its size: every line in it becomes a directory the scan walks. A leading byte-order mark no longer turns your own comment line into a directory to scan.

  • Open at a size the current screen can hold, whatever size the window was last closed at (LWSM-1172) A window sized on a large monitor and reopened on a smaller one came back at its remembered size — off the edge of the display, where the corner you would drag to shrink it is out of reach. The remembered size is now bounded by the screen the window opens on, and the compositor is told the bounded size rather than the stored one.

  • "Centre on screen" now reports a desktop that refused the request. (LWSM-1170) On a non-KDE Linux desktop the menu entry looked available, did nothing when clicked, and said nothing about why. The exit status the desktop returns is now read, so the window says it could not be moved instead of appearing to have moved.

  • Log rotation now works through its own copy of the log descriptor. (LWSM-1169) Stopping a server while its log was being rotated could blank an unrelated file the app had just opened. Reproduced, not theoretical.

  • Pressing Start while a server is still stopping no longer launches a second copy (LWSM-1168) Stopping a stubborn server can take a few seconds. During that window the app had forgotten it owned the project, so a Start arriving in the middle spawned a second copy — and the stop then killed the first one while reporting the new one as a server the app had not started. Start is now refused with a clear message until the stop finishes.

  • A project whose start script runs a separate program now shows its port (LWSM-1184) Many projects have a start script that only prepares the machine and then hands over to the real program. The scanner read the script and the program, and stopped — so a port the program looks up in a settings file beside it was never found. It now follows that one last step. Contact_List picked up its port, and RetroDB kept the same port while learning where it actually comes from.

  • A port number written inside a sentence is no longer read as a setting (LWSM-1190) Help text and error messages often name a port — "or PORT=5001 in the environment", "expected an integer in 1024-65535". The scanner read those as declarations and showed the wrong port, which is worse than showing none: the app matches status against whoever holds the port, so a wrong number reports a running project as stopped. Both port rules now ask where the declaration sits as well as what it looks like.

  • The scanner follows a launcher whose interpreter is a shell variable (LWSM-1183) A project whose start.sh picks its interpreter at run time and then runs $PYTHON app.py showed no port: the walk never saw the launch line, and followed the assignment above it instead.

  • A very long project name no longer pushes the buttons off the window (LWSM-1174) A single project with a long folder name used to stretch the name column for every row, pushing Start, Stop and Open past the edge of the window with no scrollbar and no way to get them back. Long names are now shortened with an ellipsis; the full name is in the tooltip and is still read out in full by a screen reader.

  • A refused registry write is retried by the next rescan (LWSM-1166) The write gate compared the merge against the controller's in-memory set, which _apply_merge had already updated unconditionally — so a save that was refused once reported "no changes" from then on while nothing ever reached the disk. It now compares against the load, which is refreshed only when a write succeeds.

  • A project whose server crashes can be started again (LWSM-1165) If a project's server exited on its own — a missing dependency, a typo in its start script, an ordinary crash — the app kept treating that project as one it was still managing. Start refused, and Stop and Restart were both greyed out, so there was no way back short of restarting the app. The app now notices within a second and hands the project back. A start script that launches a server and then exits itself is unaffected: that server is still running, so the project stays under management and Stop still works.

  • Config files edited in an editor that adds a byte-order mark now work (LWSM-1182) Some editors put an invisible marker at the start of a file they save. The project list already tolerated it; settings.json and the scan-roots file did not. In settings.json that meant none of your choices were kept, with a message pointing at a character you cannot see; in scan-roots it silently replaced your own comments with ours. Both now accept the marker like the project list always has.

  • A malformed settings file can no longer stop the app opening (LWSM-1164) One particular shape of broken settings.json — thousands of nested brackets — crashed the app on startup instead of being ignored, every launch, until the file was deleted by hand. It is now reported like any other unreadable settings file: the app opens with its defaults and says what it could not read.

  • A typo in the settings file no longer wipes your settings (LWSM-1163) If you hand-edited settings.json and made a small mistake — a stray comma, say — the app fell back to its defaults, as it should. But the next time it saved anything, closing the window included, it wrote those defaults out over your file: theme, text size, poll interval and log cap all reset, and the text you could have fixed gone with them. The app now refuses to save over a settings file it could not read, and says so in the status bar, so the file stays there for you to correct.

  • The pre-push hook gated the working tree, not the commits being pushed. (LWSM-1160) The check before a push tested the files as they sit on disk, which is not what GitHub receives.

  • The pre-push hook ran the gate in a developer's environment, not the runner's. (LWSM-1159) The check that runs before you push now refuses a push when one of its tools is missing or the wrong version, instead of just warning — which is what GitHub does, so the two now agree.

  • The task-accumulation test no longer fails on a busy machine. (LWSM-1158) It counted Python objects rather than live ones. PySide holds a reference to every task handed to the thread pool and releases those lazily, so a loaded machine left up to 163 of 200 behind and looked exactly like the leak the test guards. It now counts only tasks whose underlying C++ object is still alive, and the allowance drops from 20 to 0.

  • The scanner finds a port that lives in a file the launcher imports (LWSM-1155) A project whose launcher keeps its port one import away — serve.mjs importing ./lib/port.mjs — showed as "port unknown". The scanner followed a launcher that runs another file and not one that imports one, and for Node and Python launchers it followed nothing at all. Relative imports only, still exactly one hop, and every existing safety constraint unchanged.

  • A project's whole row, controls included, now fits in one magnifier view (LWSM-1032) Buttons took a fixed width whatever their label said, so a row for a long-named project ran past the width a magnifier shows at once and reading one row meant panning across it.

  • Text-size changes now enlarge the text, not just the space around it (LWSM-1032) The columns widened at 200 % while every word, button and the filter box stayed at their original size — so the one setting the app offers for readability did nothing readable. Three existing tests reported this working, because all three measured the column rather than the letters.

  • The pre-push gate no longer exempts the markdown its own test suite asserts against CLAUDE.md, README.md and every file under docs/standards/ are read by tests/test_docs.py, so an edit to one can redden the suite — but the hook classified a markdown-only push as docs-only and skipped the gate. A prose count documentation.md § 1.5 forbids therefore reached GitHub and failed CI on 5f1891f. The carve-out list is imported from test_docs.GOVERNED rather than copied, and the contract test now RUNS docs_only() instead of scanning its case arms as strings — the predecessor's every assertion held while the escape went through.

  • Quitting while a rescan is running no longer saves over your project list (LWSM-1139) A scan that finished after you closed the window still wrote its result, on the way out. It is now discarded, and a scan that will not finish no longer holds the app open indefinitely either.

  • A chatty project can no longer fill your disk (LWSM-1136) Each project's log was supposed to be capped at 5 MB with one rollover, and nothing was actually applying the cap. It is now applied once a second, per running project.

  • Double-clicking Start no longer launches two copies of a server (LWSM-1137) The check that says "this one is already running" was made before the work that starts it, so two clicks in quick succession could both get through — and the app then forgot about the first server while it was still holding the port.

  • Stopping a project twice at once can no longer close another project's file (LWSM-1138) Two overlapping stops both released the same log file, and the second release could land on a file belonging to something else entirely.

  • The local gate and the GitHub run install the same tools, and a hook runs it before every push. (LWSM-1150) The gate now pins its tool versions in scripts/ci-tools.env, which the workflow reads too, and reports any tool that differs as TOOL DRIFT. A pre-push hook runs the gate automatically, skipping a docs-only push. Local shellcheck 0.11.0 against the runner's 0.9 was why five consecutive pushes went red on a green local run.

  • P04: the rows do not line up, because each lays itself out on its own. (LWSM-1145) Every row positions its own text and buttons, so the Start button sits in a different place on every line. It should read as a table.

  • Projects started by npm, Python or Node now actually start (LWSM-1132) Only projects launched by a shell script worked. Anything the scanner detected as npm run dev, python3 serve.py or node serve.mjs refused to start, with a message that blamed your project for it. The app looked for a file called npm inside your project instead of using the one installed on your machine.

  • A note saying "switch to vite later" is no longer read as a Vite project (LWSM-1130) Rule 3's framework evidence now reads the comment-stripped script value, as the port rules already did, so a commented-out plan cannot hand a project the wrong default port.

  • A folder whose name is not valid text no longer makes its log entry vanish (LWSM-1128) The name is now cleaned before it is shown or stored, so it can be written out as text.

  • A launcher whose command ends in a redirect or a config path has its port found again (LWSM-1123) exec python3 app.py >> /var/log/app.log is an ordinary start script; the port scan gave up at the trailing token instead of reading the rest of the line.

  • One unreadable or hostile project directory no longer blanks the whole scan (LWSM-1122) A directory the app may not enter — root-owned, another user's, or chmod 000 — made the scan return nothing at all instead of skipping that one project. Measured before the fix: 20 healthy projects came back as 0.

  • The core/UI layering check now covers every module the rule names (LWSM-1006) applog.py was covered by the rule and missing from the check, and the rule itself was worded in a way that would have failed on the entry point. Both corrected, and a new test derives the list from the rule so the two cannot drift apart again.

  • Quitting is no longer delayed by a port check that never finishes (LWSM-1117) A port check that hangs could keep the process alive indefinitely. That was already handled for the app itself, but not for anything else that uses the same machinery — including the test suite, which paid a hidden 2.6 seconds on every run. Measured while fixing it: against a check that genuinely never returns, the wait was not slow but unbounded — a probe process had to be killed after three minutes.

  • Turning up the system text size now resizes rows already on screen (LWSM-1119) Changing the application font did nothing to rows that were already displayed; only rows created afterwards picked it up. For a partially-sighted user the text-size setting is the one most likely to be changed, and it was the one route that did not work.

  • Colour themes now reach the text, not just the window frame (LWSM-1118) The theme's colours were applied to the window and discarded before they reached the rows inside it. The default light theme hid this by luck — the fallback colour happened to have better contrast — but a dark theme rendered project names and ports at 1.25:1 against a 4.5:1 readability floor, which is effectively invisible. Found before any dark theme shipped.

  • The app starts on a machine with no home directory instead of crashing (LWSM-1116) Where no home directory can be resolved — rare, but real in a stripped container — the app died with a traceback and no window, instead of falling back to printing its log to the terminal as it was designed to. The same fix found a second half nobody had reported: the guard protecting the project-list path was being evaluated outside the code that catches it, so it had never been able to do anything.

  • A broken project file can no longer wipe your log or stall the window (LWSM-1114) Two limits were missing from projects.json handling. One field — schema_version — was never length-limited, so a large value in it produced a single log record over a megabyte and forced the log to roll over, destroying the history you are told to consult. And nothing bounded how many problems were reported: a maximally broken file produced 524,271 of them, 28.7 MB of log, and 8.7 seconds of no window at all before the app appeared. Now at most 100 problems are reported, with a line saying how many more there were, and every value is length-limited. Measured after: 101 problems, 5 KB, one millisecond. (LWSM-1115)

  • Two more pieces of text can be translated, and switching language reaches rows already on screen (LWSM-1107)

  • A project removed from the list no longer stays drawn on screen (LWSM-1106) It was left visible on top of the row that moved up into its place.

  • Pointing the app at a folder instead of a settings file no longer uses up a system resource (LWSM-1104)

  • The status dot is no longer sliced in half at large text sizes (LWSM-1101) The space reserved for it was measured once at startup and never again, so at 200 % text the dot needed 14 pixels and had 13 — exactly the setting the people who rely on it are using.

  • Closing the window cannot let one last status update through (LWSM-1098) A status message already on its way was still delivered after shutdown.

  • Quitting is no longer held up by a stuck port lookup (LWSM-1100) The shutdown wait was bounded but only moved: the process still waited for the stuck lookup at the very end. Measured at 4.16 s to exit behind a 4 s probe. The app now ends without waiting for work it has already given up on.

  • The app no longer leaks memory while it watches (LWSM-1099) One internal task and one signal object were kept for the life of the process on every one-second poll — about 210 MB a day, in a program meant to stay open. Measured at 200 retained objects after 200 polls.

  • Screen readers are told what changed, and not told what did not (LWSM-1071, LWSM-1076) The little status dot was read aloud as "black circle", which a note in the code claimed could not happen. It is now drawn rather than labelled, so it stays on screen and out of the screen reader. Separately, a status change was never announced at all; it now is, exactly once, and unchanged rows stay quiet.

  • Keyboard focus is visible, and the row no longer spreads across the window (LWSM-1070, LWSM-1074) Moving around the window with the keyboard showed nothing at all — the focused and unfocused window were pixel-for-pixel identical. There is now a visible ring, and it grows with the text size. Widening the window used to fling a project's name and its port to opposite edges, so reading one row meant sweeping a magnifier across the screen; the row now stays together.

  • A damaged or hostile settings file can no longer hang or crash startup (LWSM-1072, LWSM-1082) A booby-trapped projects.json — a named pipe, a device, an enormous file, a 5000-digit port or deeply nested brackets — could hang the app on startup or kill it with no window at all. All of them now open an empty window that tells you what is wrong, which is what was always intended. A file saved with a byte-order mark by an editor is also accepted rather than refused for a reason that pointed nowhere.

  • The status display no longer freezes silently (LWSM-1069, LWSM-1073, LWSM-1079) If the part that checks which ports are busy hit an unexpected error, the app quietly stopped checking for good — the window kept showing whatever it last saw, with no warning, no error and nothing in the log. It now keeps checking, and says what went wrong. Closing the app is also reliable now: a last message could arrive after the window had gone, and a stuck lookup could stop the app quitting at all. And a fault that repeats is written to the log once with a count, instead of once a second until it has scrubbed away everything else.

  • A rapid second push to main no longer leaves the first commit ungated cancel-in-progress applied to pushes as well as pull requests, and nothing ever returns to a cancelled run. It is now limited to pull requests, where only the head commit is ever merged.

  • A missing actionlint no longer reports a clean local-CI pass actionlint and yamllint shared one flag, so when actionlint was absent and yamllint present, yamllint set the flag and suppressed the actionlint skip as well — the gate printed "Local CI passed." with zero SKIPs and exit 0. Reproduced with stub executables on PATH. Each tool now has its own guard and its own skip. actionlint is the likeliest to be missing, having no distro package.

  • The state directory's parents are created 0700, not just its leaf mkdir(parents=True, mode=0o700) applies the mode to the final component only, leaving every intermediate at the umask default.

  • lwsm rejects an unrecognised option instead of ignoring it --version was a membership test over the argument list, so lwsm --versoin printed the startup banner and exited 0, and there was no --help at all. An argparse parser now exits 2 with usage.

  • A log file deleted underneath the app is reopened instead of silently lost logrotate, systemd-tmpfiles and a user tidying ~/.local/state all do this. The idempotence guard compared only the path, so it reused a handler holding an unlinked inode and every later record went where no name could reach it. It now compares the inode too.

  • The app starts even when it cannot write its log Any OSError from log setup — a read-only filesystem, a full disk, an unwritable state directory, or one of the hostile states the hard-link and FIFO hardening under Security now refuses — killed the process with a traceback. It warns on stderr and carries on. Without this, that hardening would have turned a log-integrity attack into a total-outage one.

Security

  • Approving an npm project now covers its pre and post scripts and its .npmrc (LWSM-1363) A project could add a predev or postdev script, or an .npmrc that changes how npm runs, and the next Start ran it with no new question. Every npm project approved before this asks once more.

  • A package.json that is a link no longer makes one approval cover any later change (LWSM-1364) Its scripts are read through the link, and an npm launch whose script cannot be read is refused rather than offered.

  • The approval window for an npm project lists the commands npm will run (LWSM-1365) It showed only "npm run dev". It now lists each script and each .npmrc line, escaped so none can fake the window's layout.

  • A server another account runs on the same port is no longer shown as yours (LWSM-1366) With your server on 127.0.0.1 and theirs on ::1, the row showed as managed and Open could reach theirs without a warning.

  • An imported profile never sets a start command or start at login (LWSM-1369) Like custom actions, these run something, and a profile is a file someone can hand you.

  • A launcher reached through a folder link is refused when another account could swap that link (LWSM-1352) The trust gate checked the folders around the file a launcher resolves to, but not the folders the path as written passes through. A link in a group-writable folder could be swapped after approval.

  • A project or browser name holding markup is shown as typed in tooltips, not rendered A name like <b>x</b> was drawn as formatting and could forge a sentence inside the tooltip that explains whose server is running.

  • An npm run project whose package.json another account can rewrite is now refused, and a launched server no longer resolves commands from relative PATH entries Both were ways past the confirmation the app asks for before it first runs a project. package.json now meets the same ownership and writability checks a start script does, and an empty, . or relative PATH entry is dropped before the child starts (review-code 2026-10-01).

  • MEDIUM: an argv of three or more elements names no launcher, so it skips validate_launcher entirely. (LWSM-1228) A start command the manager cannot classify — bash -x start.sh, env node serve.mjs, npm start — is now refused instead of run with none of the launcher safety checks applied. Every launcher the scanner detects is unaffected; the route in was a hand-edited projects.json.

  • MEDIUM: the launcher byte cap truncates instead of refusing, so appended content never re-arms trust. (LWSM-1227) A start script or package.json larger than the fingerprint cap is now refused outright instead of being hashed on its opening mebibyte, so content appended past the cap can no longer stay trusted. No real launcher here comes close to the cap.

  • A launcher that someone else could swap out is refused (LWSM-1226) Approving a project's start-up script vouches for what it says. The app checked that the file itself was not writable by others, but replacing a file needs permission on the folder holding it — so a script in a shared folder could be swapped for a different one after you approved it. The folder and the file's owner are now checked too.

  • No directory name can forge the "Run this launcher?" prompt (LWSM-1196) The prompt escaped carriage return and newline only. Qt also breaks a line on U+2028 and U+2029, and U+202E reverses the text it draws — so a project folder could add its own "This will execute:" heading naming a different launcher, or make the real path read as another one. Every character that moves the layout rather than adding a glyph is now escaped.

  • A project folder cannot dress up the "run this launcher?" box any more (LWSM-1181) That box exists to show you exactly what is about to run, and the folder name in it comes from whoever created the folder — which may not be you. A name containing a line break could draw a second, fake "This will execute:" heading above the real one, naming a different program; a name containing the box's own placeholder markers could paste the launcher path into itself. Names now go in without being re-read, and line breaks are shown as text rather than acted on.

  • Create the directory holding the KWin placement script owner-only, every component of it (LWSM-1171) "Centre on screen" writes a script the compositor then executes. The directory it goes in was created with a form that sets the mode on the last component only, leaving the ones above it readable and traversable by other accounts on the machine — and reused an existing directory whatever its permissions were. It now goes through the same routine the log and the config files already use.

  • Open in browser now checks who is actually holding the port (LWSM-1167) Open is meant to be offered only for a server this app started. It was deciding that from its own records — "we started something for this project" — rather than from who is listening right now. So if your project's server had crashed and anything else picked up the port, Open would light up and send your browser to a stranger's page with this app's name behind it. The app now asks the system which process holds the port and only offers Open when that process belongs to the server it started.

  • A start script pointing outside its project is now refused (LWSM-1162) A project could point its start script at a file somewhere else on the disk. The app showed you the harmless-looking name, ran the other file, and never asked again — even if that other file was rewritten afterwards. That launcher is now refused outright, so it never reaches the "do you trust this?" dialog at all.

  • Open in browser is offered only for servers this app started (LWSM-1141) Anything on your machine can sit on a project's port, and the app would then show that row as running and offer to open it in your browser. It cannot tell you what is actually there yet, so it no longer sends you to a page it cannot vouch for. Servers the app started itself are unaffected.

  • Approving an npm or Node project no longer approves whatever it is later changed to run (LWSM-1140) Approving a project to run binds that approval to what it actually runs. For an npm run dev project that is the dev command inside package.json, and for a Python or Node project it is the script file. Change either and the app asks you again, instead of silently reusing the old approval — which matters because installing a package can rewrite that command without you seeing it.

  • The last rejection message built from a foreign filename is escaped and length-bounded (LWSM-1124) A folder with a strange name could push invisible control characters into the app log and the status bar, and past the 120-character bound its seven sibling messages keep.

  • Every file the scanner reads is read under a bound (LWSM-1050) The scanner reads files inside other people's project folders, so none of it is trusted: a size cap and a line cap per file, a deadline checked per line, a refusal of anything that is not an ordinary file, a refusal of symlinks at both the folder and the file level, and a containment check so a launcher cannot point the scanner at something outside its own project.

  • Two entries for the same folder can no longer both load (LWSM-1103) A path written with two slashes at the front counted as a different folder from the same path with one, so the same project could appear twice.

  • A hand-edited project file cannot flood the status bar or the log (LWSM-1102) A long value in a project's port field produced a 200,000-character message, which would have pushed the app's own history out of the log the user is told to consult. Long values in the name and folder path were already capped; the cap now covers every field, and caps the escaped text rather than the raw text.

  • The tool that installs dependencies is updated for a published advisory (LWSM-1083, LWSM-1064) uv is moved from 0.11.7 to 0.12.2, both on this machine and in CI. The old version is affected by GHSA-4gg8-gxpx-9rph, where a malicious package could place an executable outside the intended environment. The lockfile is unchanged, so no dependency moved.

  • A project name in the settings file can no longer forge log lines (LWSM-1078) A name in projects.json was copied into the app's messages and log exactly as written, so one containing a line break could fake what looked like a separate log entry, and a very long one could flood the status bar. Names are now escaped and shortened. Two entries pointing at the same folder by different routes are also caught, and a name containing a stray zero byte is refused instead of loaded.

  • The application log now refuses a hard link and a FIFO, not just a symlink O_NOFOLLOW closed only part of the hole it was added for. A hard link is not a symlink, so linking app.log to any file the user owns still fed it every log record; and O_NOFOLLOW does not reject a FIFO, where O_WRONLY blocks until a reader appears, so a named pipe planted at app.log hung startup forever with no error and no log line. Both were reproduced against the shipped code. The handler now interrogates the opened descriptor instead of trusting the path: a regular file, exactly one link, owned by us.

  • The application log is written 0600 in a 0700 directory and refuses to write through a symlink (O_NOFOLLOW).

  • The setuptools build backend is pinned and constrained; it executes at build time and was outside the lockfile.

  • CI actions pinned to commit SHAs, and the checkout no longer leaves the job token in the workspace.