feat(planning): ship holiday-aware planning and assistant upgrades

This commit is contained in:
2026-03-28 22:49:28 +01:00
parent 2a005794e7
commit 4f48afe7b4
151 changed files with 17738 additions and 1940 deletions
+41
View File
@@ -0,0 +1,41 @@
#!/usr/bin/env node
import { URL } from "node:url";
import { loadWorkspaceEnv, resolveWorkspaceEnvPath } from "./load-env.mjs";
const envPath = loadWorkspaceEnv();
const expectedDatabase = process.argv[2] ?? "capakraken";
const rawUrl = process.env.DATABASE_URL;
if (!rawUrl) {
console.error(`DATABASE_URL is not configured. Expected it from ${envPath ?? resolveWorkspaceEnvPath()}.`);
process.exit(1);
}
let parsed;
try {
parsed = new URL(rawUrl);
} catch (error) {
console.error(`DATABASE_URL is invalid: ${error instanceof Error ? error.message : String(error)}`);
process.exit(1);
}
const databaseName = parsed.pathname.replace(/^\/+/, "");
const target = `${parsed.protocol}//${decodeURIComponent(parsed.username)}@${parsed.hostname}${parsed.port ? `:${parsed.port}` : ""}/${databaseName}`;
if (!databaseName) {
console.error(`DATABASE_URL does not contain a database name. Target=${target}`);
process.exit(1);
}
if (databaseName !== expectedDatabase) {
console.error(`Unexpected database target '${databaseName}'. Expected '${expectedDatabase}'. Target=${target}`);
process.exit(1);
}
if (databaseName === "planarchy") {
console.error(`Refusing to continue with deprecated database '${databaseName}'. Target=${target}`);
process.exit(1);
}
console.log(`DB target OK: ${target}`);
+2 -3
View File
@@ -2,17 +2,16 @@
# Remove SUPERUSER from the application database user
# Run after initial setup: bash scripts/harden-postgres.sh
CONTAINER="planarchy-postgres-1" # Note: container name may still use old naming
DB_USER="capakraken"
DB_NAME="capakraken"
echo "Hardening PostgreSQL for $DB_USER..."
# Remove SUPERUSER privilege
docker exec $CONTAINER psql -U postgres -c "ALTER USER $DB_USER NOSUPERUSER;"
docker compose exec -T postgres psql -U postgres -c "ALTER USER $DB_USER NOSUPERUSER;"
# Grant only needed permissions
docker exec $CONTAINER psql -U postgres -d $DB_NAME -c "
docker compose exec -T postgres psql -U postgres -d $DB_NAME -c "
GRANT CONNECT ON DATABASE $DB_NAME TO $DB_USER;
GRANT USAGE ON SCHEMA public TO $DB_USER;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO $DB_USER;
+45
View File
@@ -0,0 +1,45 @@
import { existsSync, readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
function resolveWorkspaceRoot() {
return resolve(dirname(fileURLToPath(import.meta.url)), "..");
}
export function resolveWorkspaceEnvPath() {
return resolve(resolveWorkspaceRoot(), ".env");
}
export function loadWorkspaceEnv() {
const envPath = resolveWorkspaceEnvPath();
if (!existsSync(envPath)) {
return envPath;
}
const contents = readFileSync(envPath, "utf8");
for (const rawLine of contents.split(/\r?\n/u)) {
const line = rawLine.trim();
if (!line || line.startsWith("#")) {
continue;
}
const separatorIndex = line.indexOf("=");
if (separatorIndex <= 0) {
continue;
}
const key = line.slice(0, separatorIndex).trim();
const rawValue = line.slice(separatorIndex + 1).trim();
const quoted =
(rawValue.startsWith("\"") && rawValue.endsWith("\""))
|| (rawValue.startsWith("'") && rawValue.endsWith("'"));
const value = quoted ? rawValue.slice(1, -1) : rawValue;
if (process.env[key] == null) {
process.env[key] = value;
}
}
return envPath;
}
+1 -1
View File
@@ -2,7 +2,7 @@
set -euo pipefail
cd "$(dirname "$0")/.."
echo "Restarting Planarchy..."
echo "Restarting CapaKraken..."
echo ""
# Stop
+9 -18
View File
@@ -2,7 +2,7 @@
set -euo pipefail
cd "$(dirname "$0")/.."
echo "Starting Planarchy..."
echo "Starting CapaKraken..."
# 1. Start Docker services
echo " Starting PostgreSQL + Redis..."
@@ -12,40 +12,31 @@ sleep 2
# 2. Wait for PostgreSQL to be healthy
echo " Waiting for PostgreSQL..."
for i in {1..30}; do
if docker exec capakraken-postgres-1 pg_isready -U capakraken -q 2>/dev/null; then
if docker compose exec -T postgres pg_isready -U capakraken -d capakraken -q 2>/dev/null; then
break
fi
sleep 1
done
# 3. Regenerate Prisma client
echo " Generating Prisma client..."
pnpm --filter @capakraken/db exec prisma generate --no-hints 2>/dev/null
# 3. Start the web app in Docker for a stable lifecycle
echo " Starting app container on port 3100..."
docker compose --profile full up -d app
# 4. Clear stale Next.js cache
rm -rf apps/web/.next
# 5. Start Next.js dev server
echo " Starting Next.js on port 3100..."
nohup pnpm --filter @capakraken/web dev > /tmp/capakraken-dev.log 2>&1 &
echo $! > /tmp/capakraken-dev.pid
# 6. Wait for server to be ready
# 4. Wait for server to be ready
echo " Waiting for server..."
for i in {1..30}; do
if curl -sf http://localhost:3100/api/health > /dev/null 2>&1; then
echo ""
echo "Planarchy is running!"
echo "CapaKraken is running!"
curl -s http://localhost:3100/api/ready | python3 -m json.tool 2>/dev/null || curl -s http://localhost:3100/api/ready
echo ""
echo " URL: http://localhost:3100"
echo " Logs: tail -f /tmp/capakraken-dev.log"
echo " PID: $(cat /tmp/capakraken-dev.pid)"
echo " Logs: docker logs -f capakraken-app-1"
exit 0
fi
sleep 1
done
echo "ERROR: Server failed to start within 30 seconds"
echo "Check logs: tail -50 /tmp/capakraken-dev.log"
echo "Check logs: docker logs --tail 100 capakraken-app-1"
exit 1
+5 -5
View File
@@ -2,9 +2,9 @@
set -euo pipefail
cd "$(dirname "$0")/.."
echo "Stopping Planarchy..."
echo "Stopping CapaKraken..."
# 1. Stop Next.js dev server
# 1. Stop any legacy local dev server
if [ -f /tmp/capakraken-dev.pid ]; then
PID=$(cat /tmp/capakraken-dev.pid)
if kill -0 "$PID" 2>/dev/null; then
@@ -20,8 +20,8 @@ fi
fuser -k 3100/tcp 2>/dev/null || true
# 2. Stop Docker services (keep data volumes)
echo " Stopping PostgreSQL + Redis..."
docker compose stop postgres redis 2>/dev/null || true
echo " Stopping app, PostgreSQL and Redis..."
docker compose --profile full stop app postgres redis 2>/dev/null || true
echo ""
echo "Planarchy stopped."
echo "CapaKraken stopped."
+26
View File
@@ -0,0 +1,26 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import { loadWorkspaceEnv } from "./load-env.mjs";
loadWorkspaceEnv();
const args = process.argv.slice(2);
if (args.length === 0) {
console.error("Usage: node scripts/with-env.mjs <command> [args...]");
process.exit(1);
}
const result = spawnSync(args[0], args.slice(1), {
stdio: "inherit",
env: process.env,
});
if (result.error) {
console.error(result.error.message);
process.exit(1);
}
process.exit(result.status ?? 1);