Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
199 commits
Select commit Hold shift + click to select a range
be2ac35
docs: add local test/dev environment design spec
alexandre433 Jun 24, 2026
67e3c47
docs: add local test/dev environment implementation plan
alexandre433 Jun 24, 2026
a5ab897
test: run Pest on SQLite + env-overridable script path for local dev
alexandre433 Jun 24, 2026
91f8d8b
build: systemd-enabled Ubuntu 24.04 image with full Laranode stack
alexandre433 Jun 24, 2026
15e0c1a
docs: revise tooling-location decision to committed local-dev/
alexandre433 Jun 24, 2026
d422671
build: clean apt lists in Dockerfile layers
alexandre433 Jun 25, 2026
0acb1aa
feat(local-dev): env config + Pebble-aware SSL manager
alexandre433 Jun 25, 2026
5d8a675
feat(local-dev): idempotent runtime provisioning entrypoint
alexandre433 Jun 25, 2026
c21230b
fix(local-dev): entrypoint sentinel guard + shell-interp admin seed
alexandre433 Jun 25, 2026
32fcf4f
feat(local-dev): compose for systemd box + opt-in Pebble sidecars
alexandre433 Jun 25, 2026
cfd8e14
feat(local-dev): Makefile task runner + verified full boot
alexandre433 Jun 25, 2026
9d99b60
fix(local-dev): single-line Makefile recipes + force .env on provision
alexandre433 Jun 25, 2026
05fbc41
fix: guard missing notify key + rename-dir test uses local adapter
alexandre433 Jun 25, 2026
7012a3c
fix(test): mock Process in AccountsTest to avoid sudo dependency
alexandre433 Jun 25, 2026
be679ec
docs: document local-dev Docker workflow + Windows PowerShell require…
alexandre433 Jun 25, 2026
5d4d182
fix(local-dev): static IP for laranode + disable MSYS path conversion
alexandre433 Jun 25, 2026
0ccff51
docs: complete the Windows/MSYS warning in CLAUDE.md
alexandre433 Jun 25, 2026
069f883
fix(local-dev): address final-review findings
alexandre433 Jun 25, 2026
a28ec8f
Merge local-dev-env: local Docker systemd test environment
alexandre433 Jun 25, 2026
386c935
docs: agreed Laranode feature roadmap (multi-cycle)
alexandre433 Jun 25, 2026
c0c1509
docs: design spec for platform-async-progress (sub-project #1)
alexandre433 Jun 25, 2026
ea766f1
docs(roadmap): add notifications system + user-facing analytics
alexandre433 Jun 25, 2026
9136dc7
docs: implementation plan for platform-async-progress
alexandre433 Jun 25, 2026
1f4dbbb
docs(spec): add front-end testing (Vitest+RTL component/hook + Playwr…
alexandre433 Jun 25, 2026
eb14110
docs(plan): add front-end test harnesses (Vitest+Playwright) + Vitest…
alexandre433 Jun 25, 2026
febb867
test(frontend): add Vitest + React Testing Library harness
alexandre433 Jun 25, 2026
9ebe6d9
test(e2e): add Playwright harness + login/dashboard/websites smokes
alexandre433 Jun 25, 2026
621f847
test(e2e): assert a concrete websites-page element (not body text)
alexandre433 Jun 25, 2026
da41f46
feat(operations): operations table + model (scopeMine, prunable)
alexandre433 Jun 25, 2026
8d1c610
feat(operations): OperationUpdated event + channel auth + lifecycle m…
alexandre433 Jun 25, 2026
af5a2ac
test(operations): assert broadcast dispatch + payload in lifecycle tests
alexandre433 Jun 25, 2026
1bceaf0
feat(operations): abstract OperationJob base (run/emit + lifecycle + …
alexandre433 Jun 25, 2026
f7434e5
feat(ssl): run SSL generate as an async operation with live output
alexandre433 Jun 25, 2026
d1262e8
fix(ssl): don't swallow dispatch errors; test job failure directly
alexandre433 Jun 25, 2026
3bfb466
feat(operations): admin operations audit page
alexandre433 Jun 25, 2026
d736820
feat(operations): paginate navigation on the audit page
alexandre433 Jun 25, 2026
6437f08
feat(platform): scheduler hook + daily operations prune
alexandre433 Jun 25, 2026
1315a83
feat(ui): live operation progress (hook + component) + SSL toggle str…
alexandre433 Jun 25, 2026
dd646cb
fix(ui): onDone via useEffect, assert it, scoped channel cleanup, ena…
alexandre433 Jun 25, 2026
ff018e9
fix: final-review items (onDone fire-once, operations nav link, strip…
alexandre433 Jun 25, 2026
73444c9
test(operations): assert schedule via schedule:list (fix full-suite i…
alexandre433 Jun 25, 2026
4f95925
chore: gitignore Playwright output dirs (test-results, playwright-rep…
alexandre433 Jun 25, 2026
5d4e743
Merge feature/platform-async-progress: async operations + live progre…
alexandre433 Jun 26, 2026
82d5ce2
docs(roadmap): promote 6 deferred features to Phase 6 + capture full …
alexandre433 Jun 26, 2026
2129af5
docs(roadmap): reconcile dev-branch features + renumber
alexandre433 Jun 26, 2026
352cb14
Merge branch 'main' into development
alexandre433 Jun 26, 2026
9a99d12
docs(plans): draft design specs + implementation plans for 5-feature …
alexandre433 Jun 26, 2026
707e1cc
docs(review): adversarial review findings for the 5-feature tranche
alexandre433 Jun 26, 2026
87a7a4d
docs(plans): revise tranche per review - fixes + defaults + trim
alexandre433 Jun 26, 2026
86c5a1d
feat(db): add engine column to databases table + backfill
alexandre433 Jun 26, 2026
a22d1b9
feat(config): add db_engines map + mysql_admin/mariadb_admin/pgsql_ad…
alexandre433 Jun 26, 2026
59ee19e
feat(databases): add DatabaseEngineDriver contract + DTOs (DatabaseSp…
alexandre433 Jun 26, 2026
389f1a6
feat(database): add MysqlDriver with SQL injection protections
alexandre433 Jun 26, 2026
d923ae8
feat(db): add MariaDbDriver and PostgresDriver (Task 5)
alexandre433 Jun 26, 2026
f35cba9
test(task5): fix PostgresDriverTest — assert stdin and verify ordering
alexandre433 Jun 26, 2026
c830cc4
feat(databases): EngineManager + DatabaseServiceProvider (Task 6)
alexandre433 Jun 26, 2026
dc0b25d
feat(database): Task 7 — Create/Update/Delete/GetStats services + Dat…
alexandre433 Jun 26, 2026
5251ea3
fix(task7): add missing GetDatabasesWithStatsException, remove double…
alexandre433 Jun 26, 2026
61811fd
feat(db): DatabasesController + engine-aware FormRequests + databases…
alexandre433 Jun 26, 2026
368460c
test(routes): add RouteTest for databases.* and mysql.* route registr…
alexandre433 Jun 26, 2026
3efa58d
feat(ui): Databases pages with engine-aware create/edit forms
alexandre433 Jun 26, 2026
620883a
feat(task11): Postgres system infrastructure + integration tests
alexandre433 Jun 26, 2026
00184d6
fix(task-11): resolve all blocking reviewer issues in postgres infras…
alexandre433 Jun 26, 2026
cba6026
refactor: remove old MySQL namespaces + shim MysqlController + update…
alexandre433 Jun 26, 2026
d8c49b9
fix(security): validate all interpolated identifiers in db drivers + …
alexandre433 Jun 26, 2026
4376de8
fix(review): wire postgres encoding/locale + isolate stats cache in t…
alexandre433 Jun 26, 2026
f3a5c06
Merge feature/db-relational-engines into development
alexandre433 Jun 26, 2026
2344b3a
feat(backups): add flysystem-aws-s3-v3 + backups disk in filesystems …
alexandre433 Jun 26, 2026
5de4517
feat(backups): Task 1 — migrations, Backup/ScheduledBackup models, fa…
alexandre433 Jun 26, 2026
616e693
feat(backups): BackupEngineDriver interface + MysqlBackupDriver + Bac…
alexandre433 Jun 26, 2026
88d250b
fix(tests): Pint brace_position violation in BackupEngineTest
alexandre433 Jun 26, 2026
9a4cc38
feat(backups): task 3 — DumpDatabaseAction, TarFilesAction, UploadToS…
alexandre433 Jun 26, 2026
bebc720
fix(backups): eliminate TOCTOU race in DumpDatabaseAction + fix pint …
alexandre433 Jun 26, 2026
b41402c
feat(backups): Task 4 — backup/restore bash scripts + explicit sudoer…
alexandre433 Jun 26, 2026
c65fe2b
fix(backups): Task 4 review fixes — script modes, visudo validation, …
alexandre433 Jun 26, 2026
5a8098a
feat(backups): BackupJob + BackupService with Pest tests (Task 5)
alexandre433 Jun 26, 2026
8f535fc
fix(backups): S3 disk re-registration, temp leak, missing tests
alexandre433 Jun 26, 2026
093683e
feat(backups): add RestoreJob + RestoreService with validation and tests
alexandre433 Jun 26, 2026
4b2876e
test(backups): add S3 disk re-registration test for RestoreJob
alexandre433 Jun 26, 2026
e2976d3
feat(backups): Task 7 — BackupController, policies, FormRequests, rou…
alexandre433 Jun 26, 2026
850c33a
fix(backups): Task 7 review — fix scopeMine static call + add missing…
alexandre433 Jun 26, 2026
0f59251
feat(backups): Task 8 — RunScheduledBackupsJob, RetainBackupsJob, sch…
alexandre433 Jun 26, 2026
2c210b9
fix(scheduler): dispatch RetainBackupsJob + stamp last_run_at after d…
alexandre433 Jun 26, 2026
1b97a5f
feat(backups): React UI — Backups/Index.jsx + Vitest tests (Task 9)
alexandre433 Jun 26, 2026
2bf08c9
test(system): fix BackupSystemTest to exercise MysqlBackupDriver and …
alexandre433 Jun 26, 2026
0e4294d
fix(backups): harden privileged scripts + null test broadcaster + gua…
alexandre433 Jun 26, 2026
ebd14ab
fix(backups): hide S3 secrets from props + register S3 disk for reten…
alexandre433 Jun 26, 2026
0508176
Merge feature/backups into development
alexandre433 Jun 26, 2026
b1157ab
feat(cron): cron_jobs migration + CronJob model
alexandre433 Jun 26, 2026
66b8f74
feat(cron): ValidCronExpression + AllowedCronCommand rules (php-only v1)
alexandre433 Jun 26, 2026
f745ad3
fix(cron): reject path-traversal (..) in AllowedCronCommand + add tests
alexandre433 Jun 26, 2026
b2a8e2e
feat(cron): StoreCronJobRequest + CronJobPolicy
alexandre433 Jun 26, 2026
e4b51ae
fix(cron): test 50-job cap through real withValidator() pipeline
alexandre433 Jun 27, 2026
e87be49
feat(cron): laranode-cron.sh + sudoers drop-in + Create/DeleteCronJob…
alexandre433 Jun 27, 2026
909e55f
fix(cron): bash allowlist, trap cleanup, TOCTOU fix, guard tests
alexandre433 Jun 27, 2026
138ed57
feat(cron): CronJobsController (index/store/destroy/toggleActive) + r…
alexandre433 Jun 27, 2026
5347487
fix(cron): Task 5 reviewer findings — markFinished outside tx, guard …
alexandre433 Jun 27, 2026
98b5d75
feat(cron): CronJobs UI (Index + CreateCronJobForm) + nav link + Vitest
alexandre433 Jun 27, 2026
f2afdd1
fix(cron): wire onError to surface server validation errors in Create…
alexandre433 Jun 27, 2026
70c66b3
test(cron): system test for real crontab create/delete (LARANODE_SYST…
alexandre433 Jun 27, 2026
f61a953
fix(cron): install laranode-cron sudoers drop-in in entrypoint-setup.…
alexandre433 Jun 27, 2026
a4b20a8
fix(cron): harden AllowedCronCommand against control-char + crontab i…
alexandre433 Jun 27, 2026
4637e8f
fix(cron): final-review — catch unexpected exceptions + duplicate gua…
alexandre433 Jun 27, 2026
4e5efac
Merge feature/cron-tasks into development
alexandre433 Jun 27, 2026
51290d7
feat(local-dev): multi-instance parallel lab harness
alexandre433 Jun 27, 2026
e666fdf
feat(notifications): Task 1 - notifications table, webhook_url on use…
alexandre433 Jun 27, 2026
12830c2
feat(analytics): Task 1 — migrations, models, User::databases() relation
alexandre433 Jun 27, 2026
5a67869
docs(roadmap): Phase 7 (dashboard-ux-polish, db-service-control, php-…
alexandre433 Jun 27, 2026
57533dd
test(notifications): Task 1 Pest tests for webhook_url cast, hidden, …
alexandre433 Jun 27, 2026
d5d6687
test(analytics): add UserSiteStat multi-tenancy isolation test
alexandre433 Jun 27, 2026
f54f6c0
feat: notification_preferences table + NotificationPreference model
alexandre433 Jun 27, 2026
33f5703
feat(analytics): Task 2 — rollup services (UserResourceSnapshotServic…
alexandre433 Jun 27, 2026
a111b65
feat(analytics): rollup jobs (Task 3)
alexandre433 Jun 27, 2026
fa01b77
feat(analytics): scheduler registration + model:prune extension (Task 4)
alexandre433 Jun 27, 2026
db17ac8
feat(notifications): Task 3 — NotificationService, WebhookChannel, no…
alexandre433 Jun 27, 2026
9295912
fix(analytics): replace duplicate model:prune assertion with extensio…
alexandre433 Jun 27, 2026
0874e7f
feat(analytics): UserAnalyticsService + AnalyticsController + route (…
alexandre433 Jun 27, 2026
0efaba3
fix(notifications): add Notification::assertSentTo mail channel asser…
alexandre433 Jun 27, 2026
01a67a1
test(analytics): add admin multi-tenant isolation tests for siteStats…
alexandre433 Jun 27, 2026
d01bdc4
feat(analytics): implement Analytics/Index.jsx with Vitest tests (Tas…
alexandre433 Jun 27, 2026
7141724
feat(notifications): Task 4 — NotificationCreated event + Notificatio…
alexandre433 Jun 27, 2026
13ceb60
feat(nav): add Analytics link to sidebar after File Manager
alexandre433 Jun 27, 2026
e71000c
test(ui): add Vitest tests for SidebarNavi Analytics link
alexandre433 Jun 27, 2026
8ce43de
feat(notifications): OperationJob notification hook + GenerateSslOper…
alexandre433 Jun 27, 2026
4f38b6a
fix(notifications): isolate safeNotify so Reverb/queue outage cannot …
alexandre433 Jun 27, 2026
2758d6d
fix(analytics): eager-load website url for per-site chart labels
alexandre433 Jun 27, 2026
600099e
Merge feature/user-analytics into development
alexandre433 Jun 27, 2026
a27a2bf
feat(notifications): Task 6 — SSL-expiry scheduler + Inertia unread-c…
alexandre433 Jun 27, 2026
2c56a91
test(notifications): add HandleInertiaRequests unread count prop tests
alexandre433 Jun 27, 2026
80d6661
feat(notifications): Task 7 — controllers + routes
alexandre433 Jun 27, 2026
bb60bf1
test(notifications): add cross-user isolation guard for markRead
alexandre433 Jun 27, 2026
2d33ea0
feat(notifications): useNotifications hook + NotificationBell component
alexandre433 Jun 27, 2026
85c99e9
feat(notifications): Task 9 — React pages + TopNavi integration
alexandre433 Jun 27, 2026
efb0a87
test(ui): Vitest coverage for Notifications/Index and Profile/Notific…
alexandre433 Jun 27, 2026
1f5906a
style(notifications): remove unused imports in ObserverBroadcastTest …
alexandre433 Jun 27, 2026
9a073d3
Merge branch 'development' into feature/notifications
alexandre433 Jun 27, 2026
df5c481
Merge feature/notifications into development
alexandre433 Jun 27, 2026
bf3208d
style(cron): pint ValidCronExpressionRuleTest (pre-existing debt)
alexandre433 Jun 27, 2026
46c6548
docs(phase7): review+revise the 3 draft plans (dashboard-ux-polish, d…
alexandre433 Jun 27, 2026
115abd6
feat(db-service): laranode-db-service.sh + sudoers entry
alexandre433 Jun 27, 2026
0e9324b
feat(dashboard): seed liveStats from server-side initialStats prop (D1)
alexandre433 Jun 27, 2026
93dd2ca
feat(runtimes): add runtime + runtime_port columns to websites table
alexandre433 Jun 27, 2026
47a0e1b
fix(db-service): emit engine key not resolved service name in script …
alexandre433 Jun 27, 2026
be4c26d
feat(dashboard): top-processes doughnut chart above table (D2)
alexandre433 Jun 27, 2026
331cf8c
feat(runtimes): PortAllocatorService (9100-9499, first-gap allocation)
alexandre433 Jun 27, 2026
d0c3242
feat(db-service): DbServiceRequest (admin gate + engine/action allowl…
alexandre433 Jun 27, 2026
5c66256
fix(runtimes): correct race test — two sites, same DB state, second s…
alexandre433 Jun 27, 2026
6e8a7c0
feat(dashboard): multi-engine DB widget (DbEnginesLive), replace MySQ…
alexandre433 Jun 27, 2026
e613d28
feat(db-service): DbServiceStatusService + DbServiceController + routes
alexandre433 Jun 27, 2026
95672a9
feat(runtimes): SwitchRuntimeRequest (php-fpm + frankenphp only in v1)
alexandre433 Jun 27, 2026
f2861af
feat(layout): center main content with mx-auto (D4)
alexandre433 Jun 27, 2026
7805fd5
fix(db-service): emit engine key not service name; add missing tests
alexandre433 Jun 27, 2026
e8e4611
feat(sidebar): collapsible with localStorage persist + engine-agnosti…
alexandre433 Jun 27, 2026
2490d4a
feat(runtimes): privileged scripts (install/manage/unit/vhost-switch)…
alexandre433 Jun 27, 2026
4eba35f
feat(operations): dark-mode styles + max-w-7xl container (D6)
alexandre433 Jun 27, 2026
809fdbe
feat(filemanager): double-click hint banner (D10) + breadcrumb naviga…
alexandre433 Jun 27, 2026
7655126
fix(task4): correct FrankenPHP URL, add missing leading-dash guards, …
alexandre433 Jun 27, 2026
5670a66
feat(db-service): DbServiceControl component + Databases/Index admin …
alexandre433 Jun 27, 2026
9f6d521
feat(runtimes): Apache proxy (+ ACME exception) + systemd unit templa…
alexandre433 Jun 27, 2026
c106129
test(db-service): system integration test + final verification gate
alexandre433 Jun 27, 2026
a831260
feat(php): block reinstall via 409 guard + disable installed options …
alexandre433 Jun 27, 2026
eee877f
fix(db-service): make script executable + drop dead service-name vari…
alexandre433 Jun 27, 2026
fde6121
test(runtimes): add RuntimeTemplateConfigTest for Task 5 acceptance c…
alexandre433 Jun 27, 2026
b0a1ad0
Merge feature/db-service-control into development
alexandre433 Jun 27, 2026
e1f37ab
fix(accounts): block self-impersonation + admin-to-admin guard (D13)
alexandre433 Jun 27, 2026
efb6d7b
fix(layout): truly center main content (px-3 + mx-auto, drop conflict…
alexandre433 Jun 27, 2026
2918b93
Merge feature/dashboard-ux-polish into development
alexandre433 Jun 27, 2026
6b5f7b8
feat(runtimes): SwitchRuntimeService + SwitchRuntimeOperationJob + de…
alexandre433 Jun 27, 2026
41b558e
fix(runtimes): SSL guard + rollback + domain validation in SwitchRunt…
alexandre433 Jun 27, 2026
a765f62
feat(runtimes): Websites index — runtime column, select, OperationPro…
alexandre433 Jun 27, 2026
09ec827
test(runtimes): system integration tests (13) for FrankenPHP install …
alexandre433 Jun 27, 2026
1bfe4ff
test(runtimes): add missing system tests for runtime-unit.sh and cons…
alexandre433 Jun 27, 2026
b5b4b89
fix(runtimes): final-review — snake_case runtime_label key + SHA clea…
alexandre433 Jun 27, 2026
1665bcb
Merge branch 'development' into feature/php-runtimes
alexandre433 Jun 27, 2026
2184b3b
Merge feature/php-runtimes into development
alexandre433 Jun 27, 2026
fdedcd7
fix(local-dev): install cron package in entrypoint provisioning
alexandre433 Jun 27, 2026
d078753
docs(roadmap): Phase 7 complete; 9 features shipped to main
alexandre433 Jun 27, 2026
b75ad8f
chore(gitignore): ignore Playwright MCP output and dark-mode sweep sc…
alexandre433 Jun 27, 2026
b90cac0
fix(accounts): stop the edit-account modal showing both scrollbars
alexandre433 Jun 27, 2026
677dd0d
fix(accounts): prevent an admin from deleting their own account
alexandre433 Jun 27, 2026
00637fa
fix(sidebar): keep the collapse toggle reachable when collapsed
alexandre433 Jun 27, 2026
2b29316
fix(theme): apply the persisted theme before first paint
alexandre433 Jun 27, 2026
b7165f8
fix(filemanager): always show the current path and make Back single-c…
alexandre433 Jun 27, 2026
4944dad
feat(firewall): prevent lockout when enabling the firewall
alexandre433 Jun 27, 2026
abb4d3b
docs(claude): require test + build + Playwright verification after ev…
alexandre433 Jun 27, 2026
3221727
fix(dashboard): stop the Top Processes doughnut rendering as a tiny dot
alexandre433 Jun 27, 2026
47b5aee
fix(firewall): grant www-data NOPASSWD sudo for ufw
alexandre433 Jun 27, 2026
9638b79
fix(databases): stop a missing Postgres database crashing the list
alexandre433 Jun 27, 2026
163cf1a
fix(profile): make Notification Preferences readable in dark mode
alexandre433 Jun 27, 2026
c5b1628
chore(lab): grant ufw sudoers + fix admin-seed lockout on make up
alexandre433 Jun 27, 2026
7b3ca1b
feat(dashboard): CPU and RAM usage-by-process doughnuts
alexandre433 Jun 27, 2026
fdfb891
fix(lab): stop `make test` wiping the panel database
alexandre433 Jun 27, 2026
024a7e7
chore(tests): pin a test APP_KEY in phpunit.xml
alexandre433 Jun 27, 2026
1dbc9fe
feat(dashboard): optional GPU detection and usage section
alexandre433 Jun 27, 2026
91e4cab
fix(install): make the installer work on a clean machine
alexandre433 Jun 27, 2026
3f742b6
test(install): clean-room installer test on vanilla ubuntu:24.04
alexandre433 Jun 27, 2026
f657101
chore(gitignore): ignore .env.local-backup and scratch screenshots
alexandre433 Jun 27, 2026
4bc0193
docs(readme): rewrite feature list and roadmap for current build
alexandre433 Jul 1, 2026
ef7f06a
chore(docs): drop internal superpowers planning docs from public repo
alexandre433 Jul 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
12 changes: 12 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,18 @@ DB_DATABASE=laranode
DB_USERNAME=laranode
DB_PASSWORD=

MYSQL_ADMIN_HOST=127.0.0.1
MYSQL_ADMIN_PORT=3306
MYSQL_ADMIN_DB=mysql
MYSQL_ADMIN_USERNAME=
MYSQL_ADMIN_PASSWORD=

PGSQL_HOST=127.0.0.1
PGSQL_PORT=5432
PGSQL_DB=postgres
PGSQL_USERNAME=postgres
PGSQL_PASSWORD=

SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
Expand Down
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,11 @@ yarn-error.log
package-lock.json
*.DS_Store*
*.php-cs-fixer.cache*
/test-results
/playwright-report
/.playwright-mcp
/dark-*.jpeg
/graphs-dark.jpeg
/notif-dark-fixed.jpeg
/pie-fixed.jpeg
.env.local-backup
106 changes: 106 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

Laranode is a self-hosted server control panel (cPanel/Plesk alternative) built on Laravel 12 + Inertia 2 + React 18. It manages the **host machine itself** — Apache vhosts, per-site PHP-FPM pools, MySQL databases, Let's Encrypt SSL, UFW firewall, a web file manager, and live system stats. Target host is Ubuntu 24.04+; the panel is deployed at `/home/laranode_ln/panel`.

## Local dev/test (Docker)

`local-dev/` provides a single systemd-enabled Ubuntu 24.04 container ("VPS-in-a-box") with the full Laranode stack (Apache, PHP-FPM, MySQL, Reverb, queue worker). No real Linux VPS needed for integration testing.

Key targets (run from repo root):
- `make up` — build image + start container + run entrypoint provisioning
- `make verify` — check all services running + HTTP panel response
- `make test` — run Pest suite inside container
- `make test-system` — Pest with `LARANODE_SYSTEM_TESTS=1` (exercises sudo scripts)
- `make ssl-test` — bring up Pebble ACME sidecars (pebble + challtestsrv) + test SSL issuance
- `make nuke` — destroy container + all named volumes (full reset)

Admin login: `admin@laranode.test` / `password`

> **Windows:** Run `make` and `docker compose` from **PowerShell or cmd**, NOT Git Bash.
> Git Bash (MSYS) breaks docker two ways: it strips the Windows environment that
> `docker.exe` needs to locate its compose plugin, and it rewrites in-container paths
> (`/home/…`, `/opt/…`) passed to `docker exec` into `C:/msys64/…`, which breaks the
> provisioning recipes. The Makefile's `MSYS_*` exports fix the path rewriting, but the
> plugin-discovery failure remains — so use PowerShell/cmd. Plain `docker exec
> laranode-lab …` works from any shell.

## Commands

```bash
composer dev # all-in-one dev: php artisan serve + queue:listen + pail (logs) + vite, concurrently
npm run dev # vite only
npm run build # production asset build
php artisan reverb:start # websocket server — NOT started by `composer dev`; needed for live stats
./vendor/bin/pest # run tests (Pest 3)
./vendor/bin/pest --filter="text" # single test by name
php artisan test --filter=AccountsTest # alt runner, by file/test
./vendor/bin/pint # format (Laravel Pint) — run before committing PHP
php artisan migrate
php artisan laranode:create-admin # interactive admin creation (username is forced to "laranode")
```

Tests use Pest with `RefreshDatabase` (see `tests/Pest.php`); feature tests live in `tests/Feature/<Domain>/`.

## Verify every feature before calling it done

After implementing ANY feature or bug fix, run this gate before declaring it complete (and before committing):

1. **Backend tests** — `./vendor/bin/pest` (or `--filter` for the touched domain, then the full suite to catch regressions).
2. **Frontend tests** — `npx vitest run` for the touched component, then the full Vitest suite.
3. **Build assets** — `npm run build` (the lab serves `public/build/`, not Vite dev — stale assets are why a change "doesn't show"). Clear caches if a blade/route changed: `php artisan optimize:clear`.
4. **Playwright check** — drive the running panel (lab container, admin `admin@laranode.test` / `password`) to confirm the change actually works in the real app and toggle dark mode on the affected page(s). Verify against the live result, not just the diff.

Run all four in the lab container, e.g. `docker exec laranode-lab bash -lc 'cd /home/laranode_ln/panel && <cmd>'`.

**Pint scope:** only format files you changed (`./vendor/bin/pint <file> ...`). NEVER run `pint app/` or a whole-tree pint — it sweeps dozens of pre-existing unformatted files into your diff.

## Environment caveat

System-touching features (sudo scripts, `systemctl`, `/proc`, `certbot`, `ufw`) only run on a real Linux host. On Windows/macOS dev machines those `Process` calls fail — exercise that behavior on a Linux VPS, not locally. DB is MySQL in prod (`.env.example`).

## Architecture

### Request layering
Controllers are thin. The pattern is: **Controller → FormRequest (validation) → Service or Action (work)**.

- `app/Services/<Domain>/` — orchestration, usually wrapping system calls (`Websites`, `MySQL`, `Accounts`, `Dashboard`, and `Laranode` infra helpers). Convention: a single `handle()` method, and a sibling custom `*Exception` class declared in the same file (e.g. `CreateWebsiteException`).
- `app/Actions/<Domain>/` — single-purpose units (`Filemanager`, `Firewall`, `SSL`, `MySQL`). Filemanager actions receive a Flysystem `Filesystem` injected by `AppServiceProvider`, sandboxed to the authenticated user's homedir (`DISALLOW_LINKS`).

### How the panel touches the system (the core idea)
Two distinct mechanisms, both via the `Process` facade:

1. **Privileged mutations** shell out to whitelisted bash scripts:
```php
Process::run(['sudo', config('laranode.laranode_bin_path') . '/laranode-add-vhost.sh', ...$args]);
```
Scripts live in `laranode-scripts/bin/`, config templates (Apache vhost, PHP-FPM pool, systemd units) in `laranode-scripts/templates/`. The installer grants `www-data` NOPASSWD sudo for `laranode-scripts/bin/*.sh`. When adding a privileged op: add a `*.sh` script there and call it through a Service — do not run privileged commands inline.
2. **Read-only stats** call system tools directly (`top`, `free`, `df`, `systemctl`, `ps`, `certbot`, `/proc/net/dev`) via `Process::run('…')` / `Process::pipe([...])`. See `app/Services/Dashboard/SystemStatsService.php`.

### Identity & path conventions (computed, never stored)
Used throughout the codebase — accessors on the models, not DB columns (comments note casts were unreliable here):
- System user = `{username}_ln` (`User::systemUsername`)
- Home dir = `/home/{username}_ln` (`User::homedir`)
- Website root = `{homedir}/domains/{url}`; `fullDocumentRoot` = website root + `document_root` (`Website`)

### Auth & multi-tenancy
- `users.role` is `admin` | `user`. `AdminMiddleware` gates admin-only routes (accounts, firewall, PHP manager, admin dashboard, stats history).
- Non-admins are scoped to their own rows via the `scopeMine()` query scope on `Website`/`Database`.
- Admins impersonate users via `lab404/laravel-impersonate`. Shared Inertia props (`HandleInertiaRequests`): `auth.user`, `auth.isImpersonating`, `flash.{success,error}`.

### Live stats over websockets (no polling)
Reverb-based push, not polling:
1. React page subscribes to a private channel and whispers a `client-typing` event (`resources/js/Pages/Dashboard/...`).
2. Server's `MessageReceivedListener` (auto-discovered, hooks `Laravel\Reverb\Events\MessageReceived`) matches the channel and dispatches `SystemStatsEvent` / `TopStatsEvent`.
3. Those events gather fresh stats in their constructor and broadcast back on private channels `systemstats` / `topstats` — both authorized to admins only (`routes/channels.php`).

Historical stats use sysstat/`sar`: `app/Services/Dashboard/{SarHistory,CPUHistoryService,MemoryHistoryService,NetworkHistoryService}.php`, all implementing `HistoricStatsContract`.

### Frontend
Inertia + React (JSX, **not** TypeScript). Pages in `resources/js/Pages/<Domain>/`, layouts in `resources/js/Layouts/`. `route()` in JS comes from Ziggy; websockets from Echo/Reverb (`resources/js/echo.js`). Tables use `react-data-table-component`, charts use `chart.js`/`react-chartjs-2`.

### Production runtime
Apache2 (vhost per site) + per-site PHP-FPM pools + MySQL + certbot (Let's Encrypt, 90-day certs) + UFW. Two systemd services from `laranode-scripts/templates/`: `laranode-reverb.service` (websockets) and `laranode-queue-worker.service` (queue, `QUEUE_CONNECTION=database`). Full provisioning is in `laranode-scripts/bin/laranode-installer.sh`.
30 changes: 24 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,28 @@ Laranode is a simple but powerful open-source alternative to cPanel and Plesk, d

✅ **File Manager** – Built-in (from the ground up) web-based file manager for quick access.

✅ **Live System Stats** – Monitor CPU, memory, and network usage in real-time.
✅ **Live System Stats & Analytics** – Real-time CPU/memory/network monitoring plus historical usage charts and per-user quota tracking.

✅ **LAMP Stack Administration** – Manage Apache, MySQL, and PHP with ease.

✅ **User-Friendly Interface** – Clean and simple UI designed for efficiency.
✅ **PHP Manager** – Install, update, and remove PHP versions from the web UI.

✅ **Alternative PHP Runtimes** – Switch individual sites between PHP-FPM and FrankenPHP.

✅ **Multi-Engine Database Management** – Create and manage MySQL, MariaDB, and PostgreSQL databases, with per-engine service control (start/stop/restart).

✅ **Automated Backups** – Scheduled or on-demand database and file backups to local disk or S3-compatible storage, with retention and restore.

✅ **MySQL Database Management** – Create and control MySQL databases.
✅ **Cron Job Manager** – Create and manage per-user scheduled tasks from the web UI.

✅ **Notifications** – In-app notification center plus email/webhook alerts for operations, SSL expiry, and more.

✅ **Async Operations with Live Progress** – Long-running tasks (SSL issuance, runtime switches, backups) stream real-time progress instead of blocking the request.

✅ **UFW Firewall** – Manage uncomplicated firewall rules with ease directly from the web interface.

✅ **User-Friendly Interface** – Clean and simple UI designed for efficiency.

## Installation

Laranode can be installed on a FRESH VPS or dedicated server.
Expand All @@ -35,8 +47,9 @@ Laranode can be installed on a FRESH VPS or dedicated server.
- 10GB Disk Space

### Quick Install
Run on a clean Ubuntu 24.04 server as root:
```bash
curl -sSL https://raw.githubusercontent.com/crivion/laranode/refs/heads/main/laranode-scripts/bin/laranode-installer.sh | bash
curl -sSL https://raw.githubusercontent.com/alexandre433/laranode/refs/heads/main/laranode-scripts/bin/laranode-installer.sh | bash
```

## Getting Started
Expand Down Expand Up @@ -69,8 +82,13 @@ Login with the credentials provided during installation.

## Roadmap - Future Release Plans

- 🔹 PHP Manager - install, update, remove PHP versions
- 🔹 Backup Manager - backup websites, databases, and files
- 🔹 Git-Based Deployments – push-to-deploy workflow for websites
- 🔹 Fail2ban Integration – automatic intrusion prevention
- 🔹 DNS Zone Management – built-in authoritative DNS
- 🔹 One-Click App Installers – WordPress and more
- 🔹 Email Server – mailboxes with webmail
- 🔹 Teams & Granular Roles – per-resource collaborator access
- 🔹 Staging Environments – clone, sync, and promote sites

## Contributing
Laranode is open-source and welcomes contributions! Feel free to submit issues, feature requests, or pull requests.
Expand Down
36 changes: 36 additions & 0 deletions app/Actions/Backup/DumpDatabaseAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

namespace App\Actions\Backup;

use App\Backup\BackupEngineManager;
use App\Models\Database;

class DumpDatabaseAction
{
public function __construct(private BackupEngineManager $engineManager) {}

/**
* Write a temp .cnf (mode 0600), call the engine driver, return the dump path.
* The .cnf is deleted in finally regardless of success/failure.
*/
public function execute(Database $database, string $tempPath, callable $emit): string
{
$cnfPath = sys_get_temp_dir().'/laranode-db-'.uniqid().'.cnf';

// Set umask so the file is created 0600 from the start, eliminating the
// TOCTOU window between file_put_contents() and a subsequent chmod().
$prevUmask = umask(0177);
file_put_contents($cnfPath, "[client]\npassword={$database->db_password}\n");
umask($prevUmask);

try {
$driver = $this->engineManager->for($database->engine ?? 'mysql');

return $driver->dump($database->name, $database->db_user, $cnfPath, $emit);
} finally {
if (file_exists($cnfPath)) {
unlink($cnfPath);
}
}
}
}
33 changes: 33 additions & 0 deletions app/Actions/Backup/RetainBackupsAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace App\Actions\Backup;

use App\Models\Backup;
use Illuminate\Contracts\Filesystem\Filesystem;

class RetainBackupsAction
{
/**
* Prune backups beyond $retentionCount for the given user/type/target.
* Deletes the physical file from $disk then destroys the model row.
*/
public function execute(int $userId, string $type, string $target, int $retentionCount, Filesystem $disk): void
{
$backups = Backup::where('user_id', $userId)
->where('type', $type)
->where('target', $target)
->where('status', 'completed')
->orderBy('created_at', 'asc')
->get();

$excess = $backups->slice(0, max(0, $backups->count() - $retentionCount));

foreach ($excess as $backup) {
if ($backup->path) {
$disk->delete($backup->path);
}

$backup->delete();
}
}
}
37 changes: 37 additions & 0 deletions app/Actions/Backup/TarFilesAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

namespace App\Actions\Backup;

use App\Models\Website;
use Illuminate\Support\Facades\Process;
use RuntimeException;

class TarFilesAction
{
/**
* Archive the website root via laranode-backup-files.sh, return the archive path.
*/
public function execute(Website $website, string $tempPath, callable $emit): string
{
$binPath = config('laranode.laranode_bin_path');
$sysUser = $website->user->systemUsername ?? ($website->user->username.'_ln');

$emit("Archiving files for '{$website->url}'...");

$result = Process::run([
'sudo',
$binPath.'/laranode-backup-files.sh',
$website->websiteRoot,
$tempPath,
$sysUser,
]);

if ($result->exitCode() !== 0) {
throw new RuntimeException('File archive failed: '.$result->errorOutput());
}

$emit('File archive completed.');

return $tempPath;
}
}
38 changes: 38 additions & 0 deletions app/Actions/Backup/UploadToStorageAction.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace App\Actions\Backup;

use Illuminate\Contracts\Filesystem\Filesystem;
use RuntimeException;

class UploadToStorageAction
{
/**
* Stream a local temp file onto the given disk at $remotePath.
* Returns $remotePath on success.
*
* @throws RuntimeException if the file cannot be read or the upload fails.
*/
public function execute(string $localPath, string $remotePath, Filesystem $disk, callable $emit): string
{
$emit("Uploading backup to storage at '{$remotePath}'...");

$stream = fopen($localPath, 'rb');

if ($stream === false) {
throw new RuntimeException("Cannot open local file for upload: {$localPath}");
}

try {
$disk->writeStream($remotePath, $stream);
} finally {
if (is_resource($stream)) {
fclose($stream);
}
}

$emit('Upload completed.');

return $remotePath;
}
}
7 changes: 7 additions & 0 deletions app/Actions/Firewall/FirewallLockoutException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?php

namespace App\Actions\Firewall;

use RuntimeException;

class FirewallLockoutException extends RuntimeException {}
Loading