Commit Graph

2085 Commits

Author SHA1 Message Date
fuomag9 394287bd76 retake analytics screenshots with new UI and seeded traffic data
Screenshots now show the current UI (with WAF in sidebar) and realistic
traffic data: 2,299 requests over 7 days, country heatmap, protocol
breakdown, user agent distribution, and blocked request log.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 15:07:03 +01:00
fuomag9 8549376e2b fix: restore analytics screenshots with sample data
The refreshed analytics screenshots showed empty data which looks bad
for the website showcase. Restored the previous versions with real
traffic data.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 14:40:14 +01:00
fuomag9 a332bc46a0 docs: update website with WAF, Instance Sync, mobile UI features and refresh all screenshots
- Add WAF, Instance Sync, and Mobile Responsive cards to features grid
- Update Auto HTTPS card to mention built-in CA
- Add WAF spotlight section with screenshot
- Update hero tagline and OG meta description
- Refresh all 9 screenshots from running app (new WAF screenshot added)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 14:38:58 +01:00
fuomag9 4412b7ad94 Update README.md 2026-03-12 13:44:33 +01:00
fuomag9 2ba026c0dc Update .gitignore 2026-03-12 10:22:45 +01:00
fuomag9 cf7eb7757e fix: address code review — responsive overflow, map minWidth, DataTable mobile click handler
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 09:58:45 +01:00
fuomag9 27b7fadacc test: restrict mobile-iphone project to mobile tests only 2026-03-12 09:07:15 +01:00
fuomag9 6e8db4ec39 test: add mobile layout E2E tests for iPhone 15
- Create tests/e2e/mobile/mobile-layout.spec.ts with 8 tests covering
  AppBar/hamburger visibility, drawer open/close, mobile card rendering,
  PageHeader button layout, dialog width, card actions, and analytics overflow.
- Fix AnalyticsClient: make Autocomplete full-width on mobile, add
  overflow:hidden to outer Stack to prevent body scrollWidth growth.
- Fix WorldMapInner: remove hard-coded minWidth:400 that caused 73px
  horizontal overflow on 393px iPhone 15 viewport.
- Fix DashboardLayoutClient: add overflowX:hidden to main content area
  to contain chart library elements that exceed viewport width.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 09:04:58 +01:00
fuomag9 bfcc24eac0 test: add iPhone 15 Playwright project 2026-03-12 01:33:17 +01:00
fuomag9 599479befa feat: make analytics charts mobile-safe with overflow wrappers
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 01:31:34 +01:00
fuomag9 222da9c731 feat: add mobile card views to ImportedTab and CaTab
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 01:28:09 +01:00
fuomag9 b97e41ed07 feat: add mobile card view to AcmeTab
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 01:16:19 +01:00
fuomag9 3c47bbf110 feat: add mobile card view to AuditLogClient
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 01:13:37 +01:00
fuomag9 4245688129 feat: add mobile card view to WafEventsClient
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 01:11:20 +01:00
fuomag9 adda0df2ef feat: add mobile card view to ProxyHostsClient
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 00:50:44 +01:00
fuomag9 ee8c3095ab feat: add mobileCard prop to DataTable for card view on mobile
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 00:29:45 +01:00
fuomag9 44dc7d128b feat: stack PageHeader vertically on mobile 2026-03-12 00:28:26 +01:00
fuomag9 a75642bc7f feat: replace floating hamburger with proper mobile AppBar
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-12 00:26:00 +01:00
dependabot[bot] c1e20db3ab ci(deps): bump docker/metadata-action from 5 to 6 (#52)
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 18:57:25 +01:00
dependabot[bot] e62639a5a1 ci(deps): bump docker/build-push-action from 6 to 7 (#53)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 18:57:16 +01:00
dependabot[bot] 61cedfe657 ci(deps): bump docker/login-action from 3 to 4 (#54)
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 18:57:05 +01:00
dependabot[bot] bbc5ec3ed4 ci(deps): bump docker/setup-buildx-action from 3 to 4 (#55)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 18:55:27 +01:00
dependabot[bot] 738c5f84ea ci(deps): bump actions/setup-node from 4 to 6 (#56)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 18:55:22 +01:00
dependabot[bot] bfedd1e958 deps(deps-dev): bump the development-dependencies group with 2 updates (#57)
Bumps the development-dependencies group with 2 updates: [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) and [eslint](https://github.com/eslint/eslint).


Updates `@types/node` from 25.3.3 to 25.3.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `eslint` from 10.0.2 to 10.0.3
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.0.2...v10.0.3)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.3.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
- dependency-name: eslint
  dependency-version: 10.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 18:55:17 +01:00
dependabot[bot] 2b64f67271 deps(deps): bump apexcharts in the production-dependencies group (#58)
Bumps the production-dependencies group with 1 update: [apexcharts](https://github.com/apexcharts/apexcharts.js).


Updates `apexcharts` from 5.10.1 to 5.10.3
- [Release notes](https://github.com/apexcharts/apexcharts.js/releases)
- [Commits](https://github.com/apexcharts/apexcharts.js/compare/v5.10.1...v5.10.3)

---
updated-dependencies:
- dependency-name: apexcharts
  dependency-version: 5.10.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-09 18:55:12 +01:00
fuomag9 26fcf8ca90 fix WAF silently dropping WebSocket upgrade requests
When allowWebsocket=true and WAF is enabled, the WAF handler sits first
in the handler chain and processes the initial HTTP upgrade request
(GET + Upgrade: websocket). If any rule matches, Coraza can block the
handshake before SecAuditEngine captures it — producing no log entry
and an unexplained connection failure from the client's perspective.

Fix: when allowWebsocket=true, prepend a phase:1 SecLang rule that
matches Upgrade: websocket (case-insensitive) and turns the rule engine
off for that transaction via ctl:ruleEngine=off. After the 101
Switching Protocols response the connection becomes a raw WebSocket
tunnel that the WAF cannot inspect anyway, so this bypass has no impact
on normal HTTP traffic through the same host.

The rule is inserted before OWASP CRS includes so it always fires first
regardless of which ruleset is loaded.

Add 9 unit tests in caddy-waf.test.ts covering: bypass present/absent,
phase:1 placement, case-insensitive regex, nolog/noauditlog flags,
ordering before CRS, and compatibility with custom directives.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 23:14:12 +01:00
fuomag9 d6df70ab5f fix WAF merge mode ignoring host.enabled=false, extract resolveEffectiveWaf
Bug: when a proxy host had per-host WAF explicitly disabled (enabled:false)
with waf_mode:"merge" (or no waf_mode set), resolveEffectiveWaf entered the
merge branch and returned enabled:true unconditionally, applying the global
WAF to a host the user had opted out of.

Fix: add `if (host.enabled === false) return null` at the top of the merge
branch. Explicit opt-out now takes precedence over the global setting
regardless of mode. The override mode already handled this correctly.

Also extract resolveEffectiveWaf from caddy.ts into caddy-waf.ts so it
can be unit tested. Add 12 new tests covering no-config fallback,
merge opt-out regression, merge settings combination, and override mode.

What runs without OWASP CRS: only SecRuleEngine + audit directives +
any custom_directives. The @coraza.conf-recommended and CRS includes
are gated behind load_owasp_crs (fixed in previous commit).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 21:39:00 +01:00
fuomag9 b5bb668bd9 test: suppress console output noise in vitest
Add onConsoleLog: () => false to vitest config to silence expected
warn/error calls from production code when tests deliberately feed bad
input (e.g. parseJson with malformed JSON). Tests can still use
vi.spyOn(console, ...) to assert on console calls explicitly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 02:39:29 +01:00
fuomag9 05f06cf386 ci: security review — add PR test trigger, explicit permissions on all jobs
- test.yml: add pull_request trigger (safe — permissions: {}, no secrets
  referenced; pull_request event from forks gets no secrets and read-only token)
- stale.yml: add explicit permissions (issues: write, pull-requests: write)
  instead of relying on potentially over-broad repo defaults
- docker-build-pr.yml: add actions: write permission required for GHA
  cache writes (cache-to: type=gha)

Remaining known limitation: actions are pinned to tags (@v3/@v6) rather
than SHAs — moving a tag upstream would run arbitrary code. Low risk for
official Docker/GitHub actions but worth noting.

ci: remove cache-to from PR build, drop actions: write permission

PR builds read from the GHA cache (warmed by main branch pushes) but
don't write back. This avoids needing actions: write on a job that runs
untrusted PR code.

ci: restrict test job permissions to zero

Zero out GITHUB_TOKEN permissions and remove the non-existent
ACTIONS_RUNNER_NO_SECRETS variable. No repo secrets are referenced
in this workflow, so the test runner has no credentials available.
The workflow only triggers on push to protected branches (not PRs),
so code is reviewed before it runs.

ci: add test workflow to run unit/integration tests on push

Runs `npm test` (Vitest unit + integration tests) on every push to
main and develop. E2E tests are excluded as they require a full
Docker Compose stack.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 01:49:04 +01:00
fuomag9 f0825d6497 fix WAF crash when enabled without OWASP CRS, add regression tests
The WAF handler always prepended 'Include @coraza.conf-recommended' to the
SecLang directives regardless of load_owasp_crs. The @-prefixed paths only
resolve from the embedded coraza-coreruleset filesystem, which the Caddy
WAF plugin mounts only when load_owasp_crs=true. Without it Caddy fails:
  "failed to readfile: open @coraza.conf-recommended: no such file or directory"

Fix: gate all @-prefixed Include directives behind load_owasp_crs.

Also extract buildWafHandler from caddy.ts into caddy-waf.ts so it can be
unit tested in isolation, and add tests/unit/caddy-waf.test.ts (19 tests)
covering the regression, CRS include ordering, excluded rule IDs, and
handler structure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-08 01:19:10 +01:00
fuomag9 fd847e7eb5 fix mTLS cross-CA isolation bug, add instance-sync and mTLS tests
Extract pemToBase64Der and buildClientAuthentication from caddy.ts into
a new caddy-mtls.ts module, adding groupMtlsDomainsByCaSet to group mTLS
domains by their CA fingerprint before building TLS connection policies.

Previously all mTLS domains sharing a cert type (auto-managed, imported,
or managed) were grouped into a single policy, causing CA union: a client
cert from CA_B could authenticate against a host that only trusted CA_A.
The fix creates one policy per unique CA set, ensuring strict per-host
CA isolation across all three TLS policy code paths.

Also adds:
- tests/unit/caddy-mtls.test.ts (26 tests) covering pemToBase64Der,
  buildClientAuthentication, groupMtlsDomainsByCaSet, and cross-CA
  isolation regression tests
- tests/unit/instance-sync-env.test.ts (33 tests) for the five pure
  env-reading functions in instance-sync.ts
- tests/integration/instance-sync.test.ts (16 tests) for
  buildSyncPayload and applySyncPayload using an in-memory SQLite db
- Fix tests/helpers/db.ts to use a relative import for db/schema so it
  works inside vi.mock factory dynamic imports

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 18:32:52 +01:00
fuomag9 e5ba3e1ed9 refractor code to allow more tests 2026-03-07 16:53:36 +01:00
fuomag9 f85c425ac1 fixed more tests 2026-03-07 11:22:54 +01:00
fuomag9 543b598d78 fixed tests more 2026-03-07 02:28:16 +01:00
fuomag9 2537fc8a14 fix tests 2026-03-07 02:16:35 +01:00
fuomag9 6e987e2c90 fix more tests paths 2026-03-07 02:11:45 +01:00
fuomag9 77e9a7d2f1 fix test docker 2026-03-07 02:08:21 +01:00
fuomag9 3572b482e8 added tests 2026-03-07 02:02:14 +01:00
fuomag9 7e134fe6b5 added QUIC ports to docker compose 2026-03-07 01:43:26 +01:00
fuomag9 264e80ed73 consolidate WAF into unified page, reorder sidebar nav
- Move WAF config (enable, CRS, custom directives, templates) from
  Settings page into a new Settings tab on the WAF page
- WAF page now has three tabs: Events | Suppressed Rules | Settings
- Rename nav item from "WAF Events" to "WAF", route /waf-events → /waf
- Fix excluded_rule_ids preservation: no longer wiped when form field
  is absent (Settings tab omits the hidden field intentionally)
- Allow pre-adding suppressed rules even when WAF is disabled
- Reorder sidebar: Overview, Proxy Hosts, Access Lists, Certificates,
  WAF, Analytics, Audit Log, Settings

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 23:58:02 +01:00
fuomag9 41c6db3a3f waf suppressed rules: add by ID with lookup, search filter
- Add lookupWafRuleMessageAction server action — queries WAF event
  history for a known message for any rule ID
- Suppressed Rules tab: type a rule ID, look it up to see its
  description (or a "not triggered yet" note), confirm to suppress
- Duplicate-guard: looking up an already-suppressed rule shows an error
- Search field filters the suppressed list by rule ID or message text
- Newly added rules show their message immediately without page reload

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 22:50:21 +01:00
fuomag9 9a82ad9033 hide revoked client certs by default, add show-revoked toggle
- IssuedCertsPanel preview: only show active (non-revoked) certs
- ManageIssuedClientCertsDialog: filter out revoked by default; show
  "Show revoked (N)" toggle when revoked certs exist

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 22:38:18 +01:00
fuomag9 6ecd195073 redesign certificates page: tabs, drawers, relative expiry, status bar
- Split ACME / Imported / CA-mTLS into tabs with count badges
- Add clickable status summary bar (expired / expiring soon / healthy)
- Per-tab search filter by name and domain
- Replace accordion cards with DataTable for imported certs
- Slide-in Drawers (480 px) for add/edit imported and CA certs
- File upload + show/hide toggle for private key in ImportCertDrawer
- CaCertDrawer: Generate / Import PEM tabs for add, simple form for edit
- CA tab: expandable rows showing issued client certs inline
- RelativeTime component: "in 45 days" / "EXPIRED 3 days ago" with date tooltip
- Remove CreateCaCertDialog and EditCaCertDialog (replaced by CaCertDrawer)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 22:36:46 +01:00
fuomag9 d6658f09fd fix mTLS: fail closed when all certs revoked, fix domain split ordering
When all issued certs for a CA are revoked, buildAuth returns null.
Previously the code would merge mTLS domains back into a policy with no
client_authentication, silently dropping the requirement and allowing
unauthenticated access (open bypass).

Fix by always splitting mTLS and non-mTLS domains first, then using
drop: true when buildAuth returns null — so a fully-revoked CA causes
Caddy to drop TLS connections for those domains rather than admit them
without a client certificate.

Also removed the redundant first buildAuth(domains) call in the
auto-managed path that was used only as an existence check.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 21:46:13 +01:00
fuomag9 90fa11ae3c fix mTLS: trusted_leaf_certs requires trusted_ca_certs for chain validation
Caddy's trusted_leaf_certs is an additional check on top of CA chain
validation, not a replacement. Without trusted_ca_certs, Go's TLS
rejects the client cert before the leaf check runs, causing 'unknown ca'.

Updated buildClientAuthentication to always include the CA cert in
trusted_ca_certs for chain validation, and additionally set
trusted_leaf_certs for managed CAs to enforce revocation. When all
issued certs for a CA are revoked, the CA is excluded from
trusted_ca_certs entirely so chain validation fails for any cert from it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 21:18:33 +01:00
fuomag9 9fa57bcf28 fix mTLS: use trusted_leaf_certs for issued certs, surface CA delete errors
Two bugs fixed:

1. buildClientAuthentication was placing issued leaf cert PEMs into
   trusted_ca_certs. Caddy uses that field for CA chain validation, not
   leaf pinning — putting leaf certs there made chain verification fail
   for every presented client cert, causing the browser to be asked
   repeatedly. Fixed by using trusted_leaf_certs for managed CAs.

2. If all issued certs for a CA were revoked, the active cert map would
   be empty and the code fell back to trusting the CA cert directly,
   effectively un-revoking everything. Fixed by tracking which CAs have
   ever had issued certs (including revoked) and keeping them in
   trusted_leaf_certs mode permanently (empty list = no one trusted).

Also fix CA certificate delete action not surfacing the error message
to the user in production (Next.js strips thrown error messages in
server actions). Changed to return { success, error } and updated the
client dialog to check the result instead of using try/catch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 18:21:48 +01:00
fuomag9 7760f2d2c8 normalise stale DetectionOnly engine mode on WafFields init
Old DB records may still have mode='DetectionOnly'. The previous
value?.mode ?? 'inherit' would pass that string into state, leaving no
engine mode button selected. Explicitly accept only 'Off'/'On'; anything
else (including legacy DetectionOnly) falls back to 'inherit'.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 17:33:39 +01:00
fuomag9 9bfa86f2fc remove dead DetectionOnly coercion in buildWafHandler
WafSettings.mode is now 'Off' | 'On' so the legacy DB coercion guard
triggered a TS2367 type error. DB values are already normalised upstream.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 17:31:59 +01:00
fuomag9 9834fe20c9 simplify global WAF settings: replace toggle+radio with single switch
With DetectionOnly removed, the global WAF had two redundant controls:
an Enable toggle and an Off/On radio, both doing the same thing. Collapse
them into a single labelled switch. Mode is now derived from the enabled
state in the action rather than being a separate form field.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 17:30:13 +01:00
fuomag9 b348dae4be remove DetectionOnly WAF mode
DetectionOnly was fundamentally broken in coraza-caddy (actually blocks
requests via anomaly scoring), caused massive audit log flooding, and the
threshold workaround had several issues:
- t:none is meaningless in a SecAction (no target to transform)
- SecRuleEngine directive ordering relative to SecAction is implementation-
  defined, making the override fragile
- host.mode ?? 'DetectionOnly' fallbacks silently gave any host without an
  explicit mode the broken DetectionOnly behaviour

Changes:
- Remove DetectionOnly from UI (global settings radio, per-host engine mode)
- Coerce legacy DB values of 'DetectionOnly' to 'On' in buildWafHandler
- Fix fallback defaults: host.mode ?? 'DetectionOnly' → host.mode ?? 'On'
- Fix action parsers: unknown mode defaults to 'On' (was 'DetectionOnly')
- Fix global settings defaultValue: ?? 'DetectionOnly' → ?? 'On' (or 'Off')
- Remove the fragile threshold SecAction workaround
- Update types: mode is now 'Off' | 'On' throughout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-06 17:27:08 +01:00