From 791cd4d6af7e55e37ee1f6b482a7beed76f6d3b6 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sun, 12 Apr 2026 21:30:23 +0300 Subject: [PATCH] feat: rename Docker Watcher to Tinyforge 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. --- .claude/settings.json | 4 +- .gitea/workflows/build.yml | 38 ++++++ .gitea/workflows/release.yml | 115 ++++++++++++++++++ .gitignore | 4 +- CLAUDE.md | 2 +- Dockerfile | 6 +- MINI-CI-FEATURE-IDEAS.md | 51 ++++++++ Makefile | 4 +- README.md | 63 +++++++--- cmd/server/main.go | 50 ++++---- docker-compose.yml | 14 +-- go.mod | 2 +- internal/api/auth.go | 6 +- internal/api/backups.go | 2 +- internal/api/config_export.go | 4 +- internal/api/deploys.go | 4 +- internal/api/dns.go | 6 +- internal/api/docker.go | 4 +- internal/api/eventlog.go | 2 +- internal/api/instances.go | 2 +- internal/api/projects.go | 4 +- internal/api/registries.go | 6 +- internal/api/router.go | 24 ++-- internal/api/settings.go | 16 +-- internal/api/sse.go | 4 +- internal/api/stage_env.go | 4 +- internal/api/stages.go | 4 +- internal/api/stale.go | 6 +- internal/api/static_sites.go | 4 +- internal/api/stats.go | 2 +- internal/api/volume_browser.go | 4 +- internal/api/volumes.go | 4 +- internal/auth/local.go | 4 +- internal/backup/engine.go | 4 +- internal/config/export.go | 2 +- internal/config/seed.go | 4 +- internal/deployer/bluegreen.go | 4 +- internal/deployer/deployer.go | 18 +-- internal/deployer/promote.go | 2 +- internal/docker/client.go | 8 +- internal/docker/container.go | 16 +-- internal/docker/network.go | 2 +- internal/proxy/npm_provider.go | 2 +- internal/registry/poller.go | 4 +- internal/stale/scanner.go | 6 +- internal/staticsite/deno/template.go | 2 +- internal/staticsite/healthcheck.go | 4 +- internal/staticsite/manager.go | 20 +-- internal/store/store.go | 4 +- internal/volume/resolver.go | 2 +- internal/webhook/autocreate.go | 4 +- internal/webhook/handler.go | 4 +- internal/webhook/matcher.go | 2 +- scripts/dev-server.sh | 4 +- ...her.example.yaml => tinyforge.example.yaml | 6 +- web.go | 2 +- web/package-lock.json | 4 +- web/package.json | 2 +- web/src/lib/components/ContainerLogs.svelte | 38 ++++-- web/src/lib/components/ContainerStats.svelte | 9 +- web/src/lib/components/icons/index.ts | 2 +- web/src/lib/i18n/en.json | 6 +- web/src/lib/i18n/ru.json | 6 +- web/src/lib/sse.ts | 2 +- web/src/lib/stores/event-log-bus.ts | 23 ++++ web/src/routes/events/+page.svelte | 16 +-- web/src/routes/projects/[id]/+page.svelte | 31 ++--- web/src/routes/settings/auth/+page.svelte | 2 +- 68 files changed, 512 insertions(+), 224 deletions(-) create mode 100644 .gitea/workflows/build.yml create mode 100644 .gitea/workflows/release.yml create mode 100644 MINI-CI-FEATURE-IDEAS.md rename docker-watcher.example.yaml => tinyforge.example.yaml (91%) create mode 100644 web/src/lib/stores/event-log-bus.ts diff --git a/.claude/settings.json b/.claude/settings.json index 95f68d5..60c7fc2 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -12,7 +12,9 @@ "Bash(git stash:*)", "Bash(echo \"EXIT: $?\")", "Bash(./scripts/dev-server.sh)", - "Bash(go doc:*)" + "Bash(go doc:*)", + "Bash(ls -la /c/Users/Alexei/Documents/docker-watcher/internal/*/)", + "Bash(go get:*)" ], "additionalDirectories": [ "C:\\Users\\Alexei\\Documents\\docker-watcher\\internal", diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..ab07943 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,38 @@ +name: Build + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: '1.24' + + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install frontend dependencies + working-directory: web + run: npm ci --no-audit + + - name: Build frontend + working-directory: web + run: npm run build + + - name: Vet Go code + run: go vet ./... + + - name: Build Go binary + run: CGO_ENABLED=0 go build -ldflags="-s -w" -o tinyforge ./cmd/server + + - name: Build Docker image + run: docker build -t tinyforge:dev . diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..18e4b3b --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,115 @@ +name: Release + +on: + push: + tags: + - 'v*' + +env: + SERVER_HOST: git.dolgolyov-family.by + REGISTRY: git.dolgolyov-family.by/alexei.dolgolyov/tiny-forge + +jobs: + create-release: + runs-on: ubuntu-latest + outputs: + release_id: ${{ steps.create.outputs.release_id }} + steps: + - name: Fetch RELEASE_NOTES.md only + uses: actions/checkout@v4 + with: + sparse-checkout: RELEASE_NOTES.md + sparse-checkout-cone-mode: false + + - name: Create Gitea release + id: create + env: + DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }} + run: | + TAG="${{ gitea.ref_name }}" + VERSION="${TAG#v}" + BASE_URL="${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}" + + # Detect pre-release (alpha/beta/rc) + IS_PRE="false" + if echo "$TAG" | grep -qE '(alpha|beta|rc)'; then + IS_PRE="true" + fi + + # Read release notes if present + if [ -f RELEASE_NOTES.md ]; then + export RELEASE_NOTES=$(cat RELEASE_NOTES.md) + echo "Found RELEASE_NOTES.md" + else + export RELEASE_NOTES="" + echo "No RELEASE_NOTES.md found — release will have no body" + fi + + BODY_JSON=$(python3 -c " + import json, os + notes = os.environ.get('RELEASE_NOTES', '') + print(json.dumps(notes.strip())) + ") + + # Create release via Gitea API + RELEASE=$(curl -s -X POST "$BASE_URL/releases" \ + -H "Authorization: token $DEPLOY_TOKEN" \ + -H "Content-Type: application/json" \ + -d "{ + \"tag_name\": \"$TAG\", + \"name\": \"$VERSION\", + \"body\": $BODY_JSON, + \"draft\": false, + \"prerelease\": $IS_PRE + }") + + # Fallback: if release already exists for this tag, reuse it + RELEASE_ID=$(echo "$RELEASE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])" 2>/dev/null) + if [ -z "$RELEASE_ID" ]; then + echo "::warning::Release already exists for tag $TAG — reusing existing release" + RELEASE=$(curl -s "$BASE_URL/releases/tags/$TAG" \ + -H "Authorization: token $DEPLOY_TOKEN") + RELEASE_ID=$(echo "$RELEASE" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])") + fi + echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT" + echo "Created release $RELEASE_ID for $TAG" + + build-docker: + needs: create-release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Login to Gitea Container Registry + id: docker-login + continue-on-error: true + run: | + echo "${{ secrets.DEPLOY_TOKEN }}" | docker login \ + "$SERVER_HOST" -u "${{ gitea.actor }}" --password-stdin + + - name: Build and tag + if: steps.docker-login.outcome == 'success' + run: | + TAG="${{ gitea.ref_name }}" + VERSION="${TAG#v}" + docker build -t "$REGISTRY:$TAG" -t "$REGISTRY:$VERSION" . + # Tag as 'latest' only for stable releases + if ! echo "$TAG" | grep -qE '(alpha|beta|rc)'; then + docker tag "$REGISTRY:$TAG" "$REGISTRY:latest" + fi + + - name: Push + if: steps.docker-login.outcome == 'success' + run: docker push "$REGISTRY" --all-tags + + - name: Trigger Portainer redeploy + if: steps.docker-login.outcome == 'success' + continue-on-error: true + run: | + if [ -n "${{ secrets.DOCKER_REDEPLOY_WEBHOOK_URL }}" ]; then + echo "Triggering Portainer redeploy..." + curl -sf -X POST "${{ secrets.DOCKER_REDEPLOY_WEBHOOK_URL }}" \ + --max-time 30 || echo "::warning::Portainer webhook failed" + else + echo "DOCKER_REDEPLOY_WEBHOOK_URL not set — skipping auto-deploy" + fi diff --git a/.gitignore b/.gitignore index 8eadf1c..a447796 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,6 @@ web/build/ web/.svelte-kit/ data/ .env -docker-watcher -docker-watcher.exe +tinyforge +tinyforge.exe server.exe diff --git a/CLAUDE.md b/CLAUDE.md index dc6e444..933da9b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,4 +1,4 @@ -# Docker Watcher +# Tinyforge ## Dev Server diff --git a/Dockerfile b/Dockerfile index 8b914db..54dbcc0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,7 +22,7 @@ COPY . . # Copy built frontend into the expected embed location. COPY --from=frontend-builder /build/web/build ./web/build -RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /docker-watcher ./cmd/server +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /tinyforge ./cmd/server # Stage 3: Minimal runtime image FROM alpine:3.19 @@ -34,7 +34,7 @@ RUN addgroup -g 1000 -S app && adduser -u 1000 -S app -G app WORKDIR /app -COPY --from=backend-builder /docker-watcher /app/docker-watcher +COPY --from=backend-builder /tinyforge /app/tinyforge # Data directory for SQLite database. RUN mkdir -p /app/data && chown -R app:app /app @@ -46,4 +46,4 @@ EXPOSE 8080 ENV DATA_DIR=/app/data ENV LISTEN_ADDR=:8080 -ENTRYPOINT ["/app/docker-watcher"] +ENTRYPOINT ["/app/tinyforge"] diff --git a/MINI-CI-FEATURE-IDEAS.md b/MINI-CI-FEATURE-IDEAS.md new file mode 100644 index 0000000..93f9f1f --- /dev/null +++ b/MINI-CI-FEATURE-IDEAS.md @@ -0,0 +1,51 @@ +# Mini CI Feature Ideas + +Feature ideas for evolving the project from a Docker container watcher into a self-hosted mini CI/deployment platform for local developers. + +## Name Candidates + +| Name | Vibe | Domain feel | +|---|---|---| +| **Shipyard** | Where you build and launch ships (deployments). Nautical, memorable. | `shipyard.dev` | +| **Dockside** | Nods to Docker heritage, but broader — "the place beside the dock." | `dockside.dev` | +| **Launchpad** | CI/CD connotation, action-oriented. | `launchpad.run` | +| **Portside** | Same nautical lane as Portainer, but fresh. | `portside.dev` | +| **Homeport** | Self-hosted feel, "home" + "port" (Docker). | `homeport.dev` | +| **Tinyforge** | Small but powerful — a forge for building/deploying. | `tinyforge.dev` | +| **Deployr** | Blunt, says exactly what it does. | `deployr.dev` | +| **Runwell** | "Run things well." Simple, positive. | `runwell.dev` | + +## Build Pipeline + +- **Build from source** — clone a repo, run a `Dockerfile` or `docker-compose.yml`, build the image locally, then deploy it. Closes the loop from source to running container. +- **Build logs streaming** — SSE stream of `docker build` output, reusing the existing container logs streaming pattern. +- **Build cache management** — show Docker layer cache stats, allow selective cache invalidation. + +## Git Integration + +- **Webhook receiver for push events** — Gitea/GitHub/GitLab sends a push webhook, the platform rebuilds and redeploys automatically. Reuses existing webhook infra from registry polling. +- **Branch preview environments** — push to `feature/foo`, get a temporary deployment at `foo.preview.local`. Auto-cleanup when the branch is deleted. +- **Commit status reporting** — push deploy status back to Gitea/GitHub as commit statuses (green check / red X). + +## Developer Experience + +- **CLI tool** — `shipyard deploy`, `shipyard logs`, `shipyard status` from the terminal for developers who prefer the shell. +- **`.shipyard.yml` project config** — a declarative file in the repo root that defines how to build, which env vars to inject, health check paths, proxy rules. One file, full deploy config. +- **Environment promotion** — one-click promote from `dev` to `staging` to `prod`. Builds on the existing multi-stage project model by adding a promotion workflow. + +## Observability + +- **Resource dashboard** — CPU/memory/disk per container over time (not just a snapshot). Use Docker stats API with a small ring buffer in SQLite. +- **Deploy timeline** — a visual timeline showing deploys, rollbacks, and incidents across all projects. "What happened in my infra today?" +- **Alerting** — container OOM, high CPU, health check failures pushed to Telegram/Discord/email/webhook. + +## Multi-Service Orchestration + +- **Compose support** — import a `docker-compose.yml` and manage the entire stack as one project. Deploy/rollback the stack atomically. +- **Service dependency graph** — visualize which services depend on which. Block deploys if a dependency is unhealthy. +- **Shared secrets** — secrets scoped to a project or global, injected into any service that needs them. Extends the existing encrypted secrets model from static sites. + +## Database / Persistence + +- **Database snapshots** — one-click snapshot/restore of database volumes before risky deploys. +- **Automatic pre-deploy backup** — snapshot the data volume before every deploy, auto-prune old snapshots. diff --git a/Makefile b/Makefile index d17f702..6a2c0df 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ build-frontend: # Build the Go binary (embeds web/build/ via go:embed). build-backend: - go build -o docker-watcher ./cmd/server + go build -o tinyforge ./cmd/server # Run in development mode with hot reload. # Requires air (go install github.com/air-verse/air@latest). @@ -18,4 +18,4 @@ dev: # Clean build artifacts. clean: - rm -rf web/build web/node_modules/.vite docker-watcher + rm -rf web/build web/node_modules/.vite tinyforge diff --git a/README.md b/README.md index 7485edf..b4acbe0 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,44 @@ -# Docker Watcher +# Tinyforge -Automated Docker deployment orchestrator with a web dashboard. Watches container registries for new image tags and deploys them with zero-downtime blue-green strategy, health checks, and automatic NPM (Nginx Proxy Manager) proxy configuration. +Self-hosted deployment platform with a web dashboard. Deploy Docker containers from registries with zero-downtime blue-green strategy, host static sites and Deno APIs directly from Git repositories, and manage reverse proxy configuration — all from a single binary. ## Features +### Container Deployments + - **Registry polling** and **webhook receiver** for automatic deployments - **Blue-green deploys** with health checks and automatic rollback -- **NPM integration** for automatic reverse proxy configuration - **Multi-stage projects** (dev, staging, prod) with tag pattern matching - **Real-time deploy logs** via SSE streaming -- **OIDC/SSO support** alongside local auth + +### Static Sites + +Deploy static sites and Deno-powered APIs directly from Git repositories: + +- **Git providers**: Gitea/Forgejo, GitHub, and GitLab (public and private repos) +- **Static mode**: Serves HTML/CSS/JS via nginx container +- **Deno mode**: Full-stack with TypeScript API backend + static frontend — API routes are auto-discovered from `/api` folder using a naming convention (`API_get_users`, `API_post_items`, etc.) +- **Markdown rendering**: Optionally converts `.md` files to styled HTML +- **Branch & folder picker**: Select any branch and subfolder as the deployment root +- **Auto-sync**: Trigger redeployment on push or tag events, or manually +- **Per-site secrets**: Encrypted environment variables injected at runtime + +### Infrastructure + +- **NPM / Traefik integration** for automatic reverse proxy and SSL configuration +- **Cloudflare DNS** sync for automatic DNS record management +- **Volume management**: Create, browse, upload, and download Docker volumes +- **Stale container cleanup**: Detect and remove unused containers +- **Image management**: List and prune unused Docker images +- **Database backups**: Scheduled and manual backups with one-click restore +- **Config export/import**: YAML-based seed configuration for reproducible setups + +### Auth & Security + +- **Local auth** with bcrypt password hashing +- **OIDC/SSO** support for single sign-on - **Encrypted credential storage** (AES-256-GCM) -- **Single binary** with embedded SPA frontend +- **Role-based access**: Admin and user roles ## Prerequisites @@ -36,7 +63,7 @@ Automated Docker deployment orchestrator with a web dashboard. Watches container # Generate a key: openssl rand -hex 32 ``` -3. **Start Docker Watcher**: +3. **Start Tinyforge**: ```bash docker compose up -d @@ -48,19 +75,19 @@ Automated Docker deployment orchestrator with a web dashboard. Watches container ### Environment Variables -| Variable | Required | Description | -|----------|----------|-------------| -| `ENCRYPTION_KEY` | Yes | AES-256 key for encrypting stored credentials. Use `openssl rand -hex 32` | -| `ADMIN_PASSWORD` | Yes (first launch) | Password for the default admin user | -| `SEED_FILE` | No | Path to YAML seed config (default: `./docker-watcher.yaml`) | -| `DATA_DIR` | No | SQLite database directory (default: `./data`) | -| `LISTEN_ADDR` | No | HTTP listen address (default: `:8080`) | -| `NPM_URL` | No | Override NPM API URL (otherwise uses value from settings) | -| `POLLING_INTERVAL` | No | Registry polling interval, Go duration string e.g. `5m` (default from settings) | +| Variable | Required | Description | +| ------------------ | ------------------- | -------------------------------------------------------------------------------- | +| `ENCRYPTION_KEY` | Yes | AES-256 key for encrypting stored credentials. Use `openssl rand -hex 32` | +| `ADMIN_PASSWORD` | Yes (first launch) | Password for the default admin user | +| `SEED_FILE` | No | Path to YAML seed config (default: `./tinyforge.yaml`) | +| `DATA_DIR` | No | SQLite database directory (default: `./data`) | +| `LISTEN_ADDR` | No | HTTP listen address (default: `:8080`) | +| `NPM_URL` | No | Override NPM API URL (otherwise uses value from settings) | +| `POLLING_INTERVAL` | No | Registry polling interval, Go duration string e.g. `5m` (default from settings) | ### Seed Config -On first launch, Docker Watcher imports a YAML seed file to pre-configure registries, projects, and settings. See `docker-watcher.example.yaml` for the full format. +On first launch, Tinyforge imports a YAML seed file to pre-configure registries, projects, and settings. See `tinyforge.example.yaml` for the full format. ### Webhook Integration @@ -95,9 +122,11 @@ make dev ## Architecture -``` +```text CI/Registry --> Webhook/Poller --> Deployer --> Docker + NPM | +Git Repo ----> Static Sites -------> Docker + NPM + | Event Bus --> SSE --> Web Dashboard ``` diff --git a/cmd/server/main.go b/cmd/server/main.go index 601e0af..2d6d323 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -15,26 +15,26 @@ import ( "github.com/robfig/cron/v3" - dockerwatcher "github.com/alexei/docker-watcher" - "github.com/alexei/docker-watcher/internal/api" - "github.com/alexei/docker-watcher/internal/auth" - "github.com/alexei/docker-watcher/internal/config" - "github.com/alexei/docker-watcher/internal/crypto" - "github.com/alexei/docker-watcher/internal/backup" - "github.com/alexei/docker-watcher/internal/deployer" - "github.com/alexei/docker-watcher/internal/dns" - "github.com/alexei/docker-watcher/internal/docker" - "github.com/alexei/docker-watcher/internal/events" - "github.com/alexei/docker-watcher/internal/health" - "github.com/alexei/docker-watcher/internal/logging" - "github.com/alexei/docker-watcher/internal/notify" - "github.com/alexei/docker-watcher/internal/npm" - "github.com/alexei/docker-watcher/internal/proxy" - "github.com/alexei/docker-watcher/internal/registry" - "github.com/alexei/docker-watcher/internal/stale" - "github.com/alexei/docker-watcher/internal/staticsite" - "github.com/alexei/docker-watcher/internal/store" - "github.com/alexei/docker-watcher/internal/webhook" + tinyforge "github.com/alexei/tinyforge" + "github.com/alexei/tinyforge/internal/api" + "github.com/alexei/tinyforge/internal/auth" + "github.com/alexei/tinyforge/internal/config" + "github.com/alexei/tinyforge/internal/crypto" + "github.com/alexei/tinyforge/internal/backup" + "github.com/alexei/tinyforge/internal/deployer" + "github.com/alexei/tinyforge/internal/dns" + "github.com/alexei/tinyforge/internal/docker" + "github.com/alexei/tinyforge/internal/events" + "github.com/alexei/tinyforge/internal/health" + "github.com/alexei/tinyforge/internal/logging" + "github.com/alexei/tinyforge/internal/notify" + "github.com/alexei/tinyforge/internal/npm" + "github.com/alexei/tinyforge/internal/proxy" + "github.com/alexei/tinyforge/internal/registry" + "github.com/alexei/tinyforge/internal/stale" + "github.com/alexei/tinyforge/internal/staticsite" + "github.com/alexei/tinyforge/internal/store" + "github.com/alexei/tinyforge/internal/webhook" ) func main() { @@ -49,7 +49,7 @@ func main() { } // Open database. - dbPath := filepath.Join(dataDir, "docker-watcher.db") + dbPath := filepath.Join(dataDir, "tinyforge.db") db, err := store.New(dbPath) if err != nil { slog.Error("open store", "error", err) @@ -65,7 +65,7 @@ func main() { } // Import seed config on first launch (idempotent). - seedPath := envOrDefault("SEED_FILE", "./docker-watcher.yaml") + seedPath := envOrDefault("SEED_FILE", "./tinyforge.yaml") if err := config.ImportSeed(db, seedPath); err != nil { slog.Error("seed import", "error", err) os.Exit(1) @@ -306,7 +306,7 @@ func main() { // Serve embedded static files for the SPA frontend. // The embed.FS has "web/build" as a prefix, so we sub it to get the root. - webBuildFS, err := fs.Sub(dockerwatcher.WebBuildFS, "web/build") + webBuildFS, err := fs.Sub(tinyforge.WebBuildFS, "web/build") if err != nil { slog.Warn("embedded frontend not available", "error", err) } else { @@ -337,7 +337,7 @@ func main() { }) go func() { - slog.Info("Docker Watcher started", "addr", addr) + slog.Info("Tinyforge started", "addr", addr) if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed { slog.Error("HTTP server error", "error", err) os.Exit(1) @@ -371,7 +371,7 @@ func main() { slog.Error("database close error", "error", err) } - slog.Info("Docker Watcher stopped") + slog.Info("Tinyforge stopped") } // envOrDefault reads an environment variable or returns the fallback value. diff --git a/docker-compose.yml b/docker-compose.yml index 196fb48..32f5608 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,8 +1,8 @@ services: - docker-watcher: + tinyforge: build: . - image: docker-watcher:latest - container_name: docker-watcher + image: tinyforge:latest + container_name: tinyforge restart: unless-stopped ports: - "8080:8080" @@ -10,16 +10,16 @@ services: # Mount Docker socket for container management. - /var/run/docker.sock:/var/run/docker.sock # Persistent data (SQLite database). - - docker-watcher-data:/app/data + - tinyforge-data:/app/data # Optional seed config (read on first launch only). - - ./docker-watcher.yaml:/app/docker-watcher.yaml:ro + - ./tinyforge.yaml:/app/tinyforge.yaml:ro environment: # Required: protects all credentials stored in the database. - ENCRYPTION_KEY=${ENCRYPTION_KEY:?Set ENCRYPTION_KEY in .env} # Required on first launch: password for the default admin user. - ADMIN_PASSWORD=${ADMIN_PASSWORD:?Set ADMIN_PASSWORD in .env} # Optional: override seed file location. - - SEED_FILE=/app/docker-watcher.yaml + - SEED_FILE=/app/tinyforge.yaml # Optional: override data directory. - DATA_DIR=/app/data # Optional: override listen address. @@ -38,7 +38,7 @@ services: start_period: 10s volumes: - docker-watcher-data: + tinyforge-data: driver: local # NOTE: The staging-net network must exist before starting. diff --git a/go.mod b/go.mod index 2b2bdb5..47b6330 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/alexei/docker-watcher +module github.com/alexei/tinyforge go 1.24.0 diff --git a/internal/api/auth.go b/internal/api/auth.go index cc363fc..eb94d79 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -10,9 +10,9 @@ import ( "github.com/go-chi/chi/v5" - "github.com/alexei/docker-watcher/internal/auth" - "github.com/alexei/docker-watcher/internal/crypto" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/auth" + "github.com/alexei/tinyforge/internal/crypto" + "github.com/alexei/tinyforge/internal/store" ) // rateLimitedLogin wraps the login handler with per-IP rate limiting. diff --git a/internal/api/backups.go b/internal/api/backups.go index 95a2e3e..92e85ef 100644 --- a/internal/api/backups.go +++ b/internal/api/backups.go @@ -9,7 +9,7 @@ import ( "strings" "time" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/store" "github.com/go-chi/chi/v5" ) diff --git a/internal/api/config_export.go b/internal/api/config_export.go index fb549ca..3704fa8 100644 --- a/internal/api/config_export.go +++ b/internal/api/config_export.go @@ -4,7 +4,7 @@ import ( "log/slog" "net/http" - "github.com/alexei/docker-watcher/internal/config" + "github.com/alexei/tinyforge/internal/config" ) // exportConfig handles GET /api/config/export — downloads current state as YAML. @@ -17,7 +17,7 @@ func (s *Server) exportConfig(w http.ResponseWriter, r *http.Request) { } w.Header().Set("Content-Type", "application/x-yaml") - w.Header().Set("Content-Disposition", "attachment; filename=docker-watcher.yaml") + w.Header().Set("Content-Disposition", "attachment; filename=tinyforge.yaml") w.WriteHeader(http.StatusOK) w.Write(data) } diff --git a/internal/api/deploys.go b/internal/api/deploys.go index ca38144..d2f4588 100644 --- a/internal/api/deploys.go +++ b/internal/api/deploys.go @@ -6,8 +6,8 @@ import ( "strconv" "strings" - "github.com/alexei/docker-watcher/internal/docker" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/docker" + "github.com/alexei/tinyforge/internal/store" ) // listDeploys handles GET /api/deploys. diff --git a/internal/api/dns.go b/internal/api/dns.go index dca2f8f..2af4c49 100644 --- a/internal/api/dns.go +++ b/internal/api/dns.go @@ -6,9 +6,9 @@ import ( "net/http" "strings" - "github.com/alexei/docker-watcher/internal/crypto" - "github.com/alexei/docker-watcher/internal/dns" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/crypto" + "github.com/alexei/tinyforge/internal/dns" + "github.com/alexei/tinyforge/internal/store" "github.com/go-chi/chi/v5" ) diff --git a/internal/api/docker.go b/internal/api/docker.go index 9e745d0..ac06cae 100644 --- a/internal/api/docker.go +++ b/internal/api/docker.go @@ -11,7 +11,7 @@ import ( "github.com/go-chi/chi/v5" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/store" ) // listProjectImages handles GET /api/projects/{id}/images. @@ -220,7 +220,7 @@ func (s *Server) unusedImageStats(w http.ResponseWriter, r *http.Request) { } // pruneImages handles POST /api/docker/prune-images. -// Only removes images that belong to Docker Watcher projects (not all system images). +// Only removes images that belong to Tinyforge projects (not all system images). func (s *Server) pruneImages(w http.ResponseWriter, r *http.Request) { if s.docker == nil { respondError(w, http.StatusServiceUnavailable, "Docker is not available") diff --git a/internal/api/eventlog.go b/internal/api/eventlog.go index 9b188b0..e442281 100644 --- a/internal/api/eventlog.go +++ b/internal/api/eventlog.go @@ -7,7 +7,7 @@ import ( "github.com/go-chi/chi/v5" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/store" ) // listEventLog handles GET /api/events/log. diff --git a/internal/api/instances.go b/internal/api/instances.go index a9bcb43..170509f 100644 --- a/internal/api/instances.go +++ b/internal/api/instances.go @@ -9,7 +9,7 @@ import ( "github.com/go-chi/chi/v5" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/store" ) // listInstances handles GET /api/projects/{id}/stages/{stage}/instances. diff --git a/internal/api/projects.go b/internal/api/projects.go index 498005f..c7dae97 100644 --- a/internal/api/projects.go +++ b/internal/api/projects.go @@ -7,8 +7,8 @@ import ( "github.com/go-chi/chi/v5" - "github.com/alexei/docker-watcher/internal/events" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/events" + "github.com/alexei/tinyforge/internal/store" ) // projectRequest is the expected JSON body for creating/updating a project. diff --git a/internal/api/registries.go b/internal/api/registries.go index 4c5845c..ffa5f86 100644 --- a/internal/api/registries.go +++ b/internal/api/registries.go @@ -8,9 +8,9 @@ import ( "github.com/go-chi/chi/v5" - "github.com/alexei/docker-watcher/internal/crypto" - "github.com/alexei/docker-watcher/internal/registry" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/crypto" + "github.com/alexei/tinyforge/internal/registry" + "github.com/alexei/tinyforge/internal/store" ) // registryRequest is the expected JSON body for creating/updating a registry. diff --git a/internal/api/router.go b/internal/api/router.go index 2befa58..a56cb4a 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -7,18 +7,18 @@ import ( "github.com/go-chi/chi/v5" - "github.com/alexei/docker-watcher/internal/auth" - "github.com/alexei/docker-watcher/internal/backup" - "github.com/alexei/docker-watcher/internal/crypto" - "github.com/alexei/docker-watcher/internal/dns" - "github.com/alexei/docker-watcher/internal/docker" - "github.com/alexei/docker-watcher/internal/events" - "github.com/alexei/docker-watcher/internal/npm" - "github.com/alexei/docker-watcher/internal/proxy" - "github.com/alexei/docker-watcher/internal/stale" - "github.com/alexei/docker-watcher/internal/staticsite" - "github.com/alexei/docker-watcher/internal/store" - "github.com/alexei/docker-watcher/internal/webhook" + "github.com/alexei/tinyforge/internal/auth" + "github.com/alexei/tinyforge/internal/backup" + "github.com/alexei/tinyforge/internal/crypto" + "github.com/alexei/tinyforge/internal/dns" + "github.com/alexei/tinyforge/internal/docker" + "github.com/alexei/tinyforge/internal/events" + "github.com/alexei/tinyforge/internal/npm" + "github.com/alexei/tinyforge/internal/proxy" + "github.com/alexei/tinyforge/internal/stale" + "github.com/alexei/tinyforge/internal/staticsite" + "github.com/alexei/tinyforge/internal/store" + "github.com/alexei/tinyforge/internal/webhook" ) // DNSProviderChangedFunc is called when DNS settings change so the caller can diff --git a/internal/api/settings.go b/internal/api/settings.go index 71f147b..bba9a38 100644 --- a/internal/api/settings.go +++ b/internal/api/settings.go @@ -7,14 +7,14 @@ import ( "path/filepath" "strings" - "github.com/alexei/docker-watcher/internal/crypto" - "github.com/alexei/docker-watcher/internal/dns" - "github.com/alexei/docker-watcher/internal/docker" - "github.com/alexei/docker-watcher/internal/npm" - "github.com/alexei/docker-watcher/internal/proxy" - "github.com/alexei/docker-watcher/internal/store" - "github.com/alexei/docker-watcher/internal/volume" - "github.com/alexei/docker-watcher/internal/webhook" + "github.com/alexei/tinyforge/internal/crypto" + "github.com/alexei/tinyforge/internal/dns" + "github.com/alexei/tinyforge/internal/docker" + "github.com/alexei/tinyforge/internal/npm" + "github.com/alexei/tinyforge/internal/proxy" + "github.com/alexei/tinyforge/internal/store" + "github.com/alexei/tinyforge/internal/volume" + "github.com/alexei/tinyforge/internal/webhook" ) // settingsRequest is the expected JSON body for updating settings. diff --git a/internal/api/sse.go b/internal/api/sse.go index 434d435..36b6e54 100644 --- a/internal/api/sse.go +++ b/internal/api/sse.go @@ -10,8 +10,8 @@ import ( "github.com/go-chi/chi/v5" - "github.com/alexei/docker-watcher/internal/events" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/events" + "github.com/alexei/tinyforge/internal/store" ) // streamDeployLogs handles GET /api/deploys/{id}/logs. diff --git a/internal/api/stage_env.go b/internal/api/stage_env.go index 12ce71c..079b0ce 100644 --- a/internal/api/stage_env.go +++ b/internal/api/stage_env.go @@ -7,8 +7,8 @@ import ( "github.com/go-chi/chi/v5" - "github.com/alexei/docker-watcher/internal/crypto" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/crypto" + "github.com/alexei/tinyforge/internal/store" ) // stageEnvRequest is the expected JSON body for creating/updating a stage env override. diff --git a/internal/api/stages.go b/internal/api/stages.go index fa89c99..cf8d256 100644 --- a/internal/api/stages.go +++ b/internal/api/stages.go @@ -7,8 +7,8 @@ import ( "github.com/go-chi/chi/v5" - "github.com/alexei/docker-watcher/internal/events" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/events" + "github.com/alexei/tinyforge/internal/store" ) // stageRequest is the expected JSON body for creating/updating a stage. diff --git a/internal/api/stale.go b/internal/api/stale.go index 10dfa2c..ffa4262 100644 --- a/internal/api/stale.go +++ b/internal/api/stale.go @@ -7,9 +7,9 @@ import ( "github.com/go-chi/chi/v5" - "github.com/alexei/docker-watcher/internal/events" - "github.com/alexei/docker-watcher/internal/stale" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/events" + "github.com/alexei/tinyforge/internal/stale" + "github.com/alexei/tinyforge/internal/store" ) // listStaleContainers handles GET /api/containers/stale. diff --git a/internal/api/static_sites.go b/internal/api/static_sites.go index 68ab8bb..388e797 100644 --- a/internal/api/static_sites.go +++ b/internal/api/static_sites.go @@ -8,8 +8,8 @@ import ( "github.com/go-chi/chi/v5" - "github.com/alexei/docker-watcher/internal/crypto" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/crypto" + "github.com/alexei/tinyforge/internal/store" ) // ── List / Get ───────────────────────────────────────────────────────── diff --git a/internal/api/stats.go b/internal/api/stats.go index f1e5ea2..6574ac6 100644 --- a/internal/api/stats.go +++ b/internal/api/stats.go @@ -7,7 +7,7 @@ import ( "github.com/go-chi/chi/v5" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/store" ) // getInstanceStats handles GET /api/projects/{id}/stages/{stage}/instances/{iid}/stats. diff --git a/internal/api/volume_browser.go b/internal/api/volume_browser.go index 65ba5f3..17f48ab 100644 --- a/internal/api/volume_browser.go +++ b/internal/api/volume_browser.go @@ -11,8 +11,8 @@ import ( "github.com/go-chi/chi/v5" - "github.com/alexei/docker-watcher/internal/store" - "github.com/alexei/docker-watcher/internal/volume" + "github.com/alexei/tinyforge/internal/store" + "github.com/alexei/tinyforge/internal/volume" ) // sanitizeFilename removes characters unsafe for Content-Disposition headers. diff --git a/internal/api/volumes.go b/internal/api/volumes.go index 3a50bb5..7debf29 100644 --- a/internal/api/volumes.go +++ b/internal/api/volumes.go @@ -10,8 +10,8 @@ import ( "github.com/go-chi/chi/v5" - "github.com/alexei/docker-watcher/internal/store" - "github.com/alexei/docker-watcher/internal/volume" + "github.com/alexei/tinyforge/internal/store" + "github.com/alexei/tinyforge/internal/volume" ) // safeNamePattern restricts volume names to alphanumeric, dash, underscore, and dot. diff --git a/internal/auth/local.go b/internal/auth/local.go index 747b9a9..ad96b3f 100644 --- a/internal/auth/local.go +++ b/internal/auth/local.go @@ -41,7 +41,7 @@ type LocalAuth struct { // using HMAC-SHA256. func NewLocalAuth(encKey [32]byte) *LocalAuth { mac := hmac.New(sha256.New, encKey[:]) - mac.Write([]byte("docker-watcher-jwt-secret")) + mac.Write([]byte("tinyforge-jwt-secret")) la := &LocalAuth{ jwtSecret: mac.Sum(nil), blacklist: make(map[string]time.Time), @@ -110,7 +110,7 @@ func (la *LocalAuth) GenerateToken(claims Claims) (SessionToken, error) { RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(expiresAt), IssuedAt: jwt.NewNumericDate(time.Now()), - Issuer: "docker-watcher", + Issuer: "tinyforge", }, UserID: claims.UserID, Username: claims.Username, diff --git a/internal/backup/engine.go b/internal/backup/engine.go index edacbdc..aaa8585 100644 --- a/internal/backup/engine.go +++ b/internal/backup/engine.go @@ -8,7 +8,7 @@ import ( "sync" "time" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/store" ) // Engine manages database backup operations. @@ -52,7 +52,7 @@ func (e *Engine) CreateBackup(backupType string) (store.Backup, error) { defer e.mu.Unlock() timestamp := time.Now().UTC().Format("20060102-150405") - filename := fmt.Sprintf("docker-watcher-%s-%s.db", backupType, timestamp) + filename := fmt.Sprintf("tinyforge-%s-%s.db", backupType, timestamp) destPath := filepath.Join(e.backupDir, filename) // VACUUM INTO creates a clean, standalone copy of the database. diff --git a/internal/config/export.go b/internal/config/export.go index 76f9f0a..cbc6ac3 100644 --- a/internal/config/export.go +++ b/internal/config/export.go @@ -4,7 +4,7 @@ import ( "encoding/json" "fmt" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/store" "gopkg.in/yaml.v3" ) diff --git a/internal/config/seed.go b/internal/config/seed.go index 8bcb088..4e5eb68 100644 --- a/internal/config/seed.go +++ b/internal/config/seed.go @@ -6,8 +6,8 @@ import ( "log/slog" "os" - "github.com/alexei/docker-watcher/internal/crypto" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/crypto" + "github.com/alexei/tinyforge/internal/store" "github.com/google/uuid" ) diff --git a/internal/deployer/bluegreen.go b/internal/deployer/bluegreen.go index b7eff99..ff587d1 100644 --- a/internal/deployer/bluegreen.go +++ b/internal/deployer/bluegreen.go @@ -5,8 +5,8 @@ import ( "fmt" "log/slog" - "github.com/alexei/docker-watcher/internal/docker" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/docker" + "github.com/alexei/tinyforge/internal/store" "github.com/google/uuid" ) diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index f81754e..1471e5c 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -9,15 +9,15 @@ import ( "sync" "sync/atomic" - "github.com/alexei/docker-watcher/internal/crypto" - "github.com/alexei/docker-watcher/internal/dns" - "github.com/alexei/docker-watcher/internal/docker" - "github.com/alexei/docker-watcher/internal/events" - "github.com/alexei/docker-watcher/internal/health" - "github.com/alexei/docker-watcher/internal/notify" - "github.com/alexei/docker-watcher/internal/proxy" - "github.com/alexei/docker-watcher/internal/store" - "github.com/alexei/docker-watcher/internal/volume" + "github.com/alexei/tinyforge/internal/crypto" + "github.com/alexei/tinyforge/internal/dns" + "github.com/alexei/tinyforge/internal/docker" + "github.com/alexei/tinyforge/internal/events" + "github.com/alexei/tinyforge/internal/health" + "github.com/alexei/tinyforge/internal/notify" + "github.com/alexei/tinyforge/internal/proxy" + "github.com/alexei/tinyforge/internal/store" + "github.com/alexei/tinyforge/internal/volume" "github.com/moby/moby/api/types/mount" "github.com/google/uuid" ) diff --git a/internal/deployer/promote.go b/internal/deployer/promote.go index 043468d..a7ae82b 100644 --- a/internal/deployer/promote.go +++ b/internal/deployer/promote.go @@ -3,7 +3,7 @@ package deployer import ( "fmt" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/store" ) // validatePromoteFrom checks that a tag is running in the promote_from stage diff --git a/internal/docker/client.go b/internal/docker/client.go index 5b29d40..8716242 100644 --- a/internal/docker/client.go +++ b/internal/docker/client.go @@ -7,11 +7,11 @@ import ( "github.com/moby/moby/client" ) -// Labels applied to all containers managed by docker-watcher. +// Labels applied to all containers managed by Tinyforge. const ( - LabelProject = "docker-watcher.project" - LabelStage = "docker-watcher.stage" - LabelInstanceID = "docker-watcher.instance-id" + LabelProject = "tinyforge.project" + LabelStage = "tinyforge.stage" + LabelInstanceID = "tinyforge.instance-id" ) // Client wraps the Docker Engine API client. diff --git a/internal/docker/container.go b/internal/docker/container.go index 71b5d88..07aa1df 100644 --- a/internal/docker/container.go +++ b/internal/docker/container.go @@ -36,16 +36,16 @@ type ContainerConfig struct { NetworkID string // Labels are additional labels to apply to the container. - // docker-watcher management labels are added automatically via Project, Stage, and InstanceID. + // Tinyforge management labels are added automatically via Project, Stage, and InstanceID. Labels map[string]string - // Project is the docker-watcher project name (used for labelling). + // Project is the Tinyforge project name (used for labelling). Project string - // Stage is the docker-watcher stage name (used for labelling). + // Stage is the Tinyforge stage name (used for labelling). Stage string - // InstanceID is the docker-watcher instance ID (used for labelling). + // InstanceID is the Tinyforge instance ID (used for labelling). InstanceID string // Mounts is a list of bind mounts to attach to the container. @@ -88,7 +88,7 @@ func (c *Client) CreateContainer(ctx context.Context, cfg ContainerConfig) (stri } } - // Merge docker-watcher labels with any additional labels. + // Merge Tinyforge labels with any additional labels. labels := make(map[string]string) for k, v := range cfg.Labels { labels[k] = v @@ -198,7 +198,7 @@ func (c *Client) RestartContainer(ctx context.Context, containerID string, timeo return nil } -// ManagedContainer holds summary information about a container managed by docker-watcher. +// ManagedContainer holds summary information about a container managed by Tinyforge. type ManagedContainer struct { ID string Name string @@ -212,12 +212,12 @@ type ManagedContainer struct { } // ListContainers returns all containers matching the given label filters. -// Pass nil or an empty map to list all docker-watcher managed containers. +// Pass nil or an empty map to list all Tinyforge managed containers. // Label filters are key=value pairs applied as Docker label filters. func (c *Client) ListContainers(ctx context.Context, labelFilters map[string]string) ([]ManagedContainer, error) { filterArgs := make(client.Filters) - // Always filter by the docker-watcher project label to only return managed containers. + // Always filter by the Tinyforge project label to only return managed containers. filterArgs.Add("label", LabelProject) for k, v := range labelFilters { diff --git a/internal/docker/network.go b/internal/docker/network.go index 240c05e..3bf066e 100644 --- a/internal/docker/network.go +++ b/internal/docker/network.go @@ -32,7 +32,7 @@ func (c *Client) EnsureNetwork(ctx context.Context, networkName string) (string, resp, err := c.api.NetworkCreate(ctx, networkName, client.NetworkCreateOptions{ Driver: "bridge", Labels: map[string]string{ - LabelProject: "docker-watcher", + LabelProject: "tinyforge", }, }) if err != nil { diff --git a/internal/proxy/npm_provider.go b/internal/proxy/npm_provider.go index e52a18a..92cee2e 100644 --- a/internal/proxy/npm_provider.go +++ b/internal/proxy/npm_provider.go @@ -5,7 +5,7 @@ import ( "fmt" "strconv" - "github.com/alexei/docker-watcher/internal/npm" + "github.com/alexei/tinyforge/internal/npm" ) // NpmProvider wraps the NPM client behind the Provider interface. diff --git a/internal/registry/poller.go b/internal/registry/poller.go index 3b60332..1b340ac 100644 --- a/internal/registry/poller.go +++ b/internal/registry/poller.go @@ -7,8 +7,8 @@ import ( "sync" "time" - "github.com/alexei/docker-watcher/internal/crypto" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/crypto" + "github.com/alexei/tinyforge/internal/store" "github.com/robfig/cron/v3" ) diff --git a/internal/stale/scanner.go b/internal/stale/scanner.go index 384dde7..d84979d 100644 --- a/internal/stale/scanner.go +++ b/internal/stale/scanner.go @@ -8,9 +8,9 @@ import ( "sync" "time" - "github.com/alexei/docker-watcher/internal/docker" - "github.com/alexei/docker-watcher/internal/events" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/docker" + "github.com/alexei/tinyforge/internal/events" + "github.com/alexei/tinyforge/internal/store" "github.com/robfig/cron/v3" ) diff --git a/internal/staticsite/deno/template.go b/internal/staticsite/deno/template.go index 09385a1..cd253c3 100644 --- a/internal/staticsite/deno/template.go +++ b/internal/staticsite/deno/template.go @@ -126,7 +126,7 @@ func parseAPIFunctionName(funcName, baseRoute, importPath string) (RouteEntry, b } // routerTemplate is the Deno router entrypoint template. -var routerTemplate = template.Must(template.New("router").Parse(`// Auto-generated by Docker Watcher — do not edit manually. +var routerTemplate = template.Must(template.New("router").Parse(`// Auto-generated by Tinyforge — do not edit manually. import { serveDir } from "https://deno.land/std/http/file_server.ts"; {{- range .Imports}} diff --git a/internal/staticsite/healthcheck.go b/internal/staticsite/healthcheck.go index 30caa2e..eef9c95 100644 --- a/internal/staticsite/healthcheck.go +++ b/internal/staticsite/healthcheck.go @@ -7,8 +7,8 @@ import ( "sync" "time" - "github.com/alexei/docker-watcher/internal/docker" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/docker" + "github.com/alexei/tinyforge/internal/store" "github.com/robfig/cron/v3" ) diff --git a/internal/staticsite/manager.go b/internal/staticsite/manager.go index ed7e957..9c59188 100644 --- a/internal/staticsite/manager.go +++ b/internal/staticsite/manager.go @@ -10,12 +10,12 @@ import ( "strconv" "time" - "github.com/alexei/docker-watcher/internal/crypto" - "github.com/alexei/docker-watcher/internal/docker" - "github.com/alexei/docker-watcher/internal/events" - "github.com/alexei/docker-watcher/internal/proxy" - "github.com/alexei/docker-watcher/internal/staticsite/deno" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/crypto" + "github.com/alexei/tinyforge/internal/docker" + "github.com/alexei/tinyforge/internal/events" + "github.com/alexei/tinyforge/internal/proxy" + "github.com/alexei/tinyforge/internal/staticsite/deno" + "github.com/alexei/tinyforge/internal/store" ) // Manager orchestrates the static site deployment pipeline. @@ -207,8 +207,8 @@ func (m *Manager) Deploy(ctx context.Context, siteID string, force bool) error { NetworkName: networkName, NetworkID: networkID, Labels: map[string]string{ - "docker-watcher.static-site": site.ID, - "docker-watcher.static-site-name": site.Name, + "tinyforge.static-site": site.ID, + "tinyforge.static-site-name": site.Name, }, Project: "static-site", Stage: site.Name, @@ -230,8 +230,8 @@ func (m *Manager) Deploy(ctx context.Context, siteID string, force bool) error { NetworkName: networkName, NetworkID: networkID, Labels: map[string]string{ - "docker-watcher.static-site": site.ID, - "docker-watcher.static-site-name": site.Name, + "tinyforge.static-site": site.ID, + "tinyforge.static-site-name": site.Name, }, Project: "static-site", Stage: site.Name, diff --git a/internal/store/store.go b/internal/store/store.go index ede79f6..8650439 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -110,7 +110,7 @@ func (s *Store) runMigrations() error { `ALTER TABLE settings ADD COLUMN traefik_network TEXT NOT NULL DEFAULT ''`, `ALTER TABLE settings ADD COLUMN traefik_api_url TEXT NOT NULL DEFAULT ''`, // Set default network for existing databases with empty network. - `UPDATE settings SET network = 'docker-watcher' WHERE network = ''`, + `UPDATE settings SET network = 'tinyforge' WHERE network = ''`, // NPM remote mode: forward to server_ip instead of container name. `ALTER TABLE settings ADD COLUMN npm_remote INTEGER NOT NULL DEFAULT 0`, // Resource limits per stage. @@ -219,7 +219,7 @@ CREATE TABLE IF NOT EXISTS settings ( domain TEXT NOT NULL DEFAULT '', server_ip TEXT NOT NULL DEFAULT '', public_ip TEXT NOT NULL DEFAULT '', - network TEXT NOT NULL DEFAULT 'docker-watcher', + network TEXT NOT NULL DEFAULT 'tinyforge', subdomain_pattern TEXT NOT NULL DEFAULT 'stage-{stage}-{project}', notification_url TEXT NOT NULL DEFAULT '', npm_url TEXT NOT NULL DEFAULT '', diff --git a/internal/volume/resolver.go b/internal/volume/resolver.go index 769eef4..12b74c5 100644 --- a/internal/volume/resolver.go +++ b/internal/volume/resolver.go @@ -6,7 +6,7 @@ import ( "path/filepath" "strings" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/store" ) // ResolveParams holds the parameters needed to resolve a volume's host path. diff --git a/internal/webhook/autocreate.go b/internal/webhook/autocreate.go index 67499e8..e306397 100644 --- a/internal/webhook/autocreate.go +++ b/internal/webhook/autocreate.go @@ -6,8 +6,8 @@ import ( "log/slog" "strings" - "github.com/alexei/docker-watcher/internal/docker" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/docker" + "github.com/alexei/tinyforge/internal/store" ) // AutoCreateProject creates a new project and a default "dev" stage from an diff --git a/internal/webhook/handler.go b/internal/webhook/handler.go index 795d13b..7dbb58c 100644 --- a/internal/webhook/handler.go +++ b/internal/webhook/handler.go @@ -12,8 +12,8 @@ import ( "github.com/go-chi/chi/v5" "github.com/google/uuid" - "github.com/alexei/docker-watcher/internal/docker" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/docker" + "github.com/alexei/tinyforge/internal/store" ) // DeployTriggerer is called when a webhook determines a deploy should happen. diff --git a/internal/webhook/matcher.go b/internal/webhook/matcher.go index f6225e3..4bd3f63 100644 --- a/internal/webhook/matcher.go +++ b/internal/webhook/matcher.go @@ -5,7 +5,7 @@ import ( "fmt" "path" - "github.com/alexei/docker-watcher/internal/store" + "github.com/alexei/tinyforge/internal/store" ) // FindProjectAndStage searches for a project whose image matches the parsed diff --git a/scripts/dev-server.sh b/scripts/dev-server.sh index 8a88026..432319b 100644 --- a/scripts/dev-server.sh +++ b/scripts/dev-server.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Start (or restart) the Docker Watcher dev server on port 8090. +# Start (or restart) the Tinyforge dev server on port 8090. # Usage: ./scripts/dev-server.sh set -euo pipefail @@ -32,6 +32,6 @@ export ENCRYPTION_KEY export ADMIN_PASSWORD="${ADMIN_PASSWORD:-admin123}" export LISTEN_ADDR="${PORT}" -echo "Starting Docker Watcher on http://localhost:${PORT_NUM}" +echo "Starting Tinyforge on http://localhost:${PORT_NUM}" echo "Login: admin / ${ADMIN_PASSWORD}" exec go run ./cmd/server diff --git a/docker-watcher.example.yaml b/tinyforge.example.yaml similarity index 91% rename from docker-watcher.example.yaml rename to tinyforge.example.yaml index 072b5a4..d329d0c 100644 --- a/docker-watcher.example.yaml +++ b/tinyforge.example.yaml @@ -1,11 +1,11 @@ -# Docker Watcher — Seed Configuration +# Tinyforge — Seed Configuration # # This file is read ONCE on first launch to populate the SQLite database. # After import, all configuration is managed via the Web UI. # The only required env var is ENCRYPTION_KEY (used to encrypt credentials in DB). # -# Place this file as ./docker-watcher.yaml (or set SEED_FILE env var) -# and start Docker Watcher. Once imported, the file is never read again. +# Place this file as ./tinyforge.yaml (or set SEED_FILE env var) +# and start Tinyforge. Once imported, the file is never read again. global: # Your base domain — must have a Cloudflare wildcard DNS record (*.domain) diff --git a/web.go b/web.go index 5d23f87..07751bd 100644 --- a/web.go +++ b/web.go @@ -1,4 +1,4 @@ -package dockerwatcher +package tinyforge import "embed" diff --git a/web/package-lock.json b/web/package-lock.json index 025f5d4..50e17b7 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,11 +1,11 @@ { - "name": "docker-watcher-web", + "name": "tinyforge-web", "version": "0.1.0", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "docker-watcher-web", + "name": "tinyforge-web", "version": "0.1.0", "devDependencies": { "@sveltejs/adapter-static": "^3.0.8", diff --git a/web/package.json b/web/package.json index 7f1a9df..e759246 100644 --- a/web/package.json +++ b/web/package.json @@ -1,5 +1,5 @@ { - "name": "docker-watcher-web", + "name": "tinyforge-web", "version": "0.1.0", "private": true, "scripts": { diff --git a/web/src/lib/components/ContainerLogs.svelte b/web/src/lib/components/ContainerLogs.svelte index 3dddadb..dd57cdb 100644 --- a/web/src/lib/components/ContainerLogs.svelte +++ b/web/src/lib/components/ContainerLogs.svelte @@ -25,6 +25,29 @@ let logContainer: HTMLDivElement | undefined = $state(); let eventSource: EventSource | null = null; + // Batch incoming SSE log lines to avoid per-line re-renders. + let pendingLines: string[] = []; + let flushTimer: ReturnType | null = null; + + function flushPendingLines() { + flushTimer = null; + if (pendingLines.length === 0) return; + let updated = [...lines, ...pendingLines]; + pendingLines = []; + if (updated.length > tailCount * 2) { + updated = updated.slice(-tailCount); + } + lines = updated; + scrollToBottom(); + } + + function enqueueLine(line: string) { + pendingLines.push(line); + if (!flushTimer) { + flushTimer = setTimeout(flushPendingLines, 150); + } + } + async function loadLogs() { loading = true; error = ''; @@ -49,12 +72,7 @@ try { const data = JSON.parse(e.data); if (data.line) { - lines = [...lines, data.line]; - // Trim to max lines. - if (lines.length > tailCount * 2) { - lines = lines.slice(-tailCount); - } - scrollToBottom(); + enqueueLine(data.line); } } catch { /* ignore parse errors */ } }; @@ -69,6 +87,9 @@ eventSource.close(); eventSource = null; } + // Flush any buffered lines before stopping. + if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; } + flushPendingLines(); following = false; } @@ -90,7 +111,10 @@ // Load on mount. $effect(() => { loadLogs(); }); - onDestroy(() => { stopFollowing(); }); + onDestroy(() => { + stopFollowing(); + if (flushTimer) { clearTimeout(flushTimer); flushTimer = null; } + });
diff --git a/web/src/lib/components/ContainerStats.svelte b/web/src/lib/components/ContainerStats.svelte index 5339c54..189211a 100644 --- a/web/src/lib/components/ContainerStats.svelte +++ b/web/src/lib/components/ContainerStats.svelte @@ -19,8 +19,11 @@ $effect(() => { let cancelled = false; + let inflight = false; async function load() { + if (inflight) return; // Skip if previous request still pending. + inflight = true; try { const result = await api.fetchContainerStats(projectId, stageId, instanceId); if (!cancelled) { @@ -31,13 +34,15 @@ if (!cancelled) { error = true; } + } finally { + inflight = false; } } load(); - // Poll every 10 seconds. - const interval = setInterval(load, 10_000); + // Poll every 30 seconds (reduced from 10s to limit concurrent connections). + const interval = setInterval(load, 30_000); return () => { cancelled = true; diff --git a/web/src/lib/components/icons/index.ts b/web/src/lib/components/icons/index.ts index 8733fa5..6a5f44e 100644 --- a/web/src/lib/components/icons/index.ts +++ b/web/src/lib/components/icons/index.ts @@ -1,5 +1,5 @@ /** - * Lucide-based SVG icon components for Docker Watcher. + * Lucide-based SVG icon components for Tinyforge. * Task 2: Inline SVGs from Lucide icon set as Svelte components. * * Each icon is a standalone .svelte component accepting size and class props. diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 7f3ec9f..5abe7d1 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -1,6 +1,6 @@ { "app": { - "name": "Docker Watcher", + "name": "Tinyforge", "version": "v0.1" }, "health": { @@ -508,7 +508,7 @@ "password": "Password" }, "login": { - "title": "Docker Watcher", + "title": "Tinyforge", "subtitle": "Sign in to your account", "username": "Username", "password": "Password", @@ -819,7 +819,7 @@ }, "dns": { "title": "DNS Records", - "description": "View and manage DNS records created by Docker Watcher.", + "description": "View and manage DNS records created by Tinyforge.", "wildcardActive": "Wildcard DNS Mode Active", "wildcardActiveDesc": "DNS records are managed externally via wildcard DNS. Disable wildcard DNS in Settings to manage records individually.", "refresh": "Refresh", diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index 784f70b..f2d9745 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -1,6 +1,6 @@ { "app": { - "name": "Docker Watcher", + "name": "Tinyforge", "version": "v0.1" }, "health": { @@ -508,7 +508,7 @@ "password": "Пароль" }, "login": { - "title": "Docker Watcher", + "title": "Tinyforge", "subtitle": "Войдите в свой аккаунт", "username": "Имя пользователя", "password": "Пароль", @@ -819,7 +819,7 @@ }, "dns": { "title": "DNS-записи", - "description": "Просмотр и управление DNS-записями, созданными Docker Watcher.", + "description": "Просмотр и управление DNS-записями, созданными Tinyforge.", "wildcardActive": "Режим Wildcard DNS активен", "wildcardActiveDesc": "DNS-записи управляются внешне через wildcard DNS. Отключите wildcard DNS в настройках для индивидуального управления записями.", "refresh": "Обновить", diff --git a/web/src/lib/sse.ts b/web/src/lib/sse.ts index aa301ad..b785243 100644 --- a/web/src/lib/sse.ts +++ b/web/src/lib/sse.ts @@ -1,7 +1,7 @@ /** * SSE client helper with auto-reconnect and exponential backoff. * - * Provides type-safe event handling for Docker Watcher's real-time + * Provides type-safe event handling for Tinyforge's real-time * event streams (deploy logs and instance status changes). */ diff --git a/web/src/lib/stores/event-log-bus.ts b/web/src/lib/stores/event-log-bus.ts new file mode 100644 index 0000000..d999a16 --- /dev/null +++ b/web/src/lib/stores/event-log-bus.ts @@ -0,0 +1,23 @@ +/** + * Simple pub/sub bus for SSE event_log payloads. + * + * The layout component publishes events from the single global SSE connection. + * Pages (e.g. /events) subscribe without opening a duplicate SSE connection. + */ + +import type { EventLogSSEPayload } from '$lib/sse'; + +type Listener = (payload: EventLogSSEPayload) => void; + +const listeners = new Set(); + +export function subscribeEventLog(fn: Listener): () => void { + listeners.add(fn); + return () => { listeners.delete(fn); }; +} + +export function publishEventLog(payload: EventLogSSEPayload): void { + for (const fn of listeners) { + fn(payload); + } +} diff --git a/web/src/routes/events/+page.svelte b/web/src/routes/events/+page.svelte index 540d507..0b5af56 100644 --- a/web/src/routes/events/+page.svelte +++ b/web/src/routes/events/+page.svelte @@ -8,7 +8,8 @@ import { fetchEventLog, fetchEventLogStats, clearAllEvents, deleteEvent } from '$lib/api'; import ConfirmDialog from '$lib/components/ConfirmDialog.svelte'; import { toasts } from '$lib/stores/toast'; - import { connectGlobalEvents, type SSEConnection, type EventLogSSEPayload } from '$lib/sse'; + import { subscribeEventLog } from '$lib/stores/event-log-bus'; + import type { EventLogSSEPayload } from '$lib/sse'; import type { EventLogEntry, EventLogStats } from '$lib/types'; import EventLogEntryComponent from '$lib/components/EventLogEntry.svelte'; import EventLogFilter from '$lib/components/EventLogFilter.svelte'; @@ -35,7 +36,7 @@ const PAGE_SIZE = 50; let offset = $state(0); - let sseConnection: SSEConnection | null = null; + let unsubscribeEventLog: (() => void) | null = null; let listEl: HTMLDivElement | undefined = $state(); let showClearConfirm = $state(false); @@ -198,16 +199,15 @@ loadEvents(); loadStats(); - sseConnection = connectGlobalEvents({ - onEventLog(payload) { - handleSSEEvent(payload); - } + // Subscribe to event_log events from the global SSE connection (no duplicate connection). + unsubscribeEventLog = subscribeEventLog((payload: EventLogSSEPayload) => { + handleSSEEvent(payload); }); }); onDestroy(() => { - sseConnection?.close(); - sseConnection = null; + unsubscribeEventLog?.(); + unsubscribeEventLog = null; }); diff --git a/web/src/routes/projects/[id]/+page.svelte b/web/src/routes/projects/[id]/+page.svelte index 6a44572..45175cd 100644 --- a/web/src/routes/projects/[id]/+page.svelte +++ b/web/src/routes/projects/[id]/+page.svelte @@ -161,7 +161,7 @@ let showDeleteConfirm = $state(false); - const projectId = $derived($page.params.id); + const projectId = $derived($page.params.id!); // always present on [id] route async function loadProject() { if (!project) loading = true; @@ -188,21 +188,22 @@ } instancesByStage = mapped; - try { - const allDeploys = await api.listDeploys(20); - deploys = allDeploys.filter((d) => d.project_id === projectId); - } catch { - deploys = []; - } + // Fetch deploys, settings, and images in parallel (independent of each other). + const [deploysResult, settingsResult, imagesResult] = await Promise.allSettled([ + api.listDeploys(20), + api.getSettings(), + api.listProjectImages(projectId) + ]); - try { - const settings = await api.getSettings(); - settingsDomain = settings.domain ?? ''; - } catch { /* non-critical */ } - - try { - localImages = await api.listProjectImages(projectId); - } catch { localImages = []; } + deploys = deploysResult.status === 'fulfilled' + ? deploysResult.value.filter((d) => d.project_id === projectId) + : []; + settingsDomain = settingsResult.status === 'fulfilled' + ? (settingsResult.value.domain ?? '') + : settingsDomain; + localImages = imagesResult.status === 'fulfilled' + ? imagesResult.value + : []; } catch (e) { error = e instanceof Error ? e.message : $t('projectDetail.loadFailed'); } finally { diff --git a/web/src/routes/settings/auth/+page.svelte b/web/src/routes/settings/auth/+page.svelte index 20be7a7..1b886fe 100644 --- a/web/src/routes/settings/auth/+page.svelte +++ b/web/src/routes/settings/auth/+page.svelte @@ -145,7 +145,7 @@

{$t('settingsAuth.oidcConfig')}

{#each [ - { id: 'issuer', label: $t('settingsAuth.issuerUrl'), type: 'url', key: 'oidc_issuer_url', placeholder: 'https://auth.example.com/application/o/docker-watcher/' }, + { id: 'issuer', label: $t('settingsAuth.issuerUrl'), type: 'url', key: 'oidc_issuer_url', placeholder: 'https://auth.example.com/application/o/tinyforge/' }, { id: 'client_id', label: $t('settingsAuth.clientId'), type: 'text', key: 'oidc_client_id', placeholder: '' }, { id: 'client_secret', label: $t('settingsAuth.clientSecret'), type: 'password', key: 'oidc_client_secret', placeholder: '' }, { id: 'redirect', label: $t('settingsAuth.redirectUrl'), type: 'url', key: 'oidc_redirect_url', placeholder: 'https://watcher.example.com/api/auth/oidc/callback' }