Compare commits

..

No commits in common. "ui-before-merged-beheer-v0.4.08" and "main" have entirely different histories.

171 changed files with 4866 additions and 19509 deletions

View file

@ -13,8 +13,3 @@ POSTGRES_PASSWORD=change-me-locally
# Connectiestring die de app gebruikt.
# Hostnaam 'db' verwijst naar de postgres-service binnen het compose-netwerk.
DATABASE_URL=postgres://teach:change-me-locally@db:5432/teach
# Eerste overkoepelende beheerder (wordt eenmalig aangemaakt als er nog geen bestaat).
# Laat SUPER_PASS leeg om een willekeurig wachtwoord in de serverlog te krijgen.
SUPER_USER=beheerder
SUPER_PASS=

View file

@ -11,7 +11,7 @@ concurrency:
jobs:
build-and-deploy:
runs-on: dev # jouw dev-runner (test-server)
runs-on: ubuntu-latest # pas aan naar het label van jouw Forgejo-runner
env:
# 10.0.20.22:3000 - host van je Forgejo (= container registry host)
REGISTRY: ${{ vars.REGISTRY }}
@ -19,23 +19,13 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Dependencies en securitytests
run: |
docker run --rm \
-v "$PWD:/app" \
-v /app/node_modules \
-w /app node:22-alpine \
sh -lc 'npm ci && npm test && npm audit --omit=dev --audit-level=moderate'
- name: Image-tags bepalen
id: meta
run: |
IMAGE="${REGISTRY}/${IMAGE_NAME}"
VERSION="$(tr -d '[:space:]' < VERSION)"
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
echo "sha_tag=${IMAGE}:dev-${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
echo "dev_tag=${IMAGE}:dev" >> "$GITHUB_OUTPUT"
echo "app_version=${VERSION}" >> "$GITHUB_OUTPUT"
# Platte docker build/push i.p.v. buildx: betrouwbaarder tegen een
# HTTP-registry. De runner-host moet 10.0.20.22:3000 als
@ -44,25 +34,24 @@ jobs:
run: |
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "$REGISTRY" -u "${{ secrets.REGISTRY_USER }}" --password-stdin
docker build \
--build-arg APP_VERSION="${{ steps.meta.outputs.app_version }}" \
--build-arg GIT_SHA="${GITHUB_SHA::7}" \
--build-arg APP_VERSION="dev-${GITHUB_SHA::7}" \
-t "${{ steps.meta.outputs.sha_tag }}" \
-t "${{ steps.meta.outputs.dev_tag }}" .
docker push "${{ steps.meta.outputs.sha_tag }}"
docker push "${{ steps.meta.outputs.dev_tag }}"
# Deploy via gewone ssh (geen externe action nodig).
- name: Deploy naar test-VM
run: |
echo "${{ secrets.TEST_SSH_KEY }}" > deploy_key
chmod 600 deploy_key
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
-i deploy_key deploy/compose.deploy.yaml \
${{ secrets.TEST_USER }}@${{ secrets.TEST_HOST }}:${{ secrets.TEST_DEPLOY_PATH }}/compose.deploy.yaml
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
-i deploy_key deploy/nginx.conf \
${{ secrets.TEST_USER }}@${{ secrets.TEST_HOST }}:${{ secrets.TEST_DEPLOY_PATH }}/nginx.conf.new
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
-i deploy_key ${{ secrets.TEST_USER }}@${{ secrets.TEST_HOST }} \
"set -e && cd ${{ secrets.TEST_DEPLOY_PATH }} && mv -f nginx.conf.new nginx.conf && echo '${{ secrets.REGISTRY_TOKEN }}' | docker login ${{ vars.REGISTRY }} -u '${{ secrets.REGISTRY_USER }}' --password-stdin && sed -i 's|^IMAGE=.*|IMAGE=${{ steps.meta.outputs.dev_tag }}|' .env && docker compose -f compose.deploy.yaml pull app && docker compose -f compose.deploy.yaml up -d --force-recreate app web && docker image prune -f"
rm -f deploy_key
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.TEST_HOST }}
username: ${{ secrets.TEST_USER }}
key: ${{ secrets.TEST_SSH_KEY }}
script: |
set -e
cd ${{ secrets.TEST_DEPLOY_PATH }}
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${{ vars.REGISTRY }} -u "${{ secrets.REGISTRY_USER }}" --password-stdin
# Zet de te draaien image-tag in de env van de VM
sed -i "s|^IMAGE=.*|IMAGE=${{ steps.meta.outputs.dev_tag }}|" .env
docker compose -f compose.deploy.yaml pull
docker compose -f compose.deploy.yaml up -d
docker image prune -f

View file

@ -12,21 +12,13 @@ concurrency:
jobs:
build-and-deploy:
runs-on: prod # jouw prod-runner (productieserver)
runs-on: ubuntu-latest # pas aan naar het label van jouw Forgejo-runner
env:
REGISTRY: ${{ vars.REGISTRY }}
IMAGE_NAME: ${{ github.repository }} # bes-r/teach
steps:
- uses: actions/checkout@v4
- name: Dependencies en securitytests
run: |
docker run --rm \
-v "$PWD:/app" \
-v /app/node_modules \
-w /app node:22-alpine \
sh -lc 'npm ci && npm test && npm audit --omit=dev --audit-level=moderate'
- name: Image-tags bepalen
id: meta
run: |
@ -43,24 +35,22 @@ jobs:
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "$REGISTRY" -u "${{ secrets.REGISTRY_USER }}" --password-stdin
docker build \
--build-arg APP_VERSION="${{ steps.meta.outputs.version }}" \
--build-arg GIT_SHA="${GITHUB_SHA::7}" \
-t "${{ steps.meta.outputs.version_tag }}" \
-t "${{ steps.meta.outputs.latest_tag }}" .
docker push "${{ steps.meta.outputs.version_tag }}"
docker push "${{ steps.meta.outputs.latest_tag }}"
# Deploy via gewone ssh (geen externe action nodig).
- name: Deploy naar productie-VM
run: |
echo "${{ secrets.PROD_SSH_KEY }}" > deploy_key
chmod 600 deploy_key
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
-i deploy_key deploy/compose.deploy.yaml \
${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}:${{ secrets.PROD_DEPLOY_PATH }}/compose.deploy.yaml
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
-i deploy_key deploy/nginx.conf \
${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }}:${{ secrets.PROD_DEPLOY_PATH }}/nginx.conf.new
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o BatchMode=yes -o ConnectTimeout=15 \
-i deploy_key ${{ secrets.PROD_USER }}@${{ secrets.PROD_HOST }} \
"set -e && cd ${{ secrets.PROD_DEPLOY_PATH }} && mv -f nginx.conf.new nginx.conf && echo '${{ secrets.REGISTRY_TOKEN }}' | docker login ${{ vars.REGISTRY }} -u '${{ secrets.REGISTRY_USER }}' --password-stdin && sed -i 's|^IMAGE=.*|IMAGE=${{ steps.meta.outputs.version_tag }}|' .env && docker compose -f compose.deploy.yaml pull app && docker compose -f compose.deploy.yaml up -d --force-recreate app web && docker image prune -f"
rm -f deploy_key
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PROD_HOST }}
username: ${{ secrets.PROD_USER }}
key: ${{ secrets.PROD_SSH_KEY }}
script: |
set -e
cd ${{ secrets.PROD_DEPLOY_PATH }}
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${{ vars.REGISTRY }} -u "${{ secrets.REGISTRY_USER }}" --password-stdin
sed -i "s|^IMAGE=.*|IMAGE=${{ steps.meta.outputs.version_tag }}|" .env
docker compose -f compose.deploy.yaml pull
docker compose -f compose.deploy.yaml up -d
docker image prune -f

1
.gitignore vendored
View file

@ -8,7 +8,6 @@ npm-debug.log*
# Postgres data volume bij lokaal draaien
pgdata/
storage/
# OS / editor
.DS_Store

View file

@ -1,11 +0,0 @@
# Projectafspraken
- Werk nooit rechtstreeks op main
- Push naar de dev branch
- Werk met versienummering (v0.0.00)
- In de dev-branch altijd beta weergeven bij de versienummering
- Beschrijf gewijzigde functionaliteiten puntsgewijs in de pull request.
- Gebruik geen sudo
- Commit geen wachtwoorden, tokens, `.env`-bestanden of privésleutels.
- Voer relevante tests uit voor iedere commit.
- Push wijzigingen naar Forgejo, maar merge niet zelfstandig.
- Bij opdracht "Release versie" maak een versie releaseversie

View file

@ -1,11 +0,0 @@
# Projectafspraken
- Werk nooit rechtstreeks op main
- Push naar de dev branch
- Werk met versienummering (v0.0.00)
- In de dev-branch altijd beta weergeven bij de versienummering
- Beschrijf gewijzigde functionaliteiten puntsgewijs in de pull request.
- Gebruik geen sudo
- Commit geen wachtwoorden, tokens, `.env`-bestanden of privésleutels.
- Voer relevante tests uit voor iedere commit.
- Push wijzigingen naar Forgejo, maar merge niet zelfstandig.
- Bij opdracht "Release versie" maak een versie releaseversie

View file

@ -13,19 +13,16 @@ ENV NODE_ENV=production
WORKDIR /app
# Draai als non-root gebruiker
RUN addgroup -S app && adduser -S app -G app && mkdir -p /app/storage/images && chown -R app:app /app/storage
RUN addgroup -S app && adduser -S app -G app
COPY --chown=app:app --from=deps /app/node_modules ./node_modules
COPY --chown=app:app package.json ./
COPY --chown=app:app src ./src
COPY --chown=app:app public ./public
COPY --chown=app:app db ./db
# Versie + commit-hash worden tijdens de build meegegeven door CI
# Versie wordt tijdens de build meegegeven door CI
ARG APP_VERSION=dev
ENV APP_VERSION=$APP_VERSION
ARG GIT_SHA=
ENV GIT_SHA=$GIT_SHA
USER app
EXPOSE 3000

View file

@ -35,9 +35,8 @@ App op http://localhost:3000, healthcheck op `/healthz`, DB-check op `/readyz`.
serveert de HTML én biedt de `/api`, en praat met de DB.
- **Forgejo container registry**: CI bouwt de image één keer en pusht die; beide
VM's pullen exact dezelfde geteste image. Geen build op de productie-VM.
- **Pangolin/Traefik verzorgt de publieke HTTPS-ingang**. De meegeleverde nginx
vormt de interne proxylaag en luistert standaard op poort `8081`, zodat de
Pangolin/Traefik-route deze via het VM-/containernetwerk kan bereiken. De Fastify-app vertrouwt precies twee proxy-hops.
- **Nginx blijft ervoor** als reverse proxy / TLS op elke VM; de app luistert
alleen op `127.0.0.1:<APP_PORT>`.
## Eenmalige setup
@ -108,29 +107,28 @@ POSTGRES_USER=teach
POSTGRES_PASSWORD=<sterk-wachtwoord>
DATABASE_URL=postgres://teach:<sterk-wachtwoord>@db:5432/teach
APP_PORT=3000
WEB_BIND_IP=0.0.0.0
WEB_PORT=8081
TRUST_PROXY_HOPS=2
SUPER_USER=beheerder
SUPER_PASS=<uniek-sterk-eerste-wachtwoord>
EOF
```
CI werkt bij elke deploy de `IMAGE=`-regel bij, pullt en herstart.
### 4. Pangolin/Traefik publiceren
### 4. Nginx reverse proxy (per VM)
```nginx
server {
listen 443 ssl;
server_name digibord.familiebesselink.nl; # test-VM eigen subdomein
Publiceer `http://<interne-vm-ip>:8081` via Pangolin/Traefik en laat daar TLS
beëindigen. De interne nginx behoudt `X-Forwarded-Proto: https`, waarna de app
Secure/HttpOnly/SameSite-cookies, HSTS en overige securityheaders gebruikt.
# ssl_certificate ... (bestaande config)
- Beperk poort 8081 met de hostfirewall tot het Pangolin/Traefik- of tunnelnetwerk.
- Pas `TRUST_PROXY_HOPS` alleen aan wanneer de proxyketen werkelijk verandert.
- Bij een aparte Traefik-container kan een gedeeld intern Docker-netwerk nodig
zijn; zet `WEB_BIND_IP` niet ruimer dan noodzakelijk.
- `SUPER_PASS` is alleen nodig bij een lege database, wordt nooit gelogd en
moet via de beveiligde deploy-omgeving worden aangeleverd.
- De overgang naar v0.3.03-beta trekt bestaande browsersessies bewust in.
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
## Workflow / branching

View file

@ -1 +0,0 @@
0.4.08-beta

View file

@ -16,14 +16,8 @@ services:
PORT: 3000
DATABASE_URL: ${DATABASE_URL}
APP_VERSION: dev
SUPER_USER: ${SUPER_USER:-beheerder}
SUPER_PASS: ${SUPER_PASS:-}
TRUST_PROXY_HOPS: 0
COOKIE_SECURE: "false"
ports:
- "3000:3000"
volumes:
- image_data:/app/storage/images
restart: unless-stopped
db:
@ -45,4 +39,3 @@ services:
volumes:
pgdata:
image_data:

View file

@ -1,52 +0,0 @@
-- v0.2.00: scholen, klassen, rollen en sessies.
-- Rollen: super (overkoepelend beheerder), admin (schoolbeheerder),
-- teacher (groepsleiding), pupil (leerling).
CREATE TABLE IF NOT EXISTS schools (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS classes (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
school_id BIGINT NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
name TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE users
ADD COLUMN IF NOT EXISTS school_id BIGINT REFERENCES schools(id) ON DELETE CASCADE,
ADD COLUMN IF NOT EXISTS display_name TEXT,
ADD COLUMN IF NOT EXISTS password_hash TEXT,
-- alleen voor leerlingen: wachtwoord dat groepsleiding/beheer mag inzien
ADD COLUMN IF NOT EXISTS password_plain TEXT,
-- eenmalige koppelcode waarmee nieuwe gebruikers hun account activeren
ADD COLUMN IF NOT EXISTS link_code TEXT UNIQUE,
ADD COLUMN IF NOT EXISTS class_id BIGINT REFERENCES classes(id) ON DELETE SET NULL,
-- alle app-gegevens van de gebruiker (borden, thema's, eigen woorden)
ADD COLUMN IF NOT EXISTS data JSONB NOT NULL DEFAULT '{}'::jsonb;
-- gebruikersnaam uniek binnen een school; supers (school_id IS NULL) landelijk uniek
ALTER TABLE users DROP CONSTRAINT IF EXISTS users_username_key;
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_school_username
ON users (school_id, lower(username)) WHERE school_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_super_username
ON users (lower(username)) WHERE school_id IS NULL;
-- welke groepsleiding hoort bij welke klas
CREATE TABLE IF NOT EXISTS class_teachers (
class_id BIGINT NOT NULL REFERENCES classes(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
PRIMARY KEY (class_id, user_id)
);
CREATE TABLE IF NOT EXISTS sessions (
token TEXT PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_users_school ON users(school_id);
CREATE INDEX IF NOT EXISTS idx_users_class ON users(class_id);

View file

@ -1,5 +0,0 @@
-- v0.3.00: bijhouden of een account al gebruikt is, zodat leerling-
-- wachtwoorden na de eerste keer inloggen niet langer inzichtelijk zijn.
ALTER TABLE users
ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ;

View file

@ -1,9 +0,0 @@
-- v0.2.21: extra functies naast de hoofdrol (users.role) - alleen voor stafaccounts.
-- Leerlingen krijgen nooit een extra rol; dat wordt in de applicatielaag afgedwongen
-- (net als de bestaande regel dat alleen super rollen mag wijzigen).
CREATE TABLE IF NOT EXISTS user_roles (
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role TEXT NOT NULL,
PRIMARY KEY (user_id, role)
);
CREATE INDEX IF NOT EXISTS idx_user_roles_user ON user_roles(user_id);

View file

@ -1,16 +0,0 @@
-- Toewijzingen: een leerkracht koppelt één van haar eigen borden (via het
-- stabiele board-id uit users.data) aan een klas of een individuele leerling.
-- Puur additief, leeg bij aanmaak. Precies één van class_id/pupil_id gezet:
-- een klas-toewijzing (standaard voor iedereen in de klas) of een leerling-
-- toewijzing (uitzondering, voor een leerling op een ander niveau).
CREATE TABLE IF NOT EXISTS assignments (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
class_id BIGINT REFERENCES classes(id) ON DELETE CASCADE,
pupil_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
teacher_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
board_id TEXT NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (num_nonnulls(class_id, pupil_id) = 1)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_assignments_class ON assignments(class_id) WHERE class_id IS NOT NULL;
CREATE UNIQUE INDEX IF NOT EXISTS idx_assignments_pupil ON assignments(pupil_id) WHERE pupil_id IS NOT NULL;

View file

@ -1,62 +0,0 @@
-- v0.3.03: tenant-isolatie en security-invarianten ook in PostgreSQL afdwingen.
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
-- Leerlingwachtwoorden worden voortaan alleen eenmalig in de API-respons getoond.
ALTER TABLE users DROP COLUMN IF EXISTS password_plain;
-- Ruim eventuele historische tenant-kruisingen veilig op vóór de triggers actief worden.
UPDATE users u SET class_id = NULL
WHERE class_id IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM classes c WHERE c.id = u.class_id AND c.school_id = u.school_id
);
DELETE FROM class_teachers ct
WHERE NOT EXISTS (
SELECT 1 FROM classes c
JOIN users u ON u.id = ct.user_id AND u.school_id = c.school_id
WHERE c.id = ct.class_id
AND (u.role = 'teacher' OR EXISTS (
SELECT 1 FROM user_roles ur WHERE ur.user_id = u.id AND ur.role = 'teacher'
))
);
CREATE OR REPLACE FUNCTION teach_validate_user_class_school()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NEW.class_id IS NOT NULL AND NOT EXISTS (
SELECT 1 FROM classes c WHERE c.id = NEW.class_id AND c.school_id = NEW.school_id
) THEN
RAISE EXCEPTION 'leerling en klas moeten bij dezelfde school horen';
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_users_class_school ON users;
CREATE TRIGGER trg_users_class_school
BEFORE INSERT OR UPDATE OF class_id, school_id ON users
FOR EACH ROW EXECUTE FUNCTION teach_validate_user_class_school();
CREATE OR REPLACE FUNCTION teach_validate_class_teacher()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM classes c
JOIN users u ON u.id = NEW.user_id AND u.school_id = c.school_id
WHERE c.id = NEW.class_id
AND (u.role = 'teacher' OR EXISTS (
SELECT 1 FROM user_roles ur WHERE ur.user_id = u.id AND ur.role = 'teacher'
))
) THEN
RAISE EXCEPTION 'groepsleiding en klas moeten bij dezelfde school horen';
END IF;
RETURN NEW;
END;
$$;
DROP TRIGGER IF EXISTS trg_class_teachers_tenant ON class_teachers;
CREATE TRIGGER trg_class_teachers_tenant
BEFORE INSERT OR UPDATE ON class_teachers
FOR EACH ROW EXECUTE FUNCTION teach_validate_class_teacher();

View file

@ -1,19 +0,0 @@
-- Voortgang: leerlingen loggen elk afgerond "moment" in een widget (ronde
-- sommen, voltooid woord, geknapte ballon) zodat leerkrachten een tijdlijn
-- per leerling kunnen bekijken. teacher_id/board_id komen server-side uit de
-- bestaande assignments-tabel, nooit van de client, om te voorkomen dat een
-- leerling voortgang onder een andere leerkracht kan wegschrijven.
CREATE TABLE IF NOT EXISTS progress_events (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
pupil_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
teacher_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
board_id TEXT NOT NULL,
widget_id TEXT NOT NULL,
widget_type TEXT NOT NULL,
attempts INT NOT NULL DEFAULT 0,
correct INT NOT NULL DEFAULT 0,
stars INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_progress_events_pupil ON progress_events(pupil_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_progress_events_teacher ON progress_events(teacher_id, created_at DESC);

View file

@ -1,7 +0,0 @@
-- Widget-delen: een toewijzing kan naast een heel bord (widget_id NULL,
-- bestaand gedrag) ook één enkele widget van dat bord delen, met een modus:
-- 'kijken' (leerling ziet de widget read-only, snellere poll) of 'werken'
-- (leerling werkt erin; voortgang loopt via progress_events). widget_id
-- verwijst naar het stabiele wid in de borddata van de leerkracht.
ALTER TABLE assignments ADD COLUMN IF NOT EXISTS widget_id TEXT;
ALTER TABLE assignments ADD COLUMN IF NOT EXISTS mode TEXT NOT NULL DEFAULT 'werken';

View file

@ -1,16 +0,0 @@
-- Opslaansysteem: optimistische concurrency + versiegeschiedenis.
-- data_rev telt elke wijziging van users.data; een client stuurt de rev mee
-- die hij geladen heeft en de update slaagt alleen als die nog actueel is
-- (anders 409: een ander tabblad/digibord schreef eerder). user_data_versions
-- bewaart periodieke snapshots van de vorige data zodat een leerkracht een
-- eerdere versie kan terugzetten.
ALTER TABLE users ADD COLUMN IF NOT EXISTS data_rev BIGINT NOT NULL DEFAULT 0;
ALTER TABLE users ADD COLUMN IF NOT EXISTS data_updated_at TIMESTAMPTZ;
CREATE TABLE IF NOT EXISTS user_data_versions (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
data JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_user_data_versions ON user_data_versions(user_id, created_at DESC);

View file

@ -1,17 +0,0 @@
-- Gedeelde bibliotheek: borden en ankerdiagrammen die de systeemmanager
-- (school_id NULL = systeembreed, voor iedereen zichtbaar) of stafleden van
-- een school (school_id gezet = alleen binnen die school zichtbaar) hebben
-- klaargezet. Andere gebruikers kopiëren een item naar hun eigen collectie;
-- er wordt niet live in gedeelde items gewerkt. folder is een pad met
-- "/"-scheiding, dezelfde conventie als de mappenverkenner.
CREATE TABLE IF NOT EXISTS shared_library (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
school_id BIGINT REFERENCES schools(id) ON DELETE CASCADE,
owner_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
kind TEXT NOT NULL,
folder TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL,
data JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_shared_library ON shared_library(kind, school_id);

View file

@ -1,6 +0,0 @@
-- Alleen-lezen-vlag voor gedeelde items: een vergrendeld school-item kan ook
-- door de eigenaar niet meer gewijzigd worden, alleen nog door de
-- schoolbeheerder of de systeemmanager (die de vlag ook zetten/weghalen).
-- Systeembrede items zijn impliciet al alleen-lezen voor iedereen behalve de
-- systeemmanager (beheerregel), de vlag is daar niet voor nodig.
ALTER TABLE shared_library ADD COLUMN IF NOT EXISTS readonly BOOLEAN NOT NULL DEFAULT false;

View file

@ -1,130 +0,0 @@
-- v0.3.55-beta: doorzoekbare afbeeldingscatalogus met meervoudige thema's,
-- map-paden, eigenaarschap en instelbare opslagquota.
ALTER TABLE users
ADD COLUMN IF NOT EXISTS image_quota_bytes BIGINT
CHECK (image_quota_bytes IS NULL OR image_quota_bytes >= -1);
ALTER TABLE schools
ADD COLUMN IF NOT EXISTS image_quota_bytes BIGINT
CHECK (image_quota_bytes IS NULL OR image_quota_bytes >= -1);
CREATE TABLE IF NOT EXISTS image_themes (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
key TEXT UNIQUE,
scope TEXT NOT NULL CHECK (scope IN ('global','school','personal')),
school_id BIGINT REFERENCES schools(id) ON DELETE CASCADE,
owner_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
folder TEXT NOT NULL DEFAULT '',
name_nl TEXT NOT NULL,
name_en TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (
(scope = 'global' AND school_id IS NULL) OR
(scope = 'school' AND school_id IS NOT NULL AND owner_id IS NOT NULL) OR
(scope = 'personal' AND owner_id IS NOT NULL)
)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_themes_global_name
ON image_themes (lower(name_nl)) WHERE scope = 'global';
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_themes_school_name
ON image_themes (school_id, lower(name_nl)) WHERE scope = 'school';
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_themes_personal_name
ON image_themes (owner_id, lower(name_nl)) WHERE scope = 'personal';
CREATE TABLE IF NOT EXISTS image_folders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
scope TEXT NOT NULL CHECK (scope IN ('global','school','personal')),
school_id BIGINT REFERENCES schools(id) ON DELETE CASCADE,
owner_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
path TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK (
(scope = 'global' AND school_id IS NULL) OR
(scope = 'school' AND school_id IS NOT NULL AND owner_id IS NOT NULL) OR
(scope = 'personal' AND owner_id IS NOT NULL)
)
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_folders_global_path
ON image_folders (path) WHERE scope = 'global';
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_folders_school_path
ON image_folders (school_id, path) WHERE scope = 'school';
CREATE UNIQUE INDEX IF NOT EXISTS idx_image_folders_personal_path
ON image_folders (owner_id, path) WHERE scope = 'personal';
CREATE TABLE IF NOT EXISTS image_assets (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
key TEXT NOT NULL UNIQUE,
scope TEXT NOT NULL CHECK (scope IN ('global','school','personal')),
school_id BIGINT REFERENCES schools(id) ON DELETE CASCADE,
owner_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
folder TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL,
original_name TEXT,
mime_type TEXT NOT NULL,
size_bytes BIGINT NOT NULL DEFAULT 0 CHECK (size_bytes >= 0),
storage_key TEXT UNIQUE,
public_path TEXT UNIQUE,
tags TEXT[] NOT NULL DEFAULT '{}',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
CHECK ((storage_key IS NULL) <> (public_path IS NULL)),
CHECK (
(scope = 'global' AND school_id IS NULL) OR
(scope = 'school' AND school_id IS NOT NULL AND owner_id IS NOT NULL) OR
(scope = 'personal' AND owner_id IS NOT NULL)
)
);
CREATE INDEX IF NOT EXISTS idx_image_assets_owner ON image_assets(owner_id);
CREATE INDEX IF NOT EXISTS idx_image_assets_school ON image_assets(school_id);
CREATE INDEX IF NOT EXISTS idx_image_assets_folder ON image_assets(scope, folder);
CREATE TABLE IF NOT EXISTS image_asset_themes (
asset_id BIGINT NOT NULL REFERENCES image_assets(id) ON DELETE CASCADE,
theme_id BIGINT NOT NULL REFERENCES image_themes(id) ON DELETE CASCADE,
PRIMARY KEY (asset_id, theme_id)
);
CREATE INDEX IF NOT EXISTS idx_image_asset_themes_theme ON image_asset_themes(theme_id);
INSERT INTO image_themes (key, scope, name_nl, name_en)
VALUES
('builtin-money', 'global', 'Geld', 'Money'),
('builtin-coins', 'global', 'Munten', 'Coins'),
('builtin-banknotes', 'global', 'Briefgeld', 'Banknotes')
ON CONFLICT (key) DO NOTHING;
INSERT INTO image_folders (scope, path)
VALUES ('global','Rekenen'), ('global','Rekenen/Geld'),
('global','Rekenen/Geld/Munten'), ('global','Rekenen/Geld/Briefgeld')
ON CONFLICT DO NOTHING;
INSERT INTO image_assets (key, scope, folder, name, mime_type, public_path, tags)
VALUES
('builtin-money-coin-1', 'global', 'Rekenen/Geld/Munten', '1 cent', 'image/png', '/img/money/coin-1.png', ARRAY['geld','money','munt','coin','cent']),
('builtin-money-coin-2', 'global', 'Rekenen/Geld/Munten', '2 cent', 'image/png', '/img/money/coin-2.png', ARRAY['geld','money','munt','coin','cent']),
('builtin-money-coin-5', 'global', 'Rekenen/Geld/Munten', '5 cent', 'image/png', '/img/money/coin-5.png', ARRAY['geld','money','munt','coin','cent']),
('builtin-money-coin-10', 'global', 'Rekenen/Geld/Munten', '10 cent', 'image/png', '/img/money/coin-10.png', ARRAY['geld','money','munt','coin','cent']),
('builtin-money-coin-20', 'global', 'Rekenen/Geld/Munten', '20 cent', 'image/png', '/img/money/coin-20.png', ARRAY['geld','money','munt','coin','cent']),
('builtin-money-coin-50', 'global', 'Rekenen/Geld/Munten', '50 cent', 'image/png', '/img/money/coin-50.png', ARRAY['geld','money','munt','coin','cent']),
('builtin-money-coin-100', 'global', 'Rekenen/Geld/Munten', '1 euro', 'image/png', '/img/money/coin-100.png', ARRAY['geld','money','munt','coin','euro']),
('builtin-money-coin-200', 'global', 'Rekenen/Geld/Munten', '2 euro', 'image/png', '/img/money/coin-200.png', ARRAY['geld','money','munt','coin','euro']),
('builtin-money-note-500', 'global', 'Rekenen/Geld/Briefgeld', '5 euro', 'image/jpeg', '/img/money/note-500.jpg', ARRAY['geld','money','briefgeld','banknote','euro']),
('builtin-money-note-1000', 'global', 'Rekenen/Geld/Briefgeld', '10 euro', 'image/jpeg', '/img/money/note-1000.jpg', ARRAY['geld','money','briefgeld','banknote','euro']),
('builtin-money-note-2000', 'global', 'Rekenen/Geld/Briefgeld', '20 euro', 'image/jpeg', '/img/money/note-2000.jpg', ARRAY['geld','money','briefgeld','banknote','euro']),
('builtin-money-note-5000', 'global', 'Rekenen/Geld/Briefgeld', '50 euro', 'image/jpeg', '/img/money/note-5000.jpg', ARRAY['geld','money','briefgeld','banknote','euro'])
ON CONFLICT (key) DO NOTHING;
INSERT INTO image_asset_themes (asset_id, theme_id)
SELECT a.id, t.id
FROM image_assets a
JOIN image_themes t ON t.key = 'builtin-money'
WHERE a.key LIKE 'builtin-money-%'
ON CONFLICT DO NOTHING;
INSERT INTO image_asset_themes (asset_id, theme_id)
SELECT a.id, t.id
FROM image_assets a
JOIN image_themes t ON t.key = CASE
WHEN a.key LIKE 'builtin-money-coin-%' THEN 'builtin-coins'
ELSE 'builtin-banknotes'
END
WHERE a.key LIKE 'builtin-money-%'
ON CONFLICT DO NOTHING;

View file

@ -1,30 +0,0 @@
-- v0.3.57-beta: ouderportaal.
-- Ouders (role 'parent', school_id NULL) koppelen aan kind-accounts (pupils):
-- * parent_children: de koppeling zelf (een kind kan meerdere ouders hebben).
-- * users.parent_code: eenmalige code waarmee staf een schoolleerling aan een
-- ouder koppelt (code vervalt na gebruik).
-- * school_link_requests: aanmeldverzoek van een thuiskind (pupil zonder
-- school) bij een school; de school handelt het verzoek af (adopteren,
-- koppelen aan een bestaande leerling of afwijzen).
CREATE TABLE IF NOT EXISTS parent_children (
parent_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
pupil_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (parent_id, pupil_id)
);
CREATE INDEX IF NOT EXISTS idx_parent_children_pupil ON parent_children(pupil_id);
ALTER TABLE users ADD COLUMN IF NOT EXISTS parent_code TEXT;
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_parent_code
ON users (parent_code) WHERE parent_code IS NOT NULL;
CREATE TABLE IF NOT EXISTS school_link_requests (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
school_id BIGINT NOT NULL REFERENCES schools(id) ON DELETE CASCADE,
parent_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
pupil_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE (pupil_id, school_id)
);
CREATE INDEX IF NOT EXISTS idx_school_link_requests_school ON school_link_requests(school_id);

View file

@ -1,11 +0,0 @@
-- v0.3.83-beta: CMS voor de systeemmanager.
-- Kleine sleutel/waarde-opslag voor site-inhoud die zonder release aanpasbaar
-- moet zijn: welkomstwoord (inlogscherm), update-meldingen (banner) en
-- aanvullende widget-uitleg (galerij). De waarde is JSONB zodat elke sleutel
-- zijn eigen vorm heeft (bv. { nl, en } of { actief, tekst per taal, id }).
CREATE TABLE IF NOT EXISTS site_content (
key TEXT PRIMARY KEY,
value JSONB NOT NULL DEFAULT '{}'::jsonb,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_by BIGINT REFERENCES users(id) ON DELETE SET NULL
);

View file

@ -1,33 +0,0 @@
-- v0.3.95-beta: een gebruiker moet altijd te verwijderen zijn.
--
-- Alle foreign keys die naar users(id) verwijzen horen ON DELETE CASCADE te
-- zijn (of bewust SET NULL, zoals site_content.updated_by). Databases die zijn
-- ontstaan vóór een tabel z'n cascade kreeg houden echter de oude constraint:
-- `CREATE TABLE IF NOT EXISTS` werkt een bestaande tabel nooit bij. Op zo'n
-- database geeft het verwijderen van een gebruiker een foreign-key-fout (500
-- "serverfout"). Deze migratie zoekt elke FK naar users die NIET cascade en
-- NIET set-null is, en zet die alsnog op ON DELETE CASCADE - de bewuste
-- SET NULL-constraints blijven ongemoeid.
DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN
SELECT con.conname AS name,
con.conrelid::regclass::text AS tbl,
att.attname AS col
FROM pg_constraint con
JOIN pg_attribute att
ON att.attrelid = con.conrelid AND att.attnum = con.conkey[1]
WHERE con.contype = 'f'
AND con.confrelid = 'users'::regclass
AND array_length(con.conkey, 1) = 1 -- alle user-FK's zijn 1 kolom
AND con.confdeltype NOT IN ('c', 'n') -- niet cascade, niet set null
LOOP
EXECUTE format('ALTER TABLE %s DROP CONSTRAINT %I', r.tbl, r.name);
EXECUTE format(
'ALTER TABLE %s ADD CONSTRAINT %I FOREIGN KEY (%I) REFERENCES users(id) ON DELETE CASCADE',
r.tbl, r.name, r.col);
RAISE NOTICE 'FK % op %(%): ON DELETE CASCADE hersteld', r.name, r.tbl, r.col;
END LOOP;
END $$;

View file

@ -1,28 +0,0 @@
-- v0.4.1-beta: een klasnaam mag niet dubbel voorkomen binnen dezelfde school.
-- Zonder deze constraint kon "Nieuwe klas" herhaaldelijk dezelfde naam
-- aanmaken (bv. na een eerdere poging die door een andere bug leek te
-- mislukken, maar stiekem toch al aanmaakte) - de app-laag controleert dit nu
-- ook (POST /admin/classes), maar de database-constraint is het echte vangnet.
--
-- Bestaande dubbele klasnamen eerst uniek maken: anders faalt de CREATE
-- UNIQUE INDEX hieronder op een database die deze bug al heeft laten
-- ontstaan (en dan start de app helemaal niet meer op). Er wordt niets
-- verwijderd, alleen de latere dubbele naam hernoemd - de klas, z'n
-- leerlingen en koppelingen blijven ongemoeid.
DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN
SELECT id, name,
ROW_NUMBER() OVER (PARTITION BY school_id, lower(name) ORDER BY id) AS rn
FROM classes
LOOP
IF r.rn > 1 THEN
UPDATE classes SET name = r.name || ' (' || r.rn || ')' WHERE id = r.id;
END IF;
END LOOP;
END $$;
CREATE UNIQUE INDEX IF NOT EXISTS idx_classes_school_name
ON classes (school_id, lower(name));

View file

@ -1,44 +0,0 @@
-- v0.4.07-beta: schoolbeheerder (en systeemmanager) konden niet aan een klas
-- gekoppeld worden, met een verwarrende "groepsleiding en klas moeten bij
-- dezelfde school horen" - ook al staat dit in de applicatielaag (POST
-- /admin/classes/:id/teachers, src/api.js) al langer voor elke stafrol open.
--
-- Oorzaak: de trigger teach_validate_class_teacher() (uit 006) is destijds
-- geschreven toen alleen groepsleiding aan een klas gekoppeld kon worden, en
-- is nooit meegegroeid toen schoolbeheerder/systeemmanager daar later bij
-- kwamen - de trigger stond dus in de weg van iets wat de app allang toestond.
CREATE OR REPLACE FUNCTION teach_validate_class_teacher()
RETURNS trigger LANGUAGE plpgsql AS $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM classes c
JOIN users u ON u.id = NEW.user_id
WHERE c.id = NEW.class_id
AND (
-- systeemmanager is school-onafhankelijk, mag overal aan gekoppeld worden
u.role = 'super'
OR (
u.school_id = c.school_id
AND (
u.role IN ('teacher', 'admin')
OR EXISTS (
SELECT 1 FROM user_roles ur
WHERE ur.user_id = u.id AND ur.role IN ('teacher', 'admin')
)
)
)
)
) THEN
RAISE EXCEPTION 'alleen staf uit dezelfde school (of een systeemmanager) kan aan een klas gekoppeld worden';
END IF;
RETURN NEW;
END;
$$;
-- trigger zelf blijft ongewijzigd (verwijst naar de herdefinieerde functie),
-- alleen opnieuw aangemaakt zodat deze migratie op zichzelf leesbaar is
DROP TRIGGER IF EXISTS trg_class_teachers_tenant ON class_teachers;
CREATE TRIGGER trg_class_teachers_tenant
BEFORE INSERT OR UPDATE ON class_teachers
FOR EACH ROW EXECUTE FUNCTION teach_validate_class_teacher();

View file

@ -19,29 +19,10 @@ services:
environment:
PORT: 3000
DATABASE_URL: ${DATABASE_URL}
# APP_VERSION komt uit de image zelf (build-arg); hier niet overschrijven.
SUPER_USER: ${SUPER_USER:-beheerder}
SUPER_PASS: ${SUPER_PASS:-}
# Pangolin/Traefik -> nginx -> app: twee vertrouwde proxy-hops.
TRUST_PROXY_HOPS: ${TRUST_PROXY_HOPS:-2}
COOKIE_SECURE: "true"
APP_VERSION: ${IMAGE}
ports:
# Alleen op localhost van de VM; nginx zit ervoor als reverse proxy
- "127.0.0.1:${APP_PORT:-3000}:3000"
volumes:
- image_data:/app/storage/images
restart: unless-stopped
web:
image: nginx:alpine
depends_on:
- app
ports:
# Bereikbaar voor Pangolin/Traefik via het VM-/containernetwerk.
# Beperk poort 8081 met de hostfirewall tot de proxy/tunnel.
- "${WEB_BIND_IP:-0.0.0.0}:${WEB_PORT:-8081}:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
restart: unless-stopped
db:
@ -62,4 +43,3 @@ services:
volumes:
pgdata:
image_data:

View file

@ -1,33 +0,0 @@
# Behoud het publieke schema van Pangolin/Traefik; val lokaal terug op nginx.
map $http_x_forwarded_proto $teach_forwarded_proto {
default $http_x_forwarded_proto;
"" $scheme;
}
server {
listen 80;
server_name _;
server_tokens off;
# Wat groter zodat grote borden/afbeeldingen niet geweigerd worden
client_max_body_size 55m;
# Gzip-vangnet: de app comprimeert zelf al (brotli/gzip via @fastify/compress)
# en nginx comprimeert nooit dubbel (responses mét Content-Encoding blijven
# ongemoeid) - dit vangt alleen wat de app mist. Let op: zonder
# gzip_proxied comprimeert nginx geproxyde responses helemaal niet.
gzip on;
gzip_proxied any;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/css application/javascript application/json image/svg+xml text/plain;
location / {
proxy_pass http://app:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $teach_forwarded_proto;
}
}

639
package-lock.json generated
View file

@ -8,11 +8,7 @@
"name": "teach",
"version": "0.1.0",
"dependencies": {
"@fastify/compress": "^8.3.1",
"@fastify/cookie": "^11.1.1",
"@fastify/helmet": "^13.1.0",
"@fastify/rate-limit": "^11.1.0",
"@fastify/static": "^10.1.0",
"@fastify/static": "^8.0.1",
"fastify": "^5.1.0",
"pg": "^8.13.1"
},
@ -57,81 +53,6 @@
"fast-uri": "^3.0.0"
}
},
"node_modules/@fastify/compress": {
"version": "8.3.1",
"resolved": "https://registry.npmjs.org/@fastify/compress/-/compress-8.3.1.tgz",
"integrity": "sha512-BUpItLr6MUX9e9ukg5Y6xekyA/7pBFG8QWtFCrUDm9ctoBc3R2/nA16yOaOWtVoccpXGjdDEYA/MxAb5+8cxag==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"@fastify/accept-negotiator": "^2.0.0",
"fastify-plugin": "^5.0.0",
"mime-db": "^1.52.0",
"minipass": "^7.0.4",
"peek-stream": "^1.1.3",
"pump": "^3.0.0",
"pumpify": "^2.0.1",
"readable-stream": "^4.5.2"
}
},
"node_modules/@fastify/compress/node_modules/fastify-plugin": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz",
"integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT"
},
"node_modules/@fastify/cookie": {
"version": "11.1.1",
"resolved": "https://registry.npmjs.org/@fastify/cookie/-/cookie-11.1.1.tgz",
"integrity": "sha512-sJ0NXzGVYjUB4OynPZRsIcQ1mKSP4rW45xLCN0aelRq5Vl37xVVbz5kJ6Y0a9m2T0mCUjYCuvlUA9QlTafrZWw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"cookie": "^2.0.0",
"fastify-plugin": "^6.0.0"
}
},
"node_modules/@fastify/cookie/node_modules/cookie": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz",
"integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==",
"license": "MIT",
"engines": {
"node": ">=22"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/@fastify/error": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
@ -183,26 +104,6 @@
],
"license": "MIT"
},
"node_modules/@fastify/helmet": {
"version": "13.1.0",
"resolved": "https://registry.npmjs.org/@fastify/helmet/-/helmet-13.1.0.tgz",
"integrity": "sha512-SvVOU0IrzYJW1BvSkfq9G1WUdW3dnaRUvg6m0BtgGMBmML62No0VmSu087jecH58SFbicbREgZTPJ89mAguupA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"fastify-plugin": "^6.0.0",
"helmet": "^8.0.0"
}
},
"node_modules/@fastify/merge-json-schemas": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz",
@ -242,27 +143,6 @@
"ipaddr.js": "^2.1.0"
}
},
"node_modules/@fastify/rate-limit": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-11.1.0.tgz",
"integrity": "sha512-BeJ9tizLvmTXGD7deYU5G04OtHhwk5uHxbpEPVp09gKvUBIXmau/4Bshxhu9ci54MvVWfGjCEx4RzvsTntojwA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"license": "MIT",
"dependencies": {
"@lukeed/ms": "^2.0.2",
"fastify-plugin": "^6.0.0",
"toad-cache": "^3.7.0"
}
},
"node_modules/@fastify/send": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.0.tgz",
@ -287,9 +167,9 @@
}
},
"node_modules/@fastify/static": {
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-10.1.0.tgz",
"integrity": "sha512-iK/8TvRM/EgNOyQL+EpWu+x3aR6o4GWt+UI+27zmE7w6t/6d80mXqOtWLdEKQ13vL/g1Jry0ae2icj6GP7tGzA==",
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@fastify/static/-/static-8.3.0.tgz",
"integrity": "sha512-yKxviR5PH1OKNnisIzZKmgZSus0r2OZb8qCSbqmw34aolT4g3UlzYfeBRym+HJ1J471CR8e2ldNub4PubD1coA==",
"funding": [
{
"type": "github",
@ -303,12 +183,20 @@
"license": "MIT",
"dependencies": {
"@fastify/accept-negotiator": "^2.0.0",
"@fastify/error": "^4.0.0",
"@fastify/send": "^4.0.0",
"content-disposition": "^2.0.1",
"fastify-plugin": "^6.0.0",
"content-disposition": "^0.5.4",
"fastify-plugin": "^5.0.0",
"fastq": "^1.17.1",
"glob": "^13.0.0"
"glob": "^11.0.0"
}
},
"node_modules/@isaacs/cliui": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz",
"integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==",
"license": "BlueOak-1.0.0",
"engines": {
"node": ">=18"
}
},
"node_modules/@lukeed/ms": {
@ -326,18 +214,6 @@
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==",
"license": "MIT"
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
"license": "MIT",
"dependencies": {
"event-target-shim": "^5.0.0"
},
"engines": {
"node": ">=6.5"
}
},
"node_modules/abstract-logging": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
@ -415,26 +291,6 @@
"node": "18 || 20 || >=22"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz",
@ -447,47 +303,16 @@
"node": "18 || 20 || >=22"
}
},
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"node_modules/content-disposition": {
"version": "0.5.4",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"license": "MIT"
},
"node_modules/content-disposition": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz",
"integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==",
"license": "MIT",
"engines": {
"node": ">=18"
"safe-buffer": "5.2.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
"engines": {
"node": ">= 0.6"
}
},
"node_modules/cookie": {
@ -503,11 +328,19 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/depd": {
"version": "2.0.0",
@ -527,81 +360,12 @@
"node": ">=6"
}
},
"node_modules/duplexify": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
"integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.0.0",
"inherits": "^2.0.1",
"readable-stream": "^2.0.0",
"stream-shift": "^1.0.0"
}
},
"node_modules/duplexify/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/duplexify/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/duplexify/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
"integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
"license": "MIT",
"dependencies": {
"once": "^1.4.0"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/event-target-shim": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/fast-decode-uri-component": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
@ -697,9 +461,9 @@
}
},
"node_modules/fastify-plugin": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.0.tgz",
"integrity": "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==",
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz",
"integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==",
"funding": [
{
"type": "github",
@ -735,33 +499,44 @@
"node": ">=20"
}
},
"node_modules/glob": {
"version": "13.0.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
"integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
"license": "BlueOak-1.0.0",
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"license": "ISC",
"dependencies": {
"minimatch": "^10.2.2",
"minipass": "^7.1.3",
"path-scurry": "^2.0.2"
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
},
"engines": {
"node": "18 || 20 || >=22"
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/helmet": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/helmet/-/helmet-8.3.0.tgz",
"integrity": "sha512-Qgpiaws3Sm30Av8Eah6sjMCZZwjlBu+E68rhpCWBshY1lb09HtLwj5GviX0OyQIn+ulUS0iX0AxN5n3tLZzz1w==",
"license": "MIT",
"node_modules/glob": {
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"license": "BlueOak-1.0.0",
"dependencies": {
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
"minimatch": "^10.1.1",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": ">=18.0.0"
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/EvanHahn"
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/http-errors": {
@ -784,26 +559,6 @@
"url": "https://opencollective.com/express"
}
},
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "BSD-3-Clause"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@ -819,11 +574,26 @@
"node": ">= 10"
}
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"license": "ISC"
},
"node_modules/jackspeak": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz",
"integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/cliui": "^9.0.0"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/json-schema-ref-resolver": {
"version": "3.0.0",
@ -888,9 +658,9 @@
"license": "MIT"
},
"node_modules/lru-cache": {
"version": "11.5.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
"integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
"version": "11.5.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz",
"integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==",
"license": "BlueOak-1.0.0",
"engines": {
"node": "20 || >=22"
@ -908,15 +678,6 @@
"node": ">=10.0.0"
}
},
"node_modules/mime-db": {
"version": "1.54.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "10.2.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
@ -950,13 +711,19 @@
"node": ">=14.0.0"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"license": "ISC",
"dependencies": {
"wrappy": "1"
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0"
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-scurry": {
@ -975,17 +742,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/peek-stream": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/peek-stream/-/peek-stream-1.1.3.tgz",
"integrity": "sha512-FhJ+YbOSBb9/rIl2ZeE/QHEsWn7PqNYt8ARAY3kIgNGOk13g9FGyIY6JIl/xB/3TFRVoTv5as0l11weORrTekA==",
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"duplexify": "^3.5.0",
"through2": "^2.0.3"
}
},
"node_modules/pg": {
"version": "8.22.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz",
@ -1151,21 +907,6 @@
"node": ">=0.10.0"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/process-warning": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
@ -1182,75 +923,12 @@
],
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
"integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.1.0",
"once": "^1.3.1"
}
},
"node_modules/pumpify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/pumpify/-/pumpify-2.0.1.tgz",
"integrity": "sha512-m7KOje7jZxrmutanlkS1daj1dS6z6BgslzOXmcSEpIlCxM3VJH7lG5QLeck/6hgF6F4crFf01UtQmNsJfweTAw==",
"license": "MIT",
"dependencies": {
"duplexify": "^4.1.1",
"inherits": "^2.0.3",
"pump": "^3.0.0"
}
},
"node_modules/pumpify/node_modules/duplexify": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
"integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==",
"license": "MIT",
"dependencies": {
"end-of-stream": "^1.4.1",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1",
"stream-shift": "^1.0.2"
}
},
"node_modules/pumpify/node_modules/readable-stream": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
"integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
"integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
"license": "MIT"
},
"node_modules/readable-stream": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/real-require": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
@ -1385,6 +1063,39 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/sonic-boom": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz",
@ -1412,21 +1123,6 @@
"node": ">= 0.8"
}
},
"node_modules/stream-shift": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz",
"integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/thread-stream": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz",
@ -1445,46 +1141,6 @@
"integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==",
"license": "MIT"
},
"node_modules/through2": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz",
"integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==",
"license": "MIT",
"dependencies": {
"readable-stream": "~2.3.6",
"xtend": "~4.0.1"
}
},
"node_modules/through2/node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/through2/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/through2/node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.0"
}
},
"node_modules/toad-cache": {
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz",
@ -1503,17 +1159,20 @@
"node": ">=0.6"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"license": "MIT"
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC"
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/xtend": {
"version": "4.0.2",

View file

@ -9,15 +9,10 @@
},
"scripts": {
"start": "node src/server.js",
"dev": "node --watch src/server.js",
"test": "node --test"
"dev": "node --watch src/server.js"
},
"dependencies": {
"@fastify/compress": "^8.3.1",
"@fastify/cookie": "^11.1.1",
"@fastify/helmet": "^13.1.0",
"@fastify/rate-limit": "^11.1.0",
"@fastify/static": "^10.1.0",
"@fastify/static": "^8.0.1",
"fastify": "^5.1.0",
"pg": "^8.13.1"
}

View file

@ -1,44 +0,0 @@
.image-catalog-modal{width:min(1080px,96vw);max-height:94vh}
/* titelbalk met ✕ blijft altijd in beeld, ook na scrollen in een lange grid */
.image-catalog-modal .gallery-head{position:sticky;top:-24px;z-index:4;background:var(--surface);padding:4px 0 8px;margin-top:-4px}
.ic-tools{display:grid;grid-template-columns:minmax(180px,1.5fr) repeat(3,minmax(130px,.8fr)) auto;gap:8px;position:sticky;top:12px;z-index:3;background:var(--surface);padding:8px 0 10px}
.ic-tools input,.ic-tools select,.ic-add select,.ic-add input,.ic-editor input,.ic-editor select{min-width:0;width:100%;border:1px solid var(--line);border-radius:10px;padding:9px 10px;background:var(--surface);color:var(--ink);font:inherit}
.ic-word-themes{display:flex;align-items:center;gap:7px;overflow-x:auto;padding:9px 10px;margin:0 0 10px;border:1px solid var(--line);border-radius:13px;background:linear-gradient(135deg,var(--surface-2),var(--accent-soft));scrollbar-width:thin}
.ic-word-themes strong{flex:none;margin-right:3px;font-size:12px;color:var(--muted)}
.ic-word-themes button{flex:none;border:1px solid var(--line);border-radius:999px;padding:7px 11px;background:var(--surface);color:var(--ink);font:inherit;font-size:12px;font-weight:850;cursor:pointer;transition:transform .12s,border-color .12s,background .12s}
.ic-word-themes button:hover{transform:translateY(-1px);border-color:var(--accent)}
.ic-word-themes button.active{border-color:var(--accent);background:var(--accent);color:#fff;box-shadow:var(--shadow-1)}
.ic-quota{padding:8px 12px;margin-bottom:10px;border-radius:10px;background:var(--accent-soft);color:var(--accent-ink);font-size:13px;font-weight:800}
.ic-add{border:1px solid var(--line);border-radius:12px;padding:10px 12px;margin-bottom:10px;background:var(--surface-2)}
.ic-add summary{cursor:pointer;font-weight:900}
.ic-add-grid,.ic-editor-grid{display:grid;grid-template-columns:1fr 1fr 1.2fr;gap:10px;margin-top:10px;align-items:end}
.ic-field{display:flex;flex-direction:column;gap:4px;font-size:12px;font-weight:800;color:var(--muted)}
.ic-field select[multiple]{min-height:88px}
.ic-upload{display:flex;align-items:center;justify-content:center;min-height:44px;margin:0;cursor:pointer;text-align:center;color:var(--ink)}
.ic-theme-new{display:flex;gap:8px;margin-top:10px}.ic-theme-new input{flex:1}
.ic-status{min-height:20px;font-size:13px;font-weight:800;color:var(--red);padding:2px 0}.ic-status.ok{color:var(--green)}
.ic-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(155px,1fr));gap:10px}
.ic-card{position:relative;min-width:0;border:1px solid var(--line);border-radius:14px;background:var(--surface);overflow:hidden;transition:border-color .15s,box-shadow .15s}
.ic-card:hover{border-color:var(--accent);box-shadow:var(--shadow-1)}
.ic-choose{display:grid;grid-template-rows:120px auto auto;width:100%;height:100%;gap:5px;padding:10px;border:0;background:transparent;color:var(--ink);font:inherit;text-align:left;cursor:pointer}
.ic-choose img{width:100%;height:120px;object-fit:contain;border-radius:9px;background:#fff}
.ic-choose strong{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.ic-choose span{font-size:11px;color:var(--muted);min-height:28px;overflow:hidden}
.ic-card-edit{position:absolute;right:7px;top:7px;width:32px;height:32px;border:1px solid var(--line);border-radius:9px;background:var(--surface);cursor:pointer;box-shadow:var(--shadow-1)}
.ic-editor{position:sticky;bottom:-24px;z-index:4;margin-top:12px;padding:14px;border:2px solid var(--accent);border-radius:14px;background:var(--surface);box-shadow:var(--shadow-2)}
.ic-editor h3{margin:0}.ic-editor-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:10px}
.ic-explorer-thumb{width:64px;height:56px;object-fit:contain;border-radius:8px;background:#fff}
.img-quota-admin{display:flex;flex-direction:column;gap:7px}.img-quota-row{display:grid;grid-template-columns:minmax(180px,1fr) 130px 130px auto;align-items:center}
.img-quota-users{display:flex;flex-direction:column;gap:6px;margin-top:8px}
body.hc .ic-card,body.hc .ic-add,body.hc .ic-tools input,body.hc .ic-tools select,body.hc .ic-editor{border-color:#000}
@media(max-width:760px){
/* dvh i.p.v. vh: met een dynamische browserbalk (telefoon) is 100vh hoger
dan het zichtbare scherm, waardoor titel en sluitknop wegvielen */
.image-catalog-modal{width:100vw;max-height:100dvh;height:100dvh;border-radius:0;padding:16px}
.image-catalog-modal .gallery-head{top:-16px}
.ic-tools{top:20px;grid-template-columns:1fr 1fr;padding-top:4px}.ic-search,.ic-folders{grid-column:1/-1}
.ic-word-themes{margin-inline:-2px;padding:8px}.ic-word-themes strong{position:sticky;left:0;z-index:1;padding:7px 6px;background:var(--surface-2)}
.ic-add-grid,.ic-editor-grid{grid-template-columns:1fr}.ic-grid{grid-template-columns:repeat(2,minmax(0,1fr))}
.ic-choose{grid-template-rows:96px auto auto}.ic-choose img{height:96px}
.img-quota-row{grid-template-columns:1fr 1fr}.img-quota-row .am-name{grid-column:1/-1}
}
@media(max-width:420px){.ic-grid{grid-template-columns:1fr}.ic-editor-actions{flex-wrap:wrap}.ic-editor-actions .tbtn{flex:1}}

File diff suppressed because it is too large Load diff

View file

@ -1,4 +0,0 @@
Quicksand - ontworpen door Andrew Paglinawan e.a.
Gelicenseerd onder de SIL Open Font License 1.1 (https://openfontlicense.org).
Bron: Google Fonts (https://fonts.google.com/specimen/Quicksand).
Dit bestand (quicksand.woff2) is het latin-subset van de variabele font (gewichten 300-700).

Binary file not shown.

View file

@ -1,48 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 104" role="img" aria-label="teachus.today uil">
<!-- laag-poly uil: vlakke facetten in het merk-palet, transparante achtergrond -->
<!-- oren -->
<polygon points="33,28 40,10 48,22" fill="#1d3461"/>
<polygon points="62,22 70,10 77,28" fill="#1d3461"/>
<!-- voorhoofd -->
<polygon points="40,10 70,10 62,22 48,22" fill="#2456b8"/>
<!-- gezicht -->
<polygon points="33,28 48,22 44,38" fill="#2b4d94"/>
<polygon points="77,28 62,22 66,38" fill="#2b4d94"/>
<polygon points="48,22 62,22 66,38 44,38" fill="#24407a"/>
<!-- wangen -->
<polygon points="33,28 44,38 36,46" fill="#2f6fe4"/>
<polygon points="77,28 66,38 74,46" fill="#2f6fe4"/>
<!-- ogen -->
<circle cx="48.5" cy="30.5" r="4.6" fill="#cfe0f5"/>
<circle cx="61.5" cy="30.5" r="4.6" fill="#cfe0f5"/>
<circle cx="48.5" cy="30.5" r="2.3" fill="#101d3a"/>
<circle cx="61.5" cy="30.5" r="2.3" fill="#101d3a"/>
<!-- snavel -->
<polygon points="55,36 58.5,41 55,46 51.5,41" fill="#f28c38"/>
<!-- linkervleugel: cyaan/teal -->
<polygon points="36,46 44,38 44,56" fill="#3ac2e8"/>
<polygon points="36,46 44,56 32,62" fill="#18c5c0"/>
<polygon points="32,62 44,56 40,74" fill="#66d4ef"/>
<polygon points="32,62 40,74 30,78" fill="#12a8a4"/>
<polygon points="30,78 40,74 38,88" fill="#18c5c0"/>
<!-- borst: roze/oranje -->
<polygon points="44,38 55,46 44,56" fill="#e83e8c"/>
<polygon points="55,46 66,38 66,56" fill="#f28c38"/>
<polygon points="44,56 55,46 58,64" fill="#c2257f"/>
<polygon points="55,46 66,56 58,64" fill="#f06ba8"/>
<polygon points="44,56 58,64 48,78" fill="#f5a55c"/>
<polygon points="58,64 66,56 68,74" fill="#e83e8c"/>
<polygon points="48,78 58,64 68,74" fill="#8b5cf6"/>
<polygon points="48,78 68,74 58,86" fill="#f28c38"/>
<!-- rechterflank: blauw/navy -->
<polygon points="66,38 74,46 66,56" fill="#2f6fe4"/>
<polygon points="66,56 74,46 76,66" fill="#1d3461"/>
<polygon points="66,56 76,66 68,74" fill="#2456b8"/>
<polygon points="68,74 76,66 70,84" fill="#1d3461"/>
<!-- tak -->
<polygon points="26,92 106,85 106,88.4 26,95.4" fill="#1d3461"/>
<!-- blaadjes -->
<polygon points="92,70 99,78 92,84 85,78" fill="#18c5c0"/>
<polygon points="104,77 110,83 104,88 98,83" fill="#3ac2e8"/>
<polygon points="96,90 102,95 96,100 90,95" fill="#12a8a4"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,8 +0,0 @@
# Bronnen euro-afbeeldingen
Deze afbeeldingen worden lokaal meegeleverd, zodat de geldwidget ook zonder internetverbinding werkt.
- **Munten (1, 2, 5, 10, 20 en 50 cent, €1 en €2):** actuele gemeenschappelijke zijden van de Europese Commissie, gedownload van [Common sides of euro coins](https://economy-finance.ec.europa.eu/euro/euro-coins-and-notes/euro-coins/common-sides-euro-coins_en) op 15 juli 2026. De tweedimensionale afbeeldingen zijn eigendom van de Europese Unie en worden hier educatief gebruikt volgens de reproductievoorwaarden van de Europese Commissie.
- **Bankbiljetten (€5, €10, €20 en €50):** voorzijde van de Europa-serie met Lagarde-handtekening uit het officiële [ECB-pakket met 72-dpi SPECIMEN-afbeeldingen](https://www.ecb.europa.eu/euro/shared/img/euro_banknotes_specimen_72dpi.zip), gedownload op 15 juli 2026. De zichtbare SPECIMEN-markering en originele 72-dpi-bestanden worden behouden volgens de [ECB-reproductieregels](https://www.ecb.europa.eu/euro/banknotes/images/html/index.en.html).
De bestandsnamen gebruiken centen als waarde, bijvoorbeeld coin-200.png voor €2 en note-500.jpg voor €5.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 456 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 543 KiB

View file

@ -1,115 +0,0 @@
# Wereldoriëntatie beeldbronnen
De lokale WebP-bestanden zijn uitsluitend verkleinde en, waar nodig, gekaderde
afgeleiden van de hieronder genoemde foto's. De soortnamen zijn gecontroleerd
aan de hand van de bestandsbeschrijvingen op Wikimedia Commons.
## Bomen
| Bestand | Bronwerk | Maker | Licentie |
| --- | --- | --- | --- |
| `trees/oak.webp` | [1. English Oak (Quercus robur)](https://commons.wikimedia.org/wiki/File:1._English_Oak_(Quercus_robur)_(3607652442).jpg) | AnemoneProjectors | [CC BY-SA 2.0](https://creativecommons.org/licenses/by-sa/2.0/) |
| `trees/birch.webp` | [Betula pendula winter](https://commons.wikimedia.org/wiki/File:Betula_pendula_winter.jpg) | Maplan | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `trees/horse-chestnut.webp` | [Paardenkastanje2](https://commons.wikimedia.org/wiki/File:Paardenkastanje2.jpg) | Toubib | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `trees/pine.webp` | [Pinus sylvestris Glenmuick](https://commons.wikimedia.org/wiki/File:Pinus_sylvestris_Glenmuick.jpg) | Hello, I am Bruce | [CC BY-SA 2.0](https://creativecommons.org/licenses/by-sa/2.0/) |
| `trees/beech.webp` | [Fagales Fagus sylvatica](https://commons.wikimedia.org/wiki/File:Fagales_-_Fagus_sylvatica.jpg) | Emőke Dénes | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `trees/maple.webp` | [Acer-pseudoplatanus](https://commons.wikimedia.org/wiki/File:Acer-pseudoplatanus.jpg) | MPF | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `trees/willow.webp` | [Salix alba an der Isar](https://commons.wikimedia.org/wiki/File:Salix_alba_an_der_Isar.jpg) | Burkhard Mücke | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `trees/alder.webp` | [Alnus glutinosa 011](https://commons.wikimedia.org/wiki/File:Alnus_glutinosa_011.jpg) | Willow | [CC BY 2.5](https://creativecommons.org/licenses/by/2.5/) |
| `trees/lime.webp` | [Winterlinde (Wiki Loves Earth 2015)](https://commons.wikimedia.org/wiki/File:012_2015_05_06_Winterlinde_(Wiki_Loves_Earth_2015).jpg) | Friedrich Haag | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `trees/ash.webp` | [Fraxinus excelsior 001](https://commons.wikimedia.org/wiki/File:Fraxinus_excelsior_001.jpg) | Willow | [CC BY 2.5](https://creativecommons.org/licenses/by/2.5/) |
| `trees/sweet-chestnut.webp` | [Anatolian sweet chestnut](https://commons.wikimedia.org/wiki/File:Castanea_sativa_-_Anatolian_sweet_chestnut_04.jpg) | Zeynel Cebeci | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `trees/rowan.webp` | [Rowan tree 20081002b](https://commons.wikimedia.org/wiki/File:Rowan_tree_20081002b.jpg) | Eeno11 | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `trees/hawthorn.webp` | [Crataegus monogyna in Joncret](https://commons.wikimedia.org/wiki/File:Crataegus_monogyna_in_Joncret_AR1aJPG.jpg) | Jean-Pol GRANDMONT | [CC BY 2.5](https://creativecommons.org/licenses/by/2.5/) |
| `trees/aspen.webp` | [Populus tremula 004](https://commons.wikimedia.org/wiki/File:Populus_tremula_004.jpg) | Willow | [CC BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/) |
| `trees/spruce.webp` | [Kuusk Keila-Paldiski rdt ääres](https://commons.wikimedia.org/wiki/File:Kuusk_Keila-Paldiski_rdt_%C3%A4%C3%A4res.jpg) | Ivar Leidus | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `trees/larch.webp` | [European larch Larix decidua](https://commons.wikimedia.org/wiki/File:European_larch_-_Larix_decidua.jpg) | MrPanyGoff | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `trees/yew.webp` | [Witchs Yew Tree](https://commons.wikimedia.org/wiki/File:2020_Witch%E2%80%99s_Yew_Tree_IRL.jpg) | Blarney Castle Estate | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `trees/elm.webp` | [Ulmus minor tree](https://commons.wikimedia.org/wiki/File:Ulmus_minor_tree.jpg) | Ptelea | [Publiek domein](https://commons.wikimedia.org/wiki/Commons:Copyright_tags#Public_domain) |
## Bladeren en naalden
| Bestand | Bronwerk | Maker | Licentie |
| --- | --- | --- | --- |
| `leaves/oak.webp` | [Zomereik blad Quercus robur](https://commons.wikimedia.org/wiki/File:Zomereik_blad_Quercus_robur.jpg) | Rasbak | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `leaves/birch.webp` | [Atlas roslin pl Brzoza brodawkowata 1393 6470](https://commons.wikimedia.org/wiki/File:Atlas_roslin_pl_Brzoza_brodawkowata_1393_6470.jpg) | Joanna Boisse | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `leaves/horse-chestnut.webp` | [Aesculus hippocastanum in botanical garden of UKW Bydgoszcz (2)](https://commons.wikimedia.org/wiki/File:Aesculus_hippocastanum_in_botanical_garden_of_UKW_Bydgoszcz_(2).jpg) | Tournasol7 | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| `leaves/pine.webp` | [20151215Pinus sylvestris2](https://commons.wikimedia.org/wiki/File:20151215Pinus_sylvestris2.jpg) | AnRo0002 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
| `leaves/beech.webp` | [20130530Hain Rotbuche](https://commons.wikimedia.org/wiki/File:20130530Hain_Rotbuche.jpg) | AnRo0002 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
| `leaves/maple.webp` | [Acer pseudoplatanus 003](https://commons.wikimedia.org/wiki/File:Acer_pseudoplatanus_003.jpg) | Willow | [CC BY 2.5](https://creativecommons.org/licenses/by/2.5/) |
| `leaves/willow.webp` | [20160721Salix alba1](https://commons.wikimedia.org/wiki/File:20160721Salix_alba1.jpg) | AnRo0002 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
| `leaves/alder.webp` | [20200531Alnus glutinosa2](https://commons.wikimedia.org/wiki/File:20200531Alnus_glutinosa2.jpg) | AnRo0002 | [CC0 1.0](https://creativecommons.org/publicdomain/zero/1.0/) |
| `leaves/lime.webp` | [Herbarium Tilia cordata img-007](https://commons.wikimedia.org/wiki/File:2020_year._Herbarium._Tilia_cordata._img-007.jpg) | Dmitry Makeev | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `leaves/ash.webp` | [Herbarium Fraxinus img-015](https://commons.wikimedia.org/wiki/File:2020_year._Herbarium._Fraxinus._img-015.jpg) | Dmitry Makeev | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `leaves/sweet-chestnut.webp` | [Castanea sativa leaves and immature fruit](https://commons.wikimedia.org/wiki/File:Castanea_sativa_leaves_and_immature_fruit.jpg) | Ekir | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `leaves/rowan.webp` | [Herbarium Sorbus aucuparia img-005](https://commons.wikimedia.org/wiki/File:2020_year._Herbarium._Sorbus_aucuparia._img-005.jpg) | Dmitry Makeev | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `leaves/hawthorn.webp` | [Crataegus monogyna leaves and stipules](https://commons.wikimedia.org/wiki/File:(MHNT)_Crataegus_monogyna_-_Leaves_and_stipules.jpg) | Didier Descouens | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `leaves/aspen.webp` | [Herbarium Populus tremula img-017](https://commons.wikimedia.org/wiki/File:2020_year._Herbarium._Populus_tremula._img-017.jpg) | Dmitry Makeev | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `leaves/spruce.webp` | [Picea abies needles in Väsmestorp](https://commons.wikimedia.org/wiki/File:Picea_abies_needles_in_V%C3%A4smestorp_2024-10-29_(0261).jpg) | Achird | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `leaves/larch.webp` | [Larix decidua needles and male cones](https://commons.wikimedia.org/wiki/File:Larix_decidua_needles_and_male_cones.JPG) | Rosser1954 / Roger Griffith | [Publiek domein](https://commons.wikimedia.org/wiki/Commons:Copyright_tags#Public_domain) |
| `leaves/yew.webp` | [Yew berries](https://commons.wikimedia.org/wiki/File:Yew_berries_(50578142398).jpg) | hedera.baltica | [CC BY-SA 2.0](https://creativecommons.org/licenses/by-sa/2.0/) |
| `leaves/elm.webp` | [Elm leaves (Ulmus minor)](https://commons.wikimedia.org/wiki/File:Elm_leaves_(Ulmus_minor).jpg) | Daniel Capilla | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
## Vogels
| Bestand | Bronwerk | Maker | Licentie |
| --- | --- | --- | --- |
| `birds/robin.webp` | [Erithacus rubecula, Roodborstje](https://commons.wikimedia.org/wiki/File:Erithacus_rubecula,_Roodborstje.jpg) | Rasbak | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `birds/great-tit.webp` | [Parus major m](https://commons.wikimedia.org/wiki/File:Parus_major_m.jpg) | Sławek Staszczuk | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `birds/blackbird.webp` | [Solsort](https://commons.wikimedia.org/wiki/File:Solsort.jpg) | Malene Thyssen | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `birds/mallard.webp` | [Duisburg 10401IMG 0401](https://commons.wikimedia.org/wiki/File:Duisburg_10401IMG_0401.JPG) | Mbdortmund | [CC BY 3.0](https://creativecommons.org/licenses/by/3.0/) |
| `birds/blue-tit.webp` | [Cyanistes caeruleus 3 Luc Viatour](https://commons.wikimedia.org/wiki/File:Cyanistes_caeruleus_3_Luc_Viatour.jpg) | Luc Viatour | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `birds/house-sparrow.webp` | [Huismus, man](https://commons.wikimedia.org/wiki/File:Huismus,_man.jpg) | JrPol | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `birds/magpie.webp` | [Сорока (Pica pica), Калининград](https://commons.wikimedia.org/wiki/File:%D0%A1%D0%BE%D1%80%D0%BE%D0%BA%D0%B0_(Pica_pica),_%D0%9A%D0%B0%D0%BB%D0%B8%D0%BD%D0%B8%D0%BD%D0%B3%D1%80%D0%B0%D0%B4.jpg) | Dion Art | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `birds/woodpecker.webp` | [Grote bonte specht](https://commons.wikimedia.org/wiki/File:Grote_bonte_specht.JPG) | Usitea | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `birds/chaffinch.webp` | [Fringilla coelebs chaffinch male](https://commons.wikimedia.org/wiki/File:Fringilla_coelebs_chaffinch_male_edit2.jpg) | MichaelMaggs / Arad | [CC BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/) |
| `birds/starling.webp` | [Sturnus vulgaris England standing](https://commons.wikimedia.org/wiki/File:Sturnus_vulgaris_-England_-standing-8.jpg) | Mark Robinson | [CC BY 2.0](https://creativecommons.org/licenses/by/2.0/) |
| `birds/grey-heron.webp` | [Grey heron 2022 04 03 03](https://commons.wikimedia.org/wiki/File:Grey_heron_2022_04_03_03.jpg) | Alexis Lours | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| `birds/mute-swan.webp` | [Wild Mute swan at Lake Geneva](https://commons.wikimedia.org/wiki/File:093_Wild_Mute_swan_at_Lake_Geneva_during_sunset_Photo_by_Giles_Laurent.jpg) | Giles Laurent | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `birds/coot.webp` | [Fulica atra Common coot](https://commons.wikimedia.org/wiki/File:Fulica_atra_-_Common_coot,_Adana_2016-11-12_01-1.jpg) | Zeynel Cebeci | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `birds/greylag-goose.webp` | [Graugans Anser Anser](https://commons.wikimedia.org/wiki/File:Graugans_Anser_Anser.jpg) | H005 / Pro2 | [Publiek domein](https://commons.wikimedia.org/wiki/Commons:Copyright_tags#Public_domain) |
| `birds/collared-dove.webp` | [Eurasian Collared Dove](https://commons.wikimedia.org/wiki/File:Streptopelia_decaocto_-_Eurasian_Collared_Dove_05.jpg) | Zeynel Cebeci | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `birds/wren.webp` | [Eurasian wren](https://commons.wikimedia.org/wiki/File:Eurasian_wren_(Troglodytes_troglodytes).jpg) | Alexis Lours | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| `birds/buzzard.webp` | [Buteo buteo juv Log](https://commons.wikimedia.org/wiki/File:Buteo_buteo_juv_Log_(2).jpg) | Yerpo | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `birds/barn-owl.webp` | [Tyto alba 1 Luc Viatour](https://commons.wikimedia.org/wiki/File:Tyto_alba_1_Luc_Viatour.jpg) | Luc Viatour | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
## Wilde planten
| Bestand | Bronwerk | Maker | Licentie |
| --- | --- | --- | --- |
| `plants/dandelion.webp` | [Paardenbloem (Taraxacum officinale)](https://commons.wikimedia.org/wiki/File:Paardenbloem_(Taraxacum_officinale)_06.JPG) | Dominicus Johannes Bergsma | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `plants/nettle.webp` | [Brandnetel, Urtica dioica](https://commons.wikimedia.org/wiki/File:Brandnetel,_Urtica_dioica,_Locatie,_Famberhorst.jpg) | Dominicus Johannes Bergsma | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `plants/daisy.webp` | [Bellis perennis full plant](https://commons.wikimedia.org/wiki/File:Bellis_perennis_full_plant.jpg) | Grand-Duc | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `plants/red-clover.webp` | [Trifolium pratense](https://commons.wikimedia.org/wiki/File:Trifolium_pratense_RF.jpg) | Robert Flogaus-Faust | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| `plants/white-deadnettle.webp` | [Lamium album](https://commons.wikimedia.org/wiki/File:Lamium_album_RF.jpg) | Robert Flogaus-Faust | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
| `plants/cow-parsley.webp` | [Fluitenkruid (Anthriscus sylvestris)](https://commons.wikimedia.org/wiki/File:Fluitenkruid_(Anthriscus_sylvestris)_26-04-2020._(actm.)_02.jpg) | Agnes Monkelbaan | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `plants/yarrow.webp` | [Achillea millefolium](https://commons.wikimedia.org/wiki/File:Achillea_millefolium_(52354).jpg) | Rhododendrites | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `plants/ribwort-plantain.webp` | [Plantago lanceolata kz09](https://commons.wikimedia.org/wiki/File:Plantago_lanceolata_kz09.jpg) | Krzysztof Ziarnek, Kenraiz | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `plants/common-poppy.webp` | [Papaver rhoeas LC0050](https://commons.wikimedia.org/wiki/File:Papaver_rhoeas_LC0050.jpg) | Jörg Hempel | [CC BY-SA 2.5](https://creativecommons.org/licenses/by-sa/2.5/) |
| `plants/chicory.webp` | [Cichorium intybus](https://commons.wikimedia.org/wiki/File:Cichorium_intybus_TK_2021-07-12_1.jpg) | T. Kebert | [CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/) |
| `plants/foxglove.webp` | [Digitalis purpurea LC0101](https://commons.wikimedia.org/wiki/File:Digitalis_purpurea_LC0101.jpg) | Jörg Hempel | [CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/) |
| `plants/common-sorrel.webp` | [Rumex acetosa in Aveyron](https://commons.wikimedia.org/wiki/File:Rumex_acetosa_in_Aveyron_(7).jpg) | Tournasol7 | [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/) |
## Topografie
`netherlands-provinces.svg` is een vereenvoudigde afgeleide zonder labels van
[Netherlands provinces map blank.svg](https://commons.wikimedia.org/wiki/File:Netherlands_provinces_map_blank.svg)
door Gust Justice, gelicentieerd onder
[CC BY-SA 4.0](https://creativecommons.org/licenses/by-sa/4.0/). De afgeleide
provinciecontouren worden onder dezelfde licentie verspreid.
De plaatsing van stadspunten is daarnaast gecontroleerd met
[Map provinces Netherlands-en.svg](https://commons.wikimedia.org/wiki/File:Map_provinces_Netherlands-en.svg)
door Alphathon ([CC BY-SA 3.0](https://creativecommons.org/licenses/by-sa/3.0/)).
`world-continents.svg` is gegenereerd uit de fysieke
[Natural Earth 1:110m Geography Regions Polygons](https://www.naturalearthdata.com/downloads/110m-physical-vectors/110m-physical-labels/)
en, voor de eilanden van Oceanië,
[Admin 0 Countries](https://www.naturalearthdata.com/downloads/110m-cultural-vectors/110m-admin-0-countries/).
Natural Earth stelt deze geografische brondata beschikbaar als publiek domein.
De lokale afgeleide bevat zeven afzonderlijk bedienbare werelddeelcontouren.
De vier Nederlandse landsdelen volgen de
[CBS-indeling voor Gebieden in Nederland 2026](https://www.cbs.nl/nl-nl/cijfers/detail/86247NED).
De selectie en loop van de grote rivieren zijn gecontroleerd aan de hand van het
[Rijkswaterstaat-overzicht Rivieren](https://www.rijkswaterstaat.nl/water/waterbeheer/beheer-en-ontwikkeling-rijkswateren/rivieren).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 153 KiB

Some files were not shown because too many files have changed in this diff Show more