safeLoad(); } $appEnv = getenv('APP_ENV') ?: 'production'; if ($appEnv !== 'production') { ini_set('display_errors', '1'); error_reporting(E_ALL); } else { ini_set('display_errors', '0'); } // --------------------------------------------------------------------------- // Migrations — cheap no-op when already current // --------------------------------------------------------------------------- try { $pdo = Connection::pdo(); (new Migrator($pdo))->migrate(); } catch (\Throwable $e) { http_response_code(500); header('Content-Type: text/plain; charset=utf-8'); echo "Database bootstrap failed.\n"; if ($appEnv !== 'production') { echo $e->getMessage() . "\n"; } exit; } // --------------------------------------------------------------------------- // Shared services // --------------------------------------------------------------------------- $view = new View(APP_ROOT . '/views'); $users = new UserRepository($pdo); $workers = new WorkerRepository($pdo); $sprints = new SprintRepository($pdo); $sprintWeeks = new SprintWeekRepository($pdo); $sprintWorkers = new SprintWorkerRepository($pdo); $swDays = new SprintWorkerDayRepository($pdo); $audit = new AuditLogger($pdo); $auth = new AuthController($pdo, $users, $audit, $view); $workerCtrl = new WorkerController($pdo, $users, $workers, $audit, $view); $sprintCtrl = new SprintController( $pdo, $users, $sprints, $sprintWeeks, $sprintWorkers, $swDays, $workers, $audit, $view, ); // --------------------------------------------------------------------------- // Routing // --------------------------------------------------------------------------- $router = new Router(); $router->get('/', function (Request $req) use ($view, $pdo, $users, $sprints, $appEnv): Response { $currentUser = SessionGuard::currentUser($users); $schemaVersion = (int) $pdo->query( 'SELECT COALESCE(MAX(version), 0) FROM schema_version' )->fetchColumn(); $sprintRows = $currentUser === null ? [] : $sprints->allWithCounts(); return Response::html($view->render('home', [ 'title' => 'Sprint Planner', 'currentUser' => $currentUser, 'schemaVersion' => $schemaVersion, 'dbPath' => Connection::path(), 'appEnv' => $appEnv, 'oidcConfigured' => OidcClient::isConfigured(), 'localAdminEnabled' => LocalAdmin::isEnabled(), 'authError' => isset($req->query['auth_error']), 'csrfToken' => SessionGuard::csrfToken(), 'sprintRows' => $sprintRows, ])); }); $router->get('/healthz', fn() => Response::text('ok')); $router->get('/auth/login', $auth->login(...)); $router->get('/auth/callback', $auth->callback(...)); $router->post('/auth/logout', $auth->logout(...)); $router->get('/auth/local', $auth->loginLocalForm(...)); $router->post('/auth/local', $auth->loginLocal(...)); $router->get('/workers', $workerCtrl->index(...)); $router->post('/workers', $workerCtrl->create(...)); $router->post('/workers/{id}', $workerCtrl->update(...)); $router->get('/sprints/new', $sprintCtrl->newForm(...)); $router->post('/sprints', $sprintCtrl->create(...)); $router->get('/sprints/{id}', $sprintCtrl->show(...)); $router->get('/sprints/{id}/settings', $sprintCtrl->settings(...)); // JSON mutation endpoints (admin, CSRF via X-CSRF-Token header): $router->patch('/sprints/{id}', $sprintCtrl->updateMeta(...)); $router->post('/sprints/{id}/weeks', $sprintCtrl->replaceWeeks(...)); $router->post('/sprints/{id}/workers', $sprintCtrl->addWorker(...)); $router->delete('/sprints/{id}/workers/{sw_id}', $sprintCtrl->removeWorker(...)); $router->post('/sprints/{id}/workers/reorder', $sprintCtrl->reorderWorkers(...)); $router->patch('/sprints/{id}/workers/{sw_id}', $sprintCtrl->updateWorker(...)); // Phase 5 — Arbeitstage grid: $router->patch('/sprints/{id}/week-cells', $sprintCtrl->updateWeekCells(...)); $router->patch('/sprints/{id}/week/{week_id}', $sprintCtrl->updateWeekMax(...)); // --------------------------------------------------------------------------- // Dispatch // --------------------------------------------------------------------------- $request = Request::fromGlobals(); $response = $router->dispatch($request); $response->send(); // Flush the output buffer opened at the top. if (ob_get_level() > 0) { @ob_end_flush(); }