a6b6a7d67c5dc261a066268f32783f81f93d4ebb
6 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| d08e4f86fe |
Den Bereich einer Einheit ueber das Etikett suchen, nicht ueber die Stellung
divisionOf nahm "die oberste Einheit unterhalb der Gesellschaft" -- eine Aussage ueber die Stellung im Baum statt ueber unit_type. Im Altmodell fiel beides zusammen, weil unter der Gesellschaft genau die Bereiche hingen. Bei Manner liegt dort allein "CEO", darunter erst die sechs C-Level-Bereiche: die Funktion gab fuer alle 784 Personen "CEO" zurueck. In der Mitarbeiterliste stand es in jeder Zeile, auf der Uebersicht lag die ganze Belegschaft im Balken "CEO", waehrend die sechs uebrigen Bereiche auf null standen. Gesucht wird jetzt der naechste Vorfahre mit unit_type = 'Bereich', die Einheit selbst eingeschlossen. Der naechste und nicht der oberste: "CEO" ist selbst ein Bereich und liegt ueber den anderen, sonst stuende er wieder ueberall. Damit meinen Liste, Uebersicht, Sortierung (lib/employee-sort.ts) und Berichte (lib/reports-data.ts) denselben Bereich -- vorher sortierte die Liste bereits richtig, zeigte aber in der Spalte etwas anderes an. |
|||
| b87c8ad64c |
Remove Supabase
The database moved to a container of our own; the platform is gone. This takes out what was left of it — and, where the leftovers were load bearing, moves rather than deletes. Moved, not deleted: supabase/migrations/ -> db/migrations/ the schema's source of truth supabase/build-org.ts -> scripts/build-org.ts lib/supabase/types.ts -> lib/types.ts 52 import sites repointed The bookkeeping needed care. It lived in `supabase_migrations.schema_migrations`, and simply renaming the schema would have left the runner facing an empty table: it would have called all 67 migrations pending and replayed them against a database that is long since current. So the runner now creates `migrationen.schema_migrations` and, once, copies the old rows across — guarded so a second run does nothing and a fresh database skips it entirely. Only then does migration 20260907100000 drop the old schema. Deleted: the CLI config, the seed, the historical schema/function dumps (nothing read them), scripts/umzug-von-supabase.sh (the move is done), and both Supabase packages plus the CLI. Nothing in the application imported them — the build now succeeds with no environment variables at all, which is the proof. Integration tests: six of them signed in through Supabase Auth and asserted against the anon key and the service role. That model is gone, so the tests were not portable — they are deleted. session-context and employee-status-filter already ran on pg and are untouched; om-reporting is ported to a direct connection because it guards a real risk (the reporting line rule exists twice, once in SQL and once in TypeScript). CI: the integration job started a Supabase stack. It now runs a postgres service, applies deploy/db-init and every migration to an empty database — that was the valuable part, and it still holds — then checks that a second run is a no-op, which is what proves the bookkeeping works. Docs: security-review.md audited a service-role key, a cookie adapter and auth.users, none of which exist. Restating findings about removed components would suggest today's system had been reviewed; it has not. It now records what was removed and says a fresh review is due. data-model.md was already marked obsolete and described the pre-OM schema; azure-migration.md was a plan for a route not taken. Both deleted. Verified: npm ci, typecheck, lint, 445 tests, build — all clean without the packages. Integration tests skip cleanly with no database. Migration SQL and the runner are reviewed but NOT executed: no Docker here, and the old instance no longer resolves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| 91b2b3406b |
Stop waiting on the network eleven times per page
The app got slower as pages grew, and the reason was not the queries. It was their number. A transaction is pinned to one connection, and a connection runs queries one after another. Every Promise.all in a withUser block looked like concurrency and was a queue. Measured against the real database: the round trip is ~36 ms, ten trivial `select 1` over one connection take 343 ms, over ten connections 39 ms. Nothing here is slow — the whole dashboard payload is under 200 kB, and every table is around a thousand rows. More connections is the wrong answer: the RLS session context is per transaction, so parallel reads mean parallel transactions, and those multiply the connections the database will grant. Fewer round trips instead. Postgres will return each sub-select as its own JSON column of one result. Per page view, counting the transaction frame: shell (paid by every page) 10 → 4 overview 14 → 5 employee file 14 → 7 employee list 8 → 6 The overview plus its shell went from 24 round trips to 9 — about 860 ms of pure waiting down to about 320 ms. The one trap is documented where it bites: inside json_agg, Postgres formats values itself and the driver's parsers (lib/db/pool.ts) never see them. Dates, numerics and uuids come out identical; timestamptz does not — "+00:00" where the driver gives "…Z". Timestamps are compared as strings in lib/history.ts to decide what happened later, and those two forms sort against each other wrongly. Every timestamptz in a bundled query therefore goes through zeitstempel(), which was checked character-for-character against the driver. Four loaders moved out of their pages into lib/ so the number of round trips can be measured without building a React tree, and so the new path could be held against the old one field by field: same rows, same order, same strings, for the overview and for four employee files chosen to differ (with history, a chief, a planned entry, one with dependents). withUser now counts the queries in each transaction and says so in development past a threshold. Without that, this grows back: each new tile brings its own query, and nobody notices until everybody does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|||
| b3a0af2b8f |
Talk to PostgreSQL directly, and let the pooled connection forget
Zweiter Schritt weg von Supabase. Sämtliche 49 Lesezugriffe und alle
Mutationen laufen jetzt über lib/db statt über die REST-Schicht: Kysely auf
einem pg-Pool, jede Abfrage in einer Transaktion, in der zuerst
app.user_id gesetzt wird. Die Anmeldung hängt noch an GoTrue — sie liefert
die Kennung, die in withUser() geht. Damit war der Umbau in zwei Hälften
teilbar und die Anwendung durchgehend lauffähig.
Was dabei ersatzlos verschwindet:
- fetchAllRows. Es gab die Funktion nur, weil PostgREST jede Antwort bei
1000 Zeilen still abschneidet und ein Bericht dann leise falsch war.
Am direkten Zugang ist eine Abfrage eine Abfrage.
- sanitizeIlikeTerm samt Test. Sie entschärfte Zeichen, die in der
Filtersyntax strukturelle Bedeutung hatten; jetzt wird der Suchbegriff
als Parameter gebunden und ein Komma ist ein Komma. Die Lücke ist nicht
abgesichert, sondern weg.
- lib/supabase/admin.ts. Der Dienstschlüssel, der RLS aushebelte, hatte
genau einen Aufrufer — den nächtlichen Lauf. Der benutzt jetzt dieselbe
Rolle ohne BYPASSRLS und ruft eine SECURITY-DEFINER-Funktion auf, die
selbst prüft, was sie tut. Es gibt keinen privilegierten Zugang mehr.
Nebenbei besser geworden, weil der direkte Zugang es erlaubt:
- Eine Seite ist eine Transaktion. Das Layout etwa liest Profil,
Planstellen, Standorte, Entwürfe und Notizen auf einem einheitlichen
Lesestand statt in fünf unabhängigen Anfragen.
- Der Bereichsfilter der Mitarbeiterliste ist ein EXISTS statt einer
eingebetteten Ressource mit !inner — eine Person mit mehreren
Zuordnungen über die Zeit erschien dort mehrfach.
- Seitenweise Listen sortieren zusätzlich nach id. Bei gleichem Nachnamen
oder gleichem Zeitstempel war die Reihenfolge vorher unbestimmt, und
dieselbe Zeile konnte auf zwei Seiten erscheinen oder auf keiner.
- Angehörige werden in der Datenbank gezählt statt alle Zeilen zu holen.
- Namen an Ereigniszeilen kommen aus einem Join statt aus einem
Nachschlag, der ausserhalb der Transaktion lag.
Der Statusfilter ist mitgezogen: dieselbe Regel wie deriveStatusAsOf,
Klausel für Klausel, jetzt als Kysely-Ausdruck. Der Integrationstest, der
beide über den gesamten Bestand vergleicht, läuft weiter — mit eigener
Verbindung, denn geprüft wird die Bedingung, nicht die Berechtigung.
Zwei Fehler auf dem Weg, beide vom Typprüfer gefangen: apply_due_pending_
changes() nimmt kein Argument, wurde von callFunction aber mit jsonb
aufgerufen — Postgres hätte keine passende Signatur gefunden. Und der
Sicherheitstest lädt jetzt Module mit `import "server-only"`, was ausserhalb
der Server-Übersetzung wirft.
Typecheck, Lint, Build und 180 Tests sind grün. Ungeprüft bleibt der Lauf
gegen eine echte Datenbank — dafür fehlt eine DATABASE_URL.
|
|||
| 27669e0359 |
Put the whole application on the OM model, and delete what it replaced
Die Datenbank stand seit dem Cut-over auf org_units/om_positions/
position_assignments, die Anwendung fragte weiter nach employees.division_id,
team_id und manager_id — Spalten, die es nicht mehr gab. Die Oberfläche war
deshalb leer, obwohl die Daten vollständig da waren. Das ist jetzt behoben,
und zwar nicht durch Nachbau der alten Begriffe, sondern indem sie verschwinden.
Neu ist eine dünne Schicht, die die Verkettung Person → Besetzung →
Planstelle → Einheit einmal auflöst (lib/placement.ts) und der Baum als reine
Funktionen darauf (lib/org.ts): Vorfahrenkette, Teilbaum, Brotkrume. Alles
Weitere hängt daran.
Was sich dadurch von selbst erledigt hat:
- Das Organigramm musste drei Quellen versöhnen, weil keine den ganzen
Zeitstrahl abdeckte. position_assignments ist zeitabhängig, also
beantwortet eine Abfrage "wer besetzte am Stichtag welche Planstelle" —
für Vergangenheit und Zukunft gleichermassen. Wer keine Planstelle hatte,
war nicht da; eine zweite Zugehörigkeitsregel braucht es nicht mehr.
- Die Struktursicht war auf genau vier Ebenen verdrahtet und rendert jetzt
rekursiv über parent_id. Liste und Grafik entstehen aus *einem* Baum;
vorher lag dieselbe Hierarchie zweimal vor und konnte auseinanderlaufen.
- Eine offene Stelle ist keine eigene Tabelle mehr, sondern eine Planstelle
ohne laufende Besetzung — das Komplement kann nicht aus dem Tritt geraten.
- Eine Versetzung ist der Wechsel auf eine Zielplanstelle statt Zielteam
plus frei getipptem Titel. Sie kann damit nicht mehr dort landen, wo es
keine Stelle gibt, und die Tätigkeit kommt aus dem Job-Katalog.
- Beim Anlegen einer Planstelle entfällt die Suche nach der vorgesetzten
Person: sie ergibt sich aus der Einheit, die Frage kann nicht mehr falsch
beantwortet werden.
Zwei Auswertungen werden dabei richtiger, nicht nur anders. Ein
Stichtagsbericht gruppierte bisher nach der *heutigen* Zuordnung, weil es
keine Historie gab; er löst sie jetzt zum Stichtag auf. Und ein Ereignis
trägt die Einheit, in der die Person am Tag des Ereignisses sass — vorher
stand ein Austritt von vor zwei Jahren unter einem Team, in das sie nie
versetzt worden war. Der Bereichsfilter greift überall auf den ganzen
Teilbaum; auf den Bereich allein angewandt lieferte er nur die
Bereichsleitung.
Gelöscht: die Reorganisations-Werkbank samt Szenarien und Zügen (sie
verschob Teams und Abteilungen zwischen Bereichen — Objekte, die es nicht
mehr gibt; im OM-Modell ist das ein Umhängen von parent_id), die
Mitarbeiter- und Vorgesetztensuche, die nur sie und die Ausschreibung
brauchten, und aus lib/supabase/types.ts die Tabellen divisions,
departments, teams, positions und employee_assignments.
Die beiliegende Migration räumt die Datenbank entsprechend auf. Sie entfernt
auch Funktionen, die der Cut-over verfehlt hat: create_position,
delete_position und undo_reorg existierten zusätzlich in einer
jsonb-Variante und tauchen deshalb weiter in der PostgREST-Schnittstelle auf,
obwohl ihre Tabellen weg sind — ein Aufruf wäre erst zur Laufzeit
gescheitert. An ihre Stelle treten create_position und delete_position im
OM-Sinn; letzteres schliesst eine früher besetzte Planstelle, statt sie zu
löschen, sonst verschwände mit ihr die Besetzungshistorie.
Typecheck, Lint, Build und 182 Tests sind grün. Die Integrationstests sind
mitgezogen, aber weiterhin ungelaufen — dafür braucht es eine laufende
lokale Datenbank.
|
|||
| 366731ec85 |
Phase 2/3: Employees list/detail + mutation RPCs + action panels
- supabase/functions.sql, functions_2.sql: Postgres RPCs for every employee/position/reorg mutation (hire, terminate, transfer, promote, start/adjust/return karenz, change data, rehire, create position, staff internally, apply/undo reorg). Each resolves manager_id server-side, writes history + audit atomically, and enforces hr_admin via require_hr_admin() (backed by the existing RLS policy). - actions/employees.ts, positions.ts, reorg.ts: Server Actions wrapping the RPCs, returning success/error for client-side toast handling. - Employees list (search/filter/pagination) and detail (4 tabs: Stammdaten, Vertrag & Gehalt, Organisation, Historie) reading from employees_directory. - 6 action slide-over panels: Transfer, Promote, Karenz (start/adjust/ return), Daten aendern (person+contract diffing), Terminate (with direct- report reparenting warning + offboarding checklist), Rehire. - lib/org.ts: shared division/department/team/location lookups. Verified live: promote mutation updates salary, writes history/audit, and the detail page reflects it after refresh, no console errors. Note: the spec's Karenz-verwalten panel only covers employees already on Karenz; added a start-Karenz mode (Karenzbeginn/geplante Rueckkehr) to cover the Aktiv-employee case implied by the header button but not specified in the panel list. |