Stop pretending Vercel is an option
It was never used. The repository lives on a self-hosted Gitea, which Vercel's git integration cannot connect to at all — so the documented route amounted to "mirror to GitHub first", and nobody did. vercel.json is gone, and with it the branch in next.config.ts that switched off `output: "standalone"` when the VERCEL variable was present. That branch was the only functional trace; everything else was documentation and comments describing a second deployment path that did not exist. DEPLOYMENT.md loses its "two supported ways" framing and the whole Vercel section — about fifty lines. Several statements next to it were stale for a different reason and are corrected in the same pass: the outbound-firewall table still listed Supabase's pooler (the database is a container now, nothing leaves the server), the prerequisites still demanded an existing Supabase project, and the .env table still asked for a pooler connection string instead of the two new passwords. The nightly job is described as what it is — a container in docker-compose.yml — rather than as a replacement for Vercel Cron. Migrations keep their references: two comments from July mention Vercel Cron, and they describe what was true when they were written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -57,7 +57,9 @@ AUTH_MICROSOFT_ENTRA_ID_ISSUER=https://login.microsoftonline.com/<verzeichnis-id
|
|||||||
# Auth.js die Rückruf-Adresse aus den Request-Headern.
|
# Auth.js die Rückruf-Adresse aus den Request-Headern.
|
||||||
AUTH_URL=
|
AUTH_URL=
|
||||||
|
|
||||||
# Shared secret Vercel Cron sends as `Authorization: Bearer <value>` when it
|
# Gemeinsames Geheimnis, mit dem sich der nächtliche Lauf ausweist: der
|
||||||
# calls /api/cron/apply-pending-changes (set the same value in the Vercel
|
# `cron`-Container schickt es als `Authorization: Bearer <wert>` an
|
||||||
# project's env vars). Generate with e.g. `openssl rand -hex 32`.
|
# /api/cron/apply-pending-changes. Ohne angemeldete Person gibt es keine
|
||||||
|
# Sitzung, an der die Route den Aufruf prüfen könnte.
|
||||||
|
# Erzeugen mit: openssl rand -hex 32
|
||||||
CRON_SECRET=
|
CRON_SECRET=
|
||||||
|
|||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -34,9 +34,6 @@ yarn-error.log*
|
|||||||
.env*
|
.env*
|
||||||
!.env.example
|
!.env.example
|
||||||
|
|
||||||
# vercel
|
|
||||||
.vercel
|
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|||||||
104
DEPLOYMENT.md
104
DEPLOYMENT.md
@@ -1,71 +1,14 @@
|
|||||||
# Deployment
|
# Deployment
|
||||||
|
|
||||||
Zwei Wege, beide unterstützt. Das Abbild ist umgebungsneutral — es gibt keine
|
Die Anwendung läuft als Docker-Container auf einem eigenen Linux-Server,
|
||||||
Werte mehr, die beim Bauen eingebacken werden —, ein Wechsel ist also
|
zusammen mit ihrer Datenbank. Das Abbild ist umgebungsneutral — es gibt
|
||||||
jederzeit möglich.
|
keine Werte, die beim Bauen eingebacken werden; alles kommt zur Laufzeit
|
||||||
|
aus `.env`.
|
||||||
|
|
||||||
| | passt, wenn |
|
Zu klären, bevor es losgeht: die Umgebungsvariablen aus
|
||||||
|---|---|
|
[Abschnitt 1](#1-env-anlegen), die Umleitungs-URI in der
|
||||||
| [Vercel](#vercel) | ihr nichts betreiben wollt; schnellster Weg |
|
Entra-Registrierung, und dass eine neue Person nach ihrer ersten Anmeldung
|
||||||
| [Docker](#deployment-mit-docker) | es in eure eigene Infrastruktur soll |
|
eine `profiles`-Zeile braucht (siehe [docs/entra-sso.md](docs/entra-sso.md)).
|
||||||
|
|
||||||
**In beiden Fällen gleich:** die Umgebungsvariablen aus [Abschnitt 1](#1-env-anlegen),
|
|
||||||
die Umleitungs-URI in der Entra-Registrierung, und dass eine neue Person nach
|
|
||||||
ihrer ersten Anmeldung eine `profiles`-Zeile braucht (siehe
|
|
||||||
[docs/entra-sso.md](docs/entra-sso.md)).
|
|
||||||
|
|
||||||
## Vercel
|
|
||||||
|
|
||||||
Das Repository liegt auf `git.elycon.solutions` — einem selbst betriebenen
|
|
||||||
Git. **Vercels Git-Anbindung kann nur GitHub, GitLab und Bitbucket**, dieses
|
|
||||||
Repository lässt sich dort also nicht verknüpfen. Zwei Möglichkeiten:
|
|
||||||
|
|
||||||
### a) Von der Arbeitsstation ausrollen (ohne GitHub)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx vercel login
|
|
||||||
npx vercel link
|
|
||||||
npx vercel --prod
|
|
||||||
```
|
|
||||||
|
|
||||||
Funktioniert mit jedem Repository. Der Preis: kein automatisches Ausrollen
|
|
||||||
bei einem Push — jede Veröffentlichung ist ein bewusster Befehl. Für zwei
|
|
||||||
Personen ist das eher Vorteil als Nachteil.
|
|
||||||
|
|
||||||
### b) Zusätzlich nach GitHub spiegeln
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git remote add github git@github.com:<konto>/alpenwerk-hr.git
|
|
||||||
git push github feat/sap-om-org-model
|
|
||||||
```
|
|
||||||
|
|
||||||
Danach das GitHub-Repository in Vercel verbinden. Ab dann rollt jeder Push
|
|
||||||
aus. Zwei Fernziele bedeuten aber auch: beide müssen gepflegt werden.
|
|
||||||
|
|
||||||
### Danach
|
|
||||||
|
|
||||||
1. **Umgebungsvariablen** im Vercel-Projekt setzen (Settings → Environment
|
|
||||||
Variables), dieselben wie in [Abschnitt 1](#1-env-anlegen). `AUTH_URL` ist
|
|
||||||
nicht nötig, Vercel setzt den Host selbst.
|
|
||||||
2. **Umleitungs-URI** in der Entra-Registrierung ergänzen:
|
|
||||||
`https://<projekt>.vercel.app/api/auth/callback/microsoft-entra-id`
|
|
||||||
3. Der nächtliche Lauf ist über `vercel.json` bereits eingerichtet.
|
|
||||||
|
|
||||||
Zwei Eigenheiten der Plattform, die im Code berücksichtigt sind:
|
|
||||||
`output: "standalone"` entfällt dort automatisch (Vercel baut selbst), und
|
|
||||||
`/api/import` ist auf 60 Sekunden begrenzt — die Obergrenze des kostenlosen
|
|
||||||
Tarifs. Im Pro-Tarif liessen sich 300 setzen, falls eine Importdatei mit
|
|
||||||
vielen tausend Zeilen ansteht.
|
|
||||||
|
|
||||||
Der Verbindungspool passt zu serverlosen Aufrufen, **weil** `DATABASE_URL`
|
|
||||||
auf den Transaktions-Modus zeigt (Port 6543). Mit dem Sitzungs-Modus wären
|
|
||||||
die 15 Verbindungen des Tarifs nach wenigen gleichzeitigen Aufrufen
|
|
||||||
verbraucht.
|
|
||||||
|
|
||||||
## Deployment mit Docker
|
|
||||||
|
|
||||||
Dieser Guide beschreibt, wie die App stattdessen als Docker-Container auf
|
|
||||||
einem eigenen Linux-Server läuft.
|
|
||||||
|
|
||||||
## Was wird containerisiert – und was nicht
|
## Was wird containerisiert – und was nicht
|
||||||
|
|
||||||
@@ -84,12 +27,10 @@ einem eigenen Linux-Server läuft.
|
|||||||
- **Ebenfalls nicht containerisiert:** die Anmeldung. Sie läuft über
|
- **Ebenfalls nicht containerisiert:** die Anmeldung. Sie läuft über
|
||||||
Microsoft Entra ID; die App hält nur das Sitzungscookie (Auth.js). Es gibt
|
Microsoft Entra ID; die App hält nur das Sitzungscookie (Auth.js). Es gibt
|
||||||
keinen Anmeldedienst, der mit ausgerollt werden müsste.
|
keinen Anmeldedienst, der mit ausgerollt werden müsste.
|
||||||
- **Ersetzt:** der Vercel-Cron-Job aus `vercel.json` (täglich 03:00 Uhr,
|
- **Der nächtliche Lauf** ist ein eigener Container (`cron`): täglich 03:00
|
||||||
ruft `/api/cron/apply-pending-changes` auf, um fällige Versetzungen/
|
Uhr ruft er `/api/cron/apply-pending-changes` auf, um fällige Versetzungen,
|
||||||
Beförderungen/Karenz/Reorg-Änderungen zu übernehmen). Da es außerhalb von
|
Beförderungen, Karenz- und Reorg-Änderungen zu übernehmen. Ausgewiesen wird
|
||||||
Vercel kein Vercel-Cron gibt, übernimmt das im `docker-compose.yml`
|
der Aufruf über `CRON_SECRET` — es gibt dabei keine angemeldete Person.
|
||||||
enthaltene `cron`-Sidecar-Container diese Aufgabe mit demselben Schema
|
|
||||||
und demselben Bearer-Secret, das die Route bereits erwartet.
|
|
||||||
|
|
||||||
## Betrieb im Firmennetz — zwei Dinge vorab
|
## Betrieb im Firmennetz — zwei Dinge vorab
|
||||||
|
|
||||||
@@ -103,12 +44,13 @@ nichts, ausgehend zwingend:
|
|||||||
| Ziel | Wofür | Ohne das |
|
| Ziel | Wofür | Ohne das |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `login.microsoftonline.com` (443) | Auth.js tauscht den Anmeldecode **serverseitig** gegen ein Token und lädt die Konfiguration des Ausstellers | keine Anmeldung möglich |
|
| `login.microsoftonline.com` (443) | Auth.js tauscht den Anmeldecode **serverseitig** gegen ein Token und lädt die Konfiguration des Ausstellers | keine Anmeldung möglich |
|
||||||
| Die Datenbank (Supabase: `*.pooler.supabase.com`, 6543) | jede Abfrage | die App startet, zeigt aber nichts |
|
|
||||||
|
|
||||||
Dass die Anmeldung im Browser der Person stattfindet, genügt **nicht** — der
|
Dass die Anmeldung im Browser der Person stattfindet, genügt **nicht** — der
|
||||||
Tausch von Code gegen Token läuft vom Server aus. Liegt die VM in einem
|
Tausch von Code gegen Token läuft vom Server aus. Liegt die VM in einem
|
||||||
abgeschotteten Netz, ist entweder ein Proxy nötig oder eine PostgreSQL-
|
abgeschotteten Netz, braucht es dafür einen Proxy.
|
||||||
Instanz im selben Netz statt Supabase.
|
|
||||||
|
Die Datenbank taucht hier nicht mehr auf: sie läuft als Container daneben, im
|
||||||
|
selben Compose-Netz. Nach aussen geht dafür nichts.
|
||||||
|
|
||||||
**2. HTTPS ist Pflicht, auch intern.** Entra ID akzeptiert `http` nur für
|
**2. HTTPS ist Pflicht, auch intern.** Entra ID akzeptiert `http` nur für
|
||||||
`localhost`. Der praktikable Weg ohne öffentliche Erreichbarkeit: ein
|
`localhost`. Der praktikable Weg ohne öffentliche Erreichbarkeit: ein
|
||||||
@@ -126,11 +68,9 @@ bei zwanzig nicht.
|
|||||||
|
|
||||||
## Voraussetzungen
|
## Voraussetzungen
|
||||||
|
|
||||||
- Docker + Docker Compose (v2, das im Docker Desktop/Docker Engine
|
- Docker + Docker Compose (v2, das in Docker Engine enthaltene
|
||||||
enthaltene `docker compose`) auf dem Zielserver.
|
`docker compose`) auf dem Zielserver. Sonst nichts — weder Node noch psql;
|
||||||
- Ein bestehendes Supabase-Projekt mit den Migrationen aus
|
was gebraucht wird, kommt aus Containern.
|
||||||
`supabase/migrations/` bereits eingespielt (`supabase db push` bzw. wie
|
|
||||||
bisher).
|
|
||||||
|
|
||||||
## 1. `.env` anlegen
|
## 1. `.env` anlegen
|
||||||
|
|
||||||
@@ -142,8 +82,10 @@ Werte eintragen:
|
|||||||
|
|
||||||
| Variable | Woher |
|
| Variable | Woher |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `DATABASE_URL` | Verbindungsstring der PostgreSQL-Instanz. Die Rolle darf **kein** `BYPASSRLS` haben; hinter einem Pooler den **Transaktions-Modus** (bei Supabase Port 6543) |
|
| `POSTGRES_PASSWORD` | selbst erzeugen: `openssl rand -base64 24` — das des Verwalters |
|
||||||
| `DATABASE_SSL` | nur setzen (`false`), wenn die Datenbank ohne TLS läuft |
|
| `APP_DB_PASSWORD` | selbst erzeugen — das der Anwendungsrolle `alpenwerk_app` |
|
||||||
|
| `DATABASE_URL` | `postgresql://alpenwerk_app:<APP_DB_PASSWORD>@db:5432/alpenwerk`. Die Rolle darf **kein** `BYPASSRLS` haben — `deploy/db-init` legt sie genau so an |
|
||||||
|
| `DATABASE_SSL` | `false` im Compose-Netz; die Verbindung verlässt den Server nicht |
|
||||||
| `AUTH_SECRET` | selbst generieren: `openssl rand -base64 32` |
|
| `AUTH_SECRET` | selbst generieren: `openssl rand -base64 32` |
|
||||||
| `AUTH_MICROSOFT_ENTRA_ID_ID` | Entra-Portal → App-Registrierung → Übersicht |
|
| `AUTH_MICROSOFT_ENTRA_ID_ID` | Entra-Portal → App-Registrierung → Übersicht |
|
||||||
| `AUTH_MICROSOFT_ENTRA_ID_SECRET` | Entra-Portal → Zertifikate & Geheimnisse (nur einmal sichtbar!) |
|
| `AUTH_MICROSOFT_ENTRA_ID_SECRET` | Entra-Portal → Zertifikate & Geheimnisse (nur einmal sichtbar!) |
|
||||||
|
|||||||
14
README.md
14
README.md
@@ -110,12 +110,8 @@ selbst spricht. Ein Docker-Abbild ist damit umgebungsneutral: einmal gebaut,
|
|||||||
`/api/cron/apply-pending-changes` wendet wirksam gewordene, zukunftsdatierte
|
`/api/cron/apply-pending-changes` wendet wirksam gewordene, zukunftsdatierte
|
||||||
Änderungen an (`pending_org_changes` → `apply_due_pending_changes()`).
|
Änderungen an (`pending_org_changes` → `apply_due_pending_changes()`).
|
||||||
|
|
||||||
- **Auf Vercel:** `vercel.json` definiert den täglichen Schedule; Vercel Cron
|
- Den Zeitplan hält der `cron`-Container aus `docker-compose.yml`: täglich
|
||||||
sendet `Authorization: Bearer <CRON_SECRET>` automatisch, wenn
|
03:00 Uhr, mit `Authorization: Bearer <CRON_SECRET>`.
|
||||||
`CRON_SECRET` in den Projekt-Env-Vars gesetzt ist.
|
|
||||||
- **Außerhalb von Vercel (Docker):** kein Vercel Cron verfügbar — siehe
|
|
||||||
[`DEPLOYMENT.md`](DEPLOYMENT.md) für den Cron-Sidecar-Container, der
|
|
||||||
denselben Endpoint mit demselben Schema aufruft.
|
|
||||||
- Fehlt `CRON_SECRET` oder stimmt der Header nicht, antwortet die Route mit
|
- Fehlt `CRON_SECRET` oder stimmt der Header nicht, antwortet die Route mit
|
||||||
`401` (nicht `500` — bewusst, siehe `tests/unit/security.test.ts`).
|
`401` (nicht `500` — bewusst, siehe `tests/unit/security.test.ts`).
|
||||||
|
|
||||||
@@ -140,10 +136,8 @@ selbst spricht. Ein Docker-Abbild ist damit umgebungsneutral: einmal gebaut,
|
|||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
|
||||||
Siehe [`DEPLOYMENT.md`](DEPLOYMENT.md) für Docker-basiertes Deployment
|
Siehe [`DEPLOYMENT.md`](DEPLOYMENT.md): Anwendung und Datenbank als Container,
|
||||||
(Dockerfile, docker-compose.yml, Reverse-Proxy/TLS, Cron-Ersatz, Updates).
|
Reverse-Proxy/TLS, der nächtliche Lauf, Migrationen und Updates.
|
||||||
Für Vercel: `vercel.json` ist bereits vorhanden; Env-Vars im
|
|
||||||
Vercel-Projekt setzen (siehe oben).
|
|
||||||
|
|
||||||
## Known TODOs vor Produktivbetrieb
|
## Known TODOs vor Produktivbetrieb
|
||||||
|
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ function pageHref(params: SearchParams, page: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pinned to Vienna and built once: audit_log.occurred_at is a timestamptz, and
|
// Pinned to Vienna and built once: audit_log.occurred_at is a timestamptz, and
|
||||||
// an unpinned formatter renders it in the *server's* zone — UTC in Docker and
|
// an unpinned formatter renders it in the *server's* zone — UTC in the
|
||||||
// on Vercel — so every entry would read an hour or two early for the people
|
// container — so every entry would read an hour or two early for the people
|
||||||
// the log is for.
|
// the log is for.
|
||||||
const dateTimeFormatter = new Intl.DateTimeFormat("de-AT", {
|
const dateTimeFormatter = new Intl.DateTimeFormat("de-AT", {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { callFunction } from "@/lib/db/rpc";
|
|||||||
|
|
||||||
// Applies effective-dated changes (Versetzung/Beförderung/Karenz/Reorg/Daten
|
// Applies effective-dated changes (Versetzung/Beförderung/Karenz/Reorg/Daten
|
||||||
// ändern with a future "Wirksam ab" date) once their date has arrived — see
|
// ändern with a future "Wirksam ab" date) once their date has arrived — see
|
||||||
// apply_due_pending_changes() in supabase/migrations. Runs as a Vercel Cron
|
// apply_due_pending_changes() in supabase/migrations.
|
||||||
// job (see vercel.json), not on behalf of any HR user, so it authenticates
|
//
|
||||||
// via a shared secret rather than a Supabase session and uses the
|
// Gerufen wird das vom `cron`-Dienst aus docker-compose.yml, täglich um 03:00.
|
||||||
// service-role client (the one legitimate server-only use case for it).
|
// Nicht im Namen einer HR-Person: es gibt keine angemeldete Sitzung, deshalb
|
||||||
|
// weist sich der Aufruf mit einem gemeinsamen Geheimnis aus (CRON_SECRET).
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const authHeader = request.headers.get("authorization");
|
const authHeader = request.headers.get("authorization");
|
||||||
if (!process.env.CRON_SECRET || authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
|
if (!process.env.CRON_SECRET || authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ class Rueckabwicklung extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 60 Sekunden, weil das die Obergrenze im kostenlosen Vercel-Tarif ist —
|
// Im eigenen Container wirkt diese Angabe nicht — sie richtet sich an
|
||||||
// ein höherer Wert lässt sich dort nicht ausrollen. Auf einem eigenen Server
|
// serverlose Plattformen, die einen Aufruf nach Ablauf abschneiden. Sie bleibt
|
||||||
// gilt die Angabe ohnehin nicht, und im Pro-Tarif liesse sie sich auf 300
|
// als Absichtserklärung stehen: ein Import, der länger als eine Minute
|
||||||
// heben, falls eine Datei mit vielen tausend Zeilen ansteht.
|
// braucht, ist einer, der in Teilen laufen sollte.
|
||||||
export const maxDuration = 60;
|
export const maxDuration = 60;
|
||||||
|
|
||||||
type Antwort = {
|
type Antwort = {
|
||||||
|
|||||||
@@ -100,9 +100,10 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
|
||||||
# Replaces the Vercel Cron job from vercel.json (not available outside
|
# Der nächtliche Lauf: ruft täglich um 03:00 Uhr
|
||||||
# Vercel): calls the same endpoint on the same daily schedule using the
|
# /api/cron/apply-pending-changes auf, damit fällig gewordene Versetzungen,
|
||||||
# same bearer-secret auth the route already expects.
|
# Beförderungen, Karenzen und Reorganisationen wirksam werden. Ausgewiesen
|
||||||
|
# über CRON_SECRET — es gibt dabei keine angemeldete Person.
|
||||||
cron:
|
cron:
|
||||||
image: alpine:3.20
|
image: alpine:3.20
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -99,10 +99,9 @@ Ein einziges Rollenmodell, kein Mehrfach-Rollen-System:
|
|||||||
Funktion, deren Ausführungsrecht explizit auf `service_role` beschränkt ist
|
Funktion, deren Ausführungsrecht explizit auf `service_role` beschränkt ist
|
||||||
(`revoke ... from public, anon, authenticated; grant ... to service_role`).
|
(`revoke ... from public, anon, authenticated; grant ... to service_role`).
|
||||||
Sie wird von `app/api/cron/apply-pending-changes/route.ts` aufgerufen —
|
Sie wird von `app/api/cron/apply-pending-changes/route.ts` aufgerufen —
|
||||||
täglich per Vercel Cron (`vercel.json`), außerhalb von Vercel per
|
täglich vom `cron`-Container aus `docker-compose.yml`. Die Route selbst
|
||||||
Ersatz-Scheduler (siehe `DEPLOYMENT.md`, Docker-Cron-Sidecar). Die Route
|
authentifiziert per `CRON_SECRET`-Bearer-Token, nicht über eine Sitzung — es
|
||||||
selbst authentifiziert per `CRON_SECRET`-Bearer-Token, nicht per
|
gibt keine anfragende Person, nur den Zeitplan.
|
||||||
Supabase-Session — es gibt keine anfragende Person, nur den Scheduler.
|
|
||||||
|
|
||||||
Idempotenz: `pending_org_changes.status` läuft `pending` → `applied` (oder
|
Idempotenz: `pending_org_changes.status` läuft `pending` → `applied` (oder
|
||||||
`cancelled` bei Reorg-Undo); die Auswahl-Query filtert immer auf
|
`cancelled` bei Reorg-Undo); die Auswahl-Query filtert immer auf
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// This app stores dates as date-only strings ("YYYY-MM-DD") and timestamps as
|
// This app stores dates as date-only strings ("YYYY-MM-DD") and timestamps as
|
||||||
// timestamptz, and is used from a single timezone. Every conversion here is
|
// timestamptz, and is used from a single timezone. Every conversion here is
|
||||||
// pinned to Europe/Vienna rather than the runtime's zone: the server renders
|
// pinned to Europe/Vienna rather than the runtime's zone: the server renders
|
||||||
// in UTC (Docker/Vercel) while the browser renders in Vienna, so an unpinned
|
// in UTC (im Container) while the browser renders in Vienna, so an unpinned
|
||||||
// formatter produces a different day on each side — wrong dates for the user
|
// formatter produces a different day on each side — wrong dates for the user
|
||||||
// near midnight, and a React hydration mismatch.
|
// near midnight, and a React hydration mismatch.
|
||||||
const TIMEZONE = "Europe/Vienna";
|
const TIMEZONE = "Europe/Vienna";
|
||||||
|
|||||||
@@ -46,10 +46,9 @@ const nextConfig: NextConfig = {
|
|||||||
// Emits a self-contained .next/standalone server (only the deps actually
|
// Emits a self-contained .next/standalone server (only the deps actually
|
||||||
// used at runtime, no full node_modules) — what the Dockerfile copies in.
|
// used at runtime, no full node_modules) — what the Dockerfile copies in.
|
||||||
//
|
//
|
||||||
// Auf Vercel ist das falsch: dort baut die Plattform selbst und erwartet
|
// Ohne Ausnahme: die Anwendung läuft in einem Container auf einem eigenen
|
||||||
// die übliche Ausgabe. `VERCEL` setzt sie in jeder Baustrecke, die Angabe
|
// Server, und dort ist das der richtige Ausgabemodus.
|
||||||
// entfällt dort also von selbst — und der Docker-Weg bleibt unberührt.
|
output: "standalone",
|
||||||
output: process.env.VERCEL ? undefined : "standalone",
|
|
||||||
// Baseline security headers (clickjacking, MIME-sniffing, referrer leakage,
|
// Baseline security headers (clickjacking, MIME-sniffing, referrer leakage,
|
||||||
// browser feature access) plus the report-only CSP described above.
|
// browser feature access) plus the report-only CSP described above.
|
||||||
async headers() {
|
async headers() {
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"crons": [
|
|
||||||
{
|
|
||||||
"path": "/api/cron/apply-pending-changes",
|
|
||||||
"schedule": "0 3 * * *"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user