* feat(shell): zoom shortcuts on Windows and Linux (#1109)
Cmd/Ctrl and +/−/0 (numpad included) now zoom the app on every platform.
Windows/Linux run without a menu (`setMenu(null)`), so the shortcuts are a
renderer key binding in `WorkspaceKeyboardShortcutsService` calling a new
synchronous, preload-local bridge method `adjustZoomLevel`, which steps the
frame-bound temporary level through `webFrame.setZoomLevel` — never a
main-process `webContents.setZoomLevel`, whose per-URL entry the app's
`file://` path routing resets. Step and limits live in
`libs/shared/interfaces` (`stepZoomLevel`: 0.5 per press like Electron's
zoomIn/zoomOut roles, clamped to levels −4…6). On macOS the renderer sees the
key before the application menu, and `preventDefault()` keeps the menu role
from stepping a second time (Electron only performs the menu key equivalent
in its unhandled-keyboard-event hook).
Persistence is unchanged: the main process still reads the live level back
on close, quit and reload. The zoom E2E now drives the real shortcuts (in,
out, numpad, reset). Help dialog entries added and translated for all
locales; contract updated in docs/architecture/workspace-shell.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(shell): never step a stored out-of-range zoom level against the request
A level persisted before the shortcuts existed (the macOS menu roles never
clamped, and the store restores any finite level) was clamped BEFORE the
step, so the first zoom-in from level 7 rendered smaller. Step from the raw
level instead: a press further out leaves an out-of-range level where it is,
a press back in lands on the limit.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(shell): state the zoom bridge's return contract precisely
`adjustZoomLevel` steps by `stepZoomLevel`'s rules; a stored out-of-range
level is never moved against the request, so the returned level is not
itself guaranteed to be within `ZOOM_LEVEL_MIN..ZOOM_LEVEL_MAX`.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* docs(release): add a release note for the zoom shortcuts
The Release note gate requires an added `.changes/*.md` for runtime changes;
the shortcuts are a user-visible feature of their own, so they get their own
note and the persistence note stays about persistence.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* chore(deps): upgrade Angular to 22.1 and Nx to 23.2
* fix(deps): complete Angular migrations after rebasing on master
* fix(ci): use the Node pin for Windows runtime refresh
* docs(deps): synchronize the workspace-shell Node requirements
* fix(build): include shared UI stylesheets in Nx cache inputs
`libs/ui/styles` held shared SCSS partials but had no `project.json`, so its
files belonged to no Nx project and were absent from every task hash. Editing
a partial and running `pnpm nx build web` reported 4 of 4 tasks cached and
shipped the previous CSS — a silent wrong build rather than a failure.
Nx derives its project graph from TypeScript imports only, so a relative Sass
`@use` that crosses a project root creates no edge. Verified directly: after
adding the project but before declaring anything, `ui-styles` still had zero
dependents in the graph.
Make it the `ui-styles` project (no targets — it exists to be hashed) and
declare `implicitDependencies` on the 8 consumers. Chosen over adding the path
to `sharedGlobals`, which would put shared styles into every project's hash and
make a one-line SCSS tweak mark the whole workspace affected. A styles edit now
marks 15 projects affected and leaves electron-backend, website, the mock
servers and the shared libs alone.
`libs/ui/styles` was the only projectless directory holding files under `libs/`
or `apps/`.
Add `pnpm run styles:inputs:validate` to keep it closed: it resolves every
relative stylesheet import against Nx's real project graph and fails when one
escapes the input closure of a build that compiles it, naming the project to
declare. It exits 1 with 21 diagnostics on the pre-fix tree. Imports of
`apps/web/src/nav-list.scss` are deliberately accepted — `web` already hashes
that file, and a lib -> app edge would make the graph cyclic.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(build): spawn git without a shell in the stylesheet check
`execSync("git ls-files '*.scss'")` runs through `cmd.exe` on Windows, where
single quotes are literal characters rather than quoting. Git received the
pathspec with the quotes intact, matched nothing and exited 0, so
`styles:inputs:validate` reported success after checking zero stylesheets —
silently disabling the check for Windows developers while staying green.
Spawn with `execFileSync` so no shell is involved and git expands its own
pathspec; verified to return the identical 133 files.
Both this and the eslint glob trap next to it in the docs report success while
covering nothing, so also make an empty scan fail rather than pass: the
workspace always contains SCSS, and a listing that returns none means the scan
broke.
Reported by Codex review on #1360.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(styles): move nav-list partial into ui-styles (#1361)
* fix(build): count every target of a comma-separated Sass @import
`@import` is the only rule that takes a list, and the scan read just its
first target. A later entry crossing an Nx project boundary escaped the
cache key while the check still reported success — the same silent-pass
failure the tool exists to prevent.
Parse every target of an `@import` list. The obvious "read all quoted
strings" fix trades one silent gap for a phantom one, so the rule decides:
`@use`/`@forward` load exactly one module and a quoted string after it is
`with (...)` configuration, and `url(...)` stays a plain CSS import the
browser resolves at runtime. Neither is a module Sass compiles.
The workspace has no relative `@import` at all today, so the scan still
finds the same 42 imports across 133 files; this closes the gap before
someone writes one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Pipeline audit follow-up: reduce wasted runner time on PRs and tighten CI
security, without reducing what actually gets validated.
Runner-time waste:
- Concurrency with PR-only cancel-in-progress on CI, E2E, and docker-build,
so a new push cancels the previous commit's still-running checks. Non-PR
runs use the unique run_id as the group, because GitHub keeps at most one
pending run per group even with cancel-in-progress: false — a shared ref
group could silently drop a queued master run.
- paths-ignore for docs-only changes (Markdown, docs/, .plans/, .codex/,
.claude/) on the Electron build matrix and the E2E suites; E2E also skips
apps/website/**. The build workflow keeps apps/website/** because its Linux
job builds the website to verify AppStream assets. Tag pushes are
unaffected: GitHub does not evaluate paths filters for tags.
- PRs lint affected projects only; master pushes keep the full run-many.
Lint-global inputs (eslint.config.mjs, tools/eslint/**) now mark all 41
lint projects affected, including the run-commands targets database and
packaging, so the max-lines baseline cannot be widened without lint.
Hardening:
- Explicit least-privilege permissions on CI, E2E, and build-and-make; the
create-release job keeps its job-level contents: write. The repository
default workflow token was switched to read-only.
- New actionlint job (image pinned by digest, shellcheck at warning+), with
the shared-anchor false positive suppressed in .github/actionlint.yaml.
Fixed one real finding: unquoted $GITHUB_OUTPUT.
- .github/dependabot.yml: weekly cadence, minor+patch grouped per ecosystem
(npm, GitHub Actions, Docker), majors stay individual PRs.
Docs updated: CLAUDE.md, docs/architecture/nx-workspace-boundaries.md, and
docs/architecture/validation-map.md now describe affected-lint on PRs and the
E2E path-filter exceptions.
* fix(lint): resolve module-boundary and prefer-inject errors
Retag workspace-shell-util as type:data-access to match its injectable
services that depend on @iptvnator/services, and convert
RemoteControlService to inject(HttpClient).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(lint): enforce max-lines 400 with generated baseline
Add a max-lines ESLint error (hard cap 400 raw lines per TypeScript
file) per the repo file-size rule. The 134 pre-existing offenders are
baselined in tools/eslint/max-lines-baseline.mjs, regenerable via
generate-max-lines-baseline.mjs; the list should only shrink.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(ci): enforce lint on PRs and guard coverage policy drift
- Add a Lint job to ci.yml running nx run-many -t lint --all, so
module-boundary tags, legacy-alias bans, and max-lines gate merges.
- Fix the root lint script (was linting only electron-backend).
- Add tools/coverage/check-coverage-policy.mjs: fails CI when a project
with a test target is missing from coverage-policy.json; wired into
coverage:ci as coverage:policy:check.
- Run Tier B/C unit tests in CI without coverage (list derived from the
policy), so website/packaging/remote-control tests run on PRs.
- Replace the hand-picked 16-project test:unit:ci list with --all.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: document CI lint enforcement and coverage policy guard
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(ci): address bot review feedback on policy guard and baseline generator
- Drive Tier B/C validation from each policy entry's validationCommand
(falling back to nx test), skipping projects with an e2e target since
the E2E workflow already runs them (Codex).
- Fail when a Tier A entry has no test target (Greptile, adapted:
checking all entries against test targets would false-positive on the
intentionally spec-less e2e/mock-server tiers).
- Guard against missing JSON array in nx show projects output (Greptile).
- Scan .tsx files in the max-lines baseline generator to match the
ESLint rule's file patterns (Greptile).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>