Commit Graph

23 Commits

Author SHA1 Message Date
alexei.dolgolyov 97f338fba3 feat(maintenance): add Docker build-cache prune action
Add an admin-only POST /api/docker/prune-build-cache endpoint plus a Settings > Maintenance danger-zone button to reclaim disk used by the Docker build cache (image + static-site builds), which previously grew unbounded with no UI lever. Prunes unused-only (all=false) so a warm cache is preserved for apps redeploying soon. Mirrors the existing prune-images vertical slice; full en/ru i18n parity.
2026-06-02 13:34:05 +03:00
alexei.dolgolyov 410a131cec feat(apps): stepped creation wizard, branch previews, and app-creation fixes
This session (frontend focus):
- Rebuild /apps/new as a 4-step wizard (Basics → Configure → Trigger → Review):
  WizardRail, SourceKindPicker card grid, AppManifest review, per-step validation,
  ConfirmDialog-based unsaved-changes guard.
- Extract lib/workload/sourceForms.ts (single source of truth for source_config)
  + {Image,Compose,Static,Dockerfile}SourceForm + StaticDiscoveryWizard; fold the
  /apps/[id] edit form onto the same components (removes the duplication). Add
  vitest + sourceForms unit tests.
- Branch preview environments UI: /chain is_preview/preview_branch + a Preview
  environments panel on /apps/[id] (per-branch URLs, ConfirmDialog teardown, armed
  state); RegistryImagePicker on the registry trigger and the image source.
- Fixes: image-inspect 404 -> admin-gated POST /api/discovery/image/inspect;
  conflict-panel blur flicker; friendly localized discovery errors; CPU/Memory
  label hints; dashboard + /apps "Total workloads" count only source_kind workloads
  (drop stale trigger_kind gate); NPM cert/access-list name cache; EntityPicker
  empty-list guard.
- Update CLAUDE.md frontend conventions + add a Build & Test section.

Also captures pre-existing in-progress platform work (not from this session):
workload notifications, Prometheus metrics export, store lockfile, health probes,
backup hardening, and related store/webhook/scheduler changes.
2026-05-29 02:09:54 +03:00
alexei.dolgolyov 7a9ff7ad54 feat(observability): event triggers + log scanner backend
Two paired backends sharing the events.Bus seam:

Event triggers (consumer-side):
- internal/store/event_triggers.go — CRUD with action_secret
  redaction on read (placeholder echo treated as "no change" on
  PATCH so secrets aren't accidentally wiped).
- internal/events/dispatcher.go — bus subscriber, AND-composed
  filters (severity CSV, source CSV, message regex with memoized
  compile cache). Structural loop-prevention: never writes to
  event_log. Sends via notifier.SendPayload.
- internal/notify: SendPayload + SendSyncForTestPayload methods,
  TierEventTrigger constant, doSendRaw shared with the legacy
  Event-shaped path.
- internal/api/event_triggers.go — admin-gated CRUD + /test
  sending the real TriggerWebhookPayload shape. SSRF guard
  rejects loopback / link-local / unspecified targets. PATCH
  uses pointer-typed DTO for partial updates.

Log scanner (producer-side):
- internal/logscanner/ — engine (per-rule cooldown +
  per-container token bucket, atomic drop counters), tail
  (multiplexed docker frame demuxer with TTY fallback + 16 MiB
  payload cap + 1 MiB reassembly cap + RFC3339Nano-validated
  timestamp strip + UTF-8-safe message truncation), manager
  (5s container polling, atomic.Pointer[Snapshot] hot-reload,
  HitEmitter writes event_log + publishes EventLog so the
  trigger dispatcher picks them up immediately).
- internal/docker/container.go — ContainerLogsOpts exposes
  stream selection for stderr-only / stdout-only rules.
- internal/store: log_scan_rules table + CRUD with
  EffectiveLogScanRules resolver (globals minus per-workload
  overrides plus workload-only additions). Transactional
  cascade-delete of overrides when a global rule is removed.
- internal/api/log_scan_rules.go — admin-gated CRUD + /test
  (sample_line → matched/captures) + /stats (drop counters +
  active tail count + last-snapshot compile errors) +
  GET /api/workloads/{id}/effective-rules.

cmd/server/main.go wires both subsystems next to the existing
RegisterPersistentLogger. Coverage spans engine cooldown / bucket
counter tests, snapshot effective-set semantics, manager compile-
error capture, dispatcher matching, store validation +
cascade-delete, API URL validator + secret redaction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 22:18:11 +03:00
alexei.dolgolyov d8ab22876f refactor(workload): extract Instance entirely; Container is canonical
Build / build (push) Successful in 10m41s
End-to-end extraction of the Instance concept. After this commit:

  * internal/store/instances.go — DELETED
  * internal/store/models.go — Instance struct gone, ProxyRoute moved here
  * containers table is the single source of truth for project/stack/site
    container state. instances table is dropped via DROP TABLE migration
    (idempotent; re-runnable on every boot).
  * Legacy tinyforge.project / tinyforge.stage / tinyforge.instance-id
    Docker labels are no longer emitted; only tinyforge.workload.{id,kind},
    tinyforge.role, and tinyforge.managed are stamped on new containers.

Backend rewrites:
  - internal/deployer:        executeDeploy + blueGreenDeploy + rollback +
                              promote use store.Container natively. New
                              removeContainer() replaces removeInstance().
                              enforceMaxInstances reads via
                              ListContainersByStageID.
  - internal/reconciler:      legacy tinyforge.instance-id dispatch removed;
                              upsertByWorkloadLabel now finds existing rows
                              by docker container ID first and falls back to
                              the deterministic workloadID:role key.
  - internal/stale/scanner:   Scan + new FindStaleContainers walk the
                              containers table; emit StaleContainer JSON.
  - internal/stats/collector: ListContainers replaces ListAllInstances.
  - internal/webhook/handler: workload-secret lookup tried first; falls back
                              to project / static_site secret column.
  - internal/api: instances.go, stale.go, stats.go, stats_history.go,
                  projects.go, settings.go, docker.go, dns.go all read /
                  write through Container.

Docker layer:
  - ManagedContainer exposes WorkloadID/Kind/Role from the canonical labels.
  - ListContainers filters by tinyforge.managed=true.
  - Network creation uses LabelManaged instead of LabelProject.

Frontend:
  - Instance type is now a Container alias; .status → .state,
    .last_alive_at → .last_seen_at.
  - InstanceCard takes stageId as a prop (no longer derived from Instance).
  - StaleContainer JSON shape rewritten: { container, workload_name, role,
    days_stale }. StaleContainerCard + /containers/stale page updated.
  - ProjectCard / homepage / SystemHealthCard filter by .state.

The migration loop now tolerates "no such table" alongside "duplicate
column" / "already exists" so obsolete ALTER TABLE entries targeting the
dropped instances table no-op cleanly on first boot.

Tests: store + deployer + reconciler + webhook + staticsite + notify all
still pass. Frontend svelte-check: zero errors.
2026-05-09 14:43:12 +03:00
alexei.dolgolyov af82be3fb8 feat(workload): container index reconciler
Background worker that keeps the containers table in sync with
docker ps. Runs one boot pass and ticks every 30s.

Dispatch precedence per container:
  1. tinyforge.workload.id label   (canonical, new)
  2. tinyforge.instance-id label   (legacy project — joins via instances)
  3. tinyforge.static-site label   (legacy site)
  4. com.docker.compose.project    (stacks — joins via ComposeProjectName)

Rows whose Docker container ID is no longer present are flipped
to state='missing'. Placeholder rows (empty container_id, e.g.
a deploy mid-flight) are left alone so a tick that races a
deploy doesn't mark them as missing.

DockerLister interface lets tests substitute a fake daemon —
6 unit tests cover the dispatch matrix, missing-sweep, and
state normalization.

Wired into cmd/server/main.go between docker.New and the
existing startup chain. Boot pass populates the containers
table from any pre-refactor running containers.
2026-05-09 13:45:13 +03:00
alexei.dolgolyov abb1da903f feat(workload): emit workload labels + dual-write containers from deployer
Project deploys (both standard and blue-green) now stamp the new
workload labels on every container and dual-write a row into the
containers index alongside the existing instances row. The legacy
project/stage/instance-id labels stay for now so operator runbooks
don't break — they will be removed after the migration soaks.

New labels:
- tinyforge.managed       (every Tinyforge container)
- tinyforge.workload.id   (workload row primary key)
- tinyforge.workload.kind ('project' | 'stack' | 'site')
- tinyforge.role          (stage name for projects)

ContainerConfig grows WorkloadID/WorkloadKind/Role fields. The
deployer resolves the project's workload row (guaranteed to exist
by boot-time backfill) and passes the IDs through. Container row
ID matches instance ID by construction so removeInstance can drop
both records together.

Stack and static-site managers still need the same treatment;
those land in the next commit.
2026-05-09 13:37:19 +03:00
alexei.dolgolyov a4362b842d fix: harden security, fix concurrency bugs, and address review findings
Build / build (push) Successful in 11m42s
Security:
- rate limit /api/webhook routes per-IP and cap concurrent site syncs
- global SSE connection cap (256) with new sse_gate
- validate ?tail= and cap JSON log responses at 4 MiB
- strip ANSI/CSI/OSC and control bytes from streamed log lines
- redact webhook secret from request log middleware
- scrub host details from /api/health for non-admin viewers
- drop container_id from /api/system/stats/top for non-admins
- generate webhook secrets via crypto/rand; require >=32 chars on insert
- verify iid path consistency in streamContainerLogs
- LimitReader on site webhook body; reject malformed non-empty bodies

Concurrency / correctness:
- stats collector: Stop() no longer hangs without Start(), semaphore
  acquired in parent loop so ctx cancellation short-circuits the queue,
  in-flight tick cancellable via shared base context, zero-ts guard
- webhook handler: replace fire-and-forget goroutine with WaitGroup-tracked
  workers + Drain() wired into graceful shutdown
- $derived(() => ...) mis-idiom fixed in ContainerStats / InstanceCard /
  ProjectCard (returned function instead of value)
- SystemResourcesCard: rename `window` and `t` locals to avoid shadowing
  globalThis.window and the i18n `t` import

Quality / performance:
- replace O(n^2) insertion sort with sort.Slice in stats top
- runMigrations only swallows duplicate-column / already-exists errors
- PruneStatsSamplesBefore wrapped in a transaction
- collapse N+1 in unusedImageStats / pruneImages to one ListAllInstances
  pass; surface DB errors instead of silently treating them as inactive
- run Docker Info + DiskUsage in parallel via errgroup
- container log SSE emits `: ping` heartbeat every 20 s
- imageMatches case-insensitive on registry host (RFC behaviour)
- log warning on invalid stage tag pattern instead of silent skip
- reject malformed non-empty site webhook payloads

Frontend / i18n:
- shared formatBytes utility replaces three local copies
- statsInterval store drives dynamic "no samples / collection disabled"
  copy across ContainerStats and SystemResourcesCard
- top consumers row now shows owner_name (project/stage or site name)
- drop seven `as any` casts on the Settings type; add cloudflare_api_token
  write-only field
- move "Service status", "Docker daemon", "Docker unreachable",
  "Proxy unreachable", "reachable", and "Docker daemon is not reachable."
  strings into en/ru i18n bundles
2026-05-07 00:56:14 +03:00
alexei.dolgolyov 05440a5f92 feat(stats): resource metrics dashboard + sites logs/stats
Build / build (push) Successful in 10m50s
Background collector samples CPU/memory/network/block I/O for every
instance and site on a configurable interval (default 15s, range
5-300s), persists samples to SQLite with a configurable retention
window (default 2h, range 0-24h), and skips ticks gracefully when
the Docker daemon is unreachable. Settings are reloadable without
a restart — each tick re-reads them.

New API endpoints:
- GET /api/system/stats (host snapshot: info + df)
- GET /api/system/stats/history
- GET /api/system/stats/top?by=cpu|memory
- GET /api/projects/{id}/stages/{s}/instances/{iid}/stats/history
- GET /api/sites/{id}/stats[/history]
- GET /api/sites/{id}/logs (SSE + JSON, reuses instance log streamer)

Frontend:
- ECharts added with tree-shaken imports (~180KB gzip) for
  future-proof time-series/gantt/graph visualizations
- CollapsibleSection wraps all dashboard sections (system health,
  daemons, system resources, static sites, projects) with
  localStorage-persisted open state
- SystemResourcesCard shows capacity tiles, workload utilization
  chart with 30m/2h/6h/24h window picker, disk breakdown with
  reclaimable callouts, and top 5 consumers
- ContainerStats and ContainerLogs take a source discriminated union
  so sites reuse the same components as instances; sites detail page
  embeds both for Deno backend debugging
- Settings › Maintenance exposes collection interval + retention
- Docker-unavailable state returns 503 and renders an amber banner
  instead of a generic 500

Full i18n coverage (en + ru) for all new strings.
2026-04-24 15:02:43 +03:00
alexei.dolgolyov 90e6e59d9e feat: daemon health panel, brand-rail status chips, user timezone selector
Build / build (push) Successful in 10m35s
- Health API now surfaces Docker /info + /version (version, platform,
  kernel, container/image counts, storage driver, memory, latency) and
  NPM aggregates (proxy host total, managed-by-Tinyforge count, access
  lists, certificates, endpoint URL).
- Docker/NPM indicators moved out of the sidebar footer and into a
  compact mono-styled rail directly under the Tinyforge brand title,
  with pulse/fault animations and click-to-expand error hints.
- New SystemDaemonsCard on the dashboard: two terminal-styled panels
  (Docker Engine + Proxy) with a running/paused/stopped stacked bar,
  key-value diagnostics, and a total-vs-managed proportion meter on
  the proxy-hosts tile.
- Shared health store so the sidebar and dashboard share a single
  30 s poll instead of duplicating traffic.
- User-facing timezone preference with auto-detect fallback; all
  dates across projects, sites, stacks, settings, backup, event log
  and stale containers now render through \$fmt.date / \$fmt.datetime.
- en/ru translations for both features.
2026-04-23 14:32:30 +03:00
alexei.dolgolyov b622384774 feat: persistent storage for Deno static sites
Build / build (push) Successful in 10m21s
- Add storage_enabled and storage_limit_mb columns to static_sites.
- Create/attach Docker volumes (tinyforge-site-{name}-data) for Deno
  sites with storage enabled, mounted at /app/data.
- Grant --allow-write=/app/data in Deno container CMD.
- Add storage usage API endpoint (GET /api/sites/{id}/storage).
- Show storage section in site detail page with usage bar.
- Add storage toggle and limit field to new site wizard.
- Use ConfirmDialog for secret deletion instead of inline delete.
2026-04-13 00:12:51 +03:00
alexei.dolgolyov 791cd4d6af feat: rename Docker Watcher to Tinyforge
Build / build (push) Successful in 12m20s
Rebrand the project as Tinyforge to reflect its evolution from a Docker
container watcher into a self-hosted mini CI/deployment platform.

Rename covers: Go module path, Docker labels, DB/config filenames,
JWT issuer, Dockerfile binary, docker-compose, CI workflows, frontend
i18n, README with static sites docs, and all code comments.
2026-04-12 21:30:39 +03:00
alexei.dolgolyov 8d2c5a063b feat: static sites feature with Gitea/GitHub/GitLab support and Deno backend
Deploy static content from Git repository folders with optional server-side
API endpoints. Supports Gitea/Forgejo/Gogs, GitHub, and GitLab with provider
autodetection.

- New Sites entity with CRUD, encrypted secrets, and manual/push/tag sync triggers
- Pluggable GitProvider interface with three implementations
- Deno container mode: auto-generates router from API_{method}_{name} exports
- Static container mode: nginx serving files with optional markdown rendering
- Wizard UI with provider selector, repo picker, branch/folder tree pickers
- Deploy pipeline builds fresh image, starts container, configures NPM proxy
- Stop/Start buttons, force redeploy on manual trigger
- Periodic health checker detects crashed containers
- Proxy route existence check during auto-sync
2026-04-11 03:35:57 +03:00
alexei.dolgolyov d03cc3c811 feat: container logs viewer with SSE streaming and line limiter
- Add GET /api/projects/{id}/stages/{stage}/instances/{iid}/logs endpoint
- Supports JSON mode (returns array of lines) and SSE mode (streams in real-time)
- Docker log stream header (8-byte prefix) stripped automatically
- ContainerLogs component with:
  - Tail line selector (50/200/500/1000)
  - Follow button for real-time streaming via SSE
  - Auto-scroll to bottom
  - Dark terminal-style display
  - Close button
- Logs button (events icon) on each instance card
- i18n keys in EN and RU
2026-04-05 14:04:45 +03:00
alexei.dolgolyov ac3132d172 feat: show local Docker images on project detail page
- Add GET /api/projects/{id}/images endpoint returning local images matching the project
- Add ListImagesByRef with tag, size, and created timestamp to Docker client
- Display images table on project page with tag, ID (truncated), size (MB), and created date
- Only shown when Docker is available and images exist locally
2026-04-05 13:56:55 +03:00
alexei.dolgolyov 5577851f22 feat: project-scoped Docker image prune, conflict fix, deploy toggle, access list picker
- Image prune only removes images matching project image refs, skips active instances
- Add ListImagesByRef and RemoveImage to Docker client
- Fix 409 conflict: use listProjects instead of duplicate POST
- Add "Deploy immediately" toggle to Quick Deploy (off by default)
- Replace raw access list ID with EntityPicker on project edit form
- Trigger proxy resync on access list change
- Fix stage form layout: single responsive row
- Fix empty port default on project creation
- Improve inspect error message for remote Docker
2026-04-05 13:49:20 +03:00
alexei.dolgolyov 7550fe9e32 feat: CPU/RAM limits per stage, NPM access list (global + per-project)
Resource limits:
- Add cpu_limit (cores) and memory_limit (MB) fields to Stage model
- Pass limits to Docker container via NanoCPUs and Memory in HostConfig
- Add CPU/Memory fields to stage creation form in project detail
- 0 = unlimited (default)

NPM access list:
- Add npm_access_list_id to Settings (global default) and Project (per-project override)
- Per-project overrides global when > 0
- NPM provider passes access_list_id when configuring proxy hosts
- Add GET /api/settings/npm-access-lists endpoint to list NPM access lists
- Add access list picker on NPM settings page (global)
- Add access list ID field on project edit form (per-project)
- DB migrations for all new columns
2026-04-05 12:44:26 +03:00
alexei.dolgolyov 4ff8daafc4 fix: reconcile instance status with Docker on list, add IsContainerRunning 2026-04-05 02:42:31 +03:00
alexei.dolgolyov 1a8dfefa77 feat: Docker diagnostic hints on disconnection
- Classify Docker errors into categories (socket_not_found, connection_refused,
  permission_denied, timeout, tls_error) with platform-specific hints
- Enrich GET /api/health with structured diagnostics (category, hints, platform)
- Expandable hints panel in sidebar when Docker is disconnected
- "Retry now" button for immediate re-check
- Collapsible raw error details for advanced users
2026-03-30 14:05:00 +03:00
alexei.dolgolyov 7c57c740b4 feat(observability): phase 8 - container stats, notifications & dashboard
Add container monitoring and notification system:
- Docker Stats API: real-time CPU/memory for running containers
- Webhook notifications for errors (deploy failures, stale, proxy unhealthy)
- Event log auto-pruning (daily, 30-day retention)
- ContainerStats component with auto-polling progress bars
- SystemHealthCard dashboard widget with running/proxy/error counts
- Full EN/RU i18n for stats and system health
2026-03-30 11:37:25 +03:00
alexei.dolgolyov be6ad15efc fix: comprehensive security, performance, and quality hardening
Security: apply AdminOnly middleware to mutating routes, require
ENCRYPTION_KEY and ADMIN_PASSWORD (no insecure defaults), restrict
CORS to same-origin, fix OIDC token delivery via cookie instead of
URL query param, add rate limiting on login, add MaxBytesReader,
validate volume paths against traversal, add security headers,
validate user roles, add Secure flag to OIDC cookie.

Performance: set SQLite MaxOpenConns(1) to prevent SQLITE_BUSY,
add FK indexes on 8 columns, track notifier goroutines with
WaitGroup for graceful shutdown, use GetRegistryByName instead of
GetAllRegistries in deployer, pass basePath param to avoid redundant
settings query, return empty slices from store to remove reflection.

Quality: refactor TriggerDeploy to delegate to runDeploy (~100 lines
removed), consolidate duplicated utilities (extractPort, boolToInt,
now, isTerminalStatus) into shared exports, migrate all log.Printf
to slog structured logging, use consistent webhook response envelope,
remove dead code (parseEnvVars, duplicate auth types).

UX: clean up NPM proxy on instance removal via API, add README with
quickstart guide, add .env.example, require ADMIN_PASSWORD in
docker-compose, document staging-net prerequisite.
2026-03-29 12:49:24 +03:00
alexei.dolgolyov 652229c67f chore: fix build dependencies and frontend config
Migrate Docker SDK from github.com/docker/docker (+incompatible)
to github.com/moby/moby/client v0.3.0 + moby/moby/api v1.54.0
(proper Go modules). Adapt all container/image/network operations
to the new moby API (Filters, ContainerListOptions, PullResponse,
InspectResult, etc.). Add AsyncTriggerDeploy runDeploy method.

Fix SvelteKit build: disable prerender, set strict=false for SPA,
bump vite-plugin-svelte to v5 for vite 6 compat.

Add .dockerignore to exclude .git, node_modules, plans.
2026-03-28 13:13:45 +03:00
alexei.dolgolyov d4659146fc feat(docker-watcher): phase 13 - volumes & environment
Per-stage env var overrides with encryption for secrets.
Volume mounts with shared/isolated modes (isolated appends
/{stage}-{tag}/ to source path). Store CRUD, API endpoints,
and frontend editors for both. Env merge during deploy.
2026-03-27 23:28:59 +03:00
alexei.dolgolyov 389ed5aff8 feat(docker-watcher): phases 3+4 - Docker client & NPM client
Phase 3: Docker Engine API wrapper — pull/inspect images, container
lifecycle (create/start/stop/remove/restart), network management,
label-based container tracking, deterministic naming.

Phase 4: Nginx Proxy Manager API client — JWT auth with auto-refresh,
CRUD for proxy hosts, domain-based host lookup.
2026-03-27 21:08:57 +03:00