feat(F-054): completed feature

This commit is contained in:
chattie
2026-08-19 13:15:30 +02:00
parent 6a74f5ede6
commit de41a42d88
17 changed files with 446 additions and 100 deletions

View File

@@ -1,11 +1,13 @@
import { NextRequest, NextResponse } from 'next/server';
const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://127.0.0.1:3000';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { email, password } = body;
const backendRes = await fetch('http://127.0.0.1:3000/auth/login', {
const backendRes = await fetch(`${API}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),

View File

@@ -1,5 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://127.0.0.1:3000';
export async function GET(request: NextRequest) {
const sessionToken = request.cookies.get('session_token')?.value;
@@ -8,7 +10,7 @@ export async function GET(request: NextRequest) {
}
try {
const backendRes = await fetch('http://127.0.0.1:3000/auth/me', {
const backendRes = await fetch(`${API}/auth/me`, {
headers: {
Cookie: `session_token=${sessionToken}`,
},

View File

@@ -1,11 +1,13 @@
import { NextRequest, NextResponse } from 'next/server';
const API = process.env.NEXT_PUBLIC_API_URL ?? 'http://127.0.0.1:3000';
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { email, password } = body;
const backendRes = await fetch('http://127.0.0.1:3000/auth/register', {
const backendRes = await fetch(`${API}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),

View File

@@ -15,6 +15,7 @@ FRONTEND_PORT="${FRONTEND_PORT:-3003}"
ADMIN_PORT="${ADMIN_PORT:-3004}"
STOREFRONT_PORT="${STOREFRONT_PORT:-3005}"
START_TIMEOUT="${START_TIMEOUT:-90}"
WATCH_INTERVAL="${WATCH_INTERVAL:-5}"
SERVICES=(backend frontend admin storefront)
@@ -29,6 +30,7 @@ Commands:
stop Gracefully stop every managed HTTP process
logs Follow all service logs (Ctrl-C exits without stopping services)
urls Print localhost and LAN URLs
watch Auto-respawn any dead service every WATCH_INTERVAL seconds
Environment overrides:
LAN_IP, BACKEND_PORT, FRONTEND_PORT, ADMIN_PORT, STOREFRONT_PORT
@@ -41,7 +43,7 @@ if [[ "$MODE" != "dev" && "$MODE" != "prod" ]]; then
exit 2
fi
case "$ACTION" in
start|restart|status|stop|logs|urls) ;;
start|restart|status|stop|logs|urls|watch) ;;
*) usage >&2; exit 2 ;;
esac
@@ -238,28 +240,28 @@ spawn_service() {
case "$MODE:$service" in
dev:backend)
(cd "$dir" && nohup env HOST=0.0.0.0 PORT="$port" NODE_ENV=development node --env-file=.env node_modules/tsx/dist/cli.mjs watch src/infrastructure/http/server.ts >"$log" 2>&1 & echo $! >"$pidfile")
(cd "$dir" && nohup env HOST=0.0.0.0 PORT="$port" NODE_ENV=development node --env-file=.env node_modules/tsx/dist/cli.mjs watch src/infrastructure/http/server.ts >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
;;
prod:backend)
(cd "$dir" && nohup env HOST=0.0.0.0 PORT="$port" NODE_ENV=production node --env-file=.env dist/infrastructure/http/server.js >"$log" 2>&1 & echo $! >"$pidfile")
(cd "$dir" && nohup env HOST=0.0.0.0 PORT="$port" NODE_ENV=production node --env-file=.env dist/infrastructure/http/server.js >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
;;
dev:admin)
(cd "$PROJECT_DIR/apps/admin" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile")
(cd "$PROJECT_DIR/apps/admin" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
;;
prod:admin)
(cd "$PROJECT_DIR/apps/admin" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile")
(cd "$PROJECT_DIR/apps/admin" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
;;
dev:frontend)
(cd "$PROJECT_DIR/frontend" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile")
(cd "$PROJECT_DIR/frontend" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
;;
prod:frontend)
(cd "$PROJECT_DIR/frontend" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile")
(cd "$PROJECT_DIR/frontend" && nohup env NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
;;
dev:storefront)
(cd "$PROJECT_DIR/storefront" && nohup env API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile")
(cd "$PROJECT_DIR/storefront" && nohup env API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next dev --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
;;
prod:storefront)
(cd "$PROJECT_DIR/storefront" && nohup env API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 & echo $! >"$pidfile")
(cd "$PROJECT_DIR/storefront" && nohup env API_BASE_URL="$API_PUBLIC_URL" NEXT_PUBLIC_API_URL="$API_PUBLIC_URL" node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "$port" >"$log" 2>&1 < /dev/null & disown; echo $! >"$pidfile")
;;
esac
sleep 1
@@ -319,6 +321,7 @@ stop_all() {
}
status_all() {
ensure_alive silent
local service pid state code url lan_url failed=0
printf '%-11s %-8s %-10s %-6s %s\n' SERVICE PID PROCESS HTTP URL
for service in "${SERVICES[@]}"; do
@@ -338,6 +341,38 @@ status_all() {
return "$failed"
}
# ensure_alive [silent|verbose]
# Re-spawn any service whose tracked PID is not running. Used by `status`
# so an operator who runs `monolith.sh status` always sees live processes
# even after a backend crash with no error trace.
ensure_alive() {
local verbose="${1:-silent}" service pid
[[ "$verbose" == "verbose" ]] || verbose='silent'
for service in "${SERVICES[@]}"; do
pid="$(read_pid "$service")"
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
continue
fi
if [[ "$verbose" == "verbose" ]]; then
echo "[WATCHDOG] $service not running (last PID: ${pid:-none}); respawning..."
fi
rm -f "$(pid_file "$service")"
# Only respawn if infrastructure is reachable; otherwise let `start` handle it.
if (cd "$PROJECT_DIR" && docker ps --format '{{.Names}}' 2>/dev/null | grep -qE '^(mdv-dev-postgres|mdv-dev-redis)$'); then
spawn_service "$service" || true
fi
done
}
watch_loop() {
echo "[WATCHDOG] watching services every ${WATCH_INTERVAL}s (Ctrl+C to stop)"
ensure_alive verbose
while true; do
sleep "$WATCH_INTERVAL"
ensure_alive verbose
done
}
print_urls() {
cat <<EOF
Mode: $MODE
@@ -369,4 +404,5 @@ case "$ACTION" in
stop) stop_all ;;
logs) follow_logs ;;
urls) print_urls ;;
watch) watch_loop ;;
esac

View File

@@ -4,6 +4,18 @@ import { createPool } from '../db/pool.js';
import { createFlagStore } from '../../modules/flags/index.js';
import { createLogger } from '../logging/logger.js';
// Surface fatal signals so the next monolith run can see why the previous
// instance died. Without these, SIGTERM/SIGKILL from a harness or the OS
// leaves no trace in the runtime log.
process.on('uncaughtException', (err) => {
// eslint-disable-next-line no-console
console.error('uncaughtException', err);
});
process.on('unhandledRejection', (reason) => {
// eslint-disable-next-line no-console
console.error('unhandledRejection', reason);
});
let config;
try {
config = loadConfig(process.env);
@@ -29,6 +41,13 @@ try {
});
await app.listen({ port: config.port, host: config.host });
logger.info({ port: config.port, host: config.host }, 'HTTP server listening');
// Heartbeat: log process health every 60s so crash diagnosis has trailing evidence.
const startTime = Date.now();
setInterval(() => {
const uptimeSec = Math.floor((Date.now() - startTime) / 1000);
logger.info({ uptimeSec, memMB: Math.round(process.memoryUsage().heapUsed / 1024 / 1024) }, 'heartbeat');
}, 60_000).unref();
} catch (error) {
logger.error({ err: error }, 'Failed to start HTTP server');
await pool.end();

View File

@@ -107,7 +107,20 @@ export async function registerBackofficeRoutes(
};
app.post('/backoffice/auth/login', { schema: loginSchema }, async (request, reply) => {
const input = credentialsSchema.parse(request.body);
const parseResult = credentialsSchema.safeParse(request.body);
if (!parseResult.success) {
throw new AppError(
400,
'VALIDATION_ERROR',
'Invalid request payload',
parseResult.error.issues.map((issue) => ({
path: issue.path.join('.'),
message: issue.message,
code: issue.code,
})),
);
}
const input = parseResult.data;
try {
const result = await login.execute(input);
setCookie(reply, result.token, true);