diff --git a/README.md b/README.md
index d4c9d2f..7fec97e 100644
--- a/README.md
+++ b/README.md
@@ -26,8 +26,9 @@ Begründung siehe `docs/distro-auswahl.md`.
linux-workstations/
├── README.md
├── docs/
-│ └── distro-auswahl.md # ADR Distribution
-├── install/ # Netinstall-Preseed, Partitionierungsnotizen (folgt)
+│ ├── distro-auswahl.md # ADR Distribution
+│ ├── installation.md # Schritt-für-Schritt Netinstall + Dualboot
+│ └── postinstall-ansible.md # Bootstrap & Ablauf
└── ansible/
├── ansible.cfg
├── inventory.yml
@@ -35,20 +36,30 @@ linux-workstations/
├── group_vars/all.yml
├── host_vars/{notebook,pc,werkstatt}.yml
└── roles/
- ├── base/ # Grundsystem, APT-Quellen, Firmware
- ├── desktop_kde/ # KDE Plasma + Anwendungen
+ ├── base/ # APT-Quellen, Locale, Firmware, Grundpakete
+ ├── desktop_kde/ # KDE Plasma 6 + SDDM + Apps
├── hardening/ # SSH, UFW, unattended-upgrades
- ├── dev_tools/ # Editor, Git, Sprachen
- └── workstation_apps/ # Browser, Office, Mediencodecs
+ ├── dev_tools/ # Git, Node.js (NodeSource), Python, Perl
+ ├── workstation_apps/ # Browser, Office, Multimedia
+ └── claude_workspace/ # Claude Code, ccusage, Workspace-Clone, ~/.claude/settings.json
```
-## Workflow
+## Workflow (Kurz)
+
+Siehe `docs/installation.md` und `docs/postinstall-ansible.md` für Details.
```bash
-# Auf einem Zielrechner nach Erstinstallation:
-ssh-copy-id tom@notebook
+# Nach Debian-Erstinstallation auf Zielrechner:
+ssh-copy-id tom@notebook.egonlebt.lan
ansible -i ansible/inventory.yml notebook -m ping
+
+ansible-playbook -i ansible/inventory.yml ansible/site.yml --limit notebook --check --diff
ansible-playbook -i ansible/inventory.yml ansible/site.yml --limit notebook
+
+# Anschließend manuell pro Maschine (~1 min):
+# 1. Public-Key aus ~/.ssh/id_ed25519_gitea.pub auf Gitea hochladen
+# 2. ansible-playbook ... --tags claude_workspace (holt Workspace nach)
+# 3. Auf dem Zielrechner: `claude` → OAuth-Login
```
## Repo
diff --git a/ansible/roles/base/tasks/main.yml b/ansible/roles/base/tasks/main.yml
index 1cc22d1..838fb64 100644
--- a/ansible/roles/base/tasks/main.yml
+++ b/ansible/roles/base/tasks/main.yml
@@ -1,3 +1,84 @@
---
-# Grundsystem: APT-Quellen, Lokalisierung, Firmware, Hilfspakete
-# TODO: implementieren — Skeleton-Datei
+- name: APT-Quellen mit contrib + non-free-firmware + (Backports)
+ ansible.builtin.copy:
+ dest: /etc/apt/sources.list
+ mode: '0644'
+ content: |
+ deb http://deb.debian.org/debian/ trixie main contrib non-free-firmware
+ deb http://security.debian.org/debian-security trixie-security main contrib non-free-firmware
+ deb http://deb.debian.org/debian/ trixie-updates main contrib non-free-firmware
+ {% if enable_backports | default(true) %}
+ deb http://deb.debian.org/debian/ trixie-backports main contrib non-free-firmware
+ {% endif %}
+ register: apt_sources
+
+- name: APT cache aktualisieren
+ ansible.builtin.apt:
+ update_cache: true
+ when: apt_sources.changed
+
+- name: Locale-Paket
+ ansible.builtin.apt:
+ name: locales
+ state: present
+ cache_valid_time: 3600
+
+- name: Locale aktivieren
+ ansible.builtin.lineinfile:
+ path: /etc/locale.gen
+ regexp: "^# ?{{ locale }} "
+ line: "{{ locale }} UTF-8"
+ register: locale_line
+
+- name: locale-gen ausführen
+ ansible.builtin.command: locale-gen
+ when: locale_line.changed
+
+- name: Standard-Locale setzen
+ ansible.builtin.copy:
+ dest: /etc/default/locale
+ mode: '0644'
+ content: "LANG={{ locale }}\n"
+
+- name: Tastaturlayout
+ ansible.builtin.copy:
+ dest: /etc/default/keyboard
+ mode: '0644'
+ content: |
+ XKBMODEL="pc105"
+ XKBLAYOUT="{{ keyboard_layout }}"
+ XKBVARIANT=""
+ XKBOPTIONS=""
+ BACKSPACE="guess"
+
+- name: Zeitzone
+ ansible.builtin.command: "timedatectl set-timezone {{ timezone }}"
+ changed_when: false
+
+- name: Grundpakete
+ ansible.builtin.apt:
+ name:
+ - sudo
+ - curl
+ - wget
+ - gnupg
+ - ca-certificates
+ - apt-transport-https
+ - vim
+ - htop
+ - tmux
+ - rsync
+ - net-tools
+ - dnsutils
+ - firmware-linux
+ - firmware-linux-nonfree
+ - lsb-release
+ - bash-completion
+ - man-db
+ state: present
+
+- name: Extra-Pakete je Host
+ ansible.builtin.apt:
+ name: "{{ extra_packages }}"
+ state: present
+ when: extra_packages | default([]) | length > 0
diff --git a/ansible/roles/claude_workspace/defaults/main.yml b/ansible/roles/claude_workspace/defaults/main.yml
new file mode 100644
index 0000000..31dacf3
--- /dev/null
+++ b/ansible/roles/claude_workspace/defaults/main.yml
@@ -0,0 +1,11 @@
+---
+claude_workspace_repo: "ssh://git@docker.egonlebt.lan:2222/egon/claude-workspace.git"
+claude_workspace_dest: "/home/{{ primary_user }}/Claude"
+gitea_ssh_host: docker.egonlebt.lan
+gitea_ssh_port: 2222
+claude_settings:
+ model: opus
+ advisorModel: opus
+ statusLine:
+ type: command
+ command: "node ~/Claude/.claude/statusline.js"
diff --git a/ansible/roles/claude_workspace/tasks/main.yml b/ansible/roles/claude_workspace/tasks/main.yml
new file mode 100644
index 0000000..5467516
--- /dev/null
+++ b/ansible/roles/claude_workspace/tasks/main.yml
@@ -0,0 +1,106 @@
+---
+# Setzt voraus: Node.js + git (aus dev_tools), npm verfügbar
+
+- name: Claude Code (npm global)
+ ansible.builtin.command: npm install -g @anthropic-ai/claude-code
+ args:
+ creates: /usr/lib/node_modules/@anthropic-ai/claude-code/package.json
+
+- name: ccusage (npm global, für Statusline-Tokenverbrauch)
+ ansible.builtin.command: npm install -g ccusage
+ args:
+ creates: /usr/lib/node_modules/ccusage/package.json
+
+- name: ~/.ssh existiert
+ become_user: "{{ primary_user }}"
+ ansible.builtin.file:
+ path: "/home/{{ primary_user }}/.ssh"
+ state: directory
+ mode: '0700'
+
+- name: SSH-Key für Gitea (ed25519, ohne Passphrase)
+ become_user: "{{ primary_user }}"
+ ansible.builtin.command: >
+ ssh-keygen -t ed25519
+ -f /home/{{ primary_user }}/.ssh/id_ed25519_gitea
+ -N "" -C "{{ primary_user }}@{{ inventory_hostname }} -> gitea"
+ args:
+ creates: "/home/{{ primary_user }}/.ssh/id_ed25519_gitea"
+
+- name: known_hosts für Gitea vorpopulieren
+ become_user: "{{ primary_user }}"
+ ansible.builtin.shell: |
+ ssh-keyscan -p {{ gitea_ssh_port }} -H {{ gitea_ssh_host }} 2>/dev/null \
+ | grep -v '^#' >> /home/{{ primary_user }}/.ssh/known_hosts
+ sort -u /home/{{ primary_user }}/.ssh/known_hosts \
+ -o /home/{{ primary_user }}/.ssh/known_hosts
+ args:
+ creates: "/home/{{ primary_user }}/.ssh/known_hosts"
+
+- name: SSH-Config für Gitea
+ become_user: "{{ primary_user }}"
+ ansible.builtin.blockinfile:
+ path: "/home/{{ primary_user }}/.ssh/config"
+ create: true
+ mode: '0600'
+ marker: "# {mark} ANSIBLE MANAGED — gitea"
+ block: |
+ Host {{ gitea_ssh_host }}
+ Port {{ gitea_ssh_port }}
+ IdentityFile ~/.ssh/id_ed25519_gitea
+ IdentitiesOnly yes
+ User git
+
+- name: Check ob Workspace schon geklont
+ become_user: "{{ primary_user }}"
+ ansible.builtin.stat:
+ path: "{{ claude_workspace_dest }}/.git"
+ register: ws_git
+
+- name: claude-workspace klonen (mit Submodules)
+ become_user: "{{ primary_user }}"
+ ansible.builtin.git:
+ repo: "{{ claude_workspace_repo }}"
+ dest: "{{ claude_workspace_dest }}"
+ recursive: true
+ update: false
+ accept_hostkey: true
+ key_file: "/home/{{ primary_user }}/.ssh/id_ed25519_gitea"
+ when: not ws_git.stat.exists
+ ignore_errors: true # scheitert bevor Pubkey in Gitea liegt — wird erneut versucht
+ register: clone_result
+
+- name: ~/.claude existiert
+ become_user: "{{ primary_user }}"
+ ansible.builtin.file:
+ path: "/home/{{ primary_user }}/.claude"
+ state: directory
+ mode: '0700'
+
+- name: Globale Claude-Settings (~/.claude/settings.json)
+ become_user: "{{ primary_user }}"
+ ansible.builtin.copy:
+ dest: "/home/{{ primary_user }}/.claude/settings.json"
+ mode: '0644'
+ content: "{{ claude_settings | to_nice_json }}\n"
+
+- name: Public-Key für Gitea-Upload anzeigen
+ become_user: "{{ primary_user }}"
+ ansible.builtin.command: "cat /home/{{ primary_user }}/.ssh/id_ed25519_gitea.pub"
+ register: pubkey
+ changed_when: false
+
+- name: HINWEIS — Public-Key auf Gitea hochladen
+ ansible.builtin.debug:
+ msg:
+ - "==========================================================="
+ - "Public-Key dieser Maschine ({{ inventory_hostname }}):"
+ - ""
+ - "{{ pubkey.stdout }}"
+ - ""
+ - "→ http://{{ gitea_ssh_host }}:3000/user/settings/keys"
+ - " → 'Schlüssel hinzufügen', oben einfügen, speichern."
+ - ""
+ - "Danach ggf. Workspace nachholen:"
+ - " ansible-playbook ... --tags claude_workspace --limit {{ inventory_hostname }}"
+ - "==========================================================="
diff --git a/ansible/roles/desktop_kde/tasks/main.yml b/ansible/roles/desktop_kde/tasks/main.yml
index 52e3ac7..4a413a7 100644
--- a/ansible/roles/desktop_kde/tasks/main.yml
+++ b/ansible/roles/desktop_kde/tasks/main.yml
@@ -1,3 +1,39 @@
---
-# KDE Plasma 6, SDDM, KDE-Anwendungen
-# TODO: implementieren — Skeleton-Datei
+- name: KDE Plasma + SDDM (Standardumfang ohne riesige Discover-Hänger)
+ ansible.builtin.apt:
+ name:
+ - kde-plasma-desktop
+ - sddm
+ - plasma-nm
+ - plasma-pa
+ - kde-config-sddm
+ - konsole
+ - dolphin
+ - kate
+ - okular
+ - gwenview
+ - spectacle
+ - ark
+ - kcalc
+ - kfind
+ - partitionmanager
+ - xdg-utils
+ - fonts-noto
+ - fonts-noto-color-emoji
+ state: present
+ install_recommends: false
+
+- name: SDDM aktivieren als Default-Display-Manager
+ ansible.builtin.copy:
+ dest: /etc/X11/default-display-manager
+ mode: '0644'
+ content: "/usr/bin/sddm\n"
+
+- name: Graphical Target als Default
+ ansible.builtin.command: systemctl set-default graphical.target
+ changed_when: false
+
+- name: SDDM aktivieren
+ ansible.builtin.systemd:
+ name: sddm
+ enabled: true
diff --git a/ansible/roles/dev_tools/tasks/main.yml b/ansible/roles/dev_tools/tasks/main.yml
index 3ec1b20..7e920a7 100644
--- a/ansible/roles/dev_tools/tasks/main.yml
+++ b/ansible/roles/dev_tools/tasks/main.yml
@@ -1,3 +1,50 @@
---
-# Git, Editor, Sprachen (Python/Node/Perl für FHEM)
-# TODO: implementieren — Skeleton-Datei
+- name: Dev-Pakete (Sprachen, Build-Tools, Editor)
+ ansible.builtin.apt:
+ name:
+ - git
+ - build-essential
+ - python3
+ - python3-venv
+ - python3-pip
+ - pipx
+ - perl
+ - jq
+ - direnv
+ - shellcheck
+ - meld
+ state: present
+
+- name: NodeSource Keyring (für Node.js LTS)
+ ansible.builtin.get_url:
+ url: https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key
+ dest: /etc/apt/keyrings/nodesource.asc
+ mode: '0644'
+ register: ns_key
+
+- name: NodeSource Repo
+ ansible.builtin.copy:
+ dest: /etc/apt/sources.list.d/nodesource.list
+ mode: '0644'
+ content: "deb [signed-by=/etc/apt/keyrings/nodesource.asc] https://deb.nodesource.com/node_20.x nodistro main\n"
+ register: ns_repo
+
+- name: APT update nach NodeSource
+ ansible.builtin.apt:
+ update_cache: true
+ when: ns_key.changed or ns_repo.changed
+
+- name: Node.js (LTS) installieren
+ ansible.builtin.apt:
+ name: nodejs
+ state: present
+
+- name: Git globale Defaults für {{ primary_user }}
+ become_user: "{{ primary_user }}"
+ ansible.builtin.command: "git config --global {{ item.k }} {{ item.v }}"
+ loop:
+ - { k: 'user.name', v: 'egon' }
+ - { k: 'user.email', v: 'egon@egonlebt.de' }
+ - { k: 'pull.rebase', v: 'true' }
+ - { k: 'init.defaultBranch', v: 'main' }
+ changed_when: false
diff --git a/ansible/roles/hardening/handlers/main.yml b/ansible/roles/hardening/handlers/main.yml
new file mode 100644
index 0000000..57d0861
--- /dev/null
+++ b/ansible/roles/hardening/handlers/main.yml
@@ -0,0 +1,5 @@
+---
+- name: restart sshd
+ ansible.builtin.systemd:
+ name: ssh
+ state: restarted
diff --git a/ansible/roles/hardening/tasks/main.yml b/ansible/roles/hardening/tasks/main.yml
index 5c7f4d4..565379f 100644
--- a/ansible/roles/hardening/tasks/main.yml
+++ b/ansible/roles/hardening/tasks/main.yml
@@ -1,3 +1,48 @@
---
-# SSH-Hardening, UFW, unattended-upgrades, fail2ban
-# TODO: implementieren — Skeleton-Datei
+- name: Security-Pakete
+ ansible.builtin.apt:
+ name:
+ - ufw
+ - unattended-upgrades
+ - apt-listchanges
+ state: present
+
+- name: SSH — Passwort-Login deaktivieren
+ ansible.builtin.lineinfile:
+ path: /etc/ssh/sshd_config
+ regexp: '^#?\s*PasswordAuthentication\s'
+ line: 'PasswordAuthentication no'
+ validate: 'sshd -t -f %s'
+ notify: restart sshd
+
+- name: SSH — Root-Login deaktivieren
+ ansible.builtin.lineinfile:
+ path: /etc/ssh/sshd_config
+ regexp: '^#?\s*PermitRootLogin\s'
+ line: 'PermitRootLogin no'
+ validate: 'sshd -t -f %s'
+ notify: restart sshd
+
+- name: UFW Default Policy (incoming deny)
+ ansible.builtin.command: ufw default deny incoming
+ register: ufw_default
+ changed_when: "'Default incoming policy changed' in ufw_default.stdout"
+
+- name: UFW — SSH erlauben
+ ansible.builtin.command: ufw allow OpenSSH
+ register: ufw_allow
+ changed_when: "'Rule added' in ufw_allow.stdout or 'Rules updated' in ufw_allow.stdout"
+
+- name: UFW aktivieren
+ ansible.builtin.command: ufw --force enable
+ register: ufw_enable
+ changed_when: "'Firewall is active' in ufw_enable.stdout"
+
+- name: unattended-upgrades konfigurieren
+ ansible.builtin.copy:
+ dest: /etc/apt/apt.conf.d/20auto-upgrades
+ mode: '0644'
+ content: |
+ APT::Periodic::Update-Package-Lists "1";
+ APT::Periodic::Unattended-Upgrade "1";
+ APT::Periodic::AutocleanInterval "7";
diff --git a/ansible/roles/workstation_apps/tasks/main.yml b/ansible/roles/workstation_apps/tasks/main.yml
index 9799787..0048c77 100644
--- a/ansible/roles/workstation_apps/tasks/main.yml
+++ b/ansible/roles/workstation_apps/tasks/main.yml
@@ -1,3 +1,16 @@
---
-# Browser, Office, Multimedia-Codecs
-# TODO: implementieren — Skeleton-Datei
+- name: Anwendungen (Browser, Office, Multimedia, Tools)
+ ansible.builtin.apt:
+ name:
+ - firefox-esr
+ - libreoffice
+ - libreoffice-l10n-de
+ - thunderbird
+ - thunderbird-l10n-de
+ - keepassxc
+ - vlc
+ - ffmpeg
+ - libavcodec-extra
+ - gimp
+ - inkscape
+ state: present
diff --git a/ansible/site.yml b/ansible/site.yml
index e779de3..ba8ef92 100644
--- a/ansible/site.yml
+++ b/ansible/site.yml
@@ -3,8 +3,15 @@
hosts: workstations
gather_facts: true
roles:
- - base
- - desktop_kde
- - hardening
- - dev_tools
- - workstation_apps
+ - role: base
+ tags: [base]
+ - role: hardening
+ tags: [hardening]
+ - role: dev_tools
+ tags: [dev_tools]
+ - role: desktop_kde
+ tags: [desktop, kde]
+ - role: workstation_apps
+ tags: [apps]
+ - role: claude_workspace
+ tags: [claude_workspace, claude]
diff --git a/docs/installation.md b/docs/installation.md
new file mode 100644
index 0000000..62a39a9
--- /dev/null
+++ b/docs/installation.md
@@ -0,0 +1,105 @@
+# Installation Debian 13 (Trixie) — Dualboot
+
+Diese Anleitung gilt für alle drei Workstations (notebook, pc, werkstatt). Unterschiede sind unten markiert.
+
+## 1. Vorbereitung in Windows
+
+1. **Updates fertigstellen**, dann `Datenträgerverwaltung` (`diskmgmt.msc`) öffnen
+2. **BitLocker** deaktivieren auf der Partition, die verkleinert wird, sonst startet Windows nach dem GRUB-Eintrag nicht mehr sauber
+3. Windows-Partition verkleinern → freien Bereich erzeugen:
+ - **Notebook:** mindestens 80 GB frei
+ - **PC:** mindestens 150 GB frei
+ - **Werkstatt:** mindestens 60 GB frei
+4. **Schnellstart** in Windows deaktivieren (`Energieoptionen → Auswählen, was beim Drücken …`), sonst bleibt das NTFS in einem halben Zustand und Grub kann es nicht sicher zeigen
+5. **BIOS/UEFI**:
+ - Boot-Mode: UEFI (nicht Legacy)
+ - **Secure Boot:** anlassen, Debian 13 unterstützt es out-of-the-box
+ - Fast Boot: aus (sonst kein USB-Boot)
+
+## 2. Boot-Stick erstellen
+
+ISO: `firmware-13.x.x-amd64-netinst.iso` von
+> Die **firmware**-Variante ist Pflicht — sonst fehlen WLAN-/Grafik-Treiber.
+
+Stick (mind. 2 GB) schreiben:
+- Windows: [Rufus](https://rufus.ie) → "DD-Image" Modus
+- Linux: `sudo dd if=firmware-13.x.x-amd64-netinst.iso of=/dev/sdX bs=4M status=progress oflag=sync`
+
+## 3. Debian-Installation
+
+Vom Stick booten (BIOS-Boot-Menu, meist F12/F10/Esc).
+
+### Wichtige Antworten im Installer
+
+| Schritt | Antwort |
+|---|---|
+| Sprache | Deutsch |
+| Tastatur | Deutsch |
+| Hostname | `notebook` / `pc` / `werkstatt` |
+| Domain | `egonlebt.lan` |
+| Root-Passwort | **leer lassen** → Tom bekommt automatisch `sudo` |
+| Benutzer | Tom Egon (`tom`) |
+| Zeitzone | Europe/Berlin |
+
+### Partitionierung
+
+**Notebook (mit LUKS):**
+- Methode: **Geführt — gesamten Laufwerk verwenden, mit verschlüsseltem LVM**
+- Wenn der Installer die Methode wegen Windows nicht anbietet → **Manuell**:
+ - ESP existiert bereits (Windows) — wiederverwenden, mountpoint `/boot/efi`
+ - 1 GB unverschlüsselte `/boot` (ext4)
+ - Rest: LUKS-Container → LVM:
+ - `lv-root` 30 GB ext4 → `/`
+ - `lv-home` Rest ext4 → `/home`
+ - `lv-swap` 8 GB Swap (= RAM-Größe für Hibernate)
+
+**PC + Werkstatt (ohne LUKS):**
+- Methode: **Manuell**
+ - ESP wiederverwenden, mountpoint `/boot/efi`
+ - 30 GB ext4 → `/`
+ - Rest ext4 → `/home`
+ - Swap-File statt -Partition (per `systemd` nachgereicht, einfacher)
+
+### Paketauswahl (Software-Auswahl)
+
+**Nur ankreuzen:**
+- ✅ SSH-Server
+- ✅ Standard-Systemwerkzeuge
+
+**Abwählen:**
+- ❌ Debian-Desktop, GNOME, KDE — kommt alles über Ansible
+
+So bleibt die Basis schlank (~1.2 GB).
+
+### GRUB
+
+- "GRUB auf primärem Laufwerk installieren" → **Ja**
+- Nach Reboot: GRUB zeigt Debian + "Windows Boot Manager"
+
+## 4. Erstboot (vor Ansible)
+
+Auf der Zielmaschine einloggen (Konsole), dann:
+
+```bash
+# IP merken
+ip -4 addr show | grep inet
+# SSH läuft schon (Paketauswahl). Test vom Admin-Rechner:
+# ssh tom@.egonlebt.lan
+```
+
+Auf dem **Admin-Rechner** (Windows mit OpenSSH, oder schon installierter Linux):
+
+```bash
+# Public Key kopieren — danach kein Passwort mehr nötig
+ssh-copy-id tom@notebook.egonlebt.lan
+```
+
+Damit ist die Basis bereit für den Ansible-Run → `docs/postinstall-ansible.md`.
+
+## 5. Sanity Checks
+
+```bash
+ssh tom@notebook.egonlebt.lan 'cat /etc/debian_version' # → 13.x
+ssh tom@notebook.egonlebt.lan 'sudo -n true && echo sudo-ok'
+ssh tom@notebook.egonlebt.lan 'efibootmgr -v | grep -E "debian|Windows"'
+```
diff --git a/docs/postinstall-ansible.md b/docs/postinstall-ansible.md
new file mode 100644
index 0000000..1e3172e
--- /dev/null
+++ b/docs/postinstall-ansible.md
@@ -0,0 +1,88 @@
+# Postinstall via Ansible
+
+Voraussetzung: Debian-Grundinstallation läuft, SSH-Login per Key funktioniert (siehe `installation.md`).
+
+## 1. Ansible auf dem Admin-Rechner
+
+Ansible kann von **jedem Rechner** ausgeführt werden, der die Zielmaschinen per SSH erreicht und Python 3 hat. Empfohlen: die erste fertige Linux-Workstation wird zum Admin-Rechner für die nächsten.
+
+Initial vom Windows-Rechner (WSL/Git-Bash) reicht auch:
+
+```bash
+# Linux/WSL
+sudo apt install -y ansible
+# oder via pipx
+pipx install ansible-core
+```
+
+## 2. Ablauf
+
+```bash
+cd ~/Claude/linux-workstations
+
+# Verbindungstest
+ansible -i ansible/inventory.yml notebook -m ping
+
+# Trockenlauf
+ansible-playbook -i ansible/inventory.yml ansible/site.yml --limit notebook --check --diff
+
+# Echter Run
+ansible-playbook -i ansible/inventory.yml ansible/site.yml --limit notebook
+```
+
+Die Playbook-Ausführung dauert beim ersten Mal ~20 min (KDE-Plasma-Pakete sind groß).
+
+## 3. Was nach `site.yml` fertig ist
+
+- Debian-Basis mit allen Updates, deutsche Locale, Zeitzone Berlin
+- KDE Plasma 6 + SDDM (Login-Manager) — Reboot fällig
+- SSH gehärtet (Key-only), UFW aktiv (nur SSH offen), `unattended-upgrades` an
+- Git, Node.js, Python, Editor, Browser, LibreOffice, Codecs
+- **Claude Code + ccusage installiert, `~/.claude/settings.json` mit Statusline gesetzt**
+- **`~/Claude` geklont (`--recurse-submodules`)**, Git-User auf `egon` konfiguriert
+- SSH-Key für Gitea generiert (`~/.ssh/id_ed25519_gitea`), `known_hosts` für `docker.egonlebt.lan:2222` vorpopuliert
+
+## 4. Restmanuelle Schritte
+
+Diese zwei Dinge muss man pro Maschine selbst tun — beides je 1 Minute.
+
+### 4.1 SSH-Public-Key auf Gitea hochladen
+
+Am Ende des Ansible-Runs zeigt die Rolle `claude_workspace` den Public-Key an. Wenn der Run schon vorbei ist:
+
+```bash
+ssh tom@notebook 'cat ~/.ssh/id_ed25519_gitea.pub'
+```
+
+→ in als neuen SSH-Key einfügen, Name z.B. `notebook-tom`.
+
+Danach auf dem Zielrechner einmal:
+
+```bash
+ssh -i ~/.ssh/id_ed25519_gitea -T git@docker.egonlebt.lan -p 2222
+# Antwort: "Hi there, egon! You've successfully authenticated…"
+```
+
+### 4.2 Claude Code anmelden
+
+Direkt am Zielrechner (oder per `ssh -t`):
+
+```bash
+claude
+# → folgt dem Browser-Login-Flow einmalig
+```
+
+Danach ist die Maschine voll einsatzbereit. Statusline, Hooks (Auto-Pull beim Start, Auto-Commit/Push beim Stop), MCP-Server (paperless, imap, ssh-infra) — alles aktiv, weil Konfiguration im Repo liegt.
+
+## 5. Wartung
+
+```bash
+# Alles aktualisieren (nur APT-Pakete + Konfig-Drift heilen):
+ansible-playbook -i ansible/inventory.yml ansible/site.yml
+
+# Eine einzelne Rolle:
+ansible-playbook -i ansible/inventory.yml ansible/site.yml --tags claude_workspace
+
+# Eine einzelne Maschine:
+ansible-playbook -i ansible/inventory.yml ansible/site.yml --limit werkstatt
+```