Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3009c53b5c | ||
|
|
44e6c8bb79 | ||
|
|
54ce7d87ae | ||
|
|
76836197b5 | ||
|
|
70a3cd2d28 |
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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.)
|
||||||
@@ -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.)
|
||||||
@@ -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.)
|
||||||
@@ -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.
|
||||||
@@ -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
@@ -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
@@ -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**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -254,7 +293,7 @@ mit Querverweisen.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 11. Merkkarten lernen
|
### 12. Merkkarten lernen
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -273,7 +312,7 @@ nachzuholen.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 12. Eine Praxisaufgabe abgeben
|
### 13. Eine Praxisaufgabe abgeben
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -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
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -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). |
|
||||||
|
|||||||
@@ -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 |
Generated
+2
-2
@@ -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
@@ -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": {
|
||||||
|
|||||||
@@ -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('/');
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -155,58 +155,64 @@ export function WochenAnsicht({
|
|||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="karte" aria-labelledby="woche-aufgaben">
|
{sicht.module.aufgaben ? (
|
||||||
<h2 id="woche-aufgaben" style={{ fontSize: '1rem' }}>
|
<section className="karte" aria-labelledby="woche-aufgaben">
|
||||||
Praxisaufgaben
|
<h2 id="woche-aufgaben" style={{ fontSize: '1rem' }}>
|
||||||
</h2>
|
Praxisaufgaben
|
||||||
{sicht.aufgaben.length === 0 ? (
|
</h2>
|
||||||
<p className="leise" style={{ marginBottom: 0 }}>Keine.</p>
|
{sicht.aufgaben.length === 0 ? (
|
||||||
) : (
|
<p className="leise" style={{ marginBottom: 0 }}>Keine.</p>
|
||||||
<ul className="stapel-eng" style={{ margin: 0, paddingLeft: '1.1rem' }}>
|
) : (
|
||||||
{sicht.aufgaben.map((aufgabe) => (
|
<ul className="stapel-eng" style={{ margin: 0, paddingLeft: '1.1rem' }}>
|
||||||
<li key={aufgabe.id}>
|
{sicht.aufgaben.map((aufgabe) => (
|
||||||
<strong>{aufgabe.id}</strong> {aufgabe.titel}
|
<li key={aufgabe.id}>
|
||||||
<br />
|
<strong>{aufgabe.id}</strong> {aufgabe.titel}
|
||||||
<span className="leise">{aufgabe.zeitrahmen}</span>
|
<br />
|
||||||
</li>
|
<span className="leise">{aufgabe.zeitrahmen}</span>
|
||||||
))}
|
</li>
|
||||||
</ul>
|
))}
|
||||||
)}
|
</ul>
|
||||||
</section>
|
)}
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<section className="karte" aria-labelledby="woche-checklisten">
|
{sicht.module.checklisten ? (
|
||||||
<h2 id="woche-checklisten" style={{ fontSize: '1rem' }}>
|
<section className="karte" aria-labelledby="woche-checklisten">
|
||||||
Checklisten
|
<h2 id="woche-checklisten" style={{ fontSize: '1rem' }}>
|
||||||
</h2>
|
Checklisten
|
||||||
{sicht.checklisten.length === 0 ? (
|
</h2>
|
||||||
<p className="leise" style={{ marginBottom: 0 }}>Keine.</p>
|
{sicht.checklisten.length === 0 ? (
|
||||||
) : (
|
<p className="leise" style={{ marginBottom: 0 }}>Keine.</p>
|
||||||
<ul className="stapel-eng" style={{ margin: 0, paddingLeft: '1.1rem' }}>
|
) : (
|
||||||
{sicht.checklisten.map((liste) => (
|
<ul className="stapel-eng" style={{ margin: 0, paddingLeft: '1.1rem' }}>
|
||||||
<li key={liste.id}>{liste.titel}</li>
|
{sicht.checklisten.map((liste) => (
|
||||||
))}
|
<li key={liste.id}>{liste.titel}</li>
|
||||||
</ul>
|
))}
|
||||||
)}
|
</ul>
|
||||||
</section>
|
)}
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
<section className="karte" aria-labelledby="woche-kontrolle">
|
{sicht.module.kontrollen ? (
|
||||||
<h2 id="woche-kontrolle" style={{ fontSize: '1rem' }}>
|
<section className="karte" aria-labelledby="woche-kontrolle">
|
||||||
Lernzielkontrolle
|
<h2 id="woche-kontrolle" style={{ fontSize: '1rem' }}>
|
||||||
</h2>
|
Lernzielkontrolle
|
||||||
{sicht.kontrolle ? (
|
</h2>
|
||||||
<p style={{ marginBottom: 0 }}>
|
{sicht.kontrolle ? (
|
||||||
{sicht.kontrolle.titel}
|
<p style={{ marginBottom: 0 }}>
|
||||||
<br />
|
{sicht.kontrolle.titel}
|
||||||
<span className="leise">
|
<br />
|
||||||
{sicht.kontrolle.fragen.length} Fragen ·{' '}
|
<span className="leise">
|
||||||
{sicht.kontrolle.fragen.reduce((summe, f) => summe + f.punkte, 0)} Punkte
|
{sicht.kontrolle.fragen.length} Fragen ·{' '}
|
||||||
{sicht.kontrolle.dauer_minuten ? ` · ${sicht.kontrolle.dauer_minuten} Minuten` : ''}
|
{sicht.kontrolle.fragen.reduce((summe, f) => summe + f.punkte, 0)} Punkte
|
||||||
</span>
|
{sicht.kontrolle.dauer_minuten ? ` · ${sicht.kontrolle.dauer_minuten} Minuten` : ''}
|
||||||
</p>
|
</span>
|
||||||
) : (
|
</p>
|
||||||
<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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -103,38 +103,44 @@ 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`}
|
||||||
/>
|
/>
|
||||||
<Kachel
|
{stand.module.karten ? (
|
||||||
titel="Merkkarten, die sitzen"
|
<Kachel
|
||||||
wert={`${stand.karten.erledigt} von ${stand.karten.gesamt}`}
|
titel="Merkkarten, die sitzen"
|
||||||
anteil={anteil(stand.karten.erledigt, stand.karten.gesamt)}
|
wert={`${stand.karten.erledigt} von ${stand.karten.gesamt}`}
|
||||||
fuss={stand.karten.faellig > 0 ? `${stand.karten.faellig} heute fällig` : 'heute nichts fällig'}
|
anteil={anteil(stand.karten.erledigt, stand.karten.gesamt)}
|
||||||
/>
|
fuss={stand.karten.faellig > 0 ? `${stand.karten.faellig} heute fällig` : 'heute nichts fällig'}
|
||||||
<Kachel
|
/>
|
||||||
titel="Praxisaufgaben abgenommen"
|
) : null}
|
||||||
wert={`${stand.aufgaben.erledigt} von ${stand.aufgaben.gesamt}`}
|
{stand.module.aufgaben ? (
|
||||||
anteil={anteil(stand.aufgaben.erledigt, stand.aufgaben.gesamt)}
|
<Kachel
|
||||||
fuss={
|
titel="Praxisaufgaben abgenommen"
|
||||||
stand.aufgaben.zurAbnahme > 0
|
wert={`${stand.aufgaben.erledigt} von ${stand.aufgaben.gesamt}`}
|
||||||
? `${stand.aufgaben.zurAbnahme} wartet auf Abnahme`
|
anteil={anteil(stand.aufgaben.erledigt, stand.aufgaben.gesamt)}
|
||||||
: `${stand.aufgaben.offen} noch offen`
|
fuss={
|
||||||
}
|
stand.aufgaben.zurAbnahme > 0
|
||||||
/>
|
? `${stand.aufgaben.zurAbnahme} wartet auf Abnahme`
|
||||||
<Kachel
|
: `${stand.aufgaben.offen} noch offen`
|
||||||
titel="Berichtsheft abgezeichnet"
|
}
|
||||||
wert={`${stand.berichtsheft.abgezeichnet} von ${stand.berichtsheft.gesamt}`}
|
/>
|
||||||
anteil={anteil(stand.berichtsheft.abgezeichnet, stand.berichtsheft.gesamt)}
|
) : null}
|
||||||
fuss={
|
{stand.module.berichtsheft ? (
|
||||||
stand.berichtsheft.fehlendeWochen > 0
|
<Kachel
|
||||||
? `${stand.berichtsheft.fehlendeWochen} Wochen ohne Blatt`
|
titel="Berichtsheft abgezeichnet"
|
||||||
: stand.berichtsheft.eingereicht > 0
|
wert={`${stand.berichtsheft.abgezeichnet} von ${stand.berichtsheft.gesamt}`}
|
||||||
? `${stand.berichtsheft.eingereicht} beim Ausbilder`
|
anteil={anteil(stand.berichtsheft.abgezeichnet, stand.berichtsheft.gesamt)}
|
||||||
: 'nichts offen'
|
fuss={
|
||||||
}
|
stand.berichtsheft.fehlendeWochen > 0
|
||||||
ton={stand.berichtsheft.fehlendeWochen > 0 ? 'fehler' : undefined}
|
? `${stand.berichtsheft.fehlendeWochen} Wochen ohne Blatt`
|
||||||
/>
|
: stand.berichtsheft.eingereicht > 0
|
||||||
|
? `${stand.berichtsheft.eingereicht} beim Ausbilder`
|
||||||
|
: 'nichts offen'
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
|||||||
@@ -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,24 +132,26 @@ export default async function HandoutSeite({ params }: { params: Promise<{ id: s
|
|||||||
</details>
|
</details>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<section aria-labelledby="karten" className="karte stapel-eng">
|
{modulStand().karten ? (
|
||||||
<h2 id="karten" style={{ marginBottom: '0.3rem' }}>
|
<section aria-labelledby="karten" className="karte stapel-eng">
|
||||||
Merkkarten zu diesem Handout
|
<h2 id="karten" style={{ marginBottom: '0.3rem' }}>
|
||||||
</h2>
|
Merkkarten zu diesem Handout
|
||||||
<p className="leise" style={{ margin: 0 }}>
|
</h2>
|
||||||
{handout.karten.length === 1 ? 'Eine Karte' : `${handout.karten.length} Karten`} – im Lernmodus tauchen sie
|
<p className="leise" style={{ margin: 0 }}>
|
||||||
nach Fälligkeit auf.
|
{handout.karten.length === 1 ? 'Eine Karte' : `${handout.karten.length} Karten`} – im Lernmodus tauchen sie
|
||||||
</p>
|
nach Fälligkeit auf.
|
||||||
<ol className="stapel-eng" style={{ margin: '0.5rem 0 0', paddingLeft: '1.2rem' }}>
|
</p>
|
||||||
{handout.karten.map((karte, index) => (
|
<ol className="stapel-eng" style={{ margin: '0.5rem 0 0', paddingLeft: '1.2rem' }}>
|
||||||
<li key={index}>
|
{handout.karten.map((karte, index) => (
|
||||||
<strong>{karte.frage}</strong>
|
<li key={index}>
|
||||||
<br />
|
<strong>{karte.frage}</strong>
|
||||||
<span className="leise">{karte.antwort}</span>
|
<br />
|
||||||
</li>
|
<span className="leise">{karte.antwort}</span>
|
||||||
))}
|
</li>
|
||||||
</ol>
|
))}
|
||||||
</section>
|
</ol>
|
||||||
|
</section>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{istAzubi ? (
|
{istAzubi ? (
|
||||||
<form action={gelesenUmschalten} className="nicht-drucken">
|
<form action={gelesenUmschalten} className="nicht-drucken">
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 „Module“ 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
|
||||||
|
|||||||
@@ -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,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
@@ -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' },
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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]);
|
||||||
|
}
|
||||||
@@ -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 });
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user