Cloudflare Free plan doesn't support custom response body in block
rules. Use plain block action which returns CF's default 403 page.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add country-based access control managed through the Settings page.
Global allow-list with per-app overrides, searchable country selector,
automatic sync to Cloudflare WAF on settings change / deploy / remove,
plus periodic 6-hour verification.
New package: internal/cloudflare/ (client, zone, waf, countries, geosync)
New API: /api/geo/* (6 endpoints) + /api/stacks/{name}/geo/override
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add network-level health probing from the controller to deployed apps.
The controller probes containers over the shared Docker network and
overrides stack state to "unhealthy" if the service isn't responding.
Three probe types: http (any response = alive), api (validates status
code and body content), tcp (port reachability). Configured per-app
via healthcheck: section in .felhom.yml. Runs every minute, per-app
interval defaults to 5 minutes.
This replaces Docker-level healthchecks for distroless images (e.g.
Vikunja) that lack shell utilities, and complements existing Docker
healthchecks for other apps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Logo handler now checks Hub-synced assets first, falling back to
embedded SVG. Added SVG favicon to layout and catchall templates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Catalog sync could fail permanently if the container was killed mid-fetch,
leaving behind .git/shallow.lock (or index.lock, HEAD.lock). Now cleaned
up automatically before each git fetch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Passwords and secrets from deploy fields (type: password/secret) are now
encrypted at rest in app.yaml using a per-node 32-byte key. Values stored
as ENC:base64(nonce+ciphertext), decrypted transparently for docker-compose
and web UI. Key included in infra backup bundle for disaster recovery.
Existing plaintext values migrated automatically on startup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix password fields showing empty after deployment: now reads value
from DeployedFieldValues (app.yaml env) instead of only .Default
- Post-deploy card: passwords are masked with reveal + copy buttons
instead of showing plaintext
- Settings page: deployed password fields show "initial password" hint
explaining the value won't update if changed in the app
- Hide Generate button on settings page for already-deployed apps
- Added EMAIL to username-detection heuristic for credential display
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Password deploy fields now use type=password (masked by default)
- Added eye toggle button to reveal/hide password and confirm fields
- Added confirmation field below each password input
- Generate button fills both password and confirmation fields
- Form validation checks password confirmation matches before deploy
- Confirmation field only shown for new deployments (not already deployed)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Instead of a generic "default creds" message, the post-deploy card now
reads actual username/password values from the deploy form fields and
displays them in a table. Filters out internal DB passwords and secret
keys, showing only user-facing credentials (admin user, admin password).
Falls back to metadata defaultCreds for apps without typed deploy fields.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Naplóviewer was showing relative times like '3586mp' (seconds ago)
which were also negative due to timezone mismatch — parseLine used
time.Parse (UTC) but log.LstdFlags outputs local time. Now:
- parseLine uses time.ParseInLocation with time.Local
- fmtTime JS shows absolute HH:MM:SS (or MM-DD HH:MM:SS for old entries)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Protected stacks like filebrowser have no .felhom.yml or app.yaml,
so the subdomain lookup found nothing. Added well-known subdomain
fallback map for programmatically managed protected stacks.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
After successful deploy, shows a rich info card instead of auto-redirecting
to the apps list. Includes direct app link, first steps from catalog metadata,
default credentials info, and link to settings page for password reveal.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Deployed-but-stopped apps were included in telemetry reports with all-zero
memory/CPU values, dragging down hub-side averages. Now isStackRunning()
filters to only running/starting/unhealthy/restarting states.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
CatchAllMiddleware was intercepting Docker healthcheck requests (Host:
localhost) and internal API calls, returning 404 instead of passing
through. Also removed certresolver from catch-all Traefik router to
avoid cert provisioning issues with HostRegexp(.+).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stopped/undeployed app subdomains now show a branded page instead of
Traefik 404. Deploy settings page gains start/stop/restart controls.
Dashboard shows "Megnyitás" button for running apps.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Previously used bare "docker compose restart" which doesn't inject
env vars or pick up template changes. Now matches StartStack behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Deploy API now returns immediately after validation + config save.
docker compose up -d runs in a background goroutine so the UI shows
progress during image pulls instead of blocking for 30-60s.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- New GET /api/debug/telemetry endpoint runs full telemetry pipeline on-demand
- GetTelemetryPreview callback added to DebugCallbacks, wired in main.go
- BuildAppTelemetryForDebug() exported wrapper in report/telemetry.go
- Debug page: new collapsible section with per-app table (memory, CPU, log errors/warnings, issues) and raw JSON viewer
- Available regardless of hub configuration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New internal/metrics/telemetry.go: MetricsStore.GetContainerTelemetry()
aggregates container memory/CPU from SQLite over the last 15 min
- New internal/metrics/logscanner.go: ScanContainerLogs() scans docker logs
for errors/warnings, deduplicates via fingerprinting (strips timestamps,
replaces 6+ digit numbers, hex strings, UUIDs)
- New internal/report/telemetry.go: buildAppTelemetrySection() assembles
per-stack AppTelemetry by aggregating container metrics and log summaries
- internal/report/types.go: added AppTelemetry field to Report struct plus
AppTelemetry type with memory/CPU/log fields and LogIssue references
- internal/report/builder.go: calls buildAppTelemetrySection() in BuildReport()
- Backward-compatible: old Hub versions silently ignore app_telemetry field
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Deploy page, pre-start check, and deploy validation now use actual
/proc/meminfo usage instead of declared mem_request sums. New
GetMemoryMB() helper for lightweight real-time memory reads. Monitoring
page gains a stacked memory distribution bar showing per-container
usage, OS overhead, and free memory.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Memory validation: stopped apps excluded from CommittedMemory()
- Pre-start memory check (409 Conflict) on stack start
- hungarian_ui metadata field in resources
- USB badge on storage cards
- Manual Tier2 triggers now push infra backup to Hub
- showAlert() replaces native alert() for copyable error text
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The filebrowser stack has no .env file — domain is baked into compose labels
by docker-setup.sh. The sync always bailed with a WARN and storage paths
were never applied to FileBrowser's config.yaml or docker-compose.yml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove "Automatikusan generálva" badge from domain field (it's not
generated, it's the customer's configured domain)
- Shrink subdomain input width (8rem) so the .domain suffix appears
directly next to it on the same line
- Suppress redundant "Az alkalmazás aldomainje" description hint for
subdomain fields (the warning hint is sufficient)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Users can now customize the subdomain for each app during deployment
instead of using a fixed value. The deploy page shows an editable text
input with the default pre-filled and the base domain as a suffix.
New "subdomain" deploy field type with DNS-safe format validation,
reserved name blocklist, and uniqueness check across deployed stacks.
Locked after deploy — changing requires Remove + Redeploy.
Backward compatible: InjectMissingFields() auto-fills SUBDOMAIN from
.felhom.yml defaults for existing deployed apps on next sync/restart.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Domain field now displays subdomain.base_domain (e.g. wiki.demo-felhom.eu)
instead of just the base domain, matching the app card display.
Applies to both pre-deploy and post-deploy settings pages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Pre-generate domain + secret field values when deploy page loads,
so user sees actual domain and masked passwords (with reveal button)
before deploying. Same values submitted as hidden inputs → saved to app.yaml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- detect_storage_paths(): also matches drives that have .felhom-infra-backup/
so sys_drive-style mounts (internal SSD partitions with DR markers) are
detected even when settings.json is gone and felhom-data/ doesn't exist.
- do_nuclear_wipe(): rmdir all empty /mnt/*/ dirs at end of nuclear wipe
to clean up leftover mount point directories (e.g. /mnt/hdd_1 when the
raw mount was already cleaned by a prior wipe run). rmdir is safe — it
refuses non-empty directories.
- print_plan(): show per-drive .felhom-infra-backup entries for nuclear level.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After unmounting /mnt/hdd_1 (bind) and /mnt/.felhom-raw/hdd_1 (raw),
the /mnt/hdd_1 directory remained as an empty directory. Now rmdir is
called on each bind target after unmounting so the mount point is fully
cleaned up. rmdir (not rm -rf) ensures we only remove truly empty dirs.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
[ -f "$f" ] && rm -f "$f" && info "..." returns exit code 1 when the
file doesn't exist, triggering set -euo pipefail. Nuclear wipe was
silently stopping after settings.json and metrics.db (the only two
state files present), never reaching the controller/full/nuclear steps.
Fix: if [ -f "$f" ]; then rm -f "$f" && info "..."; fi
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- docker-setup.sh install_filebrowser(): removed /mnt/* auto-discovery;
FileBrowser now installed with no drive volumes. Initial config.yaml
written with /srv fallback. Controller's SyncFileBrowserMounts() takes
over on first startup and manages volumes/config going forward.
- Added ./config.yaml bind mount to initial docker-compose.yml so
FileBrowser starts correctly before controller syncs.
- Fixed ((step_num++)) → step_num=$(( step_num + 1 )) to prevent
set -euo pipefail trap when var starts at 0 (same class of bug as
the found_mounts fix in the previous commit).
- scripts/README.md: step 7 updated to reflect controller-managed volumes.
- CHANGELOG.md: added entry for all scripts changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
((found_mounts++)) with found_mounts=0 evaluates the post-increment
expression to 0, which bash treats as exit code 1 under set -e,
silently killing the script on the first mount discovered.
Use arithmetic assignment instead to avoid the zero-exit trap.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hub YAML is generated by Go's yaml.v3 which uses 4-space indentation.
The yaml_get helper was matching " key:" (2 spaces) so all extractions
silently returned empty — BASE_DOMAIN stayed as homeserver.local and
CF_TUNNEL_TOKEN was never set from hub config.
Strip leading whitespace before key matching, making yaml_get
indentation-agnostic.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After docker system prune, the nuclear wipe now also removes:
- /opt/docker/felhom-controller/ (compose + .env)
- /opt/docker/traefik/ (configs + acme.json)
- /opt/docker/cloudflared/ (configs)
- /opt/docker/stacks/ (empty dir)
These were left behind previously, preventing a clean redeploy since
docker-setup.sh checks for existing installations and skips steps
if directories already exist.
Also updated print_plan to show these deletions in the dry-run output.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs prevented /mnt/sys_drive (and similar drives) from being detected:
1. controller.yaml is root-owned (permission denied from host), so data_dir
could not be read. Settings.json was never loaded, falling back to /mnt/*
scan only. Fix: also try `docker volume inspect felhom-controller_controller-data`
to locate the actual settings.json in the Docker volume.
2. Fallback /mnt/* scan only checked for felhom-data/ or appdata/, missing
drives that only have backups/ (e.g. sys_drive pre-v0.26.0). Fix: also
check for backups/ in the scan condition.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'full' was ambiguously described as 'Controller + felhom-data/' making
it sound like the controller container is removed. Clarified that 'full'
runs controller-level cleanup (app containers only) and infra containers
(felhom-controller, traefik, etc.) are preserved throughout.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>