4 Commits
6 changed files with 227 additions and 6 deletions
+13 -2
View File
@@ -1,4 +1,4 @@
# TeamVis — Self-Hosting-Bundle (v0.31.0) # TeamVis — Self-Hosting-Bundle (v0.33.0)
Dieses Bundle enthält alles zum **Betreiben** von TeamVis auf eigener Dieses Bundle enthält alles zum **Betreiben** von TeamVis auf eigener
Infrastruktur — **keinen** App-Quellcode. Die App selbst kommt als fertiges Infrastruktur — **keinen** App-Quellcode. Die App selbst kommt als fertiges
@@ -12,6 +12,17 @@ docker login git.zfx.services # Benutzer: teamvis-pull · Passwort: <Im
bash deploy/selfhost/install.sh # bei der DB-Frage Modus 2 = alles mitinstallieren bash deploy/selfhost/install.sh # bei der DB-Frage Modus 2 = alles mitinstallieren
``` ```
## Aktualisieren
```bash
bash deploy/selfhost/update.sh
```
Macht ein DB-Backup, holt neue Migrationen + das aktuelle Image und spielt **nur
die neu hinzugekommenen** Migrationen ein. **Wichtig:** Updates immer über
`update.sh` laufen lassen — nicht selbst `git pull`, sonst können Migrationen
übersprungen werden.
Vollständige Schritt-für-Schritt-Anleitung (von der nackten VM bis live): Vollständige Schritt-für-Schritt-Anleitung (von der nackten VM bis live):
**`docs/SELFHOST-QUICKSTART.md`**. Hintergrund & Varianten: **`docs/SELFHOST-QUICKSTART.md`**. Hintergrund & Varianten:
`deploy/selfhost/README.md` · Detail-Runbook: `docs/NEUKUNDE.md`. `deploy/selfhost/README.md` · Detail-Runbook: `docs/NEUKUNDE.md`.
@@ -25,4 +36,4 @@ Vollständige Schritt-für-Schritt-Anleitung (von der nackten VM bis live):
| `supabase/migrations/` | Datenbank-Schema (DDL) | | `supabase/migrations/` | Datenbank-Schema (DDL) |
| `docs/` | Anleitungen | | `docs/` | Anleitungen |
Stand: TeamVis 0.31.0. Stand: TeamVis 0.33.0.
+112
View File
@@ -0,0 +1,112 @@
#!/usr/bin/env bash
# =====================================================================
# TeamVis — Update einer Self-Host-Instanz
# =====================================================================
# Aktualisiert eine bestehende Instanz in einem Rutsch:
# 1. Pre-Update-Backup der Datenbank (Modus 2)
# 2. `git pull` im Bundle → neue Migrationen + Skripte
# 3. NUR die NEU hinzugekommenen Migrationen einspielen (git als Ledger:
# welche Dateien seit dem letzten Stand dazukamen)
# 4. neues App-Image ziehen + Container neu starten
#
# Warum git als Ledger? Die Migrationen sind append-only. `git diff` zwischen
# altem und neuem Stand liefert exakt die neuen Migrationsdateien — kein
# separater DB-Tracker nötig, kein erneutes Ausführen alter Migrationen.
#
# Aufruf: bash deploy/selfhost/update.sh [instanz-verzeichnis]
# (ohne Argument: das Instanz-Verzeichnis wird automatisch erkannt, wenn
# es genau eines gibt.)
set -euo pipefail
c_bold=$'\033[1m'; c_grn=$'\033[32m'; c_red=$'\033[31m'; c_yel=$'\033[33m'; c_rst=$'\033[0m'
say() { printf '%s\n' "$*"; }
head() { printf '\n%s%s%s\n' "$c_bold" "$*" "$c_rst"; }
ok() { printf '%s✓%s %s\n' "$c_grn" "$c_rst" "$*"; }
warn() { printf '%s!%s %s\n' "$c_yel" "$c_rst" "$*"; }
die() { printf '%s✗ %s%s\n' "$c_red" "$*" "$c_rst" >&2; exit 1; }
BUNDLE="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$BUNDLE"
[ -d .git ] || die "Kein git-Repo in $BUNDLE — Updates laufen über 'git pull'. Bitte das Bundle per 'git clone' beziehen."
command -v docker >/dev/null || die "docker fehlt."
# ── Instanz-Verzeichnis bestimmen (enthält docker-compose.yml + .env) ──
INSTANCE="${1:-}"
if [ -z "$INSTANCE" ]; then
mapfile -t cands < <(
for d in "$BUNDLE"/*/; do
[ -f "${d}docker-compose.yml" ] && [ -f "${d}.env" ] && printf '%s\n' "${d%/}"
done
)
if [ "${#cands[@]}" -eq 1 ]; then
INSTANCE="${cands[0]}"
elif [ "${#cands[@]}" -eq 0 ]; then
die "Kein Instanz-Verzeichnis gefunden. Bitte als Argument angeben: bash deploy/selfhost/update.sh <verzeichnis>"
else
say "Mehrere Instanzen gefunden:"; printf ' %s\n' "${cands[@]}"
die "Bitte das gewünschte Verzeichnis als Argument angeben."
fi
fi
[ -f "$INSTANCE/docker-compose.yml" ] || die "In $INSTANCE liegt keine docker-compose.yml."
ok "Instanz: $INSTANCE"
# Modus erkennen: eigener db-Service (Modus 2) oder externe Supabase (Modus 1).
MODE2=0
grep -qE '^[[:space:]]+db:' "$INSTANCE/docker-compose.yml" && MODE2=1
POSTGRES_DB="$(grep -E '^POSTGRES_DB=' "$INSTANCE/.env" 2>/dev/null | cut -d= -f2- || true)"
POSTGRES_DB="${POSTGRES_DB:-postgres}"
PGPW="$(grep -E '^POSTGRES_PASSWORD=' "$INSTANCE/.env" 2>/dev/null | cut -d= -f2- || true)"
dbsql() { ( cd "$INSTANCE" && docker compose exec -T -e PGPASSWORD="$PGPW" db psql -v ON_ERROR_STOP=1 -U supabase_admin -d "$POSTGRES_DB" "$@" ); }
# ── 1. Pre-Update-Backup (nur Modus 2 — lokale DB) ────────────────────
if [ "$MODE2" = "1" ]; then
head "1) Datenbank-Backup"
ts="$(date +%Y%m%d-%H%M%S)"
dump="$INSTANCE/backup-db-$ts.sql"
if ( cd "$INSTANCE" && docker compose exec -T -e PGPASSWORD="$PGPW" db pg_dump -U supabase_admin "$POSTGRES_DB" ) > "$dump" 2>/dev/null; then
ok "Backup: $dump ($(wc -l < "$dump") Zeilen)"
else
warn "Backup fehlgeschlagen — trotzdem fortfahren? (Strg-C zum Abbrechen)"; read -r _ || true
fi
else
warn "Externe Supabase (Modus 1): Backup macht dein Supabase-Anbieter. Kein lokaler Dump."
fi
# ── 2. git pull → neue Migrationen/Skripte ────────────────────────────
head "2) Bundle aktualisieren (git pull)"
OLD="$(git rev-parse HEAD)"
git pull --ff-only origin main || die "git pull fehlgeschlagen (abweichende Branches?). Notfalls: git fetch && git reset --hard origin/main"
NEW="$(git rev-parse HEAD)"
if [ "$OLD" = "$NEW" ]; then
ok "Schon aktuell (keine neuen Commits)."
else
ok "Aktualisiert: ${OLD:0:8}${NEW:0:8}"
fi
# ── 3. NUR neue Migrationen einspielen ────────────────────────────────
head "3) Neue Migrationen"
mapfile -t NEWMIG < <(git diff --diff-filter=A --name-only "$OLD" "$NEW" -- 'supabase/migrations/*.sql' 2>/dev/null | sort)
if [ "${#NEWMIG[@]}" -eq 0 ]; then
ok "Keine neuen Migrationen."
elif [ "$MODE2" = "1" ]; then
for m in "${NEWMIG[@]}"; do
say "$m"
dbsql < "$BUNDLE/$m"
done
ok "${#NEWMIG[@]} Migration(en) eingespielt."
else
warn "Externe Supabase: bitte diese neuen Migrationen EINMAL in Supabase Studio (SQL-Editor) ausführen:"
for m in "${NEWMIG[@]}"; do say " $BUNDLE/$m"; done
read -r -p " Enter drücken, sobald sie eingespielt sind … " _ || true
fi
# ── 4. Neues Image ziehen + Container neu starten ─────────────────────
head "4) App aktualisieren"
( cd "$INSTANCE" && docker compose pull && docker compose up -d )
ok "Container neu gestartet."
head "Fertig 🎉"
SITE_URL="$(grep -E '^SITE_URL=|^NEXT_PUBLIC_SITE_URL=' "$INSTANCE/.env" 2>/dev/null | head -1 | cut -d= -f2-)"
[ -n "$SITE_URL" ] && say " $SITE_URL"
say " Bei Problemen Logs prüfen: ( cd $INSTANCE && docker compose logs -f app )"
+12 -3
View File
@@ -216,12 +216,21 @@ Patch + Minor sind DB-rückwärtskompatibel (altes Image läuft mit neuer DB).
Image-Tag in Production **immer pinnen** (`teamvis:0.11.6`), nicht `:latest` Image-Tag in Production **immer pinnen** (`teamvis:0.11.6`), nicht `:latest`
sonst zieht ein `compose pull` ungewollt einen Major-Sprung. sonst zieht ein `compose pull` ungewollt einen Major-Sprung.
**Standard-Update:** **Standard-Update (Self-Host-Bundle, empfohlen):**
```bash
# Im Bundle-Verzeichnis (teamvis-selfhost) — erledigt Backup, neue Migrationen
# (nur die NEUEN, git als Ledger) und Image-Update in einem Schritt:
bash deploy/selfhost/update.sh
```
> Wichtig: Updates **immer** über `update.sh` — nicht selbst `git pull`, sonst
> können Migrationen übersprungen werden.
**Manuell (externe Supabase ohne psql-Zugang / Sonderfall):**
```bash ```bash
# 1. Backup (siehe Abschnitt 8) # 1. Backup (siehe Abschnitt 8)
# 2. Release-Notes / CHANGELOG.md prüfen # 2. Release-Notes / CHANGELOG.md prüfen
# 3. fehlende Migration(en) aus supabase/migrations/ einspielen (nur die neuen!) # 3. NEUE Migration(en) aus supabase/migrations/ in Supabase Studio einspielen
# 4. Tag im docker-compose.yml bumpen # 4. Image-Tag aktualisieren
docker compose pull && docker compose up -d --force-recreate docker compose pull && docker compose up -d --force-recreate
``` ```
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "teamvis-selfhost", "name": "teamvis-selfhost",
"version": "0.31.0", "version": "0.33.0",
"private": true, "private": true,
"description": "Self-Hosting-Bundle für TeamVis (Installer + Migrationen, ohne App-Quellcode).", "description": "Self-Hosting-Bundle für TeamVis (Installer + Migrationen, ohne App-Quellcode).",
"type": "module", "type": "module",
@@ -0,0 +1,48 @@
-- ====================================================================
-- 0055_smtp_settings — SMTP-Konfiguration über die Admin-UI
-- ====================================================================
-- Bisher kam SMTP ausschließlich aus ENV-Variablen (lib/email.ts →
-- process.env.SMTP_*). Self-Host-Kunden mussten dafür die .env editieren +
-- Container neu starten. Jetzt pro Mandant in site_settings (Admin → Mail)
-- pflegbar — mit ENV-Fallback (bestehende ENV-Setups laufen unverändert).
--
-- Vorrang in lib/email.ts: ist smtp_host in der DB gesetzt → DB-Konfig,
-- sonst ENV. smtp_pass/-user sind Geheimnisse → NICHT für anon lesbar.
alter table public.site_settings
add column if not exists smtp_host text,
add column if not exists smtp_port integer,
add column if not exists smtp_secure boolean,
add column if not exists smtp_user text,
add column if not exists smtp_pass text,
add column if not exists smtp_from_email text,
add column if not exists smtp_from_name text;
-- anon-Spalten-Grant neu setzen (Muster aus 0043): anon Vollzugriff entziehen,
-- dann ALLE Spalten AUSSER der Geheimnis-/SMTP-Block-Liste freigeben.
-- WICHTIG: Block-Liste = die Original-Geheimnisse aus 0043 + alle smtp_*,
-- sonst würden die in 0043 geschützten Keys wieder anon-lesbar.
revoke all on public.site_settings from anon;
do $$
declare
v_cols text;
begin
select string_agg(quote_ident(column_name), ', ')
into v_cols
from information_schema.columns
where table_schema = 'public'
and table_name = 'site_settings'
and column_name not in (
-- Geheimnis-Spalten aus 0043 (weiter geschützt):
'ai_anthropic_key', 'ai_openai_key', 'ai_openrouter_key',
'apple_pass_cert_p12', 'apple_pass_passphrase', 'apple_pass_type_id',
'google_wallet_service_account_json',
'phone_api_token', 'phone_lookup_token', 'phone_webhook_secret',
'license_key',
-- neu: SMTP-Konfiguration (Zugangsdaten + Infrastruktur, kein anon-Bedarf):
'smtp_host', 'smtp_port', 'smtp_secure', 'smtp_user', 'smtp_pass',
'smtp_from_email', 'smtp_from_name'
);
execute 'grant select (' || v_cols || ') on public.site_settings to anon';
end $$;
+41
View File
@@ -0,0 +1,41 @@
-- ====================================================================
-- 0056_brand_font — eigene Marken-Schriftart (Branding)
-- ====================================================================
-- Der Mandant kann eine eigene Schrift hinterlegen (Google-Font-Name,
-- kuratierte Auswahl oder Datei-Upload). DSGVO: die Font-Dateien werden
-- SELF-HOSTED (im branding-assets-Bucket), NICHT von Google's CDN geladen.
-- Angewandt auf die öffentlichen Flächen (--font-sans-Override + @font-face)
-- und das QR-Wallpaper.
alter table public.site_settings
add column if not exists brand_font_family text, -- CSS/SVG-Family, z.B. "Inter"
add column if not exists brand_font_regular_url text, -- self-hosted TTF (400)
add column if not exists brand_font_bold_url text, -- self-hosted TTF (700)
add column if not exists brand_font_source text; -- 'google' | 'upload'
-- anon-Spalten-Grant neu setzen (Muster 0043/0055): die Font-Spalten sind
-- KEINE Geheimnisse — die öffentliche Karte liest site_settings via anon und
-- braucht sie. Also: Block-Liste = bisherige Geheimnisse (bleiben gesperrt),
-- die neuen brand_font_*-Spalten werden dadurch automatisch anon-lesbar.
revoke all on public.site_settings from anon;
do $$
declare
v_cols text;
begin
select string_agg(quote_ident(column_name), ', ')
into v_cols
from information_schema.columns
where table_schema = 'public'
and table_name = 'site_settings'
and column_name not in (
'ai_anthropic_key', 'ai_openai_key', 'ai_openrouter_key',
'apple_pass_cert_p12', 'apple_pass_passphrase', 'apple_pass_type_id',
'google_wallet_service_account_json',
'phone_api_token', 'phone_lookup_token', 'phone_webhook_secret',
'license_key',
'smtp_host', 'smtp_port', 'smtp_secure', 'smtp_user', 'smtp_pass',
'smtp_from_email', 'smtp_from_name'
);
execute 'grant select (' || v_cols || ') on public.site_settings to anon';
end $$;