5 Commits
Author SHA1 Message Date
Florian WildeandClaude Opus 5.5 3009c53b5c Fassung 1.1.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:58:32 +02:00
Florian WildeandClaude Opus 5.5 44e6c8bb79 Module abschaltbar machen
Merkkarten, Praxisaufgaben, Checklisten, Berichtsheft, Lernzielkontrollen
und Feedbackgespraech lassen sich unter Verwaltung -> Module fuer die ganze
Lern-Bar ab- und einschalten. Wochenplan, Handouts, Glossar, Auswertung und
Verwaltung bleiben immer an.

Gesperrt wird an drei Stellen, nicht im Layout (Next rendert Layouts beim
Seitenwechsel darunter nicht neu): Seiten mit erfordereModul, Routen fuer
PDF, Anhaenge und Lernstand mit modulAn, alle 37 Serveraktionen der Module
mit pruefeModul. Die Anmeldepruefung kommt jeweils zuerst, damit ein
Unangemeldeter nicht am 404 abliest, was abgeschaltet ist.

Ein abgeschaltetes Modul traegt nirgends bei: Wochenplan, Fortschritt,
Was-wartet, Mein Stand, Ergebnisverlauf, Rahmenplanabdeckung, Vorschlaege
fuers Berichtsheft, Karten im Handout, Kurzbefehle im Manifest. Die
Lernstand-Schnittstelle antwortet mit 410, das Geraet verwirft daraufhin
seine Offline-Warteschlange. Abschalten loescht nichts.

Gespeichert in modul_einstellung (Migration 0010); fehlt die Zeile, ist das
Modul an, eine bestehende Datenbank verhaelt sich also wie vorher.

12 Tests fuer die Regeln, Abschnitt A8 im Abnahmelauf, Anleitung Kapitel 9
mit Bildschirmfoto, README, E43.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 16:58:32 +02:00
Florian WildeandClaude Opus 5.5 54ce7d87ae Fassung 1.0.5
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:33:31 +02:00
Florian WildeandClaude Opus 5.5 76836197b5 Anleitung fuer den Betrieb auf Proxmox
Von der leeren VM ueber Docker, Caddy mit HTTPS, Deploy-Schluessel,
erstes Konto und zweistufige Sicherung bis zur Abnahme vor dem
Echtbetrieb. Die Caddy-Konfiguration ist mit caddy validate geprueft.

Die README weist jetzt darauf hin, dass HTTPS Pflicht ist: Das
Anmeldecookie ist im Betrieb Secure, ueber http:// scheitert die
Anmeldung ohne Fehlermeldung.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:33:31 +02:00
Florian WildeandClaude Opus 5.5 70a3cd2d28 Inhalte: Wochen 13 bis 16
- Woche 13 Linux-Grundlagen (B3, A6, LF9)
- Woche 14 Betrieb und Ueberwachung (A8, A5, LF11)
- Woche 15 Serviceanfragen bearbeiten (A1, A2, A5, LF6)
- Woche 16 Vorbereitung auf Teil 1 (C3, C4, A10, LF2, LF5)

12 Handouts, 36 Merkkarten, 4 Praxisaufgaben, 3 Checklisten,
2 Lernzielkontrollen und eine Probepruefung Teil 1 (90 Minuten,
100 Punkte, eine durchgehende Fallsituation). Alle Rechenbeispiele
nachgerechnet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 06:33:31 +02:00
82 changed files with 3079 additions and 162 deletions
+66
View File
@@ -26,6 +26,72 @@ Vor `npm version` wandert der Abschnitt „Unveröffentlicht" unter eine neue
## [Unveröffentlicht] ## [Unveröffentlicht]
## [1.1.0] – 2026-10-05
Module lassen sich abschalten.
### Hinzugefügt
- **Verwaltung → Module.** Merkkarten, Praxisaufgaben, Checklisten,
Berichtsheft, Lernzielkontrollen und Feedbackgespräch lassen sich für die
ganze Lern-Bar ab- und wieder einschalten, sofort und ohne Neustart. Zu jedem
Modul steht dort, was es tut, was sich beim Abschalten ändert und wer es
zuletzt umgeschaltet hat. Wochenplan, Handouts, Glossar, Auswertung und
Verwaltung bleiben immer an
- Ein abgeschaltetes Modul ist **überall weg**: Navigation, Wochenplan und
Wochenfortschritt (eine Woche erreicht ohne das Modul trotzdem 100 %), „Was
auf dich wartet", „Mein Stand", Ergebnisverlauf, Rahmenplanabdeckung,
Vorschläge fürs Berichtsheft, Merkkarten im Handout, Kurzbefehle im
App-Symbol
- Ein abgeschaltetes Modul ist **gesperrt, nicht nur ausgeblendet**: Seiten,
PDF-Ausgaben, Anhänge und alle 37 Serveraktionen der Module prüfen den
Schalter. Die Lernstand-Schnittstelle antwortet mit 410, und Geräte verwerfen
daraufhin offline gesammelte Bewertungen
- **Abschalten löscht nichts.** Nach dem Einschalten ist alles unverändert da
- Die Verwaltung hat zwei Bereiche: Konten und Module
- Migration `0010_module` – eine bestehende Datenbank hat danach alle Module an
und verhält sich wie vorher
- 12 Tests für die Modulregeln, Abschnitt A8 im Abnahmelauf (abschalten,
Sperre für beide Rollen prüfen, einschalten, Daten noch da)
- Anleitung Kapitel 9 „Module ein- und ausschalten" mit Bildschirmfoto,
README-Abschnitt, Entscheidung E43
### Geändert
- Das App-Manifest wird bei jedem Abruf erzeugt statt einmal beim Bau, damit
die Kurzbefehle zum Modulstand passen
- In der Anleitung haben sich die Kapitel ab 9 um eins verschoben
## [1.0.5] – 2026-10-05
Das zweite Vierteljahr beginnt, mit Blick auf Teil 1 der Abschlussprüfung.
Dazu eine Anleitung für den Betrieb auf Proxmox.
### Hinzugefügt
- **Woche 13 · Linux-Grundlagen** – Handouts S6 (Dateisystem und Befehle), S7
(Benutzer, Gruppen, Dateirechte), S8 (Pakete, systemd, SSH), Praxisaufgabe
13.1 (Linux-Server einrichten und absichern), Checkliste „Linux-Server
grundlegend einrichten", Lernzielkontrolle
- **Woche 14 · Betrieb und Überwachung** – Handouts S9 (Überwachung und
Schwellwerte), S10 (Protokolle und Zeitsynchronisation), S11
(Patch-Management), Praxisaufgabe 14.1 (Überwachung für drei Systeme)
- **Woche 15 · Serviceanfragen bearbeiten** – Handouts A10 (Störung,
Serviceanfrage, Problem, Änderung), A11 (Priorisieren und eskalieren), A12
(Wissensartikel), Praxisaufgabe 15.1 (eine Woche im ersten Level), Checkliste
„Änderung planen und durchführen", Lernzielkontrolle
- **Woche 16 · Vorbereitung auf Teil 1** – Handouts H4 (Ergonomie), H5 (Energie
rechnen, nachhaltig beschaffen und entsorgen), P4 (Pseudocode, Struktogramm,
Schreibtischtest), Praxisaufgabe 16.1 (Arbeitsplätze bewerten), Checkliste
„Bildschirmarbeitsplatz prüfen" und eine **Probeprüfung Teil 1** – 90
Minuten, 100 Punkte, durchgehende Fallsituation
- 18 Glossareinträge, 36 Merkkarten
- **`docs/BETRIEB-PROXMOX.md`** – von der leeren VM über Docker, Caddy mit
HTTPS, erstes Konto und Sicherung bis zur Abnahme vor dem Echtbetrieb
- Die README weist darauf hin, dass HTTPS Pflicht ist: Über `http://` nimmt der
Browser das Anmeldecookie nicht an, und die Anmeldung scheitert ohne
Fehlermeldung
## [1.0.4] – 2026-10-04 ## [1.0.4] – 2026-10-04
Vier weitere Wochen Lehrgang – damit steht das erste Vierteljahr. Vier weitere Wochen Lehrgang – damit steht das erste Vierteljahr.
+38
View File
@@ -493,3 +493,41 @@ oder eine erfolgreiche Anmeldung setzt den Zähler zurück.
- **Gezählt wird vor der Prüfung.** Jeder Versuch gilt sofort als Fehlversuch - **Gezählt wird vor der Prüfung.** Jeder Versuch gilt sofort als Fehlversuch
und wird bei Erfolg zurückgenommen. Zwischen Sperrprüfung und Zählen liegt und wird bei Erfolg zurückgenommen. Zwischen Sperrprüfung und Zählen liegt
so kein `await`; sonst kämen gleichzeitige Anfragen alle an der Sperre vorbei. so kein `await`; sonst kämen gleichzeitige Anfragen alle an der Sperre vorbei.
## E43 · Module schaltbar für die ganze Lern-Bar, gesperrt an drei Stellen, nie gelöscht
Merkkarten, Praxisaufgaben, Checklisten, Berichtsheft, Lernzielkontrollen und
Feedbackgespräch lassen sich unter **Verwaltung → Module** abschalten. Wochenplan,
Handouts, Glossar, Auswertung und Verwaltung bleiben immer an – an ihnen hängt
alles andere, und die Auswertung passt sich ohnehin an.
- **Für die ganze Lern-Bar, nicht je Azubi.** Ein Betrieb startet mit einer
Fachrichtung und entscheidet einmal, womit er arbeitet. Schalter je Azubi
hätten jede Ausbilderansicht je Person unterschiedlich gemacht. Ließe sich
später ergänzen, ohne das Bestehende umzubauen: Der Stand ist schon heute ein
Objekt, das an einer Stelle entsteht (`repos/module`).
- **In der Datenbank, nicht in einer Konfigurationsdatei.** Ein Ausbilder soll
ohne Zugang zum Server umschalten können, und die Änderung soll sofort
wirken, ohne Neustart. Gespeichert wird nur, was je umgeschaltet wurde; fehlt
die Zeile, ist das Modul an. Eine bestehende Datenbank verhält sich nach der
Migration also genau wie vorher.
- **Gesperrt an drei Stellen, nicht im Layout.** Seiten rufen `erfordereModul`
auf (Antwort „nicht gefunden", wie bei fehlender Berechtigung, E21), Routen
für PDF, Anhänge und Lernstand fragen `modulAn`, Serveraktionen rufen
`pruefeModul`. Ein Layout je Modul wäre kürzer gewesen, greift aber nicht
zuverlässig: Next.js rendert ein Layout beim Wechsel zwischen den Seiten
darunter nicht neu.
- **Abschalten löscht nichts.** Beim Berichtsheft ist das zwingend – es ist ein
Nachweis. Für alle anderen Module gilt dasselbe, damit niemand beim
Ausprobieren Daten verliert.
- **Abgeschaltet heißt: trägt nirgends bei.** Kein Eintrag im Wochenplan, kein
Anteil am Wochenfortschritt, kein Nachweis in der Rahmenplanabdeckung, kein
Vorschlag fürs Berichtsheft, kein Kurzbefehl im App-Symbol. Sonst stünden
Inhalte für immer als offen da, die niemand mehr bearbeiten kann.
- **Offline gesammelte Bewertungen werden verworfen**, wenn die Merkkarten aus
sind. Die Lernstand-Schnittstelle antwortet mit 410 statt 404, und das Gerät
leert daraufhin seine Warteschlange, statt sie bei jeder Verbindung erneut zu
schicken.
- **Die Prüfung der Anmeldung kommt zuerst**, die des Moduls danach. Sonst sähe
ein nicht angemeldeter Besucher an „nicht gefunden" statt der Anmeldeseite,
welche Module abgeschaltet sind.
+24 -3
View File
@@ -12,6 +12,7 @@ Lernplattform-Baukasten, keine Mandanten, keine Cloud.
> **Stand:** Alle zehn Bausteine sind fertig – Fassung 1.0.0. > **Stand:** Alle zehn Bausteine sind fertig – Fassung 1.0.0.
> >
> - [**Anleitung mit Bildschirmfotos**](docs/ANLEITUNG.md) – wie man damit arbeitet > - [**Anleitung mit Bildschirmfotos**](docs/ANLEITUNG.md) – wie man damit arbeitet
> - [**Betrieb auf Proxmox**](docs/BETRIEB-PROXMOX.md) – von der leeren VM bis zum Echtbetrieb
> - [Abnahmeprotokoll](docs/ABNAHME.md) – die Abnahmekriterien, einzeln durchgespielt > - [Abnahmeprotokoll](docs/ABNAHME.md) – die Abnahmekriterien, einzeln durchgespielt
> - [Änderungsprotokoll](CHANGELOG.md) · [Fahrplan](ROADMAP.md) · > - [Änderungsprotokoll](CHANGELOG.md) · [Fahrplan](ROADMAP.md) ·
> [Entscheidungen](ENTSCHEIDUNGEN.md) > [Entscheidungen](ENTSCHEIDUNGEN.md)
@@ -25,7 +26,9 @@ Lernplattform-Baukasten, keine Mandanten, keine Cloud.
- **Betrieb:** Docker mit Compose-Erweiterung (Docker 24 oder neuer) - **Betrieb:** Docker mit Compose-Erweiterung (Docker 24 oder neuer)
- **Entwicklung:** Node.js 22 oder neuer, Git - **Entwicklung:** Node.js 22 oder neuer, Git
- Ein Reverse Proxy, der TLS beendet (nginx, Caddy, Traefik) – die Anwendung - Ein Reverse Proxy, der TLS beendet (nginx, Caddy, Traefik) – die Anwendung
selbst spricht nur HTTP und lauscht nur auf der Schleife selbst spricht nur HTTP und lauscht nur auf der Schleife. **HTTPS ist Pflicht:**
Das Anmeldecookie ist im Betrieb als `Secure` gesetzt, über `http://` scheitert
die Anmeldung ohne Fehlermeldung
### Betrieb mit Docker ### Betrieb mit Docker
@@ -141,6 +144,21 @@ npm run inhalte:pruefen
--- ---
## Module ein- und ausschalten
Unter **Verwaltung → Module** lassen sich Merkkarten, Praxisaufgaben,
Checklisten, Berichtsheft, Lernzielkontrollen und Feedbackgespräch abschalten –
für die ganze Lern-Bar, sofort und ohne Neustart. Wochenplan, Handouts, Glossar,
Auswertung und Verwaltung bleiben immer an.
Ein abgeschaltetes Modul verschwindet aus Navigation, Wochenplan, Fortschritt,
Auswertung und Rahmenplanabdeckung, und seine Seiten, PDFs und Serveraktionen
sind gesperrt. **Gelöscht wird nichts:** Nach dem Einschalten ist alles wieder
da. Einzelheiten in der [Anleitung](docs/ANLEITUNG.md#9-module-ein--und-ausschalten),
die Begründung in [E43](ENTSCHEIDUNGEN.md).
---
## Datensicherung ## Datensicherung
Gesichert werden **Datenbank und Anhänge**. Die Inhalte stecken im Repository Gesichert werden **Datenbank und Anhänge**. Die Inhalte stecken im Repository
@@ -241,7 +259,7 @@ Protokollzeilen.
```bash ```bash
npm run typecheck # TypeScript npm run typecheck # TypeScript
npm run lint # ESLint npm run lint # ESLint
npm run test # Vitest – Logik, die wehtut (315 Tests) npm run test # Vitest – Logik, die wehtut (341 Tests)
npm run abnahme # der Abnahmelauf, siehe unten npm run abnahme # der Abnahmelauf, siehe unten
npm run abnahme:container # derselbe Weg gegen das Docker-Abbild npm run abnahme:container # derselbe Weg gegen das Docker-Abbild
``` ```
@@ -260,7 +278,10 @@ Datenbank** und spielt den ganzen Weg durch:
4. Der Azubi liest ein Handout, lernt eine Karte, meldet eine Aufgabe zur Abnahme 4. Der Azubi liest ein Handout, lernt eine Karte, meldet eine Aufgabe zur Abnahme
5. Der Ausbilder nimmt sie ab; sie schlägt in der Rahmenplanabdeckung durch 5. Der Ausbilder nimmt sie ab; sie schlägt in der Rahmenplanabdeckung durch
6. Das Wochenblatt wird angelegt und als PDF ausgegeben 6. Das Wochenblatt wird angelegt und als PDF ausgegeben
7. **Gesichert, absichtlich beschädigt, zurückgespielt.** Eine Sicherung, die 7. Berichtsheft und Merkkarten werden über die Verwaltung abgeschaltet: Seite
und PDF sind für beide Rollen gesperrt, nach dem Einschalten ist das
Wochenblatt unverändert da
8. **Gesichert, absichtlich beschädigt, zurückgespielt.** Eine Sicherung, die
nie zurückgespielt wurde, ist keine Sicherung nie zurückgespielt wurde, ist keine Sicherung
Das Protokoll landet in [`docs/ABNAHME.md`](docs/ABNAHME.md). Das Protokoll landet in [`docs/ABNAHME.md`](docs/ABNAHME.md).
+11
View File
@@ -117,6 +117,17 @@ Legende: `[ ]` offen · `[~]` in Arbeit · `[x]` fertig
- [x] Abnahmekriterien einzeln durchgespielt und protokolliert - [x] Abnahmekriterien einzeln durchgespielt und protokolliert
(`docs/ABNAHME.md`) (`docs/ABNAHME.md`)
## 1.1 – Module
- [x] **Module abschaltbar** unter Verwaltung → Module
- [x] Merkkarten, Praxisaufgaben, Checklisten, Berichtsheft,
Lernzielkontrollen, Feedbackgespräch; das Gerüst bleibt immer an
- [x] Gesperrt in Seiten, Routen und Serveraktionen, nicht nur ausgeblendet
- [x] Wochenplan, Fortschritt, Auswertung, Rahmenplanabdeckung, Vorschläge
und App-Kurzbefehle richten sich nach dem Stand
- [x] Abschalten löscht nichts
- [x] Im Abnahmelauf geprüft (A8)
--- ---
## Danach – bewusst zurückgestellt ## Danach – bewusst zurückgestellt
@@ -0,0 +1,49 @@
---
id: "13.1"
titel: Einen Linux-Server grundlegend einrichten und absichern
woche: 13
block: system
ziel: Du richtest auf dem Testhost einen Linux-Server ein, sicherst den Fernzugang ab, stellst einen Webserver als Dienst bereit und weist nach, dass nur erreichbar ist, was erreichbar sein soll.
zeitrahmen: 6 Stunden
rahmenplan: [B3, A6, B1, A5]
lernfeld: LF9
material:
- Testhost aus Praxisaufgabe 9.1 und ein Installationsabbild von Debian oder Ubuntu Server
- Ein eigener Rechner mit SSH-Client
- Checkliste „Linux-Server grundlegend einrichten"
kriterien:
- Die VM ist nach Handout V3 geplant und dokumentiert
- Es gibt einen persönlichen Benutzer mit sudo-Recht, gearbeitet wird nicht als root
- Die Anmeldung per SSH geht nur mit Schlüssel, root kann sich nicht direkt anmelden – nachgewiesen durch einen fehlschlagenden Versuch
- Automatische Sicherheitsupdates sind eingerichtet
- Die Firewall lässt nur SSH und HTTP zu, ss -tlnp bestätigt, dass nichts Unerwartetes lauscht
- nginx läuft als Dienst, startet nach einem Neustart von selbst und liefert eine eigene Testseite aus
- Ein absichtlich herbeigeführter Fehler in der nginx-Konfiguration wurde mit journalctl gefunden und behoben
---
## Auftrag
1. **Planen und installieren:** VM nach Handout V3 planen, Debian oder Ubuntu Server
installieren, nur das Grundsystem und den SSH-Server auswählen.
2. **Benutzer:** persönlichen Benutzer anlegen und in die Gruppe `sudo` aufnehmen.
3. **Schlüssel:** Schlüsselpaar auf deinem Rechner erzeugen, öffentlichen Schlüssel
hinterlegen, Anmeldung testen.
4. **SSH absichern:** root-Anmeldung und Passwort-Anmeldung abschalten. Zweite Sitzung offen
lassen und testen, bevor du die erste schließt.
5. **Aktualisieren:** alle Updates einspielen, automatische Sicherheitsupdates einrichten.
6. **Firewall:** nur SSH und HTTP freigeben, mit `ss -tlnp` gegenprüfen.
7. **Webserver:** nginx installieren, eine eigene Testseite unter `/var/www/html` ablegen,
Neustart des Servers, prüfen, ob die Seite wieder da ist.
8. **Fehlersuche üben:** Baue einen Tippfehler in die nginx-Konfiguration ein, starte nginx neu,
finde die Ursache mit `systemctl status` und `journalctl`, behebe sie. Halte die Meldung fest.
## Worauf es ankommt
Der Nachweis, dass die root-Anmeldung und die Anmeldung per Passwort **scheitern**, gehört
dazu. Eine Einstellung in der Konfigurationsdatei ist erst dann wirksam, wenn der Dienst sie
geladen hat – und das zeigt nur der Versuch.
## Zeit
Sechs Stunden. Wenn du dich ausgesperrt hast, kommst du über die Konsole des Hypervisors
wieder hinein – genau dafür gibt es sie.
@@ -0,0 +1,48 @@
---
id: "14.1"
titel: Eine Überwachung für drei Systeme einrichten
woche: 14
block: system
ziel: Du richtest für drei Systeme der Testumgebung eine Überwachung mit begründeten Schwellwerten ein, legst fest, wer bei welchem Alarm was tut, und weist mit einem absichtlich herbeigeführten Ausfall nach, dass der Alarm ankommt.
zeitrahmen: 6 Stunden
rahmenplan: [A8, A5, B3]
lernfeld: LF11
material:
- Zugang zum Überwachungssystem des Betriebs oder eine Test-Installation (z. B. Checkmk, Zabbix oder Uptime Kuma)
- Testserver aus den Praxisaufgaben 9.1 und 13.1 sowie ein Testclient
- Ein Postfach oder Kanal, an den Alarme gehen dürfen
kriterien:
- Für jedes System sind mindestens vier Prüfungen eingerichtet, darunter Erreichbarkeit und Plattenplatz
- Jede Prüfung hat Schwellen für Warnung und kritisch, mit Begründung
- Für jeden kritischen Alarm ist eine Handlung und ein Empfänger festgelegt
- Ein absichtlicher Ausfall (Dienst gestoppt) hat einen Alarm ausgelöst, die Zeit bis zum Eingang ist gemessen
- Nach Behebung meldet die Überwachung die Entwarnung
- Es ist beschrieben, wie auffällt, dass die Überwachung selbst ausgefallen ist
- Die Einrichtung ist dokumentiert
---
## Auftrag
1. **Auswählen:** Nimm den Windows-Testserver, den Linux-Server und einen Testclient.
2. **Prüfungen festlegen:** Was muss je System überwacht werden? Nutze die Übersicht aus Handout
S9 als Ausgangspunkt, aber übernimm sie nicht blind – ein Client braucht anderes als ein
Server.
3. **Schwellen begründen:** Schreib zu jeder Schwelle einen Satz, warum sie dort liegt.
4. **Handlungen festlegen:** Wer bekommt welchen Alarm, und was tut er dann? Ein Alarm ohne
Handlung ist keiner.
5. **Einrichten** im Überwachungssystem.
6. **Testen:** Stoppe einen überwachten Dienst. Miss die Zeit, bis der Alarm ankommt. Starte den
Dienst wieder und prüfe die Entwarnung.
7. **Die Überwachung überwachen:** Beschreibe, woran ihr merkt, dass das Überwachungssystem
selbst ausgefallen ist.
## Worauf es ankommt
Weniger ist mehr. Zwanzig Prüfungen je System sehen gründlich aus und erzeugen nach einer
Woche so viele Meldungen, dass niemand mehr hinschaut. Gesucht sind die Prüfungen, bei denen
ein Alarm tatsächlich bedeutet: Jetzt muss jemand etwas tun.
## Zeit
Sechs Stunden. Vor dem Test kurz ankündigen, dass gleich ein Alarm kommt – sonst sucht jemand
anderes die Ursache.
@@ -0,0 +1,47 @@
---
id: "15.1"
titel: Eine Woche im ersten Level
woche: 15
block: arbeitsalltag
ziel: Du übernimmst unter Aufsicht den ersten Level im Ticketsystem, ordnest jedes Ticket nach Art und Priorität ein, erkennst ein wiederkehrendes Problem und schreibst zwei Wissensartikel.
zeitrahmen: Montag bis Donnerstag, jeweils vormittags
rahmenplan: [A1, A2, A5, A7]
lernfeld: LF6
material:
- Zugang zum Ticketsystem mit Bearbeitungsrecht im ersten Level
- Prioritätenmatrix und Zielzeiten des Betriebs (oder die aus Handout A11)
- Zugang zur Wissensdatenbank
kriterien:
- Mindestens 15 Tickets sind nach Art (Störung, Serviceanfrage) und Priorität eingeordnet, jeweils mit einem Satz Begründung
- Die Einordnungen sind mit dem Ausbilder abgeglichen, Abweichungen besprochen
- Jede Eskalation an den zweiten Level enthält alle Angaben, sodass keine Rückfrage nötig war
- Die Nutzer wurden bei längerer Bearbeitung von sich aus über den Stand informiert
- Mindestens ein wiederkehrendes Muster ist als Problem mit verknüpften Tickets angelegt
- Zwei Wissensartikel nach dem Aufbau aus Handout A12 liegen in der Wissensdatenbank
---
## Auftrag
An vier Vormittagen bist du der erste Ansprechpartner für neue Tickets – der Ausbilder oder ein
Kollege aus dem zweiten Level sitzt daneben oder ist erreichbar.
1. **Annehmen:** Jedes neue Ticket lesen, bei Bedarf beim Melder nachfragen, bis die Angaben
aus Handout A1 vollständig sind.
2. **Einordnen:** Art und Priorität festlegen und in einem Satz begründen.
3. **Lösen oder eskalieren:** Was du lösen kannst, löst du. Was nicht, gibst du vollständig an
den zweiten Level weiter.
4. **Informieren:** Melde dich bei Nutzern, deren Ticket länger dauert, bevor sie nachfragen.
5. **Muster erkennen:** Sammle Tickets, die sich ähneln. Lege für ein Muster ein Problem an.
6. **Aufschreiben:** Für zwei Lösungen, die du in dieser Woche gebraucht hast, schreibst du einen
Wissensartikel.
7. **Abgleichen:** Am Ende jedes Vormittags gehst du deine Einordnungen mit dem Ausbilder durch.
## Worauf es ankommt
Die Begründung der Priorität ist wichtiger als die Priorität selbst. Wer begründen kann, warum
der Bildschirm der Geschäftsführung nach dem Lagerdrucker kommt, hat das Prinzip verstanden – und
kann es dem Geschäftsführer auch erklären.
## Zeit
Vier Vormittage. Nachmittags laufen die übrigen Inhalte der Woche.
@@ -0,0 +1,47 @@
---
id: "16.1"
titel: Drei Arbeitsplätze nach Ergonomie und Energie bewerten
woche: 16
block: hardware
ziel: Du prüfst drei Bildschirmarbeitsplätze auf ergonomische Mängel, misst ihren Stromverbrauch, rechnest die Jahreskosten hoch und legst eine begründete Empfehlung mit Kosten vor.
zeitrahmen: 5 Stunden
rahmenplan: [C3, C4, A3, A2]
lernfeld: LF2
material:
- Checkliste „Bildschirmarbeitsplatz prüfen"
- Eine Energiemessgerät-Steckdose
- Maßband
- Aktueller Strompreis des Betriebs (bei der Buchhaltung erfragen)
kriterien:
- An drei Arbeitsplätzen ist die Checkliste vollständig abgearbeitet, Mängel sind mit Foto oder Maß belegt
- Die Nutzer wurden nach Beschwerden gefragt, ihre Antworten sind festgehalten
- Der Verbrauch ist im Betrieb und im Leerlauf gemessen, nicht aus dem Datenblatt übernommen
- Die Jahreskosten sind nach der Formel aus Handout H5 nachvollziehbar berechnet
- Für jeden Mangel ist eine Maßnahme mit Kosten oder Aufwand vorgeschlagen
- Sofort behebbare Mängel (Bildschirmhöhe, Ausrichtung, Energiesparplan) sind mit Einverständnis der Nutzer behoben
---
## Auftrag
1. **Auswählen:** Drei Arbeitsplätze in verschiedenen Abteilungen, mit dem Einverständnis der
Personen, die dort arbeiten.
2. **Prüfen:** Checkliste abarbeiten, Maße nehmen, Mängel fotografieren.
3. **Fragen:** Haben die Nutzer Beschwerden – Nacken, Augen, Rücken, Handgelenke?
4. **Messen:** Mit der Messsteckdose Rechner und Bildschirme im Betrieb und im Leerlauf messen,
außerdem nach Feierabend, falls sie nicht ausgeschaltet werden.
5. **Rechnen:** Jahreskosten je Arbeitsplatz und hochgerechnet auf alle gleichartigen
Arbeitsplätze im Betrieb.
6. **Beheben, was sofort geht:** Bildschirm einstellen, Arbeitsplatz drehen, Energiesparplan
setzen.
7. **Empfehlen:** Was bleibt, mit Maßnahme, Kosten und Nutzen – auf einer Seite.
## Worauf es ankommt
Die Messung zählt, nicht das Datenblatt. Ein Rechner, der laut Hersteller 15 Watt im Leerlauf
braucht, kann mit einem schlecht eingestellten Energiesparplan das Dreifache aufnehmen. Und:
Die Aufgabe ist eine gute Übung für Teil 1 der Abschlussprüfung, in dem Ergonomie und
Energiekosten regelmäßig vorkommen.
## Zeit
Fünf Stunden. Die Messung nach Feierabend kann über Nacht laufen.
@@ -0,0 +1,60 @@
id: 10-linux-server-einrichten
titel: Linux-Server grundlegend einrichten
block: system
worum: >-
Die Reihenfolge schützt vor dem Aussperren: erst der Benutzer, dann der
Schlüssel, dann testen, dann erst die Passwort-Anmeldung abschalten.
rahmenplan: [B3, A6]
lernfeld: LF9
mit_ticket: true
gruppen:
- id: vorbereiten
titel: Vorbereiten
schritte:
- id: v1
text: Zweck, Name, IP-Adresse und Verantwortlicher festgelegt
- id: v2
text: Konsolenzugang über den Hypervisor geprüft – der Notausgang, falls SSH nicht mehr geht
- id: durchfuehren
titel: Durchführen
schritte:
- id: d1
text: Grundsystem mit SSH-Server installiert, nichts Unnötiges ausgewählt
- id: d2
text: Alle Updates eingespielt, automatische Sicherheitsupdates eingerichtet
- id: d3
text: Persönlicher Benutzer angelegt und in die Gruppe sudo aufgenommen
- id: d4
text: Öffentlicher SSH-Schlüssel hinterlegt, Anmeldung mit Schlüssel getestet
- id: d5
text: In sshd_config root-Anmeldung und Passwort-Anmeldung abgeschaltet, Dienst neu geladen
- id: d6
text: In einer zweiten Sitzung erfolgreich angemeldet, bevor die erste geschlossen wurde
- id: d7
text: Firewall eingerichtet, nur benötigte Dienste freigegeben
- id: d8
text: Zeitzone und Zeitsynchronisation geprüft
- id: pruefen
titel: Prüfen
schritte:
- id: p1
text: Anmeldung als root per SSH schlägt fehl
- id: p2
text: Anmeldung per Passwort schlägt fehl
- id: p3
text: ss -tlnp zeigt nur die erwarteten Dienste
- id: p4
text: Nach einem Neustart laufen alle Dienste wieder
- id: abschliessen
titel: Abschließen
schritte:
- id: a1
text: Server in Sicherung und Überwachung aufgenommen
- id: a2
text: Dokumentation mit Name, Adresse, Diensten und Zugängen abgelegt
- id: a3
text: Ticket geschlossen mit tatsächlichem Aufwand
@@ -0,0 +1,57 @@
id: 11-aenderung-durchfuehren
titel: Änderung planen und durchführen
block: arbeitsalltag
worum: >-
Jede Änderung an einem laufenden System ist ein Risiko. Die Liste sorgt
dafür, dass vorher feststeht, wie man zurückkommt – nicht erst, wenn es
nötig wird.
rahmenplan: [A5, A8, A1]
lernfeld: LF6
mit_ticket: true
gruppen:
- id: vorbereiten
titel: Vorbereiten
schritte:
- id: v1
text: Änderung beschrieben – was, warum, welche Systeme und Nutzer betroffen sind
- id: v2
text: Risiko eingeschätzt und Art der Änderung festgelegt (Standard, normal, Notfall)
- id: v3
text: Rückweg geplant und, wo möglich, vorher getestet
- id: v4
text: Aktuelle Sicherung oder Snapshot der betroffenen Systeme vorhanden
- id: v5
text: Freigabe durch die zuständige Person eingeholt
- id: v6
text: Zeitpunkt festgelegt und den Betroffenen angekündigt
- id: durchfuehren
titel: Durchführen
schritte:
- id: d1
text: Änderung nach Plan umgesetzt, Abweichungen sofort notiert
- id: d2
text: Bei unerwartetem Verhalten nach festgelegter Zeit abgebrochen und Rückweg genutzt
- id: pruefen
titel: Prüfen
schritte:
- id: p1
text: Ziel der Änderung erreicht und nachgewiesen
- id: p2
text: Betroffene Dienste funktionieren, Stichprobe bei einem Nutzer
- id: p3
text: Überwachung zeigt keine neuen Auffälligkeiten
- id: abschliessen
titel: Abschließen
schritte:
- id: a1
text: Dokumentation und Netzplan bzw. Konfigurationsübersicht aktualisiert
- id: a2
text: Snapshots aus der Änderung entfernt
- id: a3
text: Abschluss den Betroffenen mitgeteilt
- id: a4
text: Ticket geschlossen mit Ergebnis, Abweichungen und tatsächlichem Aufwand
@@ -0,0 +1,61 @@
id: 12-bildschirmarbeitsplatz-pruefen
titel: Bildschirmarbeitsplatz prüfen
block: hardware
worum: >-
Gemessen wird an der Person, die dort arbeitet, nicht am Möbelkatalog. Erst
einstellen lassen, dann prüfen – viele Mängel sind nur falsch eingestellte
Möbel.
rahmenplan: [C3, C4]
lernfeld: LF2
mit_ticket: false
gruppen:
- id: vorbereiten
titel: Vorbereiten
schritte:
- id: v1
text: Termin mit der Person vereinbart, die dort arbeitet, und Zweck erklärt
- id: v2
text: Nach Beschwerden gefragt – Nacken, Augen, Rücken, Handgelenke
- id: durchfuehren
titel: Durchführen
schritte:
- id: d1
text: Oberste Bildschirmzeile auf oder knapp unter Augenhöhe
- id: d2
text: Abstand zum Bildschirm etwa eine Armlänge
- id: d3
text: Blickrichtung parallel zum Fenster, keine Spiegelungen oder Blendung
- id: d4
text: Hauptbildschirm direkt vor dem Körper, zwei Bildschirme gleich hoch
- id: d5
text: Füße flach auf dem Boden, Oberschenkel etwa waagerecht
- id: d6
text: Unterarme bei lockeren Schultern etwa waagerecht auf Tischhöhe
- id: d7
text: Tastatur und Maus vor dem Körper, Maus direkt neben der Tastatur
- id: d8
text: Notebook nur mit externem Bildschirm, Tastatur und Maus im Dauerbetrieb
- id: d9
text: Energiesparplan aktiv, Bildschirm schaltet bei Abwesenheit ab
- id: d10
text: Kabel geordnet, keine Stolperfallen, keine Mehrfachsteckdosen hintereinander
- id: pruefen
titel: Prüfen
schritte:
- id: p1
text: Mängel mit Foto oder Maß belegt
- id: p2
text: Mit der Person besprochen, ob die Einstellungen nach der Änderung angenehm sind
- id: abschliessen
titel: Abschließen
schritte:
- id: a1
text: Sofort behebbare Mängel behoben
- id: a2
text: Bleibende Mängel mit Vorschlag an die zuständige Stelle gemeldet
- id: a3
text: Auf die Angebotsvorsorge für die Augen hingewiesen, falls Beschwerden bestehen
+137
View File
@@ -538,3 +538,140 @@
Reaktionszeit, Wiederherstellungszeit und Verfügbarkeit. 99,9 Prozent Reaktionszeit, Wiederherstellungszeit und Verfügbarkeit. 99,9 Prozent
Verfügbarkeit erlauben knapp neun Stunden Ausfall im Jahr. Verfügbarkeit erlauben knapp neun Stunden Ausfall im Jahr.
siehe_auch: [RTO] siehe_auch: [RTO]
# --- Woche 13: Linux-Grundlagen --------------------------------------------
- begriff: SSH
steht_fuer: Secure Shell
erklaerung: >-
Ein verschlüsselter Fernzugang zur Kommandozeile eines Rechners, üblich für
Linux-Server und Netzwerkgeräte. Sicher betrieben wird er mit Schlüsseln statt
Passwörtern.
- begriff: sudo
erklaerung: >-
Ein Befehl, mit dem ein berechtigter Benutzer unter Linux einen einzelnen
Befehl mit Administratorrechten ausführt. Jeder Aufruf wird protokolliert.
- begriff: systemd
formen: [systemctl, journalctl]
erklaerung: >-
Der Dienst, der auf den meisten Linux-Systemen alle anderen Dienste startet und
verwaltet. Mit systemctl steuert man Dienste, mit journalctl liest man ihre
Protokolle.
# --- Woche 14: Betrieb und Überwachung -------------------------------------
- begriff: NTP
steht_fuer: Network Time Protocol
erklaerung: >-
Das Verfahren, mit dem Rechner ihre Uhrzeit über das Netz abgleichen. Ohne
übereinstimmende Uhren lassen sich Protokolle nicht vergleichen, und die
Anmeldung im Active Directory scheitert.
siehe_auch: [Kerberos]
- begriff: Kerberos
erklaerung: >-
Das Anmeldeverfahren im Active Directory. Es arbeitet mit zeitlich begrenzten
Tickets und lässt nur eine kleine Abweichung der Uhren zu, standardmäßig fünf
Minuten.
siehe_auch: [NTP, Active Directory]
- begriff: Patchday
erklaerung: >-
Der feste Termin, an dem Microsoft Sicherheitsupdates veröffentlicht – der
zweite Dienstag jedes Monats. Viele Betriebe richten ihren Update-Takt danach
aus.
siehe_auch: [End of Life]
- begriff: End of Life
formen: [End-of-Life, EOL]
erklaerung: >-
Der Zeitpunkt, ab dem ein Hersteller für ein Produkt keine Sicherheitsupdates
mehr liefert. Neue Lücken bleiben danach offen, das Produkt muss ersetzt werden.
siehe_auch: [Patchday]
# --- Woche 15: Serviceanfragen ---------------------------------------------
- begriff: ITIL
erklaerung: >-
Das verbreitetste Rahmenwerk für IT-Servicemanagement. Es beschreibt unter
anderem, wie Störungen, Serviceanfragen, Probleme und Änderungen
unterschieden und bearbeitet werden.
- begriff: Serviceanfrage
formen: [Serviceanfragen, Service Request]
erklaerung: >-
Die Bitte um eine vorgesehene Standardleistung, ohne dass etwas kaputt ist –
etwa ein neues Konto, eine Software aus dem Katalog oder ein Zugriffsrecht.
siehe_auch: [ITIL, Workaround]
- begriff: Workaround
formen: [Workarounds, Umgehungslösung]
erklaerung: >-
Eine Umgehung, mit der Nutzer weiterarbeiten können, obwohl die eigentliche
Ursache einer Störung noch nicht beseitigt ist.
siehe_auch: [ITIL]
- begriff: Eskalation
formen: [eskalieren, eskaliert]
erklaerung: >-
Die Weitergabe eines Tickets – funktional an eine Stelle mit mehr Fachwissen
oder hierarchisch an die Führungsebene, wenn Fristen oder Entscheidungen es
verlangen.
siehe_auch: [SLA]
# --- Woche 16: Vorbereitung auf Teil 1 -------------------------------------
- begriff: Arbeitsstättenverordnung
steht_fuer: ArbStättV
formen: [ArbStättV]
erklaerung: >-
Die Verordnung, die Anforderungen an Arbeitsstätten festlegt. Ihr Anhang Nr. 6
regelt die Bildschirmarbeitsplätze und hat 2016 die Bildschirmarbeitsverordnung
abgelöst.
siehe_auch: [Gefährdungsbeurteilung]
- begriff: Gefährdungsbeurteilung
formen: [Gefährdungsbeurteilungen]
erklaerung: >-
Die Pflicht des Arbeitgebers nach dem Arbeitsschutzgesetz, die Gefährdungen
an jedem Arbeitsplatz zu ermitteln und Maßnahmen dagegen festzulegen – auch
für Bildschirmarbeit.
siehe_auch: [Arbeitsstättenverordnung]
- begriff: DIN 66399
erklaerung: >-
Die Norm für die Vernichtung von Datenträgern. Sie unterscheidet drei
Schutzklassen und sieben Sicherheitsstufen; für personenbezogene Daten wird in
der Regel mindestens Stufe 3 angesetzt.
siehe_auch: [ElektroG, personenbezogene Daten]
- begriff: ElektroG
steht_fuer: Elektro- und Elektronikgerätegesetz
erklaerung: >-
Das Gesetz, das Rücknahme und Verwertung von Elektrogeräten regelt. Alte
Rechner, Bildschirme und Drucker gehören über Rücknahmestellen oder
zertifizierte Entsorger in die Verwertung, nicht in den Restmüll.
siehe_auch: [DIN 66399]
- begriff: Pseudocode
erklaerung: >-
Die Beschreibung eines Ablaufs in strukturierter Umgangssprache mit festen
Schlüsselwörtern wie WENN, SOLANGE und FÜR. Er ist an keine Programmiersprache
gebunden.
siehe_auch: [Struktogramm, Schreibtischtest]
- begriff: Struktogramm
formen: [Struktogramme, Nassi-Shneiderman-Diagramm]
erklaerung: >-
Die zeichnerische Darstellung eines Ablaufs als ineinander geschachtelte
Kästen für Folge, Verzweigung und Schleife, nach Nassi und Shneiderman.
siehe_auch: [Pseudocode]
- begriff: Schreibtischtest
formen: [Schreibtischtests]
erklaerung: >-
Das Durchspielen eines Algorithmus von Hand mit Beispielwerten, wobei nach jedem
Schritt die Werte aller Variablen in einer Tabelle festgehalten werden.
siehe_auch: [Pseudocode]
@@ -0,0 +1,65 @@
---
id: A10
titel: Störung, Anfrage, Problem, Änderung – vier Arten von Tickets
block: arbeitsalltag
woche: 15
worum: Nicht jedes Ticket ist eine Störung. Wer Störung, Serviceanfrage, Problem und Änderung auseinanderhält, löst schneller und lernt aus Wiederholungen.
rahmenplan: [A1, A5, A8]
lernfeld: LF6
merksatz: Die Störung wird behoben, das Problem wird gelöst. Jede Änderung wird geplant, freigegeben und hat einen Rückweg.
karten:
- frage: Was ist der Unterschied zwischen einer Störung (Incident) und einem Problem?
antwort: Eine Störung ist eine einzelne ungeplante Unterbrechung oder Beeinträchtigung – Ziel ist, den Nutzer schnell wieder arbeiten zu lassen. Ein Problem ist die Ursache hinter einer oder mehreren Störungen – Ziel ist, sie dauerhaft zu beseitigen.
- frage: Was ist eine Serviceanfrage (Service Request)?
antwort: Eine Bitte um eine vorgesehene Standardleistung, ohne dass etwas kaputt ist – etwa ein neues Konto, eine Softwareinstallation aus dem Katalog oder ein zurückgesetztes Passwort.
- frage: Was ist ein Workaround?
antwort: Eine Umgehungslösung, mit der der Nutzer weiterarbeiten kann, obwohl die eigentliche Ursache noch nicht beseitigt ist – etwa ein anderer Drucker, solange der Treiberfehler besteht.
- frage: Welche drei Arten von Änderungen unterscheidet man?
antwort: Standardänderungen sind vorab genehmigt, risikoarm und laufen nach festem Ablauf. Normale Änderungen werden einzeln geprüft und freigegeben. Notfalländerungen sind eilig und werden mit verkürzter Freigabe umgesetzt und nachträglich dokumentiert.
---
## Das Wesentliche
- **Störung (Incident):** Etwas funktioniert nicht oder schlechter als vorgesehen. Ziel ist,
den Betrieb **schnell** wiederherzustellen – notfalls mit einem Workaround.
- **Serviceanfrage (Service Request):** Nichts ist kaputt, jemand braucht eine vorgesehene
Leistung: neues Konto, Software, Zugriff, Gerät. Solche Anfragen laufen nach festen Abläufen
und lassen sich oft vorab genehmigen.
- **Problem:** die Ursache hinter einer oder mehreren Störungen. Drei Tickets mit „Outlook hängt
morgens" sind drei Störungen und vermutlich ein Problem. Ziel ist, die Ursache **dauerhaft**
zu beseitigen.
- **Bekannter Fehler (Known Error):** ein Problem mit bekannter Ursache und dokumentiertem
Workaround, dessen endgültige Lösung noch aussteht. Gehört in die Wissensdatenbank.
- **Änderung (Change):** alles, was an Systemen verändert wird. Jede Änderung hat ein Risiko –
deshalb wird sie beschrieben, bewertet, freigegeben, angekündigt und hat einen Rückweg.
- **Drei Arten von Änderungen:** Standard (vorab genehmigt, festgelegter Ablauf, etwa Arbeitsplatz
einrichten), normal (einzeln prüfen und freigeben), Notfall (sofort, Freigabe verkürzt,
Dokumentation danach).
- **Die Störung schließt man, wenn der Nutzer wieder arbeiten kann** – nicht erst, wenn die
Ursache gefunden ist. Die Ursache verfolgt man im Problem weiter.
- **Begriffe nach ITIL**, dem verbreitetsten Rahmenwerk für IT-Servicemanagement. Man muss es
nicht vollständig einführen, um von seinen Begriffen zu profitieren.
## Übersicht
| Ticket | Art | Ziel |
|---|---|---|
| „Ich kann nicht drucken" | Störung | Nutzer wieder arbeitsfähig machen |
| „Ich brauche Zugriff auf die Ablage Einkauf" | Serviceanfrage | Leistung nach festem Ablauf erbringen |
| Zum fünften Mal diese Woche hängt der Druckserver | Problem | Ursache finden und beseitigen |
| Druckserver bekommt neuen Treiber | Änderung | geplant, freigegeben, mit Rückweg umsetzen |
## Prüfungsfrage
**Frage:** Im Laufe einer Woche gehen sieben Tickets ein, dass die Verbindung zum
Warenwirtschaftssystem morgens zwischen 8 und 9 Uhr abbricht. Beschreiben Sie, wie Sie mit
diesen Tickets umgehen.
**Antwort:** Jedes Ticket ist zunächst eine Störung, die möglichst schnell behoben wird – etwa
durch einen Neustart der Anwendung oder einen Workaround wie das Arbeiten über eine andere
Verbindung. Weil sich die Störungen zur selben Zeit wiederholen, wird ein Problem angelegt und
mit den sieben Störungen verknüpft. Im Problem wird die Ursache gesucht, z. B. ein zeitgesteuerter
Vorgang wie eine Sicherung oder Anmeldespitzen am Morgen. Bis zur Lösung wird der Workaround als
bekannter Fehler in der Wissensdatenbank dokumentiert, damit der Support weitere Meldungen
schnell bedienen kann. Die endgültige Behebung – etwa die Verlegung der Sicherung – wird als
Änderung geplant, freigegeben und umgesetzt; danach wird das Problem geschlossen.
@@ -0,0 +1,63 @@
---
id: A11
titel: Priorisieren und eskalieren – was zuerst, und wer übernimmt
block: arbeitsalltag
woche: 15
worum: Wer immer das lauteste Ticket zuerst bearbeitet, lässt die wichtigen liegen. Die Priorität ergibt sich aus Auswirkung und Dringlichkeit – nicht aus der Lautstärke.
rahmenplan: [A1, A2, A7]
lernfeld: LF6
merksatz: Priorität = Auswirkung × Dringlichkeit. Eskalieren ist keine Niederlage, sondern ein vereinbarter Weg.
karten:
- frage: Woraus ergibt sich die Priorität eines Tickets?
antwort: Aus der Auswirkung (wie viele Nutzer, wie wichtige Abläufe sind betroffen) und der Dringlichkeit (wie schnell entsteht Schaden, gibt es eine Umgehung). Beides wird eingestuft und in einer Matrix zur Priorität verknüpft.
- frage: Was ist der Unterschied zwischen funktionaler und hierarchischer Eskalation?
antwort: Funktional wird ein Ticket an eine Stelle mit mehr Fachwissen weitergegeben, etwa vom ersten an den zweiten Level. Hierarchisch wird die Führungsebene einbezogen, etwa wenn eine Frist zu reißen droht oder Ressourcen fehlen.
- frage: Was unterscheidet die Reaktionszeit von der Lösungszeit in einem SLA?
antwort: Die Reaktionszeit ist die Zeit bis zur ersten qualifizierten Rückmeldung an den Nutzer, die Lösungszeit die Zeit bis zur Wiederherstellung des Betriebs.
---
## Das Wesentliche
- **Auswirkung:** Wie viele sind betroffen, und wie wichtig ist, was nicht geht? Ein einzelner
Nutzer ist gering, eine Abteilung mittel, der ganze Betrieb oder ein Kernprozess hoch.
- **Dringlichkeit:** Wie schnell entsteht Schaden? Kann gar nicht gearbeitet werden, nur
eingeschränkt, oder gibt es eine Umgehung?
- **Priorität aus beidem**, nicht aus Gefühl, Lautstärke oder Rang des Anrufers. Der Drucker der
Geschäftsführung ist eine Störung mit geringer Auswirkung; die Kasse im Lager, an der zwanzig
Lieferungen hängen, nicht.
- **Prioritäten dürfen sich ändern.** Kommt heraus, dass mehr Nutzer betroffen sind, wird
hochgestuft – und das im Ticket vermerkt.
- **Level:** Der erste Level nimmt an, erfasst, löst Bekanntes. Der zweite Level bearbeitet, was
Fachwissen braucht. Der dritte Level sind Spezialisten oder der Hersteller.
- **Funktional eskalieren**, wenn das eigene Wissen nicht reicht – rechtzeitig, mit vollständigem
Ticket. **Hierarchisch eskalieren**, wenn eine Frist zu reißen droht oder Entscheidungen
nötig sind, die man selbst nicht treffen darf.
- **SLA:** Vereinbarte Reaktions- und Lösungszeiten je Priorität. Die Uhr läuft ab Eingang, nicht
ab dem Moment, in dem man das Ticket öffnet.
- **Den Nutzer informieren**, auch wenn es nichts Neues gibt. Ein Nutzer, der weiß, dass jemand
dran ist, ruft nicht dreimal an.
## Übersicht
| Auswirkung ↓ / Dringlichkeit → | hoch | mittel | gering |
|---|---|---|---|
| **hoch** | P1 kritisch | P2 hoch | P3 mittel |
| **mittel** | P2 hoch | P3 mittel | P4 gering |
| **gering** | P3 mittel | P4 gering | P4 gering |
Beispiel für Zielzeiten: P1 Reaktion 15 Minuten, Lösung 4 Stunden · P2 1 Stunde / 1 Arbeitstag ·
P3 4 Stunden / 3 Arbeitstage · P4 1 Arbeitstag / nach Absprache.
## Prüfungsfrage
**Frage:** Gleichzeitig gehen drei Tickets ein: (a) Der Geschäftsführer kann an seinem zweiten
Bildschirm nichts sehen. (b) Im Lager kann niemand Lieferscheine drucken, der LKW wartet.
(c) Eine Mitarbeiterin bittet um Installation eines neuen PDF-Programms. Priorisieren Sie mit
Begründung.
**Antwort:** (b) zuerst – hohe Auswirkung, weil ein Kernprozess (Versand) für alle im Lager
steht, und hohe Dringlichkeit, weil der wartende LKW unmittelbar Kosten verursacht: P1. (a) danach
– geringe Auswirkung (ein Nutzer, ein Teil seines Arbeitsplatzes) und mittlere Dringlichkeit, weil
er mit einem Bildschirm weiterarbeiten kann: P4, auch wenn der Melder ranghoch ist; eine kurze
Rückmeldung mit Zeitangabe ist trotzdem angebracht. (c) ist keine Störung, sondern eine
Serviceanfrage, die nach dem vorgesehenen Ablauf (Freigabe, Softwarekatalog) erledigt wird.
@@ -0,0 +1,61 @@
---
id: A12
titel: Wissen festhalten – ein Wissensartikel, der wirklich hilft
block: arbeitsalltag
woche: 15
worum: Jede Störung, die zum zweiten Mal gelöst wird, hätte ein Wissensartikel beim ersten Mal verhindert. Gute Artikel sind kurz, auffindbar und aktuell.
rahmenplan: [A5, C5, A1]
lernfeld: LF6
merksatz: Symptom so, wie der Nutzer es sagt. Ursache, Lösung in Schritten, Gültigkeit, Stand. Wer etwas zum zweiten Mal löst, schreibt es auf.
karten:
- frage: Welche Teile gehören in einen Wissensartikel?
antwort: Das Symptom in den Worten des Nutzers samt Fehlermeldung, die Ursache, die Lösung in nummerierten Schritten, für welche Systeme und Versionen sie gilt, sowie Verfasser und Datum des letzten Stands.
- frage: Warum formuliert man das Symptom in den Worten des Nutzers?
antwort: Weil danach gesucht wird. Wer „Outlook fragt ständig nach dem Passwort" sucht, findet keinen Artikel mit dem Titel „Probleme mit der modernen Authentifizierung".
---
## Das Wesentliche
- **Wann schreiben?** Wer eine Lösung zum zweiten Mal braucht, schreibt sie auf. Bei jeder
Störung mit unklarer Ursache, sobald sie gefunden ist.
- **Titel = Symptom.** So, wie der Nutzer es am Telefon sagt, mit der Fehlermeldung im Wortlaut.
Danach wird gesucht.
- **Aufbau:** Symptom, betroffene Systeme und Versionen, Ursache, Lösung in nummerierten Schritten,
ggf. Workaround, Verfasser und Datum.
- **Eine Lösung, ein Artikel.** Ein Artikel „Alles zu Outlook" findet niemand und pflegt niemand.
- **Für wen?** Artikel für den Support dürfen Fachbegriffe und Befehle enthalten. Artikel für
Nutzer brauchen Bildschirmfotos und einfache Sprache.
- **Keine Zugangsdaten** in Wissensartikeln. Ein Verweis auf den Eintrag im Passwortmanager genügt.
- **Mit Tickets verknüpfen:** Im Ticket auf den Artikel verweisen, im Artikel auf Beispieltickets.
So sieht man, wie oft er gebraucht wird.
- **Pflegen:** Ein Artikel mit veralteter Lösung ist schlimmer als keiner. Wer einen Artikel nutzt
und ihn falsch findet, korrigiert ihn sofort oder meldet es.
## Übersicht
```
Titel: Outlook fragt nach jedem Start erneut nach dem Passwort
Gilt für: Outlook (Microsoft 365) unter Windows 11
Symptom: Anmeldefenster erscheint bei jedem Start, Passwort wird nicht gespeichert
Ursache: Beschädigter Eintrag in der Windows-Anmeldeinformationsverwaltung
Lösung: 1. Outlook schließen
2. Systemsteuerung → Anmeldeinformationsverwaltung → Windows-Anmeldeinformationen
3. Alle Einträge mit „MicrosoftOffice" entfernen
4. Outlook starten und einmal anmelden
Prüfen: Outlook schließen und erneut starten – keine Abfrage mehr
Stand: 2026-10-05, Ausbildung IT
Tickets: #4711, #4733
```
## Prüfungsfrage
**Frage:** Ihr Team beantwortet dieselben Fragen immer wieder neu, obwohl es ein Wiki gibt.
Nennen Sie drei mögliche Ursachen und je eine Maßnahme.
**Antwort:** (1) Die Artikel sind nicht auffindbar, weil die Titel technisch statt nach Symptom
formuliert sind – Titel nach den Worten der Nutzer und mit Fehlermeldung formulieren. (2) Die
Artikel sind veraltet, deshalb traut ihnen niemand – Datum und Verantwortlichen angeben und
beim Gebrauch prüfen und korrigieren. (3) Schreiben gehört nicht zum Ablauf – festlegen, dass beim
Schließen eines Tickets mit neuer Lösung ein Artikel angelegt oder verknüpft wird. Ebenfalls
richtig: zu lange Sammelartikel aufteilen, Artikel mit Tickets verknüpfen, Suche im Ticketsystem
statt in einem getrennten Wiki.
@@ -0,0 +1,64 @@
---
id: H4
titel: Ergonomie am Bildschirmarbeitsplatz
block: hardware
woche: 16
worum: Wer einen Arbeitsplatz einrichtet, richtet ihn für acht Stunden am Tag und viele Jahre ein. Nacken, Augen und Handgelenke merken jeden Zentimeter, den der Bildschirm falsch steht.
rahmenplan: [C3, A2, A7]
lernfeld: LF2
merksatz: Oberkante auf Augenhöhe, eine Armlänge Abstand, seitlich zum Fenster. Füße flach, Arme im rechten Winkel. Und zwischendurch aufstehen.
karten:
- frage: Wie wird ein Bildschirm richtig aufgestellt?
antwort: Die oberste Zeile liegt auf oder leicht unter Augenhöhe, der Abstand beträgt etwa eine Armlänge (rund 50 bis 70 cm), und die Blickrichtung verläuft parallel zum Fenster, damit weder Spiegelungen noch Blendung entstehen.
- frage: Welche Vorschrift regelt die Anforderungen an Bildschirmarbeitsplätze?
antwort: Die Arbeitsstättenverordnung (ArbStättV), Anhang Nr. 6. Sie hat 2016 die frühere Bildschirmarbeitsverordnung abgelöst. Der Arbeitgeber muss die Gefährdungen in einer Gefährdungsbeurteilung ermitteln.
- frage: Was muss der Arbeitgeber Beschäftigten an Bildschirmarbeitsplätzen zur Augenvorsorge anbieten?
antwort: Eine arbeitsmedizinische Angebotsvorsorge mit Untersuchung der Augen und des Sehvermögens. Ergibt sie, dass eine spezielle Sehhilfe für die Bildschirmarbeit nötig ist, muss der Arbeitgeber sie bezahlen.
---
## Das Wesentliche
- **Bildschirm:** Oberste Zeile auf oder knapp unter Augenhöhe, sodass der Blick leicht nach
unten geht. Abstand etwa eine Armlänge. Zwei Bildschirme gleich hoch, der Hauptbildschirm
direkt vor dem Körper.
- **Licht:** Blickrichtung parallel zum Fenster, nicht davor und nicht mit dem Rücken dazu.
Keine Lampe, die sich im Bildschirm spiegelt. Für Büroarbeit sind 500 Lux am Arbeitsplatz
vorgesehen.
- **Stuhl:** Füße flach auf dem Boden, Oberschenkel etwa waagerecht, Rückenlehne stützt den
unteren Rücken. Die Armlehnen so hoch, dass die Schultern locker bleiben.
- **Tisch:** Unterarme liegen bei entspannten Schultern etwa waagerecht auf, die Ellbogen im
rechten Winkel. Höhenverstellbare Tische erlauben den Wechsel zwischen Sitzen und Stehen.
- **Tastatur und Maus:** Vor dem Körper, etwa eine Handbreite Platz zur Tischkante. Die Maus
direkt neben der Tastatur, nicht am ausgestreckten Arm.
- **Notebooks** sind für Unterwegs. Wer täglich stundenlang daran arbeitet, braucht
Dockingstation, externen Bildschirm, Tastatur und Maus – sonst ist der Blick dauerhaft zu tief.
- **Bewegung:** Bildschirmarbeit wird durch andere Tätigkeiten oder Pausen unterbrochen. Kein
Stuhl gleicht acht Stunden Stillsitzen aus.
- **Rechtlicher Rahmen:** Arbeitsstättenverordnung, Anhang Nr. 6. Der Arbeitgeber ermittelt die
Belastungen in der Gefährdungsbeurteilung und bietet arbeitsmedizinische Vorsorge für die
Augen an.
## Übersicht
| Beschwerde | Wahrscheinliche Ursache | Abhilfe |
|---|---|---|
| Nackenschmerzen | Bildschirm zu hoch, zu tief oder seitlich versetzt | Höhe und Position anpassen, Hauptbildschirm mittig |
| Brennende, müde Augen | Spiegelungen, Blendung, zu kleine Schrift | Bildschirm drehen, Licht ändern, Skalierung erhöhen |
| Verspannte Schultern | Tisch zu hoch, Armlehnen falsch | Stuhl und Armlehnen nachstellen, Tischhöhe anpassen |
| Schmerzen im Handgelenk | abgeknickte Hand, Maus zu weit weg | Maus heran, Handballen auflegen, ggf. vertikale Maus |
| Rückenschmerzen | Rückenlehne nicht genutzt, zu langes Sitzen | Lehne einstellen, Steh-Phasen, Bewegungspausen |
## Prüfungsfrage
**Frage:** Eine Mitarbeiterin klagt über Nackenschmerzen und brennende Augen. Ihr Bildschirm
steht seitlich neben dem Notebook, mit dem Fenster im Rücken. Nennen Sie vier Maßnahmen und
begründen Sie zwei davon.
**Antwort:** (1) Den Bildschirm direkt vor die Mitarbeiterin stellen, damit sie den Kopf nicht
dauerhaft dreht – Ursache der Nackenschmerzen. (2) Die Höhe so einstellen, dass die oberste Zeile
auf oder leicht unter Augenhöhe liegt. (3) Den Arbeitsplatz so drehen, dass die Blickrichtung
parallel zum Fenster verläuft – mit dem Fenster im Rücken spiegelt es sich im Bildschirm, und
die Augen ermüden. (4) Notebook mit Dockingstation, externer Tastatur und Maus betreiben, statt auf
den Notebook-Bildschirm zu schauen. (5) Schriftgröße bzw. Skalierung erhöhen. (6) Auf die
Angebotsvorsorge für die Augen hinweisen; ggf. ist eine Bildschirmarbeitsbrille nötig, die der
Arbeitgeber bezahlt.
@@ -0,0 +1,60 @@
---
id: H5
titel: Energie rechnen und nachhaltig beschaffen
block: hardware
woche: 16
worum: Ein Rechner, der nachts läuft, kostet Geld und Strom, ohne dass jemand etwas davon hat. Wer den Verbrauch rechnen kann, kann Entscheidungen begründen – auch beim Kauf und bei der Entsorgung.
rahmenplan: [C4, A3, A6]
lernfeld: LF2
merksatz: Leistung in kW mal Stunden mal Preis je kWh. Länger nutzen schlägt sparsamer kaufen. Daten vernichten, bevor das Gerät den Betrieb verlässt.
karten:
- frage: Wie berechnet man die Stromkosten eines Geräts?
antwort: Leistung in Kilowatt mal Betriebsstunden ergibt die Energie in Kilowattstunden, mal Preis je kWh ergibt die Kosten. Ein Server mit 150 W, der das ganze Jahr läuft – 0,15 kW × 8.760 h = 1.314 kWh, bei 0,30 € je kWh also 394,20 €.
- frage: Welche Norm regelt die Vernichtung von Datenträgern, und welche Sicherheitsstufe ist für personenbezogene Daten üblich?
antwort: DIN 66399. Sie kennt drei Schutzklassen und sieben Sicherheitsstufen. Für personenbezogene Daten wird in der Regel mindestens Sicherheitsstufe 3 angesetzt, für besonders schützenswerte Daten wie Gesundheitsdaten höher.
- frage: Warum ist eine längere Nutzung oft nachhaltiger als ein sparsameres neues Gerät?
antwort: Weil ein großer Teil der Umweltbelastung eines Rechners bei der Herstellung entsteht, nicht im Betrieb. Ein Gerät ein, zwei Jahre länger zu nutzen, spart meist mehr als der geringere Verbrauch eines neuen.
---
## Das Wesentliche
- **Die Formel:** Leistung (kW) × Zeit (h) = Energie (kWh). Energie × Preis je kWh = Kosten.
Watt erst durch 1.000 teilen, sonst stimmt die Kommastelle nicht.
- **Die Stunden richtig zählen:** Ein Arbeitsplatz läuft etwa 8 Stunden an 220 Arbeitstagen,
ein Server 24 Stunden an 365 Tagen, also 8.760 Stunden.
- **Leerlauf kostet am meisten**, weil er die meisten Stunden hat. Ruhezustand und
Energiesparpläne per Gruppenrichtlinie, Abschalten über Nacht, Drucker mit Ruhemodus.
- **Beim Kauf:** Verbrauch im Leerlauf und unter Last vergleichen, Energielabel bei Bildschirmen,
Siegel wie TCO Certified, Blauer Engel, EPEAT oder ENERGY STAR. Netzteile mit hohem
Wirkungsgrad (80 PLUS).
- **Länger nutzen:** Ein großer Teil der Umweltbelastung entsteht bei der Herstellung. Aufrüsten
(SSD, Arbeitsspeicher), Reparieren und Weitergeben sind oft nachhaltiger als ein neues Gerät.
Aufbereitete Geräte (refurbished) sind für viele Arbeitsplätze eine gute Wahl.
- **Entsorgen:** Elektrogeräte gehören nicht in den Restmüll. Nach dem Elektro- und
Elektronikgerätegesetz (ElektroG) gehen sie über Rücknahmestellen oder zertifizierte
Entsorger zurück in die Verwertung.
- **Vorher die Daten:** Datenträger werden vor der Abgabe nachweisbar gelöscht oder vernichtet –
nach DIN 66399, mit Protokoll. Ein formatierter Datenträger ist nicht gelöscht.
## Übersicht
| Gerät | Leistung | Stunden im Jahr | kWh | Kosten bei 0,30 €/kWh |
|---|---|---|---|---|
| Arbeitsplatzrechner, 8 h an 220 Tagen | 45 W | 1.760 | 79,2 | 23,76 € |
| Server, durchgehend | 150 W | 8.760 | 1.314 | 394,20 € |
| Rechner, der außerhalb der Arbeitszeit im Leerlauf läuft | 30 W | 7.000 | 210 | 63,00 € |
Bei 100 Rechnern, die nachts und am Wochenende nicht ausgeschaltet werden, sind das allein für
den Leerlauf 6.300 € im Jahr.
## Prüfungsfrage
**Frage:** In einem Betrieb laufen 60 Arbeitsplatzrechner rund um die Uhr. Im Leerlauf nimmt jeder
35 W auf. Wie viel ließe sich im Jahr sparen, wenn die Rechner außerhalb der Arbeitszeit
(8 Stunden an 220 Tagen) ausgeschaltet würden? Strompreis 0,30 € je kWh.
**Antwort:** Stunden außerhalb der Arbeitszeit: 8.760 h − (8 h × 220) = 8.760 h − 1.760 h =
7.000 h. Energie je Rechner: 0,035 kW × 7.000 h = 245 kWh. Für 60 Rechner: 245 kWh × 60 =
14.700 kWh. Ersparnis: 14.700 kWh × 0,30 €/kWh = **4.410 €** im Jahr. Hinzu kommt, dass
ausgeschaltete Rechner nachts nicht angegriffen werden können; Updates lassen sich mit
Wake-on-LAN trotzdem außerhalb der Arbeitszeit einspielen.
@@ -0,0 +1,95 @@
---
id: P4
titel: Pseudocode, Struktogramm und Schreibtischtest
block: programmierung
woche: 16
worum: In der Prüfung schreibt man keinen lauffähigen Code, sondern beschreibt den Ablauf so, dass ihn jeder umsetzen kann. Drei Bausteine genügen – Folge, Verzweigung, Schleife.
rahmenplan: [A10, A4]
lernfeld: LF5
merksatz: Erst den Ablauf in Worten, dann in Pseudocode. Jede Schleife braucht ein Ende. Mit einem Schreibtischtest prüfen, bevor man abgibt.
karten:
- frage: Was unterscheidet eine kopfgesteuerte von einer fußgesteuerten Schleife?
antwort: Die kopfgesteuerte Schleife (SOLANGE … ENDE SOLANGE) prüft die Bedingung vor jedem Durchlauf und läuft unter Umständen gar nicht. Die fußgesteuerte (WIEDERHOLE … BIS) prüft erst danach und läuft deshalb mindestens einmal.
- frage: Was ist ein Schreibtischtest?
antwort: Man spielt einen Algorithmus von Hand mit Beispielwerten durch und notiert in einer Tabelle nach jedem Schritt die Werte aller Variablen. So findet man Fehler, ohne den Code auszuführen.
- frage: Was liefert dieser Ablauf für die Liste 72, 91, 85, 60 – zaehler ← 0; FÜR jeden wert IN liste – WENN wert > 80 DANN zaehler ← zaehler + 1?
antwort: 2, denn nur 91 und 85 sind größer als 80.
---
## Das Wesentliche
- **Drei Bausteine** reichen für jeden Ablauf: **Folge** (Schritt für Schritt), **Verzweigung**
(WENN … DANN … SONST), **Schleife** (wiederholen, bis etwas erreicht ist).
- **Pseudocode** ist strukturierte Umgangssprache mit festen Schlüsselwörtern. In der Prüfung
gibt es keine verbindliche Schreibweise; wichtig ist, dass Einrückung und Schlüsselwörter
eindeutig zeigen, was wozu gehört.
- **Zuweisung:** `summe ← 0` – die Variable bekommt einen Wert. Vergleich: `summe = 0`.
- **Schleifen:**
- `FÜR i ← 1 BIS n` – Anzahl der Durchläufe steht vorher fest
- `SOLANGE bedingung` – kopfgesteuert, prüft vorher, läuft eventuell nie
- `WIEDERHOLE … BIS bedingung` – fußgesteuert, läuft mindestens einmal
- **Struktogramm (Nassi-Shneiderman):** Der Ablauf als Kasten; eine Verzweigung teilt den Kasten
mit einem Dreieck in Ja und Nein, eine Schleife umschließt die zu wiederholenden Schritte.
Sprünge gibt es nicht.
- **Programmablaufplan (PAP):** Ablauf mit Symbolen nach DIN 66001 – Oval für Start und Ende,
Rechteck für Tätigkeiten, Raute für Entscheidungen, Parallelogramm für Ein- und Ausgabe.
- **Schreibtischtest:** Beispielwerte wählen, Schritt für Schritt durchgehen und die Werte aller
Variablen in einer Tabelle festhalten. Grenzfälle mitnehmen: leere Liste, ein Element, genau
der Grenzwert.
## Übersicht
Gesucht: Wie viele Server haben mehr als 80 % Plattenbelegung, und wie hoch ist die höchste?
```
anzahlKritisch ← 0
maximum ← belegung[0]
FÜR i ← 0 BIS laenge(belegung) − 1
WENN belegung[i] > 80 DANN
anzahlKritisch ← anzahlKritisch + 1
ENDE WENN
WENN belegung[i] > maximum DANN
maximum ← belegung[i]
ENDE WENN
ENDE FÜR
AUSGABE anzahlKritisch, maximum
```
Schreibtischtest mit `belegung = [72, 91, 85, 60]` – „Wert" ist `belegung[i]`, „Anzahl" ist
`anzahlKritisch`, „Max." ist `maximum`:
| i | Wert | > 80? | Anzahl | Max. |
|---|---|---|---|---|
| – | – | – | 0 | 72 |
| 0 | 72 | nein | 0 | 72 |
| 1 | 91 | ja | 1 | 91 |
| 2 | 85 | ja | 2 | 91 |
| 3 | 60 | nein | 2 | 91 |
Ausgabe: 2 und 91.
## Prüfungsfrage
**Frage:** Ein Prüfprogramm erhält eine Liste von Messwerten. Ein Wert ist in Ordnung, wenn er
zwischen 9,8 und 10,2 liegt (Grenzen eingeschlossen). Schreiben Sie einen Pseudocode, der
ausgibt, wie viele Werte außerhalb der Toleranz liegen, und ob mehr als 5 % der Werte betroffen
sind.
**Antwort:**
```
ausserhalb ← 0
FÜR jeden wert IN messwerte
WENN wert < 9,8 ODER wert > 10,2 DANN
ausserhalb ← ausserhalb + 1
ENDE WENN
ENDE FÜR
AUSGABE ausserhalb
WENN laenge(messwerte) > 0 UND ausserhalb / laenge(messwerte) > 0,05 DANN
AUSGABE "Mehr als 5 % außerhalb der Toleranz"
ENDE WENN
```
Die Bedingung prüft beide Grenzen mit ODER, weil ein Wert entweder zu klein oder zu groß sein
kann. Die Grenzwerte selbst gelten als in Ordnung, deshalb `<` und `>` statt `≤` und `≥`. Die
Prüfung auf eine leere Liste verhindert eine Division durch null.
@@ -0,0 +1,64 @@
---
id: S10
titel: Protokolle lesen – und warum die Uhrzeit stimmen muss
block: system
woche: 14
worum: Protokolle sagen, was passiert ist. Aber nur, wenn man weiß, wo man suchen muss, und wenn die Uhren aller Systeme übereinstimmen.
rahmenplan: [A8, A6, B3]
lernfeld: LF11
merksatz: Zeitpunkt eingrenzen, dann suchen. Alle Uhren per NTP gleich. Protokolle enthalten personenbezogene Daten.
karten:
- frage: Welche drei Protokolle der Windows-Ereignisanzeige sind für die Fehlersuche am wichtigsten?
antwort: System (Treiber, Dienste, Neustarts), Anwendung (Meldungen der Programme) und Sicherheit (Anmeldungen, Zugriffe, Änderungen an Konten).
- frage: Warum muss die Uhrzeit auf allen Systemen übereinstimmen?
antwort: Damit sich Ereignisse aus verschiedenen Protokollen einander zuordnen lassen, und weil Kerberos-Anmeldungen im Active Directory scheitern, wenn die Uhren um mehr als fünf Minuten abweichen.
- frage: Was bedeuten die Ereignis-IDs 4624 und 4625 im Sicherheitsprotokoll von Windows?
antwort: 4624 ist eine erfolgreiche Anmeldung, 4625 eine fehlgeschlagene. Viele 4625 in kurzer Zeit für dasselbe Konto deuten auf Passwortraten hin.
---
## Das Wesentliche
- **Erst den Zeitpunkt eingrenzen, dann suchen.** „Seit wann?" ist die wichtigste Frage bei
jeder Störung. Wer den Zeitraum kennt, findet die Ursache im Protokoll in Minuten statt
Stunden.
- **Windows – Ereignisanzeige:** Protokoll *System* für Dienste, Treiber und Neustarts,
*Anwendung* für Programme, *Sicherheit* für Anmeldungen und Zugriffe. Filtern nach Zeitraum,
Ebene (Fehler, Warnung) und Ereignis-ID.
- **Linux – Journal:** `journalctl --since "2026-10-05 08:00" --until "09:00"`, `-u dienst`
für einen Dienst, `-p err` für Fehler. Klassische Textdateien unter `/var/log`.
- **Bekannte Ereignis-IDs sparen Zeit:** 4624 Anmeldung erfolgreich, 4625 fehlgeschlagen,
4740 Konto gesperrt, 6008 unerwartetes Herunterfahren, 41 (Kernel-Power) Neustart ohne
sauberes Herunterfahren.
- **Zeitsynchronisation:** Alle Systeme beziehen ihre Zeit per **NTP** aus derselben Quelle –
in einer Domäne vom Domänencontroller, dieser von einer verlässlichen externen Quelle.
Weichen Uhren um mehr als fünf Minuten ab, scheitert die Anmeldung über Kerberos.
- **Zentral sammeln:** Bei mehr als einer Handvoll Systeme gehören die Protokolle an einen
zentralen Ort. Dann lässt sich eine Anmeldung über mehrere Systeme verfolgen – und ein
Angreifer kann seine Spuren nicht auf dem befallenen System löschen.
- **Protokolle sind personenbezogen.** Sie zeigen, wer sich wann wo angemeldet hat. Deshalb:
Aufbewahrung begrenzen, Zugriff beschränken, nur für den vereinbarten Zweck auswerten.
## Übersicht
| Frage | Windows | Linux |
|---|---|---|
| Warum ist der Server neu gestartet? | System, Ereignis 6008 bzw. 41 und 1074 | `journalctl --list-boots`, `last -x reboot` |
| Wer hat sich angemeldet? | Sicherheit, Ereignis 4624 | `last`, `journalctl -u ssh` |
| Warum startet der Dienst nicht? | System, Quelle „Service Control Manager" | `systemctl status`, `journalctl -u dienst` |
| Stimmt die Uhrzeit? | `w32tm /query /status` | `timedatectl` |
## Prüfungsfrage
**Frage:** Mehrere Benutzer können sich seit heute Morgen nicht mehr an der Domäne anmelden,
andere schon. Die Fehlermeldung nennt einen Zeitunterschied. Erklären Sie die Ursache und Ihr
Vorgehen.
**Antwort:** Die Anmeldung im Active Directory erfolgt über Kerberos, das nur eine geringe
Abweichung der Uhrzeiten zwischen Client und Domänencontroller zulässt (standardmäßig fünf
Minuten). Bei den betroffenen Rechnern geht die Uhr vermutlich falsch, etwa weil die
Zeitsynchronisation nicht läuft oder eine leere CMOS-Batterie die Uhr nach einem Neustart
zurückgesetzt hat. Vorgehen: (1) Auf einem betroffenen Client mit `w32tm /query /status`
Zeitquelle und Abweichung prüfen. (2) Prüfen, ob der Domänencontroller selbst die richtige
Zeit hat und von einer verlässlichen Quelle bezieht. (3) Die Zeit mit `w32tm /resync`
synchronisieren und die Anmeldung testen. (4) Die Ursache beheben – Zeitdienst, Gruppenrichtlinie
für die Zeitquelle oder Batterie – und die Zeitabweichung in die Überwachung aufnehmen.
@@ -0,0 +1,64 @@
---
id: S11
titel: Updates und Patches – planbar statt hektisch
block: system
woche: 14
worum: Die meisten erfolgreichen Angriffe nutzen Lücken, für die es längst ein Update gibt. Patch-Management sorgt dafür, dass Updates zuverlässig ankommen, ohne dass dabei der Betrieb stehen bleibt.
rahmenplan: [A8, A6, A5]
lernfeld: LF11
merksatz: Wissen, was läuft. Erst Test, dann Pilotgruppe, dann alle. Kritische Lücken sofort, der Rest im festen Takt.
karten:
- frage: In welcher Reihenfolge verteilt man Updates im Betrieb?
antwort: Zuerst auf Testsysteme, dann auf eine kleine Pilotgruppe aus normalen Arbeitsplätzen, und erst wenn dort nach einigen Tagen nichts auffällt, auf alle. Server in einem angekündigten Wartungsfenster.
- frage: Wann ist der Patchday von Microsoft?
antwort: Am zweiten Dienstag jedes Monats. Für Lücken, die bereits aktiv ausgenutzt werden, erscheinen zusätzlich außerplanmäßige Updates.
- frage: Was bedeutet End of Life bei Software, und was folgt daraus?
antwort: Der Hersteller liefert keine Sicherheitsupdates mehr – neue Lücken bleiben für immer offen. Solche Software muss ersetzt oder, wenn das nicht sofort geht, vom Netz getrennt und besonders abgesichert werden. Beispiel – Windows 10 seit dem 14. Oktober 2025.
---
## Das Wesentliche
- **Ohne Bestand kein Patch-Management.** Man muss wissen, welche Geräte, Betriebssysteme und
Programme mit welcher Version im Betrieb laufen. Was nicht in der Liste steht, wird auch nicht
aktualisiert.
- **Fester Takt:** Microsoft veröffentlicht am **zweiten Dienstag** im Monat. Viele Betriebe
planen danach: Test in der Woche danach, Pilotgruppe, dann alle innerhalb von zwei bis drei
Wochen.
- **Ringe statt alle auf einmal:** Testsysteme, Pilotgruppe (die IT selbst und einige
freiwillige Nutzer), dann alle. Ein fehlerhaftes Update trifft so zehn Rechner, nicht hundert.
- **Kritische Lücken außer der Reihe.** Wird eine Lücke bereits aktiv ausgenutzt, wird sofort
gehandelt – notfalls mit verkürztem Test. Hinweise liefern der Hersteller und die Warnungen
des BSI.
- **Nicht nur Windows:** Programme wie Browser und PDF-Leser, Firmware von Firewall, Switch, NAS
und BIOS, Linux-Server, Smartphones. Gerade Netzwerkgeräte werden oft jahrelang vergessen.
- **Server im Wartungsfenster**, angekündigt, mit Snapshot oder Sicherung vorher und einem
Rückweg, falls etwas schiefgeht.
- **Nachprüfen:** Kam das Update überall an? Ein Bericht über den Stand aller Geräte gehört zum
Abschluss jedes Zyklus.
- **End of Life im Blick behalten.** Software ohne Sicherheitsupdates wird rechtzeitig ersetzt –
das Ende ist Jahre vorher bekannt.
## Übersicht
| Ring | Wer | Wann nach Veröffentlichung |
|---|---|---|
| 0 – Test | Testsysteme der IT | sofort |
| 1 – Pilot | IT und einige freiwillige Nutzer | nach 2–3 Tagen |
| 2 – Breit | alle Arbeitsplätze | nach etwa einer Woche |
| 3 – Server | Server im Wartungsfenster | nach etwa zwei Wochen |
| Notfall | alle betroffenen Systeme | sofort, bei aktiv ausgenutzter Lücke |
## Prüfungsfrage
**Frage:** In Ihrem Betrieb werden Updates bisher „wenn jemand Zeit hat" eingespielt.
Entwerfen Sie ein Vorgehen für das Patch-Management und begründen Sie zwei Bestandteile.
**Antwort:** (1) Einen vollständigen Bestand aller Geräte und Programme mit Version führen.
(2) Einen festen Takt nach dem monatlichen Patchday festlegen. (3) Updates in Ringen verteilen:
Testsysteme, Pilotgruppe, alle Arbeitsplätze, Server im angekündigten Wartungsfenster mit
Sicherung vorher. (4) Für aktiv ausgenutzte Lücken ein beschleunigtes Notfallverfahren
vorsehen. (5) Nach jedem Zyklus prüfen und berichten, auf welchem Stand alle Geräte sind.
(6) Firmware von Netzwerkgeräten und Software ohne automatische Aktualisierung einbeziehen.
Begründungen etwa: Die Ringe begrenzen den Schaden eines fehlerhaften Updates auf wenige
Geräte. Der Bestand ist Voraussetzung, weil ein Gerät, das niemand kennt, auch nie aktualisiert
wird – und genau dort sitzt dann die Lücke.
@@ -0,0 +1,62 @@
---
id: S6
titel: Linux – Dateisystem und die Befehle für jeden Tag
block: system
woche: 13
worum: Unter Linux gibt es keine Laufwerksbuchstaben, sondern einen einzigen Verzeichnisbaum. Wer weiß, wo was liegt, findet Konfiguration und Fehler in Minuten.
rahmenplan: [B3, A8]
lernfeld: LF9
merksatz: Konfiguration in /etc, Protokolle in /var/log, Benutzer in /home. Erst lesen mit less und grep, dann ändern.
karten:
- frage: Wo liegen unter Linux die Konfigurationsdateien und wo die Protokolle?
antwort: Konfiguration in /etc, etwa /etc/ssh/sshd_config. Protokolle in /var/log und im Journal von systemd, das man mit journalctl liest.
- frage: Was zeigen df -h und du -sh?
antwort: df -h zeigt die Belegung der eingehängten Dateisysteme, also wie voll jede Platte ist. du -sh zeigt, wie viel Platz ein bestimmtes Verzeichnis belegt – damit findet man, wer die Platte füllt.
- frage: Wie findet man alle Zeilen mit „error" in den Protokollen unter /var/log, ohne Groß- und Kleinschreibung zu beachten?
antwort: Mit grep -ri error /var/log – r durchsucht die Unterverzeichnisse, i ignoriert die Schreibweise. Für das Journal von systemd stattdessen journalctl -p err.
---
## Das Wesentliche
- **Ein Baum ab `/`.** Weitere Platten und Freigaben werden irgendwo in diesen Baum
eingehängt, etwa unter `/mnt` oder `/srv`.
- **Die wichtigsten Orte:** `/etc` Konfiguration, `/var/log` Protokolle, `/home` die
Benutzer, `/root` der Administrator, `/usr/bin` Programme, `/tmp` Zwischendateien,
`/opt` und `/srv` für eigene Anwendungen und Daten.
- **Groß- und Kleinschreibung zählt.** `Bericht.txt` und `bericht.txt` sind zwei Dateien.
- **Navigieren:** `pwd` (wo bin ich?), `ls -la` (was liegt hier, auch versteckt, mit Rechten),
`cd` (wechseln), `cd ~` (nach Hause), `cd -` (zurück).
- **Dateien:** `cp`, `mv`, `rm`, `mkdir -p`. `rm -r` löscht ohne Papierkorb und ohne
Rückfrage – vorher mit `ls` prüfen, was getroffen wird.
- **Lesen:** `less` zum Blättern (`/` sucht, `q` beendet), `tail -f` zum Mitlesen eines
wachsenden Protokolls, `grep` zum Suchen in Dateien.
- **Finden:** `find /etc -name "*.conf"` nach Namen, `find /var -size +500M` nach Größe.
- **Platz:** `df -h` für die Platten, `du -sh /var/*` für die Verzeichnisse darin.
- **Bearbeiten:** `nano` ist für den Anfang der freundlichste Editor. Vor jeder Änderung eine
Kopie: `cp datei datei.bak`.
- **Hilfe:** `man befehl` oder `befehl --help`.
## Übersicht
| Frage | Befehl |
|---|---|
| Wie voll sind die Platten? | `df -h` |
| Welches Verzeichnis ist so groß? | `du -sh /var/* \| sort -h` |
| Was steht zuletzt im Protokoll? | `tail -n 50 /var/log/syslog` oder `journalctl -n 50` |
| Welche Netzwerkadressen habe ich? | `ip a` |
| Welche Ports sind offen? | `ss -tlnp` |
| Welche Prozesse brauchen Leistung? | `top` oder `htop` |
| Welche Distribution läuft? | `cat /etc/os-release` |
## Prüfungsfrage
**Frage:** Ein Linux-Server meldet, dass die Platte voll ist. Beschreiben Sie mit Befehlen,
wie Sie die Ursache finden.
**Antwort:** (1) Mit `df -h` prüfen, welches Dateisystem voll ist und wo es eingehängt ist.
(2) Mit `du -sh /pfad/* | sort -h` die Unterverzeichnisse nach Größe sortieren und schrittweise
in das größte hineingehen, bis die Ursache gefunden ist – häufig sind es Protokolle unter
`/var/log`, alte Sicherungen oder ungenutzte Container-Abbilder. (3) Mit `find /pfad -size +1G`
gezielt nach sehr großen Dateien suchen. (4) Vor dem Löschen klären, ob die Daten noch
gebraucht werden, und bei Protokollen die Ursache beheben, etwa eine fehlende
Protokollrotation oder einen Dienst, der in einer Fehlerschleife schreibt.
@@ -0,0 +1,60 @@
---
id: S7
titel: Linux – Benutzer, Gruppen und Dateirechte
block: system
woche: 13
worum: Unter Linux hat jede Datei einen Besitzer, eine Gruppe und drei Mal drei Rechte. Das ist einfacher als unter Windows – und genauso schnell falsch gesetzt.
rahmenplan: [B3, A6]
lernfeld: LF9
merksatz: Lesen 4, Schreiben 2, Ausführen 1 – für Besitzer, Gruppe, alle anderen. Täglich arbeiten als Benutzer, Rechte holen mit sudo.
karten:
- frage: Was bedeutet chmod 640 bericht.txt?
antwort: Der Besitzer darf lesen und schreiben (6 = 4 + 2), die Gruppe darf lesen (4), alle anderen dürfen nichts (0).
- frage: Was bedeutet das Recht x bei einem Verzeichnis?
antwort: Man darf in das Verzeichnis wechseln und auf die Dateien darin zugreifen. Ohne x nützt auch das Leserecht r wenig – man sieht die Namen, kommt aber nicht an die Dateien.
- frage: Wie nimmt man einen Benutzer nachträglich in eine Gruppe auf, ohne ihn aus den anderen zu entfernen?
antwort: Mit sudo usermod -aG gruppe benutzer. Das a für append ist entscheidend – ohne es ersetzt -G alle bisherigen Zusatzgruppen.
---
## Das Wesentliche
- **Jede Datei** hat einen Besitzer, eine Gruppe und Rechte für drei Klassen: Besitzer (u),
Gruppe (g), alle anderen (o).
- **Drei Rechte:** r lesen (4), w schreiben (2), x ausführen (1). Bei Verzeichnissen heißt x:
hineinwechseln dürfen.
- **Lesen in `ls -l`:** `-rw-r----- 1 anna buchhaltung bericht.txt` – Datei, Besitzerin
anna liest und schreibt, Gruppe buchhaltung liest, alle anderen nichts.
- **Setzen:** `chmod 640 datei` als Zahl oder `chmod g+w datei` als Zeichen. `chown anna:buchhaltung
datei` ändert Besitzer und Gruppe.
- **777 ist nie die Lösung.** Wer „Permission denied" mit `chmod 777` beseitigt, hat das Problem
versteckt und eine Lücke geöffnet. Richtig ist, Besitzer oder Gruppe passend zu setzen.
- **Benutzer:** `sudo adduser anna` legt Benutzer samt Heimatverzeichnis an (Debian, Ubuntu).
Gespeichert wird in `/etc/passwd`, die Passwort-Hashes in `/etc/shadow`, Gruppen in
`/etc/group`.
- **Gruppen:** `sudo usermod -aG gruppe anna`. Wirksam erst nach erneuter Anmeldung.
- **root und sudo:** root darf alles. Im Alltag arbeitet man als normaler Benutzer und holt sich
Rechte für einen einzelnen Befehl mit `sudo`. Jeder `sudo`-Aufruf wird protokolliert.
- **Konto sperren statt löschen**, wenn jemand geht: `sudo usermod -L anna` und
`sudo chage -E 0 anna`. So bleibt nachvollziehbar, wem welche Dateien gehörten.
## Übersicht
| Zahl | Zeichen | Bedeutung | Typisch für |
|---|---|---|---|
| 644 | rw-r--r-- | Besitzer schreibt, alle lesen | Konfigurationsdateien, Webseiten |
| 640 | rw-r----- | Besitzer schreibt, Gruppe liest | Dateien mit vertraulichem Inhalt |
| 600 | rw------- | nur der Besitzer | private Schlüssel, Zugangsdaten |
| 755 | rwxr-xr-x | Besitzer schreibt, alle lesen und führen aus | Programme, Verzeichnisse |
| 750 | rwxr-x--- | wie 755, aber ohne die anderen | Verzeichnisse einer Abteilung |
## Prüfungsfrage
**Frage:** Im Verzeichnis `/srv/einkauf` sollen alle Mitglieder der Gruppe `einkauf` Dateien
anlegen und ändern dürfen, andere Benutzer gar nichts. Wie richten Sie das ein?
**Antwort:** `sudo chown root:einkauf /srv/einkauf` setzt die Gruppe; `sudo chmod 2770
/srv/einkauf` gibt Besitzer und Gruppe Lesen, Schreiben und Hineinwechseln und den anderen
nichts. Die vorangestellte 2 setzt das setgid-Bit: Neue Dateien im Verzeichnis gehören dann
automatisch der Gruppe `einkauf` statt der primären Gruppe ihres Erstellers – sonst könnten die
Kollegen eine neu angelegte Datei womöglich nicht ändern. Die Benutzer kommen mit `sudo usermod
-aG einkauf benutzer` in die Gruppe und müssen sich danach neu anmelden.
@@ -0,0 +1,63 @@
---
id: S8
titel: Linux – Pakete, Dienste und Fernzugang per SSH
block: system
woche: 13
worum: Ein Linux-Server wird über drei Werkzeuge betrieben – den Paketmanager, systemd und SSH. Wer die drei beherrscht, kann fast jeden Server pflegen.
rahmenplan: [B3, A6, A8]
lernfeld: LF9
merksatz: apt für Software, systemctl für Dienste, journalctl für Protokolle, SSH nur mit Schlüssel. Zweite Sitzung offen lassen, bevor man den Zugang ändert.
karten:
- frage: Was ist der Unterschied zwischen systemctl start und systemctl enable?
antwort: start startet den Dienst jetzt, enable sorgt dafür, dass er bei jedem Systemstart mitstartet. systemctl enable --now erledigt beides auf einmal.
- frage: Warum meldet man sich per SSH mit Schlüssel statt mit Passwort an?
antwort: Ein Schlüssel lässt sich nicht erraten und nicht durch Durchprobieren knacken, und der private Teil verlässt nie den eigenen Rechner. Danach kann die Anmeldung per Passwort am Server ganz abgeschaltet werden.
- frage: Warum lässt man beim Ändern der SSH-Konfiguration eine zweite Sitzung offen?
antwort: Weil man sich mit einem Fehler in der Konfiguration aussperren kann. Die offene Sitzung bleibt bestehen, und man kann den Fehler darüber beheben, bevor man sie schließt.
---
## Das Wesentliche
- **Pakete (Debian, Ubuntu):** `sudo apt update` holt die Paketlisten, `sudo apt upgrade`
spielt Aktualisierungen ein, `sudo apt install nginx` installiert. Sicherheitsupdates lassen
sich mit `unattended-upgrades` automatisch einspielen.
- **Software nur aus den Paketquellen**, nicht per heruntergeladenem Skript, wenn es sich
vermeiden lässt. Was der Paketmanager kennt, aktualisiert er auch.
- **Dienste mit systemd:** `systemctl status nginx` zeigt Zustand und letzte
Protokollzeilen, `start`, `stop`, `restart` steuern, `enable` startet ihn beim Hochfahren mit.
- **Protokolle:** `journalctl -u nginx` für einen Dienst, `-f` zum Mitlesen, `-b` seit dem
letzten Start, `--since "1 hour ago"` für einen Zeitraum, `-p err` nur Fehler.
- **SSH-Schlüssel:** Auf dem eigenen Rechner `ssh-keygen -t ed25519` erzeugen, mit
`ssh-copy-id benutzer@server` hinterlegen. Der private Schlüssel bleibt beim Benutzer und
bekommt eine Passphrase.
- **SSH absichern** in `/etc/ssh/sshd_config`: `PermitRootLogin no`,
`PasswordAuthentication no`. Danach `sudo systemctl reload ssh` – und **in einer zweiten
Sitzung testen**, bevor die erste geschlossen wird.
- **Firewall:** `sudo ufw allow OpenSSH`, danach nur die Dienste freigeben, die gebraucht
werden, und `sudo ufw enable`. Mit `ss -tlnp` prüfen, was tatsächlich lauscht.
- **Neustart nach Kernel-Updates:** Ob einer nötig ist, zeigt unter Debian und Ubuntu die
Datei `/var/run/reboot-required`.
## Übersicht
| Aufgabe | Befehl |
|---|---|
| Aktualisieren | `sudo apt update && sudo apt upgrade` |
| Dienst starten und dauerhaft einschalten | `sudo systemctl enable --now nginx` |
| Läuft der Dienst? | `systemctl status nginx` |
| Warum startet er nicht? | `journalctl -u nginx -b --no-pager \| tail -n 30` |
| Schlüssel erzeugen und hinterlegen | `ssh-keygen -t ed25519` und `ssh-copy-id anna@srv-web-01` |
| Firewall-Zustand | `sudo ufw status verbose` |
## Prüfungsfrage
**Frage:** Ein frisch installierter Linux-Server soll per SSH verwaltet werden. Beschreiben Sie
die Schritte, mit denen Sie den Zugang absichern, in der richtigen Reihenfolge.
**Antwort:** (1) Einen persönlichen Benutzer mit `sudo`-Recht anlegen, damit nicht als root
gearbeitet wird. (2) Auf dem eigenen Rechner ein Schlüsselpaar erzeugen und den öffentlichen
Schlüssel mit `ssh-copy-id` für diesen Benutzer hinterlegen. (3) Die Anmeldung mit dem Schlüssel
testen. (4) In `sshd_config` die direkte Anmeldung als root und die Anmeldung per Passwort
abschalten und den Dienst neu laden. (5) In einer zweiten Sitzung prüfen, dass die Anmeldung
weiterhin funktioniert, bevor die erste geschlossen wird. (6) Die Firewall so einrichten, dass
SSH nur aus dem Verwaltungsnetz erreichbar ist, und automatische Sicherheitsupdates einschalten.
+66
View File
@@ -0,0 +1,66 @@
---
id: S9
titel: Überwachung – wissen, dass etwas kaputt ist, bevor es jemand meldet
block: system
woche: 14
worum: Eine gute Überwachung meldet sich selten, aber dann zu Recht. Eine schlechte meldet sich ständig – und wird deshalb irgendwann ignoriert.
rahmenplan: [A8, A5, B3]
lernfeld: LF11
merksatz: Überwachen, was dem Nutzer wehtut. Warnen bei Trend, alarmieren bei Handlungsbedarf. Jeder Alarm braucht eine Handlung.
karten:
- frage: Was ist der Unterschied zwischen einer Warnung und einem kritischen Alarm?
antwort: Eine Warnung zeigt, dass sich etwas in eine falsche Richtung entwickelt und bald Handlung nötig wird – etwa Platte zu 80 Prozent voll. Ein kritischer Alarm verlangt sofortiges Handeln – etwa Platte zu 95 Prozent voll oder Dienst ausgefallen.
- frage: Was versteht man unter Alarmmüdigkeit, und wie vermeidet man sie?
antwort: Wer zu viele unwichtige Alarme bekommt, beachtet irgendwann auch die wichtigen nicht mehr. Abhilfe – nur alarmieren, wenn eine Handlung nötig ist, Schwellwerte nach Erfahrung anpassen, Warnungen gesammelt statt einzeln zustellen.
- frage: Warum überwacht man auch die Datensicherung und den Ablauf von Zertifikaten?
antwort: Weil beide still ausfallen. Eine Sicherung, die seit Wochen fehlschlägt, merkt man erst beim Zurückspielen; ein abgelaufenes Zertifikat erst, wenn alle Nutzer eine Fehlermeldung bekommen.
---
## Das Wesentliche
- **Von außen nach innen überwachen.** Zuerst, was der Nutzer merkt: Ist die Anwendung
erreichbar, antwortet sie schnell genug? Dann die Ursachen darunter: Dienste, Prozessor,
Arbeitsspeicher, Platte, Netz.
- **Was nicht fehlen darf:** Erreichbarkeit, wichtige Dienste, Plattenplatz, Ergebnis der
Datensicherung, Ablauf von Zertifikaten, Zustand von RAID und Platten (SMART), USV und
Temperatur im Serverraum.
- **Zwei Schwellen:** Warnung, wenn Zeit zum Handeln bleibt, kritisch, wenn sofort gehandelt
werden muss. Eine Platte, die von 70 auf 90 Prozent in einer Woche wächst, ist schlimmer als
eine, die seit einem Jahr bei 92 steht.
- **Trends ansehen.** Eine Kurve über Wochen sagt mehr als der aktuelle Wert. Sie zeigt, wann
die Platte voll sein wird, bevor sie es ist.
- **Jeder Alarm braucht eine Handlung** und einen Empfänger. Ein Alarm, auf den niemand
reagieren muss, ist eine Information und gehört in einen Bericht, nicht aufs Handy.
- **Alarmmüdigkeit** ist die größte Gefahr. Wer täglich zwanzig unwichtige Meldungen bekommt,
übersieht die eine wichtige.
- **Die Überwachung überwachen.** Fällt das Überwachungssystem aus, ist alles still – und Stille
sieht aus wie „alles in Ordnung". Ein externer Check oder ein täglicher Lebenszeichen-Alarm
hilft.
- **Werkzeuge:** Checkmk, Zabbix, PRTG für ganze Umgebungen, Uptime Kuma für einfache
Erreichbarkeitsprüfungen. Wichtiger als das Werkzeug ist, was man damit überwacht.
## Übersicht
| Prüfung | Warnung | Kritisch | Handlung |
|---|---|---|---|
| Plattenplatz | 80 % | 90 % | Aufräumen oder erweitern |
| Webanwendung erreichbar | Antwort über 2 s | keine Antwort | Dienst und Server prüfen |
| Datensicherung | ein Lauf fehlgeschlagen | zwei Läufe in Folge | Ursache finden, Lauf wiederholen |
| Zertifikat | läuft in 30 Tagen ab | läuft in 7 Tagen ab | Verlängern |
| RAID | – | Platte ausgefallen | Platte tauschen |
| Temperatur Serverraum | 27 °C | 32 °C | Klimaanlage prüfen |
## Prüfungsfrage
**Frage:** Ihr Ausbilder bittet Sie, eine Überwachung für den neuen Dateiserver einzurichten.
Nennen Sie fünf Prüfungen mit sinnvollen Schwellwerten und begründen Sie, warum eine
Überwachung nur des Prozessors nicht genügt.
**Antwort:** Fünf aus: (1) Erreichbarkeit per Ping und SMB – kritisch bei Ausfall. (2)
Plattenplatz je Laufwerk – Warnung 80 %, kritisch 90 %. (3) Ergebnis der nächtlichen Sicherung –
Warnung bei einem, kritisch bei zwei fehlgeschlagenen Läufen. (4) Zustand von RAID und SMART –
kritisch bei ausgefallener oder auffälliger Platte. (5) Arbeitsspeicher – Warnung über 85 %
über längere Zeit. (6) Dienst „Server" bzw. Freigabedienst läuft. (7) Ablauf von Zertifikaten,
falls verwendet. Der Prozessor allein genügt nicht, weil die häufigsten Ausfälle eines
Dateiservers gar nicht mit Rechenlast zusammenhängen: Eine volle Platte, ein ausgefallener
Dienst oder eine fehlgeschlagene Sicherung lassen die Prozessorlast unauffällig.
+79
View File
@@ -0,0 +1,79 @@
id: woche-13
woche: 13
titel: Lernzielkontrolle Woche 13 – Linux-Grundlagen
dauer_minuten: 40
hinweis: >-
Offene Fragen. Befehle dürfen mit oder ohne sudo geschrieben werden, wenn
aus der Antwort hervorgeht, dass Administratorrechte nötig sind.
fragen:
- id: f1
frage: >-
Nennen Sie die Verzeichnisse, in denen unter Linux (a) die Konfiguration
des SSH-Servers, (b) die Protokolldateien und (c) die Heimatverzeichnisse
der Benutzer liegen.
punkte: 3
musterloesung: >-
(a) /etc/ssh, die Datei heißt sshd_config. (b) /var/log, ergänzend das
Journal von systemd. (c) /home, für den Administrator /root. (Je 1 P.)
- id: f2
frage: >-
Die Ausgabe von ls -l lautet: -rwxr-x--- 1 lena technik pruefen.sh.
Erklären Sie die Rechte und geben Sie den passenden chmod-Befehl als Zahl an.
punkte: 5
musterloesung: >-
Es handelt sich um eine Datei (erstes Zeichen „-"). Besitzerin lena darf
lesen, schreiben und ausführen (rwx = 7). Die Gruppe technik darf lesen
und ausführen (r-x = 5). Alle anderen dürfen nichts (--- = 0). Befehl:
chmod 750 pruefen.sh. (Besitzer 1 P., Gruppe 1 P., andere 1 P., Befehl 2 P.)
- id: f3
frage: >-
Ein Benutzer soll zusätzlich in die Gruppe docker aufgenommen werden.
Ein Kollege schlägt sudo usermod -G docker anna vor. Was ist daran falsch?
punkte: 4
musterloesung: >-
Ohne die Option -a ersetzt -G alle bisherigen Zusatzgruppen durch die
angegebene – anna verlöre also etwa die Gruppe sudo oder ihre
Abteilungsgruppen. Richtig ist sudo usermod -aG docker anna. Die
Änderung wird erst nach erneuter Anmeldung wirksam. (Fehler 2 P., richtiger
Befehl 1 P., Hinweis auf Neuanmeldung 1 P.)
- id: f4
frage: >-
Der Webserver nginx startet nach einer Änderung nicht mehr. Mit welchen
zwei Befehlen finden Sie die Ursache, und was zeigen sie?
punkte: 4
musterloesung: >-
systemctl status nginx zeigt den Zustand des Dienstes und die letzten
Protokollzeilen, oft schon mit der Fehlermeldung. journalctl -u nginx -b
zeigt alle Meldungen des Dienstes seit dem letzten Systemstart.
Ebenfalls richtig: nginx -t prüft die Konfiguration und nennt Datei und
Zeile des Fehlers. (Je Befehl mit Erklärung 2 P.)
- id: f5
frage: >-
Beschreiben Sie, wie Sie einen Server so einrichten, dass die Anmeldung per
SSH nur noch mit Schlüssel möglich ist, und worauf Sie dabei achten, um sich
nicht auszusperren.
punkte: 6
musterloesung: >-
Auf dem eigenen Rechner mit ssh-keygen ein Schlüsselpaar erzeugen und den
öffentlichen Schlüssel mit ssh-copy-id beim Benutzer auf dem Server
hinterlegen. Die Anmeldung mit Schlüssel testen. Dann in
/etc/ssh/sshd_config PasswordAuthentication no und PermitRootLogin no
setzen und den Dienst neu laden. Gegen das Aussperren: die bestehende
Sitzung offen lassen und in einer zweiten Sitzung testen; außerdem den
Konsolenzugang über Hypervisor oder Fernwartungskarte als Notausgang
bereithalten. (Schlüssel 2 P., Konfiguration 2 P., Schutz vor Aussperren 2 P.)
- id: f6
frage: >-
Was unterscheidet systemctl start nginx von systemctl enable nginx?
punkte: 3
musterloesung: >-
start startet den Dienst sofort, ohne dass er beim nächsten Systemstart
wieder mitstartet. enable sorgt dafür, dass er bei jedem Systemstart
gestartet wird, startet ihn aber nicht sofort. enable --now erledigt beides.
(Je Befehl 1 P., enable --now 1 P.)
+94
View File
@@ -0,0 +1,94 @@
id: woche-15
woche: 15
titel: Lernzielkontrolle Woche 15 – Serviceanfragen bearbeiten
dauer_minuten: 40
hinweis: >-
Offene Fragen. Es zählt die Begründung – bei Priorisierungen sind auch
abweichende Einstufungen richtig, wenn sie schlüssig begründet sind.
fragen:
- id: f1
frage: >-
Ordnen Sie zu und begründen Sie kurz, ob es sich um eine Störung, eine
Serviceanfrage, ein Problem oder eine Änderung handelt: (a) Ein neuer
Mitarbeiter braucht ein Konto. (b) Das VPN bricht bei allen
Außendienstlern seit drei Wochen jeden Montag ab. (c) Die Firewall soll eine
neue Firmware bekommen. (d) Ein Nutzer kann keine Mails mehr senden.
punkte: 8
musterloesung: >-
(a) Serviceanfrage – nichts ist kaputt, verlangt wird eine vorgesehene
Standardleistung. (b) Problem – eine wiederkehrende Ursache hinter vielen
Störungen, die dauerhaft beseitigt werden muss; die einzelnen Meldungen
sind jeweils Störungen. (c) Änderung – ein geplanter Eingriff in ein
laufendes System, der bewertet, freigegeben und mit Rückweg umgesetzt
wird. (d) Störung – eine ungeplante Beeinträchtigung, Ziel ist die schnelle
Wiederherstellung. (Je Zuordnung mit Begründung 2 P.)
- id: f2
frage: >-
Erklären Sie, wie die Priorität eines Tickets ermittelt wird, und wenden Sie
das auf folgenden Fall an: Der Server mit der Lohnbuchhaltung ist am
Tag vor der Gehaltsabrechnung nicht erreichbar.
punkte: 6
musterloesung: >-
Die Priorität ergibt sich aus Auswirkung (wie viele Nutzer bzw. wie
wichtige Abläufe) und Dringlichkeit (wie schnell entsteht Schaden, gibt es
eine Umgehung), die in einer Matrix verknüpft werden. Fall: Die Auswirkung
ist hoch, weil die Gehaltszahlung aller Beschäftigten betroffen ist und
gesetzliche bzw. vertragliche Fristen drohen. Die Dringlichkeit ist hoch,
weil die Abrechnung am nächsten Tag laufen muss und es keine Umgehung
gibt. Ergebnis: höchste Priorität (P1). (Verfahren 2 P., Auswirkung 2 P.,
Dringlichkeit und Ergebnis 2 P.)
- id: f3
frage: >-
Was ist der Unterschied zwischen funktionaler und hierarchischer
Eskalation? Nennen Sie je ein Beispiel.
punkte: 4
musterloesung: >-
Funktional: Weitergabe an eine Stelle mit mehr Fachwissen, z. B. vom
ersten Level an den Netzwerkadministrator, weil die Ursache im Switch
liegt. Hierarchisch: Einbeziehung der Führungsebene, z. B. weil die
vereinbarte Lösungszeit nicht zu halten ist oder für die Lösung eine
Anschaffung freigegeben werden muss. (Je Begriff mit Beispiel 2 P.)
- id: f4
frage: >-
Was ist ein Workaround, und warum wird ein Ticket geschlossen, sobald er
greift, obwohl die Ursache noch besteht?
punkte: 4
musterloesung: >-
Ein Workaround ist eine Umgehung, mit der der Nutzer weiterarbeiten kann,
obwohl die Ursache noch nicht beseitigt ist. Die Störung wird geschlossen,
weil ihr Ziel – die Arbeitsfähigkeit des Nutzers – erreicht ist. Die
Ursache wird getrennt im zugehörigen Problem weiterverfolgt; so bleibt
sichtbar, dass noch etwas offen ist, ohne dass das Ticket des Nutzers
wochenlang offen steht. (Begriff 2 P., Begründung 2 P.)
- id: f5
frage: >-
Nennen Sie fünf Angaben, die vor einer normalen Änderung feststehen
müssen.
punkte: 5
musterloesung: >-
Fünf aus: Beschreibung der Änderung; Grund bzw. Ziel; betroffene Systeme
und Nutzer; Risikoeinschätzung; Rückweg; Sicherung bzw. Snapshot vorher;
Freigabe durch die zuständige Person; Zeitpunkt bzw. Wartungsfenster;
Ankündigung an die Betroffenen; Verantwortlicher für die Umsetzung;
Kriterien, an denen der Erfolg geprüft wird. (Je Angabe 1 P.)
- id: f6
frage: >-
Formulieren Sie den Titel und die Gliederung eines Wissensartikels zu
folgender Störung: Nutzer können nach dem Kennwortwechsel nicht mehr auf
ihr Netzlaufwerk zugreifen, weil Windows noch das alte Kennwort gespeichert
hat.
punkte: 5
musterloesung: >-
Titel nach dem Symptom in Nutzersprache, z. B. „Netzlaufwerk nach
Kennwortwechsel nicht erreichbar – Zugriff verweigert". Gliederung: Gilt
für (Windows-Version, betroffene Laufwerke); Symptom mit Fehlermeldung im
Wortlaut; Ursache (altes Kennwort in der Anmeldeinformationsverwaltung
gespeichert); Lösung in Schritten (Eintrag entfernen, neu anmelden bzw.
Laufwerk neu verbinden); Prüfen; Stand und Verfasser. (Titel 2 P.,
Gliederung 3 P.)
+202
View File
@@ -0,0 +1,202 @@
id: woche-16
woche: 16
titel: Probeprüfung Teil 1 – Einrichten eines IT-gestützten Arbeitsplatzes
dauer_minuten: 90
hinweis: >-
Nachempfunden dem Aufbau von Teil 1 der Abschlussprüfung, keine Original-
aufgabe der Kammer. Ausgangslage für alle Aufgaben: Die Muster GmbH, ein
Betrieb mit 130 Beschäftigten, richtet für ihre neue Abteilung
Qualitätssicherung fünf Bildschirmarbeitsplätze und ein Abteilungs-NAS ein.
Sie unterstützen die IT-Abteilung. Hilfsmittel – ein nicht
programmierbarer Taschenrechner. Bei Rechenaufgaben zählt der Rechenweg.
fragen:
- id: f1
frage: >-
Die Abteilungsleiterin sagt: „Wir brauchen schnelle, moderne Rechner."
Nennen Sie drei Fragen, die Sie ihr stellen, bevor Sie Angebote einholen,
und formulieren Sie eine der Antworten als prüfbare Muss-Anforderung.
punkte: 8
musterloesung: >-
Drei aus: Welche Anwendungen laufen darauf (z. B. Mess- oder
CAD-Software und deren Systemanforderungen)? Werden große Datenmengen
verarbeitet oder Messgeräte angeschlossen – welche Schnittstellen? Wird
mobil gearbeitet? Wie viele Bildschirme, welche Größe? Budget und
Termin? Gibt es Vorgaben zu Lautstärke oder Datenschutz? (Je Frage 2 P.)
Beispiel einer prüfbaren Muss-Anforderung: „Die Messsoftware öffnet ein
Prüfprotokoll mit 10.000 Messpunkten in weniger als 5 Sekunden." oder
„Jeder Arbeitsplatz hat zwei 27-Zoll-Bildschirme mit mindestens 2560 × 1440
Bildpunkten." (2 P., nur wenn prüfbar formuliert)
- id: f2
frage: >-
Für die fünf Arbeitsplätze werden je ein Bildschirm benötigt. Ein Händler
bietet ihn zum Listenpreis von 289,00 € netto je Stück an, gewährt 12 %
Rabatt und 2 % Skonto bei Zahlung innerhalb von 10 Tagen. Die
Versandkosten betragen pauschal 19,90 €. Berechnen Sie den Bezugspreis für
alle fünf Bildschirme.
punkte: 10
musterloesung: >-
Listeneinkaufspreis 5 × 289,00 € = 1.445,00 €. Rabatt 12 % = 173,40 €,
Zieleinkaufspreis 1.271,60 €. Skonto 2 % vom Zieleinkaufspreis = 25,43 €,
Bareinkaufspreis 1.246,17 €. Bezugskosten 19,90 €. Bezugspreis
1.266,07 € netto. (Listenpreis 1 P., Rabatt und Zieleinkaufspreis 3 P.,
Skonto vom richtigen Betrag und Bareinkaufspreis 3 P., Bezugspreis 3 P.)
- id: f3
frage: >-
Je Arbeitsplatz nehmen Rechner und Bildschirm im Betrieb zusammen 58 W auf.
Gearbeitet wird 8 Stunden an 220 Tagen im Jahr. (a) Berechnen Sie die
jährlichen Stromkosten für alle fünf Arbeitsplätze bei 0,32 € je kWh.
(b) Bisher bleiben die Rechner nach Feierabend an den Arbeitstagen
16 Stunden mit 25 W im Leerlauf eingeschaltet. Wie hoch sind die
zusätzlichen Kosten im Jahr?
punkte: 10
musterloesung: >-
(a) 0,058 kW × 8 h × 220 = 102,08 kWh je Arbeitsplatz; × 5 = 510,4 kWh;
× 0,32 €/kWh = 163,33 €. (5 P.) (b) 0,025 kW × 16 h × 220 = 88 kWh je
Arbeitsplatz; × 5 = 440 kWh; × 0,32 €/kWh = 140,80 €. (5 P.) Je Teilaufgabe:
Umrechnung in kW 1 P., Stunden 1 P., kWh 1 P., Kosten 2 P.
- id: f4
frage: >-
Die Abteilung bekommt ein eigenes VLAN. Aus dem Bereich 10.20.40.0/24 soll
ein Subnetz für höchstens 25 Geräte zuzüglich Gateway gebildet werden, so
klein wie möglich. Geben Sie Präfix, Netzadresse, Broadcastadresse, den
nutzbaren Bereich und eine Gateway-Adresse an. Begründen Sie das Präfix.
punkte: 10
musterloesung: >-
26 Adressen für Geräte und Gateway plus Netz- und Broadcastadresse ergeben
28; die nächste Blockgröße ist 32, also /27 (Maske 255.255.255.224) mit 30
nutzbaren Adressen; /28 hätte nur 14. Netzadresse 10.20.40.0,
Broadcast 10.20.40.31, nutzbar 10.20.40.1 bis 10.20.40.30, Gateway z. B.
10.20.40.1. (Begründung und Präfix 4 P., Netz 1 P., Broadcast 2 P.,
Bereich 2 P., Gateway 1 P.)
- id: f5
frage: >-
Das Abteilungs-NAS erhält vier Festplatten mit je 6 TB. Vergleichen Sie
RAID 5 und RAID 10 hinsichtlich nutzbarer Kapazität und Ausfallsicherheit
und erklären Sie, warum das NAS trotzdem gesichert werden muss.
punkte: 8
musterloesung: >-
RAID 5: (4 − 1) × 6 TB = 18 TB nutzbar, eine beliebige Platte darf
ausfallen. RAID 10: 4 / 2 × 6 TB = 12 TB nutzbar, je Spiegelpaar darf eine
Platte ausfallen – fallen beide Platten desselben Paares aus, sind die
Daten verloren. (Je Stufe 3 P.) Ein RAID schützt nur gegen den Ausfall von
Platten, nicht gegen Löschen, Verschlüsselung durch Schadsoftware,
Bedienfehler oder den Verlust des ganzen Geräts; diese Fehler übernimmt es
sofort auf alle Platten. (2 P.)
- id: f6
frage: >-
In der Qualitätssicherung entstehen Prüfprotokolle, mit denen der Betrieb
gegenüber Kunden nachweist, dass Teile maßhaltig sind. (a) Bestimmen Sie
den Schutzbedarf der Prüfprotokolle für die drei Schutzziele mit
Begründung. (b) Nennen Sie je eine Maßnahme für das Schutzziel mit dem
höchsten Bedarf und für die Anmeldung an den Arbeitsplätzen.
punkte: 10
musterloesung: >-
(a) Integrität hoch bis sehr hoch – verfälschte Protokolle können zu
Haftung, Vertragsstrafen und Rückrufen führen. Verfügbarkeit normal bis
hoch – ein kurzer Ausfall verzögert, ein Verlust der Protokolle verhindert
den Nachweis gegenüber Kunden. Vertraulichkeit normal bis hoch – sie
können Konstruktions- und Kundendaten enthalten. Abweichende Einstufungen
zählen bei schlüssiger Begründung. (Je Schutzziel 2 P.) (b) Integrität:
Schreibrechte nur für die Prüfer, Protokolle nach Freigabe schreibgeschützt
ablegen, Änderungen protokollieren, Sicherung mit Generationen. Anmeldung:
persönliche Konten mit zweitem Faktor bzw. Smartcard, Bildschirmsperre.
(Je Maßnahme 2 P.)
- id: f7
frage: >-
Die fünf bisherigen Rechner der Abteilung sollen entsorgt werden. Auf ihren
Festplatten liegen Prüfprotokolle mit Namen von Kunden und Prüfern.
Beschreiben Sie das richtige Vorgehen.
punkte: 8
musterloesung: >-
(1) Prüfen, ob noch Daten gebraucht werden, und sie ggf. sichern. (2) Die
Datenträger ausbauen und nachweisbar löschen oder vernichten – nach DIN
66399, für personenbezogene Daten mindestens Sicherheitsstufe 3; ein
Formatieren genügt nicht. (3) Die Vernichtung mit Seriennummern
protokollieren bzw. das Vernichtungszertifikat des Dienstleisters ablegen.
(4) Die Geräte über einen zertifizierten Entsorger oder eine Rücknahmestelle
nach dem ElektroG abgeben, nicht in den Restmüll. (5) Die Geräte in der
Geräteliste als ausgesondert kennzeichnen. Ebenfalls richtig: Weitergabe zur
Wiederverwendung nach sicherer Löschung. (2 P. für Löschen/Vernichten mit
Norm, 2 P. Nachweis, 2 P. Entsorgung nach ElektroG, 2 P. für weitere
Schritte)
- id: f8
frage: >-
Ein Messgerät liefert eine Liste von Messwerten in Millimetern. Ein Teil ist
in Ordnung, wenn der Wert zwischen 24,95 und 25,05 liegt, Grenzen
eingeschlossen. Schreiben Sie einen Pseudocode, der die Anzahl der Teile
außerhalb der Toleranz und den größten gemessenen Wert ausgibt. Führen Sie
einen Schreibtischtest mit den Werten 25,01; 24,90; 25,06 durch.
punkte: 12
musterloesung: >-
ausserhalb ← 0; groesster ← werte[0]; FÜR jeden wert IN werte: WENN wert
< 24,95 ODER wert > 25,05 DANN ausserhalb ← ausserhalb + 1; WENN wert >
groesster DANN groesster ← wert; ENDE FÜR; AUSGABE ausserhalb, groesster.
Schreibtischtest: Start ausserhalb 0, groesster 25,01. Wert 25,01 – in
Toleranz, groesster bleibt 25,01. Wert 24,90 – außerhalb, ausserhalb 1.
Wert 25,06 – außerhalb, ausserhalb 2, groesster 25,06. Ausgabe 2 und 25,06.
(Initialisierung 2 P., Schleife 2 P., Bedingung mit ODER und richtigen
Grenzen 3 P., Maximum 2 P., Schreibtischtest 3 P.)
- id: f9
frage: >-
Eine Mitarbeiterin der Abteilung klagt nach einer Woche über
Nackenschmerzen und Blendung. Ihr Bildschirm steht vor dem Fenster.
Nennen Sie vier Maßnahmen und die Vorschrift, die die Anforderungen an
Bildschirmarbeitsplätze regelt.
punkte: 8
musterloesung: >-
Vier aus: Arbeitsplatz so drehen, dass die Blickrichtung parallel zum
Fenster verläuft; Bildschirm so einstellen, dass die oberste Zeile auf oder
knapp unter Augenhöhe liegt; Abstand etwa eine Armlänge; Hauptbildschirm
direkt vor den Körper; Stuhl und Tischhöhe anpassen; Sonnenschutz am
Fenster; Hinweis auf die Angebotsvorsorge für die Augen. (Je Maßnahme
1,5 P., zusammen 6 P.) Vorschrift: Arbeitsstättenverordnung, Anhang Nr. 6.
(2 P.)
- id: f10
frage: >-
Ein externer Dienstleister richtet das NAS ein. Um welche Vertragsart
handelt es sich, und welche Folgen hat die Abnahme?
punkte: 6
musterloesung: >-
Werkvertrag, weil ein Erfolg geschuldet ist – ein eingerichtetes,
funktionierendes NAS. (2 P.) Folgen der Abnahme (vier aus, je 1 P.): Die
Vergütung wird fällig; die Verjährungsfrist für Mängelansprüche beginnt;
die Gefahr geht auf den Auftraggeber über; bekannte, nicht vorbehaltene
Mängel können später nur eingeschränkt geltend gemacht werden; Restmängel
werden im Protokoll festgehalten.
- id: f11
frage: >-
Am ersten Arbeitstag gehen zwei Tickets ein: (a) Eine Prüferin kann die
Messsoftware nicht starten, die Abteilung kann deshalb keine Teile
freigeben. (b) Ein Prüfer möchte ein anderes Hintergrundbild. Ordnen Sie
Art und Priorität zu und begründen Sie.
punkte: 5
musterloesung: >-
(a) Störung mit hoher Priorität – hohe Auswirkung, weil ohne Freigabe der
Ablauf der Abteilung steht, und hohe Dringlichkeit, weil Teile nicht
ausgeliefert werden können. (3 P.) (b) Serviceanfrage mit geringer
Priorität – nichts ist beeinträchtigt, die Anfrage wird nach dem üblichen
Ablauf bzw. den Vorgaben des Betriebs erledigt oder abgelehnt. (2 P.)
- id: f12
frage: >-
Die Abteilungsleiterin verkraftet höchstens einen Arbeitstag Datenverlust.
Wie oft muss das NAS mindestens gesichert werden, und welche Regel gilt
für die Aufbewahrung der Kopien?
punkte: 5
musterloesung: >-
Mindestens einmal täglich, z. B. jede Nacht nach Arbeitsende, weil das
RPO einen Arbeitstag beträgt. (2 P.) Für die Aufbewahrung gilt die
3-2-1-Regel: drei Kopien auf zwei verschiedenen Medien, eine davon außer
Haus; zusätzlich eine Kopie offline oder unveränderlich gegen
Schadsoftware. (3 P.)
+30
View File
@@ -0,0 +1,30 @@
nummer: 13
titel: Linux-Grundlagen
block: system
leitfrage: Wo steht unter Linux, warum etwas nicht funktioniert?
ziel: >-
Du kannst dich auf der Linux-Kommandozeile sicher bewegen, Benutzer und
Dateirechte richtig setzen, Software und Dienste verwalten und einen Server
so einrichten, dass er nur per SSH-Schlüssel erreichbar ist.
handouts: [S6, S7, S8]
aufgaben: ["13.1"]
checklisten: [10-linux-server-einrichten]
lernzielkontrolle: woche-13
tage:
- tag: Montag
theorie: [S6]
praxis: Auf einem Testsystem zwanzig Fragen mit Befehlen beantworten – wo liegt was, wie voll ist was, was steht im Protokoll.
- tag: Dienstag
theorie: [S7]
praxis: Benutzer, Gruppen und ein gemeinsames Verzeichnis für eine Abteilung anlegen und mit zwei Konten prüfen.
- tag: Mittwoch
theorie: [S8]
praxis: Praxisaufgabe 13.1 beginnen – VM planen, installieren, Benutzer und Schlüssel einrichten.
- tag: Donnerstag
theorie: []
praxis: Praxisaufgabe 13.1 abschließen – SSH absichern, Firewall, nginx, Fehlersuche mit journalctl.
- tag: Freitag
theorie: []
praxis: Lernzielkontrolle, danach Berichtsheft schreiben.
@@ -0,0 +1,30 @@
nummer: 14
titel: Betrieb und Überwachung
block: system
leitfrage: Wer merkt zuerst, dass etwas nicht stimmt – die IT oder der Nutzer?
ziel: >-
Du kannst eine Überwachung mit begründeten Schwellwerten einrichten, eine
Störung anhand der Protokolle zeitlich eingrenzen und Updates so planen, dass
sie zuverlässig ankommen, ohne den Betrieb zu stören.
handouts: [S9, S10, S11]
aufgaben: ["14.1"]
checklisten: []
lernzielkontrolle: null
tage:
- tag: Montag
theorie: [S9]
praxis: Die Überwachung des Betriebs mit dem Ausbilder durchgehen – was wird überwacht, welche Alarme kamen im letzten Monat, welche davon waren nützlich.
- tag: Dienstag
theorie: [S10]
praxis: Drei echte Störungen aus dem Ticketsystem nehmen und in den Protokollen nachvollziehen, wann und warum sie auftraten.
- tag: Mittwoch
theorie: [S11]
praxis: Den Update-Stand aller Arbeitsplätze und Netzwerkgeräte erheben, Geräte ohne aktuellen Stand und mit End of Life auflisten.
- tag: Donnerstag
theorie: []
praxis: Praxisaufgabe 14.1 – Prüfungen und Schwellen festlegen, einrichten, Ausfall testen, dokumentieren.
- tag: Freitag
theorie: []
praxis: Ergebnis der Bestandsaufnahme vom Mittwoch dem Ausbilder vorstellen, Berichtsheft schreiben.
+30
View File
@@ -0,0 +1,30 @@
nummer: 15
titel: Serviceanfragen bearbeiten
block: arbeitsalltag
leitfrage: Welches Ticket ist wirklich das dringendste – und woran erkennst du das?
ziel: >-
Du kannst Störungen, Serviceanfragen, Probleme und Änderungen unterscheiden,
Tickets nachvollziehbar priorisieren und eskalieren und Lösungen so
festhalten, dass andere sie wiederfinden.
handouts: [A10, A11, A12]
aufgaben: ["15.1"]
checklisten: [11-aenderung-durchfuehren]
lernzielkontrolle: woche-15
tage:
- tag: Montag
theorie: [A10]
praxis: Vormittags Praxisaufgabe 15.1 – erster Level im Ticketsystem. Nachmittags zwanzig geschlossene Tickets nach Art einordnen.
- tag: Dienstag
theorie: [A11]
praxis: Vormittags Praxisaufgabe 15.1. Nachmittags die Prioritätenmatrix des Betriebs mit dem Ausbilder durchgehen und an zehn Beispielen anwenden.
- tag: Mittwoch
theorie: [A12]
praxis: Vormittags Praxisaufgabe 15.1. Nachmittags eine geplante Änderung nach Checkliste vorbereiten und vom Ausbilder freigeben lassen.
- tag: Donnerstag
theorie: []
praxis: Vormittags Praxisaufgabe 15.1 abschließen. Nachmittags zwei Wissensartikel schreiben und das Problem anlegen.
- tag: Freitag
theorie: []
praxis: Lernzielkontrolle, danach Berichtsheft schreiben.
@@ -0,0 +1,31 @@
nummer: 16
titel: Den Arbeitsplatz als Ganzes – Vorbereitung auf Teil 1
block: hardware
leitfrage: Was gehört zu einem Arbeitsplatz außer dem Rechner?
ziel: >-
Du kannst einen Bildschirmarbeitsplatz ergonomisch beurteilen, Energiekosten
berechnen und Nachhaltigkeit bei Beschaffung und Entsorgung berücksichtigen,
Abläufe in Pseudocode beschreiben und eine Aufgabe im Stil von Teil 1 der
Abschlussprüfung in der vorgegebenen Zeit bearbeiten.
handouts: [H4, H5, P4]
aufgaben: ["16.1"]
checklisten: [12-bildschirmarbeitsplatz-pruefen]
lernzielkontrolle: woche-16
tage:
- tag: Montag
theorie: [H4]
praxis: Den eigenen Arbeitsplatz nach Checkliste prüfen und einstellen, danach Praxisaufgabe 16.1 beginnen.
- tag: Dienstag
theorie: [H5]
praxis: Praxisaufgabe 16.1 – Messungen und Hochrechnung, zehn Übungsaufgaben zu Energiekosten.
- tag: Mittwoch
theorie: [P4]
praxis: Fünf Abläufe aus dem Arbeitsalltag in Pseudocode beschreiben und je einen Schreibtischtest machen. Praxisaufgabe 16.1 abschließen.
- tag: Donnerstag
theorie: []
praxis: Probeprüfung Teil 1 unter Prüfungsbedingungen – 90 Minuten, ohne Hilfsmittel außer Taschenrechner.
- tag: Freitag
theorie: []
praxis: Monatliches Feedbackgespräch mit Besprechung der Probeprüfung, danach Berichtsheft schreiben.
+16 -6
View File
@@ -1,6 +1,6 @@
# Abnahmeprotokoll # Abnahmeprotokoll
Erzeugt am 2026-10-04 16:24 Uhr Erzeugt am 2026-10-05 14:48 Uhr
Erzeugt von `scripts/pruefungen/abnahme.mjs` gegen eine **frische, leere Datenbank**. Erzeugt von `scripts/pruefungen/abnahme.mjs` gegen eine **frische, leere Datenbank**.
@@ -54,15 +54,25 @@ Erzeugt von `scripts/pruefungen/abnahme.mjs` gegen eine **frische, leere Datenba
- ✓ Der Azubi legt das Wochenblatt der laufenden Woche an - ✓ Der Azubi legt das Wochenblatt der laufenden Woche an
- ✓ Ein Wochenblatt lässt sich öffnen - ✓ Ein Wochenblatt lässt sich öffnen
- ✓ Das Wochenblatt kommt als PDF heraus — HTTP 200, 2649 Bytes - ✓ Das Wochenblatt kommt als PDF heraus — HTTP 200, 2646 Bytes
## A8 · Sicherung zurückspielen ## A8 · Module abschalten
- ✓ Die Sicherung läuft durch — lernbar-20261004-182433.tar.gz - ✓ Der Ausbilder schaltet das Berichtsheft ab
- ✓ Das Berichtsheft verschwindet aus der Navigation
- ✓ Die Seite ist für beide Rollen gesperrt, nicht nur ausgeblendet — Azubi HTTP 404, Ausbilder HTTP 404
- ✓ Auch das PDF ist gesperrt — HTTP 404
- ✓ Ohne Merkkarten zeigt das Handout keine Karten
- ✓ Ein Gerät mit gesammelten Bewertungen erfährt, dass es sie verwerfen soll — HTTP 410
- ✓ Nach dem Einschalten ist das Wochenblatt unverändert wieder da — HTTP 200
## A9 · Sicherung zurückspielen
- ✓ Die Sicherung läuft durch — lernbar-20261005-164818.tar.gz
- ✓ Zum Beweis wird der Datenbestand beschädigt — 2 → 1 Konten - ✓ Zum Beweis wird der Datenbestand beschädigt — 2 → 1 Konten
- ✓ Die Rückspielung läuft durch — ohne Fehler - ✓ Die Rückspielung läuft durch — ohne Fehler
- ✓ Der gelöschte Azubi ist wieder da — Tim Vogt - ✓ Der gelöschte Azubi ist wieder da — Tim Vogt
- ✓ Auch der Lernverlauf kam mit zurück — 1 Einträge - ✓ Auch der Lernverlauf kam mit zurück — 1 Einträge
- ✓ Der überschriebene Stand wurde zur Seite gelegt, nicht gelöscht — verdraengt-20261004-182436 - ✓ Der überschriebene Stand wurde zur Seite gelegt, nicht gelöscht — verdraengt-20261005-164821
**36 von 36 Prüfungen erfüllt.** **43 von 43 Prüfungen erfüllt.**
+64 -24
View File
@@ -22,24 +22,25 @@ gemalt, sondern gemessen.
6. [Eine Lernzielkontrolle korrigieren](#6-eine-lernzielkontrolle-korrigieren) 6. [Eine Lernzielkontrolle korrigieren](#6-eine-lernzielkontrolle-korrigieren)
7. [Die Rahmenplanabdeckung lesen](#7-die-rahmenplanabdeckung-lesen) 7. [Die Rahmenplanabdeckung lesen](#7-die-rahmenplanabdeckung-lesen)
8. [Ein Feedbackgespräch führen](#8-ein-feedbackgespräch-führen) 8. [Ein Feedbackgespräch führen](#8-ein-feedbackgespräch-führen)
9. [Module ein- und ausschalten](#9-module-ein--und-ausschalten)
**Für den Azubi** **Für den Azubi**
9. [Die Woche](#9-die-woche) 10. [Die Woche](#10-die-woche)
10. [Handouts lesen und suchen](#10-handouts-lesen-und-suchen) 11. [Handouts lesen und suchen](#11-handouts-lesen-und-suchen)
11. [Merkkarten lernen](#11-merkkarten-lernen) 12. [Merkkarten lernen](#12-merkkarten-lernen)
12. [Eine Praxisaufgabe abgeben](#12-eine-praxisaufgabe-abgeben) 13. [Eine Praxisaufgabe abgeben](#13-eine-praxisaufgabe-abgeben)
13. [Eine Checkliste abarbeiten](#13-eine-checkliste-abarbeiten) 14. [Eine Checkliste abarbeiten](#14-eine-checkliste-abarbeiten)
14. [Das Berichtsheft führen](#14-das-berichtsheft-führen) 15. [Das Berichtsheft führen](#15-das-berichtsheft-führen)
15. [Eine Lernzielkontrolle schreiben](#15-eine-lernzielkontrolle-schreiben) 16. [Eine Lernzielkontrolle schreiben](#16-eine-lernzielkontrolle-schreiben)
**Überall** **Überall**
16. [Auf dem Handy](#16-auf-dem-handy) 17. [Auf dem Handy](#17-auf-dem-handy)
17. [Ohne Netz](#17-ohne-netz) 18. [Ohne Netz](#18-ohne-netz)
18. [Hell und dunkel](#18-hell-und-dunkel) 19. [Hell und dunkel](#19-hell-und-dunkel)
19. [Drucken](#19-drucken) 20. [Drucken](#20-drucken)
20. [Wenn etwas nicht geht](#20-wenn-etwas-nicht-geht) 21. [Wenn etwas nicht geht](#21-wenn-etwas-nicht-geht)
--- ---
@@ -218,11 +219,49 @@ Unterschriftenfeldern. Abweichungen sind auch dort markiert.
> anpassen. Ein bereits geführtes Gespräch behält seine Fassung, auch wenn du > anpassen. Ein bereits geführtes Gespräch behält seine Fassung, auch wenn du
> die Vorlage später änderst. > die Vorlage später änderst.
### 9. Module ein- und ausschalten
**Verwaltung → Module**
![Verwaltung, Bereich Module, das Berichtsheft ist abgeschaltet](bilder/25-module.png)
Nicht jeder Betrieb braucht alles. Führt ihr das Berichtsheft zum Beispiel im
Portal der Kammer, schaltest du es hier ab – für die ganze Lern-Bar, für alle
Azubis und Ausbilder zugleich.
| Modul | Abschaltbar |
|---|---|
| Merkkarten, Praxisaufgaben, Checklisten, Berichtsheft, Lernzielkontrollen, Feedbackgespräch | ja |
| Wochenplan, Handouts, Glossar, Auswertung, Verwaltung | nein – das Gerüst |
Was beim Abschalten passiert:
- **Das Modul verschwindet** aus der Navigation, aus dem Wochenplan, aus „Was
auf dich wartet" und aus der Auswertung. Zu jedem Modul steht auf der Seite,
was sich genau ändert.
- **Seiten und PDFs sind gesperrt**, nicht nur ausgeblendet. Wer sich die
Adresse gemerkt hat, bekommt „Diese Seite gibt es nicht".
- **Der Wochenfortschritt** zählt das Modul nicht mehr mit. Eine Woche ohne
Merkkarten erreicht also trotzdem 100 %.
- **Die Rahmenplanabdeckung** zählt abgeschaltete Aufgaben und Checklisten
nicht mehr. Bleiben nur Handouts, gilt eine Position mit zwei gelesenen
Handouts als vermittelt.
- **Nichts wird gelöscht.** Was im Modul schon geschrieben wurde – Berichtshefte,
Bewertungen, Abnahmen –, bleibt gespeichert und ist nach dem Einschalten
unverändert wieder da.
Unter jedem Modul steht, wer es wann zuletzt umgeschaltet hat.
> **Berichtsheft abschalten heißt nicht, kein Berichtsheft zu führen.** Der
> Ausbildungsnachweis ist Pflicht und Voraussetzung für die Zulassung zur
> Abschlussprüfung. Er wird dann eben woanders geführt. In den Wochenplänen
> steht freitags weiterhin „Berichtsheft schreiben" – das gilt auch dann.
--- ---
## Für den Azubi ## Für den Azubi
### 9. Die Woche ### 10. Die Woche
![Woche des Azubis](bilder/10-azubi-woche.png) ![Woche des Azubis](bilder/10-azubi-woche.png)
@@ -232,7 +271,7 @@ Frage lautet „Was mache ich jetzt?", nicht „Wie ist meine Gesamtlage?".
--- ---
### 10. Handouts lesen und suchen ### 11. Handouts lesen und suchen
![Handouts](bilder/11-handouts-liste.png) ![Handouts](bilder/11-handouts-liste.png)
@@ -254,7 +293,7 @@ mit Querverweisen.
--- ---
### 11. Merkkarten lernen ### 12. Merkkarten lernen
![Merkkarten](bilder/14-merkkarten.png) ![Merkkarten](bilder/14-merkkarten.png)
@@ -273,7 +312,7 @@ nachzuholen.
--- ---
### 12. Eine Praxisaufgabe abgeben ### 13. Eine Praxisaufgabe abgeben
![Aufgaben](bilder/16-aufgaben-liste.png) ![Aufgaben](bilder/16-aufgaben-liste.png)
@@ -287,7 +326,7 @@ Dann gibst du ab. Der Ausbilder nimmt ab oder gibt zurück, mit Bemerkung.
--- ---
### 13. Eine Checkliste abarbeiten ### 14. Eine Checkliste abarbeiten
![Checklisten](bilder/18-checklisten.png) ![Checklisten](bilder/18-checklisten.png)
@@ -304,7 +343,7 @@ begonnen hast. Am Ende gibt es ein PDF – ein Beispiel liegt unter
--- ---
### 14. Das Berichtsheft führen ### 15. Das Berichtsheft führen
![Berichtsheft](bilder/21-berichtsheft.png) ![Berichtsheft](bilder/21-berichtsheft.png)
@@ -323,7 +362,7 @@ Nach dem Abzeichnen ist das Blatt unveränderlich.
--- ---
### 15. Eine Lernzielkontrolle schreiben ### 16. Eine Lernzielkontrolle schreiben
Der Ausbilder gibt die Kontrolle frei, du siehst sie unter *Kontrollen*. Du Der Ausbilder gibt die Kontrolle frei, du siehst sie unter *Kontrollen*. Du
schreibst deine Antworten und sendest ab. schreibst deine Antworten und sendest ab.
@@ -336,7 +375,7 @@ Kontrolle, deren Lösung vorher abrufbar wäre, misst nichts.
## Überall ## Überall
### 16. Auf dem Handy ### 17. Auf dem Handy
Die Anwendung ist für das Handy gebaut, nicht dafür angepasst. Der Azubi hat es Die Anwendung ist für das Handy gebaut, nicht dafür angepasst. Der Azubi hat es
in der Tasche, nicht den Laptop. in der Tasche, nicht den Laptop.
@@ -355,7 +394,7 @@ Danach startet sie ohne Adresszeile und mit eigenem Symbol.
--- ---
### 17. Ohne Netz ### 18. Ohne Netz
Im Serverraum, im Keller, im Zug gibt es kein Netz. Deshalb: Im Serverraum, im Keller, im Zug gibt es kein Netz. Deshalb:
@@ -376,7 +415,7 @@ weggeräumt.
--- ---
### 18. Hell und dunkel ### 19. Hell und dunkel
![Dunkle Fassung](bilder/20-dunkel.png) ![Dunkle Fassung](bilder/20-dunkel.png)
@@ -386,7 +425,7 @@ hast – sonst käme schwarzes Papier aus dem Drucker.
--- ---
### 19. Drucken ### 20. Drucken
Für die Dokumente, die eine Unterschrift tragen, gibt es **PDFs**: Für die Dokumente, die eine Unterschrift tragen, gibt es **PDFs**:
Wochenblatt, Checklisten-Durchlauf, Feedbackgespräch. Wochenblatt, Checklisten-Durchlauf, Feedbackgespräch.
@@ -397,11 +436,12 @@ mitten durchgeschnitten, aufklappbare Abschnitte drucken aufgeklappt.
--- ---
### 20. Wenn etwas nicht geht ### 21. Wenn etwas nicht geht
| Beobachtung | Woran es liegt | | Beobachtung | Woran es liegt |
|---|---| |---|---|
| „Zu viele Fehlversuche" bei der Anmeldung | Nach fünf falschen Passwörtern wird die Adresse für eine Minute gesperrt, bei weiteren Fehlversuchen länger, höchstens 15 Minuten. Abwarten – oder Passwort vom Ausbilder neu setzen lassen. Hat sich das einzige Ausbilderkonto ausgesperrt: `docker compose restart lernbar` setzt die Sperre zurück. | | „Zu viele Fehlversuche" bei der Anmeldung | Nach fünf falschen Passwörtern wird die Adresse für eine Minute gesperrt, bei weiteren Fehlversuchen länger, höchstens 15 Minuten. Abwarten – oder Passwort vom Ausbilder neu setzen lassen. Hat sich das einzige Ausbilderkonto ausgesperrt: `docker compose restart lernbar` setzt die Sperre zurück. |
| Ein Bereich fehlt in der Navigation, oder seine Seite meldet „Diese Seite gibt es nicht" | Das Modul ist abgeschaltet. Ein Ausbilder schaltet es unter **Verwaltung → Module** wieder ein; die Daten sind noch da. |
| „Diese Seite gibt es nicht" bei fremden Daten | Kein Fehler, sondern die Rechteprüfung. Ob es zu einer Kennung überhaupt Daten gibt, geht Unbefugte nichts an – deshalb 404 und nicht „kein Zugriff". | | „Diese Seite gibt es nicht" bei fremden Daten | Kein Fehler, sondern die Rechteprüfung. Ob es zu einer Kennung überhaupt Daten gibt, geht Unbefugte nichts an – deshalb 404 und nicht „kein Zugriff". |
| Die Anwendung startet nicht, nennt aber Datei und Zeile | Ein Inhaltsfehler unter `content/`. Die Meldung sagt genau, wo. `npm run inhalte:pruefen` prüft ohne Start. | | Die Anwendung startet nicht, nennt aber Datei und Zeile | Ein Inhaltsfehler unter `content/`. Die Meldung sagt genau, wo. `npm run inhalte:pruefen` prüft ohne Start. |
| Ein Anhang lässt sich nicht hochladen | Über 25 MB. Bei einem Reverse Proxy zusätzlich `client_max_body_size 30m` setzen (siehe README). | | Ein Anhang lässt sich nicht hochladen | Über 25 MB. Bei einem Reverse Proxy zusätzlich `client_max_body_size 30m` setzen (siehe README). |
+260
View File
@@ -0,0 +1,260 @@
# Lern-Bar auf Proxmox betreiben
Diese Anleitung führt von einer leeren Proxmox-Umgebung zur laufenden Lern-Bar im
Betriebsnetz: eine virtuelle Maschine mit Debian, darin Docker mit der Lern-Bar und Caddy
als Reverse Proxy mit HTTPS. Rechne mit etwa zwei Stunden.
> **HTTPS ist Pflicht, nicht Kür.** Die Lern-Bar setzt das Anmeldecookie im Betrieb mit dem
> Merkmal `Secure`. Über `http://` nimmt der Browser es nicht an – die Anmeldung scheitert
> dann ohne Fehlermeldung, man landet einfach wieder auf der Anmeldeseite.
---
## Inhalt
1. [VM oder LXC?](#1-vm-oder-lxc)
2. [Die VM anlegen](#2-die-vm-anlegen)
3. [Grundeinrichtung](#3-grundeinrichtung)
4. [Docker installieren](#4-docker-installieren)
5. [Die Lern-Bar holen und starten](#5-die-lern-bar-holen-und-starten)
6. [HTTPS mit Caddy](#6-https-mit-caddy)
7. [Das erste Konto](#7-das-erste-konto)
8. [Sicherung](#8-sicherung)
9. [Aktualisieren](#9-aktualisieren)
10. [Abnahme vor dem Echtbetrieb](#10-abnahme-vor-dem-echtbetrieb)
---
## 1. VM oder LXC?
**Eine VM.** Docker läuft auch in einem LXC-Container, braucht dort aber `nesting` und
`keyctl`, und in unprivilegierten Containern gibt es immer wieder Ärger mit dem
Speichertreiber und den Rechten auf Volumes. Eine VM ist sauber vom Host getrennt, wird von
Proxmox Backup als Ganzes gesichert und verhält sich wie jeder andere Server. Der zusätzliche
Arbeitsspeicher von ein paar hundert Megabyte fällt nicht ins Gewicht.
## 2. Die VM anlegen
| Einstellung | Wert |
|---|---|
| Betriebssystem | Debian 13 („trixie"), Netinstall-Abbild |
| Prozessor | 2 Kerne, Typ `host` |
| Arbeitsspeicher | 2 GB (der Bau des Abbilds braucht kurzzeitig mehr als der Betrieb) |
| Festplatte | 32 GB, VirtIO SCSI, auf einem Speicher, der in die Proxmox-Sicherung fällt |
| Netz | VirtIO, im **Server-VLAN** |
| Optionen | QEMU Guest Agent aktivieren |
Bei der Installation nur **SSH-Server** und **Standard-Systemwerkzeuge** auswählen, keine
grafische Oberfläche.
Danach:
- feste IP-Adresse vergeben oder im DHCP reservieren
- DNS-Eintrag anlegen, etwa `lernbar.betrieb.local` – diesen Namen nutzen die Azubis
- in der VM `sudo apt install qemu-guest-agent && sudo systemctl enable --now qemu-guest-agent`
## 3. Grundeinrichtung
Die Lern-Bar bringt dafür ihre eigene Checkliste mit: **Checklisten → Linux-Server
grundlegend einrichten** (Woche 13). Kurzfassung:
```bash
# Persönlicher Benutzer mit sudo, SSH nur mit Schlüssel (Schritte siehe Handout S8)
sudo apt update && sudo apt upgrade -y
sudo apt install -y unattended-upgrades ufw git
sudo dpkg-reconfigure -plow unattended-upgrades
# Firewall: SSH nur aus dem Verwaltungsnetz, Web für alle im LAN
sudo ufw default deny incoming
sudo ufw allow from 10.10.99.0/24 to any port 22 proto tcp # Verwaltungsnetz anpassen
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
```
> **Docker und ufw:** Ports, die Docker veröffentlicht, laufen an ufw vorbei. Bei der
> Lern-Bar ist das unkritisch, weil `docker-compose.yml` Port 3000 nur auf `127.0.0.1`
> veröffentlicht – von außen ist er nicht erreichbar. Wer das ändert, öffnet den Port für
> alle, egal was ufw sagt.
## 4. Docker installieren
Aus dem Paketarchiv von Docker, nicht aus Debian – dort fehlt die Compose-Erweiterung in
aktueller Fassung.
```bash
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Den eigenen Benutzer berechtigen – danach einmal ab- und wieder anmelden
sudo usermod -aG docker "$USER"
```
Prüfen: `docker compose version` zeigt eine Fassung 2.x.
## 5. Die Lern-Bar holen und starten
Für den Server einen **eigenen Lesezugang** zum Git-Repository anlegen, nicht die eigenen
Zugangsdaten hinterlegen. In Gitea: Repository → Einstellungen → **Deploy-Schlüssel**, einen
SSH-Schlüssel der VM ohne Schreibrecht eintragen.
```bash
ssh-keygen -t ed25519 -f ~/.ssh/lernbar_deploy -N ""
cat ~/.ssh/lernbar_deploy.pub # als Deploy-Schlüssel in Gitea eintragen
cat >> ~/.ssh/config <<'EOF'
Host git.wilde-server.de
IdentityFile ~/.ssh/lernbar_deploy
EOF
sudo mkdir -p /opt/lernbar && sudo chown "$USER": /opt/lernbar
git clone --branch feat/lernbar-erstfassung \
git@git.wilde-server.de:florian.wilde/lernbar.git /opt/lernbar
```
> Solange `main` noch nicht freigegeben ist, läuft der Betrieb vom Zweig
> `feat/lernbar-erstfassung`. Nach der Freigabe einmal `git -C /opt/lernbar checkout main`,
> danach holt `update.sh` von dort.
Bauen und starten:
```bash
cd /opt/lernbar
docker compose up -d --build # der erste Bau dauert einige Minuten
curl -s http://127.0.0.1:3000/api/gesundheit
# {"zustand":"in Ordnung"}
```
Die Daten liegen im Docker-Volume `lernbar_lernbar-daten`, auf dem Host unter
`/var/lib/docker/volumes/lernbar_lernbar-daten/_data`.
## 6. HTTPS mit Caddy
Caddy läuft direkt auf der VM, nimmt die Verbindungen auf 443 an und reicht sie an
`127.0.0.1:3000` weiter.
```bash
sudo apt install -y caddy
```
`/etc/caddy/Caddyfile`:
```caddyfile
lernbar.betrieb.local {
# Variante A: Zertifikat der eigenen Zertifizierungsstelle (empfohlen)
tls /etc/caddy/lernbar.crt /etc/caddy/lernbar.key
# Variante B: Caddys eigene Zertifizierungsstelle – siehe unten
# tls internal
request_body {
max_size 30MB # Fotos als Anhang, die Anwendung selbst begrenzt auf 25 MB
}
reverse_proxy 127.0.0.1:3000
}
```
```bash
sudo systemctl reload caddy
```
**Welches Zertifikat?**
- **Variante A – eigene Zertifizierungsstelle:** Gibt es im Betrieb eine Zertifizierungsstelle
(etwa die Active-Directory-Zertifikatsdienste), ein Serverzertifikat für
`lernbar.betrieb.local` ausstellen. Die Rechner in der Domäne vertrauen ihr bereits.
Datei und Schlüssel nach `/etc/caddy/` legen, Rechte `640`, Gruppe `caddy`.
- **Variante B – `tls internal`:** Caddy stellt das Zertifikat selbst aus. Damit Browser es
annehmen, muss das Wurzelzertifikat
`/var/lib/caddy/.local/share/caddy/pki/authorities/local/root.crt` auf allen Geräten als
vertrauenswürdig hinterlegt werden – an Domänenrechnern per Gruppenrichtlinie, an Handys von
Hand.
- **Ist die Lern-Bar aus dem Internet erreichbar** (öffentlicher DNS-Name, Port 443 offen),
holt Caddy ohne `tls`-Zeile selbst ein Zertifikat von Let's Encrypt.
> Für die **Installation auf dem Handy** (Startbildschirm, Offline-Lernen) verlangen die
> Browser ein Zertifikat, dem das Gerät vertraut. Mit Variante B heißt das: Wurzelzertifikat
> auch auf den Diensthandys installieren.
## 7. Das erste Konto
```bash
cd /opt/lernbar
docker compose exec lernbar node werkzeuge/konto.js \
--name "Vorname Nachname" --email ausbilder@betrieb.de --rolle ausbilder
```
Das ausgegebene Passwort notieren, unter `https://lernbar.betrieb.local` anmelden und es unter
**Verwaltung → Eigenes Passwort** ändern. Danach unter **Verwaltung** die Azubikonten anlegen
und im **Wochenplan** die Wochen einplanen.
Hat das einzige Ausbilderkonto sein Passwort vergessen, setzt derselbe Befehl es neu.
## 8. Sicherung
Zwei Ebenen, die sich ergänzen:
1. **Proxmox sichert die ganze VM**, etwa nächtlich auf Proxmox Backup Server. Damit lässt sich
der ganze Server zurückholen.
2. **Die Lern-Bar sichert ihre Daten selbst** – Datenbank und Anhänge in einem Archiv, das sich
auch auf einem neuen Server einspielen lässt.
`/etc/cron.d/lernbar-sicherung`:
```cron
SHELL=/bin/sh
PATH=/usr/local/bin:/usr/bin:/bin
# 02:30 sichern, 02:45 Archive älter als 30 Tage aufräumen
30 2 * * * root cd /opt/lernbar && docker compose exec -T lernbar /app/werkzeuge/sicherung.sh /daten/sicherungen >> /var/log/lernbar-sicherung.log 2>&1
45 2 * * * root cd /opt/lernbar && docker compose exec -T lernbar find /daten/sicherungen -name 'lernbar-*.tar.gz' -mtime +30 -delete
# 03:00 eine Kopie vom Server wegschaffen – Ziel und Zugang anpassen
0 3 * * * root rsync -a /var/lib/docker/volumes/lernbar_lernbar-daten/_data/sicherungen/ sicherung@nas.betrieb.local:/lernbar/ >> /var/log/lernbar-sicherung.log 2>&1
```
Die Uhrzeiten liegen vor der Proxmox-Sicherung, damit das frische Archiv mit in die
VM-Sicherung fällt.
**Zurückspielen** steht in der [README](../README.md#zurückspielen). Und: Eine Sicherung, die
nie zurückgespielt wurde, ist keine Sicherung. Einmal im Quartal nach der Checkliste
**Wiederherstellung testen** (Woche 8) auf eine Test-VM zurückspielen.
## 9. Aktualisieren
```bash
cd /opt/lernbar
./update.sh
```
Das Skript sichert, holt den neuen Stand, baut, schaltet um und wartet auf das Lebenszeichen.
Meldet sich die neue Fassung nicht gesund, schaltet es von selbst auf die vorherige zurück.
Neue Inhalte – etwa weitere Wochen – kommen auf demselben Weg, weil sie Teil des Repositorys
sind. Neue Wochen erscheinen danach im Wochenplan unter „noch nicht eingeplant".
Updates des Systems selbst spielt `unattended-upgrades` ein. Docker und Caddy kommen dabei mit,
weil sie aus Paketquellen installiert sind.
## 10. Abnahme vor dem Echtbetrieb
Bevor die Azubis die Adresse bekommen:
- [ ] Anmeldung über `https://lernbar.betrieb.local` von einem Arbeitsplatz – ohne
Zertifikatswarnung
- [ ] Anmeldung vom Handy, Installation auf dem Startbildschirm
- [ ] Ein Anhang mit 20 MB lässt sich hochladen
- [ ] Ein Berichtsheft-PDF lässt sich herunterladen
- [ ] `http://lernbar.betrieb.local` leitet auf HTTPS um
- [ ] `http://<IP>:3000` ist von einem anderen Rechner aus **nicht** erreichbar
- [ ] Sicherung von Hand ausgelöst, Archiv liegt auf dem NAS
- [ ] Die Sicherung auf eine Test-VM zurückgespielt, Konten und Daten sind da
- [ ] `./update.sh` einmal durchgelaufen
- [ ] Die VM erscheint in der Proxmox-Sicherung und in der Überwachung
Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

+2 -2
View File
@@ -1,12 +1,12 @@
{ {
"name": "lernbar", "name": "lernbar",
"version": "1.0.4", "version": "1.1.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "lernbar", "name": "lernbar",
"version": "1.0.4", "version": "1.1.0",
"dependencies": { "dependencies": {
"@node-rs/argon2": "^2.0.2", "@node-rs/argon2": "^2.0.2",
"better-sqlite3": "^12.11.1", "better-sqlite3": "^12.11.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "lernbar", "name": "lernbar",
"version": "1.0.4", "version": "1.1.0",
"private": true, "private": true,
"description": "Lern-Bar – betriebliche Ausbildung Fachinformatiker Systemintegration", "description": "Lern-Bar – betriebliche Ausbildung Fachinformatiker Systemintegration",
"engines": { "engines": {
+54 -2
View File
@@ -404,6 +404,7 @@ try {
); );
const blatt = await z.$('.blattzeile-link'); const blatt = await z.$('.blattzeile-link');
let blattAdresse = null;
if (blatt) { if (blatt) {
await blatt.click(); await blatt.click();
// Auf die Adresse warten, nicht auf eine Überschrift: Die steht auch auf // Auf die Adresse warten, nicht auf eine Überschrift: Die steht auch auf
@@ -411,6 +412,7 @@ try {
// die Anfrage landet auf der Sammelroute statt auf dem einzelnen Blatt. // die Anfrage landet auf der Sammelroute statt auf dem einzelnen Blatt.
await z.waitForURL(/\/berichtsheft\/[0-9a-f-]{36}$/, { timeout: 30000 }); await z.waitForURL(/\/berichtsheft\/[0-9a-f-]{36}$/, { timeout: 30000 });
pruefe(true, 'Ein Wochenblatt lässt sich öffnen'); pruefe(true, 'Ein Wochenblatt lässt sich öffnen');
blattAdresse = z.url();
const pdf = await azubi.request.get(`${z.url()}/pdf`); const pdf = await azubi.request.get(`${z.url()}/pdf`);
const puffer = await pdf.body(); const puffer = await pdf.body();
@@ -425,9 +427,59 @@ try {
pruefe(false, 'Ein Wochenblatt lässt sich öffnen', 'keine Blattzeile gefunden'); pruefe(false, 'Ein Wochenblatt lässt sich öffnen', 'keine Blattzeile gefunden');
} }
// --- A8 Sicherung und Rückspielung --------------------------------------- // --- A8 Module abschalten -------------------------------------------------
abschnitt('A8 · Sicherung zurückspielen'); abschnitt('A8 · Module abschalten');
// Über die Oberfläche, wie im Betrieb: Verwaltung → Module.
const umschalten = async (modul) => {
await a.goto(`${BASIS}/verwaltung/module`);
await a.click(`form:has(input[name="modul"][value="${modul}"]) button[type=submit]`);
await a.waitForLoadState('networkidle');
};
const zustandIn = async (modul) => {
await a.goto(`${BASIS}/verwaltung/module`);
// Die Statusmarke steht direkt neben der Überschrift des Moduls.
return (await a.locator(`#modul-${modul} + .marke-status`).innerText()).trim();
};
await umschalten('berichtsheft');
await umschalten('karten');
pruefe((await zustandIn('berichtsheft')).includes('aus'), 'Der Ausbilder schaltet das Berichtsheft ab');
await z.goto(`${BASIS}/`);
pruefe((await z.locator('nav a[href="/berichtsheft"]').count()) === 0, 'Das Berichtsheft verschwindet aus der Navigation');
const seiteAus = await azubi.request.get(`${BASIS}/berichtsheft`);
const ausbilderSeiteAus = await ausbilder.request.get(`${BASIS}/berichtsheft`);
pruefe(
seiteAus.status() === 404 && ausbilderSeiteAus.status() === 404,
'Die Seite ist für beide Rollen gesperrt, nicht nur ausgeblendet',
`Azubi HTTP ${seiteAus.status()}, Ausbilder HTTP ${ausbilderSeiteAus.status()}`,
);
if (blattAdresse) {
const pdfAus = await azubi.request.get(`${blattAdresse}/pdf`);
pruefe(pdfAus.status() === 404, 'Auch das PDF ist gesperrt', `HTTP ${pdfAus.status()}`);
}
await z.goto(`${BASIS}/handouts/N3`);
pruefe(
(await z.locator('text=Merkkarten zu diesem Handout').count()) === 0,
'Ohne Merkkarten zeigt das Handout keine Karten',
);
const lernstandAus = await azubi.request.post(`${BASIS}/api/lernstand`, { data: { eintraege: [] } });
pruefe(lernstandAus.status() === 410, 'Ein Gerät mit gesammelten Bewertungen erfährt, dass es sie verwerfen soll', `HTTP ${lernstandAus.status()}`);
await umschalten('berichtsheft');
await umschalten('karten');
if (blattAdresse) {
const wieder = await azubi.request.get(blattAdresse);
pruefe(wieder.status() === 200, 'Nach dem Einschalten ist das Wochenblatt unverändert wieder da', `HTTP ${wieder.status()}`);
}
// --- A9 Sicherung und Rückspielung ---------------------------------------
abschnitt('A9 · Sicherung zurückspielen');
// Pfade für bash mit Schrägstrichen: Ein Backslash ist dort ein Fluchtzeichen. // Pfade für bash mit Schrägstrichen: Ein Backslash ist dort ein Fluchtzeichen.
const alsPosix = (pfad) => pfad.split(path.sep).join('/'); const alsPosix = (pfad) => pfad.split(path.sep).join('/');
+12
View File
@@ -74,6 +74,18 @@ async function main() {
await seite.waitForSelector('#neu-name'); await seite.waitForSelector('#neu-name');
await bild(seite, '03-verwaltung'); await bild(seite, '03-verwaltung');
// Module: für das Bild ein Modul abschalten, danach den alten Stand herstellen.
const umschalten = async (modul: string) => {
await seite.goto(`${BASIS}/verwaltung/module`);
await seite.click(`form:has(input[name="modul"][value="${modul}"]) button[type=submit]`);
await seite.waitForLoadState('networkidle');
};
await umschalten('berichtsheft');
await seite.goto(`${BASIS}/verwaltung/module`);
await seite.waitForSelector('#modul-karten');
await bild(seite, '25-module');
await umschalten('berichtsheft');
await seite.goto(`${BASIS}/wochenplan`); await seite.goto(`${BASIS}/wochenplan`);
await seite.waitForSelector('h1'); await seite.waitForSelector('h1');
await bild(seite, '04-wochenplan'); await bild(seite, '04-wochenplan');
+6
View File
@@ -155,6 +155,7 @@ export function WochenAnsicht({
)} )}
</section> </section>
{sicht.module.aufgaben ? (
<section className="karte" aria-labelledby="woche-aufgaben"> <section className="karte" aria-labelledby="woche-aufgaben">
<h2 id="woche-aufgaben" style={{ fontSize: '1rem' }}> <h2 id="woche-aufgaben" style={{ fontSize: '1rem' }}>
Praxisaufgaben Praxisaufgaben
@@ -173,7 +174,9 @@ export function WochenAnsicht({
</ul> </ul>
)} )}
</section> </section>
) : null}
{sicht.module.checklisten ? (
<section className="karte" aria-labelledby="woche-checklisten"> <section className="karte" aria-labelledby="woche-checklisten">
<h2 id="woche-checklisten" style={{ fontSize: '1rem' }}> <h2 id="woche-checklisten" style={{ fontSize: '1rem' }}>
Checklisten Checklisten
@@ -188,7 +191,9 @@ export function WochenAnsicht({
</ul> </ul>
)} )}
</section> </section>
) : null}
{sicht.module.kontrollen ? (
<section className="karte" aria-labelledby="woche-kontrolle"> <section className="karte" aria-labelledby="woche-kontrolle">
<h2 id="woche-kontrolle" style={{ fontSize: '1rem' }}> <h2 id="woche-kontrolle" style={{ fontSize: '1rem' }}>
Lernzielkontrolle Lernzielkontrolle
@@ -207,6 +212,7 @@ export function WochenAnsicht({
<p className="leise" style={{ marginBottom: 0 }}>Diese Woche keine.</p> <p className="leise" style={{ marginBottom: 0 }}>Diese Woche keine.</p>
)} )}
</section> </section>
) : null}
</div> </div>
<nav className="reihe nicht-drucken" style={{ justifyContent: 'space-between' }} aria-label="Andere Wochen"> <nav className="reihe nicht-drucken" style={{ justifyContent: 'space-between' }} aria-label="Andere Wochen">
@@ -12,6 +12,7 @@ import { alsZeitpunktText } from '@/lib/zeit';
import { StatusMarke } from '../StatusMarke'; import { StatusMarke } from '../StatusMarke';
import { AnhangFormular, BewertungsFormular, NotizFormular, StatusWaehlen } from './Formulare'; import { AnhangFormular, BewertungsFormular, NotizFormular, StatusWaehlen } from './Formulare';
import { anhangEntfernen, aufgabeAbnehmen, aufgabeZurueckgeben } from '../aktionen'; import { anhangEntfernen, aufgabeAbnehmen, aufgabeZurueckgeben } from '../aktionen';
import { erfordereModul } from '@/lib/module/pruefen';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -29,6 +30,7 @@ export default async function AufgabenSeite({
searchParams: Promise<{ azubi?: string }>; searchParams: Promise<{ azubi?: string }>;
}) { }) {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
erfordereModul('aufgaben');
const { id } = await params; const { id } = await params;
const { azubi: azubiParameter } = await searchParams; const { azubi: azubiParameter } = await searchParams;
@@ -13,6 +13,7 @@ import {
zurueckgeben, zurueckgeben,
} from '@/lib/repos/aufgaben'; } from '@/lib/repos/aufgaben';
import { aufgabenStatus, type AufgabenStatus } from '@/lib/db/schema'; import { aufgabenStatus, type AufgabenStatus } from '@/lib/db/schema';
import { pruefeModul } from '@/lib/module/pruefen';
export type Rueckmeldung = { fehler?: string; erfolg?: string }; export type Rueckmeldung = { fehler?: string; erfolg?: string };
@@ -30,6 +31,7 @@ function aufgabeGibtEs(aufgabeId: string): boolean {
export async function eigenenStatusSetzen(formular: FormData): Promise<void> { export async function eigenenStatusSetzen(formular: FormData): Promise<void> {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
pruefeModul('aufgaben');
if (benutzer.rolle !== 'azubi') return; if (benutzer.rolle !== 'azubi') return;
const aufgabeId = String(formular.get('aufgabeId') ?? ''); const aufgabeId = String(formular.get('aufgabeId') ?? '');
@@ -46,6 +48,7 @@ export async function eigenenStatusSetzen(formular: FormData): Promise<void> {
export async function eigeneNotizSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> { export async function eigeneNotizSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
pruefeModul('aufgaben');
if (benutzer.rolle !== 'azubi') return { fehler: 'Nur der Azubi schreibt seine eigenen Notizen.' }; if (benutzer.rolle !== 'azubi') return { fehler: 'Nur der Azubi schreibt seine eigenen Notizen.' };
const aufgabeId = String(formular.get('aufgabeId') ?? ''); const aufgabeId = String(formular.get('aufgabeId') ?? '');
@@ -61,6 +64,7 @@ export async function eigeneNotizSpeichern(_vorher: Rueckmeldung, formular: Form
export async function anhangHochladen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> { export async function anhangHochladen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
pruefeModul('aufgaben');
if (benutzer.rolle !== 'azubi') return { fehler: 'Anhänge lädt der Azubi hoch.' }; if (benutzer.rolle !== 'azubi') return { fehler: 'Anhänge lädt der Azubi hoch.' };
const aufgabeId = String(formular.get('aufgabeId') ?? ''); const aufgabeId = String(formular.get('aufgabeId') ?? '');
@@ -107,6 +111,7 @@ export async function anhangHochladen(_vorher: Rueckmeldung, formular: FormData)
export async function anhangEntfernen(formular: FormData): Promise<void> { export async function anhangEntfernen(formular: FormData): Promise<void> {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
pruefeModul('aufgaben');
const anhangId = String(formular.get('anhangId') ?? ''); const anhangId = String(formular.get('anhangId') ?? '');
const aufgabeId = String(formular.get('aufgabeId') ?? ''); const aufgabeId = String(formular.get('aufgabeId') ?? '');
@@ -126,6 +131,7 @@ export async function anhangEntfernen(formular: FormData): Promise<void> {
export async function bewerten(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> { export async function bewerten(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
const ausbilder = await erfordereBenutzer(); const ausbilder = await erfordereBenutzer();
pruefeModul('aufgaben');
const azubiId = String(formular.get('azubiId') ?? ''); const azubiId = String(formular.get('azubiId') ?? '');
const aufgabeId = String(formular.get('aufgabeId') ?? ''); const aufgabeId = String(formular.get('aufgabeId') ?? '');
@@ -157,6 +163,7 @@ export async function bewerten(_vorher: Rueckmeldung, formular: FormData): Promi
export async function aufgabeAbnehmen(formular: FormData): Promise<void> { export async function aufgabeAbnehmen(formular: FormData): Promise<void> {
const ausbilder = await erfordereBenutzer(); const ausbilder = await erfordereBenutzer();
pruefeModul('aufgaben');
const azubiId = String(formular.get('azubiId') ?? ''); const azubiId = String(formular.get('azubiId') ?? '');
const aufgabeId = String(formular.get('aufgabeId') ?? ''); const aufgabeId = String(formular.get('aufgabeId') ?? '');
@@ -169,6 +176,7 @@ export async function aufgabeAbnehmen(formular: FormData): Promise<void> {
export async function aufgabeZurueckgeben(formular: FormData): Promise<void> { export async function aufgabeZurueckgeben(formular: FormData): Promise<void> {
const ausbilder = await erfordereBenutzer(); const ausbilder = await erfordereBenutzer();
pruefeModul('aufgaben');
const azubiId = String(formular.get('azubiId') ?? ''); const azubiId = String(formular.get('azubiId') ?? '');
const aufgabeId = String(formular.get('aufgabeId') ?? ''); const aufgabeId = String(formular.get('aufgabeId') ?? '');
@@ -182,5 +190,6 @@ export async function aufgabeZurueckgeben(formular: FormData): Promise<void> {
/** Wird von der Aufgabenseite gebraucht, um fremde Daten überhaupt lesen zu dürfen. */ /** Wird von der Aufgabenseite gebraucht, um fremde Daten überhaupt lesen zu dürfen. */
export async function pruefeLesezugriff(azubiId: string): Promise<void> { export async function pruefeLesezugriff(azubiId: string): Promise<void> {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
pruefeModul('aufgaben');
erfordereZugriffAufAzubi(benutzer, azubiId); erfordereZugriffAufAzubi(benutzer, azubiId);
} }
+2
View File
@@ -6,12 +6,14 @@ import { inhalte } from '@/lib/inhalte';
import { staendeVon } from '@/lib/repos/aufgaben'; import { staendeVon } from '@/lib/repos/aufgaben';
import { alsZeitpunktText } from '@/lib/zeit'; import { alsZeitpunktText } from '@/lib/zeit';
import { StatusMarke } from './StatusMarke'; import { StatusMarke } from './StatusMarke';
import { erfordereModul } from '@/lib/module/pruefen';
export const metadata: Metadata = { title: 'Praxisaufgaben' }; export const metadata: Metadata = { title: 'Praxisaufgaben' };
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
export default async function AufgabenListe() { export default async function AufgabenListe() {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
erfordereModul('aufgaben');
const alle = inhalte(); const alle = inhalte();
if (benutzer.rolle === 'ausbilder') { if (benutzer.rolle === 'ausbilder') {
+18 -5
View File
@@ -103,12 +103,15 @@ function AzubiAuswertung({ azubiId, name }: { azubiId: string; name: string }) {
anteil={anteil(stand.handouts.erledigt, stand.handouts.gesamt)} anteil={anteil(stand.handouts.erledigt, stand.handouts.gesamt)}
fuss={`${stand.handouts.gesamt - stand.handouts.erledigt} noch offen`} fuss={`${stand.handouts.gesamt - stand.handouts.erledigt} noch offen`}
/> />
{stand.module.karten ? (
<Kachel <Kachel
titel="Merkkarten, die sitzen" titel="Merkkarten, die sitzen"
wert={`${stand.karten.erledigt} von ${stand.karten.gesamt}`} wert={`${stand.karten.erledigt} von ${stand.karten.gesamt}`}
anteil={anteil(stand.karten.erledigt, stand.karten.gesamt)} anteil={anteil(stand.karten.erledigt, stand.karten.gesamt)}
fuss={stand.karten.faellig > 0 ? `${stand.karten.faellig} heute fällig` : 'heute nichts fällig'} fuss={stand.karten.faellig > 0 ? `${stand.karten.faellig} heute fällig` : 'heute nichts fällig'}
/> />
) : null}
{stand.module.aufgaben ? (
<Kachel <Kachel
titel="Praxisaufgaben abgenommen" titel="Praxisaufgaben abgenommen"
wert={`${stand.aufgaben.erledigt} von ${stand.aufgaben.gesamt}`} wert={`${stand.aufgaben.erledigt} von ${stand.aufgaben.gesamt}`}
@@ -119,6 +122,8 @@ function AzubiAuswertung({ azubiId, name }: { azubiId: string; name: string }) {
: `${stand.aufgaben.offen} noch offen` : `${stand.aufgaben.offen} noch offen`
} }
/> />
) : null}
{stand.module.berichtsheft ? (
<Kachel <Kachel
titel="Berichtsheft abgezeichnet" titel="Berichtsheft abgezeichnet"
wert={`${stand.berichtsheft.abgezeichnet} von ${stand.berichtsheft.gesamt}`} wert={`${stand.berichtsheft.abgezeichnet} von ${stand.berichtsheft.gesamt}`}
@@ -132,9 +137,10 @@ function AzubiAuswertung({ azubiId, name }: { azubiId: string; name: string }) {
} }
ton={stand.berichtsheft.fehlendeWochen > 0 ? 'fehler' : undefined} ton={stand.berichtsheft.fehlendeWochen > 0 ? 'fehler' : undefined}
/> />
) : null}
</div> </div>
{stand.berichtsheft.fehlendeWochen > 0 ? ( {stand.module.berichtsheft && stand.berichtsheft.fehlendeWochen > 0 ? (
<p className="hinweis hinweis-warnung"> <p className="hinweis hinweis-warnung">
<strong> <strong>
{stand.berichtsheft.fehlendeWochen}{' '} {stand.berichtsheft.fehlendeWochen}{' '}
@@ -145,7 +151,7 @@ function AzubiAuswertung({ azubiId, name }: { azubiId: string; name: string }) {
</p> </p>
) : null} ) : null}
<ErgebnisTabelle ergebnisse={stand.kontrollen.ergebnisse} /> {stand.module.kontrollen ? <ErgebnisTabelle ergebnisse={stand.kontrollen.ergebnisse} /> : null}
</div> </div>
); );
} }
@@ -180,9 +186,14 @@ function Kachel({
// --- Rahmenplanabdeckung --------------------------------------------------- // --- Rahmenplanabdeckung ---------------------------------------------------
function Rahmenplan({ azubiId }: { azubiId: string }) { function Rahmenplan({ azubiId }: { azubiId: string }) {
const { positionen, gruppennamen } = rahmenplanabdeckung(azubiId); const { positionen, gruppennamen, module: eingeschaltet } = rahmenplanabdeckung(azubiId);
const gruppen = nachGruppen(positionen); const gruppen = nachGruppen(positionen);
const gesamt = bilanz(positionen); const gesamt = bilanz(positionen);
// Die Regel steht in lib/auswertung/abdeckung – der Text nennt nur, was an ist.
const belastbar = [
eingeschaltet.aufgaben ? 'eine abgenommene Praxisaufgabe' : null,
eingeschaltet.checklisten ? 'einen abgeschlossenen Checklisten-Durchlauf' : null,
].filter((teil) => teil !== null);
return ( return (
<section className="stapel" aria-labelledby="rahmenplan"> <section className="stapel" aria-labelledby="rahmenplan">
@@ -192,8 +203,9 @@ function Rahmenplan({ azubiId }: { azubiId: string }) {
</h2> </h2>
<p className="leise" style={{ margin: 0 }}> <p className="leise" style={{ margin: 0 }}>
Die Ansicht, die im Zweifel gegenüber der IHK zählt. Als <strong>vermittelt</strong> gilt eine Die Ansicht, die im Zweifel gegenüber der IHK zählt. Als <strong>vermittelt</strong> gilt eine
Position, wenn es dazu eine abgenommene Praxisaufgabe oder einen abgeschlossenen Position, wenn es dazu{' '}
Checklisten-Durchlauf gibt – oder mindestens zwei gelesene Handouts. {belastbar.length > 0 ? `${belastbar.join(' oder ')} gibt – oder ` : ''}mindestens zwei gelesene
Handouts{belastbar.length > 0 ? '' : ' gibt'}.
</p> </p>
</div> </div>
@@ -339,6 +351,7 @@ function Lernfelder({ azubiId }: { azubiId: string }) {
function Kontrollverlauf({ azubiId }: { azubiId: string }) { function Kontrollverlauf({ azubiId }: { azubiId: string }) {
const ausbildung = ausbildungNachAzubi(azubiId); const ausbildung = ausbildungNachAzubi(azubiId);
const stand = eigenerStand(azubiId, ausbildung?.beginn ?? null); const stand = eigenerStand(azubiId, ausbildung?.beginn ?? null);
if (!stand.module.kontrollen) return null;
return <ErgebnisTabelle ergebnisse={stand.kontrollen.ergebnisse} />; return <ErgebnisTabelle ergebnisse={stand.kontrollen.ergebnisse} />;
} }
@@ -17,6 +17,9 @@ import { Formulierungshilfe } from '../Formulierungshilfe';
import { BerichtsMarke } from '../BerichtsMarke'; import { BerichtsMarke } from '../BerichtsMarke';
import { Abzeichnen } from './Abzeichnen'; import { Abzeichnen } from './Abzeichnen';
import { blattZurueckziehen, blattZumLesen } from '../aktionen'; import { blattZurueckziehen, blattZumLesen } from '../aktionen';
import { erfordereModul } from '@/lib/module/pruefen';
import { erfordereBenutzer } from '@/lib/auth/rechte';
import { modulStand } from '@/lib/repos/module';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -27,6 +30,8 @@ export async function generateMetadata({ params }: { params: Promise<{ berichtId
} }
export default async function BlattSeite({ params }: { params: Promise<{ berichtId: string }> }) { export default async function BlattSeite({ params }: { params: Promise<{ berichtId: string }> }) {
await erfordereBenutzer();
erfordereModul('berichtsheft');
const { berichtId } = await params; const { berichtId } = await params;
// Rechteprüfung steckt hier drin: kein Zugriff bedeutet null bedeutet 404. // Rechteprüfung steckt hier drin: kein Zugriff bedeutet null bedeutet 404.
@@ -238,6 +243,8 @@ function AnsichtFuerAusbilder({
/** Stellt zusammen, was die Anwendung über diese Woche weiß. */ /** Stellt zusammen, was die Anwendung über diese Woche weiß. */
function sammleVorschlaege(azubiId: string, von: string, bis: string) { function sammleVorschlaege(azubiId: string, von: string, bis: string) {
const alle = inhalte(); const alle = inhalte();
// Aus abgeschalteten Modulen kommen keine Vorschläge.
const eingeschaltet = modulStand();
const notizen = notizenIm(azubiId, von, bis).map((n) => ({ const notizen = notizenIm(azubiId, von, bis).map((n) => ({
datum: n.datum, datum: n.datum,
@@ -247,7 +254,7 @@ function sammleVorschlaege(azubiId: string, von: string, bis: string) {
// Praxisaufgaben, die in dieser Woche abgegeben oder abgenommen wurden. // Praxisaufgaben, die in dieser Woche abgegeben oder abgenommen wurden.
const staende = aufgabenStaende(azubiId); const staende = aufgabenStaende(azubiId);
const aufgaben = [...staende.values()] const aufgaben = [...(eingeschaltet.aufgaben ? staende.values() : [])]
.filter((s) => { .filter((s) => {
const zeitpunkt = (s.abgenommenAm ?? s.abgegebenAm ?? '').slice(0, 10); const zeitpunkt = (s.abgenommenAm ?? s.abgegebenAm ?? '').slice(0, 10);
return zeitpunkt >= von && zeitpunkt <= bis; return zeitpunkt >= von && zeitpunkt <= bis;
@@ -262,7 +269,7 @@ function sammleVorschlaege(azubiId: string, von: string, bis: string) {
}; };
}); });
const checklisten = laeufeVon(azubiId) const checklisten = (eingeschaltet.checklisten ? laeufeVon(azubiId) : [])
.filter((l) => l.abgeschlossenAm !== null && l.datum >= von && l.datum <= bis) .filter((l) => l.abgeschlossenAm !== null && l.datum >= von && l.datum <= bis)
.map((l) => ({ id: l.id, titel: l.titel, ticket: l.ticket, kunde: l.kunde })); .map((l) => ({ id: l.id, titel: l.titel, ticket: l.ticket, kunde: l.kunde }));
@@ -5,11 +5,15 @@ import { berichtsheftPdf } from '@/lib/pdf/berichtsheft';
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit'; import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
import { ausbildungsjahr } from '@/lib/berichtsheft/jahr'; import { ausbildungsjahr } from '@/lib/berichtsheft/jahr';
import { blattZumLesen } from '../../aktionen'; import { blattZumLesen } from '../../aktionen';
import { erfordereBenutzer } from '@/lib/auth/rechte';
import { modulAn, modulGesperrt } from '@/lib/module/pruefen';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
/** Ein einzelnes Wochenblatt als PDF. Rechteprüfung über `blattZumLesen`. */ /** Ein einzelnes Wochenblatt als PDF. Rechteprüfung über `blattZumLesen`. */
export async function GET(_anfrage: Request, kontext: { params: Promise<{ berichtId: string }> }) { export async function GET(_anfrage: Request, kontext: { params: Promise<{ berichtId: string }> }) {
await erfordereBenutzer();
if (!modulAn('berichtsheft')) return modulGesperrt();
const { berichtId } = await kontext.params; const { berichtId } = await kontext.params;
const gelesen = await blattZumLesen(berichtId); const gelesen = await blattZumLesen(berichtId);
@@ -14,6 +14,7 @@ import {
zurueckgeben, zurueckgeben,
} from '@/lib/repos/berichtsheft'; } from '@/lib/repos/berichtsheft';
import type { Wochenblatt } from '@/lib/berichtsheft/pruefung'; import type { Wochenblatt } from '@/lib/berichtsheft/pruefung';
import { pruefeModul } from '@/lib/module/pruefen';
export type Rueckmeldung = { fehler?: string; erfolg?: string }; export type Rueckmeldung = { fehler?: string; erfolg?: string };
@@ -31,6 +32,7 @@ async function blattZumBearbeiten(berichtId: string) {
/** Zum Lesen: das eigene Blatt oder das eines betreuten Azubis. */ /** Zum Lesen: das eigene Blatt oder das eines betreuten Azubis. */
export async function blattZumLesen(berichtId: string) { export async function blattZumLesen(berichtId: string) {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
pruefeModul('berichtsheft');
const bericht = berichtNachId(berichtId); const bericht = berichtNachId(berichtId);
if (!bericht) return null; if (!bericht) return null;
@@ -50,6 +52,7 @@ function aktualisiere(berichtId?: string) {
export async function tagesnotizSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> { export async function tagesnotizSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
pruefeModul('berichtsheft');
if (benutzer.rolle !== 'azubi') return { fehler: 'Tagesnotizen schreibt der Azubi.' }; if (benutzer.rolle !== 'azubi') return { fehler: 'Tagesnotizen schreibt der Azubi.' };
const datum = String(formular.get('datum') ?? '').trim(); const datum = String(formular.get('datum') ?? '').trim();
@@ -70,6 +73,7 @@ export async function tagesnotizSpeichern(_vorher: Rueckmeldung, formular: FormD
export async function blattAnlegen(formular: FormData): Promise<void> { export async function blattAnlegen(formular: FormData): Promise<void> {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
pruefeModul('berichtsheft');
if (benutzer.rolle !== 'azubi') return; if (benutzer.rolle !== 'azubi') return;
const datum = String(formular.get('datum') ?? '').trim(); const datum = String(formular.get('datum') ?? '').trim();
@@ -104,6 +108,7 @@ function leseBlatt(formular: FormData): Wochenblatt {
} }
export async function blattSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> { export async function blattSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
pruefeModul('berichtsheft');
const berichtId = String(formular.get('berichtId') ?? ''); const berichtId = String(formular.get('berichtId') ?? '');
const { bericht } = await blattZumBearbeiten(berichtId); const { bericht } = await blattZumBearbeiten(berichtId);
@@ -128,6 +133,7 @@ export async function blattSpeichern(_vorher: Rueckmeldung, formular: FormData):
* auf der Seite gesehen; ob er sie beherzigt, entscheidet er. * auf der Seite gesehen; ob er sie beherzigt, entscheidet er.
*/ */
export async function blattEinreichen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> { export async function blattEinreichen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
pruefeModul('berichtsheft');
const berichtId = String(formular.get('berichtId') ?? ''); const berichtId = String(formular.get('berichtId') ?? '');
const { bericht } = await blattZumBearbeiten(berichtId); const { bericht } = await blattZumBearbeiten(berichtId);
@@ -143,6 +149,7 @@ export async function blattEinreichen(_vorher: Rueckmeldung, formular: FormData)
} }
export async function blattZurueckziehen(formular: FormData): Promise<void> { export async function blattZurueckziehen(formular: FormData): Promise<void> {
pruefeModul('berichtsheft');
const berichtId = String(formular.get('berichtId') ?? ''); const berichtId = String(formular.get('berichtId') ?? '');
const { bericht } = await blattZumBearbeiten(berichtId); const { bericht } = await blattZumBearbeiten(berichtId);
if (bericht.status !== 'eingereicht') return; if (bericht.status !== 'eingereicht') return;
@@ -155,6 +162,7 @@ export async function blattZurueckziehen(formular: FormData): Promise<void> {
export async function blattAbzeichnen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> { export async function blattAbzeichnen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
const ausbilder = await erfordereBenutzer(); const ausbilder = await erfordereBenutzer();
pruefeModul('berichtsheft');
const berichtId = String(formular.get('berichtId') ?? ''); const berichtId = String(formular.get('berichtId') ?? '');
const bericht = berichtNachId(berichtId); const bericht = berichtNachId(berichtId);
@@ -173,6 +181,7 @@ export async function blattAbzeichnen(_vorher: Rueckmeldung, formular: FormData)
export async function blattZurueckgeben(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> { export async function blattZurueckgeben(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
const ausbilder = await erfordereBenutzer(); const ausbilder = await erfordereBenutzer();
pruefeModul('berichtsheft');
const berichtId = String(formular.get('berichtId') ?? ''); const berichtId = String(formular.get('berichtId') ?? '');
const bericht = berichtNachId(berichtId); const bericht = berichtNachId(berichtId);
@@ -9,12 +9,14 @@ import { Tagesnotizen, type Tagesfeld } from './Tagesnotizen';
import { Formulierungshilfe } from './Formulierungshilfe'; import { Formulierungshilfe } from './Formulierungshilfe';
import { blattAnlegen } from './aktionen'; import { blattAnlegen } from './aktionen';
import { BerichtsMarke } from './BerichtsMarke'; import { BerichtsMarke } from './BerichtsMarke';
import { erfordereModul } from '@/lib/module/pruefen';
export const metadata: Metadata = { title: 'Berichtsheft' }; export const metadata: Metadata = { title: 'Berichtsheft' };
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
export default async function BerichtsheftSeite() { export default async function BerichtsheftSeite() {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
erfordereModul('berichtsheft');
return benutzer.rolle === 'ausbilder' ? ( return benutzer.rolle === 'ausbilder' ? (
<AusbilderSicht ausbilderId={benutzer.id} /> <AusbilderSicht ausbilderId={benutzer.id} />
@@ -5,6 +5,7 @@ import { alsWochenblatt, berichteVon } from '@/lib/repos/berichtsheft';
import { berichtsheftPdf, type Blattangaben } from '@/lib/pdf/berichtsheft'; import { berichtsheftPdf, type Blattangaben } from '@/lib/pdf/berichtsheft';
import { ausbildungsjahr } from '@/lib/berichtsheft/jahr'; import { ausbildungsjahr } from '@/lib/berichtsheft/jahr';
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit'; import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
import { modulAn, modulGesperrt } from '@/lib/module/pruefen';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -20,6 +21,7 @@ const datumMuster = /^\d{4}-\d{2}-\d{2}$/;
*/ */
export async function GET(anfrage: Request) { export async function GET(anfrage: Request) {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
if (!modulAn('berichtsheft')) return modulGesperrt();
const adresse = new URL(anfrage.url); const adresse = new URL(anfrage.url);
const azubiId = benutzer.rolle === 'azubi' ? benutzer.id : (adresse.searchParams.get('azubi') ?? ''); const azubiId = benutzer.rolle === 'azubi' ? benutzer.id : (adresse.searchParams.get('azubi') ?? '');
@@ -19,6 +19,7 @@ import {
standVon, standVon,
} from '@/lib/repos/checklisten'; } from '@/lib/repos/checklisten';
import { darfAbschliessen } from '@/lib/checklisten/fortschritt'; import { darfAbschliessen } from '@/lib/checklisten/fortschritt';
import { pruefeModul } from '@/lib/module/pruefen';
export type Rueckmeldung = { fehler?: string; erfolg?: string }; export type Rueckmeldung = { fehler?: string; erfolg?: string };
@@ -42,6 +43,7 @@ async function laufZumBearbeiten(laufId: string) {
/** Zum Lesen: der eigene Durchlauf oder der eines betreuten Azubis. */ /** Zum Lesen: der eigene Durchlauf oder der eines betreuten Azubis. */
export async function laufZumLesen(laufId: string) { export async function laufZumLesen(laufId: string) {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
pruefeModul('checklisten');
const lauf = laufNachId(laufId); const lauf = laufNachId(laufId);
if (!lauf) return null; if (!lauf) return null;
@@ -59,6 +61,7 @@ function aktualisiere(laufId: string) {
export async function durchlaufStarten(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> { export async function durchlaufStarten(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
pruefeModul('checklisten');
if (benutzer.rolle !== 'azubi') { if (benutzer.rolle !== 'azubi') {
return { fehler: 'Durchläufe führt der Azubi – der Ausbilder liest mit.' }; return { fehler: 'Durchläufe führt der Azubi – der Ausbilder liest mit.' };
} }
@@ -82,6 +85,7 @@ export async function durchlaufStarten(_vorher: Rueckmeldung, formular: FormData
} }
export async function schrittUmschalten(formular: FormData): Promise<void> { export async function schrittUmschalten(formular: FormData): Promise<void> {
pruefeModul('checklisten');
const laufId = String(formular.get('laufId') ?? ''); const laufId = String(formular.get('laufId') ?? '');
const schrittId = String(formular.get('schrittId') ?? ''); const schrittId = String(formular.get('schrittId') ?? '');
const erledigt = String(formular.get('erledigt') ?? '') === '1'; const erledigt = String(formular.get('erledigt') ?? '') === '1';
@@ -94,6 +98,7 @@ export async function schrittUmschalten(formular: FormData): Promise<void> {
} }
export async function schrittBemerkung(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> { export async function schrittBemerkung(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
pruefeModul('checklisten');
const laufId = String(formular.get('laufId') ?? ''); const laufId = String(formular.get('laufId') ?? '');
const schrittId = String(formular.get('schrittId') ?? ''); const schrittId = String(formular.get('schrittId') ?? '');
const bemerkung = String(formular.get('bemerkung') ?? ''); const bemerkung = String(formular.get('bemerkung') ?? '');
@@ -108,6 +113,7 @@ export async function schrittBemerkung(_vorher: Rueckmeldung, formular: FormData
} }
export async function kopfdatenSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> { export async function kopfdatenSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
pruefeModul('checklisten');
const laufId = String(formular.get('laufId') ?? ''); const laufId = String(formular.get('laufId') ?? '');
const { lauf } = await laufZumBearbeiten(laufId); const { lauf } = await laufZumBearbeiten(laufId);
if (lauf.abgeschlossenAm) return { fehler: 'Der Durchlauf ist abgeschlossen.' }; if (lauf.abgeschlossenAm) return { fehler: 'Der Durchlauf ist abgeschlossen.' };
@@ -130,6 +136,7 @@ export async function kopfdatenSpeichern(_vorher: Rueckmeldung, formular: FormDa
} }
export async function durchlaufAbschliessen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> { export async function durchlaufAbschliessen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
pruefeModul('checklisten');
const laufId = String(formular.get('laufId') ?? ''); const laufId = String(formular.get('laufId') ?? '');
const { lauf } = await laufZumBearbeiten(laufId); const { lauf } = await laufZumBearbeiten(laufId);
if (lauf.abgeschlossenAm) return { fehler: 'Der Durchlauf ist schon abgeschlossen.' }; if (lauf.abgeschlossenAm) return { fehler: 'Der Durchlauf ist schon abgeschlossen.' };
@@ -148,6 +155,7 @@ export async function durchlaufAbschliessen(_vorher: Rueckmeldung, formular: For
} }
export async function durchlaufWiederOeffnen(formular: FormData): Promise<void> { export async function durchlaufWiederOeffnen(formular: FormData): Promise<void> {
pruefeModul('checklisten');
const laufId = String(formular.get('laufId') ?? ''); const laufId = String(formular.get('laufId') ?? '');
await laufZumBearbeiten(laufId); await laufZumBearbeiten(laufId);
laufWiederOeffnen(laufId); laufWiederOeffnen(laufId);
@@ -155,6 +163,7 @@ export async function durchlaufWiederOeffnen(formular: FormData): Promise<void>
} }
export async function durchlaufVerwerfen(formular: FormData): Promise<void> { export async function durchlaufVerwerfen(formular: FormData): Promise<void> {
pruefeModul('checklisten');
const laufId = String(formular.get('laufId') ?? ''); const laufId = String(formular.get('laufId') ?? '');
const { lauf } = await laufZumBearbeiten(laufId); const { lauf } = await laufZumBearbeiten(laufId);
// Ein abgeschlossener Durchlauf ist ein Nachweis und wird nicht gelöscht. // Ein abgeschlossener Durchlauf ist ein Nachweis und wird nicht gelöscht.
@@ -7,6 +7,8 @@ import { standVon } from '@/lib/repos/checklisten';
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit'; import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
import { AbschlussFormular, BemerkungFormular, KopfdatenFormular } from '../../Formulare'; import { AbschlussFormular, BemerkungFormular, KopfdatenFormular } from '../../Formulare';
import { durchlaufVerwerfen, durchlaufWiederOeffnen, laufZumLesen, schrittUmschalten } from '../../aktionen'; import { durchlaufVerwerfen, durchlaufWiederOeffnen, laufZumLesen, schrittUmschalten } from '../../aktionen';
import { erfordereModul } from '@/lib/module/pruefen';
import { erfordereBenutzer } from '@/lib/auth/rechte';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -17,6 +19,8 @@ export async function generateMetadata({ params }: { params: Promise<{ laufId: s
} }
export default async function LaufSeite({ params }: { params: Promise<{ laufId: string }> }) { export default async function LaufSeite({ params }: { params: Promise<{ laufId: string }> }) {
await erfordereBenutzer();
erfordereModul('checklisten');
const { laufId } = await params; const { laufId } = await params;
// Enthält die Rechteprüfung: Wer nicht darf, bekommt null – und damit 404. // Enthält die Rechteprüfung: Wer nicht darf, bekommt null – und damit 404.
@@ -4,6 +4,8 @@ import { standVon } from '@/lib/repos/checklisten';
import { checklistenPdf } from '@/lib/pdf/checkliste'; import { checklistenPdf } from '@/lib/pdf/checkliste';
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit'; import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
import { laufZumLesen } from '../../../aktionen'; import { laufZumLesen } from '../../../aktionen';
import { erfordereBenutzer } from '@/lib/auth/rechte';
import { modulAn, modulGesperrt } from '@/lib/module/pruefen';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -15,6 +17,8 @@ export const dynamic = 'force-dynamic';
* gibt, geht einen Unbefugten nichts an. * gibt, geht einen Unbefugten nichts an.
*/ */
export async function GET(_anfrage: Request, kontext: { params: Promise<{ laufId: string }> }) { export async function GET(_anfrage: Request, kontext: { params: Promise<{ laufId: string }> }) {
await erfordereBenutzer();
if (!modulAn('checklisten')) return modulGesperrt();
const { laufId } = await kontext.params; const { laufId } = await kontext.params;
const gelesen = await laufZumLesen(laufId); const gelesen = await laufZumLesen(laufId);
@@ -6,12 +6,14 @@ import { inhalte } from '@/lib/inhalte';
import { laeufeVon, standVon } from '@/lib/repos/checklisten'; import { laeufeVon, standVon } from '@/lib/repos/checklisten';
import { alsDatumText, alsZeitpunktText, heute } from '@/lib/zeit'; import { alsDatumText, alsZeitpunktText, heute } from '@/lib/zeit';
import { StartFormular } from './Formulare'; import { StartFormular } from './Formulare';
import { erfordereModul } from '@/lib/module/pruefen';
export const metadata: Metadata = { title: 'Checklisten' }; export const metadata: Metadata = { title: 'Checklisten' };
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
export default async function ChecklistenSeite() { export default async function ChecklistenSeite() {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
erfordereModul('checklisten');
const alle = inhalte(); const alle = inhalte();
const istAzubi = benutzer.rolle === 'azubi'; const istAzubi = benutzer.rolle === 'azubi';
@@ -8,6 +8,8 @@ import { istAbgeschlossen, zusammenfassung } from '@/lib/feedback/inhalt';
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit'; import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
import { Gespraechsformular } from '../Gespraechsformular'; import { Gespraechsformular } from '../Gespraechsformular';
import { bestaetigungZurueck, gespraechVerwerfen, gespraechZumLesen } from '../aktionen'; import { bestaetigungZurueck, gespraechVerwerfen, gespraechZumLesen } from '../aktionen';
import { erfordereModul } from '@/lib/module/pruefen';
import { erfordereBenutzer } from '@/lib/auth/rechte';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -18,6 +20,8 @@ export async function generateMetadata({ params }: { params: Promise<{ gespraech
} }
export default async function GespraechsSeite({ params }: { params: Promise<{ gespraechId: string }> }) { export default async function GespraechsSeite({ params }: { params: Promise<{ gespraechId: string }> }) {
await erfordereBenutzer();
erfordereModul('feedback');
const { gespraechId } = await params; const { gespraechId } = await params;
const gelesen = await gespraechZumLesen(gespraechId); const gelesen = await gespraechZumLesen(gespraechId);
@@ -4,11 +4,15 @@ import { inhaltVon } from '@/lib/repos/feedback';
import { feedbackPdf } from '@/lib/pdf/feedback'; import { feedbackPdf } from '@/lib/pdf/feedback';
import { alsDatumText, alsZeitpunktText } from '@/lib/zeit'; import { alsDatumText, alsZeitpunktText } from '@/lib/zeit';
import { gespraechZumLesen } from '../../aktionen'; import { gespraechZumLesen } from '../../aktionen';
import { erfordereBenutzer } from '@/lib/auth/rechte';
import { modulAn, modulGesperrt } from '@/lib/module/pruefen';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
/** Das Feedbackgespräch als PDF. Rechteprüfung über `gespraechZumLesen`. */ /** Das Feedbackgespräch als PDF. Rechteprüfung über `gespraechZumLesen`. */
export async function GET(_anfrage: Request, kontext: { params: Promise<{ gespraechId: string }> }) { export async function GET(_anfrage: Request, kontext: { params: Promise<{ gespraechId: string }> }) {
await erfordereBenutzer();
if (!modulAn('feedback')) return modulGesperrt();
const { gespraechId } = await kontext.params; const { gespraechId } = await kontext.params;
const gelesen = await gespraechZumLesen(gespraechId); const gelesen = await gespraechZumLesen(gespraechId);
@@ -16,6 +16,7 @@ import {
loeschen, loeschen,
} from '@/lib/repos/feedback'; } from '@/lib/repos/feedback';
import { istAbgeschlossen, type Gespraechsinhalt } from '@/lib/feedback/inhalt'; import { istAbgeschlossen, type Gespraechsinhalt } from '@/lib/feedback/inhalt';
import { pruefeModul } from '@/lib/module/pruefen';
export type Rueckmeldung = { fehler?: string; erfolg?: string }; export type Rueckmeldung = { fehler?: string; erfolg?: string };
@@ -30,6 +31,7 @@ const datumMuster = /^\d{4}-\d{2}-\d{2}$/;
*/ */
export async function gespraechZumLesen(id: string) { export async function gespraechZumLesen(id: string) {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
pruefeModul('feedback');
const gespraech = gespraechNachId(id); const gespraech = gespraechNachId(id);
if (!gespraech) return null; if (!gespraech) return null;
@@ -48,6 +50,7 @@ function aktualisiere(id?: string) {
export async function gespraechAnlegen(formular: FormData): Promise<void> { export async function gespraechAnlegen(formular: FormData): Promise<void> {
const ausbilder = await erfordereBenutzer(); const ausbilder = await erfordereBenutzer();
pruefeModul('feedback');
const azubiId = String(formular.get('azubiId') ?? ''); const azubiId = String(formular.get('azubiId') ?? '');
erfordereBewertungsrecht(ausbilder, azubiId); erfordereBewertungsrecht(ausbilder, azubiId);
@@ -67,6 +70,7 @@ export async function gespraechAnlegen(formular: FormData): Promise<void> {
} }
export async function gespraechSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> { export async function gespraechSpeichern(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
pruefeModul('feedback');
const id = String(formular.get('gespraechId') ?? ''); const id = String(formular.get('gespraechId') ?? '');
const gelesen = await gespraechZumLesen(id); const gelesen = await gespraechZumLesen(id);
if (!gelesen) return { fehler: 'Dieses Gespräch gibt es nicht.' }; if (!gelesen) return { fehler: 'Dieses Gespräch gibt es nicht.' };
@@ -125,6 +129,7 @@ export async function gespraechSpeichern(_vorher: Rueckmeldung, formular: FormDa
} }
export async function gespraechBestaetigen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> { export async function gespraechBestaetigen(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
pruefeModul('feedback');
const id = String(formular.get('gespraechId') ?? ''); const id = String(formular.get('gespraechId') ?? '');
const gelesen = await gespraechZumLesen(id); const gelesen = await gespraechZumLesen(id);
if (!gelesen) return { fehler: 'Dieses Gespräch gibt es nicht.' }; if (!gelesen) return { fehler: 'Dieses Gespräch gibt es nicht.' };
@@ -148,6 +153,7 @@ export async function gespraechBestaetigen(_vorher: Rueckmeldung, formular: Form
} }
export async function bestaetigungZurueck(formular: FormData): Promise<void> { export async function bestaetigungZurueck(formular: FormData): Promise<void> {
pruefeModul('feedback');
const id = String(formular.get('gespraechId') ?? ''); const id = String(formular.get('gespraechId') ?? '');
const gelesen = await gespraechZumLesen(id); const gelesen = await gespraechZumLesen(id);
if (!gelesen) return; if (!gelesen) return;
@@ -164,6 +170,7 @@ export async function bestaetigungZurueck(formular: FormData): Promise<void> {
} }
export async function gespraechVerwerfen(formular: FormData): Promise<void> { export async function gespraechVerwerfen(formular: FormData): Promise<void> {
pruefeModul('feedback');
const id = String(formular.get('gespraechId') ?? ''); const id = String(formular.get('gespraechId') ?? '');
const gelesen = await gespraechZumLesen(id); const gelesen = await gespraechZumLesen(id);
if (!gelesen) return; if (!gelesen) return;
+2
View File
@@ -7,12 +7,14 @@ import { gespraecheVon, inhaltVon } from '@/lib/repos/feedback';
import { istAbgeschlossen, zusammenfassung } from '@/lib/feedback/inhalt'; import { istAbgeschlossen, zusammenfassung } from '@/lib/feedback/inhalt';
import { alsDatumText, heute, montagDerWoche } from '@/lib/zeit'; import { alsDatumText, heute, montagDerWoche } from '@/lib/zeit';
import { gespraechAnlegen } from './aktionen'; import { gespraechAnlegen } from './aktionen';
import { erfordereModul } from '@/lib/module/pruefen';
export const metadata: Metadata = { title: 'Feedbackgespräche' }; export const metadata: Metadata = { title: 'Feedbackgespräche' };
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
export default async function FeedbackSeite() { export default async function FeedbackSeite() {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
erfordereModul('feedback');
const vorlage = inhalte().feedback; const vorlage = inhalte().feedback;
const beteiligte = const beteiligte =
@@ -8,6 +8,7 @@ import { gelesenAm } from '@/lib/repos/fortschritt';
import { alsZeitpunktText } from '@/lib/zeit'; import { alsZeitpunktText } from '@/lib/zeit';
import { gelesenUmschalten } from './aktionen'; import { gelesenUmschalten } from './aktionen';
import { Druckknopf } from '@/komponenten/Druckknopf'; import { Druckknopf } from '@/komponenten/Druckknopf';
import { modulStand } from '@/lib/repos/module';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -131,6 +132,7 @@ export default async function HandoutSeite({ params }: { params: Promise<{ id: s
</details> </details>
) : null} ) : null}
{modulStand().karten ? (
<section aria-labelledby="karten" className="karte stapel-eng"> <section aria-labelledby="karten" className="karte stapel-eng">
<h2 id="karten" style={{ marginBottom: '0.3rem' }}> <h2 id="karten" style={{ marginBottom: '0.3rem' }}>
Merkkarten zu diesem Handout Merkkarten zu diesem Handout
@@ -149,6 +151,7 @@ export default async function HandoutSeite({ params }: { params: Promise<{ id: s
))} ))}
</ol> </ol>
</section> </section>
) : null}
{istAzubi ? ( {istAzubi ? (
<form action={gelesenUmschalten} className="nicht-drucken"> <form action={gelesenUmschalten} className="nicht-drucken">
+2
View File
@@ -6,6 +6,7 @@ import { inhalte } from '@/lib/inhalte';
import { bewerte } from '@/lib/repos/karten'; import { bewerte } from '@/lib/repos/karten';
import { bewertungen, fachNamen, type Bewertung } from '@/lib/lernen/sm2'; import { bewertungen, fachNamen, type Bewertung } from '@/lib/lernen/sm2';
import { alsDatumText, heute } from '@/lib/zeit'; import { alsDatumText, heute } from '@/lib/zeit';
import { pruefeModul } from '@/lib/module/pruefen';
export type Bewertungsergebnis = { export type Bewertungsergebnis = {
fehler?: string; fehler?: string;
@@ -25,6 +26,7 @@ export type Bewertungsergebnis = {
*/ */
export async function karteBewerten(karteId: string, bewertung: string): Promise<Bewertungsergebnis> { export async function karteBewerten(karteId: string, bewertung: string): Promise<Bewertungsergebnis> {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
pruefeModul('karten');
if (benutzer.rolle !== 'azubi') { if (benutzer.rolle !== 'azubi') {
return { fehler: 'Nur der Azubi führt seinen eigenen Lernstand.' }; return { fehler: 'Nur der Azubi führt seinen eigenen Lernstand.' };
} }
@@ -7,6 +7,7 @@ import { alsKartenstand, staendeVon } from '@/lib/repos/karten';
import { fach, fachNamen, heutigerStapel } from '@/lib/lernen/sm2'; import { fach, fachNamen, heutigerStapel } from '@/lib/lernen/sm2';
import { heute } from '@/lib/zeit'; import { heute } from '@/lib/zeit';
import { Lernmodus, type Lernkarte } from './Lernmodus'; import { Lernmodus, type Lernkarte } from './Lernmodus';
import { erfordereModul } from '@/lib/module/pruefen';
export const metadata: Metadata = { title: 'Lernmodus' }; export const metadata: Metadata = { title: 'Lernmodus' };
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -15,6 +16,7 @@ const STAPELGROESSE = 20;
export default async function LernSeite({ searchParams }: { searchParams: Promise<{ block?: string }> }) { export default async function LernSeite({ searchParams }: { searchParams: Promise<{ block?: string }> }) {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
erfordereModul('karten');
if (benutzer.rolle !== 'azubi') redirect('/karten'); if (benutzer.rolle !== 'azubi') redirect('/karten');
const { block } = await searchParams; const { block } = await searchParams;
+2
View File
@@ -5,6 +5,7 @@ import { inhalte } from '@/lib/inhalte';
import { alsKartenstand, staendeVon, uebersicht } from '@/lib/repos/karten'; import { alsKartenstand, staendeVon, uebersicht } from '@/lib/repos/karten';
import { fach, fachNamen, heutigerStapel } from '@/lib/lernen/sm2'; import { fach, fachNamen, heutigerStapel } from '@/lib/lernen/sm2';
import { alsDatumText, heute } from '@/lib/zeit'; import { alsDatumText, heute } from '@/lib/zeit';
import { erfordereModul } from '@/lib/module/pruefen';
export const metadata: Metadata = { title: 'Merkkarten' }; export const metadata: Metadata = { title: 'Merkkarten' };
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -13,6 +14,7 @@ const STAPELGROESSE = 20;
export default async function KartenSeite() { export default async function KartenSeite() {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
erfordereModul('karten');
const alle = inhalte(); const alle = inhalte();
const heuteDatum = heute(); const heuteDatum = heute();
@@ -10,6 +10,7 @@ import { alsZeitpunktText } from '@/lib/zeit';
import { Fragebogen } from '../Fragebogen'; import { Fragebogen } from '../Fragebogen';
import { Korrektur, type Korrekturfrage } from '../Korrektur'; import { Korrektur, type Korrekturfrage } from '../Korrektur';
import { standZumLesen } from '../aktionen'; import { standZumLesen } from '../aktionen';
import { erfordereModul } from '@/lib/module/pruefen';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -27,6 +28,7 @@ export default async function KontrollSeite({
searchParams: Promise<{ azubi?: string }>; searchParams: Promise<{ azubi?: string }>;
}) { }) {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
erfordereModul('kontrollen');
const { kontrolleId } = await params; const { kontrolleId } = await params;
const { azubi: azubiParameter } = await searchParams; const { azubi: azubiParameter } = await searchParams;
@@ -14,6 +14,7 @@ import {
standVon, standVon,
} from '@/lib/repos/kontrollen'; } from '@/lib/repos/kontrollen';
import { darfBearbeiten } from '@/lib/kontrollen/sicht'; import { darfBearbeiten } from '@/lib/kontrollen/sicht';
import { pruefeModul } from '@/lib/module/pruefen';
export type Rueckmeldung = { fehler?: string; erfolg?: string }; export type Rueckmeldung = { fehler?: string; erfolg?: string };
@@ -36,6 +37,7 @@ export async function antwortZwischenspeichern(
antwort: string, antwort: string,
): Promise<{ gespeichertUm: string } | { fehler: string }> { ): Promise<{ gespeichertUm: string } | { fehler: string }> {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
pruefeModul('kontrollen');
const inhalt = inhalte().kontrolleNach.get(kontrolleId); const inhalt = inhalte().kontrolleNach.get(kontrolleId);
if (!inhalt) return { fehler: 'Diese Lernzielkontrolle gibt es nicht.' }; if (!inhalt) return { fehler: 'Diese Lernzielkontrolle gibt es nicht.' };
@@ -57,6 +59,7 @@ export async function antwortZwischenspeichern(
export async function kontrolleAbgeben(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> { export async function kontrolleAbgeben(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
pruefeModul('kontrollen');
const kontrolleId = String(formular.get('kontrolleId') ?? ''); const kontrolleId = String(formular.get('kontrolleId') ?? '');
const inhalt = inhalte().kontrolleNach.get(kontrolleId); const inhalt = inhalte().kontrolleNach.get(kontrolleId);
@@ -84,6 +87,7 @@ export async function kontrolleAbgeben(_vorher: Rueckmeldung, formular: FormData
export async function kontrolleFreigeben(formular: FormData): Promise<void> { export async function kontrolleFreigeben(formular: FormData): Promise<void> {
const ausbilder = await erfordereBenutzer(); const ausbilder = await erfordereBenutzer();
pruefeModul('kontrollen');
const azubiId = String(formular.get('azubiId') ?? ''); const azubiId = String(formular.get('azubiId') ?? '');
const kontrolleId = String(formular.get('kontrolleId') ?? ''); const kontrolleId = String(formular.get('kontrolleId') ?? '');
@@ -96,6 +100,7 @@ export async function kontrolleFreigeben(formular: FormData): Promise<void> {
export async function freigabeZurueck(formular: FormData): Promise<void> { export async function freigabeZurueck(formular: FormData): Promise<void> {
const ausbilder = await erfordereBenutzer(); const ausbilder = await erfordereBenutzer();
pruefeModul('kontrollen');
const azubiId = String(formular.get('azubiId') ?? ''); const azubiId = String(formular.get('azubiId') ?? '');
const kontrolleId = String(formular.get('kontrolleId') ?? ''); const kontrolleId = String(formular.get('kontrolleId') ?? '');
@@ -111,6 +116,7 @@ export async function freigabeZurueck(formular: FormData): Promise<void> {
export async function kontrolleKorrigieren(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> { export async function kontrolleKorrigieren(_vorher: Rueckmeldung, formular: FormData): Promise<Rueckmeldung> {
const ausbilder = await erfordereBenutzer(); const ausbilder = await erfordereBenutzer();
pruefeModul('kontrollen');
const azubiId = String(formular.get('azubiId') ?? ''); const azubiId = String(formular.get('azubiId') ?? '');
const kontrolleId = String(formular.get('kontrolleId') ?? ''); const kontrolleId = String(formular.get('kontrolleId') ?? '');
@@ -153,6 +159,7 @@ export async function kontrolleKorrigieren(_vorher: Rueckmeldung, formular: Form
/** Prüft den Lesezugriff und liefert den Stand. Wird von Seiten benutzt. */ /** Prüft den Lesezugriff und liefert den Stand. Wird von Seiten benutzt. */
export async function standZumLesen(azubiId: string, kontrolleId: string) { export async function standZumLesen(azubiId: string, kontrolleId: string) {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
pruefeModul('kontrollen');
const bezug = ausbildungVonAzubi(azubiId); const bezug = ausbildungVonAzubi(azubiId);
if (!darfAzubiDatenSehen(benutzer, azubiId, bezug)) return null; if (!darfAzubiDatenSehen(benutzer, azubiId, bezug)) return null;
+2
View File
@@ -8,6 +8,7 @@ import { gesamtpunkte, zustandVon, type Zustand } from '@/lib/kontrollen/sicht';
import { verlauf } from '@/lib/kontrollen/bewertung'; import { verlauf } from '@/lib/kontrollen/bewertung';
import { alsZeitpunktText } from '@/lib/zeit'; import { alsZeitpunktText } from '@/lib/zeit';
import { kontrolleFreigeben, freigabeZurueck } from './aktionen'; import { kontrolleFreigeben, freigabeZurueck } from './aktionen';
import { erfordereModul } from '@/lib/module/pruefen';
export const metadata: Metadata = { title: 'Lernzielkontrollen' }; export const metadata: Metadata = { title: 'Lernzielkontrollen' };
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -21,6 +22,7 @@ const zustandsmarke: Record<Zustand, { text: string; ton: string; zeichen: strin
export default async function KontrollenSeite() { export default async function KontrollenSeite() {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
erfordereModul('kontrollen');
const alle = inhalte(); const alle = inhalte();
if (benutzer.rolle === 'azubi') { if (benutzer.rolle === 'azubi') {
+4 -1
View File
@@ -2,6 +2,8 @@ import { erfordereBenutzer } from '@/lib/auth/rechte';
import { Kopfzeile, type Navigationseintrag } from '@/komponenten/Kopfzeile'; import { Kopfzeile, type Navigationseintrag } from '@/komponenten/Kopfzeile';
import { Netzzustand } from '@/komponenten/Netzzustand'; import { Netzzustand } from '@/komponenten/Netzzustand';
import { abmelden } from '../anmelden/aktionen'; import { abmelden } from '../anmelden/aktionen';
import { modulStand } from '@/lib/repos/module';
import { nurOffene } from '@/lib/module/module';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -37,7 +39,8 @@ const fuerAusbilder: Navigationseintrag[] = [
export default async function AngemeldetLayout({ children }: { children: React.ReactNode }) { export default async function AngemeldetLayout({ children }: { children: React.ReactNode }) {
const benutzer = await erfordereBenutzer(); const benutzer = await erfordereBenutzer();
const eintraege = benutzer.rolle === 'ausbilder' ? fuerAusbilder : fuerAzubi; // Abgeschaltete Module verschwinden aus der Navigation (lib/module).
const eintraege = nurOffene(benutzer.rolle === 'ausbilder' ? fuerAusbilder : fuerAzubi, modulStand());
return ( return (
<> <>
+13 -4
View File
@@ -10,6 +10,7 @@ import { wartetAufAbzeichnung } from '@/lib/repos/berichtsheft';
import { wartetAufKorrektur } from '@/lib/repos/kontrollen'; import { wartetAufKorrektur } from '@/lib/repos/kontrollen';
import { benutzerNachId } from '@/lib/repos/benutzer'; import { benutzerNachId } from '@/lib/repos/benutzer';
import { WochenAnsicht } from './WochenAnsicht'; import { WochenAnsicht } from './WochenAnsicht';
import { modulStand } from '@/lib/repos/module';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -151,10 +152,18 @@ function WartetAufMich({ ausbilderId }: { ausbilderId: string }) {
const meine = ausbildungenVonAusbilder(ausbilderId); const meine = ausbildungenVonAusbilder(ausbilderId);
const alle = inhalte(); const alle = inhalte();
const azubiIds = meine.map(({ azubi }) => azubi.id); const azubiIds = meine.map(({ azubi }) => azubi.id);
const abnahmen = wartetAufAbnahme(azubiIds); // Nur was zu eingeschalteten Modulen gehört – eine Abnahme, die man nicht
const berichte = wartetAufAbzeichnung(azubiIds); // öffnen kann, wäre ein toter Verweis.
const kontrollen = wartetAufKorrektur(azubiIds); const eingeschaltet = modulStand();
const abnahmen = eingeschaltet.aufgaben ? wartetAufAbnahme(azubiIds) : [];
const berichte = eingeschaltet.berichtsheft ? wartetAufAbzeichnung(azubiIds) : [];
const kontrollen = eingeschaltet.kontrollen ? wartetAufKorrektur(azubiIds) : [];
const gesamt = abnahmen.length + berichte.length + kontrollen.length; const gesamt = abnahmen.length + berichte.length + kontrollen.length;
const art = [
eingeschaltet.aufgaben ? 'keine Abnahme' : null,
eingeschaltet.kontrollen ? 'keine Korrektur' : null,
eingeschaltet.berichtsheft ? 'kein Berichtsheft' : null,
].filter((teil) => teil !== null);
return ( return (
<section className="karte stapel-eng" aria-labelledby="wartet"> <section className="karte stapel-eng" aria-labelledby="wartet">
@@ -164,7 +173,7 @@ function WartetAufMich({ ausbilderId }: { ausbilderId: string }) {
{gesamt === 0 ? ( {gesamt === 0 ? (
<p className="leise" style={{ marginBottom: 0 }}> <p className="leise" style={{ marginBottom: 0 }}>
Nichts offen – keine Abnahme, keine Korrektur, kein Berichtsheft. {art.length > 0 ? `Nichts offen – ${art.join(', ')}.` : 'Nichts offen.'}
</p> </p>
) : ( ) : (
<ul className="offen-liste"> <ul className="offen-liste">
@@ -0,0 +1,23 @@
import Link from 'next/link';
/** Die beiden Bereiche der Verwaltung. */
export function Unternavigation({ aktiv }: { aktiv: 'konten' | 'module' }) {
return (
<nav className="reihe nicht-drucken" aria-label="Bereiche der Verwaltung">
<Link
href="/verwaltung"
className={`knopf ${aktiv === 'konten' ? '' : 'knopf-zweit'}`}
aria-current={aktiv === 'konten' ? 'page' : undefined}
>
Konten
</Link>
<Link
href="/verwaltung/module"
className={`knopf ${aktiv === 'module' ? '' : 'knopf-zweit'}`}
aria-current={aktiv === 'module' ? 'page' : undefined}
>
Module
</Link>
</nav>
);
}
@@ -0,0 +1,19 @@
'use server';
import { revalidatePath } from 'next/cache';
import { erfordereAusbilder } from '@/lib/auth/rechte';
import { istModulId } from '@/lib/module/module';
import { modulSetzen } from '@/lib/repos/module';
/** Schaltet ein Modul für die ganze Lern-Bar ein oder aus. Nur Ausbilder. */
export async function modulUmschalten(formular: FormData): Promise<void> {
const ausbilder = await erfordereAusbilder();
const modul = String(formular.get('modul') ?? '');
const aktiv = String(formular.get('aktiv') ?? '') === '1';
if (!istModulId(modul)) return;
modulSetzen(modul, aktiv, ausbilder.id);
// Navigation, Startseite, Auswertung – alles hängt am Modulstand.
revalidatePath('/', 'layout');
}
@@ -0,0 +1,81 @@
import type { Metadata } from 'next';
import { erfordereAusbilder } from '@/lib/auth/rechte';
import { modulIds, moduleintraege } from '@/lib/module/module';
import { modulAenderungen, modulStand } from '@/lib/repos/module';
import { benutzerNachId } from '@/lib/repos/benutzer';
import { alsZeitpunktText } from '@/lib/zeit';
import { Unternavigation } from '../Unternavigation';
import { modulUmschalten } from './aktionen';
export const metadata: Metadata = { title: 'Module' };
export const dynamic = 'force-dynamic';
export default async function ModulSeite() {
await erfordereAusbilder();
const stand = modulStand();
const aenderungen = new Map(modulAenderungen().map((zeile) => [zeile.modul, zeile]));
return (
<div className="stapel">
<div>
<h1>Verwaltung</h1>
<p className="leise">
Module, die ihr nicht braucht, lassen sich abschalten – für die ganze Lern-Bar, für alle Azubis und
Ausbilder zugleich.
</p>
</div>
<Unternavigation aktiv="module" />
<p className="hinweis">
<strong>Abschalten löscht nichts.</strong> Ein abgeschaltetes Modul verschwindet aus Navigation, Wochenplan
und Auswertung, und seine Seiten sind gesperrt. Was darin schon geschrieben wurde, bleibt gespeichert und ist
nach dem Einschalten unverändert wieder da. Wochenplan, Handouts, Glossar, Auswertung und Verwaltung sind
immer an.
</p>
<div className="stapel">
{modulIds.map((id) => {
const eintrag = moduleintraege[id];
const an = stand[id];
const aenderung = aenderungen.get(id);
const wer = aenderung?.geaendertVon ? benutzerNachId(aenderung.geaendertVon)?.name : null;
return (
<section key={id} className="karte stapel-eng" aria-labelledby={`modul-${id}`}>
<div className="reihe" style={{ justifyContent: 'space-between' }}>
<div className="reihe" style={{ gap: '0.6rem' }}>
<h2 id={`modul-${id}`} style={{ margin: 0, fontSize: '1.05rem' }}>
{eintrag.titel}
</h2>
<span className="marke-status" data-ton={an ? 'gut' : 'warnung'}>
{an ? '● an' : '○ aus'}
</span>
</div>
<form action={modulUmschalten}>
<input type="hidden" name="modul" value={id} />
<input type="hidden" name="aktiv" value={an ? '0' : '1'} />
<button type="submit" className={`knopf ${an ? 'knopf-zweit' : ''}`}>
{an ? 'Abschalten' : 'Einschalten'}
</button>
</form>
</div>
<p style={{ margin: 0 }}>{eintrag.beschreibung}</p>
<p className="leise" style={{ margin: 0 }}>
<strong>Wenn aus:</strong> {eintrag.folgen}
</p>
{aenderung ? (
<p className="leise" style={{ margin: 0 }}>
Zuletzt {aenderung.aktiv === 1 ? 'eingeschaltet' : 'abgeschaltet'} am{' '}
{alsZeitpunktText(aenderung.geaendertAm)}
{wer ? ` von ${wer}` : ''}.
</p>
) : null}
</section>
);
})}
</div>
</div>
);
}
+5 -1
View File
@@ -4,6 +4,7 @@ import { ausbildungenVonAusbilder } from '@/lib/repos/ausbildung';
import { alsDatumText, alsZeitpunktText, heute } from '@/lib/zeit'; import { alsDatumText, alsZeitpunktText, heute } from '@/lib/zeit';
import { AusbildungFormular, AzubiAnlegenFormular, PasswortFormular } from './Formulare'; import { AusbildungFormular, AzubiAnlegenFormular, PasswortFormular } from './Formulare';
import { kontoAktivSetzen } from './aktionen'; import { kontoAktivSetzen } from './aktionen';
import { Unternavigation } from './Unternavigation';
export const metadata: Metadata = { title: 'Verwaltung' }; export const metadata: Metadata = { title: 'Verwaltung' };
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -17,10 +18,13 @@ export default async function VerwaltungsSeite() {
<div> <div>
<h1>Verwaltung</h1> <h1>Verwaltung</h1>
<p className="leise"> <p className="leise">
Hier legst du Azubikonten an und pflegst die Eckdaten der Ausbildung. Mehr Verwaltung gibt es bewusst nicht. Hier legst du Azubikonten an und pflegst die Eckdaten der Ausbildung. Unter &bdquo;Module&ldquo; schaltest du ab, was
ihr nicht braucht.
</p> </p>
</div> </div>
<Unternavigation aktiv="konten" />
<section className="karte stapel" aria-labelledby="ueberschrift-azubis"> <section className="karte stapel" aria-labelledby="ueberschrift-azubis">
<h2 id="ueberschrift-azubis" style={{ marginBottom: 0 }}> <h2 id="ueberschrift-azubis" style={{ marginBottom: 0 }}>
Deine Auszubildenden Deine Auszubildenden
+2
View File
@@ -3,6 +3,7 @@ import { darfAzubiDatenSehen } from '@/lib/auth/regeln';
import { ausbildungVonAzubi } from '@/lib/auth/rechte'; import { ausbildungVonAzubi } from '@/lib/auth/rechte';
import { anhangLesen, anhangNachId } from '@/lib/repos/anhaenge'; import { anhangLesen, anhangNachId } from '@/lib/repos/anhaenge';
import { istBild } from '@/lib/anhaenge/dateien'; import { istBild } from '@/lib/anhaenge/dateien';
import { modulAn, modulGesperrt } from '@/lib/module/pruefen';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -18,6 +19,7 @@ export const dynamic = 'force-dynamic';
export async function GET(_anfrage: Request, kontext: { params: Promise<{ id: string }> }) { export async function GET(_anfrage: Request, kontext: { params: Promise<{ id: string }> }) {
const benutzer = await angemeldeterBenutzer(); const benutzer = await angemeldeterBenutzer();
if (!benutzer) return new Response('Nicht angemeldet.', { status: 401 }); if (!benutzer) return new Response('Nicht angemeldet.', { status: 401 });
if (!modulAn('aufgaben')) return modulGesperrt();
const { id } = await kontext.params; const { id } = await kontext.params;
const eintrag = anhangNachId(id); const eintrag = anhangNachId(id);
+4
View File
@@ -4,6 +4,7 @@ import { bewerte } from '@/lib/repos/karten';
import { bewertungen, type Bewertung } from '@/lib/lernen/sm2'; import { bewertungen, type Bewertung } from '@/lib/lernen/sm2';
import { heute } from '@/lib/zeit'; import { heute } from '@/lib/zeit';
import { tagDerBewertung } from '@/lib/offline/nachtrag'; import { tagDerBewertung } from '@/lib/offline/nachtrag';
import { modulAn } from '@/lib/module/pruefen';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@@ -28,6 +29,9 @@ type Eingang = { karteId?: unknown; bewertung?: unknown; erfasstAm?: unknown };
export async function POST(anfrage: Request): Promise<Response> { export async function POST(anfrage: Request): Promise<Response> {
const benutzer = await angemeldeterBenutzer(); const benutzer = await angemeldeterBenutzer();
if (!benutzer) return Response.json({ fehler: 'Nicht angemeldet.' }, { status: 401 }); if (!benutzer) return Response.json({ fehler: 'Nicht angemeldet.' }, { status: 401 });
// 410 statt 404: Das Gerät soll seine gesammelten Bewertungen verwerfen,
// statt sie bei jeder Verbindung erneut zu schicken.
if (!modulAn('karten')) return Response.json({ fehler: 'Die Merkkarten sind abgeschaltet.' }, { status: 410 });
if (benutzer.rolle !== 'azubi') { if (benutzer.rolle !== 'azubi') {
return Response.json({ fehler: 'Nur der Azubi führt seinen eigenen Lernstand.' }, { status: 403 }); return Response.json({ fehler: 'Nur der Azubi führt seinen eigenen Lernstand.' }, { status: 403 });
} }
+15 -6
View File
@@ -1,4 +1,16 @@
import type { MetadataRoute } from 'next'; import type { MetadataRoute } from 'next';
import { modulStand } from '@/lib/repos/module';
import { pfadOffen } from '@/lib/module/module';
// Die Kurzbefehle richten sich nach den eingeschalteten Modulen – also bei
// jedem Abruf neu erzeugen, nicht einmal beim Bau.
export const dynamic = 'force-dynamic';
const kurzbefehle = [
{ name: 'Lernmodus', short_name: 'Lernen', url: '/karten/lernen' },
{ name: 'Berichtsheft', short_name: 'Bericht', url: '/berichtsheft' },
{ name: 'Handouts', short_name: 'Handouts', url: '/handouts' },
];
/** /**
* Die Angaben für die Installation auf dem Startbildschirm. * Die Angaben für die Installation auf dem Startbildschirm.
@@ -8,11 +20,12 @@ import type { MetadataRoute } from 'next';
* abgelaufen, leitet der Server ohnehin zur Anmeldung. * abgelaufen, leitet der Server ohnehin zur Anmeldung.
*/ */
export default function manifest(): MetadataRoute.Manifest { export default function manifest(): MetadataRoute.Manifest {
const stand = modulStand();
return { return {
name: 'Lern-Bar – Betriebliche Ausbildung', name: 'Lern-Bar – Betriebliche Ausbildung',
short_name: 'Lern-Bar', short_name: 'Lern-Bar',
description: description:
'Wochenplan, Handouts, Merkkarten, Aufgaben, Checklisten und Berichtsheft für die Ausbildung zum Fachinformatiker für Systemintegration.', 'Wochenplan, Lehrgang und Nachweise für die Ausbildung zum Fachinformatiker für Systemintegration.',
lang: 'de', lang: 'de',
dir: 'ltr', dir: 'ltr',
start_url: '/', start_url: '/',
@@ -27,10 +40,6 @@ export default function manifest(): MetadataRoute.Manifest {
{ src: '/symbol-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' }, { src: '/symbol-512.png', sizes: '512x512', type: 'image/png', purpose: 'any' },
{ src: '/symbol-maskabel-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, { src: '/symbol-maskabel-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
], ],
shortcuts: [ shortcuts: kurzbefehle.filter((kurz) => pfadOffen(kurz.url, stand)),
{ name: 'Lernmodus', short_name: 'Lernen', url: '/karten/lernen' },
{ name: 'Berichtsheft', short_name: 'Bericht', url: '/berichtsheft' },
{ name: 'Handouts', short_name: 'Handouts', url: '/handouts' },
],
}; };
} }
+9 -2
View File
@@ -7,6 +7,7 @@ import { berichteVon, fehlendeWochen } from '../repos/berichtsheft';
import { ergebnisVon, staendeVon as kontrollStaende } from '../repos/kontrollen'; import { ergebnisVon, staendeVon as kontrollStaende } from '../repos/kontrollen';
import { alsDatumText, alsZeitpunktText } from '../zeit'; import { alsDatumText, alsZeitpunktText } from '../zeit';
import { berechneAbdeckung, berechneLernfelder, type Nachweis } from './abdeckung'; import { berechneAbdeckung, berechneLernfelder, type Nachweis } from './abdeckung';
import { modulStand } from '../repos/module';
/** /**
* Sammelt aus Inhalten und Fortschritt alles, was die Auswertung braucht. * Sammelt aus Inhalten und Fortschritt alles, was die Auswertung braucht.
@@ -17,6 +18,9 @@ import { berechneAbdeckung, berechneLernfelder, type Nachweis } from './abdeckun
export function rahmenplanabdeckung(azubiId: string) { export function rahmenplanabdeckung(azubiId: string) {
const alle = inhalte(); const alle = inhalte();
// Abgeschaltete Module liefern keine Nachweise. Sonst stünden ihre Inhalte
// für immer als „geplant, noch nicht bearbeitet" da.
const eingeschaltet = modulStand();
const gelesen = gelesenSet(azubiId); const gelesen = gelesenSet(azubiId);
const aufgaben = aufgabenStaende(azubiId); const aufgaben = aufgabenStaende(azubiId);
const laeufe = laeufeVon(azubiId); const laeufe = laeufeVon(azubiId);
@@ -47,7 +51,7 @@ export function rahmenplanabdeckung(azubiId: string) {
} }
// --- Praxisaufgaben --- // --- Praxisaufgaben ---
for (const aufgabe of alle.aufgaben) { for (const aufgabe of eingeschaltet.aufgaben ? alle.aufgaben : []) {
const stand = aufgaben.get(aufgabe.id); const stand = aufgaben.get(aufgabe.id);
const nachweis: Nachweis = { const nachweis: Nachweis = {
art: 'aufgabe', art: 'aufgabe',
@@ -61,7 +65,7 @@ export function rahmenplanabdeckung(azubiId: string) {
} }
// --- Checklisten --- // --- Checklisten ---
for (const liste of alle.checklisten) { for (const liste of eingeschaltet.checklisten ? alle.checklisten : []) {
const abgeschlossenAm = abgeschlosseneListen.get(liste.id); const abgeschlossenAm = abgeschlosseneListen.get(liste.id);
const nachweis: Nachweis = { const nachweis: Nachweis = {
art: 'checkliste', art: 'checkliste',
@@ -75,6 +79,7 @@ export function rahmenplanabdeckung(azubiId: string) {
} }
return { return {
module: eingeschaltet,
positionen: berechneAbdeckung(alle.positionen, jePosition), positionen: berechneAbdeckung(alle.positionen, jePosition),
lernfelder: berechneLernfelder(alle.lernfelder, jeLernfeld), lernfelder: berechneLernfelder(alle.lernfelder, jeLernfeld),
gruppennamen: alle.rahmenplan.gruppen, gruppennamen: alle.rahmenplan.gruppen,
@@ -84,6 +89,7 @@ export function rahmenplanabdeckung(azubiId: string) {
/** Der persönliche Stand des Azubis – was er selbst sehen will. */ /** Der persönliche Stand des Azubis – was er selbst sehen will. */
export function eigenerStand(azubiId: string, beginn: string | null) { export function eigenerStand(azubiId: string, beginn: string | null) {
const alle = inhalte(); const alle = inhalte();
const eingeschaltet = modulStand();
const gelesen = gelesenSet(azubiId); const gelesen = gelesenSet(azubiId);
const aufgaben = aufgabenStaende(azubiId); const aufgaben = aufgabenStaende(azubiId);
const karten = kartenUebersicht(azubiId, alle.karten.map((k) => k.id)); const karten = kartenUebersicht(azubiId, alle.karten.map((k) => k.id));
@@ -111,6 +117,7 @@ export function eigenerStand(azubiId: string, beginn: string | null) {
.sort((a, b) => a.woche - b.woche); .sort((a, b) => a.woche - b.woche);
return { return {
module: eingeschaltet,
handouts: { erledigt: gelesen.size, gesamt: alle.handouts.length }, handouts: { erledigt: gelesen.size, gesamt: alle.handouts.length },
karten: { erledigt: karten.gelernt, gesamt: karten.gesamt, faellig: karten.faellig }, karten: { erledigt: karten.gelernt, gesamt: karten.gesamt, faellig: karten.faellig },
aufgaben: { aufgaben: {
+15
View File
@@ -0,0 +1,15 @@
-- Fassung 1.1: Module ein- und ausschalten.
--
-- Eine Zeile je Modul, das schon einmal umgeschaltet wurde. Fehlt die Zeile,
-- ist das Modul eingeschaltet – eine frische Datenbank hat alles an, und eine
-- bestehende verhält sich nach dem Einspielen genau wie vorher.
--
-- Abschalten löscht nichts. Die Daten eines Moduls bleiben in ihren Tabellen
-- und sind nach dem Wiedereinschalten unverändert da.
CREATE TABLE modul_einstellung (
modul TEXT PRIMARY KEY,
aktiv INTEGER NOT NULL CHECK (aktiv IN (0, 1)),
geaendert_von TEXT REFERENCES benutzer (id) ON DELETE SET NULL,
geaendert_am TEXT NOT NULL
);
+10
View File
@@ -323,6 +323,14 @@ export const feedbackgespraech = sqliteTable(
(t) => [index('feedbackgespraech_ausbildung_idx').on(t.ausbildungId, t.datum)], (t) => [index('feedbackgespraech_ausbildung_idx').on(t.ausbildungId, t.datum)],
); );
/** Abschaltbare Module – eine Zeile je Modul, das je umgeschaltet wurde. Siehe lib/module. */
export const modulEinstellung = sqliteTable('modul_einstellung', {
modul: text('modul').primaryKey(),
aktiv: integer('aktiv').notNull(),
geaendertVon: text('geaendert_von').references(() => benutzer.id, { onDelete: 'set null' }),
geaendertAm: text('geaendert_am').notNull(),
});
export type Benutzer = typeof benutzer.$inferSelect; export type Benutzer = typeof benutzer.$inferSelect;
export type Sitzung = typeof sitzung.$inferSelect; export type Sitzung = typeof sitzung.$inferSelect;
export type Ausbildung = typeof ausbildung.$inferSelect; export type Ausbildung = typeof ausbildung.$inferSelect;
@@ -339,6 +347,7 @@ export type Wochenbericht = typeof wochenbericht.$inferSelect;
export type Kontrolle = typeof kontrolle.$inferSelect; export type Kontrolle = typeof kontrolle.$inferSelect;
export type KontrollAntwort = typeof kontrollAntwort.$inferSelect; export type KontrollAntwort = typeof kontrollAntwort.$inferSelect;
export type Feedbackgespraech = typeof feedbackgespraech.$inferSelect; export type Feedbackgespraech = typeof feedbackgespraech.$inferSelect;
export type ModulEinstellung = typeof modulEinstellung.$inferSelect;
/** Wird von der Schema-Abgleichprüfung im Test verwendet. */ /** Wird von der Schema-Abgleichprüfung im Test verwendet. */
export const alleTabellen = { export const alleTabellen = {
@@ -358,4 +367,5 @@ export const alleTabellen = {
kontrolle, kontrolle,
kontrollAntwort, kontrollAntwort,
feedbackgespraech, feedbackgespraech,
modulEinstellung,
}; };
+105
View File
@@ -0,0 +1,105 @@
import { describe, expect, it } from 'vitest';
import {
abgeschaltet,
alleAn,
istModulId,
modulIds,
moduleintraege,
modulVonPfad,
nurOffene,
pfadOffen,
standAus,
} from './module';
describe('standAus', () => {
it('hat in einer frischen Datenbank alles an', () => {
expect(standAus([])).toEqual(alleAn);
expect(Object.values(standAus([])).every(Boolean)).toBe(true);
});
it('übernimmt gespeicherte Schalter', () => {
const stand = standAus([
{ modul: 'berichtsheft', aktiv: 0 },
{ modul: 'karten', aktiv: 1 },
]);
expect(stand.berichtsheft).toBe(false);
expect(stand.karten).toBe(true);
expect(stand.aufgaben).toBe(true);
});
it('übergeht unbekannte Kennungen, etwa aus einer späteren Fassung', () => {
const stand = standAus([{ modul: 'gibtsnicht', aktiv: 0 }]);
expect(stand).toEqual(alleAn);
expect('gibtsnicht' in stand).toBe(false);
});
});
describe('modulVonPfad', () => {
it('ordnet Seiten und Unterseiten ihrem Modul zu', () => {
expect(modulVonPfad('/berichtsheft')).toBe('berichtsheft');
expect(modulVonPfad('/berichtsheft/abc/pdf')).toBe('berichtsheft');
expect(modulVonPfad('/karten/lernen')).toBe('karten');
expect(modulVonPfad('/checklisten/lauf/123')).toBe('checklisten');
});
it('ordnet Routen außerhalb der Seiten ihrem Modul zu', () => {
expect(modulVonPfad('/anhaenge/abc')).toBe('aufgaben');
expect(modulVonPfad('/api/lernstand')).toBe('karten');
});
it('lässt das Gerüst unangetastet', () => {
for (const pfad of ['/', '/wochenplan', '/woche/3', '/handouts', '/handouts/N3', '/glossar', '/auswertung', '/verwaltung', '/verwaltung/module']) {
expect(modulVonPfad(pfad), pfad).toBeNull();
}
});
it('trifft nur ganze Pfadteile', () => {
// „/kartenspiel" gehört nicht zu den Merkkarten.
expect(modulVonPfad('/kartenspiel')).toBeNull();
expect(modulVonPfad('/feedbackbogen')).toBeNull();
});
});
describe('pfadOffen und nurOffene', () => {
const navigation = [
{ pfad: '/', titel: 'Woche' },
{ pfad: '/karten', titel: 'Merkkarten' },
{ pfad: '/berichtsheft', titel: 'Berichtsheft' },
{ pfad: '/glossar', titel: 'Glossar' },
];
it('lässt bei allem an die Navigation unverändert', () => {
expect(nurOffene(navigation, alleAn)).toEqual(navigation);
});
it('entfernt genau die abgeschalteten Module', () => {
const stand = { ...alleAn, berichtsheft: false };
expect(nurOffene(navigation, stand).map((e) => e.titel)).toEqual(['Woche', 'Merkkarten', 'Glossar']);
expect(pfadOffen('/berichtsheft/pdf', stand)).toBe(false);
expect(pfadOffen('/karten/lernen', stand)).toBe(true);
});
it('behält das Gerüst auch, wenn alles aus ist', () => {
const alleAus = Object.fromEntries(modulIds.map((id) => [id, false])) as typeof alleAn;
expect(nurOffene(navigation, alleAus).map((e) => e.titel)).toEqual(['Woche', 'Glossar']);
expect(abgeschaltet(alleAus)).toEqual([...modulIds]);
});
});
describe('Moduleinträge', () => {
it('beschreibt jedes Modul vollständig', () => {
for (const id of modulIds) {
const eintrag = moduleintraege[id];
expect(eintrag.id).toBe(id);
expect(eintrag.titel.length, id).toBeGreaterThan(0);
expect(eintrag.beschreibung.length, id).toBeGreaterThan(10);
expect(eintrag.folgen.length, id).toBeGreaterThan(10);
expect(eintrag.pfade.length, id).toBeGreaterThan(0);
}
});
it('erkennt gültige Kennungen', () => {
expect(istModulId('karten')).toBe(true);
expect(istModulId('verwaltung')).toBe(false);
});
});
+128
View File
@@ -0,0 +1,128 @@
/**
* Die abschaltbaren Module der Lern-Bar.
*
* Bewusst ohne Datenbank und ohne Next.js – die Liste und die Regeln stehen
* hier, der gespeicherte Zustand kommt aus `repos/module`, die Durchsetzung aus
* `module/pruefen`. So lassen sich die Regeln vollständig testen.
*
* Immer an bleiben Wochenplan, Handouts, Glossar, Auswertung und Verwaltung:
* Sie sind das Gerüst, an dem alles andere hängt. Die Auswertung passt sich an
* und zeigt nur, was eingeschaltet ist.
*
* Abschalten heißt ausblenden und sperren, **nicht löschen**. Was ein Azubi in
* einem Modul geschrieben hat, bleibt in der Datenbank und ist nach dem
* Wiedereinschalten unverändert da. Siehe E43.
*/
export const modulIds = ['karten', 'aufgaben', 'checklisten', 'berichtsheft', 'kontrollen', 'feedback'] as const;
export type ModulId = (typeof modulIds)[number];
/** Für jedes Modul: an oder aus. */
export type ModulStand = Record<ModulId, boolean>;
export type Moduleintrag = {
id: ModulId;
titel: string;
/** Was das Modul tut – für die Verwaltung. */
beschreibung: string;
/** Was sich ändert, wenn es aus ist – damit niemand überrascht wird. */
folgen: string;
/** Die Pfade, die zu diesem Modul gehören und mit ihm gesperrt werden. */
pfade: string[];
};
export const moduleintraege: Record<ModulId, Moduleintrag> = {
karten: {
id: 'karten',
titel: 'Merkkarten',
beschreibung: 'Wiederholung nach Zeitabstand, täglicher Stapel, Lernen auch ohne Netz.',
folgen:
'Merkkarten verschwinden aus Handouts, Wochenfortschritt und „Mein Stand". Bewertungen, die ein Gerät noch ohne Netz gesammelt hat, werden verworfen.',
pfade: ['/karten', '/api/lernstand'],
},
aufgaben: {
id: 'aufgaben',
titel: 'Praxisaufgaben',
beschreibung: 'Aufträge mit Bewertungskriterien, Notizen und Anhängen, Abnahme mit Note.',
folgen:
'Aufgaben verschwinden aus Wochenplan, „Was auf dich wartet" und Auswertung. In der Rahmenplanabdeckung zählen sie nicht mehr; Anhänge sind nicht mehr abrufbar.',
pfade: ['/aufgaben', '/anhaenge'],
},
checklisten: {
id: 'checklisten',
titel: 'Checklisten',
beschreibung: 'Abläufe Schritt für Schritt abhaken, Durchläufe als Nachweis mit PDF.',
folgen:
'Checklisten verschwinden aus Wochenplan und Wochenfortschritt. In der Rahmenplanabdeckung und bei den Vorschlägen fürs Berichtsheft zählen sie nicht mehr.',
pfade: ['/checklisten'],
},
berichtsheft: {
id: 'berichtsheft',
titel: 'Berichtsheft',
beschreibung: 'Tagesnotizen, Wochenblätter mit Vorschlägen, Einreichen, Abzeichnen und PDF im IHK-Layout.',
folgen:
'Das Berichtsheft verschwindet aus Navigation, „Was auf dich wartet" und „Mein Stand". Den Ausbildungsnachweis muss der Azubi dann anderswo führen – auf Papier oder im Portal der Kammer.',
pfade: ['/berichtsheft'],
},
kontrollen: {
id: 'kontrollen',
titel: 'Lernzielkontrollen',
beschreibung: 'Freigabe durch den Ausbilder, Abgabe, Korrektur mit Punkten und IHK-Notenschlüssel.',
folgen:
'Kontrollen verschwinden aus Wochenplan, Wochenfortschritt, „Was auf dich wartet" und dem Ergebnisverlauf der Auswertung.',
pfade: ['/kontrollen'],
},
feedback: {
id: 'feedback',
titel: 'Feedbackgespräch',
beschreibung: 'Leitfaden und Bewertungsbogen, getrennte Einschätzung beider Seiten, PDF für die Akte.',
folgen: 'Der Bereich „Feedback" verschwindet aus der Navigation. Bereits geführte Gespräche bleiben gespeichert.',
pfade: ['/feedback'],
},
};
/** Werkszustand: alles an. */
export const alleAn: ModulStand = Object.fromEntries(modulIds.map((id) => [id, true])) as ModulStand;
export function istModulId(wert: string): wert is ModulId {
return (modulIds as readonly string[]).includes(wert);
}
/**
* Baut den Stand aus gespeicherten Zeilen. Fehlt ein Modul, gilt es als an –
* eine frische Datenbank hat alles eingeschaltet. Unbekannte Kennungen (etwa
* aus einer späteren Fassung) werden übergangen.
*/
export function standAus(zeilen: { modul: string; aktiv: number }[]): ModulStand {
const stand: ModulStand = { ...alleAn };
for (const zeile of zeilen) {
if (istModulId(zeile.modul)) stand[zeile.modul] = zeile.aktiv === 1;
}
return stand;
}
/** Zu welchem Modul gehört ein Pfad? `null`, wenn zu keinem abschaltbaren. */
export function modulVonPfad(pfad: string): ModulId | null {
for (const id of modulIds) {
for (const wurzel of moduleintraege[id].pfade) {
if (pfad === wurzel || pfad.startsWith(`${wurzel}/`)) return id;
}
}
return null;
}
/** Ist ein Pfad bei diesem Stand erreichbar? */
export function pfadOffen(pfad: string, stand: ModulStand): boolean {
const modul = modulVonPfad(pfad);
return modul === null || stand[modul];
}
/** Lässt aus einer Liste von Zielen nur die erreichbaren übrig – für Navigation und Kurzbefehle. */
export function nurOffene<T extends { pfad: string }>(eintraege: T[], stand: ModulStand): T[] {
return eintraege.filter((eintrag) => pfadOffen(eintrag.pfad, stand));
}
/** Die abgeschalteten Module, in fester Reihenfolge. */
export function abgeschaltet(stand: ModulStand): ModulId[] {
return modulIds.filter((id) => !stand[id]);
}
+40
View File
@@ -0,0 +1,40 @@
import 'server-only';
import { notFound } from 'next/navigation';
import { modulStand } from '../repos/module';
import { moduleintraege, type ModulId } from './module';
/**
* Die Durchsetzung der Modulschalter. Ein abgeschaltetes Modul ist nicht nur
* unsichtbar, sondern zu – an drei Stellen:
*
* - **Seiten** rufen `erfordereModul` auf und enden mit „nicht gefunden",
* genau wie bei fehlender Berechtigung (E21).
* - **Routen** (PDF, Anhänge, Lernstand) fragen `modulAn` und antworten selbst.
* - **Serveraktionen** rufen `pruefeModul` auf. Ein Formular, das noch in einem
* offenen Fenster steht, darf nach dem Abschalten nichts mehr schreiben.
*
* Bewusst nicht im Layout des Moduls: Next.js rendert Layouts beim Wechsel
* zwischen Seiten darunter nicht neu, eine Prüfung dort würde also nicht bei
* jedem Aufruf greifen.
*/
export function modulAn(modul: ModulId): boolean {
return modulStand()[modul];
}
/** Für Seiten: abgeschaltet heißt „nicht gefunden". */
export function erfordereModul(modul: ModulId): void {
if (!modulAn(modul)) notFound();
}
/** Für Serveraktionen: abgeschaltet heißt abbrechen, bevor etwas geschrieben wird. */
export function pruefeModul(modul: ModulId): void {
if (!modulAn(modul)) {
throw new Error(`Das Modul „${moduleintraege[modul].titel}" ist abgeschaltet.`);
}
}
/** Für Routen: die Antwort, wenn das Modul aus ist. */
export function modulGesperrt(): Response {
return new Response('Nicht gefunden.', { status: 404 });
}
+6
View File
@@ -100,6 +100,12 @@ export async function warteschlangeAbschicken(benutzerId: string): Promise<numbe
credentials: 'same-origin', credentials: 'same-origin',
}); });
// Das Modul Merkkarten ist abgeschaltet: Die Bewertungen werden nie mehr
// angenommen, also nicht ewig weiter mitschleppen.
if (antwort.status === 410) {
warteschlangeLeeren(benutzerId);
return 0;
}
if (!antwort.ok) return null; if (!antwort.ok) return null;
const ergebnis: unknown = await antwort.json(); const ergebnis: unknown = await antwort.json();
+19 -6
View File
@@ -7,6 +7,8 @@ import { anzahlAbgenommen } from '../repos/aufgaben';
import { anzahlAbgeschlossen } from '../repos/checklisten'; import { anzahlAbgeschlossen } from '../repos/checklisten';
import { anzahlAbgegeben } from '../repos/kontrollen'; import { anzahlAbgegeben } from '../repos/kontrollen';
import { heute } from '../zeit'; import { heute } from '../zeit';
import { modulStand } from '../repos/module';
import type { ModulStand } from '../module/module';
import { aktuelleWoche } from './wochen'; import { aktuelleWoche } from './wochen';
import { wochenFortschritt, type Fortschritt } from './fortschritt'; import { wochenFortschritt, type Fortschritt } from './fortschritt';
import type { Wochenplanung } from '../db/schema'; import type { Wochenplanung } from '../db/schema';
@@ -34,6 +36,8 @@ export type Wochensicht = {
aufgaben: Aufgabe[]; aufgaben: Aufgabe[];
checklisten: Checkliste[]; checklisten: Checkliste[];
kontrolle: Kontrolle | null; kontrolle: Kontrolle | null;
/** Welche Module an sind – die Ansicht blendet die übrigen aus. */
module: ModulStand;
vorherige: number | null; vorherige: number | null;
naechste: number | null; naechste: number | null;
}; };
@@ -66,14 +70,22 @@ export function wochensicht(ausbildungId: string, azubiId: string, wochenNummer:
}; };
}); });
const aufgaben = woche.aufgaben.map((id) => alle.aufgabeNach.get(id)).filter((a) => a !== undefined); // Ein abgeschaltetes Modul trägt nichts zur Woche bei – weder in der Ansicht
const checklisten = woche.checklisten.map((id) => alle.checklisteNach.get(id)).filter((c) => c !== undefined); // noch im Fortschritt. So erreicht eine Woche auch ohne Merkkarten 100 %.
const kontrolle = woche.lernzielkontrolle ? (alle.kontrolleNach.get(woche.lernzielkontrolle) ?? null) : null; const eingeschaltet = modulStand();
const aufgaben = eingeschaltet.aufgaben
? woche.aufgaben.map((id) => alle.aufgabeNach.get(id)).filter((a) => a !== undefined)
: [];
const checklisten = eingeschaltet.checklisten
? woche.checklisten.map((id) => alle.checklisteNach.get(id)).filter((c) => c !== undefined)
: [];
const kontrolle =
eingeschaltet.kontrollen && woche.lernzielkontrolle ? (alle.kontrolleNach.get(woche.lernzielkontrolle) ?? null) : null;
// Die Merkkarten dieser Woche sind die Karten ihrer Handouts. // Die Merkkarten dieser Woche sind die Karten ihrer Handouts.
const karteIds = handouts.flatMap(({ handout }) => const karteIds = eingeschaltet.karten
alle.karten.filter((k) => k.handoutId === handout.id).map((k) => k.id), ? handouts.flatMap(({ handout }) => alle.karten.filter((k) => k.handoutId === handout.id).map((k) => k.id))
); : [];
const kartenStand = kartenUebersicht(azubiId, karteIds, heuteDatum); const kartenStand = kartenUebersicht(azubiId, karteIds, heuteDatum);
const fortschritt = wochenFortschritt({ const fortschritt = wochenFortschritt({
@@ -102,6 +114,7 @@ export function wochensicht(ausbildungId: string, azubiId: string, wochenNummer:
aufgaben, aufgaben,
checklisten, checklisten,
kontrolle, kontrolle,
module: eingeschaltet,
vorherige: stelle > 0 ? (nummern[stelle - 1] ?? null) : null, vorherige: stelle > 0 ? (nummern[stelle - 1] ?? null) : null,
naechste: stelle < nummern.length - 1 ? (nummern[stelle + 1] ?? null) : null, naechste: stelle < nummern.length - 1 ? (nummern[stelle + 1] ?? null) : null,
}; };
+28
View File
@@ -0,0 +1,28 @@
import { datenbank, schema } from '../db';
import { jetzt } from '../zeit';
import { standAus, type ModulId, type ModulStand } from '../module/module';
/** Sämtlicher Zugriff auf die Tabelle `modul_einstellung` läuft über dieses Modul. */
/** Der aktuelle Stand aller Module. Ohne gespeicherte Zeile ist ein Modul an. */
export function modulStand(): ModulStand {
const zeilen = datenbank()
.select({ modul: schema.modulEinstellung.modul, aktiv: schema.modulEinstellung.aktiv })
.from(schema.modulEinstellung)
.all();
return standAus(zeilen);
}
/** Wer wann zuletzt umgeschaltet hat – für die Anzeige in der Verwaltung. */
export function modulAenderungen() {
return datenbank().select().from(schema.modulEinstellung).all();
}
export function modulSetzen(modul: ModulId, aktiv: boolean, benutzerId: string): void {
const werte = { aktiv: aktiv ? 1 : 0, geaendertVon: benutzerId, geaendertAm: jetzt() };
datenbank()
.insert(schema.modulEinstellung)
.values({ modul, ...werte })
.onConflictDoUpdate({ target: schema.modulEinstellung.modul, set: werte })
.run();
}