Labs can now be marked as Personal or Global when creating or editing.
Personal topologies are visible only to the owner and admins; others
cannot see, book, or edit them. Global topologies are visible to all
but editable only by the creator, admins, or legacy (migrated) labs.
- DB: idempotent ALTER TABLE adds scope + ownerId columns to labs
- API: POST sets ownerId from JWT; PUT/DELETE enforce ownership (403 for
unauthorized edits; legacy ownerId='' remains freely editable)
- Types: LabTemplate extended with scope and ownerId fields
- LabTemplates UI: sectioned list (My / Global / Others' Personal),
Personal/Global toggle in form, Lock/Globe badges on cards,
edit+delete buttons hidden for non-owners
- BookingCalendar: personal labs filtered from selects/quick booking,
optgroup grouping for Global vs Personal in topology dropdown
- Light mode: add missing bg-slate-950/50 and border-slate-800/50
overrides so the Global badge renders correctly
- Rename caddy.redirect_path to caddy.redirect across schema, server, frontend and docs
- Remove obsolete ALTER TABLE migration (fresh-install model has no migrations)
- Move uid() from server.ts to server-db.ts for shared use
- Add addLog() general helper (prepared statement, shared timestamp support) and
replace ~24 inline INSERT INTO logs calls throughout server.ts
- Caddy CRUD now takes CaddyRouteInput object instead of positional arguments;
add/update reuse getCaddyRouteById() to avoid duplicate SELECT
DEPLOY_ENV=production now marks the primary instance globally - used for
Caddy ownership, the Dev/Prod header badge, and Caddy UI gating. Removes
build-time VITE_DEPLOY_ENV/import.meta.env.DEV from the header in favour
of the runtime API response (isProduction field in /api/auth/config).
Add a redirect_path column to the caddy table and an optional 'root redirect'
field in the route form. When set, buildCaddyfile emits 'redir / <path>' so the
bare host (e.g. checkmk.domain.local/) redirects to a sub-path (e.g.
/monitoring/check_mk/) while every other path still passes through to the
backend — the safe pattern for apps like CheckMK that bake their site path into
absolute URLs. Defensive ALTER TABLE keeps existing databases working.
One Caddy serves the whole container and POST /load replaces the entire
config, so two instances pushing would clobber each other. Now only the
instance with CADDY_MANAGER=true (production) pushes, seeds routes from the
Caddyfile, and accepts route mutations (others get 403). /api/auth/config
exposes caddyManaged so the non-owner Settings UI shows the Caddy section
read-only. The installer sets the flag on the production .env only.
Every generated reverse_proxy block now emits header_up for
X-Forwarded-Proto, X-Real-IP and Host. Caddy already sets the X-Forwarded-*
family and Host by default; this makes them explicit and adds X-Real-IP
(nginx convention) for backends that expect it. The https:// transport block
is preserved alongside the headers.
When a route's upstream starts with https://, buildCaddyfile emits a
transport http { tls_insecure_skip_verify } block so Caddy connects over TLS
and accepts the self-signed certificate typical of backends like Semaphore.
Added a UI hint explaining the https:// prefix.
The static/SPA fallback (app.get('*')) was registered before the Caddy
routes, so every GET /api/caddy/* request was swallowed by the catch-all and
returned index.html instead of JSON. POST/PUT/DELETE still worked because
app.get('*') only matches GET — which is why adding routes worked but the
list was always empty. Move the static block to just before app.listen, and
add Cache-Control: no-store on /api so stale HTML can't be served via 304.
GET /api/caddy/routes now returns the route array directly instead of
{ system, custom }. Frontend state is CaddyRoute[] initialised to [],
rendered with a simple .map() and an empty-state message — mirroring how
bookings are loaded and displayed.
POST /api/caddy/routes now returns 409 if the hostname already exists,
preventing duplicate DB entries that cause Caddy's "ambiguous site definition" error.
loadCaddyRoutes uses Promise.allSettled so a failure in the status check
can no longer silently prevent the routes list from loading.
On every startup, if Caddy is already enabled but the caddy table has no
routes (e.g. after a re-deploy), importCaddyfileRoutes() is called so the
static GhostGrid entries from /etc/caddy/Caddyfile are seeded automatically.
Also ensures deploy.sh is executable via proxmox-ghostgrid.sh.
Add inline edit for custom routes (Pencil icon → inline form with all fields).
Log route add/update/delete/import to the logs table (type: system) so
operations appear in the Logbook. Fix loadCaddyRoutes() called without await
after settings save, causing a race between the success message and route list.
When Caddy is enabled for the first time (caddy routes table empty),
importCaddyfileRoutes() reads /etc/caddy/Caddyfile and seeds all
hostname/upstream blocks as custom routes — no manual entry needed after deploy.
On first startup with an empty users table, a default admin user is created
(admin@ghostgrid.local / admin) so the system is immediately usable.
Add a Database section under Settings (split into Integrations/System
tabs) showing SQLite file size, last-modified date, a proportional
table-usage bar and per-table row counts. Supports downloading a
consistent backup and importing a .db file that overwrites the entire
database, with an explicit overwrite warning and confirmation.
Backend adds GET /api/database/info, GET /api/database/backup and
POST /api/database/import; DB_FILE is now exported from server-db.
caddy_prod_domain and caddy_dev_domain are already handled by the Proxmox deploy
process. The Caddy integration is a generic TLS proxy for additional services
(Semaphore, Netbox, etc.) — the custom routes list is the sole mechanism.
Ansible trigger successes now logged as type 'booking' so they appear
in the default filter view. Removed the redundant 'All incl. System'
filter button.
- Background scheduler checks every 30s for bookings that need setup or teardown
- Per-lab Semaphore template IDs stored on the labs table
- Booking flags track which jobs have been triggered and their Semaphore job IDs
- Immediate teardown triggered when an active booking is cancelled
- Settings UI section for Semaphore API URL, token, and project ID
- Lab template form fields for setup/teardown template IDs
- BookingDetailsModal shows live Ansible job status with manual trigger buttons
Replace per-device CheckMK URL field with a global, IP-based lookup.
The sync job fetches all host configs from CheckMK once per cycle,
matches each device by IP address, and updates its status accordingly.
Devices not found in CheckMK are reset to 'unknown'.
- Add checkmk_enabled / checkmk_api_user settings; toggle in Settings
mirrors the Entra ID pattern (fields dim when disabled)
- Sync job uses self-scheduling setTimeout so interval changes apply
without a server restart; POST /api/checkmk/sync for manual triggers
- Status changes and a per-cycle summary are written to the Logbook
- Remove checkMkUrl from Device type, form, list view, and detail panel;
status badge and CheckMK panel only render when CheckMK is enabled
- Booking offline warning suppressed when CheckMK is disabled
- Topology status dot color driven purely by device.status
- Add SQLite settings table with getSetting/setSetting/getAllSettings helpers
- Implement Azure OAuth2 authorization code flow via @azure/msal-node
- Add public GET /api/auth/config endpoint for frontend activation check
- Add admin-only GET/PUT /api/settings API with masked secret fields
- CheckMK sync reads credentials from DB settings (env vars as fallback)
- New Settings.tsx: Entra ID and CheckMK configuration cards
- LoginPage: "Sign in with Microsoft" button, shown only when Azure is active
- App.tsx: OAuth callback handling (?token=/?auth_error=), Settings tab for admins