feat: add Docker support with demo mode and database reset functionality
This commit is contained in:
1 parent
75af38a4b6
commit
373b9278d1
12 files changed
+248
-5
No files matched your search
@@ -0,0 +1,14 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 Paul Schoeneck
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only AND LicenseRef-KIBU-Additional-Terms
|
||||||
|
|
||||||
|
.git
|
||||||
|
.next
|
||||||
|
node_modules
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
!.env.example.license
|
||||||
|
*.tsbuildinfo
|
||||||
|
test-results
|
||||||
|
playwright-report
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 Paul Schoeneck
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only AND LicenseRef-KIBU-Additional-Terms
|
||||||
|
|
||||||
|
*.sh text eol=lf
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 Paul Schoeneck
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only AND LicenseRef-KIBU-Additional-Terms
|
||||||
|
|
||||||
|
FROM node:22-bookworm-slim AS build
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm ci
|
||||||
|
COPY . .
|
||||||
|
RUN sed -i 's/\r$//' scripts/docker-entrypoint.sh && npm run build
|
||||||
|
|
||||||
|
FROM node:22-bookworm-slim AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=3000
|
||||||
|
LABEL org.opencontainers.image.title="KIBU" \
|
||||||
|
org.opencontainers.image.source="https://git.voidcube.de/pixo2000/KIBU" \
|
||||||
|
org.opencontainers.image.licenses="AGPL-3.0-only AND LicenseRef-KIBU-Additional-Terms"
|
||||||
|
COPY --from=build --chown=node:node /app /app
|
||||||
|
USER node
|
||||||
|
EXPOSE 3000
|
||||||
|
HEALTHCHECK --interval=10s --timeout=5s --start-period=60s --retries=6 \
|
||||||
|
CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/login').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"
|
||||||
|
ENTRYPOINT ["sh", "scripts/docker-entrypoint.sh"]
|
||||||
@@ -8,6 +8,83 @@ SPDX-License-Identifier: AGPL-3.0-only AND LicenseRef-KIBU-Additional-Terms
|
|||||||
|
|
||||||
Lokale Web-App für die Kinderbetreuung mit Next.js, TypeScript und einer echten PostgreSQL-Datenbank. Keine externen Dienste, Fonts oder Bild-Uploads. Kinderkarten verwenden Initialen.
|
Lokale Web-App für die Kinderbetreuung mit Next.js, TypeScript und einer echten PostgreSQL-Datenbank. Keine externen Dienste, Fonts oder Bild-Uploads. Kinderkarten verwenden Initialen.
|
||||||
|
|
||||||
|
## Docker und oeffentliche Demo
|
||||||
|
|
||||||
|
Das gleiche Image unterstuetzt normale Instanzen und einen ausdruecklichen
|
||||||
|
Demomodus. Die Demo ist eine voll bearbeitbare, gemeinsam genutzte Instanz mit
|
||||||
|
12 fiktiven Kindern, Wochenplaenen, Abholberechtigungen und Anwesenheitsverlauf.
|
||||||
|
Nur im Demomodus sind E-Mail und Passwort im normalen Login vorausgefuellt:
|
||||||
|
`demo@kibu.local` / `KIBU-Demo-2026!`. Es gibt keinen Authentifizierungs-Bypass.
|
||||||
|
Aufnahmerunden koennen wie in einer normalen Instanz angelegt werden.
|
||||||
|
|
||||||
|
**Keine echten Daten eingeben! Jeder App-Start loescht alle Demo-Daten und
|
||||||
|
Zugaenge, ersetzt sie durch Beispieldaten und macht alte Sitzungen ungueltig.**
|
||||||
|
Auch ein automatischer App-Neustart nach einem Fehler setzt die Demo zurueck.
|
||||||
|
Die Demo-Datenbank liegt zusaetzlich auf einem fluechtigen `tmpfs`.
|
||||||
|
Nicht mehrfach skalieren: es ist eine einzelne App mit gemeinsamer Datenbank.
|
||||||
|
Der Login, Passwortaenderungen und andere Funktionen verhalten sich normal;
|
||||||
|
aendert jemand das gemeinsame Passwort, stellt ein App-Neustart es wieder her.
|
||||||
|
|
||||||
|
### Lokal bauen und starten
|
||||||
|
|
||||||
|
Docker Desktop mit Linux-Containern muss laufen. Compose verwendet ein separates
|
||||||
|
Projekt `kibu-demo`, eine separate Datenbank ohne veroeffentlichten DB-Port und
|
||||||
|
standardmaessig <http://127.0.0.1:3001>. Die bisherige lokale Instanz auf Port 3000
|
||||||
|
und ihr Datenvolume bleiben unberuehrt.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose -f compose.demo.yaml up -d --build --wait
|
||||||
|
docker compose -f compose.demo.yaml logs app
|
||||||
|
# App neu starten und damit Demo-Daten neu erzeugen:
|
||||||
|
docker compose -f compose.demo.yaml restart app
|
||||||
|
# Nur diese Demo stoppen:
|
||||||
|
docker compose -f compose.demo.yaml down
|
||||||
|
```
|
||||||
|
|
||||||
|
### Image pushen und auf einem anderen Host starten
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker build -t git.voidcube.de/pixo2000/kibu:demo .
|
||||||
|
docker login git.voidcube.de
|
||||||
|
docker push git.voidcube.de/pixo2000/kibu:demo
|
||||||
|
```
|
||||||
|
|
||||||
|
Auf dem Zielhost nur `compose.demo.yaml` bereitstellen. Bei privater Registry
|
||||||
|
vorher dort ebenfalls anmelden, dann:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
docker compose -f compose.demo.yaml pull
|
||||||
|
docker compose -f compose.demo.yaml up -d --no-build --wait
|
||||||
|
```
|
||||||
|
|
||||||
|
`KIBU_IMAGE` kann ein anderes Image/Tag oder einen unveraenderlichen Digest setzen.
|
||||||
|
`DEMO_PORT` aendert den Host-Port. `DEMO_BIND_ADDRESS` ist absichtlich
|
||||||
|
`127.0.0.1`; fuer direkten externen Zugriff muss die Bindung bewusst geaendert
|
||||||
|
werden. Vorzugsweise HTTPS ueber einen Reverse Proxy nutzen und dann
|
||||||
|
`DEMO_SESSION_SECURE=true` setzen. Gemeinsame Demo-Zugaenge sind keine
|
||||||
|
Sicherheitsgrenze fuer vertrauliche Daten. Vor oeffentlichem Betrieb sind
|
||||||
|
Ressourcen-/Request-Limits am Proxy und die Betriebsgrenzen unten zu beachten.
|
||||||
|
|
||||||
|
### Normale Instanz mit dem Image
|
||||||
|
|
||||||
|
Ohne `DEMO_MODE=true` bleiben die Loginfelder leer; der Container fuehrt nur
|
||||||
|
`db:setup` aus und behaelt bestehende Daten. `DATABASE_URL` auf die eigene
|
||||||
|
PostgreSQL-Datenbank setzen; optional `BOOTSTRAP_EMAIL`, `BOOTSTRAP_PASSWORD`,
|
||||||
|
`APP_TIMEZONE` und `SESSION_SECURE` konfigurieren. Die App lauscht im Container
|
||||||
|
auf `0.0.0.0:3000`. DB-Readiness vor App-Start sicherstellen.
|
||||||
|
|
||||||
|
Der destruktive Container-Demostart erfordert gemeinsam `DEMO_MODE=true`,
|
||||||
|
`DEMO_RESET_ON_START=true` und genau `demo-db/kibu_demo` als Datenbankziel.
|
||||||
|
Diese Einstellungen niemals auf echte Daten umbiegen. Die normalen lokalen
|
||||||
|
Reset-Befehle bleiben auf lokale Ziele begrenzt; nur das ausdruecklich
|
||||||
|
aktivierte, benannte Container-Demoziel ist zusaetzlich erlaubt.
|
||||||
|
|
||||||
|
Das Image laeuft als unprivilegierter Node-Benutzer und enthaelt Lizenztexte,
|
||||||
|
Quellcode sowie die fuer Einrichtung und Build erforderlichen Werkzeuge.
|
||||||
|
`.env`, lokale Daten, Git-Metadaten und lokale Build-Artefakte werden nicht
|
||||||
|
in den Build-Kontext uebernommen. Vor dem Push muessen Lizenz-/Quellcodehinweise
|
||||||
|
und die veroeffentlichte Quellversion zum Image passen (siehe unten).
|
||||||
|
|
||||||
## Lizenz
|
## Lizenz
|
||||||
|
|
||||||
Copyright (C) 2026 Paul Schoeneck. Originaler KIBU-Code steht unter
|
Copyright (C) 2026 Paul Schoeneck. Originaler KIBU-Code steht unter
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2026 Paul Schoeneck
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only AND LicenseRef-KIBU-Additional-Terms
|
||||||
|
|
||||||
|
name: kibu-demo
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: ${KIBU_IMAGE:-git.voidcube.de/pixo2000/kibu:demo}
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: postgresql://kibu_demo:kibu-demo-db-only@demo-db:5432/kibu_demo
|
||||||
|
DEMO_MODE: "true"
|
||||||
|
DEMO_RESET_ON_START: "true"
|
||||||
|
APP_TIMEZONE: Europe/Berlin
|
||||||
|
SESSION_SECURE: ${DEMO_SESSION_SECURE:-false}
|
||||||
|
ports:
|
||||||
|
- "${DEMO_BIND_ADDRESS:-127.0.0.1}:${DEMO_PORT:-3001}:3000"
|
||||||
|
depends_on:
|
||||||
|
demo-db:
|
||||||
|
condition: service_healthy
|
||||||
|
restart: unless-stopped
|
||||||
|
demo-db:
|
||||||
|
image: postgres:17-alpine
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: kibu_demo
|
||||||
|
POSTGRES_USER: kibu_demo
|
||||||
|
POSTGRES_PASSWORD: kibu-demo-db-only
|
||||||
|
tmpfs:
|
||||||
|
- /var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U kibu_demo -d kibu_demo"]
|
||||||
|
interval: 3s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 20
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# SPDX-FileCopyrightText: 2026 Paul Schoeneck
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-only AND LicenseRef-KIBU-Additional-Terms
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
node node_modules/tsx/dist/cli.mjs scripts/prepare-container.ts
|
||||||
|
exec node node_modules/next/dist/bin/next start --hostname 0.0.0.0 --port "${PORT:-3000}"
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Paul Schoeneck
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only AND LicenseRef-KIBU-Additional-Terms
|
||||||
|
|
||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import { demoLogin, isDemoDatabase } from "../src/lib/demo";
|
||||||
|
|
||||||
|
function prepare() {
|
||||||
|
if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL fehlt.");
|
||||||
|
if (process.env.DEMO_MODE && !["true", "false"].includes(process.env.DEMO_MODE)) {
|
||||||
|
throw new Error("DEMO_MODE muss true oder false sein.");
|
||||||
|
}
|
||||||
|
const demo = demoLogin();
|
||||||
|
if (demo) {
|
||||||
|
const target = new URL(process.env.DATABASE_URL);
|
||||||
|
if (!isDemoDatabase(target) || !["postgres:", "postgresql:"].includes(target.protocol)) {
|
||||||
|
throw new Error("Demo-Start erfordert DEMO_RESET_ON_START=true und die separate Datenbank demo-db/kibu_demo. Nichts wurde veraendert.");
|
||||||
|
}
|
||||||
|
process.env.BOOTSTRAP_EMAIL = demo.email;
|
||||||
|
process.env.BOOTSTRAP_PASSWORD = demo.password;
|
||||||
|
console.log("DEMOMODUS: Gemeinsamer oeffentlicher Zugang; alle Demodaten werden bei jedem App-Start ersetzt.");
|
||||||
|
} else if (process.env.DEMO_RESET_ON_START === "true") {
|
||||||
|
throw new Error("DEMO_RESET_ON_START erfordert DEMO_MODE=true.");
|
||||||
|
}
|
||||||
|
const tsx = createRequire(import.meta.url).resolve("tsx/cli");
|
||||||
|
const commands = demo
|
||||||
|
? [["setup.ts"], ["reset-instance.ts", "demo", "--confirm-reset"]]
|
||||||
|
: [["setup.ts"]];
|
||||||
|
for (const [script, ...args] of commands) {
|
||||||
|
const result = spawnSync(process.execPath, [tsx, fileURLToPath(new URL(script, import.meta.url)), ...args], {
|
||||||
|
env: process.env, stdio: "inherit",
|
||||||
|
});
|
||||||
|
if (result.error) throw result.error;
|
||||||
|
if (result.status !== 0) throw new Error(`${script} fehlgeschlagen; App wird nicht gestartet.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
prepare();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Container-Vorbereitung fehlgeschlagen:", error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { spawnSync } from "node:child_process";
|
|||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
import { db } from "../src/lib/db";
|
import { db } from "../src/lib/db";
|
||||||
|
import { isDemoDatabase } from "../src/lib/demo";
|
||||||
|
|
||||||
async function reset() {
|
async function reset() {
|
||||||
const [mode, confirmation, ...extra] = process.argv.slice(2);
|
const [mode, confirmation, ...extra] = process.argv.slice(2);
|
||||||
@@ -16,7 +17,7 @@ async function reset() {
|
|||||||
if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL fehlt. Zuerst npm run configure ausfuehren.");
|
if (!process.env.DATABASE_URL) throw new Error("DATABASE_URL fehlt. Zuerst npm run configure ausfuehren.");
|
||||||
const target = new URL(process.env.DATABASE_URL);
|
const target = new URL(process.env.DATABASE_URL);
|
||||||
if (!["postgres:", "postgresql:"].includes(target.protocol)
|
if (!["postgres:", "postgresql:"].includes(target.protocol)
|
||||||
|| !["localhost", "127.0.0.1", "[::1]"].includes(target.hostname)
|
|| (!["localhost", "127.0.0.1", "[::1]"].includes(target.hostname) && !isDemoDatabase(target))
|
||||||
|| !target.pathname.slice(1) || ["/postgres", "/template0", "/template1"].includes(target.pathname)) {
|
|| !target.pathname.slice(1) || ["/postgres", "/template0", "/template1"].includes(target.pathname)) {
|
||||||
throw new Error("Reset nur fuer eine ausdruecklich konfigurierte lokale Anwendungsdatenbank erlaubt.");
|
throw new Error("Reset nur fuer eine ausdruecklich konfigurierte lokale Anwendungsdatenbank erlaubt.");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { currentStaff } from "@/lib/auth";
|
import { currentStaff } from "@/lib/auth";
|
||||||
import { LoginForm } from "@/components/login-form";
|
import { LoginForm } from "@/components/login-form";
|
||||||
|
import { demoLogin } from "@/lib/demo";
|
||||||
import { Sprout, ShieldCheck, Sun } from "lucide-react";
|
import { Sprout, ShieldCheck, Sun } from "lucide-react";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -25,7 +26,7 @@ export default async function LoginPage() {
|
|||||||
<div className="login-form-wrap"><span className="section-icon"><ShieldCheck size={25} /></span>
|
<div className="login-form-wrap"><span className="section-icon"><ShieldCheck size={25} /></span>
|
||||||
<span className="eyebrow">SCHÖN, DASS DU DA BIST</span>
|
<span className="eyebrow">SCHÖN, DASS DU DA BIST</span>
|
||||||
<h2>Willkommen zurück.</h2><p className="muted">Melde dich für euren Betreuungsbereich an.</p>
|
<h2>Willkommen zurück.</h2><p className="muted">Melde dich für euren Betreuungsbereich an.</p>
|
||||||
<LoginForm />
|
<LoginForm demo={demoLogin()} />
|
||||||
<p className="login-note"><ShieldCheck size={14} /> Geschützter Zugang für Betreuungspersonen</p>
|
<p className="login-note"><ShieldCheck size={14} /> Geschützter Zugang für Betreuungspersonen</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ import { useActionState, useState } from "react";
|
|||||||
import { ArrowRight } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
import { login } from "@/app/actions";
|
import { login } from "@/app/actions";
|
||||||
|
|
||||||
export function LoginForm() {
|
export function LoginForm({ demo }: { demo?: { email: string; password: string } }) {
|
||||||
const [state, action, pending] = useActionState(login, {});
|
const [state, action, pending] = useActionState(login, {});
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState(demo?.email ?? "");
|
||||||
return <form action={action} className="stack">
|
return <form action={action} className="stack">
|
||||||
|
{demo && <p className="form-notice">Öffentliche Demo mit gemeinsamem Zugang. Bitte keine echten personenbezogenen Daten eingeben.
|
||||||
|
Änderungen sind für andere sichtbar und werden beim App-Neustart gelöscht.</p>}
|
||||||
<label>E-Mail-Adresse<input type="email" name="email" autoComplete="username" required maxLength={254} placeholder="name@betreuung.de" value={email} onChange={(event) => setEmail(event.target.value)} /></label>
|
<label>E-Mail-Adresse<input type="email" name="email" autoComplete="username" required maxLength={254} placeholder="name@betreuung.de" value={email} onChange={(event) => setEmail(event.target.value)} /></label>
|
||||||
<label>Passwort<input type="password" name="password" autoComplete="current-password" required maxLength={256} placeholder="Dein Passwort" /></label>
|
<label>Passwort<input type="password" name="password" autoComplete="current-password" required maxLength={256} placeholder="Dein Passwort" defaultValue={demo?.password ?? ""} /></label>
|
||||||
{state.error && <p className="form-error" role="alert">{state.error}</p>}
|
{state.error && <p className="form-error" role="alert">{state.error}</p>}
|
||||||
<button className="button primary full" disabled={pending}>{pending ? "Wird angemeldet …" : <>Anmelden <ArrowRight size={17} /></>}</button>
|
<button className="button primary full" disabled={pending}>{pending ? "Wird angemeldet …" : <>Anmelden <ArrowRight size={17} /></>}</button>
|
||||||
</form>;
|
</form>;
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2026 Paul Schoeneck
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only AND LicenseRef-KIBU-Additional-Terms
|
||||||
|
|
||||||
|
export function demoLogin(environment: NodeJS.ProcessEnv = process.env) {
|
||||||
|
if (environment.DEMO_MODE !== "true") return undefined;
|
||||||
|
return { email: "demo@kibu.local", password: "KIBU-Demo-2026!" };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isDemoDatabase(target: URL, environment: NodeJS.ProcessEnv = process.env) {
|
||||||
|
return environment.DEMO_MODE === "true"
|
||||||
|
&& environment.DEMO_RESET_ON_START === "true"
|
||||||
|
&& target.hostname === "demo-db" && target.pathname === "/kibu_demo";
|
||||||
|
}
|
||||||
@@ -8,6 +8,22 @@ import { localClock, localInstant, pickupState } from "../src/lib/time";
|
|||||||
import { childSchema, correctionSchema, daySchema, overrideSchema, aloneRulesSchema, contactSchema, slotSchema, applicationSchema } from "../src/lib/validation";
|
import { childSchema, correctionSchema, daySchema, overrideSchema, aloneRulesSchema, contactSchema, slotSchema, applicationSchema } from "../src/lib/validation";
|
||||||
import { hashPassword, verifyPassword } from "../src/lib/password";
|
import { hashPassword, verifyPassword } from "../src/lib/password";
|
||||||
import { chooseWinners, invitationHash, newInvitation } from "../src/lib/admissions";
|
import { chooseWinners, invitationHash, newInvitation } from "../src/lib/admissions";
|
||||||
|
import { demoLogin, isDemoDatabase } from "../src/lib/demo";
|
||||||
|
|
||||||
|
test("Demozugang nur bei explizitem Modus; Container-Reset nur fuer isolierte Demo-Datenbank", () => {
|
||||||
|
const normal = { NODE_ENV: "test" } as const;
|
||||||
|
assert.equal(demoLogin(normal), undefined);
|
||||||
|
assert.equal(demoLogin({ ...normal, DEMO_MODE: "false" }), undefined);
|
||||||
|
assert.equal(demoLogin({ ...normal, DEMO_MODE: "TRUE" }), undefined);
|
||||||
|
const demo = { ...normal, DEMO_MODE: "true", DEMO_RESET_ON_START: "true" };
|
||||||
|
assert.deepEqual(demoLogin(demo), { email: "demo@kibu.local", password: "KIBU-Demo-2026!" });
|
||||||
|
const target = new URL("postgresql://demo-db/kibu_demo");
|
||||||
|
assert.equal(isDemoDatabase(target, demo), true);
|
||||||
|
assert.equal(isDemoDatabase(target, normal), false);
|
||||||
|
assert.equal(isDemoDatabase(target, { ...demo, DEMO_RESET_ON_START: "false" }), false);
|
||||||
|
assert.equal(isDemoDatabase(new URL("postgresql://demo-db/kibu"), demo), false);
|
||||||
|
assert.equal(isDemoDatabase(new URL("postgresql://production/kibu_demo"), demo), false);
|
||||||
|
});
|
||||||
|
|
||||||
test("Abholfenster: genau jetzt und bis einschließlich 15 Minuten, überfällig erst davor", () => {
|
test("Abholfenster: genau jetzt und bis einschließlich 15 Minuten, überfällig erst davor", () => {
|
||||||
assert.equal(pickupState(null, "14:00"), "none");
|
assert.equal(pickupState(null, "14:00"), "none");
|
||||||
|
|||||||
Reference in new issue
Block a user