36 Commits
Author SHA1 Message Date
2GT-RichandClaude Opus 5 6bc7480b89 ci(release): tag by version, not by comparing with the previous commit
v0.24.3-beta shipped without a release. The workflow compared the version in
HEAD against HEAD~1, so a bump pushed together with any follow-up commit —
here a docs commit — was no longer the tip and nothing looked changed.

It now asks whether a tag for the current version already exists. That does
not care how commits are batched, is safe to re-run, and makes the job
idempotent rather than dependent on push shape. The paths filter is dropped
for the same reason: the tag check is the real guard.

Release notes are taken from the commit that last touched the version rather
than the tip, since a bump is often followed by docs or CI commits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 22:01:40 -07:00
2GT-RichandClaude Opus 5 7b994ad935 feat(sites): one install, many networks (v0.24.0-beta)
Closes #130. Multi-site, not multi-tenancy: a site is a named collection of
devices whose clients, events, topology and dashboards are viewed separately.
Sites separate what you see, not who you are — the admin/manage/read-only
roles are unchanged and every user sees every site. Per-site permissions
would have multiplied the work for a need nobody stated.

Two measurements decided the design.

Every device-derived table hangs off devices(id) — 25 of 50 tables, 35
foreign keys — so a single site_id column on devices scopes clients, events,
topology, backups and the rest transitively. No other table needed one.

The frontend has 162 useQuery call sites across 57 files. Threading a site
through each would be a large change that leaves a permanent trap: every
future query another chance to forget. The site is injected in the existing
axios interceptor instead, beside the auth token, so a new query is scoped by
default. Only 23 collection routes needed filtering; the 131 per-device
routes are already scoped by their device.

Existing installs are untouched. Migration creates one "Default Site", adopts
every device into it, and a single-site install never meets the concept. An
absent X-Site-Id header means unscoped, so existing API tokens and scripts
behave exactly as before.

Polling stays fleet-wide. Sites are a view over data already gathered, not a
change to how it is gathered — a device does not stop being monitored because
you are looking elsewhere. Rogue-AP detection scopes the scans but keeps the
"these are ours" BSSID set fleet-wide, or your own AP in another site reads
as an evil twin. Bulk actions do obey the selector: an operation that
silently spanned two customers is the blast radius the wave guards exist to
contain.

Four rounds of defects came out of testing the UI rather than the API, and
the first is the one worth recording. Switching sites called
queryClient.clear(), which empties the cache and reviews as correct. It
isn't: clear() destroys each query but a *mounted* observer keeps its own
reference and last result, so every rendered page went on showing the
previous site's data. The QueryClient is now keyed on the site, which
remounts the tree against an empty cache. Three tests pin the old behaviour
down so the shortcut cannot return.

The others: an unscoped fallback path behind the Connected Clients chart;
Config Health, TX retries and the activity feed reading fleet-wide; and
site_id added to the device list query but none of the other four that build
a device payload, so the Site field could not read its own value and appeared
to reject every change while the writes were landing. That last one is now
structural — DEVICE_BASE_COLUMNS is shared by all five, with a test that
fails when a device payload is hand-rolled, and which found two further
instances while being written.

Frontend tests were never run by CI despite vitest being configured and six
suites existing; they now run in both the workflow and ci-preflight.

Verified against a live four-device fleet: the migration left every figure
identical, an empty site returns zero from every read endpoint, moving a
device carries its history with it, and deleting a site with devices is
refused.

Minor bump rather than patch: sites change the shape of the product rather
than adding to it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 12:02:00 -07:00
2GT-RichandClaude Opus 5 273a0da737 fix(deploy): restore NetFlow to the published compose file, and generate it (v0.23.34-beta)
Closes #121.

The Quick Deploy file was missing NETFLOW_BIND_PORT and the 2055/udp mapping, so
the collector bound inside the container and nothing reached it. NetFlow did
nothing, silently, with an empty traffic page and no error — for everyone
following the path the README tells new users to take.

The report was accurate and understated. docker-compose.ghcr.yml had drifted for
seven weeks, and three closed issues had never reached it:

  * NETFLOW_BIND_PORT and the UDP port — as reported
  * all five POLLER_* tunables — #114, whose fix was "pass the tunables through"
  * the Redis memory ceiling and eviction policy — #116
  * TZ on the postgres service, which nobody had noticed at all

The middle one is the uncomfortable part. Four days ago #114 was closed by
forwarding poller settings that were documented but inert, with a comment saying
setting them "did precisely nothing". On Quick Deploy that remained true, because
one compose file was fixed and the other was not — including the advice given to
that reporter about which knob to turn.

So patching the four gaps would have been treating the symptom. The two files
describe the same system and differ only in where images come from; maintaining
both by hand meant remembering every change twice, and the one forgotten was
always the file no maintainer runs.

docker-compose.ghcr.yml is now generated from docker-compose.yml by
scripts/generate-ghcr-compose.mjs, which performs the two build-to-image swaps
and nothing else. Substitution is textual rather than parse-and-reserialise so
comments survive — the reasoning about Redis eviction is worth more than the two
lines it explains. The file stays committed and browsable, because the README
tells people to curl it directly.

A --check mode runs as the first preflight gate and as its own CI job, both of
which fail when the committed file does not match what the generator produces.
Verified by deleting the NetFlow line and watching the guard reject it, with the
reason and the fix in the failure message.

Reported by @mamb1329, on their first issue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 13:08:56 -07:00
2GT-RichandClaude Opus 5 33ca7cc651 chore: clear the dependency backlog, and fix a log-injection finding (v0.23.32-beta)
Two pieces of housekeeping that had gone quiet.

**A code-scanning alert nobody had looked at.** CodeQL flagged js/log-injection
in the preflight handler: a request value interpolated straight into a log line.
Log entries are newline-delimited, so a value containing a line break forges
additional entries — and an audit trail its own subject can write into is worse
than none, because it is believed.

The interesting part is that the risk had already been recognised. Two sites in
the clients routes strip carriage returns inline, ad hoc, and the third was
simply missed. That is what a shared helper is for, so there is now a `logSafe`
utility with tests, used at all three. It replaces control characters with a
visible marker rather than stripping them, so a tampered value reads as tampered
instead of being quietly tidied, and truncates absurd input so one field cannot
flood the log.

**Twelve Dependabot pull requests, the oldest six weeks old.** Nine had passing
checks, but those runs were from July against a main that has moved a long way,
so rather than merging stale branches serially the eight safe ones are applied
here as one change and verified together: bullmq, pg, helmet, @types/supertest,
tailwind-merge, vite, actions/setup-node and github/codeql-action.

Deliberately excluded, and left open:

  * bcryptjs 2.4.3 to 3.0.3 (#79) — a major on the password-hashing path.
    Dependabot groups it with routine bumps; it is not one, and it deserves
    its own verification rather than riding along in a batch.
  * typescript 5.9 to 7.0 (#81), tailwindcss 3.4 to 4.3 (#76) and
    eslint-plugin-react-hooks (#78) — all three already fail their own checks.

Verified beyond the gates: the application was rebuilt and exercised on the new
dependencies — authentication, the BullMQ-backed poller, Postgres reads and
helmet's headers all behave.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 16:17:35 -07:00
2GT-RichandClaude Opus 5 1ce57a25ec ci: stop interpolating commit text into a shell command
The release notes were passed through a step output and interpolated straight
into the gh command. Commit messages here routinely contain backticks, so the
shell evaluated them as command substitution — which failed the first release
outright, and would let a commit message run arbitrary code in CI.

Notes are written to a file and passed with --notes-file; nothing from the
commit reaches a shell as code.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 10:24:59 -07:00
2GT-RichandClaude Opus 5 2eda3d47ae ci: cut a release when the version changes (v0.23.30-beta)
The dashboard sends people to the releases page to see what changed. That page
held a single entry from five months earlier while the product shipped several
times a day, which tells a visitor the project is abandoned — raised on #119,
and the more useful half of that thread.

A release is now cut whenever the shipped version changes, tagged to match the
container image so the thing you are running and the notes you are reading carry
the same number. Notes are the commit body rather than a generated list of
subjects, because the commit messages here already explain what changed and why.
Betas are marked pre-release so nobody is pointed at one by accident.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 08:58:25 -07:00
2GT-RichandClaude Opus 5 cce1f88a45 ci(docs): point the pip cache at the docs requirements file
The default cache glob matches requirements.txt or pyproject.toml, and the
documentation toolchain is deliberately kept separate from the application's —
so setup-python found nothing and failed the job before it could build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 18:49:47 -07:00
2GT-RichandClaude Opus 5 98a72e1f7a docs: versioned documentation site, and the four pages that were missing (v0.23.28-beta)
Closes #112.

Source stays `docs/*.md`, rendered by MkDocs-Material and published to GitHub
Pages by Actions. Same repository on purpose: documentation drifts, and
co-location is the only thing that reliably prevents it — the pull request that
changes behaviour changes the description, and review sees both. A separate repo
would double the ceremony and guarantee divergence.

The markdown remains readable when browsing the repository, so the site renders
those files rather than replacing them.

**Versioned from the start**, using mike. Images are published to GHCR and this
project ships betas most days, so readers are routinely running something other
than the newest build; a single "latest" would confidently describe a release
they do not have. Versions follow the product rather than the docs, so the site
and the running image carry the same number.

**Content came first, and it had to.** The docs were eleven days and sixteen
releases stale — SSH keys, bulk commands, the data-cap SMS, poller health and the
timezone setting appeared in exactly zero files between them. Publishing that
would have given documentation missing five of the most consequential features a
polished, authoritative home, and a well-presented wrong answer is worse than an
obviously rough one. Four pages now cover them:

  - cellular.md — signal grading, carrier aggregation, the uplink anchor,
    observed bands, and the data-cap SMS including why it fires early and how to
    calibrate the margin
  - ssh-keys.md — leading with what installing a key costs, because RouterOS
    disables password SSH for that user and the database becomes load-bearing
  - commands.md — why waves exist, and why reading output matters when RouterOS
    reports failure in text rather than an exit code
  - scaling.md — headroom, why it is insufficient on its own, and every tunable

Pull requests build the site with `--strict`, which turns warnings into errors.
That is the point of running it there at all, and it earned itself immediately:
the first build failed on two links to the project README, which sits outside the
docs tree. They are absolute now, so they resolve both in the rendered site and
when reading the markdown on GitHub.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 18:42:40 -07:00
2GT-Rich 4ef9fd0e37 chore(ci): fix Security Audit — audit prod deps via audit-ci, patch dev advisories
The Security Audit job failed because new advisories dropped against existing
deps and `npm audit --audit-level=high` audits the whole tree (incl. dev/build
tooling that isn't shipped).

- Switch both audit steps to `audit-ci --high --skip-dev`, so the gate reflects
  what's actually deployed (backend ships --omit=dev; frontend ships a static
  build) while still failing on real high/critical production advisories.
- Allowlist GHSA-qwww-vcr4-c8h2 (React Router "RSC Mode CSRF") for the frontend:
  this is a client-side SPA using BrowserRouter — RSC mode is not used — and
  there is no forward-compatible fix for react-router-dom 7.x (only a downgrade).
- `npm audit fix` on both lockfiles to patch non-breaking transitive dev
  advisories (js-yaml, postcss, brace-expansion, minimatch). No direct/prod
  dependency versions changed; app runtime is unchanged.

Verified: backend 114 tests pass, frontend builds, and audit-ci passes for both
projects locally.
2026-07-29 15:47:52 -07:00
2GT-RichandClaude Opus 4.8 b3ba2b98dc chore(security): resolve CodeQL findings — unbiased voucher RNG, linear email check, rate-limit query filter (v0.17.2-beta)
Address the open CodeQL code-scanning alerts:
- guestWifi voucher codes now use rejection sampling so the character
  distribution is uniform (fixes js/biased-cryptographic-random).
- Scheduled-report recipient validation replaced the backtracking email regex
  with a linear split-based check (fixes js/polynomial-redos).
- Add .github/codeql/codeql-config.yml excluding js/missing-rate-limiting:
  rate limiting is applied globally via app.use('/api', rateLimitRedis) plus
  per-route limiters, which CodeQL can't attribute to individual handlers, so
  that rule fired ~38 false positives.

Remaining alerts are dismissed as accepted/false-positive (RouterOS MD5 API
login and self-signed cert acceptance are protocol-mandated; log sites already
strip CRLF).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 09:56:18 -07:00
2GT-RichandClaude Opus 4.8 3fe8341b71 chore(deps): batch Dependabot bumps + ES2022 tsconfig (v0.17.1-beta)
Apply the open Dependabot updates as one validated batch (build + test + lint +
local Docker smoke test all green) rather than 11 separate merges.

Backend:
- @types/node 20.19.43 -> 26.1.1 (aligns types with the Node 26 runtime)
- typescript-eslint 8.58.2 -> 8.64.0
- eslint 10.5.0 -> 10.7.0
- eslint-plugin-security 4.0.0 -> 4.0.1
- nodemailer 9.0.1 -> 9.0.3
- tsconfig target/lib ES2020 -> ES2022 so Array.prototype.at() (devices.ts)
  type-checks under the new @types/node — this is what unblocked #72.

Frontend:
- axios 1.18.0 -> 1.18.1
- react-router-dom 7.18.0 -> 7.18.1
- lucide-react 1.21.0 -> 1.23.0
- @xyflow/react 12.10.2 -> 12.11.1
- @vitejs/plugin-react 6.0.2 -> 6.0.3

CI:
- docker/setup-qemu-action v3 -> v4

Closes #58
Closes #60
Closes #62
Closes #65
Closes #66
Closes #67
Closes #68
Closes #69
Closes #70
Closes #71
Closes #72

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 09:33:26 -07:00
2GT-Rich 8307bcd291 Updating images to match current GUI. 2026-07-06 08:24:40 -07:00
2GT-RichandClaude Fable 5 6f0fbdb74f chore(deps): dependency upgrades — react-router 7, eslint 10, TS 6, plugin-react 6; drop unused zod (v0.16.5-beta)
Applies the open dependabot PRs in one validated pass:

- react-router-dom 6.30 -> 7.18 (drop-in for our API surface; verified
  in-browser: login, sidebar routing, ?type= deep-links, legacy redirects)
- react + react-dom -> 19.2.7, axios -> 1.18.0, lucide-react -> 1.21.0
- actions/checkout v6 -> v7 across all workflows
- backend: eslint 10 + @eslint/js 10 (fix 5 new no-useless-assignment
  findings), typescript 6.0.3 (explicit types: ["node","jest"] in
  tsconfig — TS 6 no longer auto-includes @types), remove zod (unused)
- frontend: @vitejs/plugin-react 6

Not taken: ioredis 5.11.1 (#55) — bullmq pins ioredis to exactly 5.10.1
(including latest bullmq 5.79), so the bump nests a second ioredis and
breaks BullMQ connection types.

Validation: backend tsc/lint/build + 92 tests green; frontend tsc/lint/
build + 11 tests green; docker stack rebuilt and browser-verified.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 20:35:21 -07:00
2GT-RichandClaude Fable 5 662d7852fa ci(publish): build & push multi-arch images (amd64 + arm64) to GHCR
Add docker/setup-qemu-action and platforms: linux/amd64,linux/arm64 to
both the backend and nginx build-push steps so GHCR images run natively
on arm64 (ARMv8) hosts instead of amd64-only. Refs #56.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 22:39:27 -07:00
2GT-Rich 73087a6e40 Merge pull request #21 from 2GT-Media-Group-LLC/dependabot/github_actions/docker/build-push-action-7
Bump docker/build-push-action from 6 to 7
2026-05-25 16:03:21 -07:00
2GT-Rich 86f4fbc073 Merge pull request #20 from 2GT-Media-Group-LLC/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 4 to 6
2026-05-25 16:03:18 -07:00
2GT-Rich 039e18131e Merge pull request #19 from 2GT-Media-Group-LLC/dependabot/github_actions/docker/setup-buildx-action-4
Bump docker/setup-buildx-action from 3 to 4
2026-05-25 16:03:14 -07:00
2GT-Rich 9f729d93a9 Merge pull request #18 from 2GT-Media-Group-LLC/dependabot/github_actions/docker/login-action-4
Bump docker/login-action from 3 to 4
2026-05-25 16:03:11 -07:00
2GT-Rich e37295ce96 Merge pull request #17 from 2GT-Media-Group-LLC/dependabot/github_actions/docker/metadata-action-6
Bump docker/metadata-action from 5 to 6
2026-05-25 16:03:08 -07:00
2GT-Rich 603f5fe562 Updating example images. 2026-05-15 16:05:55 -07:00
dependabot[bot] 1798406b84 Bump docker/build-push-action from 6 to 7
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>
2026-04-20 12:46:18 +00:00
dependabot[bot] 2dad8345a3 Bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-20 12:46:16 +00:00
dependabot[bot] 3f589fd081 Bump docker/setup-buildx-action from 3 to 4
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>
2026-04-20 12:46:12 +00:00
dependabot[bot] a8dd47a4d0 Bump docker/login-action from 3 to 4
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>
2026-04-20 12:46:10 +00:00
dependabot[bot] 640b474326 Bump docker/metadata-action from 5 to 6
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>
2026-04-20 12:46:06 +00:00
2GT-RichandClaude Sonnet 4.6 9b303c8cbc Fix publish workflow: use 'file' instead of 'dockerfile' parameter
docker/build-push-action@v6 renamed the parameter from 'dockerfile' to
'file'. The backend succeeded by luck (default Dockerfile matched), but
nginx failed because the Dockerfile isn't in the root context directory.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 10:27:49 -07:00
2GT-RichandClaude Sonnet 4.6 25ae3854b4 Add GHCR publish workflow and pre-built image deployment
Adds a GitHub Actions workflow that builds and pushes backend and nginx
images to GitHub Container Registry on every merge to main and on
releases. Adds docker-compose.ghcr.yml for end users who want to deploy
without cloning the repo, and updates the README with Quick Deploy
instructions alongside the existing build-from-source guide.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-19 10:21:21 -07:00
2GT-Rich fe1f7a4001 Merge pull request #4 from 2GT-Media-Group-LLC/dependabot/github_actions/github/codeql-action-4
Bump github/codeql-action from 3 to 4
2026-04-19 09:46:42 -07:00
2GT-Rich 241204a40d Merge pull request #2 from 2GT-Media-Group-LLC/dependabot/github_actions/actions/setup-node-6
Bump actions/setup-node from 4 to 6
2026-04-19 09:46:31 -07:00
2GT-Rich 114c9683c3 Merge pull request #1 from 2GT-Media-Group-LLC/dependabot/github_actions/actions/checkout-6
Bump actions/checkout from 4 to 6
2026-04-19 09:46:28 -07:00
2GT-RichandClaude Sonnet 4.6 484e529b88 Remove non-existent labels from dependabot.yml
Dependabot was logging warnings on every PR because the configured
labels (frontend, backend, dependencies, etc.) don't exist as GitHub
labels in this repository. Removing them stops the noise.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 10:38:28 -07:00
dependabot[bot] ed2d757bf4 Bump github/codeql-action from 3 to 4
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-16 16:56:46 +00:00
dependabot[bot] 3fc6a6f350 Bump actions/setup-node from 4 to 6
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>
2026-04-16 16:56:42 +00:00
dependabot[bot] 24167879ab Bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-16 16:56:39 +00:00
2GT-Rich e43b80a9a2 Add security standards and CI infrastructure
- SECURITY.md with vulnerability disclosure policy and self-hosting guidance
- Dependabot config for weekly dependency updates (npm, Docker, Actions)
- ESLint v9 flat config for frontend and backend, with eslint-plugin-security
- GitHub Actions CI: lint, typecheck, build, and npm audit on every push/PR
- CodeQL workflow for automated security scanning (runs on push and weekly)
2026-04-16 09:55:53 -07:00
Rich Teslow e16e86a0cf Updated README 2026-04-13 18:24:45 -07:00