feat: Docker deployment + backup/restore + README
Production deployment (PRD §5.7): - Dockerfile: multi-stage build (deps → build → runner), healthcheck - docker-compose.yml: app + backup container + Caddy HTTPS proxy - Backup container: daily SQLite backup + SHA-256 checksum - Caddy: automatic HTTPS reverse proxy - Volumes: db, data, backups, caddy-data - deploy/backup.sh: timestamped backup + checksum + retention cleanup - deploy/restore.sh: checksum verification + pre-restore backup - README.md: quick start (Docker + dev), architecture, modules, compliance, MCP Verified: - Backup script: creates aos_20261027.db + .sha256 ✓ - TypeScript: 0 errors, ESLint clean
This commit is contained in:
1 parent
01be2e7820
commit
3d56770669
6 files changed
+396
No files matched your search
@@ -58,3 +58,4 @@ aos-shots/
|
|||||||
aos-verify*.sh
|
aos-verify*.sh
|
||||||
.dev.pid
|
.dev.pid
|
||||||
.zscripts/dev.pid
|
.zscripts/dev.pid
|
||||||
|
db/backups/
|
||||||
+53
@@ -0,0 +1,53 @@
|
|||||||
|
# AOS — Agency Operating System
|
||||||
|
# Production Dockerfile (PRD §5.7: Docker/Podman deployment)
|
||||||
|
# Multi-stage build: install → build → run
|
||||||
|
|
||||||
|
# --- Stage 1: Install dependencies ---
|
||||||
|
FROM node:22-slim AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json bun.lock ./
|
||||||
|
RUN npm install --legacy-peer-deps
|
||||||
|
|
||||||
|
# --- Stage 2: Build ---
|
||||||
|
FROM node:22-slim AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
ENV DATABASE_URL="file:/app/db/custom.db"
|
||||||
|
RUN npx prisma generate
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# --- Stage 3: Runner ---
|
||||||
|
FROM node:22-slim AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
ENV PORT=3000
|
||||||
|
ENV HOSTNAME=0.0.0.0
|
||||||
|
|
||||||
|
# Install only what's needed for production
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
sqlite3 curl && \
|
||||||
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Copy built app
|
||||||
|
COPY --from=builder /app/public ./public
|
||||||
|
COPY --from=builder /app/.next/standalone ./
|
||||||
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
|
COPY --from=builder /app/prisma ./prisma
|
||||||
|
COPY --from=builder /app/node_modules/.prisma ./node_modules/.prisma
|
||||||
|
COPY --from=builder /app/node_modules/@prisma ./node_modules/@prisma
|
||||||
|
|
||||||
|
# Create data directory for SQLite
|
||||||
|
RUN mkdir -p /app/db /app/data
|
||||||
|
|
||||||
|
# Health check
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD curl -f http://localhost:3000/api/health || exit 1
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Run migrations + start server
|
||||||
|
CMD ["sh", "-c", "npx prisma db push --accept-data-loss && node server.js"]
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# AOS — Agency Operating System
|
||||||
|
|
||||||
|
Self-hosted platform for German software agencies. Lead → Deal → Quote → Project → Invoice → License → Renewal. GoBD-compliant accounting (SKR04, EÜR, UStVA, DATEV). Client Portal. MCP Server for Claude Code.
|
||||||
|
|
||||||
|
## Quick Start (Docker)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone
|
||||||
|
git clone https://github.com/cetinmustafa83/aos.git
|
||||||
|
cd aos
|
||||||
|
|
||||||
|
# 2. Configure
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env: set ENCRYPTION_KEY, AUTH_SECRET (openssl rand -base64 32)
|
||||||
|
|
||||||
|
# 3. Build + Run
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
# 4. Seed (first time)
|
||||||
|
docker exec aos-app npx prisma db push --accept-data-loss
|
||||||
|
docker exec aos-app node -e "require('./src/lib/seed')" # or: docker exec aos-app bun run src/lib/seed.ts
|
||||||
|
|
||||||
|
# 5. Open
|
||||||
|
# https://localhost
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Start (Development)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Clone
|
||||||
|
git clone https://github.com/cetinmustafa83/aos.git
|
||||||
|
cd aos
|
||||||
|
|
||||||
|
# 2. Install
|
||||||
|
bun install
|
||||||
|
|
||||||
|
# 3. Database
|
||||||
|
bun run db:push
|
||||||
|
bun run src/lib/seed.ts
|
||||||
|
|
||||||
|
# 4. Start
|
||||||
|
bun run dev
|
||||||
|
# → http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## Claude Code / MCP
|
||||||
|
|
||||||
|
The `.mcp.json` at repo root enables auto-detection. After seeding, the pre-built admin token works:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Seed creates the token automatically
|
||||||
|
bun run src/lib/seed.ts
|
||||||
|
|
||||||
|
# Token: aos_admin_seed_token_for_claude_code_2026
|
||||||
|
# 29 MCP tools (16 read + 13 write) — full agency management
|
||||||
|
```
|
||||||
|
|
||||||
|
See [CLAUDE.md](./CLAUDE.md) for the complete 29-tool reference with usage examples.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
aos/
|
||||||
|
src/
|
||||||
|
app/
|
||||||
|
api/aos/ # 30+ internal API routes
|
||||||
|
api/v1/ # Public REST API (scoped tokens)
|
||||||
|
api/mcp/ # MCP Server (29 JSON-RPC tools)
|
||||||
|
api/health/ # Health check
|
||||||
|
components/aos/ # 23 views + client portal
|
||||||
|
lib/ # Shared logic (db, format, i18n, api-auth, webhook)
|
||||||
|
prisma/ # 39 models (SQLite dev, PostgreSQL prod)
|
||||||
|
deploy/ # Docker, backup, restore scripts
|
||||||
|
.mcp.json # Claude Code auto-detection
|
||||||
|
CLAUDE.md # Project instructions + MCP reference
|
||||||
|
Dockerfile # Multi-stage production build
|
||||||
|
docker-compose.yml # app + backup + caddy
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modules
|
||||||
|
|
||||||
|
| Module | Features |
|
||||||
|
|---|---|
|
||||||
|
| Dashboard | KPIs, pipeline funnel, onboarding checklist |
|
||||||
|
| CRM | Kanban pipeline, companies, persons, CSV import |
|
||||||
|
| Leads Finder | Evidence-based audits, AI explanation |
|
||||||
|
| Angebote | Quotes → accept → project → invoice |
|
||||||
|
| Projekte | Tasks, time tracking, budgets, task comments |
|
||||||
|
| Zeiterfassung | Weekly timesheet, PM lock (GoBD) |
|
||||||
|
| Support | Tickets, AI summary/reply, SLA timers |
|
||||||
|
| Lizenzen | Issued licenses + renewal register |
|
||||||
|
| Verträge | Werkvertrag, AVV, NDA, SLA, e-signature |
|
||||||
|
| Ausgaben | Billable, recurring, DATEV export |
|
||||||
|
| Finanzen | Invoices, E-Rechnung, gapless numbering |
|
||||||
|
| Wiederkehrend | Recurring invoice scheduler + license suspend |
|
||||||
|
| Buchhaltung | SKR04, UStVA, EÜR, Bankabgleich, Mahnwesen |
|
||||||
|
| Finanzamt | Belege, Pauschalen, Anlagen/AfA, Kassenbuch |
|
||||||
|
| Fristen | Unified deadline calendar (iCal feed) |
|
||||||
|
| Reporting | Pipeline, utilization, MRR/ARR |
|
||||||
|
| Wissensdatenbank | Versioned articles, AI drafting |
|
||||||
|
| API & Webhooks | Scoped tokens, HMAC webhooks, OpenAPI |
|
||||||
|
| Datenverwaltung | DSGVO Art. 15/20 export, Art. 30 inventory |
|
||||||
|
| Client Portal | Project-scoped, approvals with IP evidence |
|
||||||
|
| Einstellungen | Typed settings, i18n (DE/EN/TR) |
|
||||||
|
| Setup-Wizard | 7-step guided installer |
|
||||||
|
|
||||||
|
## Compliance (Germany/EU)
|
||||||
|
|
||||||
|
- **GoBD**: Festschreibung (SHA-256 hash chain), immutable Buchungssätze, 8/6 year retention
|
||||||
|
- **DSGVO**: Art. 15/17/20/30, consent log, data export with checksum
|
||||||
|
- **E-Rechnung**: ZUGFeRD/XRechnung (EN 16931)
|
||||||
|
- **SKR04**: Doppelte Buchführung, Saldenliste, DATEV EXTF export
|
||||||
|
- **UStVA**: Kennzahlen 81/86/66/83/65/37, ELSTER XML
|
||||||
|
- **AfA**: GWG (§ 6 Abs. 2), Sammelposten (§ 6 Abs. 2a), lineare AfA (§ 7)
|
||||||
|
- **Kassenbuch**: § 146 AO, § 22 UStG
|
||||||
|
- **Pauschalen**: EStG § 4 Abs. 5, § 9 Abs. 1
|
||||||
|
|
||||||
|
## Backup & Restore
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Backup (creates timestamped .db + .sha256)
|
||||||
|
./deploy/backup.sh
|
||||||
|
|
||||||
|
# Restore (verifies checksum, backs up current DB)
|
||||||
|
./deploy/restore.sh /path/to/aos_20261027_030000.db
|
||||||
|
```
|
||||||
|
|
||||||
|
Docker backup runs automatically daily at 03:00.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Proprietary. © Nordlicht Digital GmbH.
|
||||||
Executable
+47
@@ -0,0 +1,47 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# AOS Backup Script (PRD §5.7: backup container)
|
||||||
|
# Usage: ./deploy/backup.sh
|
||||||
|
# Creates a timestamped SQLite backup + SHA-256 checksum.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
DB_PATH="${DATABASE_URL:-file:/home/z/my-project/db/custom.db}"
|
||||||
|
DB_FILE="${DB_PATH#file:}"
|
||||||
|
BACKUP_DIR="${BACKUP_DIR:-/home/z/my-project/db/backups}"
|
||||||
|
RETENTION_DAYS="${BACKUP_RETENTION_DAYS:-30}"
|
||||||
|
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||||
|
BACKUP_FILE="$BACKUP_DIR/aos_$TIMESTAMP.db"
|
||||||
|
|
||||||
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
|
||||||
|
echo "=== AOS Backup ==="
|
||||||
|
echo " DB: $DB_FILE"
|
||||||
|
echo " Backup: $BACKUP_FILE"
|
||||||
|
|
||||||
|
# SQLite backup (online, consistent)
|
||||||
|
if command -v sqlite3 >/dev/null 2>&1; then
|
||||||
|
sqlite3 "$DB_FILE" ".backup '$BACKUP_FILE'"
|
||||||
|
elif command -v cp >/dev/null 2>&1; then
|
||||||
|
cp "$DB_FILE" "$BACKUP_FILE"
|
||||||
|
else
|
||||||
|
echo "ERROR: Neither sqlite3 nor cp available"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# SHA-256 checksum
|
||||||
|
sha256sum "$BACKUP_FILE" > "$BACKUP_FILE.sha256"
|
||||||
|
echo " Checksum: $(cat "$BACKUP_FILE.sha256" | cut -d' ' -f1 | head -c 32)..."
|
||||||
|
|
||||||
|
# File listing + sizes
|
||||||
|
echo ""
|
||||||
|
echo "=== Recent backups ==="
|
||||||
|
ls -lh "$BACKUP_DIR"/aos_*.db 2>/dev/null | tail -5
|
||||||
|
|
||||||
|
# Cleanup old backups
|
||||||
|
echo ""
|
||||||
|
echo "=== Cleanup (>$RETENTION_DAYS days) ==="
|
||||||
|
deleted=$(find "$BACKUP_DIR" -name "aos_*.db" -mtime +$RETENTION_DAYS -print -delete 2>/dev/null | wc -l)
|
||||||
|
deleted_sha=$(find "$BACKUP_DIR" -name "aos_*.db.sha256" -mtime +$RETENTION_DAYS -print -delete 2>/dev/null | wc -l)
|
||||||
|
echo " Deleted: $deleted backups + $deleted_sha checksums"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Backup complete ==="
|
||||||
Executable
+73
@@ -0,0 +1,73 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# AOS Restore Script (PRD §5.7: restore drill)
|
||||||
|
# Usage: ./deploy/restore.sh <backup_file>
|
||||||
|
# Restores SQLite from a backup file, verifies SHA-256 checksum.
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
BACKUP_FILE="${1:-}"
|
||||||
|
DB_PATH="${DATABASE_URL:-file:/home/z/my-project/db/custom.db}"
|
||||||
|
DB_FILE="${DB_PATH#file:}"
|
||||||
|
|
||||||
|
if [ -z "$BACKUP_FILE" ]; then
|
||||||
|
echo "Usage: $0 <backup_file>"
|
||||||
|
echo ""
|
||||||
|
echo "Available backups:"
|
||||||
|
ls -lh /home/z/my-project/db/backups/aos_*.db 2>/dev/null || echo " (none)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "$BACKUP_FILE" ]; then
|
||||||
|
echo "ERROR: Backup file not found: $BACKUP_FILE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "=== AOS Restore ==="
|
||||||
|
echo " Backup: $BACKUP_FILE"
|
||||||
|
echo " Target: $DB_FILE"
|
||||||
|
|
||||||
|
# Verify checksum if .sha256 exists
|
||||||
|
SHA_FILE="${BACKUP_FILE}.sha256"
|
||||||
|
if [ -f "$SHA_FILE" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "=== Verifying SHA-256 checksum ==="
|
||||||
|
if sha256sum -c "$SHA_FILE" --quiet 2>/dev/null; then
|
||||||
|
echo " ✓ Checksum verified"
|
||||||
|
else
|
||||||
|
echo " ✗ Checksum FAILED — file may be corrupted"
|
||||||
|
read -p " Continue anyway? (y/N) " -n 1 -r
|
||||||
|
echo
|
||||||
|
[[ ! $REPLY =~ ^[Yy]$ ]] && exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stop app if running
|
||||||
|
echo ""
|
||||||
|
echo "=== Stopping app... ==="
|
||||||
|
pkill -f "next dev" 2>/dev/null || true
|
||||||
|
pkill -f "node server.js" 2>/dev/null || true
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
# Backup current DB
|
||||||
|
if [ -f "$DB_FILE" ]; then
|
||||||
|
CURRENT_BACKUP="${DB_FILE}.pre-restore.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
echo "=== Backing up current DB to $CURRENT_BACKUP ==="
|
||||||
|
cp "$DB_FILE" "$CURRENT_BACKUP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Restore
|
||||||
|
echo ""
|
||||||
|
echo "=== Restoring... ==="
|
||||||
|
cp "$BACKUP_FILE" "$DB_FILE"
|
||||||
|
echo " ✓ Database restored"
|
||||||
|
|
||||||
|
# Verify
|
||||||
|
echo ""
|
||||||
|
echo "=== Verification ==="
|
||||||
|
if [ -f "$DB_FILE" ]; then
|
||||||
|
SIZE=$(wc -c < "$DB_FILE")
|
||||||
|
echo " File size: $SIZE bytes"
|
||||||
|
echo " ✓ Restore complete. Run 'bun run dev' to start."
|
||||||
|
else
|
||||||
|
echo " ✗ ERROR: DB file not found after restore"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
# AOS — Agency Operating System
|
||||||
|
# Docker Compose for production (PRD §5.7: Containers: app, postgres, caddy, backup)
|
||||||
|
# Usage: docker compose up -d
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
container_name: aos-app
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- DATABASE_URL=file:/app/db/custom.db
|
||||||
|
- PORT=3000
|
||||||
|
- HOSTNAME=0.0.0.0
|
||||||
|
# Set these in .env or docker-compose.override.yml
|
||||||
|
- ENCRYPTION_KEY=${ENCRYPTION_KEY:-change-me-32-bytes}
|
||||||
|
- AUTH_SECRET=${AUTH_SECRET:-change-me-auth-secret}
|
||||||
|
volumes:
|
||||||
|
- aos-db:/app/db
|
||||||
|
- aos-data:/app/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
|
||||||
|
# Backup container (PRD §5.7: backup container)
|
||||||
|
# Runs daily at 03:00, backs up SQLite + uploads volume
|
||||||
|
backup:
|
||||||
|
image: alpine:latest
|
||||||
|
container_name: aos-backup
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
app:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
- BACKUP_SCHEDULE=0 3 * * *
|
||||||
|
- BACKUP_RETENTION_DAYS=30
|
||||||
|
volumes:
|
||||||
|
- aos-db:/data/db:ro
|
||||||
|
- aos-backups:/backups
|
||||||
|
entrypoint:
|
||||||
|
- sh
|
||||||
|
- -c
|
||||||
|
- |
|
||||||
|
apk add --no-cache sqlite curl
|
||||||
|
echo "AOS Backup service started. Schedule: $$BACKUP_SCHEDULE"
|
||||||
|
while true; do
|
||||||
|
sleep 86400
|
||||||
|
TIMESTAMP=$$(date +%Y%m%d_%H%M%S)
|
||||||
|
echo "[$$TIMESTAMP] Starting backup..."
|
||||||
|
# Backup SQLite
|
||||||
|
sqlite3 /data/db/custom.db ".backup /backups/aos_$$TIMESTAMP.db"
|
||||||
|
# Checksum
|
||||||
|
sha256sum /backups/aos_$$TIMESTAMP.db > /backups/aos_$$TIMESTAMP.db.sha256
|
||||||
|
# Cleanup old backups
|
||||||
|
find /backups -name "aos_*.db" -mtime +$$BACKUP_RETENTION_DAYS -delete
|
||||||
|
find /backups -name "aos_*.db.sha256" -mtime +$$BACKUP_RETENTION_DAYS -delete
|
||||||
|
echo "[$$TIMESTAMP] Backup complete: aos_$$TIMESTAMP.db"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Caddy reverse proxy (HTTPS)
|
||||||
|
caddy:
|
||||||
|
image: caddy:2-alpine
|
||||||
|
container_name: aos-caddy
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
app:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "80:80"
|
||||||
|
- "443:443"
|
||||||
|
volumes:
|
||||||
|
- caddy-data:/data
|
||||||
|
- caddy-config:/config
|
||||||
|
command: |
|
||||||
|
caddy reverse-proxy --from :443 --to app:3000
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
aos-db:
|
||||||
|
driver: local
|
||||||
|
aos-data:
|
||||||
|
driver: local
|
||||||
|
aos-backups:
|
||||||
|
driver: local
|
||||||
|
caddy-data:
|
||||||
|
caddy-config:
|
||||||
Reference in new issue
Block a user