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.
This commit is contained in:
2026-04-12 21:30:23 +03:00
parent 8d2c5a063b
commit 791cd4d6af
68 changed files with 512 additions and 224 deletions
+3 -1
View File
@@ -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",
+38
View File
@@ -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 .
+115
View File
@@ -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
+2 -2
View File
@@ -4,6 +4,6 @@ web/build/
web/.svelte-kit/
data/
.env
docker-watcher
docker-watcher.exe
tinyforge
tinyforge.exe
server.exe
+1 -1
View File
@@ -1,4 +1,4 @@
# Docker Watcher
# Tinyforge
## Dev Server
+3 -3
View File
@@ -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"]
+51
View File
@@ -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.
+2 -2
View File
@@ -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
+46 -17
View File
@@ -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
```
+25 -25
View File
@@ -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.
+7 -7
View File
@@ -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.
+1 -1
View File
@@ -1,4 +1,4 @@
module github.com/alexei/docker-watcher
module github.com/alexei/tinyforge
go 1.24.0
+3 -3
View File
@@ -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.
+1 -1
View File
@@ -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"
)
+2 -2
View File
@@ -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)
}
+2 -2
View File
@@ -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.
+3 -3
View File
@@ -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"
)
+2 -2
View File
@@ -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")
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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.
+3 -3
View File
@@ -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.
+12 -12
View File
@@ -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
+8 -8
View File
@@ -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.
+2 -2
View File
@@ -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.
+2 -2
View File
@@ -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.
+2 -2
View File
@@ -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.
+3 -3
View File
@@ -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.
+2 -2
View File
@@ -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 ─────────────────────────────────────────────────────────
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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.
+2 -2
View File
@@ -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.
+2 -2
View File
@@ -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,
+2 -2
View File
@@ -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.
+1 -1
View File
@@ -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"
)
+2 -2
View File
@@ -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"
)
+2 -2
View File
@@ -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"
)
+9 -9
View File
@@ -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"
)
+1 -1
View File
@@ -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
+4 -4
View File
@@ -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.
+8 -8
View File
@@ -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 {
+1 -1
View File
@@ -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 {
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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"
)
+3 -3
View File
@@ -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"
)
+1 -1
View File
@@ -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}}
+2 -2
View File
@@ -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"
)
+10 -10
View File
@@ -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,
+2 -2
View File
@@ -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 '',
+1 -1
View File
@@ -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.
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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.
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
@@ -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)
+1 -1
View File
@@ -1,4 +1,4 @@
package dockerwatcher
package tinyforge
import "embed"
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "docker-watcher-web",
"name": "tinyforge-web",
"version": "0.1.0",
"private": true,
"scripts": {
+31 -7
View File
@@ -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<typeof setTimeout> | 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; }
});
</script>
<div class="rounded-xl border border-[var(--border-primary)] bg-[var(--surface-page)] shadow-lg">
+7 -2
View File
@@ -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;
+1 -1
View File
@@ -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.
+3 -3
View File
@@ -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",
+3 -3
View File
@@ -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": "Обновить",
+1 -1
View File
@@ -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).
*/
+23
View File
@@ -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<Listener>();
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);
}
}
+8 -8
View File
@@ -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;
});
</script>
+16 -15
View File
@@ -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 {
+1 -1
View File
@@ -145,7 +145,7 @@
<h3 class="text-base font-semibold text-[var(--text-primary)]">{$t('settingsAuth.oidcConfig')}</h3>
<div class="mt-4 space-y-4">
{#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' }