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.
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.