diff --git a/.gitignore b/.gitignore index a1f5699..f2c2acf 100755 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,4 @@ aos-shots/ aos-verify*.sh .dev.pid .zscripts/dev.pid +db/backups/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e472a2e --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e7bc787 --- /dev/null +++ b/README.md @@ -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. diff --git a/deploy/backup.sh b/deploy/backup.sh new file mode 100755 index 0000000..c509b72 --- /dev/null +++ b/deploy/backup.sh @@ -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 ===" diff --git a/deploy/restore.sh b/deploy/restore.sh new file mode 100755 index 0000000..453e0f8 --- /dev/null +++ b/deploy/restore.sh @@ -0,0 +1,73 @@ +#!/bin/bash +# AOS Restore Script (PRD §5.7: restore drill) +# Usage: ./deploy/restore.sh +# 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 " + 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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..9bd009b --- /dev/null +++ b/docker-compose.yml @@ -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: