- JavaScript 86.5%
- CSS 11%
- Shell 1.6%
- PowerShell 0.4%
- HTML 0.3%
- Other 0.2%
|
All checks were successful
Tests / node (push) Successful in 19s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|---|---|---|
| .forgejo/workflows | ||
| data | ||
| deploy | ||
| docs/bilder | ||
| public | ||
| src | ||
| test | ||
| werkzeuge | ||
| .dockerignore | ||
| .gitattributes | ||
| .gitignore | ||
| CHANGELOG.md | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| server.js | ||
| start.cmd | ||
Immobilienverwaltung
Private Verwaltung der vermieteten Eigentumswohnungen: Stammdaten, Mietverhältnisse, Mieterhöhungen, Darlehen, Konten, Fristen — und die Dokumentenablage direkt in der Dropbox (oder per Abgleich mit Dropbox bzw. Google Drive).
Läuft lokal (Node ab Version 22, empfohlen 24 LTS, einmalig npm install) oder auf einem Server als Docker-Container.
Lokal hört der Server nur auf 127.0.0.1. Gemeinsame Funktionen kommen aus
tool-defaults (src/defaults/, public/assets/).
Ausführliche Anleitung zu jedem Bereich: Wiki.
Funktionsumfang
| Bereich | Was es kann |
|---|---|
| Portfolio-Übersicht | Kennzahlen über alle Objekte, Objekttabelle, Jahresrechnung mit AfA und Steuerwirkung, anstehende Fristen |
| Objekt-Assistent | Neues Objekt Schritt für Schritt: Stammdaten, Kauf, Verwaltung und Konten, Einheiten und Mieter, Finanzierung, Cloud-Ablage – Pflichtangaben einstellbar |
| Cloud-Abgleich | Ablage je Objekt mit einem Ordner in Dropbox oder Google Drive abgleichen, in beide Richtungen, per Knopf und nach Zeitplan |
| Objekt | Stammdaten, Kaufkosten, laufende Rechnung, steuerliche Wirkung, Kostenstände mit Gültigkeitsdatum, Darlehen und Konten |
| Eigenkapital-Rückfluss | Ab wann Anzahlung und Kaufnebenkosten wieder da sind – über die Kasse und mit Tilgung, mit und ohne geplante Mieterhöhungen, je Objekt und fürs Portfolio |
| Einheiten & Mieter | Mietverhältnisse je Einheit, Kaution, Historie und Planung der Mietanpassungen (Kaltmiete, Nebenkosten, Mietzuschuss) |
| Zahlungen | Soll/Ist je Monat, Einbehalte von Verwaltung und SEV, Mietzuschuss als eigene Buchung, Erfassen direkt am Monat, Serienerfassung über Zeiträume |
| Finanzierung | Darlehen mit Restschuld, Tilgungsplan, Zinsbindung, Bereitstellungsphase |
| Konten & Verwaltung | WEG-Hausverwaltung und SEV aus dem Adressbuch, Mietkonten, Kautionskonten |
| Dokumente | Einheitliche Ordnerstruktur, Upload per Drag & Drop, Texterkennung (Windows oder Tesseract im Docker-Image), „Dokumente einlesen“ mit Zuordnungsvorschlag, Suche in Dateinamen und Volltext |
| Adressbuch | Verwaltungen, Banken, Handwerker objektübergreifend, mit Verwendungsnachweis |
| Unterlagen | Vollständigkeitsprüfung: welche Unterlagen zu einem vermieteten Objekt gehören und welche davon in der Ablage liegen – am Dateinamen oder an der Überschrift im Dokument erkannt |
| Instanzen & Anmeldung | Mehrere getrennte Instanzen als Container, Anmeldung über Microsoft Entra ID (Gruppenfreigabe) oder Passwort, Name, Farben und eigenes CSS je Instanz |
| Hinweis-Mails | Fristen, fehlende Pflichtunterlagen und ausbleibende Zahlungen per E-Mail, ohne Wiederholungen |
| Fristen | Abgeleitet aus Zinsbindung, Mieterhöhungen, Sperrfrist (§ 558 BGB) und Betriebskostenfrist (§ 556 Abs. 3 BGB) |
Für welche Situationen es gebaut ist
- Miete läuft über die Sondereigentumsverwaltung – die zieht Hausgeld und Gebühren ab und überweist nur den Rest. Wirtschaftlicher Cashflow und tatsächliche Liquidität werden getrennt ausgewiesen.
- Beträge ändern sich über die Jahre – Kostenstände gelten ab einem Monat, rückwirkend erfasste Zeiträume rechnen mit den damaligen Werten.
- Zeit vor dem Nutzen-Lasten-Übergang – Bereitstellungszinsen laufen, Miete gibt es noch nicht, der Verkäufer zahlt vielleicht eine Entschädigung.
- Papierberg aus dem Kauf – ein Regelwerk sortiert Altbestand und Posteingang nach Dateinamen vor, entschieden wird per Häkchen.
Starten
cd <projektordner>
npm start
Danach im Browser: http://localhost:4711
Bequemer: Doppelklick auf start.cmd.
Wo liegen die Daten?
| Was | Wo |
|---|---|
| Dokumente | %USERPROFILE%\Dropbox\Immobilien (die Dropbox wird automatisch gefunden) |
| Stammdaten | data\daten.json (im Projektordner) |
| Gelöschte Dokumente | …\Dropbox\Immobilien\_Papierkorb\<Datum>\… |
Anderer Ablageort: Umgebungsvariable IMMO_BASIS vor dem Start setzen.
Ordnerstruktur je Objekt
Jedes Objekt bekommt dieselben Ordner — im Explorer wie in der Web-Oberfläche:
<Objekt>/
00_Posteingang noch nicht zugeordnet
01_Objektunterlagen Exposé, Grundriss, Wohnfläche, Energieausweis
02_Kauf-und-Notar Kaufvertrag, Notar, Grunderwerbsteuer, Überweisungen
03_Grundbuch-und-Recht Grundbuch, Teilungserklärung, Vollmachten
04_Finanzierung Darlehen, ESIS, Grundschuld, Zinsbescheinigung
05_WEG-Hausverwaltung Hausgeld, Wirtschaftsplan, ETV, Verwaltervertrag
06_SEV-Sondereigentumsverwaltung SE-Vertrag, SE-Vollmacht, SEV-Abrechnungen
07_Mietverhaeltnisse/<Einheit> Mietvertrag, Mieterhöhung, Übergabe, Kündigung
08_Nebenkosten-und-Heizkosten Betriebskosten- und Heizkostenabrechnung
09_Instandhaltung-und-Handwerker Angebote, Rechnungen, Gewährleistung
10_Mietkonto-und-Zahlungen Kontoauszüge, Mieteingänge, Kaution
11_Steuer-und-AfA Anlage V, Nutzungsdauergutachten
12_Versicherungen Gebäudeversicherung, Schäden
13_Fotos-und-Plaene Fotos, Zustandsdokumentation
Daneben liegt _Portfolio für alles ohne Objektbezug (Steuerbescheide, persönliche
Unterlagen, Bankunterlagen, Ankaufsprüfungen, Auswertungen).
Neue Objekte und neue Einheiten legen ihre Ordner automatisch an. „Einstellungen → Dokumentenablage → Standardstruktur anlegen / ergänzen" zieht fehlende Ordner nach.
Altbestand einsortieren
npm run vorschlag # Zuordnungsliste erzeugen, nichts wird geschrieben
npm run einsortieren # Dateien kopieren (Quelle bleibt unverändert)
Der Vorschlag landet in berichte\einsortier-vorschlag.html (zum Durchsehen) und
…csv (für Excel). Quelle ist …\Dropbox\Daniel Rabe; die Regeln stehen in
src\regeln.js und lassen sich dort ergänzen.
Inhaltsgleiche Dateien, die im Ziel schon liegen, werden übersprungen — der Lauf lässt sich also gefahrlos wiederholen.
Was die Oberfläche kann
- Übersicht — Mieteinnahmen, Cashflow, tatsächliche Liquidität, Restschuld und Rendite je Objekt und in Summe
- Objekt — Stammdaten, Kaufkosten, laufende Rechnung, steuerliche Wirkung, Kostenstände
- Einheiten & Mieter — Mietverhältnisse, Kaution, Historie und Planung der Mietanpassungen; ein doppelt angelegtes Mietverhältnis entfernt der Papierkorb neben „+ Mieterhöhung“
- Zahlungen — Soll/Ist je Monat: was hätte kommen müssen, was kam an, was SEV oder Hausverwaltung vorher einbehalten haben. Gleichbleibende Beträge lassen sich über „Für Monat … bis Monat" in einem Rutsch für einen ganzen Zeitraum buchen; bereits gebuchte Monate werden dabei übersprungen. Jede Buchung ist nachträglich änderbar. Jede Monatszeile ohne Eingang hat „+ Eingang“, Monate mit erwartetem Mietzuschuss „+ Zuschuss“ – vorbelegt mit der letzten Buchung derselben Art davor (Betrag, Einbehalte, Konto, Tag im Monat). Den Mietzuschuss bucht „+ Mietzuschuss“ eigens, auch als Sammelzahlung über mehrere Monate.
- Finanzierung — Darlehen mit Restschuld und Tilgungsplan
- Konten & Verwaltung — Hausverwaltung, SEV, Mietkonten
- Dokumente — Ordner durchblättern, hochladen (Drag & Drop), umbenennen, verschieben, suchen
- Adressbuch — Verwaltungen, Banken, Handwerker objektübergreifend
- Fristen — abgeleitet aus Zinsbindung, geplanten Mieterhöhungen, Sperrfrist (§ 558 BGB) und Betriebskostenfrist (§ 556 Abs. 3 BGB), dazu eigene Termine
Neues Objekt anlegen (Assistent)
„+ Objekt anlegen“ öffnet den Assistenten (#/neu). Er fragt in sieben Schritten ab:
Stammdaten → Kauf & Steuer (mit Eigenkapital-Einlagen) → Verwaltung & Konten (WEG-Verwaltung
und SEV aus dem Adressbuch oder neu, erster Kostenstand mit Hausgeld, Konten) → Einheiten &
Mieter (je Einheit optional das Mietverhältnis, mit Miet- und Kautionskonto aus dem vorigen
Schritt) → Finanzierung (Darlehen mit Darlehenskonto) → Cloud-Ablage → Prüfen & anlegen.
Angelegt wird erst am Ende, alles in einem Zug samt Ordnerstruktur; bis dahin bleibt der Entwurf
im Browser erhalten.
Was Pflicht ist, legt ein Administrator unter „Einstellungen → Objekt-Assistent“ fest: je Gruppe die Felder (bei Listen je Eintrag) und was mindestens erfasst sein muss. Vorgabe: Straße, PLZ, Ort und Status am Objekt, Bezeichnung je Einheit, Mieter/Beginn/Kaltmiete je Mietverhältnis, Bank/Betrag/Zins je Darlehen und mindestens eine Einheit.
Abgleich mit Dropbox und Google Drive
Je Objekt lässt sich die Ablage mit einem Ordner in der Dropbox oder in Google Drive abgleichen – gedacht vor allem für Instanzen auf dem Server, deren Ablage sonst nur per Hochladen gefüllt wird. Verbunden wird einmal unter „Einstellungen → Cloud-Speicher“ (Anleitung für die Dropbox-App bzw. den Google-OAuth-Client steht dort, samt der Redirect URI); verknüpft wird je Objekt auf der Dokumentenseite oder im Assistenten. Abgeglichen wird per Knopf, nach dem Anlegen und nach Zeitplan (Vorgabe: stündlich, 0 = aus).
- Beide Richtungen: Neues und Geändertes geht jeweils auf die andere Seite.
- Die erste Runde – auch nach einem Wechsel des Ordners oder Anbieters – löscht nichts.
- Hier gelöscht → beim Anbieter gelöscht (dort 30 Tage wiederherstellbar); dort gelöscht → hier im Papierkorb der Ablage.
- Beide Seiten geändert → beide Fassungen bleiben, die hiesige als „… (Konflikt hier)“.
- Würde eine Runde mehr als die Hälfte der Dateien löschen oder fehlt ein Ordner, hält sie an.
- Google-Dokumente, -Tabellen usw. haben keinen Dateiinhalt und bleiben außen vor.
- Bei Google den OAuth-Zustimmungsbildschirm auf „In Produktion“ stellen – im Status „Test“ laufen die Zugänge nach 7 Tagen ab.
- Liegt die Ablage lokal schon im Dropbox-Ordner, bleibt der Dropbox-Abgleich aus (Google Drive geht trotzdem).
Hausgeld, SEV-Gebühr und Nebenkosten
Am Objekt (Stammdaten) werden getrennt erfasst:
| Feld | Bedeutung |
|---|---|
| Hausgeld an die WEG / Monat | Gesamtbetrag laut Wirtschaftsplan |
| davon umlagefähig | holt der Mieter über die Nebenkostenvorauszahlung zurück |
| davon Instandhaltungsrücklage | erst bei Verwendung durch die WEG steuerlich absetzbar |
| SEV-Gebühr / Monat | Sondereigentumsverwaltung, geht nicht an die WEG |
Ist beim Mieter eine Nebenkostenvorauszahlung hinterlegt, rechnet der Cashflow mit Warmmiete gegen volles Hausgeld. Ist keine hinterlegt, wird nur der nicht umlagefähige Teil belastet — sonst wäre das Ergebnis künstlich negativ.
Einzelposten wie Grundsteuer oder Kontogebühren gehören unter „Laufende Kosten" auf der Objektseite (Bezeichnung, Betrag je Monat oder Jahr).
Miete läuft über die Sondereigentumsverwaltung
Kassiert die SEV die Miete, zahlt davon Hausgeld und Gebühren und überweist nur den Rest, wird am Objekt „Miete läuft über die SEV" gesetzt und die vereinbarte Ausschüttung eingetragen. Dann gilt:
- Cashflow = wirtschaftliches Ergebnis (Einnahmen − alle Kosten inkl. Rate)
- Auf dein Konto = Ausschüttung + Mietzuschuss direkt vom Verkäufer − Darlehensrate; das ist, was du tatsächlich merkst
- Die Differenz zwischen rechnerischem Überschuss und Ausschüttung wird als Puffer bei der SEV ausgewiesen
- Im Reiter „Zahlungen" ist dann die Ausschüttung das Soll (plus Mietzuschuss) — nicht die volle Miete, denn nur sie kommt bei dir an. Fehlt für Monate nach dem Nutzen-Lasten-Übergang die Ausschüttung im Kostenstand, nennt der Hinweis diese Monate.
Mietzuschuss des Verkäufers
Zahlt der Verkäufer eine Zeit lang einen Mietzuschuss, steht er am Mietverhältnis („Mietzuschuss Verkäufer“) und lässt sich über Mietanpassungen ändern oder beenden. Er zählt zu den Mieteinnahmen. Läuft die Miete über die SEV, zahlt der Verkäufer ihn meist direkt: Dann kommt er zur Ausschüttung hinzu – im Soll der Zahlungen wie bei „Auf dein Konto“ – und nicht in den rechnerischen Überschuss der SEV. Steckt er stattdessen schon in der Ausschüttung, das Häkchen „Zuschuss läuft über die SEV“ am Mietverhältnis setzen; dann wird er nicht eigens erwartet.
Eingänge bucht „+ Mietzuschuss“ (Reiter „Zahlungen“) als eigene Buchungsart; sie zählen zum Eingang und stehen in der Aufschlüsselung.
Kostenstände: was ab wann gilt
Hausgeld, SEV-Gebühr, Einzelposten und Ausschüttung ändern sich über die Jahre. Sie stehen deshalb nicht fest am Objekt, sondern in Kostenständen mit „gültig ab Monat" (Objektseite → Überblick → Kostenstände). Der jüngste Stand, der zum betrachteten Monat schon galt, ist maßgeblich — rückwirkend erfasste Monate rechnen also weiter mit den damaligen Beträgen, auch im Reiter „Zahlungen".
Ein neuer Stand übernimmt den bisherigen als Vorlage; meist ändert man nur ein, zwei Beträge und trägt den Monat ein, ab dem er gilt. Über „⋯" werden die Einzelposten (Grundsteuer, Kontogebühren …) eines Standes gepflegt.
Beim Mieter funktioniert dasselbe über die Mietanpassungen: Eine Anpassung mit Status „vertraglich vereinbart" oder „erfolgt" setzt ab ihrem Datum neue Kaltmiete, Nebenkostenvorauszahlung und/oder Mietzuschuss. Leere Felder bleiben unverändert.
Vor dem Nutzen-Lasten-Übergang
Zwischen Kaufvertrag und Übergang gehören Miete und Bewirtschaftung noch dem Verkäufer, während beim Käufer schon Finanzierungskosten laufen. Dafür gibt es:
- Nutzen und Lasten ab (Stammdaten) — vorher rechnet das Tool weder Miete noch Hausgeld
- Bereitstellungszinsen am Darlehen (Betrag je Monat, von/bis) — reine Kosten ohne Tilgung, steuerlich sofort abziehbar; ab dem Beginn der Auszahlung greift die reguläre Rate
- Sonstige Einnahmen im Kostenstand — etwa eine Entschädigung des Verkäufers für diese Zeit, mit eigener Bezeichnung
Hinweise per E-Mail
Unter „Einstellungen → Hinweis-Mails“ lässt sich der Versand einrichten – über einen SMTP-Server (Server, Port, Benutzer, Passwort) oder über Microsoft Graph (Exchange Online, App-Registrierung mit Mail.Send), dazu Absender und Empfänger. Gemeldet werden wahlweise:
- Fristen und Termine – anstehende Mieterhöhungen, Zinsbindung, Betriebskostenfrist
- fehlende Pflichtunterlagen aus der Vollständigkeitsprüfung
- ausbleibende Zahlungseingänge – wenn für den Vormonat keiner erfasst ist
Dieselbe Sache landet nicht jeden Tag erneut im Postfach: Der Speicher merkt sich, was gemeldet wurde, und meldet erst wieder, wenn eine Frist in eine dringendere Stufe rutscht (später → dieser Monat → diese Woche → überfällig). Die Vorschau in den Einstellungen zeigt jederzeit, was gemeldet würde und was davon neu ist.
Das Passwort wird verschlüsselt gespeichert (AES-256-GCM). Der Schlüssel steht in
IMMO_KEY oder wird beim ersten Mal in .cache\secret.key erzeugt; beides liegt außerhalb
der Datendatei. An die Oberfläche wird das Passwort nie zurückgegeben.
Solange der Server läuft, wird zyklisch geprüft. Wer den Rechner nicht durchlaufen lässt, richtet zusätzlich eine geplante Aufgabe ein:
npm run hinweise:vorschau # anzeigen, was gemeldet würde
npm run hinweise:test # Testmail verschicken
npm run hinweise # prüfen und ggf. verschicken
schtasks /create /tn "Immobilien-Hinweise" /sc daily /st 08:00 ^
/tr "node \"<projektordner>\werkzeuge\benachrichtigen.js\""
Versendet wird über mailer.js aus tool-defaults (src/defaults/mailer.js, SMTP über nodemailer
oder Microsoft Graph).
Adressbuch
Hausverwaltungen, SEV, Banken und Handwerker stehen unter „Adressbuch" und werden am Objekt nur referenziert — eine Verwaltung, die mehrere Objekte betreut, wird also einmal gepflegt. Objektbezogene Angaben (Objektnummer, Vertrag seit, Kündigungsfrist, Gebühr) bleiben beim Objekt. Die Adressbuchliste zeigt zu jedem Eintrag, bei welchen Objekten er verwendet wird.
Posteingang und „Dokumente einlesen“
Hochgeladene Dateien landen auf Objektebene standardmäßig in 00_Posteingang; im
Upload-Bereich lässt sich der Zielordner aber direkt auswählen. Der Knopf
„Dokumente einlesen“ (Seite „Dokumente“, Hinweis in der Übersicht, Dokumente-Reiter
eines Objekts, „Unterlagen“, „Einstellungen“) prüft die Posteingänge: das Regelwerk aus
src\regeln.js – dasselbe wie beim Einsortieren des Altbestands – schlägt je Datei einen
Zielordner vor, änderbar über ein Auswahlfeld (alle Ordner
aller Objekte, auch zum Umhängen auf ein anderes Objekt). Verschoben wird nur, was
angehakt ist — unsichere Vorschläge sind bewusst nicht vorausgewählt. Liegt im Zielordner
schon eine inhaltsgleiche Datei (unter beliebigem Namen), wird das gemeldet und die Datei
nicht vorausgewählt. Nach einem
Upload in den Posteingang öffnet sich der Vorschlag automatisch.
Inhalt statt nur Dateiname. PDFs und Bilder werden mit einer Texterkennung gelesen
(src/texterkennung.js), je nach Rechner auf einem von zwei Wegen:
| Wo | Wie |
|---|---|
| Windows (lokal) | die Texterkennung, die Windows mitbringt (src\texterkennung.ps1: Windows.Data.Pdf + Windows.Media.Ocr) |
| Linux / Docker-Image | pdftotext liest die Textebene digital erzeugter PDFs (ganzer Text, exakt); Scans rendert pdftoppm, tesseract (deutsch) erkennt den Text |
Gelesen werden bis zu zehn Seiten je Scan (IMMO_OCR_SEITEN); immer nur eine Erkennung zur Zeit,
Tesseract auf einem Kern. Der Dateiname hat Vorrang; gibt er nichts her, gilt
der Inhalt: das Objekt, das der Text am häufigsten nennt, die Kategorie nach den meisten
Regeltreffern, Briefkopf und Betreff doppelt gewichtet. Nennt ein Dokument mehrere
Objekte, wird je weiterem Objekt eine Kopie in derselben Kategorie angeboten –
vorausgewählt, wenn der Inhalt sie gleich oft nennt oder der Dateiname mehrere nennt. Für nichtssagende Scannernamen (CCO_000011.pdf,
Scan 3.pdf) wird ein Name aus Briefdatum und Betreff vorgeschlagen
(„2026-09-22 Auszahlungsanweisung … .pdf“). Das Ergebnis der Texterkennung liegt je
Dateiinhalt in .cache/texterkennung (dazu index.json mit den Prüfsummen je Pfad); ein zweiter
Lauf ist dadurch sofort fertig. Ohne Texterkennung bleibt es bei der Zuordnung nach Dateinamen.
Volltext der ganzen Ablage. Der Volltext-Lauf (src/volltext.js) liest jedes PDF und Bild der
Ablage außer Fotos, dessen Text noch nicht bekannt ist – auf dem Server von selbst (30 Sekunden nach
dem Start, nach jedem Hochladen und stündlich, damit auch Dateien aus dem Cloud-Abgleich
dazukommen), lokal auf Knopfdruck unter „Einstellungen → Dokumentenablage → Texterkennung und
Volltext“; dort steht auch, wie viel gelesen ist. IMMO_VOLLTEXT=an|aus überschreibt die Vorgabe. Die
Suche (Kopfzeile, Seite „Dokumente“) findet dann auch Treffer im Text: alle Suchwörter müssen
vorkommen, ein Auszug zeigt die Fundstelle; Treffer im Dateinamen stehen vorn.
Ablage prüfen. Derselbe Abgleich für schon abgelegte Dokumente – die ganze Ablage
(Seite „Dokumente“) oder ein Objekt (Dokumente-Reiter). Der Dialog zeigt nur Abweichungen:
anderer Ordner laut Regelwerk, Namensvorschlag für Scannernamen, fehlende Kopie in einem
weiteren Objekt. Ordnerwechsel sind nicht vorausgewählt, Umbenennungen und Kopien schon.
Ohne greifende Regel bleibt eine Datei, wo sie ist; eigene Unterordner gelten als passend.
Gelesen wird nur, wo der Dateiname nichts entscheidet; die Prüfung läuft im Hintergrund
(/api/ablage/pruefen) mit Fortschritt am Knopf.
Unterlagen am Inhalt erkennen. Die Vollständigkeitsprüfung („Unterlagen“) erkennt eine Unterlage
zuerst am Dateinamen. Verrät der nichts, zählt die Überschrift des Dokuments laut Texterkennung – die
ersten 600 Zeichen, geprüft mit eigenen, engen Mustern je Unterlage (inhalt in src/checkliste.js),
etwa „Kostenrechnung gem. § 19 GNotKG“ für die Notarrechnung. So zählt ein Brief, der den
Wirtschaftsplan nur erwähnt, nicht selbst als Wirtschaftsplan. Gelesen wird dabei nichts; noch
ungelesene Dateien liest der Knopf „Inhalte lesen“ im Hintergrund (/api/unterlagen/lesen).
Auf einem Server (Docker, mehrere Instanzen)
Jede Instanz ist ein eigener Container mit eigenen Daten, eigener Ablage, eigenem
Erscheinungsbild und eigener Subdomain. Alle laufen aus demselben Image hinter einem
gemeinsamen Caddy, der HTTPS (Let's Encrypt) macht. Die App-Container sind nur im internen
Docker-Netz immo-netz erreichbar.
Je Instanz gibt es zwei Arten der Anmeldung:
- entra – die App meldet über Microsoft Entra ID an (OpenID Connect mit PKCE); hinein darf nur, wer in einer der freigegebenen Entra-Gruppen ist.
- passwort – Caddy fragt Benutzer und Passwort ab, die App selbst bleibt ohne Anmeldung.
| Was | Wo auf dem Server (/opt/immobilien) |
|---|---|
Code, wird zum Image immobilien-app gebaut |
app/ |
| Je Instanz: Einstellungen, Stammdaten, Ablage | instanzen/<name>/instanz.env, daten/, ablage/ |
| Proxy und je Instanz ein Eintrag | proxy/Caddyfile, proxy/instanzen/<name>.caddy |
| Vorlage für einen Instanz-Container | instanz.compose.yml |
| Verwaltung | immo.sh |
Im Repo liegen die Vorlagen unter deploy/.
./immo.sh liste
./immo.sh anlegen kunde immo-kunde.example.de entra "Kunde Immobilien"
./immo.sh anlegen test immo-test.example.de passwort "Test" test < passwort.txt
./immo.sh start kunde # nach Änderungen an der instanz.env
./immo.sh umstellen daniel entra # Anmeldeart wechseln
./immo.sh passwort test # neues Passwort (über stdin)
./immo.sh entfernen test # Container weg, Daten bleiben
Neuen Code einspielen – alle Instanzen werden neu gestartet, Daten bleiben unberührt:
tar -cf - --exclude=werkzeuge/startdaten.js Dockerfile .dockerignore package.json package-lock.json CHANGELOG.md server.js src public werkzeuge \
| ssh root@<server> 'tar -xf - -C /opt/immobilien/app && /opt/immobilien/immo.sh aktualisieren'
Node.js und Pakete aktuell halten
Drei Ebenen, drei Wege:
| Was | Wie es auf den Server kommt |
|---|---|
npm-Pakete (package-lock.json) |
mit dem Code: das Update aus der App führt danach npm install aus |
| Node-Patches, Alpine-Pakete (Sicherheitsupdates von Node 24, poppler, tesseract …) | Image neu bauen: ./immo.sh auffrischen, am besten nach Zeitplan |
Caddy-Proxy (caddy:2-alpine, Patches und Unterversionen) |
ebenfalls ./immo.sh auffrischen |
| neue Node-Hauptversion (24 → 26) | bewusst: FROM im Dockerfile und das Image in .forgejo/workflows/ ändern, Tests abwarten, dann ./immo.sh aktualisieren |
Das Dockerfile legt nur die Hauptversion fest (node:24-alpine, LTS bis April 2028); immo.sh baut
immer mit --pull, holt also den neuesten Patch. auffrischen baut neu und startet die Instanzen nur,
wenn sich das Image geändert hat. Antwortet danach eine Instanz nicht auf /gesund, geht es zurück auf
das vorige Image (immobilien-app:vorher), und der Befehl endet mit Fehlercode.
Danach dasselbe für den Proxy: neuestes Caddy 2 holen, nur bei neuem Image neu starten, dann je Instanz
https://<domain>/gesund über den Proxy abrufen (auf diesem Rechner, 2xx/3xx/401 gelten als
erreichbar). Klappt das nicht, läuft wieder das vorige Image (immo-caddy:vorher); proxy/.env hält
es fest, bis ein späterer Lauf erfolgreich ist. Dafür muss proxy/docker-compose.yml die neue Fassung
mit image: ${CADDY_IMAGE:-caddy:2-alpine} sein – mit einer festen Version überspringt auffrischen
den Proxy und meldet das.
./immo.sh auffrischen # einmal von Hand
./immo.sh zeitplan an # wöchentlich, sonntags 04:30 (/etc/cron.d/immobilien-auffrischen)
./immo.sh zeitplan an 0 3 '*' # oder: täglich 03:00 (Minute Stunde Wochentag)
./immo.sh zeitplan aus
tail /var/log/immo-auffrischen.log # was zuletzt passiert ist
Der Workflow .forgejo/workflows/tests.yml führt bei jedem Push und montags früh die Tests auf
Node 24 aus und prüft die Pakete mit npm audit (ab „high“). Ein roter Montagslauf heißt: Es gibt
eine neue Sicherheitslücke – npm audit fix, testen, committen; das Update aus der App bringt es
auf den Server.
Anmeldung, Benutzer und Rollen
Anmeldung, Sitzungen, Benutzer und Rollen kommen aus tool-defaults (src/defaults/auth.js,
roles.js, audit.js). Drei Rollen: Admin darf alles einschließlich Einstellungen, Benutzer
und Updates, Member liest und bearbeitet, Viewer liest nur. Eingestellt wird alles unter
„Einstellungen → Benutzer & Anmeldung“:
- Benutzername und Passwort – lokale Benutzer (scrypt), optional mit Zwei-Faktor-Code (Authenticator-App, zehn Wiederherstellungscodes), Sperre nach fünf Fehlversuchen. Zwei-Faktor und Sperre sind Ergänzungen dieses Werkzeugs.
- Microsoft Entra ID – Rolle aus den Gruppen (je Rolle Gruppen-Objekt-IDs). App-Registrierung
mit den Umleitungs-URIs
https://<domain>/auth/rueckruf(bzw./auth/entra/callback) undhttps://<domain>/auth/abgemeldet, geheimer Clientschlüssel, Gruppenanspruch im ID-Token. - Google Workspace – Ergänzung dieses Werkzeugs. OAuth-Client „Webanwendung“ mit der
Weiterleitungs-URI
https://<domain>/auth/google/rueckruf, Zustimmungsbildschirm „Intern“. Zugang hat, wer eine bestätigte Adresse aus einer freigegebenen Domain hat oder einzeln eingetragen ist; neue Konten bekommen die eingestellte Rolle (nie Admin). - Demo – nur ansehen, ohne Konto.
Jeder Angemeldete pflegt unter „Mein Konto“ sein Passwort und seine Zwei-Faktor-Anmeldung. Was geschieht, steht im Protokoll (Admin). Eine Anmeldeart, über die man selbst angemeldet ist, lässt sich nicht abschalten; der letzte aktive Administrator lässt sich weder sperren noch herabstufen.
Ausgesperrt? ./immo.sh notfall <instanz> [benutzer] (Server) bzw.
node werkzeuge/zugang.js <benutzer> (lokal) legt einen lokalen Administrator an oder setzt ihn
zurück (Zwei-Faktor aus) und schaltet die lokale Anmeldung ein. Das Passwort kommt über stdin.
Update aus der App
Unter „Einstellungen → Update“ steht das Repository dieses Werkzeugs. Der Knopf „Update
einspielen“ holt, was neu ist, und startet neu. Weil alle Instanzen den Code-Ordner app/ teilen,
wirkt ein Update für alle – die übrigen starten von selbst nach, sobald bei ihnen nichts mehr läuft.
Die gemeinsamen Funktionen aus tool-defaults kommen mit dem Code dieses Repositorys; ein eigenes
tool-defaults-Repository lässt sich nicht mehr einstellen. Änderungen daran gehören nach
tool-defaults und werden von dort in src/defaults/ bzw. public/assets/ übernommen.
Erscheinungsbild
Die Oberfläche nutzt Gerüst, Schriften und Bausteine des ho.Systeme-Design-Systems aus tool-defaults
(public/assets/css/ho-base.css, Symbolsatz, Kopf- und Fußzeile aus public/assets/ho/kopf-fuss.js),
aber nicht dessen Farben: public/assets/css/base.css ersetzt die Farb-Tokens durch eine neutrale
Grundpalette (Grautöne, hell und dunkel) und schaltet die blaue Markengrafik ab. Darauf bauen die
Bausteine dieses Werkzeugs auf. Unter „Einstellungen → Erscheinungsbild“ stehen Name und Zusatz der
Marke, Fußzeile und Rechtliches, Standardmodus, Seitenbreite und die Marken-Farben je für hell und
dunkel; sie wirken über /theme.css (tool-defaults branding.js) und gewinnen gegen die
Grundpalette. Das Feld „Farbschema“ füllt die Farben mit einer Vorlage: Neutral (Grautöne, Standard –
speichert nichts, es gilt die Grundpalette) oder Neutral (Schiefer). Die Werte der Grundpalette stehen
zweimal: in base.css und als FARBEN_NEUTRAL in public/js/app.js – bei Änderungen beide anpassen.
Eigenes CSS kommt als /anpassung.css zuletzt.
Der Seitenhintergrund ist ebenfalls einstellbar: Verlauf aus Leit- und Zweitfarbe (Vorgabe), einfarbig
oder eigenes Bild, dazu die Abdeckung (wie stark die Hintergrundfarbe über dem Bild liegt). Das steht
mit dem eigenen CSS in daten.einstellungen.anpassung; frühere Werte „auto“ und „marke“ gelten als
Verlauf. Ein eigenes Bild liegt als hintergrund.bild neben der Datendatei und wird unter
/hintergrund-bild ausgeliefert (auch ohne Anmeldung, für die Anmeldeseite). /anpassung.css legt
den Hintergrund in eine fixierte Ebene statt auf <html>, weil iOS background-attachment: fixed
nicht kann.
Die Texterkennung kommt auf dem Server über das Image (poppler, tesseract, deutsche Sprachdaten).
Ein Update aus der App bringt nur neuen Code, kein neues Image – nach einem Update, das das Dockerfile
ändert (etwa auf 0.32.0), einmal ./immo.sh aktualisieren ausführen. Bis dahin zeigt „Einstellungen →
Dokumentenablage“, was fehlt. Die Ablage dort ist eine eigene Kopie; mit Dropbox oder Google Drive bleibt sie
über den Cloud-Abgleich in Gleichstand.
Zum Ausprobieren
npm run demo # erzeugt .demo\ mit erfundenen Objekten und einer Beispielablage
npm run demo:start # startet den Server damit auf Port 4799
Die Screenshots in diesem README stammen aus diesem Datensatz – echte Zahlen stehen
ausschließlich lokal in data\daten.json und werden nicht versioniert.
Tests
npm test # Anmeldung, Update, Mail, Cloud-Abgleich, Objekt-Assistent, Unterlagen, Texterkennung
Der Texterkennungs-Test braucht Linux mit poppler, tesseract, ImageMagick und DejaVu-Schrift und
überspringt sich sonst. Er läuft im Workflow .forgejo/workflows/texterkennung.yml in einem
Alpine-Container mit denselben Paketen wie das Image.
Aufbau
server.js HTTP-Server und API (Express)
src/config.js Pfade (Dropbox-Erkennung), Port
src/store.js JSON-Datenspeicher
src/struktur.js Ordnerstruktur-Standard
src/dokumente.js Dateiablage mit Pfadprüfung
src/kennzahlen.js Cashflow, Rendite, Restschuld, Kostenstände, Jahresrechnung, Fristen
src/zuordnung.js Zielordner-Ermittlung für Altbestand und Posteingang, Namensvorschlag
src/texterkennung.* Inhalt von PDFs und Bildern lesen (Windows-OCR oder poppler + tesseract), Zwischenspeicher
src/volltext.js Volltext-Lauf über die ganze Ablage im Hintergrund
src/migration.js Datenmigrationen beim Serverstart
src/assistent.js Objekt-Assistent: Pflichtangaben, Anlegen in einem Zug
src/abgleich.js Cloud-Abgleich in beide Richtungen (Konflikte, Papierkorb, Sicherheitsstopp)
src/anbieter/ Dropbox und Google Drive (OAuth mit PKCE, API-Aufrufe)
src/regeln.js Einsortier-Regelwerk
public/ Oberfläche (Vanilla JS, kein Build)
werkzeuge/ Startdaten, Einsortieren, Hinweis-Mails, Demodaten, Notfallzugang
deploy/ Server: immo.sh, Compose-Vorlage, Caddy
test/ Tests (node, ohne Testframework)




