diff --git a/controller/.gitignore b/controller/.gitignore deleted file mode 100755 index 92c2483..0000000 --- a/controller/.gitignore +++ /dev/null @@ -1,32 +0,0 @@ -# Build artifacts -bin/ -*.exe -*.dll -*.so -*.dylib - -# Test artifacts -coverage.out -coverage.html -*.test - -# IDE -.idea/ -.vscode/ -*.swp -*.swo -*~ - -# OS -.DS_Store -Thumbs.db - -# Go -vendor/ - -# Docker -*.tar - -# Local config (don't commit real customer configs) -controller.yaml -restic-password diff --git a/controller/BUILDING.md b/controller/BUILDING.md deleted file mode 100755 index db3c099..0000000 --- a/controller/BUILDING.md +++ /dev/null @@ -1,226 +0,0 @@ -# Building & Publishing felhom-controller - -## Prerequisites - -- Docker installed on your build machine -- Docker Buildx plugin (for multi-arch builds — included with Docker Desktop, may need install on Linux) -- Access to Gitea at gitea.dooplex.hu - -## Step 1: Enable Gitea Container Registry - -Gitea has a built-in container registry. Check if it's enabled: - -```bash -# SSH into your k3s node or wherever Gitea runs -# Check Gitea config (app.ini) -kubectl exec -it -n deploy/gitea -- cat /data/gitea/conf/app.ini | grep -A5 '\[packages\]' -``` - -If the `[packages]` section is missing or `ENABLED=false`, add/update: - -```ini -[packages] -ENABLED = true -``` - -Then restart Gitea: - -```bash -kubectl rollout restart deploy/gitea -n -``` - -**Verify it works:** - -```bash -# Login to the registry (use your Gitea username + password or access token) -docker login gitea.dooplex.hu -# Username: admin -# Password: -``` - -If login succeeds, the registry is working. The image URL pattern is: -`gitea.dooplex.hu//:` - -## Step 2: Sync app assets before building - -The container image includes app logos and screenshots. Sync them from -the felhom.eu website repo before building: - -```bash -# If the website repo is checked out alongside this repo: -make sync-assets - -# Or specify the path explicitly: -make sync-assets WEBSITE_ASSETS_DIR=/home/admin/repos/felhom.eu/website/assets - -# Verify -ls assets/*.webp -``` - -If you skip this step, the dashboard will work but show no app logos/screenshots -(the `onerror` handler hides broken images gracefully). - -## Step 3: Build for single architecture (quick test) - -```bash -cd ~/repos/deploy-felhom-compose - -# Build for your current architecture -docker build \ - --build-arg VERSION=$(git describe --tags --always 2>/dev/null || echo "0.1.0") \ - --build-arg GIT_COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") \ - -t gitea.dooplex.hu/admin/felhom-controller:latest \ - -t gitea.dooplex.hu/admin/felhom-controller:0.1.0 \ - . - -# Push -docker push gitea.dooplex.hu/admin/felhom-controller:latest -docker push gitea.dooplex.hu/admin/felhom-controller:0.1.0 -``` - -## Step 4: Build for both architectures (production) - -You need both amd64 (N100 mini PCs) and arm64 (Raspberry Pi). Use Docker Buildx: - -```bash -# One-time: Create a buildx builder that supports multi-arch -docker buildx create --name felhom-builder --use --bootstrap - -# Verify it supports the architectures we need -docker buildx inspect felhom-builder -# Should show: linux/amd64, linux/arm64 in Platforms - -# Build + push multi-arch image in one step -docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --build-arg VERSION=0.1.0 \ - --build-arg GIT_COMMIT=$(git rev-parse --short HEAD) \ - -t gitea.dooplex.hu/admin/felhom-controller:latest \ - -t gitea.dooplex.hu/admin/felhom-controller:0.1.0 \ - --push \ - . -``` - -**Note:** `--push` is required with multi-arch builds because buildx doesn't store -multi-platform images in the local Docker cache. It pushes directly to the registry. - -### If buildx multi-arch doesn't work (missing QEMU) - -On Linux you might need QEMU for cross-compilation: - -```bash -# Install QEMU user-mode emulation -sudo apt-get install -y qemu-user-static binfmt-support - -# Register QEMU with Docker -docker run --rm --privileged multiarch/qemu-user-static --reset -p yes - -# Verify -docker buildx ls -``` - -### Alternative: Build natively on each architecture - -If you don't want to cross-compile, build on each machine: - -```bash -# On the N100 (amd64): -docker build -t gitea.dooplex.hu/admin/felhom-controller:latest-amd64 . -docker push gitea.dooplex.hu/admin/felhom-controller:latest-amd64 - -# On the Pi (arm64): -docker build -t gitea.dooplex.hu/admin/felhom-controller:latest-arm64 . -docker push gitea.dooplex.hu/admin/felhom-controller:latest-arm64 - -# Then create a manifest list to combine them: -docker manifest create gitea.dooplex.hu/admin/felhom-controller:latest \ - gitea.dooplex.hu/admin/felhom-controller:latest-amd64 \ - gitea.dooplex.hu/admin/felhom-controller:latest-arm64 -docker manifest push gitea.dooplex.hu/admin/felhom-controller:latest -``` - -## Step 5: Deploy on a customer node - -On the customer's machine, the docker-compose.yml for the controller references -the image from the registry: - -```yaml -services: - felhom-controller: - image: gitea.dooplex.hu/admin/felhom-controller:latest - # ... -``` - -Pull and start: - -```bash -# Login to registry on the customer node (one-time) -docker login gitea.dooplex.hu - -# Start the controller -cd /opt/docker/felhom-controller -docker compose pull -docker compose up -d - -# Verify -docker compose logs -f -curl -s http://localhost:8080/api/health -``` - -## Step 6: Updating the controller - -When you release a new version: - -```bash -# On your build machine: -docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --build-arg VERSION=0.2.0 \ - -t gitea.dooplex.hu/admin/felhom-controller:latest \ - -t gitea.dooplex.hu/admin/felhom-controller:0.2.0 \ - --push . - -# On the customer node (manually for now; auto-update comes in Phase 5): -cd /opt/docker/felhom-controller -docker compose pull -docker compose up -d -``` - -## Makefile shortcuts - -The Makefile has convenience targets: - -```bash -make docker-build # Build for current platform -make docker-buildx # Build multi-arch + push -make docker-push # Push current platform image -``` - -## Troubleshooting - -### "unauthorized" when pushing -```bash -docker logout gitea.dooplex.hu -docker login gitea.dooplex.hu -``` - -### Gitea registry not accessible -Check if Gitea's HTTPS is working and the domain resolves: -```bash -curl -v https://gitea.dooplex.hu/v2/ -# Should return: {"errors":[{"code":"UNAUTHORIZED",...}]} -``` - -### Build fails on arm64 via QEMU (too slow or errors) -Cross-compiling Go via QEMU can be slow. Since the Go binary itself is cross-compiled -(CGO_ENABLED=0), only the Debian packages in the runtime stage need QEMU. -Alternative: build the Go binary natively, then build only the runtime Docker layer -via buildx. - -### Image too large -Expected sizes: -- Go binary: ~15-20 MB -- Runtime image (debian-slim + docker-cli + restic + pg_dump): ~250-350 MB - -To reduce: consider Alpine instead of Debian slim, but test pg_dump/mysqldump -compatibility first. diff --git a/controller/Dockerfile b/controller/Dockerfile deleted file mode 100755 index 448b256..0000000 --- a/controller/Dockerfile +++ /dev/null @@ -1,81 +0,0 @@ -# ============================================================================= -# felhom-controller Dockerfile -# Multi-stage build: Go binary + minimal runtime -# Supports amd64 (N100 mini PCs) and arm64 (Raspberry Pi) -# ============================================================================= - -# --- Build stage --- -FROM golang:1.22-bookworm AS builder - -ARG TARGETOS=linux -ARG TARGETARCH -ARG VERSION=dev -ARG GIT_COMMIT=unknown - -WORKDIR /build - -# Cache dependencies first -COPY go.mod go.sum ./ -RUN go mod download - -# Copy source -COPY . . - -# Build static binary -RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build \ - -ldflags="-s -w \ - -X main.Version=${VERSION} \ - -X main.BuildTime=$(date -u +%Y-%m-%dT%H:%M:%SZ) \ - -X main.GitCommit=${GIT_COMMIT}" \ - -o /build/felhom-controller \ - ./cmd/controller/ - -# --- Runtime stage --- -FROM debian:bookworm-slim - -# Install runtime dependencies: -# - docker-cli: for "docker compose" commands -# - ca-certificates: for HTTPS (healthchecks pings, git) -# - restic: for backup operations -# - postgresql-client: for pg_dump -# - default-mysql-client: for mysqldump -# - sqlite3: for SQLite backup -# - git: for stack sync from Gitea -# - curl: for health pings and debugging -RUN apt-get update && apt-get install -y --no-install-recommends \ - ca-certificates \ - curl \ - git \ - restic \ - postgresql-client \ - default-mysql-client \ - sqlite3 \ - && rm -rf /var/lib/apt/lists/* - -# Install docker-cli (without daemon) -RUN curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker.gpg] \ - https://download.docker.com/linux/debian bookworm stable" > /etc/apt/sources.list.d/docker.list \ - && apt-get update \ - && apt-get install -y --no-install-recommends docker-ce-cli docker-compose-plugin \ - && rm -rf /var/lib/apt/lists/* - -# Create non-root user (but we'll run as root for Docker socket access) -# The Docker socket requires root or docker group membership -RUN mkdir -p /opt/docker/felhom-controller/data - -COPY --from=builder /build/felhom-controller /usr/local/bin/felhom-controller - -# Copy baked-in app assets (logos, screenshots) -# These are synced from the felhom.eu website repo before building. -# See: make sync-assets -COPY assets/ /usr/share/felhom/assets/ - -# Health check -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD curl -f http://localhost:8080/api/health || exit 1 - -EXPOSE 8080 - -ENTRYPOINT ["/usr/local/bin/felhom-controller"] -CMD ["--config", "/opt/docker/felhom-controller/controller.yaml"] diff --git a/controller/Makefile b/controller/Makefile deleted file mode 100755 index e9b36f1..0000000 --- a/controller/Makefile +++ /dev/null @@ -1,98 +0,0 @@ -# felhom-controller Makefile -# Build targets for amd64 (N100 mini PCs) and arm64 (Raspberry Pi) - -VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev") -GIT_COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown") -BUILD_TIME ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) - -BINARY = felhom-controller -REGISTRY ?= gitea.dooplex.hu/admin -IMAGE = $(REGISTRY)/felhom-controller - -LDFLAGS = -ldflags="-s -w \ - -X main.Version=$(VERSION) \ - -X main.BuildTime=$(BUILD_TIME) \ - -X main.GitCommit=$(GIT_COMMIT)" - -.PHONY: all build build-amd64 build-arm64 build-all test lint clean \ - docker-build docker-push docker-buildx run sync-assets - -# Default: build for current platform -all: build - -build: - go build $(LDFLAGS) -o bin/$(BINARY) ./cmd/controller/ - -build-amd64: - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build $(LDFLAGS) -o bin/$(BINARY)-linux-amd64 ./cmd/controller/ - -build-arm64: - CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build $(LDFLAGS) -o bin/$(BINARY)-linux-arm64 ./cmd/controller/ - -build-all: build-amd64 build-arm64 - -# Run locally (for development) -run: - go run ./cmd/controller/ --config configs/controller.yaml.example - -# Tests -test: - go test -v ./... - -test-cover: - go test -coverprofile=coverage.out ./... - go tool cover -html=coverage.out -o coverage.html - -# Lint (requires golangci-lint) -lint: - golangci-lint run ./... - -# Docker image (current platform) -docker-build: - docker build \ - --build-arg VERSION=$(VERSION) \ - --build-arg GIT_COMMIT=$(GIT_COMMIT) \ - -t $(IMAGE):$(VERSION) \ - -t $(IMAGE):latest \ - . - -# Docker multi-arch build (requires docker buildx) -docker-buildx: - docker buildx build \ - --platform linux/amd64,linux/arm64 \ - --build-arg VERSION=$(VERSION) \ - --build-arg GIT_COMMIT=$(GIT_COMMIT) \ - -t $(IMAGE):$(VERSION) \ - -t $(IMAGE):latest \ - --push \ - . - -# Push docker image -docker-push: - docker push $(IMAGE):$(VERSION) - docker push $(IMAGE):latest - -# Sync app assets from felhom.eu website repo -# Override with: make sync-assets WEBSITE_ASSETS_DIR=/path/to/website/assets -WEBSITE_ASSETS_DIR ?= ../felhom.eu/website/assets - -sync-assets: - @echo "Syncing assets from $(WEBSITE_ASSETS_DIR)..." - @if [ ! -d "$(WEBSITE_ASSETS_DIR)" ]; then \ - echo "ERROR: $(WEBSITE_ASSETS_DIR) not found."; \ - echo "Set WEBSITE_ASSETS_DIR to the path containing app logos and screenshots."; \ - exit 1; \ - fi - @cp -v $(WEBSITE_ASSETS_DIR)/*-logo.svg assets/ 2>/dev/null || echo "No SVG logo files found" - @cp -v $(WEBSITE_ASSETS_DIR)/*-logo.png assets/ 2>/dev/null || echo "No PNG logo files found" - @cp -v $(WEBSITE_ASSETS_DIR)/*-screenshot-*.webp assets/ 2>/dev/null || echo "No screenshot files found" - @echo "Assets synced: $$(ls assets/*.svg assets/*.png assets/*.webp 2>/dev/null | wc -l) files" - -# Generate bcrypt password hash (usage: make password PASS=mypassword) -password: - @go run -mod=mod golang.org/x/crypto/bcrypt 2>/dev/null || \ - echo '$$2a$$10$$...' && echo "Install htpasswd or use: go run scripts/hashpass.go $(PASS)" - -# Clean build artifacts -clean: - rm -rf bin/ coverage.out coverage.html diff --git a/controller/README.md b/controller/README.md deleted file mode 100755 index 968a5f3..0000000 --- a/controller/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# App Assets - -This directory contains logos and screenshots for the dashboard. -They are baked into the Docker image at build time. - -## Naming convention - -Files must follow the felhom.eu website convention: - -- `{slug}-logo.svg` — App logo (SVG preferred, displayed on dark background) -- `{slug}-logo.png` — App logo fallback (PNG, for apps without SVG) -- `{slug}-screenshot-1.webp` — First screenshot -- `{slug}-screenshot-2.webp` — Second screenshot (and so on) - -The dashboard tries SVG first, falls back to PNG if not found. - -Example: -``` -paperless-ngx-logo.svg -paperless-ngx-screenshot-1.webp -adventurelog-logo.png -adventurelog-screenshot-1.webp -``` - -## Syncing from felhom.eu website - -Run `make sync-assets` to copy assets from the felhom.eu website repo. -This expects the website files to be available at `../felhom.eu/website/assets/` -(relative to this repo), or set `WEBSITE_ASSETS_DIR` to override. - -Alternatively, copy files manually from FileBrowser at https://felhom.eu. diff --git a/controller/config.go b/controller/config.go deleted file mode 100755 index fada409..0000000 --- a/controller/config.go +++ /dev/null @@ -1,270 +0,0 @@ -package config - -import ( - "fmt" - "os" - "strings" - - "gopkg.in/yaml.v3" -) - -// Config is the top-level configuration structure. -// Contains ONLY infrastructure/customer identity. -// App-specific config lives in per-app app.yaml files. -type Config struct { - Customer CustomerConfig `yaml:"customer"` - Infrastructure InfrastructureConfig `yaml:"infrastructure"` - Paths PathsConfig `yaml:"paths"` - Web WebConfig `yaml:"web"` - Git GitConfig `yaml:"git"` - Stacks StacksConfig `yaml:"stacks"` - Backup BackupConfig `yaml:"backup"` - Monitoring MonitoringConfig `yaml:"monitoring"` - SelfUpdate SelfUpdateConfig `yaml:"self_update"` - Notifications NotificationsConfig `yaml:"notifications"` - Logging LoggingConfig `yaml:"logging"` - Assets AssetsConfig `yaml:"assets"` -} - -type CustomerConfig struct { - ID string `yaml:"id"` - Name string `yaml:"name"` - Domain string `yaml:"domain"` - Email string `yaml:"email"` - TelegramChatID string `yaml:"telegram_chat_id"` -} - -type InfrastructureConfig struct { - CFTunnelToken string `yaml:"cf_tunnel_token"` - CFAPIToken string `yaml:"cf_api_token"` -} - -type PathsConfig struct { - StacksDir string `yaml:"stacks_dir"` - DataDir string `yaml:"data_dir"` - BackupDir string `yaml:"backup_dir"` - DBDumpDir string `yaml:"db_dump_dir"` -} - -type WebConfig struct { - Listen string `yaml:"listen"` - PasswordHash string `yaml:"password_hash"` - SessionSecret string `yaml:"session_secret"` -} - -type GitConfig struct { - RepoURL string `yaml:"repo_url"` - Branch string `yaml:"branch"` - SyncInterval string `yaml:"sync_interval"` - Username string `yaml:"username"` - Token string `yaml:"token"` -} - -type StacksConfig struct { - Protected []string `yaml:"protected"` - UpdateWindow string `yaml:"update_window"` - ComposeCommand string `yaml:"compose_command"` -} - -type BackupConfig struct { - Enabled bool `yaml:"enabled"` - ResticRepo string `yaml:"restic_repo"` - ResticPasswordFile string `yaml:"restic_password_file"` - DBDumpSchedule string `yaml:"db_dump_schedule"` - ResticSchedule string `yaml:"restic_schedule"` - Retention RetentionConfig `yaml:"retention"` - PruneSchedule string `yaml:"prune_schedule"` -} - -type RetentionConfig struct { - KeepDaily int `yaml:"keep_daily"` - KeepWeekly int `yaml:"keep_weekly"` - KeepMonthly int `yaml:"keep_monthly"` -} - -type MonitoringConfig struct { - Enabled bool `yaml:"enabled"` - HealthchecksBase string `yaml:"healthchecks_base"` - PingUUIDs PingUUIDsConfig `yaml:"ping_uuids"` - HealthCheckSchedule string `yaml:"health_check_schedule"` - Thresholds ThresholdsConfig `yaml:"thresholds"` -} - -type PingUUIDsConfig struct { - DBDump string `yaml:"db_dump"` - Backup string `yaml:"backup"` - SystemHealth string `yaml:"system_health"` -} - -type ThresholdsConfig struct { - DiskWarnPercent int `yaml:"disk_warn_percent"` - DiskCritPercent int `yaml:"disk_crit_percent"` - BackupMaxAgeHours int `yaml:"backup_max_age_hours"` - CPUWarnPercent int `yaml:"cpu_warn_percent"` - MemoryWarnPercent int `yaml:"memory_warn_percent"` - TemperatureWarnCelsius int `yaml:"temperature_warn_celsius"` -} - -type SelfUpdateConfig struct { - Enabled bool `yaml:"enabled"` - CheckInterval string `yaml:"check_interval"` - Image string `yaml:"image"` - AutoUpdate bool `yaml:"auto_update"` - HealthTimeoutSeconds int `yaml:"health_timeout_seconds"` -} - -type NotificationsConfig struct { - CustomerEvents []string `yaml:"customer_events"` - OperatorEvents []string `yaml:"operator_events"` -} - -type LoggingConfig struct { - Level string `yaml:"level"` - File string `yaml:"file"` - MaxSizeMB int `yaml:"max_size_mb"` - MaxFiles int `yaml:"max_files"` -} - -type AssetsConfig struct { - SourceURL string `yaml:"source_url"` // Only used during build, not runtime -} - -// Load reads and parses the config file, applies defaults, and validates. -func Load(path string) (*Config, error) { - data, err := os.ReadFile(path) - if err != nil { - return nil, fmt.Errorf("reading config file: %w", err) - } - - // Expand environment variables in the YAML - expanded := os.ExpandEnv(string(data)) - - cfg := &Config{} - if err := yaml.Unmarshal([]byte(expanded), cfg); err != nil { - return nil, fmt.Errorf("parsing config file: %w", err) - } - - applyDefaults(cfg) - applyEnvOverrides(cfg) - - if err := validate(cfg); err != nil { - return nil, fmt.Errorf("config validation: %w", err) - } - - return cfg, nil -} - -func applyDefaults(cfg *Config) { - d := func(val *string, def string) { - if *val == "" { - *val = def - } - } - di := func(val *int, def int) { - if *val == 0 { - *val = def - } - } - - d(&cfg.Paths.StacksDir, "/opt/docker/stacks") - d(&cfg.Paths.DataDir, "/opt/docker/felhom-controller/data") - d(&cfg.Paths.BackupDir, "/srv/backups") - d(&cfg.Paths.DBDumpDir, "/srv/backups/db-dumps") - d(&cfg.Web.Listen, ":8080") - d(&cfg.Git.Branch, "main") - d(&cfg.Git.SyncInterval, "15m") - d(&cfg.Stacks.UpdateWindow, "03:00-05:00") - d(&cfg.Backup.ResticRepo, "/srv/backups/restic-repo") - d(&cfg.Backup.DBDumpSchedule, "02:30") - d(&cfg.Backup.ResticSchedule, "03:00") - d(&cfg.Backup.PruneSchedule, "weekly") - di(&cfg.Backup.Retention.KeepDaily, 7) - di(&cfg.Backup.Retention.KeepWeekly, 4) - di(&cfg.Backup.Retention.KeepMonthly, 6) - d(&cfg.Monitoring.HealthchecksBase, "https://status.felhom.eu") - d(&cfg.Monitoring.HealthCheckSchedule, "06:00") - di(&cfg.Monitoring.Thresholds.DiskWarnPercent, 80) - di(&cfg.Monitoring.Thresholds.DiskCritPercent, 90) - di(&cfg.Monitoring.Thresholds.BackupMaxAgeHours, 36) - di(&cfg.Monitoring.Thresholds.CPUWarnPercent, 90) - di(&cfg.Monitoring.Thresholds.MemoryWarnPercent, 85) - di(&cfg.Monitoring.Thresholds.TemperatureWarnCelsius, 75) - d(&cfg.SelfUpdate.CheckInterval, "6h") - di(&cfg.SelfUpdate.HealthTimeoutSeconds, 60) - d(&cfg.Logging.Level, "info") - di(&cfg.Logging.MaxSizeMB, 10) - di(&cfg.Logging.MaxFiles, 3) - d(&cfg.Assets.SourceURL, "https://felhom.eu") -} - -func applyEnvOverrides(cfg *Config) { - envStr := func(key string, target *string) { - if v := os.Getenv(key); v != "" { - *target = v - } - } - envStr("FELHOM_CUSTOMER_ID", &cfg.Customer.ID) - envStr("FELHOM_CUSTOMER_DOMAIN", &cfg.Customer.Domain) - envStr("FELHOM_WEB_LISTEN", &cfg.Web.Listen) - envStr("FELHOM_WEB_PASSWORD_HASH", &cfg.Web.PasswordHash) - envStr("FELHOM_PATHS_STACKS_DIR", &cfg.Paths.StacksDir) - envStr("FELHOM_LOGGING_LEVEL", &cfg.Logging.Level) -} - -func validate(cfg *Config) error { - var errs []string - - if cfg.Customer.ID == "" { - errs = append(errs, "customer.id is required") - } - if cfg.Customer.Domain == "" { - errs = append(errs, "customer.domain is required") - } - - switch cfg.Logging.Level { - case "debug", "info", "warn", "error": - default: - errs = append(errs, fmt.Sprintf("logging.level must be debug|info|warn|error, got %q", cfg.Logging.Level)) - } - - if cfg.Monitoring.Thresholds.DiskWarnPercent >= cfg.Monitoring.Thresholds.DiskCritPercent { - errs = append(errs, "disk_warn_percent must be less than disk_crit_percent") - } - - if len(errs) > 0 { - return fmt.Errorf("validation errors:\n - %s", strings.Join(errs, "\n - ")) - } - - return nil -} - -// IsProtectedStack checks if a stack name is in the protected list. -func (cfg *Config) IsProtectedStack(name string) bool { - for _, p := range cfg.Stacks.Protected { - if strings.EqualFold(p, name) { - return true - } - } - return false -} - -// AppLogoURL returns the primary logo URL (SVG). Use AppLogoPNGURL as fallback. -func (cfg *Config) AppLogoURL(slug string) string { - return fmt.Sprintf("/static/assets/%s-logo.svg", slug) -} - -// AppLogoPNGURL returns the PNG fallback logo URL. -func (cfg *Config) AppLogoPNGURL(slug string) string { - return fmt.Sprintf("/static/assets/%s-logo.png", slug) -} - -// AppScreenshotURL returns the local URL for an app's screenshot. -func (cfg *Config) AppScreenshotURL(slug string, index int) string { - return fmt.Sprintf("/static/assets/%s-screenshot-%d.webp", slug, index) -} - -// AppPageURL returns the URL for an app's detail page. -// This links to the local controller-hosted app detail page. -func (cfg *Config) AppPageURL(slug string) string { - return fmt.Sprintf("/apps/%s", slug) -} diff --git a/controller/controller.yaml.example b/controller/controller.yaml.example deleted file mode 100755 index 32844ec..0000000 --- a/controller/controller.yaml.example +++ /dev/null @@ -1,123 +0,0 @@ -# ============================================================================= -# Felhom Controller Configuration -# ============================================================================= -# Location: /opt/docker/felhom-controller/controller.yaml -# -# This file contains ONLY infrastructure and customer identity config. -# Application-specific configuration (passwords, paths, etc.) is handled -# interactively during first deployment via the dashboard UI and stored -# per-app in /opt/docker/stacks//app.yaml -# -# Environment variable overrides: FELHOM_
_ -# (e.g., FELHOM_CUSTOMER_DOMAIN=example.hu) -# ============================================================================= - -# --- Customer identity --- -customer: - id: "demo-felhom" # Unique customer identifier - name: "Demo Ügyfél" # Display name (shown on dashboard) - domain: "demo-felhom.eu" # Base domain for all services - email: "" # Customer notification email (optional) - telegram_chat_id: "" # Telegram notifications (optional, future) - -# --- Infrastructure secrets --- -infrastructure: - cf_tunnel_token: "" # Cloudflare Tunnel token - cf_api_token: "" # Cloudflare API token (DNS-01 challenge) - -# --- Paths (system-level only) --- -paths: - stacks_dir: "/opt/docker/stacks" # Where compose files live - data_dir: "/opt/docker/felhom-controller/data" - backup_dir: "/srv/backups" - db_dump_dir: "/srv/backups/db-dumps" - -# --- Web UI --- -web: - listen: ":8080" - # Bcrypt hash. Empty = first-visit setup prompt. - password_hash: "" - session_secret: "" # Auto-generated on first start - -# --- Git synchronization --- -git: - repo_url: "https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git" - branch: "main" - sync_interval: "15m" - username: "" - token: "" - -# --- Stack management --- -stacks: - protected: - - "traefik" - - "cloudflared" - - "felhom-controller" - update_window: "03:00-05:00" - compose_command: "" - -# --- Backup --- -backup: - enabled: true - restic_repo: "/srv/backups/restic-repo" - restic_password_file: "/opt/docker/felhom-controller/restic-password" - db_dump_schedule: "02:30" - restic_schedule: "03:00" - retention: - keep_daily: 7 - keep_weekly: 4 - keep_monthly: 6 - prune_schedule: "weekly" - -# --- Monitoring --- -monitoring: - enabled: true - healthchecks_base: "https://status.felhom.eu" - ping_uuids: - db_dump: "CHANGEME-uuid-for-db-dump" - backup: "CHANGEME-uuid-for-backup" - system_health: "CHANGEME-uuid-for-system-health" - health_check_schedule: "06:00" - thresholds: - disk_warn_percent: 80 - disk_crit_percent: 90 - backup_max_age_hours: 36 - cpu_warn_percent: 90 - memory_warn_percent: 85 - temperature_warn_celsius: 75 - -# --- Self-update --- -self_update: - enabled: true - check_interval: "6h" - image: "gitea.dooplex.hu/admin/felhom-controller" - auto_update: false - health_timeout_seconds: 60 - -# --- Notifications --- -notifications: - customer_events: - - "disk_warning" - - "backup_failed" - - "update_available" - - "security_update" - operator_events: - - "disk_critical" - - "backup_failed" - - "self_update_failed" - - "container_unhealthy" - -# --- Logging --- -logging: - level: "info" - file: "" - max_size_mb: 10 - max_files: 3 - -# --- Assets --- -assets: - # App logos, screenshots, and descriptions are baked into the container - # image at build time (from the felhom.eu website assets). - # Served locally at /static/assets/ — no external dependency. - # The source URL is only used during image build, not at runtime. - source_url: "https://felhom.eu" diff --git a/controller/deploy.go b/controller/deploy.go deleted file mode 100755 index f290948..0000000 --- a/controller/deploy.go +++ /dev/null @@ -1,301 +0,0 @@ -package stacks - -import ( - "crypto/rand" - "encoding/hex" - "fmt" - "math/big" - "os" - "path/filepath" - "strings" - "time" - - "gopkg.in/yaml.v3" -) - -// AppConfig holds the per-app deployment configuration. -// Saved as app.yaml in each stack directory after first deployment. -type AppConfig struct { - Deployed bool `yaml:"deployed" json:"deployed"` - DeployedAt string `yaml:"deployed_at" json:"deployed_at"` - Env map[string]string `yaml:"env" json:"env"` - LockedFields []string `yaml:"locked_fields" json:"locked_fields"` -} - -// DeployRequest contains the user-provided values from the deploy form. -type DeployRequest struct { - StackName string `json:"stack_name"` - Values map[string]string `json:"values"` // env_var -> user-provided value -} - -// DeployStack handles first-time deployment of an app: -// 1. Load metadata (.felhom.yml) to know what fields exist -// 2. Auto-generate secrets for secret/password fields without user values -// 3. Auto-fill domain from controller config -// 4. Merge with user-provided values -// 5. Save app.yaml -// 6. Run docker compose up -d with env vars -func (m *Manager) DeployStack(req DeployRequest) error { - stack, ok := m.GetStack(req.StackName) - if !ok { - return fmt.Errorf("stack %q not found", req.StackName) - } - - stackDir := filepath.Dir(stack.ComposePath) - meta := LoadMetadata(stackDir) - - // Check if already deployed - existing := LoadAppConfig(stackDir) - if existing != nil && existing.Deployed { - return fmt.Errorf("stack %q is already deployed; use update instead", req.StackName) - } - - // Build the full env map - env := make(map[string]string) - var lockedFields []string - - for _, field := range meta.DeployFields { - var value string - - switch field.Type { - case "domain": - // Auto-fill from controller config - value = m.cfg.Customer.Domain - - case "secret": - // Always auto-generate, user never sees these - generated, err := generateValue(field.Generate) - if err != nil { - return fmt.Errorf("generating %s: %w", field.EnvVar, err) - } - value = generated - - case "password": - // Use user value if provided, otherwise generate - if userVal, ok := req.Values[field.EnvVar]; ok && userVal != "" { - value = userVal - } else if field.Generate != "" { - generated, err := generateValue(field.Generate) - if err != nil { - return fmt.Errorf("generating %s: %w", field.EnvVar, err) - } - value = generated - } - - default: - // text, path, select, boolean — use user value or default - if userVal, ok := req.Values[field.EnvVar]; ok { - value = userVal - } else if field.Default != "" { - value = field.Default - } - } - - // Validate required fields - if field.Required && value == "" { - return fmt.Errorf("required field %q (%s) is empty", field.Label, field.EnvVar) - } - - // Validate path fields exist - if field.Type == "path" && value != "" { - if _, err := os.Stat(value); os.IsNotExist(err) { - return fmt.Errorf("path %q does not exist for field %q", value, field.Label) - } - } - - if value != "" { - env[field.EnvVar] = value - } - - if field.LockedAfterDeploy { - lockedFields = append(lockedFields, field.EnvVar) - } - } - - // Save app.yaml - appCfg := &AppConfig{ - Deployed: true, - DeployedAt: time.Now().UTC().Format(time.RFC3339), - Env: env, - LockedFields: lockedFields, - } - - if err := SaveAppConfig(stackDir, appCfg); err != nil { - return fmt.Errorf("saving app config: %w", err) - } - - m.logger.Printf("[INFO] Deploying stack %s with %d env vars", req.StackName, len(env)) - - // Run docker compose up -d - _, err := m.composeExecWithEnv(stackDir, env, "up", "-d") - if err != nil { - // Deployment failed — keep app.yaml for debugging but mark as not deployed - appCfg.Deployed = false - _ = SaveAppConfig(stackDir, appCfg) - return fmt.Errorf("docker compose up failed: %w", err) - } - - m.logger.Printf("[INFO] Stack %s deployed successfully", req.StackName) - return m.RefreshStatus() -} - -// UpdateStackConfig updates non-locked fields for a deployed stack. -func (m *Manager) UpdateStackConfig(name string, values map[string]string) error { - stack, ok := m.GetStack(name) - if !ok { - return fmt.Errorf("stack %q not found", name) - } - - stackDir := filepath.Dir(stack.ComposePath) - appCfg := LoadAppConfig(stackDir) - if appCfg == nil || !appCfg.Deployed { - return fmt.Errorf("stack %q is not deployed yet", name) - } - - // Apply changes, respecting locked fields - lockedSet := make(map[string]bool) - for _, f := range appCfg.LockedFields { - lockedSet[f] = true - } - - for key, val := range values { - if lockedSet[key] { - return fmt.Errorf("field %q is locked and cannot be changed after deployment", key) - } - appCfg.Env[key] = val - } - - if err := SaveAppConfig(stackDir, appCfg); err != nil { - return fmt.Errorf("saving updated config: %w", err) - } - - // Restart with new env - _, err := m.composeExecWithEnv(stackDir, appCfg.Env, "up", "-d") - if err != nil { - return fmt.Errorf("restarting with new config: %w", err) - } - - m.logger.Printf("[INFO] Stack %s config updated and restarted", name) - return m.RefreshStatus() -} - -// composeExecWithEnv runs a compose command with custom env vars injected. -func (m *Manager) composeExecWithEnv(dir string, env map[string]string, args ...string) (string, error) { - // Build env slice: start with os env, then add our vars - cmdEnv := os.Environ() - for k, v := range env { - cmdEnv = append(cmdEnv, fmt.Sprintf("%s=%s", k, v)) - } - // Always inject DOMAIN from controller config - cmdEnv = append(cmdEnv, fmt.Sprintf("DOMAIN=%s", m.cfg.Customer.Domain)) - - return m.composeExecCustomEnv(dir, cmdEnv, args...) -} - -// GetDeployFields returns the deployment fields for a stack (for the deploy form). -func (m *Manager) GetDeployFields(name string) (*Metadata, *AppConfig, error) { - stack, ok := m.GetStack(name) - if !ok { - return nil, nil, fmt.Errorf("stack %q not found", name) - } - - stackDir := filepath.Dir(stack.ComposePath) - meta := LoadMetadata(stackDir) - appCfg := LoadAppConfig(stackDir) - - return &meta, appCfg, nil -} - -// --- App config persistence --- - -// LoadAppConfig reads app.yaml from a stack directory. -// Returns nil if the file doesn't exist. -func LoadAppConfig(stackDir string) *AppConfig { - path := filepath.Join(stackDir, "app.yaml") - data, err := os.ReadFile(path) - if err != nil { - return nil - } - - cfg := &AppConfig{} - if err := yaml.Unmarshal(data, cfg); err != nil { - return nil - } - return cfg -} - -// SaveAppConfig writes app.yaml to a stack directory. -func SaveAppConfig(stackDir string, cfg *AppConfig) error { - data, err := yaml.Marshal(cfg) - if err != nil { - return fmt.Errorf("marshaling app config: %w", err) - } - - path := filepath.Join(stackDir, "app.yaml") - - header := "# Auto-generated by felhom-controller — do not edit locked fields manually\n" - content := header + string(data) - - if err := os.WriteFile(path, []byte(content), 0600); err != nil { - return fmt.Errorf("writing %s: %w", path, err) - } - return nil -} - -// --- Secret generation --- - -const alphanumChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - -// generateValue creates a random value based on the generator spec. -// Formats: "password:N", "hex:N", "static:VALUE" -func generateValue(spec string) (string, error) { - if spec == "" { - return "", fmt.Errorf("empty generator spec") - } - - parts := strings.SplitN(spec, ":", 2) - if len(parts) != 2 { - return "", fmt.Errorf("invalid generator spec: %q (expected type:param)", spec) - } - - genType := parts[0] - param := parts[1] - - switch genType { - case "password": - length := 0 - if _, err := fmt.Sscanf(param, "%d", &length); err != nil || length <= 0 { - return "", fmt.Errorf("invalid password length: %q", param) - } - return randomAlphanumeric(length) - - case "hex": - byteLen := 0 - if _, err := fmt.Sscanf(param, "%d", &byteLen); err != nil || byteLen <= 0 { - return "", fmt.Errorf("invalid hex length: %q", param) - } - b := make([]byte, byteLen) - if _, err := rand.Read(b); err != nil { - return "", fmt.Errorf("reading random bytes: %w", err) - } - return hex.EncodeToString(b), nil - - case "static": - return param, nil - - default: - return "", fmt.Errorf("unknown generator type: %q", genType) - } -} - -func randomAlphanumeric(length int) (string, error) { - result := make([]byte, length) - for i := range result { - n, err := rand.Int(rand.Reader, big.NewInt(int64(len(alphanumChars)))) - if err != nil { - return "", err - } - result[i] = alphanumChars[n.Int64()] - } - return string(result), nil -} diff --git a/controller/docker-compose.yml b/controller/docker-compose.yml deleted file mode 100755 index a6de6c1..0000000 --- a/controller/docker-compose.yml +++ /dev/null @@ -1,54 +0,0 @@ -# ============================================================================= -# felhom-controller Docker Compose -# This is deployed as an infrastructure component alongside Traefik/Cloudflared -# ============================================================================= - -services: - felhom-controller: - image: gitea.dooplex.hu/admin/felhom-controller:latest - container_name: felhom-controller - restart: unless-stopped - ports: - - "8080:8080" - volumes: - # Docker socket — required for compose operations - - /var/run/docker.sock:/var/run/docker.sock:ro - # Controller config - - /opt/docker/felhom-controller/controller.yaml:/opt/docker/felhom-controller/controller.yaml:ro - # Controller persistent data (sessions, state) - - controller-data:/opt/docker/felhom-controller/data - # Stack compose files (read + write for git sync) - - /opt/docker/stacks:/opt/docker/stacks - # Backup directories - - /srv/backups:/srv/backups - # Restic password file - - /opt/docker/felhom-controller/restic-password:/opt/docker/felhom-controller/restic-password:ro - # HDD mount (if available, for backup paths) - - ${HDD_PATH:-/mnt/hdd_placeholder}:${HDD_PATH:-/mnt/hdd_placeholder}:ro - environment: - - TZ=Europe/Budapest - labels: - - "traefik.enable=true" - - "traefik.http.routers.controller.rule=Host(`dashboard.${DOMAIN}`)" - - "traefik.http.routers.controller.entrypoints=websecure" - - "traefik.http.routers.controller.tls=true" - - "traefik.http.services.controller.loadbalancer.server.port=8080" - - "traefik.docker.network=traefik-public" - # Health check labels for monitoring - - "felhom.managed=true" - - "felhom.component=controller" - networks: - - traefik-public - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8080/api/health"] - interval: 30s - timeout: 5s - start_period: 10s - retries: 3 - -volumes: - controller-data: - -networks: - traefik-public: - external: true diff --git a/controller/example-felhom-metadata.yml b/controller/example-felhom-metadata.yml deleted file mode 100755 index 7f7cae6..0000000 --- a/controller/example-felhom-metadata.yml +++ /dev/null @@ -1,106 +0,0 @@ -# ============================================================================= -# .felhom.yml — App metadata for felhom-controller -# ============================================================================= -# Place alongside docker-compose.yml in each stack directory: -# /opt/docker/stacks/paperless-ngx/.felhom.yml -# -# This file defines: -# 1. Display info (name, description, icon) -# 2. Deploy fields (what the user fills in during first deployment) -# 3. Asset references (logos, screenshots loaded from felhom.eu) -# 4. Resource hints (RAM, Pi compatibility) -# ============================================================================= - -# --- Display info (shown on dashboard) --- -display_name: "Paperless-ngx" -description: "Dokumentumok digitalizálása és rendszerezése" -category: "productivity" # productivity, media, finance, security, tools -subdomain: "paperless" # -> paperless. - -# --- Asset slug --- -# Used to construct URLs for logo and screenshots from felhom.eu: -# Logo: {assets.base_url}/assets/{slug}-logo.webp -# Screenshot: {assets.base_url}/assets/{slug}-screenshot-{n}.webp -# App page: {assets.base_url}/alkalmazasok#{slug} -# Falls back to directory name if not set. -slug: "paperless-ngx" - -# --- Resource hints (displayed on deploy screen) --- -resources: - ram: "~500MB" - pi_compatible: true # Runs on Raspberry Pi 3B+ - needs_hdd: true # Needs external storage for user data - -# --- Deploy fields --- -# Shown to the user during first deployment. -# After deployment, values are saved to app.yaml in the stack directory. -# -# Field types: -# domain - Auto-filled from controller config, read-only -# secret - Auto-generated, hidden (user sees "Generated ✓") -# password - Auto-generated but shown, user can override -# path - Filesystem path (validated for existence) -# text - Free text input -# select - Dropdown with predefined options -# boolean - Toggle switch -# -# Generator types (for secret/password): -# password:N - N chars alphanumeric -# hex:N - N bytes hex-encoded -# static:VAL - Fixed value - -deploy_fields: - - env_var: DOMAIN - label: "Domain" - type: domain - description: "A szerver domain neve" - locked_after_deploy: true - - - env_var: DB_PASSWORD - label: "Adatbázis jelszó" - type: secret - generate: "password:24" - locked_after_deploy: true - - - env_var: PAPERLESS_SECRET_KEY - label: "Titkosítási kulcs" - type: secret - generate: "hex:32" - locked_after_deploy: true - - - env_var: PAPERLESS_ADMIN_USER - label: "Admin felhasználónév" - type: text - default: "admin" - locked_after_deploy: false - - - env_var: PAPERLESS_ADMIN_PASSWORD - label: "Admin jelszó" - type: password - generate: "password:16" - description: "Első bejelentkezéshez. Utána a webes felületen módosítható." - locked_after_deploy: false - - - env_var: HDD_PATH - label: "Adattárolási útvonal" - type: path - required: true - placeholder: "/mnt/hdd_1" - description: "A külső merevlemez elérési útja, ahol a dokumentumok tárolódnak" - locked_after_deploy: true - - - env_var: PAPERLESS_OCR_LANGUAGE - label: "OCR nyelv" - type: select - default: "hun+eng" - options: - - value: "hun" - label: "Magyar" - - value: "eng" - label: "Angol" - - value: "hun+eng" - label: "Magyar + Angol" - - value: "deu+eng" - label: "Német + Angol" - description: "Dokumentum felismerés nyelve" - locked_after_deploy: false diff --git a/controller/go.mod b/controller/go.mod deleted file mode 100755 index 604e27e..0000000 --- a/controller/go.mod +++ /dev/null @@ -1,8 +0,0 @@ -module gitea.dooplex.hu/admin/felhom-controller - -go 1.22 - -require ( - gopkg.in/yaml.v3 v3.0.1 - golang.org/x/crypto v0.31.0 -) diff --git a/controller/hashpass.go b/controller/hashpass.go deleted file mode 100755 index b3e812d..0000000 --- a/controller/hashpass.go +++ /dev/null @@ -1,27 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "golang.org/x/crypto/bcrypt" -) - -// Usage: go run scripts/hashpass.go -// Outputs a bcrypt hash suitable for controller.yaml password_hash field. -func main() { - if len(os.Args) < 2 { - fmt.Fprintf(os.Stderr, "Usage: %s \n", os.Args[0]) - fmt.Fprintf(os.Stderr, "Generates a bcrypt hash for the felhom-controller config.\n") - os.Exit(1) - } - - password := os.Args[1] - hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - fmt.Fprintf(os.Stderr, "Error generating hash: %v\n", err) - os.Exit(1) - } - - fmt.Println(string(hash)) -} diff --git a/controller/main.go b/controller/main.go deleted file mode 100755 index 39c90fb..0000000 --- a/controller/main.go +++ /dev/null @@ -1,137 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "log" - "net/http" - "os" - "os/signal" - "syscall" - "time" - - "gitea.dooplex.hu/admin/felhom-controller/internal/api" - "gitea.dooplex.hu/admin/felhom-controller/internal/config" - "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" - "gitea.dooplex.hu/admin/felhom-controller/internal/web" -) - -var ( - // Set at build time via ldflags - Version = "dev" - BuildTime = "unknown" - GitCommit = "unknown" -) - -func main() { - configPath := flag.String("config", "/opt/docker/felhom-controller/controller.yaml", "Path to configuration file") - showVersion := flag.Bool("version", false, "Show version and exit") - flag.Parse() - - if *showVersion { - fmt.Printf("felhom-controller %s (built %s, commit %s)\n", Version, BuildTime, GitCommit) - os.Exit(0) - } - - // --- Load configuration --- - cfg, err := config.Load(*configPath) - if err != nil { - log.Fatalf("[FATAL] Failed to load config from %s: %v", *configPath, err) - } - - logger := setupLogger(cfg) - logger.Printf("[INFO] felhom-controller %s starting (customer: %s, domain: %s)", - Version, cfg.Customer.ID, cfg.Customer.Domain) - - // --- Initialize stack manager --- - stackMgr, err := stacks.NewManager(cfg, logger) - if err != nil { - logger.Fatalf("[FATAL] Failed to initialize stack manager: %v", err) - } - - // Initial stack scan - if err := stackMgr.ScanStacks(); err != nil { - logger.Printf("[WARN] Initial stack scan failed: %v", err) - } - - // --- Initialize API router --- - apiRouter := api.NewRouter(cfg, stackMgr, logger) - - // --- Initialize web server --- - webServer := web.NewServer(cfg, stackMgr, logger, Version) - - // --- Build HTTP mux --- - mux := http.NewServeMux() - - // API routes (no auth for health endpoint, auth for everything else) - mux.HandleFunc("/api/health", apiRouter.HealthHandler) - mux.Handle("/api/", webServer.RequireAuth(http.HandlerFunc(apiRouter.ServeHTTP))) - - // Web UI routes (auth required) - mux.Handle("/", webServer.RequireAuth(http.HandlerFunc(webServer.ServeHTTP))) - - // --- Start HTTP server --- - server := &http.Server{ - Addr: cfg.Web.Listen, - Handler: mux, - ReadTimeout: 30 * time.Second, - WriteTimeout: 60 * time.Second, - IdleTimeout: 120 * time.Second, - } - - // --- Graceful shutdown --- - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - - go func() { - sig := <-sigCh - logger.Printf("[INFO] Received signal %v, shutting down...", sig) - cancel() - - shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second) - defer shutdownCancel() - - if err := server.Shutdown(shutdownCtx); err != nil { - logger.Printf("[ERROR] HTTP server shutdown error: %v", err) - } - }() - - // --- Start background tasks --- - // Periodic stack status refresh - go func() { - ticker := time.NewTicker(30 * time.Second) - defer ticker.Stop() - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - if err := stackMgr.RefreshStatus(); err != nil { - logger.Printf("[WARN] Status refresh failed: %v", err) - } - } - } - }() - - logger.Printf("[INFO] Web UI listening on %s", cfg.Web.Listen) - if err := server.ListenAndServe(); err != http.ErrServerClosed { - logger.Fatalf("[FATAL] HTTP server error: %v", err) - } - - logger.Println("[INFO] felhom-controller stopped") -} - -func setupLogger(cfg *config.Config) *log.Logger { - // For now, log to stdout. File logging will be added later. - logger := log.New(os.Stdout, "", log.LstdFlags) - - if cfg.Logging.Level == "debug" { - logger.SetFlags(log.LstdFlags | log.Lshortfile) - } - - return logger -} diff --git a/controller/manager.go b/controller/manager.go deleted file mode 100755 index eab3979..0000000 --- a/controller/manager.go +++ /dev/null @@ -1,452 +0,0 @@ -package stacks - -import ( - "bytes" - "fmt" - "log" - "os" - "os/exec" - "path/filepath" - "strings" - "sync" - "time" - - "gitea.dooplex.hu/admin/felhom-controller/internal/config" -) - -// ContainerState represents the current state of a container. -type ContainerState string - -const ( - StateRunning ContainerState = "running" - StateStopped ContainerState = "stopped" - StateRestarting ContainerState = "restarting" - StateExited ContainerState = "exited" - StatePaused ContainerState = "paused" - StateUnknown ContainerState = "unknown" - StateNotDeployed ContainerState = "not_deployed" -) - -// ContainerInfo holds status info about a single container within a stack. -type ContainerInfo struct { - Name string `json:"name"` - Image string `json:"image"` - State ContainerState `json:"state"` - Status string `json:"status"` // e.g. "Up 3 hours" -} - -// Stack represents a docker compose stack on disk. -type Stack struct { - Name string `json:"name"` - Meta Metadata `json:"meta"` - ComposePath string `json:"compose_path"` - State ContainerState `json:"state"` - Deployed bool `json:"deployed"` // Has app.yaml with deployed=true - Protected bool `json:"protected"` - Containers []ContainerInfo `json:"containers"` - AppConfig *AppConfig `json:"app_config,omitempty"` - LastUpdated time.Time `json:"last_updated"` -} - -// Manager handles all docker compose stack operations. -type Manager struct { - cfg *config.Config - logger *log.Logger - composeCmd string - stacks map[string]*Stack - mu sync.RWMutex -} - -// NewManager creates a new stack manager. -func NewManager(cfg *config.Config, logger *log.Logger) (*Manager, error) { - composeCmd := cfg.Stacks.ComposeCommand - if composeCmd == "" { - composeCmd = detectComposeCommand() - } - if composeCmd == "" { - return nil, fmt.Errorf("docker compose not found (tried 'docker compose' and 'docker-compose')") - } - - logger.Printf("[INFO] Using compose command: %s", composeCmd) - - if err := os.MkdirAll(cfg.Paths.StacksDir, 0755); err != nil { - return nil, fmt.Errorf("creating stacks directory %s: %w", cfg.Paths.StacksDir, err) - } - - return &Manager{ - cfg: cfg, - logger: logger, - composeCmd: composeCmd, - stacks: make(map[string]*Stack), - }, nil -} - -// toTitleCase capitalizes the first letter of each word. -func toTitleCase(s string) string { - words := strings.Fields(s) - for i, w := range words { - if len(w) > 0 { - words[i] = strings.ToUpper(w[:1]) + w[1:] - } - } - return strings.Join(words, " ") -} - -func detectComposeCommand() string { - if err := exec.Command("docker", "compose", "version").Run(); err == nil { - return "docker compose" - } - if _, err := exec.LookPath("docker-compose"); err == nil { - return "docker-compose" - } - return "" -} - -// ScanStacks discovers all compose stacks in the stacks directory. -func (m *Manager) ScanStacks() error { - m.mu.Lock() - defer m.mu.Unlock() - - entries, err := os.ReadDir(m.cfg.Paths.StacksDir) - if err != nil { - return fmt.Errorf("reading stacks directory: %w", err) - } - - found := make(map[string]bool) - - for _, entry := range entries { - if !entry.IsDir() { - continue - } - - name := entry.Name() - stackDir := filepath.Join(m.cfg.Paths.StacksDir, name) - composePath := filepath.Join(stackDir, "docker-compose.yml") - - if _, err := os.Stat(composePath); os.IsNotExist(err) { - composePath = filepath.Join(stackDir, "docker-compose.yaml") - if _, err := os.Stat(composePath); os.IsNotExist(err) { - continue - } - } - - found[name] = true - - meta := LoadMetadata(stackDir) - appCfg := LoadAppConfig(stackDir) - deployed := appCfg != nil && appCfg.Deployed - - if existing, ok := m.stacks[name]; ok { - existing.ComposePath = composePath - existing.Meta = meta - existing.Protected = m.cfg.IsProtectedStack(name) - existing.Deployed = deployed - existing.AppConfig = appCfg - } else { - m.stacks[name] = &Stack{ - Name: name, - Meta: meta, - ComposePath: composePath, - State: StateNotDeployed, - Deployed: deployed, - Protected: m.cfg.IsProtectedStack(name), - AppConfig: appCfg, - } - } - } - - // Remove stacks no longer on disk - for name := range m.stacks { - if !found[name] { - delete(m.stacks, name) - } - } - - m.logger.Printf("[INFO] Scanned stacks: %d found", len(m.stacks)) - return m.refreshStatusLocked() -} - -// RefreshStatus updates container status for all known stacks. -func (m *Manager) RefreshStatus() error { - m.mu.Lock() - defer m.mu.Unlock() - return m.refreshStatusLocked() -} - -func (m *Manager) refreshStatusLocked() error { - output, err := m.execCommand("docker", "ps", "-a", - "--format", "{{.Names}}\t{{.Image}}\t{{.State}}\t{{.Status}}\t{{.Label \"com.docker.compose.project\"}}", - "--no-trunc") - if err != nil { - return fmt.Errorf("docker ps: %w", err) - } - - projectContainers := make(map[string][]ContainerInfo) - - for _, line := range strings.Split(strings.TrimSpace(output), "\n") { - if line == "" { - continue - } - parts := strings.SplitN(line, "\t", 5) - if len(parts) < 5 || parts[4] == "" { - continue - } - - ci := ContainerInfo{ - Name: parts[0], - Image: parts[1], - State: parseContainerState(parts[2]), - Status: parts[3], - } - projectContainers[parts[4]] = append(projectContainers[parts[4]], ci) - } - - for name, stack := range m.stacks { - containers, exists := projectContainers[name] - if !exists { - stack.Containers = nil - if stack.Deployed { - stack.State = StateStopped - } else { - stack.State = StateNotDeployed - } - } else { - stack.Containers = containers - stack.State = aggregateState(containers) - } - stack.LastUpdated = time.Now() - } - - return nil -} - -func parseContainerState(s string) ContainerState { - switch strings.ToLower(strings.TrimSpace(s)) { - case "running": - return StateRunning - case "exited": - return StateExited - case "restarting": - return StateRestarting - case "paused": - return StatePaused - case "created", "dead", "removing": - return StateStopped - default: - return StateUnknown - } -} - -func aggregateState(containers []ContainerInfo) ContainerState { - if len(containers) == 0 { - return StateNotDeployed - } - for _, c := range containers { - if c.State == StateRunning { - return StateRunning - } - } - for _, c := range containers { - if c.State == StateRestarting { - return StateRestarting - } - } - return StateStopped -} - -// --- Stack accessors --- - -func (m *Manager) GetStacks() []Stack { - m.mu.RLock() - defer m.mu.RUnlock() - - result := make([]Stack, 0, len(m.stacks)) - for _, s := range m.stacks { - result = append(result, *s) - } - return result -} - -func (m *Manager) GetStack(name string) (*Stack, bool) { - m.mu.RLock() - defer m.mu.RUnlock() - - s, ok := m.stacks[name] - if !ok { - return nil, false - } - copy := *s - return ©, true -} - -// --- Stack operations --- -// StartStack, StopStack, etc. now load app.yaml env for deployed stacks. - -func (m *Manager) StartStack(name string) error { - stack, ok := m.GetStack(name) - if !ok { - return fmt.Errorf("stack %q not found", name) - } - - m.logger.Printf("[INFO] Starting stack: %s", name) - - dir := filepath.Dir(stack.ComposePath) - env := m.stackEnv(dir) - - if _, err := m.composeExecCustomEnv(dir, env, "up", "-d"); err != nil { - return fmt.Errorf("starting stack %s: %w", name, err) - } - - m.logger.Printf("[INFO] Stack %s started", name) - return m.RefreshStatus() -} - -func (m *Manager) StopStack(name string) error { - if m.cfg.IsProtectedStack(name) { - return fmt.Errorf("stack %q is protected and cannot be stopped", name) - } - - stack, ok := m.GetStack(name) - if !ok { - return fmt.Errorf("stack %q not found", name) - } - - m.logger.Printf("[INFO] Stopping stack: %s", name) - dir := filepath.Dir(stack.ComposePath) - - if _, err := m.composeExec(dir, "down"); err != nil { - return fmt.Errorf("stopping stack %s: %w", name, err) - } - - m.logger.Printf("[INFO] Stack %s stopped", name) - return m.RefreshStatus() -} - -func (m *Manager) RestartStack(name string) error { - stack, ok := m.GetStack(name) - if !ok { - return fmt.Errorf("stack %q not found", name) - } - - m.logger.Printf("[INFO] Restarting stack: %s", name) - dir := filepath.Dir(stack.ComposePath) - - if _, err := m.composeExec(dir, "restart"); err != nil { - return fmt.Errorf("restarting stack %s: %w", name, err) - } - - m.logger.Printf("[INFO] Stack %s restarted", name) - return m.RefreshStatus() -} - -func (m *Manager) UpdateStack(name string) error { - stack, ok := m.GetStack(name) - if !ok { - return fmt.Errorf("stack %q not found", name) - } - - m.logger.Printf("[INFO] Updating stack: %s", name) - dir := filepath.Dir(stack.ComposePath) - env := m.stackEnv(dir) - - if _, err := m.composeExecCustomEnv(dir, env, "pull"); err != nil { - return fmt.Errorf("pulling images for %s: %w", name, err) - } - - if _, err := m.composeExecCustomEnv(dir, env, "up", "-d", "--remove-orphans"); err != nil { - return fmt.Errorf("recreating %s: %w", name, err) - } - - m.logger.Printf("[INFO] Stack %s updated", name) - return m.RefreshStatus() -} - -func (m *Manager) GetLogs(name string, lines int) (string, error) { - stack, ok := m.GetStack(name) - if !ok { - return "", fmt.Errorf("stack %q not found", name) - } - - if lines <= 0 { - lines = 100 - } - if lines > 1000 { - lines = 1000 - } - - dir := filepath.Dir(stack.ComposePath) - output, err := m.composeExec(dir, "logs", "--tail", fmt.Sprintf("%d", lines), "--no-color") - if err != nil { - return "", fmt.Errorf("getting logs for %s: %w", name, err) - } - return output, nil -} - -// --- Env and compose helpers --- - -// stackEnv builds the full OS env slice for a stack, merging app.yaml values. -func (m *Manager) stackEnv(stackDir string) []string { - env := os.Environ() - - // Always inject DOMAIN - env = append(env, fmt.Sprintf("DOMAIN=%s", m.cfg.Customer.Domain)) - - // Load app.yaml if it exists — merge its env vars - appCfg := LoadAppConfig(stackDir) - if appCfg != nil { - for k, v := range appCfg.Env { - env = append(env, fmt.Sprintf("%s=%s", k, v)) - } - } - - return env -} - -func (m *Manager) composeExec(dir string, args ...string) (string, error) { - return m.composeExecCustomEnv(dir, nil, args...) -} - -func (m *Manager) composeExecCustomEnv(dir string, env []string, args ...string) (string, error) { - var cmd *exec.Cmd - - if m.composeCmd == "docker compose" { - fullArgs := append([]string{"compose"}, args...) - cmd = exec.Command("docker", fullArgs...) - } else { - cmd = exec.Command("docker-compose", args...) - } - - cmd.Dir = dir - - if env != nil { - cmd.Env = env - } else { - cmd.Env = m.stackEnv(dir) - } - - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - m.logger.Printf("[DEBUG] Running: %s %s (in %s)", m.composeCmd, strings.Join(args, " "), dir) - - if err := cmd.Run(); err != nil { - return stdout.String(), fmt.Errorf("%w\nstderr: %s", err, stderr.String()) - } - - return stdout.String(), nil -} - -func (m *Manager) execCommand(name string, args ...string) (string, error) { - cmd := exec.Command(name, args...) - - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - - if err := cmd.Run(); err != nil { - return "", fmt.Errorf("exec %s %s: %w\nstderr: %s", name, strings.Join(args, " "), err, stderr.String()) - } - - return stdout.String(), nil -} diff --git a/controller/metadata.go b/controller/metadata.go deleted file mode 100755 index d9a3e26..0000000 --- a/controller/metadata.go +++ /dev/null @@ -1,132 +0,0 @@ -package stacks - -import ( - "os" - "path/filepath" - "strings" - - "gopkg.in/yaml.v3" -) - -// Metadata holds app information parsed from .felhom.yml. -type Metadata struct { - DisplayName string `yaml:"display_name" json:"display_name"` - Description string `yaml:"description" json:"description"` - Category string `yaml:"category" json:"category"` - Subdomain string `yaml:"subdomain" json:"subdomain"` - Slug string `yaml:"slug" json:"slug"` - Resources ResourceHints `yaml:"resources" json:"resources"` - DeployFields []DeployField `yaml:"deploy_fields" json:"deploy_fields"` -} - -// ResourceHints describe what the app needs. -type ResourceHints struct { - RAM string `yaml:"ram" json:"ram"` - PiCompatible bool `yaml:"pi_compatible" json:"pi_compatible"` - NeedsHDD bool `yaml:"needs_hdd" json:"needs_hdd"` -} - -// DeployField defines one configuration field shown during first deployment. -type DeployField struct { - EnvVar string `yaml:"env_var" json:"env_var"` - Label string `yaml:"label" json:"label"` - Type string `yaml:"type" json:"type"` // domain, secret, password, path, text, select, boolean - Generate string `yaml:"generate" json:"generate"` // e.g., "password:24", "hex:32", "static:admin" - Default string `yaml:"default" json:"default"` - Required bool `yaml:"required" json:"required"` - Placeholder string `yaml:"placeholder" json:"placeholder"` - Description string `yaml:"description" json:"description"` - LockedAfterDeploy bool `yaml:"locked_after_deploy" json:"locked_after_deploy"` - Options []SelectOption `yaml:"options" json:"options,omitempty"` -} - -// SelectOption is a choice for "select" type fields. -type SelectOption struct { - Value string `yaml:"value" json:"value"` - Label string `yaml:"label" json:"label"` -} - -// LoadMetadata reads .felhom.yml from a stack directory. -// Returns default metadata if the file doesn't exist. -func LoadMetadata(stackDir string) Metadata { - meta := Metadata{} - - path := filepath.Join(stackDir, ".felhom.yml") - data, err := os.ReadFile(path) - if err != nil { - // No metadata file — build defaults from directory name - dirName := filepath.Base(stackDir) - meta.DisplayName = toTitleCase(strings.ReplaceAll(dirName, "-", " ")) - meta.Slug = dirName - meta.Category = "tools" - return meta - } - - if err := yaml.Unmarshal(data, &meta); err != nil { - // Parse error — still return defaults - dirName := filepath.Base(stackDir) - meta.DisplayName = toTitleCase(strings.ReplaceAll(dirName, "-", " ")) - meta.Slug = dirName - return meta - } - - // Fill in defaults for missing fields - dirName := filepath.Base(stackDir) - if meta.Slug == "" { - meta.Slug = dirName - } - if meta.DisplayName == "" { - meta.DisplayName = toTitleCase(strings.ReplaceAll(dirName, "-", " ")) - } - if meta.Category == "" { - meta.Category = "tools" - } - - // DOMAIN field is always auto-filled — mark it implicitly required - for i := range meta.DeployFields { - if meta.DeployFields[i].Type == "domain" { - meta.DeployFields[i].Required = true - meta.DeployFields[i].LockedAfterDeploy = true - } - // secret fields are always locked after deploy - if meta.DeployFields[i].Type == "secret" { - meta.DeployFields[i].LockedAfterDeploy = true - } - } - - return meta -} - -// HasDeployFields returns true if the app has any user-facing deploy fields -// (i.e., fields beyond auto-filled domain and auto-generated secrets). -func (m *Metadata) HasDeployFields() bool { - for _, f := range m.DeployFields { - if f.Type != "domain" && f.Type != "secret" { - return true - } - } - return false -} - -// UserFacingFields returns only fields the user needs to interact with. -// Excludes auto-filled (domain) and fully hidden (secret) fields. -func (m *Metadata) UserFacingFields() []DeployField { - var fields []DeployField - for _, f := range m.DeployFields { - if f.Type != "domain" && f.Type != "secret" { - fields = append(fields, f) - } - } - return fields -} - -// AutoGeneratedFields returns fields that are generated without user input. -func (m *Metadata) AutoGeneratedFields() []DeployField { - var fields []DeployField - for _, f := range m.DeployFields { - if f.Type == "secret" || f.Type == "domain" { - fields = append(fields, f) - } - } - return fields -} diff --git a/controller/mnt/user-data/outputs/felhom-controller/README.md b/controller/mnt/user-data/outputs/felhom-controller/README.md deleted file mode 100755 index a8dfea5..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/README.md +++ /dev/null @@ -1,283 +0,0 @@ -# felhom-controller - -**Central management container for Felhom home servers.** - -Replaces Portainer + scattered systemd scripts with a single, lightweight container that provides: -- Hungarian-language web dashboard for customers -- Docker Compose stack management (start/stop/update) -- Backup orchestration (DB dumps + restic snapshots) -- System health monitoring with Healthchecks pings -- Git-based stack synchronization with update management -- Self-update with automatic rollback on failure - -## Architecture - -``` -┌─────────────────────────────────────────────────────────────────┐ -│ Customer Hardware (N100 mini PC / Raspberry Pi) │ -│ │ -│ ┌──────────┐ ┌────────────────────────────────────────────┐ │ -│ │ Traefik │ │ felhom-controller │ │ -│ │ (reverse │──▶│ │ │ -│ │ proxy) │ │ ┌──────────┐ ┌─────────────────────────┐│ │ -│ └──────────┘ │ │ Web UI │ │ Stack Manager ││ │ -│ │ │ (HU dash │ │ (compose up/down/pull, ││ │ -│ ┌──────────┐ │ │ board) │ │ git sync, update mgmt) ││ │ -│ │cloudflared│ │ └──────────┘ └─────────────────────────┘│ │ -│ │ (tunnel) │ │ ┌──────────┐ ┌─────────────────────────┐│ │ -│ └──────────┘ │ │ Backup │ │ Monitor & Pinger ││ │ -│ │ │ (db dump │ │ (healthchecks pings, ││ │ -│ ┌──────────┐ │ │ restic) │ │ system metrics) ││ │ -│ │ App │ │ └──────────┘ └─────────────────────────┘│ │ -│ │ stacks │ │ ┌──────────┐ ┌─────────────────────────┐│ │ -│ │ (docker │ │ │Scheduler │ │ REST API ││ │ -│ │ compose) │ │ │(cron-like│ │ (for UI + remote mgmt) ││ │ -│ └──────────┘ │ │ jobs) │ └─────────────────────────┘│ │ -│ │ └──────────┘ │ │ -│ └────────────────────────────────────────────┘ │ -└─────────────────────────────────────────────────────────────────┘ - │ pings │ git pull - ▼ ▼ - status.felhom.eu gitea.dooplex.hu - (Healthchecks on k3s) (stack definitions) -``` - -## Module Overview - -| Module | Path | Responsibility | -|--------|------|----------------| -| **Config** | `internal/config/` | Load & validate controller.yaml | -| **Stacks** | `internal/stacks/` | Docker Compose operations, catalog, container status | -| **Backup** | `internal/backup/` | DB dumps, restic snapshots, restore | -| **Monitor** | `internal/monitor/` | Health checks, Healthchecks pings, system metrics | -| **Scheduler** | `internal/scheduler/` | Cron-like job runner for all periodic tasks | -| **API** | `internal/api/` | REST API endpoints (consumed by web UI + remote mgmt) | -| **Web** | `internal/web/` | Dashboard UI, static files, server-side templates | - -## Stack Management - -### How stacks get onto the machine - -1. During initial setup, `deploy-felhom-compose.sh` clones the app catalog -2. Compose files + `.felhom.yml` metadata land in `/opt/docker/stacks//` -3. The controller periodically pulls from Git to detect changes - -### First deployment flow (via dashboard) - -1. Customer sees app card with "🚀 Telepítés" (Deploy) button -2. Clicks → deploy page shows: - - **Auto-filled**: DOMAIN (from controller config), read-only - - **Auto-generated**: DB passwords, secret keys (shown as "✓ Generated") - - **User input**: HDD path, admin password, language, etc. - - **"🎲 Generálás"** button next to password fields -3. Clicks "Telepítés" → controller: - - Generates all secrets - - Validates required fields (checks path exists, etc.) - - Saves `app.yaml` (env vars + locked fields list) - - Runs `docker compose up -d` with env vars injected -4. Post-deploy: locked fields (DB_PASSWORD, etc.) become read-only - -### Update strategy - -Stack updates are classified in the Git repository via markers: - -| Marker | Behavior | -|--------|----------| -| No marker | Optional update — shown on dashboard, customer clicks "Update" | -| `UPDATE_REQUIRED=true` | Mandatory — auto-applied during next update window | -| `UPDATE_SECURITY=true` | Critical — applied immediately (within minutes) | - -The update window is configurable per customer (default: 03:00-05:00 local time). - -### Protected stacks - -The following stacks cannot be stopped from the customer UI: -- `traefik` (reverse proxy) -- `cloudflared` (tunnel) -- `felhom-controller` (this container) - -## Backup Strategy - -The controller replaces Backrest and manages backups directly: - -1. **DB dumps** (default 02:30): Discovers running database containers, dumps via pg_dump/mysqldump -2. **Restic snapshots** (default 03:00): Backs up `/opt/docker/stacks/` data + DB dumps -3. **Verification**: Periodically checks snapshot integrity -4. **Pruning**: Configurable retention (default: 7 daily, 4 weekly, 6 monthly) - -Backup status is displayed on the dashboard and reported to Healthchecks. - -## Self-Update Mechanism - -1. Controller checks for new image versions periodically -2. Before updating: creates a restic snapshot of its own config -3. Pulls new image, recreates container -4. Health check timeout (60s) — if new container doesn't become healthy → rollback -5. Rollback: restores previous image tag, restarts with old config - -## Configuration - -### Controller config (infrastructure only) - -Single YAML file per customer: `/opt/docker/felhom-controller/controller.yaml` - -Contains customer identity, infrastructure secrets, backup/monitoring settings. -Does **not** contain app-specific config (HDD paths, DB passwords, etc.). - -See `configs/controller.yaml.example` for the full reference. - -### Per-app config (created during deployment) - -Each deployed app gets an `app.yaml` in its stack directory: - -```yaml -# /opt/docker/stacks/paperless-ngx/app.yaml -# Auto-generated by felhom-controller — do not edit locked fields manually -deployed: true -deployed_at: "2026-02-13T14:30:00Z" -env: - DOMAIN: "demo-felhom.eu" - DB_PASSWORD: "a7f2b9c1e4d..." # locked - PAPERLESS_SECRET_KEY: "8b3e..." # locked - PAPERLESS_ADMIN_USER: "admin" # editable - HDD_PATH: "/mnt/hdd_1" # locked -locked_fields: - - DB_PASSWORD - - PAPERLESS_SECRET_KEY - - DOMAIN - - HDD_PATH -``` - -Fields are defined in each stack's `.felhom.yml` metadata file. See -`configs/example-felhom-metadata.yml` for the full format. - -### App assets (logos, screenshots, descriptions) - -Baked into the container image at build time — no external dependencies at runtime. -Assets are synced from the felhom.eu website repo before building: - -```bash -make sync-assets # copies from ../felhom.eu/website/assets/ -make sync-assets WEBSITE_ASSETS_DIR=/path # or specify custom path -``` - -Served locally at `/static/assets/`. Naming convention matches the website: - -| Asset | File pattern | Served at | -|-------|-------------|-----------| -| Logo (SVG) | `assets/{slug}-logo.svg` | `/static/assets/{slug}-logo.svg` | -| Logo (PNG fallback) | `assets/{slug}-logo.png` | `/static/assets/{slug}-logo.png` | -| Screenshot | `assets/{slug}-screenshot-{n}.webp` | `/static/assets/{slug}-screenshot-{n}.webp` | - -## Build & Deploy - -```bash -# Build for both architectures -make build-all - -# Build Docker image -make docker-build - -# Push to registry -make docker-push - -# Build for specific arch -make build-amd64 -make build-arm64 -``` - -## Development - -```bash -# Run locally (needs Docker socket) -go run ./cmd/controller/ --config configs/controller.yaml.example - -# Run tests -go test ./... - -# Lint -golangci-lint run -``` - -## Repository Layout - -``` -felhom-controller/ -├── cmd/controller/ # Entry point -│ └── main.go -├── internal/ -│ ├── config/ # Configuration loading -│ │ └── config.go -│ ├── stacks/ # Docker Compose stack management -│ │ ├── manager.go # Core: scan, start, stop, restart, update, logs -│ │ ├── metadata.go # Parse .felhom.yml app metadata -│ │ └── deploy.go # First-deploy flow: secret gen, app.yaml, compose up -│ ├── backup/ # DB dumps + restic operations (Phase 3) -│ ├── monitor/ # Health checks + metrics (Phase 2) -│ ├── scheduler/ # Periodic job runner (Phase 2) -│ ├── api/ # REST API -│ │ └── router.go -│ └── web/ # Dashboard UI -│ ├── server.go # HTTP server, auth, page handlers -│ └── templates.go # Embedded HTML templates + CSS (Hungarian) -├── configs/ # Example config files -│ ├── controller.yaml.example -│ └── example-felhom-metadata.yml -├── docs/ -│ └── BUILDING.md # Container image build & registry guide -├── scripts/ -│ └── hashpass.go # Password hash generator -├── Dockerfile # Multi-stage build (Go + debian-slim) -├── docker-compose.yml # Controller's own compose definition -├── Makefile # Build targets (amd64, arm64, docker) -├── go.mod -└── README.md -``` - -## Status & Roadmap - -### Phase 1 — Stack Manager + Deploy Flow (current) -- [x] Project skeleton & config format -- [x] .felhom.yml app metadata format with deploy fields -- [x] Per-app config persistence (app.yaml) -- [x] Secret generation engine (password, hex, static) -- [x] Stack catalog (read compose files + metadata from disk) -- [x] Docker Compose operations (up/down/pull/ps/logs) -- [x] Deploy flow with interactive field input -- [x] Basic web dashboard with start/stop/deploy buttons -- [x] REST API for stack + deploy operations -- [x] Simple web authentication (bcrypt sessions) -- [x] App logos + screenshots loaded from felhom.eu -- [x] Container image build pipeline (Dockerfile + Makefile) -- [ ] First build & test on N100 hardware -- [ ] End-to-end test: deploy an app through dashboard - -### Phase 2 — Monitoring & Health -- [ ] System metrics collection (CPU, RAM, disk, temperature) -- [ ] Healthchecks.io ping integration -- [ ] Dashboard system health panel -- [ ] Customer notifications (email/Telegram) - -### Phase 3 — Backups -- [ ] DB dump engine (PostgreSQL, MariaDB/MySQL, SQLite) -- [ ] Restic integration (snapshot, prune, check) -- [ ] Backup status on dashboard -- [ ] Manual backup trigger from UI -- [ ] Restore workflow - -### Phase 4 — Git Sync & Updates -- [ ] Periodic git pull for stack definitions -- [ ] Update classification (optional/required/security) -- [ ] Update window enforcement -- [ ] Dashboard update notifications with "Update" button - -### Phase 5 — Self-Update & Resilience -- [ ] Self-update check & execution -- [ ] Pre-update config backup -- [ ] Health-based rollback mechanism -- [ ] Config export/import - -### Phase 6 — Central Management (future) -- [ ] API authentication for remote management -- [ ] Central dashboard on k3s querying all customer controllers -- [ ] Fleet-wide update management diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/README.md b/controller/mnt/user-data/outputs/felhom-controller/assets/README.md deleted file mode 100755 index 968a5f3..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# App Assets - -This directory contains logos and screenshots for the dashboard. -They are baked into the Docker image at build time. - -## Naming convention - -Files must follow the felhom.eu website convention: - -- `{slug}-logo.svg` — App logo (SVG preferred, displayed on dark background) -- `{slug}-logo.png` — App logo fallback (PNG, for apps without SVG) -- `{slug}-screenshot-1.webp` — First screenshot -- `{slug}-screenshot-2.webp` — Second screenshot (and so on) - -The dashboard tries SVG first, falls back to PNG if not found. - -Example: -``` -paperless-ngx-logo.svg -paperless-ngx-screenshot-1.webp -adventurelog-logo.png -adventurelog-screenshot-1.webp -``` - -## Syncing from felhom.eu website - -Run `make sync-assets` to copy assets from the felhom.eu website repo. -This expects the website files to be available at `../felhom.eu/website/assets/` -(relative to this repo), or set `WEBSITE_ASSETS_DIR` to override. - -Alternatively, copy files manually from FileBrowser at https://felhom.eu. diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/actualbudget-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/actualbudget-logo.svg deleted file mode 100644 index a21388f..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/actualbudget-logo.svg +++ /dev/null @@ -1 +0,0 @@ -Actual Budget \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/actualbudget-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/actualbudget-screenshot-1.webp deleted file mode 100644 index 371c45a..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/actualbudget-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/actualbudget-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/actualbudget-screenshot-2.webp deleted file mode 100644 index d883fa5..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/actualbudget-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/actualbudget-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/actualbudget-screenshot-3.webp deleted file mode 100644 index b2dd5e9..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/actualbudget-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-logo.png b/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-logo.png deleted file mode 100644 index 956ae3d..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-logo.png and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-screenshot-1.webp deleted file mode 100644 index 4402c53..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-screenshot-2.webp deleted file mode 100644 index 00aabdc..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-screenshot-3.webp deleted file mode 100644 index c211a56..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-screenshot-4.webp deleted file mode 100644 index 0688d59..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-screenshot-4.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-screenshot-5.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-screenshot-5.webp deleted file mode 100644 index eb1a7f6..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/adventurelog-screenshot-5.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/audiobookshelf-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/audiobookshelf-logo.svg deleted file mode 100644 index a89b499..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/audiobookshelf-logo.svg +++ /dev/null @@ -1 +0,0 @@ -Audiobookshelf \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/audiobookshelf-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/audiobookshelf-screenshot-1.webp deleted file mode 100644 index 0967d93..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/audiobookshelf-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/audiobookshelf-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/audiobookshelf-screenshot-2.webp deleted file mode 100644 index acc2feb..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/audiobookshelf-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/audiobookshelf-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/audiobookshelf-screenshot-3.webp deleted file mode 100644 index 3fa36d9..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/audiobookshelf-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/audiobookshelf-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/audiobookshelf-screenshot-4.webp deleted file mode 100644 index fd5d38f..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/audiobookshelf-screenshot-4.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/bentopdf-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/bentopdf-logo.svg deleted file mode 100644 index 7e480c9..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/bentopdf-logo.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/bentopdf-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/bentopdf-screenshot-1.webp deleted file mode 100644 index f547fe3..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/bentopdf-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/bentopdf-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/bentopdf-screenshot-2.webp deleted file mode 100644 index 7b13898..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/bentopdf-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/bookstack-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/bookstack-logo.svg deleted file mode 100644 index 969e416..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/bookstack-logo.svg +++ /dev/null @@ -1 +0,0 @@ -BookStack \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/bookstack-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/bookstack-screenshot-1.webp deleted file mode 100644 index d72790e..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/bookstack-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/bookstack-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/bookstack-screenshot-2.webp deleted file mode 100644 index 0aebfed..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/bookstack-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/bookstack-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/bookstack-screenshot-3.webp deleted file mode 100644 index 2e6419d..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/bookstack-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-logo.svg deleted file mode 100644 index a24d70a..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-logo.svg +++ /dev/null @@ -1 +0,0 @@ -Cal.com \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-1.webp deleted file mode 100644 index 5a5e835..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-2.webp deleted file mode 100644 index 7d0a25f..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-3.webp deleted file mode 100644 index 8cd93b9..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-4.webp deleted file mode 100644 index 241ae9b..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-4.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-5.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-5.webp deleted file mode 100644 index e81791a..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-5.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-6.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-6.webp deleted file mode 100644 index f535461..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/calcom-screenshot-6.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/calibre-web-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/calibre-web-logo.svg deleted file mode 100644 index fc15c65..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/calibre-web-logo.svg +++ /dev/null @@ -1 +0,0 @@ -Calibre-Web \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/calibre-web-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/calibre-web-screenshot-1.webp deleted file mode 100644 index fa509d3..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/calibre-web-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/calibre-web-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/calibre-web-screenshot-2.webp deleted file mode 100644 index 9ac003b..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/calibre-web-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/checkmark-green.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/checkmark-green.svg deleted file mode 100644 index 19e0bd7..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/checkmark-green.svg +++ /dev/null @@ -1,2 +0,0 @@ - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/checkmark-yellow.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/checkmark-yellow.svg deleted file mode 100644 index 39540e2..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/checkmark-yellow.svg +++ /dev/null @@ -1,2 +0,0 @@ - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/claper-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/claper-logo.svg deleted file mode 100644 index 4527934..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/claper-logo.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/claper-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/claper-screenshot-1.webp deleted file mode 100644 index b855ed8..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/claper-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/claper-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/claper-screenshot-2.webp deleted file mode 100644 index c29a350..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/claper-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/claper-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/claper-screenshot-3.webp deleted file mode 100644 index 521b5e4..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/claper-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/claper-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/claper-screenshot-4.webp deleted file mode 100644 index 917e740..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/claper-screenshot-4.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/claper-screenshot-5.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/claper-screenshot-5.webp deleted file mode 100644 index 635c61a..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/claper-screenshot-5.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/code-server-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/code-server-logo.svg deleted file mode 100644 index 693133c..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/code-server-logo.svg +++ /dev/null @@ -1 +0,0 @@ -Coder \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/code-server-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/code-server-screenshot-1.webp deleted file mode 100644 index cd77ea5..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/code-server-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/code-server-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/code-server-screenshot-2.webp deleted file mode 100644 index d136d89..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/code-server-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/crafty-logo.png b/controller/mnt/user-data/outputs/felhom-controller/assets/crafty-logo.png deleted file mode 100644 index 3406977..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/crafty-logo.png and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/crafty-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/crafty-screenshot-1.webp deleted file mode 100644 index fd16107..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/crafty-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/crafty-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/crafty-screenshot-2.webp deleted file mode 100644 index a0700d7..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/crafty-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/crafty-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/crafty-screenshot-3.webp deleted file mode 100644 index 927551a..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/crafty-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/emby-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/emby-logo.svg deleted file mode 100644 index 43ccc64..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/emby-logo.svg +++ /dev/null @@ -1 +0,0 @@ -Emby \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/emby-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/emby-screenshot-1.webp deleted file mode 100644 index a1eab5e..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/emby-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/emby-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/emby-screenshot-2.webp deleted file mode 100644 index fa10988..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/emby-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/felhom.eu-logo.png b/controller/mnt/user-data/outputs/felhom-controller/assets/felhom.eu-logo.png deleted file mode 100644 index 2dc883f..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/felhom.eu-logo.png and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/filebrowser-logo.png b/controller/mnt/user-data/outputs/felhom-controller/assets/filebrowser-logo.png deleted file mode 100644 index 8f26819..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/filebrowser-logo.png and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/filebrowser-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/filebrowser-logo.svg deleted file mode 100644 index 68ecb7b..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/filebrowser-logo.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/filebrowser-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/filebrowser-screenshot-1.webp deleted file mode 100644 index 6774a93..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/filebrowser-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/filebrowser-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/filebrowser-screenshot-2.webp deleted file mode 100644 index ecfcc52..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/filebrowser-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/ghost-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/ghost-logo.svg deleted file mode 100644 index 93000a5..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/ghost-logo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - Ghost - - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/ghost-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/ghost-screenshot-1.webp deleted file mode 100644 index 338cc2f..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/ghost-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/ghost-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/ghost-screenshot-2.webp deleted file mode 100644 index d8c95bc..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/ghost-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/ghost-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/ghost-screenshot-3.webp deleted file mode 100644 index 0844e2b..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/ghost-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/ghost-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/ghost-screenshot-4.webp deleted file mode 100644 index 582afab..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/ghost-screenshot-4.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/gitea-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/gitea-logo.svg deleted file mode 100644 index 4cce797..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/gitea-logo.svg +++ /dev/null @@ -1 +0,0 @@ -Gitea \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/gitea-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/gitea-screenshot-1.webp deleted file mode 100644 index 5cd4c99..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/gitea-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/gitea-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/gitea-screenshot-2.webp deleted file mode 100644 index 2347ce6..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/gitea-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/gitea-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/gitea-screenshot-3.webp deleted file mode 100644 index f292387..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/gitea-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/github.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/github.svg deleted file mode 100644 index b3912d6..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/github.svg +++ /dev/null @@ -1 +0,0 @@ -GitHub \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/glance-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/glance-logo.svg deleted file mode 100644 index ed733c3..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/glance-logo.svg +++ /dev/null @@ -1 +0,0 @@ -Glance \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/glance-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/glance-screenshot-1.webp deleted file mode 100644 index 42b506c..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/glance-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/glance-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/glance-screenshot-2.webp deleted file mode 100644 index e592e54..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/glance-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/glance-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/glance-screenshot-3.webp deleted file mode 100644 index bc44e09..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/glance-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/glance-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/glance-screenshot-4.webp deleted file mode 100644 index 89cf632..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/glance-screenshot-4.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/gokapi-logo.png b/controller/mnt/user-data/outputs/felhom-controller/assets/gokapi-logo.png deleted file mode 100644 index 9c4a0ef..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/gokapi-logo.png and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/gokapi-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/gokapi-screenshot-1.webp deleted file mode 100644 index 71fb248..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/gokapi-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/grafana-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/grafana-logo.svg deleted file mode 100644 index c838eb5..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/grafana-logo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - Grafana - - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/grafana-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/grafana-screenshot-1.webp deleted file mode 100644 index 549b71c..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/grafana-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/grafana-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/grafana-screenshot-2.webp deleted file mode 100644 index c3e8f39..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/grafana-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/grafana-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/grafana-screenshot-3.webp deleted file mode 100644 index 91d42cb..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/grafana-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/grafana-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/grafana-screenshot-4.webp deleted file mode 100644 index 0da7f23..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/grafana-screenshot-4.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/grampsweb-logo.png b/controller/mnt/user-data/outputs/felhom-controller/assets/grampsweb-logo.png deleted file mode 100644 index 732276b..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/grampsweb-logo.png and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/grampsweb-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/grampsweb-screenshot-1.webp deleted file mode 100644 index abb32f3..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/grampsweb-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/grampsweb-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/grampsweb-screenshot-2.webp deleted file mode 100644 index 826dbde..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/grampsweb-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/grampsweb-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/grampsweb-screenshot-3.webp deleted file mode 100644 index 6d38168..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/grampsweb-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/grampsweb-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/grampsweb-screenshot-4.webp deleted file mode 100644 index 22b45ec..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/grampsweb-screenshot-4.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-logo.svg deleted file mode 100644 index cf2a613..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-logo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - Home Assistant - - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-1.webp deleted file mode 100644 index 77a0a44..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-2.webp deleted file mode 100644 index 04cd49f..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-3.webp deleted file mode 100644 index c7a0ddf..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-4.webp deleted file mode 100644 index bf59b7d..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-4.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-5.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-5.webp deleted file mode 100644 index 22d4346..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-5.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-6.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-6.webp deleted file mode 100644 index 4b34f57..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/homeassistant-screenshot-6.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/homepage-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/homepage-logo.svg deleted file mode 100644 index 26c9eda..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/homepage-logo.svg +++ /dev/null @@ -1 +0,0 @@ -Homepage \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/homepage-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/homepage-screenshot-1.webp deleted file mode 100644 index b8ee7bd..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/homepage-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/homepage-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/homepage-screenshot-2.webp deleted file mode 100644 index f9a88be..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/homepage-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/homepage-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/homepage-screenshot-3.webp deleted file mode 100644 index 8e2c89f..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/homepage-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/homepage-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/homepage-screenshot-4.webp deleted file mode 100644 index 0f018e0..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/homepage-screenshot-4.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/immich-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/immich-logo.svg deleted file mode 100644 index 4dee35b..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/immich-logo.svg +++ /dev/null @@ -1 +0,0 @@ -Immich \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/immich-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/immich-screenshot-1.webp deleted file mode 100644 index bf9fe37..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/immich-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/immich-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/immich-screenshot-2.webp deleted file mode 100644 index 74147f7..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/immich-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/immich-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/immich-screenshot-3.webp deleted file mode 100644 index 7522e61..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/immich-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/immich-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/immich-screenshot-4.webp deleted file mode 100644 index 8c54e1b..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/immich-screenshot-4.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/jellyfin-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/jellyfin-logo.svg deleted file mode 100644 index 0ea38d0..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/jellyfin-logo.svg +++ /dev/null @@ -1 +0,0 @@ -Jellyfin \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/jellyfin-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/jellyfin-screenshot-1.webp deleted file mode 100644 index 81b827b..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/jellyfin-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/jellyfin-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/jellyfin-screenshot-2.webp deleted file mode 100644 index 3ccab9e..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/jellyfin-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-logo.png b/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-logo.png deleted file mode 100644 index 907dee9..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-logo.png and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-screenshot-1.webp deleted file mode 100644 index ea64c82..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-screenshot-2.webp deleted file mode 100644 index 0494fb0..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-screenshot-3.webp deleted file mode 100644 index 774d29e..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-screenshot-4.webp deleted file mode 100644 index 3404812..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-screenshot-4.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-screenshot-5.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-screenshot-5.webp deleted file mode 100644 index 45e0b8b..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/kimai-screenshot-5.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/komga-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/komga-logo.svg deleted file mode 100644 index 5b8093b..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/komga-logo.svg +++ /dev/null @@ -1,195 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/komga-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/komga-screenshot-1.webp deleted file mode 100644 index 8f8590a..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/komga-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/logo.png b/controller/mnt/user-data/outputs/felhom-controller/assets/logo.png deleted file mode 100644 index 122d590..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/logo.png and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/logo.svg deleted file mode 100644 index 7dee180..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/logo.svg +++ /dev/null @@ -1,232 +0,0 @@ - -felhomeu diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-logo.svg deleted file mode 100644 index e1265cd..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-logo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - n8n - - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-screenshot-1.webp deleted file mode 100644 index 881c479..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-screenshot-2.webp deleted file mode 100644 index aed895d..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-screenshot-3.webp deleted file mode 100644 index 32eb6bb..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-screenshot-4.webp deleted file mode 100644 index 2e46505..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-screenshot-4.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-screenshot-5.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-screenshot-5.webp deleted file mode 100644 index c13f447..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/n8n-screenshot-5.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/navidrome-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/navidrome-logo.svg deleted file mode 100644 index 1b52076..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/navidrome-logo.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - - Created by potrace 1.11, written by Peter Selinger 2001-2013 - - - - - - - - - - - - - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/navidrome-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/navidrome-screenshot-1.webp deleted file mode 100644 index 463ac76..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/navidrome-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/navidrome-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/navidrome-screenshot-2.webp deleted file mode 100644 index 5f9ce76..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/navidrome-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/nextcloud-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/nextcloud-logo.svg deleted file mode 100644 index 066b250..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/nextcloud-logo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - Nextcloud - - \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/nextcloud-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/nextcloud-screenshot-1.webp deleted file mode 100644 index 0d349e1..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/nextcloud-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/nextcloud-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/nextcloud-screenshot-2.webp deleted file mode 100644 index 0eb7d9a..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/nextcloud-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/nextcloud-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/nextcloud-screenshot-3.webp deleted file mode 100644 index 7613439..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/nextcloud-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/og-image.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/og-image.svg deleted file mode 100644 index 15a8221..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/og-image.svg +++ /dev/null @@ -1,313 +0,0 @@ - -Saját felhőd,saját szabályaidfelhomeu diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/og-image_wide.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/og-image_wide.svg deleted file mode 100644 index 0c9b5ba..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/og-image_wide.svg +++ /dev/null @@ -1,326 +0,0 @@ - -Saját felhőd,saját szabályaidfelhomeu diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-logo.svg deleted file mode 100644 index 06cf12c..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-logo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - ONLYOFFICE - - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-1.webp deleted file mode 100644 index 0d4fa0d..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-2.webp deleted file mode 100644 index b754526..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-3.webp deleted file mode 100644 index 0ff7378..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-4.webp deleted file mode 100644 index 81a4012..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-4.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-5.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-5.webp deleted file mode 100644 index 3052fe2..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-5.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-6.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-6.webp deleted file mode 100644 index 466539b..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-6.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-7.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-7.webp deleted file mode 100644 index 0e4cb27..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-7.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-8.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-8.webp deleted file mode 100644 index 5e86d32..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-8.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-9.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-9.webp deleted file mode 100644 index 327ca4d..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/onlyoffice-screenshot-9.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/opengist-logo.png b/controller/mnt/user-data/outputs/felhom-controller/assets/opengist-logo.png deleted file mode 100644 index bf9e33a..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/opengist-logo.png and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/opengist-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/opengist-screenshot-1.webp deleted file mode 100644 index ae6f1c5..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/opengist-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/opengist-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/opengist-screenshot-2.webp deleted file mode 100644 index ee05252..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/opengist-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/opengist-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/opengist-screenshot-3.webp deleted file mode 100644 index 889c377..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/opengist-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/outline-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/outline-logo.svg deleted file mode 100644 index c2d8fb0..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/outline-logo.svg +++ /dev/null @@ -1 +0,0 @@ -Outline \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/outline-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/outline-screenshot-1.webp deleted file mode 100644 index fc8a176..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/outline-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/outline-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/outline-screenshot-2.webp deleted file mode 100644 index eb4a7b5..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/outline-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/paperless-ngx-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/paperless-ngx-logo.svg deleted file mode 100644 index a48a838..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/paperless-ngx-logo.svg +++ /dev/null @@ -1 +0,0 @@ -Paperless-ngx \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/paperless-ngx-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/paperless-ngx-screenshot-1.webp deleted file mode 100644 index c9f6e95..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/paperless-ngx-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/paperless-ngx-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/paperless-ngx-screenshot-2.webp deleted file mode 100644 index f53abbc..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/paperless-ngx-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/paperless-ngx-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/paperless-ngx-screenshot-3.webp deleted file mode 100644 index ce9e1ab..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/paperless-ngx-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/papra-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/papra-logo.svg deleted file mode 100644 index 2100cc3..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/papra-logo.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/papra-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/papra-screenshot-1.webp deleted file mode 100644 index c824e36..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/papra-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/papra-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/papra-screenshot-2.webp deleted file mode 100644 index 365cc3d..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/papra-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/papra-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/papra-screenshot-3.webp deleted file mode 100644 index 86ba60e..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/papra-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/plantit-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/plantit-logo.svg deleted file mode 100644 index a46ff8c..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/plantit-logo.svg +++ /dev/null @@ -1 +0,0 @@ -Leaflet \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/plantit-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/plantit-screenshot-1.webp deleted file mode 100644 index 3ea31ae..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/plantit-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/plantit-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/plantit-screenshot-2.webp deleted file mode 100644 index dc710ba..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/plantit-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/plantit-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/plantit-screenshot-3.webp deleted file mode 100644 index 9eb0226..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/plantit-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/plex-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/plex-logo.svg deleted file mode 100644 index 8514757..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/plex-logo.svg +++ /dev/null @@ -1 +0,0 @@ -Plex \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/plex-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/plex-screenshot-1.webp deleted file mode 100644 index af18f17..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/plex-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/plex-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/plex-screenshot-2.webp deleted file mode 100644 index d934d28..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/plex-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/plex-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/plex-screenshot-3.webp deleted file mode 100644 index 623395a..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/plex-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/privatebin-logo.png b/controller/mnt/user-data/outputs/felhom-controller/assets/privatebin-logo.png deleted file mode 100644 index fd5e3cd..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/privatebin-logo.png and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/privatebin-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/privatebin-screenshot-1.webp deleted file mode 100644 index 54838da..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/privatebin-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/privatebin-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/privatebin-screenshot-2.webp deleted file mode 100644 index b1236bc..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/privatebin-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/radarr-logo.png b/controller/mnt/user-data/outputs/felhom-controller/assets/radarr-logo.png deleted file mode 100644 index 57ff998..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/radarr-logo.png and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/radarr-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/radarr-screenshot-1.webp deleted file mode 100644 index c1b2e7a..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/radarr-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/radarr-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/radarr-screenshot-2.webp deleted file mode 100644 index 77e6fb8..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/radarr-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/rallly-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/rallly-logo.svg deleted file mode 100644 index 1a2d3ad..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/rallly-logo.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/rallly-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/rallly-screenshot-1.webp deleted file mode 100644 index 3994f90..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/rallly-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/rallly-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/rallly-screenshot-2.webp deleted file mode 100644 index ae5b798..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/rallly-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/romm-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/romm-logo.svg deleted file mode 100644 index a8e9290..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/romm-logo.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/romm-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/romm-screenshot-1.webp deleted file mode 100644 index bd343dc..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/romm-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/romm-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/romm-screenshot-2.webp deleted file mode 100644 index 94ad5cd..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/romm-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/seerr-logo.png b/controller/mnt/user-data/outputs/felhom-controller/assets/seerr-logo.png deleted file mode 100644 index b97202a..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/seerr-logo.png and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/seerr-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/seerr-screenshot-1.webp deleted file mode 100644 index 2213082..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/seerr-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/seerr-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/seerr-screenshot-2.webp deleted file mode 100644 index 2d90318..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/seerr-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/sonarr-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/sonarr-logo.svg deleted file mode 100644 index bbbb943..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/sonarr-logo.svg +++ /dev/null @@ -1 +0,0 @@ -sonarr \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/sonarr-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/sonarr-screenshot-1.webp deleted file mode 100644 index 8f74587..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/sonarr-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/sonarr-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/sonarr-screenshot-2.webp deleted file mode 100644 index 8ff32dc..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/sonarr-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/sonarr-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/sonarr-screenshot-3.webp deleted file mode 100644 index 059e44f..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/sonarr-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/tandoor-logo.png b/controller/mnt/user-data/outputs/felhom-controller/assets/tandoor-logo.png deleted file mode 100644 index 586f277..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/tandoor-logo.png and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/tandoor-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/tandoor-screenshot-1.webp deleted file mode 100644 index d1f81d5..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/tandoor-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/tandoor-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/tandoor-screenshot-2.webp deleted file mode 100644 index e1a1347..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/tandoor-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/tandoor-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/tandoor-screenshot-3.webp deleted file mode 100644 index 558dede..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/tandoor-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/termix-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/termix-logo.svg deleted file mode 100644 index d1f4ef7..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/termix-logo.svg +++ /dev/null @@ -1,57 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/termix-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/termix-screenshot-1.webp deleted file mode 100644 index f6ad76c..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/termix-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/termix-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/termix-screenshot-2.webp deleted file mode 100644 index 29764b1..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/termix-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/termix-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/termix-screenshot-3.webp deleted file mode 100644 index 377c9f9..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/termix-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/termix-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/termix-screenshot-4.webp deleted file mode 100644 index 09e8f71..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/termix-screenshot-4.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/uptimekuma-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/uptimekuma-logo.svg deleted file mode 100644 index 65e6c1c..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/uptimekuma-logo.svg +++ /dev/null @@ -1 +0,0 @@ -Uptime Kuma \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/uptimekuma-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/uptimekuma-screenshot-1.webp deleted file mode 100644 index 6e4f7d5..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/uptimekuma-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/uptimekuma-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/uptimekuma-screenshot-2.webp deleted file mode 100644 index 41dd9b8..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/uptimekuma-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/uptimekuma-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/uptimekuma-screenshot-3.webp deleted file mode 100644 index 57fbd6a..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/uptimekuma-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/vaultwarden-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/vaultwarden-logo.svg deleted file mode 100644 index e8143af..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/vaultwarden-logo.svg +++ /dev/null @@ -1 +0,0 @@ -Vaultwarden \ No newline at end of file diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/vaultwarden-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/vaultwarden-screenshot-1.webp deleted file mode 100644 index 86e5d03..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/vaultwarden-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/vikunja-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/vikunja-logo.svg deleted file mode 100644 index a5922b1..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/vikunja-logo.svg +++ /dev/null @@ -1,4 +0,0 @@ - - Vikunja - - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/vikunja-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/vikunja-screenshot-1.webp deleted file mode 100644 index 9ffd76e..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/vikunja-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/vikunja-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/vikunja-screenshot-2.webp deleted file mode 100644 index 115c343..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/vikunja-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/vikunja-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/vikunja-screenshot-3.webp deleted file mode 100644 index fe9f613..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/vikunja-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/vikunja-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/vikunja-screenshot-4.webp deleted file mode 100644 index 898db42..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/vikunja-screenshot-4.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/wanderer-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/wanderer-logo.svg deleted file mode 100644 index d7a43bf..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/wanderer-logo.svg +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/wanderer-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/wanderer-screenshot-1.webp deleted file mode 100644 index 6efc662..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/wanderer-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/wanderer-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/wanderer-screenshot-2.webp deleted file mode 100644 index 70bf9a5..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/wanderer-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/wanderer-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/wanderer-screenshot-3.webp deleted file mode 100644 index 257eab9..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/wanderer-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/wanderer-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/wanderer-screenshot-4.webp deleted file mode 100644 index 5ded517..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/wanderer-screenshot-4.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/wger-logo.png b/controller/mnt/user-data/outputs/felhom-controller/assets/wger-logo.png deleted file mode 100644 index fa238fc..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/wger-logo.png and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/wger-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/wger-screenshot-1.webp deleted file mode 100644 index 56ff9c2..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/wger-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/wger-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/wger-screenshot-2.webp deleted file mode 100644 index a79a7d9..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/wger-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/wger-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/wger-screenshot-3.webp deleted file mode 100644 index d8ccaac..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/wger-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/wishlist-logo.svg b/controller/mnt/user-data/outputs/felhom-controller/assets/wishlist-logo.svg deleted file mode 100644 index 6c0e923..0000000 --- a/controller/mnt/user-data/outputs/felhom-controller/assets/wishlist-logo.svg +++ /dev/null @@ -1,301 +0,0 @@ - - - - - Created by potrace 1.14, written by Peter Selinger 2001-2017 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/wishlist-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/wishlist-screenshot-1.webp deleted file mode 100644 index 9e835bb..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/wishlist-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/wishlist-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/wishlist-screenshot-2.webp deleted file mode 100644 index 89b0308..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/wishlist-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/wishlist-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/wishlist-screenshot-3.webp deleted file mode 100644 index 6f77cbe..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/wishlist-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/zipline-logo.png b/controller/mnt/user-data/outputs/felhom-controller/assets/zipline-logo.png deleted file mode 100644 index ba87994..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/zipline-logo.png and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/zipline-screenshot-1.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/zipline-screenshot-1.webp deleted file mode 100644 index 12ce827..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/zipline-screenshot-1.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/zipline-screenshot-2.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/zipline-screenshot-2.webp deleted file mode 100644 index 666d15c..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/zipline-screenshot-2.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/zipline-screenshot-3.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/zipline-screenshot-3.webp deleted file mode 100644 index 0137a2a..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/zipline-screenshot-3.webp and /dev/null differ diff --git a/controller/mnt/user-data/outputs/felhom-controller/assets/zipline-screenshot-4.webp b/controller/mnt/user-data/outputs/felhom-controller/assets/zipline-screenshot-4.webp deleted file mode 100644 index aa70afd..0000000 Binary files a/controller/mnt/user-data/outputs/felhom-controller/assets/zipline-screenshot-4.webp and /dev/null differ diff --git a/controller/router.go b/controller/router.go deleted file mode 100755 index 98ad45b..0000000 --- a/controller/router.go +++ /dev/null @@ -1,231 +0,0 @@ -package api - -import ( - "encoding/json" - "log" - "net/http" - "strconv" - "strings" - - "gitea.dooplex.hu/admin/felhom-controller/internal/config" - "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" -) - -// Router handles all /api/* requests. -type Router struct { - cfg *config.Config - stackMgr *stacks.Manager - logger *log.Logger -} - -func NewRouter(cfg *config.Config, stackMgr *stacks.Manager, logger *log.Logger) *Router { - return &Router{cfg: cfg, stackMgr: stackMgr, logger: logger} -} - -type apiResponse struct { - OK bool `json:"ok"` - Data interface{} `json:"data,omitempty"` - Error string `json:"error,omitempty"` - Message string `json:"message,omitempty"` -} - -// ServeHTTP routes /api/* requests. -func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { - path := strings.TrimPrefix(req.URL.Path, "/api") - path = strings.TrimSuffix(path, "/") - - switch { - // GET /api/stacks - case path == "/stacks" && req.Method == http.MethodGet: - r.listStacks(w, req) - - // GET /api/stacks/{name} - case strings.HasPrefix(path, "/stacks/") && req.Method == http.MethodGet && !hasSubpath(path, "/stacks/"): - r.getStack(w, req, trimSegment(path, "/stacks/")) - - // GET /api/stacks/{name}/deploy-fields - case hasSuffix(path, "/deploy-fields") && req.Method == http.MethodGet: - r.getDeployFields(w, req, extractName(path, "/deploy-fields")) - - // POST /api/stacks/{name}/deploy - case hasSuffix(path, "/deploy") && req.Method == http.MethodPost: - r.deployStack(w, req, extractName(path, "/deploy")) - - // POST /api/stacks/{name}/start - case hasSuffix(path, "/start") && req.Method == http.MethodPost: - r.actionStack(w, "start", extractName(path, "/start")) - - // POST /api/stacks/{name}/stop - case hasSuffix(path, "/stop") && req.Method == http.MethodPost: - r.actionStack(w, "stop", extractName(path, "/stop")) - - // POST /api/stacks/{name}/restart - case hasSuffix(path, "/restart") && req.Method == http.MethodPost: - r.actionStack(w, "restart", extractName(path, "/restart")) - - // POST /api/stacks/{name}/update - case hasSuffix(path, "/update") && req.Method == http.MethodPost: - r.actionStack(w, "update", extractName(path, "/update")) - - // GET /api/stacks/{name}/logs - case hasSuffix(path, "/logs") && req.Method == http.MethodGet: - r.getStackLogs(w, req, extractName(path, "/logs")) - - // GET /api/system/info - case path == "/system/info" && req.Method == http.MethodGet: - r.systemInfo(w, req) - - default: - writeJSON(w, http.StatusNotFound, apiResponse{OK: false, Error: "endpoint not found"}) - } -} - -// HealthHandler responds to /api/health (no auth required). -func (r *Router) HealthHandler(w http.ResponseWriter, req *http.Request) { - writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "felhom-controller is healthy"}) -} - -// --- Stack handlers --- - -func (r *Router) listStacks(w http.ResponseWriter, _ *http.Request) { - writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: r.stackMgr.GetStacks()}) -} - -func (r *Router) getStack(w http.ResponseWriter, _ *http.Request, name string) { - stack, ok := r.stackMgr.GetStack(name) - if !ok { - writeJSON(w, http.StatusNotFound, apiResponse{OK: false, Error: "stack not found: " + name}) - return - } - writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: stack}) -} - -func (r *Router) getDeployFields(w http.ResponseWriter, _ *http.Request, name string) { - meta, appCfg, err := r.stackMgr.GetDeployFields(name) - if err != nil { - writeJSON(w, http.StatusNotFound, apiResponse{OK: false, Error: err.Error()}) - return - } - - data := map[string]interface{}{ - "metadata": meta, - "app_config": appCfg, - "domain": r.cfg.Customer.Domain, - "logo_url": r.cfg.AppLogoURL(meta.Slug), - } - writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: data}) -} - -func (r *Router) deployStack(w http.ResponseWriter, req *http.Request, name string) { - r.logger.Printf("[API] Deploy requested for stack: %s", name) - - var body struct { - Values map[string]string `json:"values"` - } - if err := json.NewDecoder(req.Body).Decode(&body); err != nil { - writeJSON(w, http.StatusBadRequest, apiResponse{OK: false, Error: "invalid request body"}) - return - } - - deployReq := stacks.DeployRequest{ - StackName: name, - Values: body.Values, - } - - if err := r.stackMgr.DeployStack(deployReq); err != nil { - r.logger.Printf("[API] Deploy failed for %s: %v", name, err) - status := http.StatusInternalServerError - if strings.Contains(err.Error(), "already deployed") { - status = http.StatusConflict - } - if strings.Contains(err.Error(), "required field") || strings.Contains(err.Error(), "does not exist") { - status = http.StatusBadRequest - } - writeJSON(w, status, apiResponse{OK: false, Error: err.Error()}) - return - } - - writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "Stack " + name + " deployed"}) -} - -func (r *Router) actionStack(w http.ResponseWriter, action, name string) { - r.logger.Printf("[API] %s requested for stack: %s", action, name) - - var err error - switch action { - case "start": - err = r.stackMgr.StartStack(name) - case "stop": - err = r.stackMgr.StopStack(name) - case "restart": - err = r.stackMgr.RestartStack(name) - case "update": - err = r.stackMgr.UpdateStack(name) - } - - if err != nil { - status := http.StatusInternalServerError - if strings.Contains(err.Error(), "protected") { - status = http.StatusForbidden - } - if strings.Contains(err.Error(), "not found") { - status = http.StatusNotFound - } - writeJSON(w, status, apiResponse{OK: false, Error: err.Error()}) - return - } - - writeJSON(w, http.StatusOK, apiResponse{OK: true, Message: "Stack " + name + " " + action + " completed"}) -} - -func (r *Router) getStackLogs(w http.ResponseWriter, req *http.Request, name string) { - lines := 100 - if v := req.URL.Query().Get("lines"); v != "" { - if n, err := strconv.Atoi(v); err == nil && n > 0 { - lines = n - } - } - - output, err := r.stackMgr.GetLogs(name, lines) - if err != nil { - writeJSON(w, http.StatusInternalServerError, apiResponse{OK: false, Error: err.Error()}) - return - } - writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: map[string]string{"logs": output}}) -} - -func (r *Router) systemInfo(w http.ResponseWriter, _ *http.Request) { - writeJSON(w, http.StatusOK, apiResponse{OK: true, Data: map[string]interface{}{ - "customer_id": r.cfg.Customer.ID, - "customer_name": r.cfg.Customer.Name, - "domain": r.cfg.Customer.Domain, - "backup_enabled": r.cfg.Backup.Enabled, - "monitor_enabled": r.cfg.Monitoring.Enabled, - }}) -} - -// --- Helpers --- - -func hasSuffix(path, suffix string) bool { return strings.HasSuffix(path, suffix) } - -func hasSubpath(path, prefix string) bool { - rest := strings.TrimPrefix(path, prefix) - return strings.Contains(rest, "/") -} - -func trimSegment(path, prefix string) string { - return strings.TrimPrefix(path, prefix) -} - -func extractName(path, suffix string) string { - s := strings.TrimPrefix(path, "/stacks/") - return strings.TrimSuffix(s, suffix) -} - -func writeJSON(w http.ResponseWriter, status int, v interface{}) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - if err := json.NewEncoder(w).Encode(v); err != nil { - log.Printf("[ERROR] Failed to write JSON response: %v", err) - } -} diff --git a/controller/server.go b/controller/server.go deleted file mode 100755 index affdd74..0000000 --- a/controller/server.go +++ /dev/null @@ -1,405 +0,0 @@ -package web - -import ( - "crypto/rand" - "crypto/subtle" - "encoding/hex" - "fmt" - "html/template" - "log" - "net/http" - "os" - "path/filepath" - "strings" - "sync" - "time" - - "gitea.dooplex.hu/admin/felhom-controller/internal/config" - "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" - "golang.org/x/crypto/bcrypt" -) - -type Server struct { - cfg *config.Config - stackMgr *stacks.Manager - logger *log.Logger - version string - tmpl *template.Template - - sessions map[string]*session - sessionsMu sync.RWMutex -} - -type session struct { - token string - expiresAt time.Time -} - -const ( - sessionCookieName = "felhom_session" - sessionMaxAge = 24 * time.Hour -) - -func NewServer(cfg *config.Config, stackMgr *stacks.Manager, logger *log.Logger, version string) *Server { - s := &Server{ - cfg: cfg, - stackMgr: stackMgr, - logger: logger, - version: version, - sessions: make(map[string]*session), - } - s.loadTemplates() - go s.cleanupSessions() - return s -} - -func (s *Server) loadTemplates() { - funcMap := template.FuncMap{ - "stateColor": func(state stacks.ContainerState) string { - switch state { - case stacks.StateRunning: - return "green" - case stacks.StateStopped, stacks.StateExited: - return "red" - case stacks.StateRestarting: - return "yellow" - default: - return "gray" - } - }, - "stateLabel": func(state stacks.ContainerState) string { - switch state { - case stacks.StateRunning: - return "Fut" - case stacks.StateStopped, stacks.StateExited: - return "Leállítva" - case stacks.StateRestarting: - return "Újraindítás..." - case stacks.StateNotDeployed: - return "Nincs telepítve" - case stacks.StatePaused: - return "Szüneteltetve" - default: - return "Ismeretlen" - } - }, - "stateIcon": func(state stacks.ContainerState) string { - switch state { - case stacks.StateRunning: - return "●" - case stacks.StateStopped, stacks.StateExited: - return "○" - case stacks.StateRestarting: - return "◐" - default: - return "◌" - } - }, - "stateStr": func(state stacks.ContainerState) string { - return string(state) - }, - "logoURL": func(slug string) string { - return s.cfg.AppLogoURL(slug) - }, - "logoPNGURL": func(slug string) string { - return s.cfg.AppLogoPNGURL(slug) - }, - "appPageURL": func(slug string) string { - return s.cfg.AppPageURL(slug) - }, - } - - s.tmpl = template.Must(template.New("").Funcs(funcMap).Parse(allTemplates)) -} - -// ServeHTTP handles all non-API web requests. -func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - path := r.URL.Path - - switch { - case path == "/" || path == "/dashboard": - s.dashboardHandler(w, r) - case path == "/stacks": - s.stacksHandler(w, r) - case strings.HasPrefix(path, "/stacks/") && strings.HasSuffix(path, "/logs"): - name := strings.TrimPrefix(path, "/stacks/") - name = strings.TrimSuffix(name, "/logs") - s.logsHandler(w, r, name) - case strings.HasPrefix(path, "/stacks/") && strings.HasSuffix(path, "/deploy"): - name := strings.TrimPrefix(path, "/stacks/") - name = strings.TrimSuffix(name, "/deploy") - s.deployHandler(w, r, name) - case path == "/static/style.css": - w.Header().Set("Content-Type", "text/css") - w.Header().Set("Cache-Control", "public, max-age=3600") - fmt.Fprint(w, cssContent) - case strings.HasPrefix(path, "/static/assets/"): - // Serve baked-in app assets (logos, screenshots) - s.serveAsset(w, r, strings.TrimPrefix(path, "/static/assets/")) - case strings.HasPrefix(path, "/apps/"): - slug := strings.TrimPrefix(path, "/apps/") - s.appDetailHandler(w, r, slug) - default: - http.NotFound(w, r) - } -} - -// RequireAuth returns middleware that checks for valid session or shows login. -func (s *Server) RequireAuth(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // Skip auth if no password is configured - if s.cfg.Web.PasswordHash == "" { - next.ServeHTTP(w, r) - return - } - - if r.URL.Path == "/api/health" { - next.ServeHTTP(w, r) - return - } - - if r.URL.Path == "/login" && r.Method == http.MethodPost { - s.handleLogin(w, r) - return - } - if r.URL.Path == "/login" { - s.renderLogin(w, "") - return - } - if r.URL.Path == "/logout" { - s.handleLogout(w, r) - return - } - - cookie, err := r.Cookie(sessionCookieName) - if err != nil || !s.isValidSession(cookie.Value) { - if strings.HasPrefix(r.URL.Path, "/api/") { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusUnauthorized) - fmt.Fprint(w, `{"ok":false,"error":"authentication required"}`) - return - } - http.Redirect(w, r, "/login", http.StatusFound) - return - } - - next.ServeHTTP(w, r) - }) -} - -// --- Auth helpers --- - -func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { - _ = r.ParseForm() - password := r.FormValue("password") - - if password == "" { - s.renderLogin(w, "Kérjük adja meg a jelszót") - return - } - - if err := bcrypt.CompareHashAndPassword([]byte(s.cfg.Web.PasswordHash), []byte(password)); err != nil { - s.logger.Printf("[WARN] Failed login from %s", r.RemoteAddr) - s.renderLogin(w, "Hibás jelszó") - return - } - - token := s.createSession() - http.SetCookie(w, &http.Cookie{ - Name: sessionCookieName, - Value: token, - Path: "/", - MaxAge: int(sessionMaxAge.Seconds()), - HttpOnly: true, - SameSite: http.SameSiteStrictMode, - Secure: true, - }) - - s.logger.Printf("[INFO] Login from %s", r.RemoteAddr) - http.Redirect(w, r, "/", http.StatusFound) -} - -func (s *Server) handleLogout(w http.ResponseWriter, r *http.Request) { - if cookie, err := r.Cookie(sessionCookieName); err == nil { - s.sessionsMu.Lock() - delete(s.sessions, cookie.Value) - s.sessionsMu.Unlock() - } - http.SetCookie(w, &http.Cookie{Name: sessionCookieName, Value: "", Path: "/", MaxAge: -1}) - http.Redirect(w, r, "/login", http.StatusFound) -} - -func (s *Server) createSession() string { - b := make([]byte, 32) - _, _ = rand.Read(b) - token := hex.EncodeToString(b) - - s.sessionsMu.Lock() - s.sessions[token] = &session{token: token, expiresAt: time.Now().Add(sessionMaxAge)} - s.sessionsMu.Unlock() - - return token -} - -func (s *Server) isValidSession(token string) bool { - s.sessionsMu.RLock() - defer s.sessionsMu.RUnlock() - - sess, ok := s.sessions[token] - if !ok || time.Now().After(sess.expiresAt) { - return false - } - return subtle.ConstantTimeCompare([]byte(sess.token), []byte(token)) == 1 -} - -func (s *Server) cleanupSessions() { - for range time.Tick(15 * time.Minute) { - s.sessionsMu.Lock() - now := time.Now() - for t, sess := range s.sessions { - if now.After(sess.expiresAt) { - delete(s.sessions, t) - } - } - s.sessionsMu.Unlock() - } -} - -// --- Page handlers --- - -func (s *Server) baseData(page, title string) map[string]interface{} { - return map[string]interface{}{ - "Page": page, - "Title": title, - "CustomerName": s.cfg.Customer.Name, - "Domain": s.cfg.Customer.Domain, - "Version": s.version, - } -} - -func (s *Server) dashboardHandler(w http.ResponseWriter, _ *http.Request) { - stackList := s.stackMgr.GetStacks() - - running, stopped := 0, 0 - for _, st := range stackList { - switch st.State { - case stacks.StateRunning: - running++ - case stacks.StateStopped, stacks.StateExited: - stopped++ - } - } - - data := s.baseData("dashboard", "Vezérlőpult") - data["Stacks"] = stackList - data["RunningCount"] = running - data["StoppedCount"] = stopped - data["TotalCount"] = len(stackList) - - s.render(w, "dashboard", data) -} - -func (s *Server) stacksHandler(w http.ResponseWriter, _ *http.Request) { - data := s.baseData("stacks", "Alkalmazások") - data["Stacks"] = s.stackMgr.GetStacks() - s.render(w, "stacks", data) -} - -func (s *Server) logsHandler(w http.ResponseWriter, _ *http.Request, name string) { - stack, ok := s.stackMgr.GetStack(name) - if !ok { - http.NotFound(w, nil) - return - } - - logs, err := s.stackMgr.GetLogs(name, 200) - if err != nil { - logs = fmt.Sprintf("Hiba a naplók lekérésekor: %v", err) - } - - data := s.baseData("logs", stack.Meta.DisplayName+" — Naplók") - data["Stack"] = stack - data["Logs"] = logs - s.render(w, "logs", data) -} - -func (s *Server) deployHandler(w http.ResponseWriter, _ *http.Request, name string) { - meta, appCfg, err := s.stackMgr.GetDeployFields(name) - if err != nil { - http.NotFound(w, nil) - return - } - - stack, _ := s.stackMgr.GetStack(name) - - data := s.baseData("deploy", meta.DisplayName+" — Telepítés") - data["Stack"] = stack - data["Meta"] = meta - data["AppConfig"] = appCfg - data["AlreadyDeployed"] = appCfg != nil && appCfg.Deployed - data["LogoURL"] = s.cfg.AppLogoURL(meta.Slug) - data["LogoPNGURL"] = s.cfg.AppLogoPNGURL(meta.Slug) - data["AppPageURL"] = s.cfg.AppPageURL(meta.Slug) - data["UserFields"] = meta.UserFacingFields() - data["AutoFields"] = meta.AutoGeneratedFields() - - s.render(w, "deploy", data) -} - -// serveAsset serves baked-in app assets (logos, screenshots) from /usr/share/felhom/assets/ -// These are copied into the container at build time. -const assetsDir = "/usr/share/felhom/assets" - -func (s *Server) serveAsset(w http.ResponseWriter, r *http.Request, filename string) { - // Sanitize: prevent directory traversal - filename = filepath.Base(filename) - path := filepath.Join(assetsDir, filename) - - if _, err := os.Stat(path); os.IsNotExist(err) { - http.NotFound(w, r) - return - } - - w.Header().Set("Cache-Control", "public, max-age=86400") - http.ServeFile(w, r, path) -} - -// appDetailHandler serves a local app detail page (description, screenshots, FAQ). -// TODO: Phase 1.5 — for now, redirect to the stacks page. -// Future: render a dedicated app page template with baked-in content. -func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug string) { - // Find the stack by slug - for _, stack := range s.stackMgr.GetStacks() { - if stack.Meta.Slug == slug { - // For now, redirect to deploy page (if not deployed) or stacks page - if !stack.Deployed { - http.Redirect(w, r, "/stacks/"+stack.Name+"/deploy", http.StatusFound) - } else { - http.Redirect(w, r, "/stacks", http.StatusFound) - } - return - } - } - http.NotFound(w, r) -} - -func (s *Server) renderLogin(w http.ResponseWriter, errorMsg string) { - data := map[string]interface{}{ - "Title": "Bejelentkezés", - "CustomerName": s.cfg.Customer.Name, - "Error": errorMsg, - } - w.Header().Set("Content-Type", "text/html; charset=utf-8") - if err := s.tmpl.ExecuteTemplate(w, "login", data); err != nil { - s.logger.Printf("[ERROR] Template error (login): %v", err) - http.Error(w, "Internal error", http.StatusInternalServerError) - } -} - -func (s *Server) render(w http.ResponseWriter, name string, data interface{}) { - w.Header().Set("Content-Type", "text/html; charset=utf-8") - if err := s.tmpl.ExecuteTemplate(w, name, data); err != nil { - s.logger.Printf("[ERROR] Template error (%s): %v", name, err) - http.Error(w, "Internal error", http.StatusInternalServerError) - } -} diff --git a/controller/templates.go b/controller/templates.go deleted file mode 100755 index d93a0f8..0000000 --- a/controller/templates.go +++ /dev/null @@ -1,540 +0,0 @@ -package web - -// All HTML templates and CSS are embedded as Go strings. -// Compiled into the binary — zero external file dependencies at runtime. -// As the UI grows, switch to go:embed for easier editing. - -const allTemplates = layoutTmpl + dashboardTmpl + stacksTmpl + loginTmpl + logsTmpl + deployTmpl - -const layoutTmpl = ` -{{define "layout_start"}} - - - - - - {{.Title}} — Felhom - - - - -
-{{end}} - -{{define "layout_end"}} -
- - - -{{end}} -` - -const dashboardTmpl = ` -{{define "dashboard"}} -{{template "layout_start" .}} - - - -
-
-
{{.RunningCount}}
-
Futó alkalmazás
-
-
-
{{.StoppedCount}}
-
Leállítva
-
-
-
{{.TotalCount}}
-
Összes alkalmazás
-
-
- -

Alkalmazások állapota

- -
- {{range .Stacks}} -
-
- -
- {{.Meta.DisplayName}} - {{if .Meta.Description}}{{.Meta.Description}}{{end}} -
-
-
- {{stateLabel .State}} - - {{if .Protected}} - 🔒 Védett - {{else if not .Deployed}} - 🚀 Telepítés - {{else}} - {{if eq (stateStr .State) "running"}} - - - {{else}} - - {{end}} - 📋 - {{end}} -
-
- {{else}} -
-

Nincs elérhető alkalmazás.

-
- {{end}} -
- -{{template "layout_end" .}} -{{end}} -` - -const stacksTmpl = ` -{{define "stacks"}} -{{template "layout_start" .}} - - - -
- {{range .Stacks}} -
-
-
- -
-

{{.Meta.DisplayName}}

- {{if .Meta.Subdomain}} - - {{.Meta.Subdomain}}.{{$.Domain}} ↗ - - {{end}} -
-
- {{stateLabel .State}} -
- - {{if .Meta.Description}} -

{{.Meta.Description}}

- {{end}} - -
- {{if .Meta.Resources.RAM}}💾 {{.Meta.Resources.RAM}}{{end}} - {{if .Meta.Resources.PiCompatible}}🥧 Pi kompatibilis{{end}} - {{if .Meta.Resources.NeedsHDD}}💿 HDD szükséges{{end}} -
- - {{if .Containers}} -
- {{range .Containers}} -
- {{.Name}} - {{.Status}} -
- {{end}} -
- {{end}} - -
- {{if .Protected}} - 🔒 Védett rendszerkomponens - {{else if not .Deployed}} - 🚀 Telepítés - ℹ️ Részletek - {{else}} - {{if eq (stateStr .State) "running"}} - - - - {{else}} - - {{end}} - 📋 Naplók - ℹ️ Részletek - {{end}} -
-
- {{end}} -
- -{{template "layout_end" .}} -{{end}} -` - -const deployTmpl = ` -{{define "deploy"}} -{{template "layout_start" .}} - - - -
-
- -
-

{{.Meta.DisplayName}}

- {{if .Meta.Description}}

{{.Meta.Description}}

{{end}} -
- {{if .Meta.Resources.RAM}}💾 {{.Meta.Resources.RAM}}{{end}} - {{if .Meta.Resources.PiCompatible}}🥧 Pi kompatibilis{{end}} - {{if .Meta.Resources.NeedsHDD}}💿 HDD szükséges{{end}} -
- - ℹ️ Részletes leírás, képernyőképek - -
-
- - {{if .AlreadyDeployed}} -
- Ez az alkalmazás már telepítve van. Az alábbi beállítások csak olvashatók. -
- {{end}} - -
- {{if .AutoFields}} -
-

🔒 Automatikusan generált értékek

-

Ezek az értékek automatikusan létrejönnek a telepítéskor.

- {{range .AutoFields}} -
- - ✓ Automatikusan generálva -
- {{end}} -
- {{end}} - - {{if .UserFields}} -
-

⚙️ Beállítások

- {{range .UserFields}} -
- - - {{if eq .Type "select"}} - - {{else if eq .Type "password"}} -
- - -
- {{else if eq .Type "boolean"}} - - {{else}} - - {{end}} - - {{if .Description}} - {{.Description}} - {{end}} -
- {{end}} -
- {{end}} - - {{if not .AlreadyDeployed}} -
- - Mégsem -
- {{end}} -
-
- - - -{{template "layout_end" .}} -{{end}} -` - -const loginTmpl = ` -{{define "login"}} - - - - - - Bejelentkezés — Felhom - - - - - - -{{end}} -` - -const logsTmpl = ` -{{define "logs"}} -{{template "layout_start" .}} - -
-
{{.Logs}}
-
-
- -
-{{template "layout_end" .}} -{{end}} -` - -// CSS is defined in a separate const for readability. -// Served at /static/style.css -const cssContent = ` -:root { - --bg:#f8f9fa; --sidebar-bg:#1a1f36; --sidebar-text:#e2e8f0; - --card-bg:#fff; --text:#1a202c; --text-muted:#718096; --border:#e2e8f0; - --green:#38a169; --green-light:#c6f6d5; --red:#e53e3e; --red-light:#fed7d7; - --yellow:#d69e2e; --yellow-light:#fefcbf; --blue:#3182ce; --blue-light:#bee3f8; - --gray:#a0aec0; --gray-light:#edf2f7; --radius:8px; --shadow:0 1px 3px rgba(0,0,0,.1); -} -*{margin:0;padding:0;box-sizing:border-box} -body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--text);display:flex;min-height:100vh} - -.sidebar{width:240px;background:var(--sidebar-bg);color:var(--sidebar-text);display:flex;flex-direction:column;position:fixed;height:100vh;overflow-y:auto} -.sidebar-header{padding:1.5rem;border-bottom:1px solid rgba(255,255,255,.1)} -.logo{font-size:1.5rem;font-weight:700;color:#fff} -.customer-name{display:block;font-size:.85rem;color:var(--gray);margin-top:.25rem} -.nav-links{list-style:none;padding:1rem 0;flex:1} -.nav-links a{display:block;padding:.75rem 1.5rem;color:var(--sidebar-text);text-decoration:none;font-size:.95rem;transition:background .15s} -.nav-links a:hover{background:rgba(255,255,255,.08)} -.nav-links a.active{background:rgba(255,255,255,.12);border-left:3px solid var(--blue)} -.sidebar-footer{padding:1rem 1.5rem;border-top:1px solid rgba(255,255,255,.1);display:flex;justify-content:space-between;align-items:center;font-size:.8rem} -.version{color:var(--gray)} .logout-link{color:var(--gray);text-decoration:none} .logout-link:hover{color:#fff} - -.content{margin-left:240px;padding:2rem;flex:1;max-width:1200px} -.page-header{display:flex;align-items:center;gap:1rem;margin-bottom:1.5rem} -.page-header h2{font-size:1.5rem;font-weight:600} -.domain-badge{background:var(--blue-light);color:var(--blue);padding:.25rem .75rem;border-radius:999px;font-size:.8rem;font-weight:500} - -.stats-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:1rem;margin-bottom:2rem} -.stat-card{background:var(--card-bg);border-radius:var(--radius);padding:1.25rem;box-shadow:var(--shadow);border-left:4px solid var(--gray)} -.stat-running{border-left-color:var(--green)} .stat-stopped{border-left-color:var(--red)} .stat-total{border-left-color:var(--blue)} -.stat-value{font-size:2rem;font-weight:700} .stat-label{color:var(--text-muted);font-size:.85rem;margin-top:.25rem} - -.stack-list{display:flex;flex-direction:column;gap:.5rem} -.stack-card{background:var(--card-bg);border-radius:var(--radius);padding:1rem 1.25rem;box-shadow:var(--shadow);display:flex;justify-content:space-between;align-items:center;border-left:4px solid var(--gray)} -.stack-state-green{border-left-color:var(--green)} .stack-state-red{border-left-color:var(--red)} .stack-state-yellow{border-left-color:var(--yellow)} .stack-state-gray{border-left-color:var(--gray)} -.stack-info{display:flex;align-items:center;gap:.75rem} -.stack-logo{width:32px;height:32px;border-radius:6px;object-fit:contain;background:#1c2128;padding:4px} -.stack-logo-lg{width:48px;height:48px;border-radius:8px;object-fit:contain;background:#1c2128;padding:6px} -.stack-name{font-size:1rem} .stack-desc{display:block;font-size:.8rem;color:var(--text-muted)} -.stack-actions{display:flex;align-items:center;gap:.5rem} .stack-state-label{font-size:.8rem;color:var(--text-muted);margin-right:.5rem} - -.stack-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(350px,1fr));gap:1rem} -.stack-detail-card{background:var(--card-bg);border-radius:var(--radius);padding:1.25rem;box-shadow:var(--shadow);border-top:4px solid var(--gray)} -.stack-detail-card.stack-state-green{border-top-color:var(--green)} .stack-detail-card.stack-state-red{border-top-color:var(--red)} -.stack-detail-header{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:.75rem} -.stack-title-row{display:flex;align-items:center;gap:.75rem} -.subdomain-link{font-size:.8rem;color:var(--blue);text-decoration:none} .subdomain-link:hover{text-decoration:underline} -.stack-state-badge{padding:.2rem .6rem;border-radius:999px;font-size:.75rem;font-weight:600;white-space:nowrap} -.state-green{background:var(--green-light);color:var(--green)} .state-red{background:var(--red-light);color:var(--red)} -.state-yellow{background:var(--yellow-light);color:var(--yellow)} .state-gray{background:var(--gray-light);color:var(--gray)} -.state-text-green{color:var(--green)} .state-text-red{color:var(--red)} -.stack-detail-desc{color:var(--text-muted);font-size:.85rem;margin-bottom:.75rem} -.stack-meta-badges{display:flex;flex-wrap:wrap;gap:.4rem;margin:.5rem 0} -.meta-badge{background:var(--gray-light);color:var(--text-muted);padding:.15rem .5rem;border-radius:6px;font-size:.75rem} -.meta-badge-ok{background:var(--green-light);color:var(--green)} -.container-list{margin:.75rem 0} .container-list h4{font-size:.8rem;color:var(--text-muted);margin-bottom:.4rem} -.container-row{display:flex;justify-content:space-between;font-size:.8rem;padding:.2rem 0} -.container-name{font-family:monospace} .container-status{font-size:.75rem} -.stack-detail-actions{display:flex;gap:.5rem;margin-top:1rem;flex-wrap:wrap} - -.btn{display:inline-flex;align-items:center;gap:.3rem;padding:.5rem 1rem;border:none;border-radius:6px;font-size:.85rem;font-weight:500;cursor:pointer;transition:opacity .15s,transform .1s;text-decoration:none;color:#fff} -.btn:hover{opacity:.9} .btn:active{transform:scale(.97)} .btn:disabled{opacity:.5;cursor:not-allowed} .btn.loading{opacity:.6} -.btn-sm{padding:.3rem .6rem;font-size:.8rem} .btn-lg{padding:.65rem 1.5rem;font-size:1rem} .btn-full{width:100%;justify-content:center} -.btn-primary{background:var(--blue)} .btn-success{background:var(--green)} .btn-warning{background:var(--yellow);color:#1a202c} .btn-danger{background:var(--red)} -.btn-outline{background:transparent;border:1px solid var(--border);color:var(--text)} .btn-outline:hover{background:var(--gray-light)} -.badge{display:inline-flex;align-items:center;gap:.25rem;padding:.2rem .6rem;border-radius:999px;font-size:.75rem;font-weight:500} -.badge-protected{background:var(--gray-light);color:var(--text-muted)} - -/* Deploy page */ -.deploy-container{max-width:700px} -.deploy-info{display:flex;gap:1rem;align-items:flex-start;background:var(--card-bg);padding:1.25rem;border-radius:var(--radius);box-shadow:var(--shadow);margin-bottom:1.5rem} -.deploy-logo{width:64px;height:64px;border-radius:12px;object-fit:contain;background:#1c2128;padding:8px;flex-shrink:0} -.deploy-info h3{font-size:1.2rem;margin-bottom:.25rem} -.deploy-info p{color:var(--text-muted);font-size:.9rem} -.deploy-form{background:var(--card-bg);padding:1.5rem;border-radius:var(--radius);box-shadow:var(--shadow)} -.form-section{margin-bottom:1.5rem} -.form-section h4{font-size:1rem;margin-bottom:.5rem} -.form-section-desc{color:var(--text-muted);font-size:.85rem;margin-bottom:.75rem} -.form-group{margin-bottom:1rem} -.form-group label{display:block;font-size:.85rem;font-weight:500;margin-bottom:.4rem} -.form-group-auto{display:flex;justify-content:space-between;align-items:center;padding:.5rem .75rem;background:var(--gray-light);border-radius:6px} -.form-group-auto label{margin:0} -.auto-generated-badge{color:var(--green);font-size:.8rem;font-weight:500} -.form-control{width:100%;padding:.55rem .75rem;border:1px solid var(--border);border-radius:6px;font-size:.9rem;background:#fff} -.form-control:focus{outline:none;border-color:var(--blue);box-shadow:0 0 0 3px rgba(49,130,206,.1)} -.form-control:disabled{background:var(--gray-light);cursor:not-allowed} -.input-with-button{display:flex;gap:.5rem} -.input-with-button .form-control{flex:1} -.form-hint{display:block;font-size:.8rem;color:var(--text-muted);margin-top:.25rem} -.required{color:var(--red)} .locked-hint{font-size:.75rem;color:var(--text-muted);font-weight:400;margin-left:.5rem} -.deploy-actions{display:flex;gap:.75rem;margin-top:1.5rem;padding-top:1rem;border-top:1px solid var(--border)} - -.alert{padding:.75rem;border-radius:6px;margin-bottom:1rem;font-size:.85rem} -.alert-error{background:var(--red-light);color:var(--red)} .alert-info{background:var(--blue-light);color:var(--blue)} - -/* Logs */ -.logs-container{background:#1a1f36;border-radius:var(--radius);padding:1rem;overflow-x:auto;margin-bottom:1rem} -.logs-output{color:#e2e8f0;font-family:'JetBrains Mono','Fira Code',monospace;font-size:.8rem;line-height:1.5;white-space:pre-wrap;word-break:break-all} -.logs-actions{display:flex;gap:.5rem} - -.empty-state{text-align:center;padding:3rem;color:var(--text-muted)} -.login-body{display:flex;justify-content:center;align-items:center;min-height:100vh;background:linear-gradient(135deg,#1a1f36,#2d3748)} -.login-card{background:var(--card-bg);padding:2.5rem;border-radius:12px;box-shadow:0 4px 24px rgba(0,0,0,.15);width:100%;max-width:380px;text-align:center} -.login-card .logo{color:var(--text);margin-bottom:.25rem} .login-subtitle{color:var(--text-muted);margin-bottom:1.5rem} -.login-footer{margin-top:1.5rem;font-size:.75rem;color:var(--text-muted)} .login-footer a{color:var(--blue);text-decoration:none} - -@media(max-width:768px){ - .sidebar{width:100%;height:auto;position:relative} - .nav-links{display:flex;padding:0;overflow-x:auto} .nav-links a{padding:.5rem 1rem;white-space:nowrap} - .content{margin-left:0;padding:1rem} body{flex-direction:column} - .stack-card{flex-direction:column;align-items:flex-start;gap:.75rem} .stack-actions{width:100%;justify-content:flex-end} - .stack-grid{grid-template-columns:1fr} .stats-grid{grid-template-columns:repeat(3,1fr)} - .deploy-info{flex-direction:column} -} -`