Initial commit (unpacked platform)

This commit is contained in:
Sven (AAMOS AI)
2026-08-05 19:21:11 +07:00
commit ac5340195a
314 changed files with 57584 additions and 0 deletions
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
# Backup-restore-övning (hardening-checklistan: "genomfört och protokollfört, kvartalsvis").
# Kör: DATABASE_URL=postgres://… ./infrastructure/deployment/backup-restore-test.sh
#
# Gör: logisk dump → återställ till temporär databas → jämför radantal per tabell
# → skriv protokoll → städa. Avslutar med exitkod 1 vid avvikelse.
set -euo pipefail
DATABASE_URL="${DATABASE_URL:?Sätt DATABASE_URL}"
# Roll med CREATEDB för test-databasen (RDS master/operatör). Faller tillbaka på DATABASE_URL.
ADMIN_URL="${ADMIN_DATABASE_URL:-$DATABASE_URL}"
STAMP=$(date +%Y%m%d_%H%M%S)
WORKDIR="${BACKUP_DIR:-/tmp/backup-restore-test}"
DUMP="$WORKDIR/dump-$STAMP.pgdump"
RESTORE_DB="restore_test_$STAMP"
PROTOKOLL="$WORKDIR/protokoll-$STAMP.md"
mkdir -p "$WORKDIR"
# Härled anslutning utan databasnamn för att kunna skapa/droppa test-databasen.
BASE_URL="${DATABASE_URL%/*}"
ADMIN_BASE_URL="${ADMIN_URL%/*}"
echo "==> 1/5 Dumpar databasen"
pg_dump "$DATABASE_URL" --format=custom --file="$DUMP"
SIZE=$(du -h "$DUMP" | cut -f1)
echo "==> 2/5 Skapar temporär databas $RESTORE_DB"
psql "$ADMIN_BASE_URL/postgres" -qc "CREATE DATABASE $RESTORE_DB" 2>/dev/null \
|| psql "$ADMIN_URL" -qc "CREATE DATABASE $RESTORE_DB"
cleanup() {
psql "$ADMIN_BASE_URL/postgres" -qc "DROP DATABASE IF EXISTS $RESTORE_DB" 2>/dev/null \
|| psql "$ADMIN_URL" -qc "DROP DATABASE IF EXISTS $RESTORE_DB" || true
}
trap cleanup EXIT
echo "==> 3/5 Återställer till $RESTORE_DB"
# Återställ som admin-rollen (äger test-databasen, får skapa extensions).
pg_restore --dbname="$ADMIN_BASE_URL/$RESTORE_DB" --no-owner --no-privileges "$DUMP"
echo "==> 4/5 Jämför radantal per tabell"
COUNT_SQL="SELECT relname, n_live_tup FROM pg_stat_user_tables ORDER BY relname"
ROWS_SQL="SELECT string_agg(format('%s:%s', t.tablename, (xpath('/row/cnt/text()', query_to_xml(format('SELECT count(*) AS cnt FROM %I', t.tablename), false, true, '')))[1]::text), E'\n' ORDER BY t.tablename) FROM pg_tables t WHERE schemaname='public'"
ORIG=$(psql "$DATABASE_URL" -tA -c "$ROWS_SQL")
REST=$(psql "$ADMIN_BASE_URL/$RESTORE_DB" -tA -c "$ROWS_SQL")
STATUS="GODKÄND"
DIFF_OUT=""
if [ "$ORIG" != "$REST" ]; then
STATUS="UNDERKÄND"
DIFF_OUT=$(diff <(echo "$ORIG") <(echo "$REST") || true)
fi
echo "==> 5/5 Skriver protokoll: $PROTOKOLL"
TABLES=$(echo "$ORIG" | wc -l)
TOTAL=$(echo "$ORIG" | awk -F: '{s+=$2} END {print s}')
{
echo "# Backup-restore-protokoll $STAMP"
echo
echo "| Fält | Värde |"
echo "| ---- | ----- |"
echo "| Resultat | **$STATUS** |"
echo "| Dumpstorlek | $SIZE |"
echo "| Tabeller jämförda | $TABLES |"
echo "| Totalt antal rader | $TOTAL |"
echo "| Dump | $DUMP |"
echo
if [ -n "$DIFF_OUT" ]; then
echo '## Avvikelser'
echo '```'
echo "$DIFF_OUT"
echo '```'
else
echo "Alla tabellers radantal identiska mellan original och återställning."
fi
} > "$PROTOKOLL"
cat "$PROTOKOLL"
[ "$STATUS" = "GODKÄND" ]
@@ -0,0 +1,29 @@
-- Körs som RDS master-användare EN gång (spec §52).
-- Separat databas + användare med minsta möjliga privilegier.
-- Ingen delning av andras tabeller.
CREATE DATABASE app; -- byt namn per varumärke (brand.config.json slug)
-- Byt ut lösenordet mot ett genererat värde från Secrets Manager!
CREATE USER app_user WITH PASSWORD 'CHANGE_ME_FROM_SECRETS_MANAGER';
-- Anslut till den nya databasen och kör:
\connect app
-- Sök-extensions (i18n M7): skapas av master appanvändaren saknar rättighet.
CREATE EXTENSION IF NOT EXISTS unaccent;
CREATE EXTENSION IF NOT EXISTS pg_trgm;
-- Låst schema: appanvändaren äger public-schemat i sin egen databas
GRANT CONNECT ON DATABASE app TO app_user;
GRANT USAGE, CREATE ON SCHEMA public TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO app_user;
-- Ingen superuser, ingen replication, ingen åtkomst till andra databaser:
REVOKE ALL ON DATABASE postgres FROM app_user;
-- Migrationer körs av samma användare (CREATE på schemat räcker för drizzle).
-- Vid senare utbrytning till egen RDS: pg_dump/pg_restore enligt docs/infrastruktur.md.
+49
View File
@@ -0,0 +1,49 @@
#!/usr/bin/env bash
# Deploy till befintlig server (spec §51, steg 1).
# Körs som den dedikerade app-användaren på servern.
set -euo pipefail
APP_DIR="/opt/${APP_SLUG:-app}-platform"
REPO_DIR="$APP_DIR/repo"
COMPOSE="$REPO_DIR/infrastructure/docker/docker-compose.production.yml"
echo "==> Deploy $(date -Iseconds)"
cd "$REPO_DIR"
git fetch --tags origin main
PREV_SHA=$(git rev-parse HEAD)
git reset --hard origin/main
NEW_SHA=$(git rev-parse HEAD)
echo "==> $PREV_SHA -> $NEW_SHA"
echo "==> Bygger images"
docker compose -f "$COMPOSE" build
echo "==> Kör databas-migrationer (med backup-punkt först)"
# RDS: automatiska snapshots finns; ta även logisk dump för snabb rollback.
mkdir -p "$APP_DIR/backups"
pg_dump "$DATABASE_URL" --format=custom \
--file="$APP_DIR/backups/pre-deploy-$(date +%Y%m%d-%H%M%S).dump" || {
echo "VARNING: pg_dump misslyckades avbryter deploy (spec §63: ingen migration utan backup)"; exit 1;
}
docker run --rm --env-file "$APP_DIR/secrets/api.env" app-api:latest \
node dist/index.js --migrate-only 2>/dev/null || true
# Migrationer körs normalt från CI/lokalt: pnpm db:migrate mot produktion via bastion.
echo "==> Startar om tjänster (rullande)"
docker compose -f "$COMPOSE" up -d app-redis
docker compose -f "$COMPOSE" up -d app-worker
docker compose -f "$COMPOSE" up -d app-api
docker compose -f "$COMPOSE" up -d app-admin
echo "==> Healthcheck"
sleep 5
if ! curl -fsS http://127.0.0.1:4100/healthz > /dev/null; then
echo "FEL: API svarar inte rollback till $PREV_SHA"
git reset --hard "$PREV_SHA"
docker compose -f "$COMPOSE" build app-api
docker compose -f "$COMPOSE" up -d app-api
exit 1
fi
echo "==> Deploy klar: $NEW_SHA"
+284
View File
@@ -0,0 +1,284 @@
#!/usr/bin/env bash
# ============================================================================
# FÖRSTA DEPLOY idempotent uppsättning av hela plattformen på en server.
#
# DEPLOY_MODE=staging ./infrastructure/deployment/first-deploy.sh
# DEPLOY_MODE=production ./infrastructure/deployment/first-deploy.sh
#
# STAGING (default): startar Postgres+Redis i Docker, genererar hemligheter
# om de saknas, AAMOS_MODE=mock tillåtet, seedar databasen, kör hela
# verifieringskedjan. Säker att köra om varje steg är idempotent.
#
# PRODUCTION: kräver befintlig .env med riktiga värden (skriptet VÄGRAR
# generera produktionshemligheter), kräver AAMOS_MODE=http, tar backup
# före migrationer, seedar ALDRIG automatiskt.
#
# Avslutar med exitkod 0 endast när ALLA grindar är gröna.
# ============================================================================
set -euo pipefail
MODE="${DEPLOY_MODE:-staging}"
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$ROOT"
LOG_PREFIX="[first-deploy:$MODE]"
say() { echo "$LOG_PREFIX $*"; }
fail() { echo "$LOG_PREFIX FEL: $*" >&2; exit 1; }
say "Startar i läge: $MODE (repo: $ROOT)"
# ---------------------------------------------------------------------------
# GRIND 0: Förutsättningar
# ---------------------------------------------------------------------------
# Docker är VALFRITT: det används bara som bekvämlighet för att starta
# Postgres/Redis i staging när de inte redan svarar. Kör tjänsterna nativt
# (WSL, systemd, molndatabas) behövs ingen Docker alls Grind 2 avgör på
# faktisk nåbarhet, inte på verktygslistan.
if command -v docker >/dev/null && docker compose version >/dev/null 2>&1; then
DOCKER_OK=1
DOCKER_NOTE="docker+compose"
else
DOCKER_OK=0
DOCKER_NOTE="utan docker (ok om Postgres/Redis redan kör)"
say "OBS: docker saknas ingen fara om Postgres/Redis redan svarar (nativt/WSL/moln). Grind 2 verifierar."
fi
command -v node >/dev/null || fail "node saknas kräver Node >= 22."
NODE_MAJOR=$(node -e "console.log(process.versions.node.split('.')[0])")
[ "$NODE_MAJOR" -ge 22 ] || fail "Node $NODE_MAJOR är för gammal kräver >= 22."
command -v pnpm >/dev/null || fail "pnpm saknas kör: corepack enable && corepack prepare pnpm@10 --activate"
say "GRIND 0 OK: $DOCKER_NOTE, node $NODE_MAJOR, pnpm $(pnpm --version)"
# ---------------------------------------------------------------------------
# GRIND 1: Miljöfil
# ---------------------------------------------------------------------------
if [ ! -f .env ]; then
if [ "$MODE" = "production" ]; then
fail ".env saknas. I produktion skapas den manuellt från .env.example med riktiga värden (Secrets Manager). Skriptet genererar ALDRIG produktionshemligheter."
fi
say "Ingen .env skapar staging-miljö med genererade hemligheter."
cp .env.example .env
gen() { node -e "console.log(require('crypto').randomBytes(48).toString('base64url'))"; }
# Ersätt dev-platshållare med riktiga slumpvärden (även staging förtjänar riktiga hemligheter).
sed -i "s|^JWT_ACCESS_SECRET=.*|JWT_ACCESS_SECRET=$(gen)|" .env
sed -i "s|^JWT_REFRESH_SECRET=.*|JWT_REFRESH_SECRET=$(gen)|" .env
sed -i "s|^ENTITLEMENT_SIGNING_SECRET=.*|ENTITLEMENT_SIGNING_SECRET=$(gen)|" .env
# AAMOS: riktiga värden via miljövariabler vid körning vinner över mock.
if [ -n "${AAMOS_API_URL:-}" ] && [ -n "${AAMOS_API_KEY:-}" ]; then
sed -i "s|^AAMOS_MODE=.*|AAMOS_MODE=http|" .env
sed -i "s|^AAMOS_API_URL=.*|AAMOS_API_URL=$AAMOS_API_URL|" .env
sed -i "s|^AAMOS_API_KEY=.*|AAMOS_API_KEY=$AAMOS_API_KEY|" .env
say "AAMOS: riktiga uppgifter angivna kör http-läge från start."
else
sed -i "s|^AAMOS_MODE=.*|AAMOS_MODE=mock|" .env
fi
# E-post: riktig SMTP från start om uppgifter angivits.
if [ -n "${SMTP_HOST:-}" ] && [ -n "${MAIL_FROM:-}" ]; then
sed -i "s|^EMAIL_MODE=.*|EMAIL_MODE=smtp|" .env
sed -i "s|^SMTP_HOST=.*|SMTP_HOST=$SMTP_HOST|" .env
sed -i "s|^SMTP_PORT=.*|SMTP_PORT=${SMTP_PORT:-587}|" .env
sed -i "s|^SMTP_USER=.*|SMTP_USER=${SMTP_USER:-}|" .env
sed -i "s|^SMTP_PASS=.*|SMTP_PASS=${SMTP_PASS:-}|" .env
sed -i "s|^MAIL_FROM=.*|MAIL_FROM=$MAIL_FROM|" .env
say "E-post: SMTP-uppgifter angivna riktiga mejl från start."
fi
# Fillagring: riktig S3 från start om bucket angivits.
if [ -n "${S3_BUCKET:-}" ]; then
sed -i "s|^S3_MODE=.*|S3_MODE=aws|" .env
sed -i "s|^S3_BUCKET=.*|S3_BUCKET=$S3_BUCKET|" .env
[ -n "${S3_REGION:-}" ] && sed -i "s|^S3_REGION=.*|S3_REGION=$S3_REGION|" .env
[ -n "${S3_ENDPOINT:-}" ] && sed -i "s|^S3_ENDPOINT=.*|S3_ENDPOINT=$S3_ENDPOINT|" .env
[ -n "${S3_ACCESS_KEY_ID:-}" ] && sed -i "s|^S3_ACCESS_KEY_ID=.*|S3_ACCESS_KEY_ID=$S3_ACCESS_KEY_ID|" .env
[ -n "${S3_SECRET_ACCESS_KEY:-}" ] && sed -i "s|^S3_SECRET_ACCESS_KEY=.*|S3_SECRET_ACCESS_KEY=$S3_SECRET_ACCESS_KEY|" .env
say "Fillagring: S3-uppgifter angivna riktig lagring från start."
fi
chmod 600 .env
fi
get_env() { { grep -E "^$1=" .env || true; } | head -1 | cut -d= -f2-; }
AAMOS_MODE_VAL=$(get_env AAMOS_MODE)
if [ "$MODE" = "production" ]; then
[ "$AAMOS_MODE_VAL" = "http" ] || fail "Produktion kräver AAMOS_MODE=http (nu: '$AAMOS_MODE_VAL')."
[ -n "$(get_env AAMOS_API_URL)" ] || fail "AAMOS_API_URL saknas i .env."
[ -n "$(get_env AAMOS_API_KEY)" ] || fail "AAMOS_API_KEY saknas i .env."
for s in JWT_ACCESS_SECRET JWT_REFRESH_SECRET ENTITLEMENT_SIGNING_SECRET; do
case "$(get_env $s)" in dev-only-*|"") fail "$s har dev-/tomt värde sätt riktig hemlighet." ;; esac
done
fi
say "GRIND 1 OK: .env på plats ($([ "$MODE" = production ] && echo 'produktionsvärden verifierade' || echo 'staging'))"
# ---------------------------------------------------------------------------
# GRIND 2: Databas + Redis (nåbarhet avgör Docker är bara reservstart)
# ---------------------------------------------------------------------------
db_reachable() {
node -e "
const { Client } = require('pg');
const c = new Client({ connectionString: process.env.DB_URL, connectionTimeoutMillis: 3000 });
c.connect().then(() => c.end()).then(() => process.exit(0)).catch(() => process.exit(1));
" 2>/dev/null
}
# Rå TCP-PING: alla svar (även -NOAUTH) bevisar att Redis är nåbar.
redis_reachable() {
node -e "
const net = require('net');
let u; try { u = new URL(process.env.R_URL); } catch { process.exit(1); }
const s = net.createConnection({ host: u.hostname || '127.0.0.1', port: Number(u.port) || 6379 });
s.setTimeout(3000);
s.on('connect', () => s.write('PING\r\n'));
s.on('data', () => process.exit(0));
s.on('error', () => process.exit(1));
s.on('timeout', () => process.exit(1));
" 2>/dev/null
}
start_docker_services() {
# Reservstart i staging kräver Docker. Utan Docker: starta tjänsterna själv.
if [ "$DOCKER_OK" != 1 ] || ! docker info >/dev/null 2>&1; then
fail "$1 svarar inte och Docker är inte tillgänglig. Starta tjänsten nativt (t.ex. 'sudo service postgresql start' / 'sudo service redis-server start' på WSL) eller installera Docker och kör om skriptet."
fi
say "Startar Postgres + Redis i Docker (idempotent) …"
docker compose -f infrastructure/docker/docker-compose.dev.yml up -d
}
export DB_URL="$(get_env DATABASE_URL)"
export R_URL="$(get_env REDIS_URL)"
if DB_URL="$DB_URL" db_reachable; then
say "Databasen svarar redan ingen uppstart behövs."
elif [ "$MODE" = "staging" ]; then
start_docker_services "Databasen"
for i in $(seq 1 30); do
DB_URL="$DB_URL" db_reachable && break
sleep 1
[ "$i" = 30 ] && fail "Postgres svarade inte inom 30 s."
done
else
fail "DATABASE_URL svarar inte i produktion startar detta skript aldrig databaser. Kontrollera RDS/nätverk."
fi
if R_URL="$R_URL" redis_reachable; then
say "Redis svarar."
elif [ "$MODE" = "staging" ]; then
start_docker_services "Redis"
for i in $(seq 1 30); do
R_URL="$R_URL" redis_reachable && break
sleep 1
[ "$i" = 30 ] && fail "Redis svarade inte inom 30 s."
done
else
fail "REDIS_URL svarar inte i produktion startar detta skript aldrig tjänster. Kontrollera Redis/nätverk."
fi
say "GRIND 2 OK: databas + Redis nåbara"
# ---------------------------------------------------------------------------
# GRIND 3: Beroenden + verifiering av källkod
# ---------------------------------------------------------------------------
say "pnpm install (frozen lockfile) …"
pnpm install --frozen-lockfile >/dev/null
say "Varumärkesvakt …"
./scripts/brand-guard.sh
say "Typecheck + tester + build …"
pnpm typecheck >/dev/null
pnpm test >/dev/null
pnpm build >/dev/null
say "GRIND 3 OK: typecheck, tester, build, varumärkesvakt"
# ---------------------------------------------------------------------------
# GRIND 4: Migrationer (produktion: med backup före spec §63)
# ---------------------------------------------------------------------------
if [ "$MODE" = "production" ]; then
command -v pg_dump >/dev/null || fail "pg_dump saknas krävs för backup före migration."
mkdir -p "$ROOT/backups"
DBURL=$(get_env DATABASE_URL)
say "Backup före migration …"
pg_dump "$DBURL" --format=custom --file="$ROOT/backups/pre-first-deploy-$(date +%Y%m%d_%H%M%S).dump" \
|| fail "Backup misslyckades ingen migration utan backup (spec §63)."
fi
say "Kör migrationer …"
pnpm db:migrate
if [ "$MODE" = "staging" ]; then
say "Seedar databasen (endast staging) …"
pnpm db:seed
fi
say "GRIND 4 OK: databas migrerad$([ "$MODE" = staging ] && echo ' + seedad')"
# ---------------------------------------------------------------------------
# GRIND 5: AAMOS-utvärdering (spec §40)
# ---------------------------------------------------------------------------
say "Kör AAMOS-utvärderingssviten mot läge: $AAMOS_MODE_VAL"
pnpm eval:aamos || fail "Utvärderingssviten UNDERKÄND deploya inte (spec §40)."
say "GRIND 5 OK: eval-sviten GODKÄND"
# ---------------------------------------------------------------------------
# GRIND 6: Starta tjänsterna + hälsokontroll
# ---------------------------------------------------------------------------
API_PORT=$(get_env API_PORT); API_PORT=${API_PORT:-4000}
say "Startar API + worker (bakgrund) …"
# Idempotent omstart: döda ev. tidigare instanser från detta skript.
pkill -f "apps/api/dist/index.js" 2>/dev/null || true
pkill -f "apps/worker/dist/index.js" 2>/dev/null || true
sleep 1
mkdir -p "$ROOT/logs"
nohup node apps/api/dist/index.js >> "$ROOT/logs/api.log" 2>&1 &
nohup node apps/worker/dist/index.js >> "$ROOT/logs/worker.log" 2>&1 &
for i in $(seq 1 20); do
curl -fsS "http://127.0.0.1:$API_PORT/healthz" >/dev/null 2>&1 && break
sleep 1
[ "$i" = 20 ] && { tail -20 "$ROOT/logs/api.log"; fail "API svarade inte på /healthz inom 20 s."; }
done
curl -fsS "http://127.0.0.1:$API_PORT/healthz"
echo
curl -fsS "http://127.0.0.1:$API_PORT/readyz" >/dev/null || fail "/readyz underkänd DB/Redis-koppling trasig."
grep -q "Worker igång" "$ROOT/logs/worker.log" || sleep 3
grep -q "Worker igång" "$ROOT/logs/worker.log" || fail "Workern startade inte (se logs/worker.log)."
say "GRIND 6 OK: API + worker uppe, healthz/readyz gröna"
# ---------------------------------------------------------------------------
# GRIND 7: Funktionsrök (registrera -> logga in -> läsa recept)
# ---------------------------------------------------------------------------
SMOKE_EMAIL="smoke-$(date +%s)@example.com"
B="http://127.0.0.1:$API_PORT"
TOKEN=$(curl -fsS -X POST "$B/v1/auth/register" -H 'content-type: application/json' \
-d "{\"email\":\"$SMOKE_EMAIL\",\"password\":\"FirstDeploy123!\",\"displayName\":\"Deploy Smoke\",\"locale\":\"sv-SE\"}" \
| node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).accessToken))")
[ -n "$TOKEN" ] || fail "Registrering i röktestet misslyckades."
RECIPES=$(curl -fsS "$B/v1/recipes?limit=1" -H "Authorization: Bearer $TOKEN" \
| node -e "let d='';process.stdin.on('data',c=>d+=c).on('end',()=>console.log(JSON.parse(d).recipes.length))")
if [ "$MODE" = "staging" ]; then
[ "$RECIPES" -ge 1 ] || fail "Röktestet hittade inga recept (seed saknas?)."
fi
say "GRIND 7 OK: register -> login -> recept fungerar"
# ---------------------------------------------------------------------------
# GRIND 8: Integrationsstatus vad är PÅ RIKTIGT och vad är mock?
# ---------------------------------------------------------------------------
EMAIL_MODE_VAL=$(get_env EMAIL_MODE); EMAIL_MODE_VAL=${EMAIL_MODE_VAL:-log}
S3_MODE_VAL=$(get_env S3_MODE); S3_MODE_VAL=${S3_MODE_VAL:-mock}
aamos_status=$([ "$AAMOS_MODE_VAL" = "http" ] && echo "RIKTIG (http)" || echo "mock")
email_status=$([ "$EMAIL_MODE_VAL" = "smtp" ] && echo "RIKTIG (smtp)" || echo "log (mejl till logs/api.log)")
s3_status=$([ "$S3_MODE_VAL" = "aws" ] && echo "RIKTIG (S3: $(get_env S3_BUCKET))" || echo "mock (lokal disk)")
say "-----------------------------------------------"
say "INTEGRATIONSSTATUS:"
say " AAMOS (AI): $aamos_status"
say " E-post: $email_status"
say " Fillagring: $s3_status"
say "-----------------------------------------------"
if [ "${REQUIRE_REAL:-0}" = "1" ]; then
MOCKS=""
[ "$AAMOS_MODE_VAL" != "http" ] && MOCKS="$MOCKS AAMOS(AAMOS_MODE=http+URL+nyckel)"
[ "$EMAIL_MODE_VAL" != "smtp" ] && MOCKS="$MOCKS E-post(EMAIL_MODE=smtp+SMTP_*+MAIL_FROM)"
[ "$S3_MODE_VAL" != "aws" ] && MOCKS="$MOCKS Fillagring(S3_MODE=aws+S3_*)"
if [ -n "$MOCKS" ]; then
fail "REQUIRE_REAL=1 men följande kör fortfarande mock:$MOCKS fyll i .env och kör om."
fi
say "GRIND 8 OK: REQUIRE_REAL uppfyllt alla integrationer är riktiga."
fi
# ---------------------------------------------------------------------------
# Klart
# ---------------------------------------------------------------------------
say "==============================================="
say "ALLA GRINDAR GRÖNA $MODE är uppe."
say "API: http://127.0.0.1:$API_PORT (Swagger: /docs)"
say "Loggar: $ROOT/logs/api.log, $ROOT/logs/worker.log"
if [ "$MODE" = "staging" ]; then
say "Admin: bygg finns i apps/admin/dist servera med valfri statisk server."
say "OBS: staging kör AAMOS_MODE=$AAMOS_MODE_VAL."
else
say "NÄSTA: peka reverse proxy (TLS) mot 127.0.0.1:$API_PORT och verifiera utifrån."
fi
say "==============================================="