Herstel installatie-instructies en hard deploy-script (v0.2.03 beta)
Some checks are pending
CI / backend (push) Waiting to run
CI / frontend (push) Waiting to run

De installatiestappen in de README waren niet uitvoerbaar en het
deploy-script kon stilzwijgend werk vernietigen.

README:
- `python manage.py migrate` liep vast op `RuntimeError: DJANGO_SECRET_KEY
  ontbreekt`. DEBUG staat standaard uit en dan is die sleutel verplicht;
  de instructies noemden `DJANGO_DEBUG` nergens. Nu toegevoegd, met uitleg.
- `createsuperuser` stond als "optioneel, voor /admin/". Dat klopt niet: de
  hele API staat op `IsAuthenticated`, dus zonder account kom je de app
  helemaal niet in. Nu gemarkeerd als verplichte stap.
- Stap voor een virtual environment toegevoegd (pip installeerde anders in
  de systeem-Python, wat op recente distributies afketst op PEP 668).
- Node-eis vermeld: Vite 8 vereist 20.19+ of 22.12+.
- Verouderde beschrijvingen bijgewerkt: de tabs van het roosterscherm
  (Week/Genereren/Instellingen in plaats van Weekrooster/Conflicten/
  Schooljaar/Instellingen), Groepsindeling is een eigen scherm en geen tab,
  en er zijn twaalf modules in plaats van "twee voorbeeldmodules".

DEPLOY.md:
- `git clone .../Roostersoftware.git` + `cd Roostersoftware` gebruikte de
  verkeerde repo-naam; dat is `roosterwijs`.
- CSRF-origin en FRONTEND_BASE_URL stonden als http-voorbeeld; achter TLS
  moet dat https zijn.
- Rechtgezet: `DJANGO_CSRF_TRUSTED_ORIGINS` blokkeert het inloggen in de app
  niet (DRF-views zijn `csrf_exempt`), maar wel de Django-admin.
- Vermeld dat er nergens automatisch een account wordt aangemaakt.

scripts/deploy.sh:
- Stopt nu als `.env` ontbreekt; anders start de stack met lege variabelen
  en faalt de backend met een onduidelijke fout.
- Stopt bij lokale wijzigingen in plaats van ze met `git reset --hard`
  weg te gooien; bewust overschrijven kan met `--force`.
- Controleert vooraf of Docker en de compose-plugin er zijn.
- Toont welke commits erbij komen (oud -> nieuw) in plaats van alleen de
  nieuwe HEAD.
- Controleert na afloop of de backend antwoordt en of er een actief account
  bestaat, met het createsuperuser-commando als dat er niet is.

frontend/Dockerfile:
- Bouwt op node:22-alpine in plaats van node:20-alpine, gelijk aan de CI en
  ruim binnen de Node-eis van Vite 8.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FAQ4Np13v8fwbTtKHKnwDz
This commit is contained in:
Ramon 2026-08-19 14:57:49 +02:00
parent 1865de948e
commit 500cf6c558
7 changed files with 164 additions and 40 deletions

View file

@ -22,9 +22,18 @@ erboven (meestal een SSH-/sleutelprobleem).
./scripts/deploy.sh ./scripts/deploy.sh
``` ```
Dit haalt de laatste code op (`git reset --hard origin/<branch>`), herbouwt de Dit controleert eerst of Docker en `.env` aanwezig zijn, haalt dan de laatste
containers (incl. migraties via de entrypoint) en controleert of er een nieuwe code op (`git reset --hard origin/<branch>`), herbouwt de containers (incl.
frontend-bundel staat. migraties via de entrypoint) en controleert daarna of de frontend-bundel er
staat, of de backend antwoordt en of er een actief account is om mee in te
loggen.
Staan er **lokale wijzigingen** in de map op de server, dan stopt het script:
`git reset --hard` zou die weggooien. Zet ze eerst veilig, of forceer bewust:
```bash
./scripts/deploy.sh --force
```
Nieuwe **modules** verschijnen daarna pas in de app nadat je ze aanzet via Nieuwe **modules** verschijnen daarna pas in de app nadat je ze aanzet via
**⚙ → Modulebeheer**. **⚙ → Modulebeheer**.
@ -45,7 +54,7 @@ EMAIL_HOST_USER=postvak@jouwschool.nl
EMAIL_HOST_PASSWORD=... EMAIL_HOST_PASSWORD=...
EMAIL_USE_TLS=1 EMAIL_USE_TLS=1
DEFAULT_FROM_EMAIL=Roosterwijs <noreply@jouwschool.nl> DEFAULT_FROM_EMAIL=Roosterwijs <noreply@jouwschool.nl>
FRONTEND_BASE_URL=http://rooster-test.example.nl:8080 FRONTEND_BASE_URL=https://rooster.example.nl
``` ```
`FRONTEND_BASE_URL` bepaalt de basis van de resetlink in de e-mail (zonder `FRONTEND_BASE_URL` bepaalt de basis van de resetlink in de e-mail (zonder
@ -76,8 +85,8 @@ Je hoeft op de server **niets** te installeren behalve Docker zelf (met de
``` ```
2. Haal de code op: 2. Haal de code op:
```bash ```bash
git clone <jouw-forgejo-url>/Roostersoftware.git git clone <jouw-forgejo-url>/roosterwijs.git
cd Roostersoftware cd roosterwijs
``` ```
3. Maak het `.env`-bestand aan vanuit het voorbeeld en vul echte waarden in: 3. Maak het `.env`-bestand aan vanuit het voorbeeld en vul echte waarden in:
```bash ```bash
@ -88,7 +97,8 @@ Je hoeft op de server **niets** te installeren behalve Docker zelf (met de
- `DJANGO_SECRET_KEY` — genereer met - `DJANGO_SECRET_KEY` — genereer met
`python3 -c "import secrets; print(secrets.token_urlsafe(50))"` `python3 -c "import secrets; print(secrets.token_urlsafe(50))"`
- `DJANGO_ALLOWED_HOSTS` — de hostnaam/IP van de test-server - `DJANGO_ALLOWED_HOSTS` — de hostnaam/IP van de test-server
- `DJANGO_CSRF_TRUSTED_ORIGINS``http://<host>:8080` (of je echte URL) - `DJANGO_CSRF_TRUSTED_ORIGINS` — exact de origin die de browser gebruikt,
dus `https://<host>` achter TLS (of `http://<host>:8080` zonder TLS)
- `POSTGRES_PASSWORD` — een sterk wachtwoord - `POSTGRES_PASSWORD` — een sterk wachtwoord
`.env` staat in `.gitignore` en komt dus **niet** in git — dat hoort zo. `.env` staat in `.gitignore` en komt dus **niet** in git — dat hoort zo.
@ -116,7 +126,10 @@ pas hem daar aan als die poort bezet is.)
docker compose exec backend python manage.py createsuperuser docker compose exec backend python manage.py createsuperuser
``` ```
Daarna kun je inloggen op `http://<server-host>:8080/admin/`. Dit account heb je **hoe dan ook** nodig: de hele API staat op
`IsAuthenticated`, dus zonder gebruiker kom je ook de app zelf niet in — niet
alleen `/admin/`. Zonder dit commando wordt er nergens automatisch een account
aangemaakt; `entrypoint.sh` draait alleen migraties en `collectstatic`.
## Updaten na nieuwe code ## Updaten na nieuwe code
@ -164,8 +177,11 @@ zonder tussenkomst van Django).
als je een eigen nginx/Apache ervoor hebt. als je een eigen nginx/Apache ervoor hebt.
2. **`DJANGO_CSRF_TRUSTED_ORIGINS` moet de https-origin zijn**, exact zoals de 2. **`DJANGO_CSRF_TRUSTED_ORIGINS` moet de https-origin zijn**, exact zoals de
browser hem gebruikt (bv. `https://rooster.example.nl`, zonder poort op 443). browser hem gebruikt (bv. `https://rooster.example.nl`, zonder poort op 443).
Staat hier nog een `http://`-adres, dan geeft elke POST — inclusief het Dit geldt voor de **Django-admin** en andere gewone Django-formulieren: staat
inloggen — een CSRF-fout (HTTP 403). hier een `http://`-adres, dan weigert `/admin/` je aanmelding met een
CSRF-fout (HTTP 403). Het inloggen in de app zelf loopt hier níet op stuk —
DRF-views zijn `csrf_exempt` en `SessionAuthentication` controleert CSRF pas
als er al een sessie is.
3. **`DJANGO_ALLOWED_HOSTS` moet de publieke hostnaam bevatten**, anders volgt 3. **`DJANGO_ALLOWED_HOSTS` moet de publieke hostnaam bevatten**, anders volgt
een 400 Bad Request. een 400 Bad Request.

View file

@ -11,10 +11,11 @@ Fase 3 (afwezigheid, uitzonderingen, effectief weekrooster) en Fase 4
Het Rooster-scherm is een **weekweergave met drag-and-drop** (muis én touch) en Het Rooster-scherm is een **weekweergave met drag-and-drop** (muis én touch) en
een **touch-vriendelijk** ontwerp; vrije dagen, afwezigheid en uitzonderingen een **touch-vriendelijk** ontwerp; vrije dagen, afwezigheid en uitzonderingen
worden automatisch verwerkt. Het Rooster heeft tabs **Weekrooster / Conflicten / worden automatisch verwerkt. De Roostermaker heeft tabs **Week / Genereren /
Schooljaar / Instellingen** (tijdsloten, activiteiten en locaties beheer je daar, Instellingen**: onder Instellingen beheer je schooljaar + kalender, tijdvakken,
en het schooljaar + de kalender voer je in onder de Schooljaar-tab, zodat het menu vakken, lokalen, locaties en vormgeving; onder Genereren zitten de Vak-wizard en
rustiger is). De zijbalk groepeert items onder inklapbare submenu's (Personen, het Auto-rooster. Conflicten verschijnen als melding boven het weekrooster, niet
als aparte tab. De zijbalk groepeert items onder inklapbare submenu's (Personen,
Groepen) en heeft onderaan een subtiel **Instellingen**-blok met Gebruikers en Groepen) en heeft onderaan een subtiel **Instellingen**-blok met Gebruikers en
Modulebeheer. Modulebeheer.
@ -22,9 +23,10 @@ Er zijn twee rooster-schermen: **Rooster** (alleen-lezen weergave, om te bekijke
en af te drukken) en **Roostermaker** (de aanpasser met drag-and-drop, tabs en en af te drukken) en **Roostermaker** (de aanpasser met drag-and-drop, tabs en
conflictdetectie). In de Roostermaker open je een blok met een **klik** om het te conflictdetectie). In de Roostermaker open je een blok met een **klik** om het te
bewerken (vak, locatie, doelgroep, begeleiders, opmerking) of te **kopiëren** naar bewerken (vak, locatie, doelgroep, begeleiders, opmerking) of te **kopiëren** naar
andere tijden/dagen. De tab **Indeling** verdeelt een hoofdgroep (of leerplein) andere tijden/dagen. Het aparte scherm **Groepsindeling** (zijbalk → Organisatie)
kolomsgewijs in subgroepen, met per subgroep de vakken en de verantwoordelijke(n), verdeelt een hoofdgroep of leerplein kolomsgewijs in subgroepen, met per subgroep
en je kunt er direct subgroepen toevoegen. Beide schermen hebben een **scope-filter** (alles / per klas / de vakken en de verantwoordelijke(n), en je kunt er direct subgroepen toevoegen.
Beide roosterschermen hebben een **scope-filter** (alles / per klas /
per leerplein / per leerling / per personeel); de module **Printen/Exporteren** per leerplein / per leerling / per personeel); de module **Printen/Exporteren**
voegt — als die aanstaat — een subtiele **Afdrukken**-knop toe die het (gefilterde) voegt — als die aanstaat — een subtiele **Afdrukken**-knop toe die het (gefilterde)
weekrooster printvriendelijk afdrukt of als PDF bewaart. weekrooster printvriendelijk afdrukt of als PDF bewaart.
@ -52,11 +54,14 @@ geboortedatum vast.
- **Backend** (`backend/`) — Django + Django REST Framework. - **Backend** (`backend/`) — Django + Django REST Framework.
- `plugins/` — het plugin-framework: een moduleregister, opslag van de - `plugins/` — het plugin-framework: een moduleregister, opslag van de
aan/uit-status per module en een beheer-API. aan/uit-status per module en een beheer-API.
- `core/` — de kern (altijd actief). **Fase 1**: modellen Persoon, Groep en - `core/` — de kern (altijd actief): personen, groepen, subgroepen,
Subgroep met een volledige CRUD-API. Roosterblokken en kalender volgen in functies, schooljaar + kalender, tijdvakken, activiteiten, locaties,
fase 2. lokalen, roosterblokken en conflictdetectie, elk met een CRUD-API.
- `modules/printing/` en `modules/accounts/` — twee voorbeeldmodules die - `modules/` — twaalf modules die zich bij opstarten aanmelden bij het
zich bij opstarten aanmelden bij het register. register: `printing`, `accounts`, `leerplein`, `stage`, `uren`,
`blokwizard`, `personenwizard`, `autorooster`, `huisstijl`,
`importexport`, `vervanging` en `portal`. De volledige lijst staat in
`MODULE_APPS` in `backend/config/settings.py`.
- **Frontend** (`frontend/`) — React (Vite). Zijbalk met Roosterwijs-logo die - **Frontend** (`frontend/`) — React (Vite). Zijbalk met Roosterwijs-logo die
alleen menu's toont van ingeschakelde modules, beheerschermen voor alleen menu's toont van ingeschakelde modules, beheerschermen voor
**Personen**, **Groepen** en **Subgroepen**, en een scherm **Modulebeheer**. **Personen**, **Groepen** en **Subgroepen**, en een scherm **Modulebeheer**.
@ -74,18 +79,34 @@ geboortedatum vast.
Een nieuwe module toevoegen = nieuwe app maken, één regel in Een nieuwe module toevoegen = nieuwe app maken, één regel in
`MODULE_APPS` (in `backend/config/settings.py`), klaar. `MODULE_APPS` (in `backend/config/settings.py`), klaar.
## Starten ## Starten (lokaal ontwikkelen)
> Voor een test- of productieserver gelden **andere** stappen: zie `DEPLOY.md`.
> Daar draait alles in Docker en komt de configuratie uit `.env`.
### Backend (Django, poort 8000) ### Backend (Django, poort 8000)
```bash ```bash
cd backend cd backend
python3 -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r requirements.txt pip install -r requirements.txt
# Zonder DJANGO_DEBUG=1 weigert Django te starten: buiten debug is
# DJANGO_SECRET_KEY verplicht. Met debug aan valt hij terug op een dev-sleutel.
export DJANGO_DEBUG=1 # Windows: $env:DJANGO_DEBUG="1"
python manage.py migrate python manage.py migrate
python manage.py createsuperuser # optioneel, voor /admin/ python manage.py createsuperuser # VERPLICHT, zie hieronder
python manage.py runserver python manage.py runserver
``` ```
**`createsuperuser` is geen optionele stap.** De hele API staat op
`IsAuthenticated` en de frontend toont eerst een inlogscherm; zonder account kom
je nergens binnen — ook niet in de app zelf, niet alleen in `/admin/`.
Zonder `POSTGRES_DB` in de omgeving gebruikt de backend automatisch SQLite in
`backend/db.sqlite3`. `DEPLOY.md` zet PostgreSQL in via `.env`.
API-eindpunten: API-eindpunten:
- `GET /api/modules/` — alle modules met status. - `GET /api/modules/` — alle modules met status.
- `POST /api/modules/<key>/state/` — body `{"enabled": true|false}`. - `POST /api/modules/<key>/state/` — body `{"enabled": true|false}`.
@ -111,6 +132,9 @@ API-eindpunten:
### Frontend (React, poort 5173) ### Frontend (React, poort 5173)
Vereist **Node 20.19+ of 22.12+** — Vite 8 weigert oudere versies.
Controleer met `node -v`.
```bash ```bash
cd frontend cd frontend
npm install npm install

View file

@ -1,9 +1,10 @@
# Frontend in twee fases: # Frontend in twee fases:
# 1) Node bouwt de React-app tot statische bestanden. # 1) Node bouwt de React-app tot statische bestanden (Node 22: Vite 8 eist
# ^20.19 of >=22.12, en de CI bouwt ook op 22 — zelfde versie overal).
# 2) Nginx serveert die bestanden en proxyt /api en /admin naar de backend. # 2) Nginx serveert die bestanden en proxyt /api en /admin naar de backend.
# --- Fase 1: build -------------------------------------------------------- # --- Fase 1: build --------------------------------------------------------
FROM node:20-alpine AS build FROM node:22-alpine AS build
WORKDIR /app WORKDIR /app
COPY package*.json ./ COPY package*.json ./
# npm ci (reproduceerbaar) zodra er een package-lock.json in git staat; # npm ci (reproduceerbaar) zodra er een package-lock.json in git staat;

View file

@ -1,12 +1,12 @@
{ {
"name": "roosterwijs-frontend", "name": "roosterwijs-frontend",
"version": "0.2.2-beta", "version": "0.2.3-beta",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "roosterwijs-frontend", "name": "roosterwijs-frontend",
"version": "0.2.2-beta", "version": "0.2.3-beta",
"dependencies": { "dependencies": {
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1" "react-dom": "^18.3.1"

View file

@ -1,7 +1,7 @@
{ {
"name": "roosterwijs-frontend", "name": "roosterwijs-frontend",
"private": true, "private": true,
"version": "0.2.2-beta", "version": "0.2.3-beta",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",

View file

@ -35,7 +35,7 @@ button,a,input,select{transition:border-color .15s,background-color .15s,color .
.cog-btn{width:100%;min-height:38px;justify-content:flex-start;font-size:15px;padding:0} .cog-btn{width:100%;min-height:38px;justify-content:flex-start;font-size:15px;padding:0}
.cog-btn::after{content:"Beheer en instellingen";margin-left:8px;font-size:13px} .cog-btn::after{content:"Beheer en instellingen";margin-left:8px;font-size:13px}
.cog-dropdown{position:static;min-width:0;margin:4px 0 0;box-shadow:none} .cog-dropdown{position:static;min-width:0;margin:4px 0 0;box-shadow:none}
.topbar::after{content:"v0.2.02 beta";order:4;margin:10px 8px 0;color:#98a2b3;font-size:10px} .topbar::after{content:"v0.2.03 beta";order:4;margin:10px 8px 0;color:#98a2b3;font-size:10px}
.content{margin-left:var(--sidebar);width:calc(100% - var(--sidebar));max-width:1680px;padding:32px 38px 48px} .content{margin-left:var(--sidebar);width:calc(100% - var(--sidebar));max-width:1680px;padding:32px 38px 48px}
.content-wide{padding:24px 28px 42px} .content-wide{padding:24px 28px 42px}
h1{font-size:clamp(24px,2.4vw,30px);line-height:1.2;letter-spacing:-.025em;font-weight:750} h1{font-size:clamp(24px,2.4vw,30px);line-height:1.2;letter-spacing:-.025em;font-weight:750}

103
scripts/deploy.sh Normal file → Executable file
View file

@ -1,19 +1,102 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Op de test-server: haal de laatste code op en herbouw de containers. # Op de test-server: haal de laatste code op en herbouw de containers.
# Gebruik: ./scripts/deploy.sh #
# Gebruik: ./scripts/deploy.sh [--force]
# --force ook doorgaan als er lokale wijzigingen zijn
# (let op: die worden dan overschreven)
set -euo pipefail set -euo pipefail
cd "$(dirname "$0")/.." cd "$(dirname "$0")/.."
branch="$(git rev-parse --abbrev-ref HEAD)" force=0
echo "Branch: $branch" for arg in "${@:-}"; do
git fetch origin case "$arg" in
# Forceer exact gelijk aan de remote (voorkomt 'already up to date'-verwarring). "") ;;
git reset --hard "origin/$branch" --force) force=1 ;;
echo "Nu op commit:" *) echo "Onbekende optie: $arg" >&2; exit 2 ;;
git --no-pager log --oneline -1 esac
done
# --- Vooraf controleren ---------------------------------------------------
if ! command -v docker >/dev/null 2>&1; then
echo "FOUT: docker is niet geïnstalleerd op deze server." >&2
exit 1
fi
if ! docker compose version >/dev/null 2>&1; then
echo "FOUT: 'docker compose' werkt niet (compose-plugin ontbreekt)." >&2
exit 1
fi
# Zonder .env start de stack met lege variabelen: compose vult dan overal
# lege strings in, de backend ziet geen POSTGRES_DB (valt terug op SQlite in
# de container) en weigert te starten omdat DJANGO_SECRET_KEY ontbreekt.
if [ ! -f .env ]; then
echo "FOUT: .env ontbreekt in $(pwd)." >&2
echo " Maak hem aan met: cp .env.example .env && nano .env" >&2
exit 1
fi
# 'git reset --hard' hieronder gooit lokale aanpassingen weg — bijvoorbeeld een
# met de hand aangepaste nginx.conf. Daarom eerst waarschuwen.
if [ -n "$(git status --porcelain)" ]; then
echo "LET OP: er staan lokale wijzigingen in deze map:"
git --no-pager status --short | sed 's/^/ /'
if [ "$force" -ne 1 ]; then
echo
echo "Die gaan verloren bij het bijwerken. Zet ze eerst veilig, of draai:" >&2
echo " ./scripts/deploy.sh --force" >&2
exit 1
fi
echo "--force opgegeven: deze wijzigingen worden overschreven."
fi
# --- Code bijwerken -------------------------------------------------------
branch="$(git rev-parse --abbrev-ref HEAD)"
oud="$(git rev-parse --short HEAD)"
echo "Branch: $branch (nu op $oud)"
git fetch origin
git reset --hard "origin/$branch"
nieuw="$(git rev-parse --short HEAD)"
if [ "$oud" = "$nieuw" ]; then
echo "Geen nieuwe commits; containers worden wel opnieuw gebouwd."
else
echo "Bijgewerkt: $oud -> $nieuw"
git --no-pager log --oneline "$oud..$nieuw" | sed 's/^/ /'
fi
# --- Bouwen en starten ----------------------------------------------------
docker compose up -d --build docker compose up -d --build
echo "Controle: nieuwe frontend-bundel aanwezig?" # --- Controleren ----------------------------------------------------------
docker compose exec -T nginx sh -c 'ls /usr/share/nginx/html/assets/*.js >/dev/null 2>&1 && echo " frontend OK" || echo " GEEN frontend-bundel gevonden"' echo
echo "Controle: frontend-bundel aanwezig?"
if docker compose exec -T nginx sh -c 'ls /usr/share/nginx/html/assets/*.js >/dev/null 2>&1'; then
echo " frontend OK"
else
echo " GEEN frontend-bundel gevonden"
fi
echo "Controle: reageert de backend?"
docker compose exec -T backend python -c "
import urllib.request, urllib.error
try:
r = urllib.request.urlopen('http://127.0.0.1:8000/api/info/', timeout=5)
print(' backend OK (HTTP', r.status, ')')
except urllib.error.HTTPError as e:
print(' backend OK (HTTP', e.code, '- reageert)')
except Exception as e:
print(' GEEN antwoord van de backend:', e)
" || echo " Kon de backend niet bereiken — zie 'docker compose logs backend'."
echo "Controle: is er een gebruiker om mee in te loggen?"
docker compose exec -T backend python manage.py shell -c "
from django.contrib.auth import get_user_model
n = get_user_model().objects.filter(is_active=True).count()
print(' actieve gebruikers:', n)
if n == 0:
print(' LET OP: geen actief account. Maak er een met:')
print(' docker compose exec backend python manage.py createsuperuser')
" || echo " Kon de gebruikers niet tellen — zie 'docker compose logs backend'."
echo "Klaar." echo "Klaar."