Ship async-ops foundation + 9 major features + local Docker dev harness - #13
Open
alexandre433 wants to merge 199 commits into
Open
Ship async-ops foundation + 9 major features + local Docker dev harness#13alexandre433 wants to merge 199 commits into
alexandre433 wants to merge 199 commits into
Conversation
Single systemd "VPS-in-a-box" Docker container (Ubuntu 24.04, PID 1) bind-mounted to /home/laranode_ln/panel. Faithful provisioning + live stats, SQLite-decoupled Pest, Pebble ACME for SSL, gitignored local-dev/ tooling. Approach live-verified on the WSL2 backend (cgroup2fs, systemd PID 1 running, systemctl service start confirmed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
7 TDD-style tasks: repo enablers (sqlite Pest + env bin path), systemd Dockerfile, .env.docker + Pebble SSL patch, idempotent entrypoint, compose, Makefile + full-boot integration, provisioning/SSL smoke. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
- phpunit.xml: enable sqlite :memory: so the suite runs with no external DB - config/laranode.php: LARANODE_BIN_PATH env override (prod default unchanged) - CreateFileTest: skip two host-dependent tests unless LARANODE_SYSTEM_TESTS=1 - gitignore local-dev/ tooling Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
User chose to commit local-dev/ tooling (was originally gitignored); resolves a plan contradiction (every task commits into local-dev/). App/config changes remain limited to 3 files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
Address task-2 review (image-size); PHP RUN gets its own apt-get update since the base layer now removes /var/lib/apt/lists. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
Address task-4 review: short-circuit on sentinel to avoid re-seed on re-run; seed admin from sourced shell vars not env(); mysqladmin -u root + hard-fail if mysql never starts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
Windows make mangles backslash-continued recipes; collapse to single lines. Entrypoint now force-copies .env.docker so a fresh clone provisions without hand-editing the bind-mounted .env. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
CreateAccountService: default notify to false when not posted (was 500). RenameFileTest: InMemory adapter can't move directories; use a temp LocalFilesystemAdapter and remove debug cruft. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
admin can create accounts test called real sudo script → 500. Process::fake() makes it pass in any env without weakening assertions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
…ment Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
- compose: pin laranode to 10.30.50.10 so it can't DHCP-grab pebble's static 10.30.50.2 (ssl profile) — fixes 'address already in use'. - Makefile: export MSYS_NO_PATHCONV/MSYS2_ARG_CONV_EXCL so make recipes don't rewrite in-container /home,/opt paths to C:/msys64/... when calling docker.exe (broke 'make provision' on Windows). No-op on Linux. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
Cover both MSYS failure modes (env-stripping breaks compose-plugin discovery; path rewriting breaks docker exec of in-container scripts) per task-7 review. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
- entrypoint: back up an existing .env to .env.local-backup before the force-copy (avoid silently clobbering a dev's host .env). - .env.docker: LOCAL-DEV-ONLY warning banner (insecure fixed creds). - ssl-manager (local copy): default LARANODE_ACME_SERVER to the Pebble sidecar so the panel SSL toggle (sudo, no env passed) targets Pebble not real Let's Encrypt; add re-sync header. Verified end-to-end via GenerateWebsiteSslAction → ssl_status=active. - AccountsTest: Process::assertRan so the test fails if the system-user script isn't invoked (was asserting only the DB row). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
Brings the 'VPS-in-a-box' dev container so feature work can be exercised locally. Verified: 52 passed / 2 skipped in-container.
Foundation-first (async/progress/audit), then DB-engine abstraction + Postgres/SQLite, fail2ban, git push-to-deploy + atomic releases, then backups/cron/monitoring/Adminer; Redis/Memcached + MongoDB last. Each sub-project gets its own spec->plan->build cycle. Backed by a research workflow + competitor analysis (Forge/Ploi/RunCloud/cPanel/Laravel Cloud). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
Async queued-Job convention + live Reverb progress (streamed output lines, user-scoped channels) + operations audit table + admin log page + scheduler hook. Proven by converting SSL generate to async. Roadmap Phase 0 crivion#1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
Per user: a real notification system (in-app center + email/webhook) and user-facing machine/resource analytics (today's stats are admin-only). Both build on the crivion#1 operations/events/scheduler foundation. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
7 TDD tasks: operations table+model, OperationUpdated event+channel+lifecycle, abstract OperationJob, SSL-generate async conversion, admin audit page, scheduler hook+prune, React live-progress hook/component + SSL toggle wiring. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
…ight E2E smokes) Project has no JS test harness; introduce it here so the UI is automatically tested, not just manually. Live SSL streaming stays component+backend+manual tested (E2E of it would be flaky).
… tests for Task 7 Project had no JS test harness; Task A (Vitest+RTL+jsdom) and Task B (Playwright E2E smoke) added as project infra, run before the feature tasks. Task 7 now ships deterministic component/hook tests. 9 tasks total. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E4JwS6k6MyYJVv27KRyW2d
…ecutive-dot manage.sh - Test 14: laranode-runtime-unit.sh rejects consecutive-dot domain (..evil.com) - Test 15: laranode-runtime-unit.sh rejects path-traversal domain (evil.com/../../etc) - Test 16: laranode-runtime-manage.sh rejects consecutive-dot unit name (foo..bar.service) Fixes punch-list item 4 system-test coverage gaps flagged in Task 9 review: direct rejection tests for laranode-runtime-unit.sh domain validation and consecutive-dot unit name guard in laranode-runtime-manage.sh were absent. All 16 system tests pass in laranode-lab-phpruntimes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…nup under set -e
- Website::$appends used camelCase runtimeLabel but the React page reads runtime_label,
so runtime labels showed the raw value ("frankenphp"); change to snake_case key +
add a model test asserting toArray() serializes runtime_label (not runtimeLabel).
- laranode-runtime-install.sh: the post-pipe `if [ $? -ne 0 ]` SHA check was dead under
set -euo pipefail (the failing pipe aborts first, leaking /tmp/frankenphp); use
`if ! ... | sha256sum -c -; then rm -f; exit 1; fi`.
php-runtimes (#24, Phase 7): per-site FrankenPHP runtime (v1) via Laravel-Octane-style app server behind an Apache reverse-proxy, per-site systemd unit + loopback port. Safe-defaults: php-server mode, non-SSL sites only (+ ACME proxy exception, documented renewal gap), rollback-to-old-runtime on boot failure, strict unit-name/domain/port validation across 4 privileged scripts. Built in an isolated worktree+container. Verified combined: Pest 491 passed / 0 failed, Vitest, build, real-FrankenPHP system tests. Final review safe-defaults all confirmed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fresh parallel lab containers lacked the crontab binary (the cron-tasks feature relied on it being present ad-hoc in the original lab), so CronJobSystemTest failed with "crontab: command not found" on a fresh instance. Install + enable cron during provisioning so every fresh container can run the cron system tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…reenshots These are local scratch artifacts from UI auditing, not source. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
The grid used `overflow-scroll`, which forces horizontal and vertical scrollbars even when the content fits. Switched to `overflow-y-auto` so a scrollbar appears only when the form actually overflows vertically. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
The delete button was rendered for every row, including the current user. Gate it like the impersonate link (`account.id !== auth.user.id`) and add a backend guard in AccountsController::destroy that aborts 403 on self-deletion, since the UI guard alone is bypassable via a direct API call. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
The "Menu" heading and the footer kept `md:block` while collapsed, so on desktop they consumed the narrow w-14 column and pushed the hamburger toggle past the `overflow-x-hidden` edge — leaving no visible button to re-expand the sidebar. Drop `md:block` when collapsed so both hide and the toggle stays put. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
The dark class was only applied by React in an effect after mount, so a hard reload in dark mode flashed light first (FOUC). Inline a tiny head script that reads localStorage and sets the `dark` class before the body renders. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
…lick The breadcrumb was nested inside the `goBack` block, so the current path was hidden at the home root and only appeared after entering a subfolder. Render the breadcrumb unconditionally (it already handles root), keep Back only when not at root, and switch Back from onDoubleClick to a single onClick. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
Enabling UFW with no allow rules (default deny incoming) locks the operator out of SSH and the panel entirely. Add a hard guard plus a guided safe path: - Refuse to enable unless an allow rule covers SSH (22) AND the panel/web port (80, 443, or the APP_URL port). The check reads `ufw show added`, which lists staged rules even while UFW is inactive (unlike `ufw status`). The guard lives in the controller so it also protects direct API calls. - "Safe Setup" stages SSH + HTTP + HTTPS (and the panel port) then enables, so turning the firewall on is lockout-proof. SSH is opened from anywhere by default, with an option to restrict it to the detected IP (validated). - Warn in the UI when the firewall is disabled and unprotected. Covered by pure-logic tests (FirewallSafety), controller tests (Process::fake), Vitest for the page, and a real-ufw system test gated behind LARANODE_SYSTEM_TESTS (staging only — never enables, to avoid cutting connectivity mid-test). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
…ery feature Codify the post-feature gate (Pest, Vitest, npm run build, Playwright on the live panel) and the rule to pint only changed files, never the whole tree. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
The chart used maintainAspectRatio:false in a height-only container. It mounts on the first websocket payload — when the parent height can momentarily be 0 — and chart.js sizes to that and never re-measures a height change, leaving a tiny doughnut. Derive height from width instead (maintainAspectRatio:true + aspectRatio), which is always non-zero and which chart.js does observe, and move the legend to the bottom in a width-capped, centred container. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
The firewall Actions call `sudo ufw ...` directly, but no sudoers entry covered the ufw binary, so every firewall operation failed with "a terminal is required to read the password" / "a password is required". Add a dedicated laranode-ufw drop-in (NOPASSWD + !requiretty for /usr/sbin/ufw) and install it from the installer alongside the other per-domain drop-ins. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
GetDatabasesWithStatsService sizes every Database row; for a postgres row whose underlying database no longer exists, pg_database_size(name) raised SQLSTATE 3D000 and took down the entire Databases page. Scope the size query to existing databases (SELECT ... FROM pg_database WHERE datname = ?) so a missing db yields no row and a size of 0 instead of throwing. Covered by a real-PG regression test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
The page title, the "Webhook URL" heading and the table column headers had no text-colour class, so they inherited the default dark text and were unreadable on the dark background. Add explicit gray-900/dark:gray-100 (headings) and gray-700/dark:gray-300 (column headers). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
- Copy the new laranode-ufw sudoers drop-in during provisioning so firewall operations work in the lab. - ADMIN_EMAIL/ADMIN_PASSWORD live in .env.docker (Laravel env), not the shell, so the admin was seeded with an empty email and you couldn't log in after `make up`. Default them in-shell and use updateOrCreate (not firstOrCreate) so a stale username='laranode' row gets its email/password repaired too. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
Add two doughnut charts above the stat cards: CPU broken down by process with an Idle slice, and RAM broken down by process with Free + Other-used slices. Process %CPU is per-core so it is scaled into the overall busy share for a coherent gauge; process %MEM is already a share of total RAM. The top-process feed is now subscribed once in AdminDashboard and passed to both the charts and the processes table (TopProcesses is now presentational) so the two consumers can't fight over leaving the Echo channel. Doughnuts size from width (aspectRatio) to avoid the zero-height-mount collapse. Pure share math is unit-tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
With config cached (Reverb needs it), phpunit.xml's DB_CONNECTION=sqlite override is ignored, so RefreshDatabase ran against the live MySQL panel DB and wiped it (including the admin user) on every test run. Clear cached config before testing so the sqlite override applies, then re-cache afterwards, preserving the exit code. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
HTTP feature tests need an app key to encrypt sessions/cookies. With config cleared (required so the sqlite DB override applies and tests don't run against the live MySQL DB), the key was read from the environment and could be missing, raising MissingAppKeyException. Pin a fixed test key so tests are self-contained. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
Detect a GPU once (at install / on demand) and persist the result to the Option store; if none is found it is recorded as not-detected and never re-probed on the per-poll stats path, so GPU-less servers pay no cost. Supports NVIDIA (nvidia-smi) and AMD (rocm-smi). When a GPU is present the dashboard shows a GPU section — utilisation and VRAM gauges plus util/VRAM/temp/power cards — fed by the live systemstats broadcast. A cogwheel in the dashboard header triggers a manual rescan (e.g. after adding a GPU). Adds the long-missing `options` table migration (the Option model's first persistent user). Detection/parsing covered by tests with faked nvidia-smi/rocm-smi output; the rescan endpoint is admin-gated. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
The installer referenced repo files (sudoers drop-ins via visudo) and read .env before it cloned the repo, so a fresh `curl | bash` aborted at the first `visudo -c -f <missing file>`. Reordered into clear phases: system packages → clone + composer + key:generate → repo/.env-dependent plumbing (sudoers, pg reader) → migrate/seed/build → services. Also: - install the PostgreSQL **server** (was client-only, so the pg stats-reader provisioning had no server to talk to); - apt-install curl/ca-certificates/sudo/openssl up front (not guaranteed on a bare image, but used throughout); - make the clone idempotent (skip if a checkout is already present) so the clean-room test can inject a local tree; LARANODE_REPO overrides the source; - point the clone + README one-liner at this fork; - fix the `echo -e"` typo. Verified end-to-end by the new clean-room install test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
`make up` uses a pre-provisioned image and can't catch installer ordering or packaging drift. This boots a bare jrei/systemd-ubuntu:24.04, injects the working tree, runs the REAL laranode-installer.sh end to end, and asserts every service is active, GET /login is 200, and an admin can authenticate — the independent checks that matter since the installer runs without `set -e`. Run via `make install-test` (or `bash local-dev/install-test/run.sh`). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
.env.local-backup holds the lab's secrets; the *-dark/pie-fixed jpegs are verification scratch. Keep both out of git. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DCGVMrDtcLc1TBjHaeBokt
This was referenced Jul 1, 2026
Features section still described the pre-fork MySQL-only baseline; Roadmap listed Backup Manager and PHP Manager as future work even though both already ship. Reflect actual shipped scope (multi-engine DBs, backups, cron, notifications, analytics, PHP runtimes, async live progress) and trim the roadmap to what's genuinely unbuilt.
docs/superpowers/{specs,plans} are AI-workflow design/planning
artifacts from building these features, not user-facing docs. Not
useful to an upstream maintainer who doesn't use that tooling.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Built via spec → plan → TDD, with Pest + Vitest + Playwright coverage throughout (85 Pest files / ~495 tests, 26 Vitest files).
What's new
OperationJobconvention, audit log (operationstable), live Reverb progress streaming, scheduler wired up.DatabaseEngineDriver+EngineManager), replacing hardcoded MySQL-only SQL.local-dev/) — systemd Ubuntu 24.04 container running the full stack, so sudo/systemctl/certbot/ufw features are testable without a live VPS.ubuntu:24.04host.Not included
feature/installer-preexisting-hardening(pre-existing-service install safety) is still WIP on a separate branch — will be its own PR later.P.S.: Yes this was made via Claude. I have a great background mainly in backend PHP, but for frontend i do need help with a lot of things.
Don't worry (much) this was manually tested also, can't guarantee that 100% is working, but most of it is working as intended.
Automated tests were also made to reduce the work in manual tests, but some tests should be always done manually since the AI still can't 100% "get there".