Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 49 additions & 14 deletions validatie_samenwijzer/app/pages/9_beheer.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
import os
import shlex
import subprocess
import time
from collections import deque
from collections.abc import Iterator
from pathlib import Path

Expand All @@ -25,9 +27,10 @@
init_db,
laatste_ingest_run,
)
from validatie_samenwijzer.styles import CSS, render_footer # noqa: E402
from validatie_samenwijzer.styles import CSS, render_footer, render_nav # noqa: E402

st.markdown(CSS, unsafe_allow_html=True)
render_nav()

# ── Toegangscontrole ───────────────────────────────────────────────────────────
if os.environ.get("BEHEER_ENABLED", "").lower() != "true":
Expand All @@ -42,8 +45,12 @@
_INSTELLING_KEYS = ["aeres", "davinci", "rijn_ijssel", "talland", "utrecht"]


def _stream_lines(cmd: list[str], cwd: Path) -> Iterator[str]:
"""Run cmd en yield stdout-regels live."""
_BUFFER_REGELS = 300
_PAINT_INTERVAL_S = 0.2


def _stream_lines(cmd: list[str], cwd: Path) -> Iterator[tuple[str, int | None]]:
"""Run cmd en yield (regel, returncode); returncode is None tot het proces eindigt."""
proc = subprocess.Popen( # noqa: S603 — input is uit hardcoded keuzes, niet user-text
cmd,
cwd=str(cwd),
Expand All @@ -52,21 +59,38 @@ def _stream_lines(cmd: list[str], cwd: Path) -> Iterator[str]:
text=True,
bufsize=1,
)
assert proc.stdout is not None
yield from iter(proc.stdout.readline, "")
if proc.stdout is None:
raise RuntimeError("Popen leverde geen stdout op — kan output niet streamen.")
for regel in iter(proc.stdout.readline, ""):
yield regel, None
proc.wait()
yield f"\n[exit={proc.returncode}]\n"
yield f"\n[exit={proc.returncode}]\n", proc.returncode


def _run_in_placeholder(cmd: list[str], cwd: Path | None = None) -> None:
"""Toon live output van cmd in een code-block met scrollende tail."""
cwd = cwd or _PROJECT_ROOT
st.caption(f"$ {shlex.join(cmd)} (cwd={cwd})")
placeholder = st.empty()
buffer: list[str] = []
for regel in _stream_lines(cmd, cwd):
# deque voorkomt onbegrensde groei bij langlopende subprocesses (bv. een 30-min
# bootstrap kan tienduizenden regels rclone-progress produceren).
buffer: deque[str] = deque(maxlen=_BUFFER_REGELS)
laatste_paint = 0.0
returncode: int | None = None
for regel, rc in _stream_lines(cmd, cwd):
buffer.append(regel)
placeholder.code("".join(buffer[-300:]), language="bash")
if rc is not None:
returncode = rc
# Throttle: bij snel-producerende subprocessen zou per-regel re-rendering de
# Streamlit-WebSocket overbelasten en de browser laten haperen.
nu = time.monotonic()
if nu - laatste_paint > _PAINT_INTERVAL_S or rc is not None:
placeholder.code("".join(buffer), language="bash")
laatste_paint = nu
if returncode == 0:
st.success(f"✅ Klaar (exit=0): `{shlex.join(cmd)}`")
else:
st.error(f"❌ Subprocess gefaald (exit={returncode}): `{shlex.join(cmd)}`")


# ── Pagina-header ──────────────────────────────────────────────────────────────
Expand Down Expand Up @@ -107,9 +131,16 @@ def _run_in_placeholder(cmd: list[str], cwd: Path | None = None) -> None:
).fetchall()
if rijen:
st.dataframe(
[{"Instelling": r["display_naam"], "Totaal OERs": r["totaal"],
"Geïndexeerd": r["geindexeerd"]} for r in rijen],
hide_index=True, use_container_width=False,
[
{
"Instelling": r["display_naam"],
"Totaal OERs": r["totaal"],
"Geïndexeerd": r["geindexeerd"],
}
for r in rijen
],
hide_index=True,
use_container_width=False,
)
else:
st.info("Geen OERs in DB. Draai eerst een ingest.")
Expand All @@ -129,8 +160,12 @@ def _run_in_placeholder(cmd: list[str], cwd: Path | None = None) -> None:
if not oeren_pad.is_absolute():
oeren_pad = (_PROJECT_ROOT / oeren_pad).resolve()
if oeren_pad.exists():
n_pdf = sum(1 for _ in oeren_pad.rglob("*.pdf"))
n_md = sum(1 for _ in oeren_pad.rglob("*.md"))
n_pdf = n_md = 0
for pad in oeren_pad.rglob("*"):
if pad.suffix == ".pdf":
n_pdf += 1
elif pad.suffix == ".md":
n_md += 1
st.write(f"📁 `{oeren_pad}` — {n_pdf} PDFs, {n_md} markdown-bestanden")
else:
st.warning(f"Map `{oeren_pad}` niet gevonden — draai eerst sync.")
Expand Down
21 changes: 14 additions & 7 deletions validatie_samenwijzer/src/validatie_samenwijzer/ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
import re
import sqlite3
import time
from pathlib import Path

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -36,7 +37,14 @@
# Generieke woorden die niet als opleidingsnaam tellen — alleen op deze
# overhouden geldt de stem als oninformatief.
_GENERIEKE_OPLEIDINGSWOORDEN = {
"examenplan", "examenplannen", "oer", "addendum", "bol", "bbl", "mbo", "roc",
"examenplan",
"examenplannen",
"oer",
"addendum",
"bol",
"bbl",
"mbo",
"roc",
}


Expand All @@ -45,8 +53,7 @@ def _stem_heeft_opleidingsnaam(stem: str) -> bool:
rest = _PREFIX_PATROON.sub("", stem).lower()
tokens = [w for w in re.split(r"[_\W]+", rest) if w]
return any(
len(w) >= 3 and not w.isdigit() and w not in _GENERIEKE_OPLEIDINGSWOORDEN
for w in tokens
len(w) >= 3 and not w.isdigit() and w not in _GENERIEKE_OPLEIDINGSWOORDEN for w in tokens
)


Expand Down Expand Up @@ -355,8 +362,10 @@ def _resolveer_oer(
# Werk de opleidingsnaam bij als de nieuwe variant informatiever is
# dan wat eerder is opgeslagen (bv. PDF-titelpagina vs. generieke
# filename als "Examenplan - 25698").
if opleiding != oer["opleiding"] and _stem_heeft_opleidingsnaam(opleiding) and not (
_stem_heeft_opleidingsnaam(oer["opleiding"])
if (
opleiding != oer["opleiding"]
and _stem_heeft_opleidingsnaam(opleiding)
and not (_stem_heeft_opleidingsnaam(oer["opleiding"]))
):
log.info("Opleiding bijgewerkt: '%s' → '%s'.", oer["opleiding"], opleiding)
update_oer_opleiding(conn, oer_id, opleiding)
Expand Down Expand Up @@ -466,8 +475,6 @@ def main() -> None:
for naam, display in _INSTELLINGEN.items():
voeg_instelling_toe(conn, naam, display)

import time

start = time.monotonic()
scope: str | None = None
if args.bestand:
Expand Down
Loading