diff --git a/.gitignore b/.gitignore index ed5ffe6..8d44fb4 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,3 @@ dist/ data/*.db data/backups/*.db .codex/*.log -dynamo/output/ diff --git a/Dockerfile b/Dockerfile index 31d28c4..41a08fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,17 +1,10 @@ -FROM node:24 +FROM node:22 WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . - -# next build writes the rewrite destinations from next.config.mjs into -# .next/routes-manifest.json, so "next start" cannot pick up a different -# API URL later. The value has to be known here, not just at runtime. -ARG API_INTERNAL_URL=http://localhost:3000 -ENV API_INTERNAL_URL=$API_INTERNAL_URL - RUN npm run build:api && npm run build:web RUN mkdir -p data && chmod +x scripts/docker-start.sh diff --git a/README.md b/README.md index fc82fca..c333917 100644 --- a/README.md +++ b/README.md @@ -62,29 +62,15 @@ docker compose logs --follow docker compose down ``` -`compose.yaml` startet den Produktionsstand: gebautes `dist/` und `next start`, -ohne Quellcode-Mounts und ohne Datei-Watcher. Details stehen in +Der Compose-Stack startet Entwicklungsserver mit Quellcode-Mounts. Er ist kein +Produktionsdeployment. Details stehen in [Deployment und Betrieb](docs/deployment.md). -Für die Entwicklung mit Hot Reload gibt es einen eigenen Stack mit -Quellcode-Mounts und Watchern: - -```powershell -docker compose -f compose.dev.yaml up --build --detach -docker compose -f compose.dev.yaml logs --follow -docker compose -f compose.dev.yaml down -``` - -Die Watcher darin laufen im Polling-Modus, weil Bind-Mounts unter Windows und -macOS keine inotify-Events durchreichen. Das kostet dauerhaft CPU, auch wenn -niemand die Anwendung benutzt — deshalb gehört dieser Stack nicht auf einen -Server. - ## Direkte lokale Entwicklung Voraussetzungen: -- Node.js 24 +- Node.js 22 - npm ```powershell diff --git a/compose.dev.yaml b/compose.dev.yaml deleted file mode 100644 index 86e42e2..0000000 --- a/compose.dev.yaml +++ /dev/null @@ -1,87 +0,0 @@ -# Development stack: source mounts, watching dev servers, hot reload. -# docker compose -f compose.dev.yaml up --build -# -# The polling watchers below are needed for bind mounts on Windows and -# macOS, where inotify events do not cross the VM boundary. They cost -# continuous CPU, which is why the production stack in compose.yaml does -# not run watchers at all. -name: leistungsbilanz-dev - -x-logging: &logging - driver: json-file - options: - max-size: "20m" - max-file: "10" - -services: - api: - build: - context: . - command: - - sh - - -c - - npm run db:migrate && npm run db:verify:circuit-schema && npm run dev:api - environment: - PORT: "3000" - CHOKIDAR_USEPOLLING: "true" - LOG_LEVEL: "${LOG_LEVEL:-debug}" - init: true - restart: unless-stopped - logging: *logging - ports: - - "3000:3000" - volumes: - - ./src:/app/src - - ./scripts:/app/scripts - - ./data:/app/data - - ./drizzle.config.ts:/app/drizzle.config.ts:ro - - ./tsconfig.json:/app/tsconfig.json:ro - healthcheck: - test: - - CMD - - node - - -e - - fetch('http://localhost:3000/health').then(response=>{if(!response.ok)process.exit(1)}).catch(()=>process.exit(1)) - interval: 30s - timeout: 3s - retries: 5 - start_period: 20s - - web: - build: - context: . - command: - - npm - - run - - dev:web - - -- - - --hostname - - 0.0.0.0 - environment: - API_INTERNAL_URL: http://api:3000 - WATCHPACK_POLLING: "true" - NEXT_TELEMETRY_DISABLED: "1" - LOG_LEVEL: "${LOG_LEVEL:-debug}" - init: true - restart: unless-stopped - logging: *logging - depends_on: - api: - condition: service_healthy - ports: - - "3001:3001" - volumes: - - ./src:/app/src - - ./next.config.mjs:/app/next.config.mjs:ro - - ./tsconfig.json:/app/tsconfig.json:ro - - ./tsconfig.next.json:/app/tsconfig.next.json:ro - healthcheck: - test: - - CMD - - node - - -e - - fetch('http://localhost:3001/web-health').then(response=>{if(!response.ok)process.exit(1)}).catch(()=>process.exit(1)) - interval: 30s - timeout: 3s - retries: 5 - start_period: 20s diff --git a/compose.yaml b/compose.yaml index 503a4b2..2062cb7 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,73 +1,68 @@ name: leistungsbilanz -x-build: &build - context: . - args: - # Baked into .next/routes-manifest.json by next build; see Dockerfile. - API_INTERNAL_URL: http://api:3000 - -x-logging: &logging - driver: json-file - options: - max-size: "20m" - max-file: "10" - services: api: - build: *build + build: + context: . command: - sh - -c - - node scripts/run-migrations.js && node scripts/db-verify-circuit-schema.js && node dist/server/index.js + - npm run db:migrate && npm run db:verify:circuit-schema && npm run dev:api environment: - NODE_ENV: production PORT: "3000" - LOG_LEVEL: "${LOG_LEVEL:-info}" + CHOKIDAR_USEPOLLING: "true" init: true - restart: unless-stopped - logging: *logging ports: - "3000:3000" volumes: + - ./src:/app/src + - ./scripts:/app/scripts - ./data:/app/data + - ./drizzle.config.ts:/app/drizzle.config.ts:ro + - ./tsconfig.json:/app/tsconfig.json:ro healthcheck: test: - CMD - node - -e - fetch('http://localhost:3000/health').then(response=>{if(!response.ok)process.exit(1)}).catch(()=>process.exit(1)) - interval: 30s + interval: 5s timeout: 3s - retries: 5 + retries: 12 start_period: 20s web: - build: *build + build: + context: . command: - - node_modules/.bin/next - - start - - -p - - "3001" + - npm + - run + - dev:web + - -- + - --hostname + - 0.0.0.0 environment: - NODE_ENV: production API_INTERNAL_URL: http://api:3000 + WATCHPACK_POLLING: "true" NEXT_TELEMETRY_DISABLED: "1" - LOG_LEVEL: "${LOG_LEVEL:-info}" init: true - restart: unless-stopped - logging: *logging depends_on: api: condition: service_healthy ports: - "3001:3001" + volumes: + - ./src:/app/src + - ./next.config.mjs:/app/next.config.mjs:ro + - ./tsconfig.json:/app/tsconfig.json:ro + - ./tsconfig.next.json:/app/tsconfig.next.json:ro healthcheck: test: - CMD - node - -e - - fetch('http://localhost:3001/web-health').then(response=>{if(!response.ok)process.exit(1)}).catch(()=>process.exit(1)) - interval: 30s + - fetch('http://localhost:3001/').then(response=>{if(!response.ok)process.exit(1)}).catch(()=>process.exit(1)) + interval: 5s timeout: 3s - retries: 5 + retries: 12 start_period: 20s diff --git a/docs/circuit-list-editor-api.md b/docs/circuit-list-editor-api.md index 9ed9e0a..4ed696b 100644 --- a/docs/circuit-list-editor-api.md +++ b/docs/circuit-list-editor-api.md @@ -359,9 +359,8 @@ Response sketch: ### Circuit Structure -- `GET /projects/:projectId/circuit-sections/:sectionId/next-identifier` +- `GET /circuit-sections/:sectionId/next-identifier` - preview next identifier for section (`prefix + maxSuffix + 1`) - - returns 404 if the section does not belong to the given project Circuit and device-row field updates, standalone insertions/deletions, single or bulk device-row moves, circuit reorders and explicit renumbering are diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 92efc98..32f6814 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -476,28 +476,16 @@ Kopieren in ein Projekt erzeugt ein eigenständiges Projektgerät. und Verteilerkomponenten. Separate 1:1-Tabellen halten Stromkreis- und Komponenten-Schutzgeräte. Die früheren flachen Stromkreis-Schutzfelder sind aus der Baseline entfernt. - Ein triggergeführtes Register erzwingt eine normalisierte, + Ein triggergeführtes Register erzwingt bereits eine normalisierte, stromkreislistenweite BMK-Eindeutigkeit über Stromkreise und - Verteilerkomponenten. Der DB-Index normalisiert dabei nur über SQLites - eingebautes `lower()` (rein ASCII), erkennt also z.B. `"Ä1"` und `"ä1"` nicht - als denselben Wert. Die gemeinsame Prüfung - `src/db/repositories/equipment-identifier-uniqueness.persistence.ts` - schließt diese Lücke: Sie normalisiert mit JavaScripts Unicode-fähigem - `toLowerCase()` gegen das vollständige Register der Stromkreisliste und wird - von jedem Anlage-/Umbenennungspfad für Stromkreise und Verteilerkomponenten - aufgerufen, auch dort, wo zuvor kein Vorab-Check existierte. Snapshot- und - Transfer-Integration verwenden aktuell Snapshot-Schema 5. Persistente - Insert/Delete/Update-Commands für veränderliche Verteilerkomponenten, - Gruppen einschließlich befüllter Unterbäume sowie vollständige - Gruppensortierung sind integriert. Der Editor zeigt die geschützte Struktur - an und bearbeitet veränderliche Gruppen- und Fußkomponenten über dedizierte - Command-Modale. Gruppenanlage, -umbenennung, -sortierung, explizite - Neunummerierung, Same-Category-Stromkreiswechsel, geschütztes - Unterbaumlöschen und Stromkreisschutz sind integriert. - Migration `0006` ergänzt additiv Indizes auf `circuits.section_id` und - `circuit_device_rows.circuit_id`, den beiden am häufigsten gefilterten - Fremdschlüsselspalten sowie den Cascade-Delete-Pfaden von Abschnitten und - Stromkreisen. + Verteilerkomponenten. Snapshot- und Transfer-Integration verwenden aktuell + Snapshot-Schema 5. Persistente Insert/Delete/Update-Commands für + veränderliche Verteilerkomponenten, Gruppen einschließlich befüllter + Unterbäume sowie vollständige Gruppensortierung sind integriert. Der Editor + zeigt die geschützte Struktur an und bearbeitet veränderliche Gruppen- und + Fußkomponenten über dedizierte Command-Modale. Gruppenanlage, -umbenennung, + -sortierung, explizite Neunummerierung, Same-Category-Stromkreiswechsel, + geschütztes Unterbaumlöschen und Stromkreisschutz sind integriert. PostgreSQL ist bewusst nicht implementiert. Die Domainregeln und Transaktionsgrenzen sollen portabel bleiben; Schema und Betriebsmodell benötigen diff --git a/docs/deployment.md b/docs/deployment.md index 06289a7..44c7dd5 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -2,27 +2,19 @@ ## Aktueller Status -Es gibt zwei Compose-Stacks. +Es gibt derzeit kein unterstütztes Produktionsdeployment. -`compose.yaml` startet den gebauten Stand: `node dist/server/index.js` und -`next start`, ohne Quellcode-Mounts und ohne Datei-Watcher. Das ist der Stack -für einen Server. +`compose.yaml` ist ausschließlich für lokale Entwicklung vorgesehen. Es startet +`tsx watch` und `next dev`, bindet Quellcode vom Host ein und enthält weder TLS, +Authentifizierung, Reverse Proxy, Prozesshärtung noch ein zentral betriebenes +Datenbanksystem. Der Stack darf deshalb nicht als produktionsreif bezeichnet oder +öffentlich erreichbar gemacht werden. -`compose.dev.yaml` startet `tsx watch` und `next dev` und bindet Quellcode vom -Host ein. Die Watcher laufen im Polling-Modus, weil Bind-Mounts unter Windows -und macOS keine inotify-Events durchreichen; das kostet dauerhaft CPU, auch -ohne Benutzeraktivität. Dieser Stack gehört deshalb nur auf einen -Entwicklungsrechner. - -Beides enthält weder TLS, Authentifizierung, Reverse Proxy, Prozesshärtung noch -ein zentral betriebenes Datenbanksystem. Der Stack darf deshalb nicht öffentlich -erreichbar gemacht werden. - -## Topologie +## Entwicklungs-Topologie | Komponente | Port | Healthcheck | Persistenz | | --- | ---: | --- | --- | -| Next.js Web | 3001 | `GET /web-health` | keine | +| Next.js Web | 3001 | `GET /` | keine | | Express API | 3000 | `GET /health` | `./data:/app/data` | | SQLite | Datei | Integritäts-/FK-Prüfung via Backup und Skript | `data/leistungsbilanz.db` | @@ -32,56 +24,11 @@ Verwendete Umgebungsvariablen: - `API_INTERNAL_URL` – internes API-Ziel des Next.js-Rewrites, im Compose-Netz `http://api:3000` - `NEXT_TELEMETRY_DISABLED=1` -- `CHOKIDAR_USEPOLLING=true` und `WATCHPACK_POLLING=true` – nur in - `compose.dev.yaml`, für Dateibeobachtung über Bind-Mounts hinweg -- `LOG_LEVEL` – steuert für beide Dienste die Ausgabestufe des strukturierten - JSON-Loggers (`error`, `warn`, `info`, `verbose`, `debug`), Standard `info`. - Setzbar über eine `.env`-Datei neben `compose.yaml` oder - `LOG_LEVEL=verbose docker compose up`. +- `CHOKIDAR_USEPOLLING=true` und `WATCHPACK_POLLING=true` für lokale + Dateibeobachtung in Docker -Beim API-Start laufen zuerst die Migrationen und die Schemaprüfung -(`scripts/run-migrations.js` und `scripts/db-verify-circuit-schema.js`, im -Entwicklungsstack über `npm run db:migrate` und -`npm run db:verify:circuit-schema`). - -`API_INTERNAL_URL` wirkt für `next start` zur **Build-Zeit**: `next build` -schreibt die Rewrite-Ziele aus `next.config.mjs` fest in -`.next/routes-manifest.json`. `compose.yaml` reicht den Wert deshalb als -Build-Argument an das Image durch, nicht nur als Laufzeit-Variable. - -## Logging - -Beide Dienste schreiben strukturierte, einzeilige JSON-Log-Zeilen nach -stdout/stderr (`docker compose logs --follow`). Jede Zeile enthält -`timestamp`, `level`, `scope` und `message`. `compose.yaml` konfiguriert für -beide Dienste den `json-file`-Treiber mit Rotation (`max-size: 20m`, -`max-file: 10`, also bis zu 200 MB je Dienst); ohne diese Einstellung würde -Docker mit der Standardkonfiguration unbegrenzt in eine einzelne Datei unter -`/var/lib/docker/containers//` schreiben. Die Logs überleben -einen Container-Neustart (`docker compose restart`), aber nicht das Entfernen -des Containers (`docker compose down` gefolgt von `up` erzeugt neue -Container und damit neue, leere Logdateien); für ein echtes Langzeitarchiv -über Rebuilds hinweg müssten die Zeilen zusätzlich in eine Datei im -gemounteten `./data`-Verzeichnis oder an ein externes Log-System geschrieben -werden. Die Express-API protokolliert -jede abgeschlossene Anfrage (Methode, Pfad, Status, Dauer; `/health` wird -nicht mitgeloggt) sowie unbehandelte Exceptions/Promise-Rejections. Das -Next.js-Frontend protokolliert Seitenanfragen (Navigation) über -`src/proxy.ts` und unbehandelte Fehler über `src/instrumentation.ts`. -Beide Prozesse schreiben zusätzlich alle fünf Minuten einen `verbose`-Heartbeat -mit Laufzeit und Speicherverbrauch – nützlich, um Speicherlecks oder Hänger vor -einem 502 über einen längeren Zeitraum nachzuvollziehen. Für die Detailsuche -`LOG_LEVEL=debug` setzen; das protokolliert zusätzlich den Start jeder -API-Anfrage und macht damit hängende (nie abgeschlossene) Requests sichtbar. -Ein `close`-Ereignis ohne vorheriges `finish` wird als `request aborted before -response finished` (`warn`) geloggt und zeigt damit vom Client oder einem -vorgeschalteten Proxy abgebrochene Verbindungen. - -Eine unbehandelte Exception oder Promise-Rejection wird geloggt und beendet -den jeweiligen Prozess anschließend bewusst (`process.exit(1)`), statt in -einem unbekannten Zustand weiterzulaufen. Beide Dienste laufen deshalb mit -`restart: unless-stopped`, damit Docker sie danach automatisch neu startet; -ohne diese Policy würde ein Crash den Dienst dauerhaft unerreichbar lassen. +Beim API-Start laufen zuerst `npm run db:migrate` und +`npm run db:verify:circuit-schema`. ## Voraussetzungen für ein späteres Produktionssetup diff --git a/dynamo/01_check_model_identity.py b/dynamo/01_check_model_identity.py deleted file mode 100644 index a3fe009..0000000 --- a/dynamo/01_check_model_identity.py +++ /dev/null @@ -1,385 +0,0 @@ -"""Read-only Revit 2026 model-identity diagnostics for a Dynamo Python node. - -Optional Dynamo input: - IN[0]: output directory or complete .json file path - -The script intentionally performs no Revit transaction and changes no model -data. OUT contains a compact summary plus the complete report. -""" - -import datetime -import json -import os -import platform -import sys -import tempfile -import traceback - -import clr - -clr.AddReference("RevitAPI") -clr.AddReference("RevitServices") - -from Autodesk.Revit.DB import ModelPathUtils, StorageType # noqa: E402 -from RevitServices.Persistence import DocumentManager # noqa: E402 - - -CHECKED_PARAMETER_NAMES = ("LB_ModelId", "LB_ProjectId") - - -def safe_text(value): - if value is None: - return None - try: - return str(value) - except Exception: - return None - - -def element_id_text(element_id): - if element_id is None: - return None - try: - return str(element_id.Value) - except Exception: - try: - return str(element_id.IntegerValue) - except Exception: - return safe_text(element_id) - - -def forge_type_id_text(value): - if value is None: - return None - try: - return value.TypeId - except Exception: - return safe_text(value) - - -def parameter_value(parameter): - result = { - "hasValue": False, - "raw": None, - "display": None, - } - try: - result["hasValue"] = bool(parameter.HasValue) - except Exception: - pass - - try: - storage_type = parameter.StorageType - if storage_type == StorageType.String: - result["raw"] = parameter.AsString() - elif storage_type == StorageType.Integer: - result["raw"] = int(parameter.AsInteger()) - elif storage_type == StorageType.Double: - result["raw"] = float(parameter.AsDouble()) - elif storage_type == StorageType.ElementId: - result["raw"] = element_id_text(parameter.AsElementId()) - except Exception as error: - result["readError"] = safe_text(error) - - try: - result["display"] = parameter.AsValueString() - except Exception: - pass - return result - - -def describe_parameter(parameter): - definition = None - try: - definition = parameter.Definition - except Exception: - pass - - name = None - if definition is not None: - try: - name = definition.Name - except Exception: - pass - - is_shared = False - try: - is_shared = bool(parameter.IsShared) - except Exception: - pass - - shared_guid = None - if is_shared: - try: - shared_guid = str(parameter.GUID) - except Exception: - pass - - data_type = None - group_type = None - if definition is not None: - try: - data_type = forge_type_id_text(definition.GetDataType()) - except Exception: - pass - try: - group_type = forge_type_id_text(definition.GetGroupTypeId()) - except Exception: - pass - - unit_type = None - try: - unit_type = forge_type_id_text(parameter.GetUnitTypeId()) - except Exception: - pass - - try: - storage_type = str(parameter.StorageType) - except Exception: - storage_type = None - - try: - is_read_only = bool(parameter.IsReadOnly) - except Exception: - is_read_only = None - - try: - user_modifiable = bool(parameter.UserModifiable) - except Exception: - user_modifiable = None - - return { - "name": name, - "parameterId": element_id_text(getattr(parameter, "Id", None)), - "isShared": is_shared, - "sharedGuid": shared_guid, - "storageType": storage_type, - "dataTypeId": data_type, - "groupTypeId": group_type, - "unitTypeId": unit_type, - "isReadOnly": is_read_only, - "userModifiable": user_modifiable, - "value": parameter_value(parameter), - } - - -def sorted_parameters(element): - parameters = [] - try: - parameters = [describe_parameter(parameter) for parameter in element.Parameters] - except Exception: - return [] - return sorted( - parameters, - key=lambda parameter: ( - (parameter.get("name") or "").casefold(), - parameter.get("parameterId") or "", - ), - ) - - -def named_parameter_occurrences(element, parameter_name): - result = [] - try: - parameters = element.GetParameters(parameter_name) - if parameters is not None: - result = [describe_parameter(parameter) for parameter in parameters] - except Exception: - parameter = None - try: - parameter = element.LookupParameter(parameter_name) - except Exception: - pass - if parameter is not None: - result = [describe_parameter(parameter)] - return result - - -def loaded_assembly_versions(): - result = {} - try: - from System import AppDomain - - for assembly in AppDomain.CurrentDomain.GetAssemblies(): - try: - name = assembly.GetName() - simple_name = str(name.Name) - if simple_name in ( - "DynamoCore", - "DynamoCoreWpf", - "DynamoRevitDS", - "RevitAPI", - "RevitServices", - ): - result[simple_name] = str(name.Version) - except Exception: - continue - except Exception: - pass - return dict(sorted(result.items())) - - -def get_cloud_identity(document): - result = {"isModelInCloud": False} - try: - result["isModelInCloud"] = bool(document.IsModelInCloud) - except Exception: - return result - if not result["isModelInCloud"]: - return result - - try: - model_path = document.GetCloudModelPath() - result["userVisiblePath"] = ModelPathUtils.ConvertModelPathToUserVisiblePath( - model_path - ) - for property_name, output_name in ( - ("GetProjectGUID", "projectGuid"), - ("GetModelGUID", "modelGuid"), - ): - try: - result[output_name] = str(getattr(model_path, property_name)()) - except Exception: - pass - except Exception as error: - result["readError"] = safe_text(error) - return result - - -def get_worksharing_identity(document): - result = {"isWorkshared": False} - try: - result["isWorkshared"] = bool(document.IsWorkshared) - except Exception: - return result - if not result["isWorkshared"]: - return result - - try: - model_path = document.GetWorksharingCentralModelPath() - result["centralModelPath"] = ModelPathUtils.ConvertModelPathToUserVisiblePath( - model_path - ) - except Exception as error: - result["readError"] = safe_text(error) - return result - - -def resolve_output_path(configured_path, report_name): - timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") - default_directory = os.path.join(tempfile.gettempdir(), "leistungsbilanz-dynamo") - raw_path = safe_text(configured_path) - if raw_path is None or not raw_path.strip(): - directory = default_directory - file_path = os.path.join(directory, report_name + "-" + timestamp + ".json") - else: - expanded = os.path.abspath(os.path.expandvars(os.path.expanduser(raw_path.strip()))) - if expanded.lower().endswith(".json"): - file_path = expanded - directory = os.path.dirname(file_path) - else: - directory = expanded - file_path = os.path.join(directory, report_name + "-" + timestamp + ".json") - if not directory: - directory = os.getcwd() - if not os.path.isdir(directory): - os.makedirs(directory) - return file_path - - -def write_json(file_path, payload): - temporary_path = file_path + ".tmp" - with open(temporary_path, "w", encoding="utf-8", newline="\n") as output: - json.dump(payload, output, ensure_ascii=False, indent=2, sort_keys=True) - output.write("\n") - os.replace(temporary_path, file_path) - - -def get_input(index, default=None): - values = globals().get("IN", []) - try: - value = values[index] - return default if value is None else value - except Exception: - return default - - -def build_report(): - document = DocumentManager.Instance.CurrentDBDocument - if document is None: - raise RuntimeError("No active Revit document is available.") - - project_information = document.ProjectInformation - if project_information is None: - raise RuntimeError("The active document has no Project Information element.") - - application = document.Application - checked_parameters = { - name: named_parameter_occurrences(project_information, name) - for name in CHECKED_PARAMETER_NAMES - } - warnings = [] - for name in CHECKED_PARAMETER_NAMES: - occurrences = checked_parameters[name] - populated = [ - parameter - for parameter in occurrences - if parameter.get("value", {}).get("raw") not in (None, "") - ] - if not occurrences: - warnings.append(name + " is not bound to Project Information.") - elif not populated: - warnings.append(name + " exists but has no value on Project Information.") - elif len(occurrences) > 1: - warnings.append(name + " occurs more than once; use a shared-parameter GUID later.") - - return { - "reportSchemaVersion": 1, - "generatedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(), - "readOnly": True, - "environment": { - "revitVersionNumber": safe_text(getattr(application, "VersionNumber", None)), - "revitVersionName": safe_text(getattr(application, "VersionName", None)), - "revitSubVersionNumber": safe_text( - getattr(application, "SubVersionNumber", None) - ), - "pythonImplementation": safe_text(getattr(sys.implementation, "name", None)), - "pythonVersion": platform.python_version(), - "assemblies": loaded_assembly_versions(), - }, - "document": { - "title": safe_text(document.Title), - "pathName": safe_text(document.PathName), - "isFamilyDocument": bool(document.IsFamilyDocument), - "cloud": get_cloud_identity(document), - "worksharing": get_worksharing_identity(document), - }, - "modelIdentityCandidates": { - "projectInformationUniqueId": safe_text(project_information.UniqueId), - "projectInformationElementId": element_id_text(project_information.Id), - "checkedProjectParameters": checked_parameters, - }, - "projectInformationParameters": sorted_parameters(project_information), - "warnings": warnings, - } - - -try: - report = build_report() - output_path = resolve_output_path(get_input(0), "model-identity") - write_json(output_path, report) - OUT = { - "ok": True, - "filePath": output_path, - "projectInformationUniqueId": report["modelIdentityCandidates"][ - "projectInformationUniqueId" - ], - "warnings": report["warnings"], - "report": report, - } -except Exception as error: - OUT = { - "ok": False, - "error": safe_text(error), - "traceback": traceback.format_exc(), - } diff --git a/dynamo/02_export_electrical_fixture_parameter_inventory.py b/dynamo/02_export_electrical_fixture_parameter_inventory.py deleted file mode 100644 index 440a222..0000000 --- a/dynamo/02_export_electrical_fixture_parameter_inventory.py +++ /dev/null @@ -1,517 +0,0 @@ -"""Export all Electrical Fixtures instance/type parameters from Revit 2026. - -Optional Dynamo inputs: - IN[0]: output directory or complete .json file path - IN[1]: include empty parameters (default True) - IN[2]: maximum aggregated sample values (default 5) - -The script is read-only and performs no Revit transaction. -""" - -import datetime -import json -import os -import platform -import sys -import tempfile -import traceback - -import clr - -clr.AddReference("RevitAPI") -clr.AddReference("RevitServices") - -from Autodesk.Revit.DB import ( # noqa: E402 - BuiltInCategory, - FilteredElementCollector, - ModelPathUtils, - StorageType, -) -from RevitServices.Persistence import DocumentManager # noqa: E402 - - -def safe_text(value): - if value is None: - return None - try: - return str(value) - except Exception: - return None - - -def element_id_text(element_id): - if element_id is None: - return None - try: - return str(element_id.Value) - except Exception: - try: - return str(element_id.IntegerValue) - except Exception: - return safe_text(element_id) - - -def forge_type_id_text(value): - if value is None: - return None - try: - return value.TypeId - except Exception: - return safe_text(value) - - -def parameter_value(parameter): - result = {"hasValue": False, "raw": None, "display": None} - try: - result["hasValue"] = bool(parameter.HasValue) - except Exception: - pass - - try: - storage_type = parameter.StorageType - if storage_type == StorageType.String: - result["raw"] = parameter.AsString() - elif storage_type == StorageType.Integer: - result["raw"] = int(parameter.AsInteger()) - elif storage_type == StorageType.Double: - result["raw"] = float(parameter.AsDouble()) - elif storage_type == StorageType.ElementId: - result["raw"] = element_id_text(parameter.AsElementId()) - except Exception as error: - result["readError"] = safe_text(error) - - try: - result["display"] = parameter.AsValueString() - except Exception: - pass - return result - - -def describe_parameter(parameter, scope): - definition = None - try: - definition = parameter.Definition - except Exception: - pass - - name = None - data_type = None - group_type = None - if definition is not None: - try: - name = definition.Name - except Exception: - pass - try: - data_type = forge_type_id_text(definition.GetDataType()) - except Exception: - pass - try: - group_type = forge_type_id_text(definition.GetGroupTypeId()) - except Exception: - pass - - is_shared = False - try: - is_shared = bool(parameter.IsShared) - except Exception: - pass - - shared_guid = None - if is_shared: - try: - shared_guid = str(parameter.GUID) - except Exception: - pass - - unit_type = None - try: - unit_type = forge_type_id_text(parameter.GetUnitTypeId()) - except Exception: - pass - - try: - storage_type = str(parameter.StorageType) - except Exception: - storage_type = None - - try: - is_read_only = bool(parameter.IsReadOnly) - except Exception: - is_read_only = None - - try: - user_modifiable = bool(parameter.UserModifiable) - except Exception: - user_modifiable = None - - return { - "scope": scope, - "name": name, - "parameterId": element_id_text(getattr(parameter, "Id", None)), - "isShared": is_shared, - "sharedGuid": shared_guid, - "storageType": storage_type, - "dataTypeId": data_type, - "groupTypeId": group_type, - "unitTypeId": unit_type, - "isReadOnly": is_read_only, - "userModifiable": user_modifiable, - "value": parameter_value(parameter), - } - - -def has_meaningful_value(parameter_description): - value = parameter_description.get("value", {}) - return bool(value.get("hasValue")) or value.get("raw") not in (None, "") or value.get( - "display" - ) not in (None, "") - - -def read_parameters(element, scope, include_empty): - result = [] - try: - for parameter in element.Parameters: - description = describe_parameter(parameter, scope) - if include_empty or has_meaningful_value(description): - result.append(description) - except Exception as error: - return [], [safe_text(error)] - result.sort( - key=lambda parameter: ( - (parameter.get("name") or "").casefold(), - parameter.get("parameterId") or "", - ) - ) - return result, [] - - -def read_space(element, document): - try: - space = element.Space - except Exception as error: - return None, safe_text(error) - if space is None: - return None, None - - level_name = None - try: - level = document.GetElement(space.LevelId) - level_name = None if level is None else safe_text(level.Name) - except Exception: - pass - - return { - "uniqueId": safe_text(space.UniqueId), - "elementId": element_id_text(space.Id), - "number": safe_text(getattr(space, "Number", None)), - "name": safe_text(getattr(space, "Name", None)), - "levelName": level_name, - }, None - - -def read_family_identity(element, document): - symbol = None - try: - symbol = element.Symbol - except Exception: - try: - symbol = document.GetElement(element.GetTypeId()) - except Exception: - pass - - family_name = None - type_name = None - type_unique_id = None - type_element_id = None - if symbol is not None: - try: - family_name = safe_text(symbol.Family.Name) - except Exception: - family_name = safe_text(getattr(symbol, "FamilyName", None)) - type_name = safe_text(getattr(symbol, "Name", None)) - type_unique_id = safe_text(getattr(symbol, "UniqueId", None)) - type_element_id = element_id_text(getattr(symbol, "Id", None)) - - return { - "familyName": family_name, - "typeName": type_name, - "typeUniqueId": type_unique_id, - "typeElementId": type_element_id, - }, symbol - - -def parameter_inventory_key(parameter): - stable_id = parameter.get("sharedGuid") or parameter.get("parameterId") or "" - return "|".join( - ( - parameter.get("scope") or "", - stable_id, - parameter.get("name") or "", - parameter.get("dataTypeId") or "", - ) - ) - - -def sample_value_key(value): - return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) - - -def add_to_inventory(inventory, parameter, max_samples): - key = parameter_inventory_key(parameter) - entry = inventory.get(key) - if entry is None: - entry = { - "scope": parameter.get("scope"), - "name": parameter.get("name"), - "parameterId": parameter.get("parameterId"), - "isShared": parameter.get("isShared"), - "sharedGuid": parameter.get("sharedGuid"), - "storageType": parameter.get("storageType"), - "dataTypeId": parameter.get("dataTypeId"), - "groupTypeId": parameter.get("groupTypeId"), - "unitTypeId": parameter.get("unitTypeId"), - "occurrenceCount": 0, - "populatedCount": 0, - "sampleValues": [], - "_sampleKeys": set(), - } - inventory[key] = entry - entry["occurrenceCount"] += 1 - if has_meaningful_value(parameter): - entry["populatedCount"] += 1 - value = parameter.get("value") - value_key = sample_value_key(value) - if len(entry["sampleValues"]) < max_samples and value_key not in entry["_sampleKeys"]: - entry["_sampleKeys"].add(value_key) - entry["sampleValues"].append(value) - - -def finalize_inventory(inventory): - result = [] - for entry in inventory.values(): - clean_entry = dict(entry) - clean_entry.pop("_sampleKeys", None) - result.append(clean_entry) - return sorted( - result, - key=lambda entry: ( - entry.get("scope") or "", - (entry.get("name") or "").casefold(), - entry.get("sharedGuid") or entry.get("parameterId") or "", - ), - ) - - -def loaded_assembly_versions(): - result = {} - try: - from System import AppDomain - - for assembly in AppDomain.CurrentDomain.GetAssemblies(): - try: - name = assembly.GetName() - simple_name = str(name.Name) - if simple_name in ( - "DynamoCore", - "DynamoCoreWpf", - "DynamoRevitDS", - "RevitAPI", - "RevitServices", - ): - result[simple_name] = str(name.Version) - except Exception: - continue - except Exception: - pass - return dict(sorted(result.items())) - - -def resolve_output_path(configured_path): - timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S") - default_directory = os.path.join(tempfile.gettempdir(), "leistungsbilanz-dynamo") - raw_path = safe_text(configured_path) - if raw_path is None or not raw_path.strip(): - directory = default_directory - file_path = os.path.join( - directory, "electrical-fixture-parameters-" + timestamp + ".json" - ) - else: - expanded = os.path.abspath(os.path.expandvars(os.path.expanduser(raw_path.strip()))) - if expanded.lower().endswith(".json"): - file_path = expanded - directory = os.path.dirname(file_path) - else: - directory = expanded - file_path = os.path.join( - directory, "electrical-fixture-parameters-" + timestamp + ".json" - ) - if not directory: - directory = os.getcwd() - if not os.path.isdir(directory): - os.makedirs(directory) - return file_path - - -def write_json(file_path, payload): - temporary_path = file_path + ".tmp" - with open(temporary_path, "w", encoding="utf-8", newline="\n") as output: - json.dump(payload, output, ensure_ascii=False, indent=2, sort_keys=True) - output.write("\n") - os.replace(temporary_path, file_path) - - -def get_input(index, default=None): - values = globals().get("IN", []) - try: - value = values[index] - return default if value is None else value - except Exception: - return default - - -def build_report(include_empty, max_samples): - document = DocumentManager.Instance.CurrentDBDocument - if document is None: - raise RuntimeError("No active Revit document is available.") - if document.IsFamilyDocument: - raise RuntimeError("Open a Revit project document, not a family document.") - - application = document.Application - collector = ( - FilteredElementCollector(document) - .OfCategory(BuiltInCategory.OST_ElectricalFixtures) - .WhereElementIsNotElementType() - ) - source_elements = list(collector) - source_elements.sort(key=lambda element: safe_text(element.UniqueId) or "") - - elements = [] - types_by_unique_id = {} - inventory = {} - errors = [] - elements_without_space = 0 - - for element in source_elements: - element_errors = [] - try: - family_identity, symbol = read_family_identity(element, document) - instance_parameters, parameter_errors = read_parameters( - element, "instance", include_empty - ) - element_errors.extend(parameter_errors) - for parameter in instance_parameters: - add_to_inventory(inventory, parameter, max_samples) - - type_unique_id = family_identity.get("typeUniqueId") - if symbol is not None and type_unique_id and type_unique_id not in types_by_unique_id: - type_parameters, type_errors = read_parameters(symbol, "type", include_empty) - element_errors.extend(type_errors) - for parameter in type_parameters: - add_to_inventory(inventory, parameter, max_samples) - types_by_unique_id[type_unique_id] = { - **family_identity, - "parameters": type_parameters, - } - - space, space_error = read_space(element, document) - if space_error: - element_errors.append("MEP Space: " + space_error) - if space is None: - elements_without_space += 1 - - elements.append( - { - "uniqueId": safe_text(element.UniqueId), - "elementId": element_id_text(element.Id), - "categoryName": safe_text( - None if element.Category is None else element.Category.Name - ), - "family": family_identity, - "space": space, - "instanceParameters": instance_parameters, - "warnings": element_errors, - } - ) - except Exception as error: - errors.append( - { - "uniqueId": safe_text(getattr(element, "UniqueId", None)), - "elementId": element_id_text(getattr(element, "Id", None)), - "error": safe_text(error), - } - ) - - return { - "reportSchemaVersion": 1, - "generatedAt": datetime.datetime.now(datetime.timezone.utc).isoformat(), - "readOnly": True, - "complete": len(errors) == 0, - "scope": { - "builtInCategory": "OST_ElectricalFixtures", - "wholeDocument": True, - "elementTypesExcluded": True, - "includeEmptyParameters": include_empty, - }, - "environment": { - "revitVersionNumber": safe_text(getattr(application, "VersionNumber", None)), - "revitVersionName": safe_text(getattr(application, "VersionName", None)), - "revitSubVersionNumber": safe_text( - getattr(application, "SubVersionNumber", None) - ), - "pythonImplementation": safe_text(getattr(sys.implementation, "name", None)), - "pythonVersion": platform.python_version(), - "assemblies": loaded_assembly_versions(), - }, - "document": { - "title": safe_text(document.Title), - "pathName": safe_text(document.PathName), - "projectInformationUniqueId": safe_text(document.ProjectInformation.UniqueId), - }, - "summary": { - "elementCount": len(source_elements), - "exportedElementCount": len(elements), - "typeCount": len(types_by_unique_id), - "parameterDefinitionCount": len(inventory), - "elementsWithoutMepSpace": elements_without_space, - "elementErrorCount": len(errors), - }, - "parameterInventory": finalize_inventory(inventory), - "types": sorted( - types_by_unique_id.values(), - key=lambda entry: ( - (entry.get("familyName") or "").casefold(), - (entry.get("typeName") or "").casefold(), - entry.get("typeUniqueId") or "", - ), - ), - "elements": elements, - "errors": errors, - } - - -try: - include_empty_input = get_input(1, True) - include_empty = bool(include_empty_input) - try: - max_samples = int(get_input(2, 5)) - except Exception: - max_samples = 5 - max_samples = max(0, min(max_samples, 50)) - - report = build_report(include_empty, max_samples) - output_path = resolve_output_path(get_input(0)) - write_json(output_path, report) - OUT = { - "ok": True, - "filePath": output_path, - "complete": report["complete"], - "summary": report["summary"], - "errors": report["errors"], - } -except Exception as error: - OUT = { - "ok": False, - "error": safe_text(error), - "traceback": traceback.format_exc(), - } diff --git a/dynamo/README.md b/dynamo/README.md deleted file mode 100644 index 800eded..0000000 --- a/dynamo/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# Revit 2026 / Dynamo diagnostics - -This directory contains self-contained Python scripts for a Dynamo **Python -Script** node. They use only Dynamo's built-in Revit integration, the Revit API -and the Python standard library. No Dynamo package is required. - -The scripts are read-only. They do not start a Revit transaction and do not -change the open model. - -## Python engine - -Use the built-in `CPython3` engine in Revit 2026. Autodesk ships Dynamo with -Revit; optional PythonNet3 packages are not required by these diagnostics. - -## 01 - Check model identity - -File: `01_check_model_identity.py` - -The script reports: - -- Revit, Dynamo and Python versions; -- `ProjectInformation.UniqueId` as a native model-identity candidate; -- all occurrences and values of `LB_ModelId` and `LB_ProjectId` on Project - Information; -- all Project Information parameters; -- optional cloud/worksharing identity information when the API exposes it. - -Input `IN[0]` is optional. It may be either an output directory or a complete -`.json` file path. With no input, the report is written below the current -Windows temporary directory in `leistungsbilanz-dynamo`. - -## 02 - Inventory Electrical Fixtures parameters - -File: `02_export_electrical_fixture_parameter_inventory.py` - -The script reads every instance of -`BuiltInCategory.OST_ElectricalFixtures` in the complete current document. It -exports: - -- element, family, type and MEP Space identities; -- every instance parameter and value; -- every unique family-type parameter and value; -- an aggregated parameter inventory with occurrence counts and sample values; -- per-element warnings instead of aborting at the first unreadable element. - -Inputs: - -- `IN[0]` (optional): output directory or complete `.json` path; -- `IN[1]` (optional): include empty parameters, default `true`; -- `IN[2]` (optional): maximum sample values per aggregated parameter, default - `5`. - -The default output location is again the Windows temporary directory. The -generated report can contain model paths and project-specific parameter values; -review it before sharing or committing it. - -## Running a script - -1. Open the target model in Revit 2026. -2. Open Dynamo from **Manage > Visual Programming > Dynamo**. -3. Create a graph and add a **Python Script** node. -4. Select the `CPython3` engine for the node. -5. Copy the complete content of the desired `.py` file into the node. -6. Optionally connect a String node containing the output path to `IN[0]`. -7. Run the graph and inspect `OUT` for status, counts and the generated path. - -For the first test, run `01_check_model_identity.py` in the Revit main model. -Then run the parameter inventory. Keep both generated JSON files so their -structure can be checked before the production snapshot DTO is finalized. diff --git a/next.config.mjs b/next.config.mjs index e0610b9..e81b670 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -2,12 +2,6 @@ const apiInternalUrl = (process.env.API_INTERNAL_URL || "http://localhost:3000").replace(/\/$/, ""); const nextConfig = { - allowedDevOrigins: [ - "192.168.3.13", - "docker01.int.jappel.io", - "lb.jappel.io" - ], - typescript: { tsconfigPath: "./tsconfig.next.json", }, @@ -26,4 +20,3 @@ const nextConfig = { }; export default nextConfig; - diff --git a/package-lock.json b/package-lock.json index 417421b..b0abb41 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,15 +22,12 @@ "devDependencies": { "@types/better-sqlite3": "^7.6.13", "@types/express": "^5.0.6", - "@types/node": "^24.10.1", + "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "drizzle-kit": "^0.31.10", "tsx": "^4.21.0", "typescript": "^6.0.3" - }, - "engines": { - "node": "24.x" } }, "node_modules/@drizzle-team/brocli": { @@ -1517,13 +1514,12 @@ "dev": true }, "node_modules/@types/node": { - "version": "24.13.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.3.tgz", - "integrity": "sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==", + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "devOptional": true, - "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~7.19.0" } }, "node_modules/@types/qs": { @@ -3692,11 +3688,10 @@ } }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "devOptional": true, - "license": "MIT" + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "devOptional": true }, "node_modules/unpipe": { "version": "1.0.0", diff --git a/package.json b/package.json index 414eb53..9bc5ae3 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,6 @@ "version": "1.0.0", "description": "Spreadsheet-style circuit list editor for electrical distribution planning", "main": "dist/server/index.js", - "engines": { - "node": "24.x" - }, "scripts": { "dev": "npm run dev:api", "dev:api": "tsx watch src/server/index.ts", @@ -13,9 +10,6 @@ "docker:up": "docker compose up --build --detach", "docker:down": "docker compose down", "docker:logs": "docker compose logs --follow", - "docker:dev:up": "docker compose -f compose.dev.yaml up --build --detach", - "docker:dev:down": "docker compose -f compose.dev.yaml down", - "docker:dev:logs": "docker compose -f compose.dev.yaml logs --follow", "build": "npm run build:api", "build:api": "tsc -p tsconfig.json", "build:web": "next build", @@ -46,7 +40,7 @@ "devDependencies": { "@types/better-sqlite3": "^7.6.13", "@types/express": "^5.0.6", - "@types/node": "^24.10.1", + "@types/node": "^25.6.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "drizzle-kit": "^0.31.10", diff --git a/src/app/globals.css b/src/app/globals.css index 22da5df..9c07a40 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3,13 +3,7 @@ Bootstrap bleibt Basis für Formulare/Tabellen/Modals; wir tönen die vorhandenen Bootstrap-Komponenten über deren eigene --bs-btn-*-Variablen um (globals.css lädt nach bootstrap.min.css, gleiche Spezifität gewinnt - per Ladereihenfolge) statt Bootstrap zu ersetzen. - - Dark mode: Bootstrap schaltet seine eigenen Komponenten (Formulare, - Modals, Cards, Alerts, ...) automatisch um, sobald `data-bs-theme="dark"` - auf steht (gesetzt von ThemeToggle.tsx, persistiert in - localStorage). Der custom Stromkreis-Grid nutzt dafür dieselben - Tokens unten, per `:root[data-bs-theme="dark"]` überschrieben. */ + per Ladereihenfolge) statt Bootstrap zu ersetzen. */ :root { --color-primary: #1c3f52; /* Petrol */ --color-primary-dark: #0f2733; /* Petrol Dark */ @@ -39,105 +33,6 @@ --space-xl: 40px; --radius: 12px; - - /* ── Grid/Editor-Tokens (hell) ──────────────────────────────────────── */ - --panel-bg: #ffffff; - --panel-bg-subtle: #f8fafc; - --panel-border: #d9dee8; - --panel-border-strong: #c4cddc; - --input-border: #9fb6e0; - --text-strong: #1f2937; - --text-muted: #4b5563; - --text-faint: #6b7280; - --text-subtle: #475569; - - --accent-blue: #2563eb; - --accent-blue-soft-bg: #eff6ff; - --accent-blue-border: #bfdbfe; - --accent-blue-strong-border: #4c7dd9; - --accent-blue-drop-border: #2b6cb0; - --accent-blue-marker: #1d4ed8; - - --surface-selected: #eaf1ff; - --surface-header-row: #e8eef8; - --surface-component-header: #e2e8f0; - --surface-component-group: #f8fafc; - --surface-component-footer: #f1f5f9; - --surface-hover: #f3f4f6; - - --grid-warn: #d97706; - --grid-warn-bg: #fff7ed; - --grid-warn-strong: #9a3412; - --grid-danger: #c2410c; - - --notice-info-bg: #ebf3ff; - --notice-info-border: #bad1f7; - --notice-error-bg: #fdecec; - --notice-error-border: #f5b5b5; - --notice-warning-bg: #fff7ed; - --notice-warning-border: #fdba74; - --notice-muted-bg: #f6f6f6; - --notice-muted-border: #e4e4e4; - - --shadow-soft: rgba(15, 39, 51, 0.07); - --shadow-strong: rgba(15, 39, 51, 0.28); - --shadow-menu: rgba(0, 0, 0, 0.12); - --shadow-drawer: rgba(31, 41, 55, 0.22); -} - -:root[data-bs-theme="dark"] { - --color-ink: #e6ecef; - --color-ink-soft: #9db0b8; - --color-bg: #141b20; - --color-surface: #0f1519; - --color-border: #2a3941; - --color-danger: #e2664f; - --color-danger-bg: #33201c; - --color-warn: #e0983f; - - /* ── Grid/Editor-Tokens (dunkel) ────────────────────────────────────── */ - --panel-bg: #182229; - --panel-bg-subtle: #1c2830; - --panel-border: #2c3c45; - --panel-border-strong: #374a54; - --input-border: #3c5568; - --text-strong: #e6ecef; - --text-muted: #aebdc4; - --text-faint: #8798a0; - --text-subtle: #9db0b8; - - --accent-blue: #6fa2f7; - --accent-blue-soft-bg: #17253a; - --accent-blue-border: #2c4a72; - --accent-blue-strong-border: #6fa2f7; - --accent-blue-drop-border: #4f84d6; - --accent-blue-marker: #6fa2f7; - - --surface-selected: #17253a; - --surface-header-row: #1a262f; - --surface-component-header: #202e37; - --surface-component-group: #1a262f; - --surface-component-footer: #182229; - --surface-hover: #1f2b33; - - --grid-warn: #e0983f; - --grid-warn-bg: #2e2214; - --grid-warn-strong: #f0b880; - --grid-danger: #e2825f; - - --notice-info-bg: #182839; - --notice-info-border: #2c4a72; - --notice-error-bg: #33201c; - --notice-error-border: #5c332c; - --notice-warning-bg: #2e2214; - --notice-warning-border: #6b4a1c; - --notice-muted-bg: #1b2226; - --notice-muted-border: #2a3338; - - --shadow-soft: rgba(0, 0, 0, 0.35); - --shadow-strong: rgba(0, 0, 0, 0.55); - --shadow-menu: rgba(0, 0, 0, 0.45); - --shadow-drawer: rgba(0, 0, 0, 0.55); } html { @@ -214,12 +109,6 @@ a { --bs-alert-border-color: #b6e2ce; } -:root[data-bs-theme="dark"] .alert-success { - --bs-alert-color: #7fd6a7; - --bs-alert-bg: #16291f; - --bs-alert-border-color: #205a38; -} - /* ── App-Shell / Seitenleiste (nur auf der Projektseite) ────────────────── */ .app-shell { @@ -276,7 +165,6 @@ a { flex-direction: column; gap: 2px; padding: 12px; - flex: 1; } .sidebar-section-label { @@ -322,32 +210,6 @@ a { justify-content: center; } -.sidebar-footer { - padding: 12px; - border-top: 1px solid rgba(255, 255, 255, 0.12); -} - -.sidebar-theme-toggle { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 14px; - border-radius: 8px; - color: #c7d6e0; - font-size: 0.92rem; - font-weight: 500; - border: 1px solid rgba(255, 255, 255, 0.16); - background: rgba(255, 255, 255, 0.06); - width: 100%; - text-align: left; - cursor: pointer; -} - -.sidebar-theme-toggle:hover { - background: rgba(255, 255, 255, 0.14); - color: #fff; -} - .app-shell-content { width: 100%; min-width: 0; @@ -368,10 +230,6 @@ a { color: var(--color-primary); } -:root[data-bs-theme="dark"] .page-header h1 { - color: var(--color-accent-pale); -} - .kicker { font-size: 0.72rem; letter-spacing: 0.12em; @@ -404,7 +262,7 @@ a { a.kpi:hover { color: inherit; text-decoration: none; - box-shadow: 0 3px 8px var(--shadow-soft), 0 16px 32px -14px var(--shadow-strong); + box-shadow: 0 3px 8px rgba(15, 39, 51, 0.07), 0 16px 32px -14px rgba(15, 39, 51, 0.28); transform: translateY(-1px); } @@ -427,16 +285,12 @@ a.kpi:hover { color: var(--color-primary); } -:root[data-bs-theme="dark"] .kpi-value { - color: var(--color-accent-pale); -} - .card { border-radius: var(--radius); border-top: 3px solid var(--color-primary); box-shadow: - 0 1px 2px var(--shadow-soft), - 0 10px 24px -14px var(--shadow-strong); + 0 1px 2px rgba(15, 39, 51, 0.05), + 0 10px 24px -14px rgba(15, 39, 51, 0.22); } .card-header { @@ -446,10 +300,6 @@ a.kpi:hover { border-bottom: 1px solid var(--color-border); } -:root[data-bs-theme="dark"] .card-header { - color: var(--color-accent-pale); -} - .table td input.form-control-sm, .table td select.form-select-sm { min-width: 8rem; @@ -470,14 +320,13 @@ a.kpi:hover { top: 0; z-index: 12; padding: 0.35rem 0; - background: var(--panel-bg); - box-shadow: 0 1px 0 var(--panel-border-strong); + background: #fff; + box-shadow: 0 1px 0 rgba(196, 205, 220, 0.8); } .editor-toolbar button { - border: 1px solid var(--panel-border-strong); - background: var(--panel-bg); - color: var(--text-strong); + border: 1px solid #c4cddc; + background: #fff; padding: 0.28rem 0.6rem; border-radius: 4px; font-size: 0.82rem; @@ -488,8 +337,8 @@ a.kpi:hover { } .editor-toolbar .project-device-drawer-toggle { - border-color: var(--accent-blue); - background: var(--accent-blue); + border-color: #2563eb; + background: #2563eb; color: #fff; font-weight: 600; } @@ -500,10 +349,10 @@ a.kpi:hover { align-items: center; gap: 0.35rem; padding: 0.4rem 0.5rem; - border: 1px solid var(--accent-blue-border); + border: 1px solid #bfdbfe; border-radius: 5px; - background: var(--accent-blue-soft-bg); - color: var(--text-subtle); + background: #eff6ff; + color: #1e3a5f; font-size: 0.78rem; } @@ -513,10 +362,10 @@ a.kpi:hover { .active-view-chip, .active-view-reset { - border: 1px solid var(--accent-blue-border); + border: 1px solid #93b4df; border-radius: 999px; - background: var(--panel-bg); - color: var(--text-subtle); + background: #fff; + color: #1e3a5f; padding: 0.18rem 0.48rem; font-size: 0.76rem; cursor: pointer; @@ -524,7 +373,7 @@ a.kpi:hover { .active-view-chip:hover, .active-view-reset:hover { - border-color: var(--accent-blue); + border-color: #2563eb; } .active-view-reset { @@ -537,9 +386,9 @@ a.kpi:hover { grid-template-columns: repeat(3, minmax(12rem, 1fr)); gap: 0.5rem; padding: 0.55rem; - border: 1px solid var(--panel-border); + border: 1px solid #cbd5e1; border-radius: 5px; - background: var(--panel-bg-subtle); + background: #f8fafc; } .distribution-power-summary > div { @@ -549,13 +398,13 @@ a.kpi:hover { } .distribution-power-summary span { - color: var(--text-subtle); + color: #475569; font-size: 0.74rem; line-height: 1.2; } .distribution-power-summary strong { - color: var(--text-strong); + color: #172033; font-size: 0.95rem; } @@ -569,13 +418,13 @@ a.kpi:hover { position: absolute; z-index: 9; margin-top: 2rem; - border: 1px solid var(--panel-border); - background: var(--panel-bg); + border: 1px solid #cfd7e5; + background: #fff; border-radius: 5px; - box-shadow: 0 6px 16px var(--shadow-menu); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); padding: 0.55rem; width: 340px; - color: var(--text-strong); + color: #1f2937; text-align: left; } @@ -595,7 +444,7 @@ a.kpi:hover { .column-settings-close { border: 0 !important; background: transparent !important; - color: var(--text-muted); + color: #4b5563; padding: 0.05rem 0.2rem !important; font-size: 1rem !important; line-height: 1; @@ -604,7 +453,7 @@ a.kpi:hover { .column-settings-explanation { margin-bottom: 0.4rem; - color: var(--text-muted); + color: #4b5563; font-size: 0.72rem; line-height: 1.3; } @@ -612,11 +461,9 @@ a.kpi:hover { .column-settings-search { width: 100%; margin-bottom: 0.4rem; - border: 1px solid var(--panel-border-strong); + border: 1px solid #c4cddc; border-radius: 4px; padding: 0.3rem 0.4rem; - background: var(--panel-bg); - color: var(--text-strong); font-size: 0.76rem; } @@ -626,7 +473,7 @@ a.kpi:hover { gap: 0.15rem; max-height: 300px; overflow: auto; - border: 1px solid var(--panel-border); + border: 1px solid #e1e6ef; border-radius: 4px; padding: 0.25rem; } @@ -642,8 +489,8 @@ a.kpi:hover { } .column-settings-item.selected { - border-color: var(--accent-blue-border); - background: var(--accent-blue-soft-bg); + border-color: #bfdbfe; + background: #eff6ff; } .column-settings-item.dragging { @@ -651,12 +498,12 @@ a.kpi:hover { } .column-settings-item.drop-target { - border-color: var(--accent-blue-drop-border); - background: var(--accent-blue-soft-bg); + border-color: #2b6cb0; + background: #ebf4ff; } .column-settings-item.locked { - background: var(--panel-bg-subtle); + background: #f7fafc; } .column-visibility-button { @@ -683,7 +530,7 @@ a.kpi:hover { flex: 0 0 1rem; align-items: center; justify-content: center; - color: var(--accent-blue-marker); + color: #1d4ed8; font-weight: 700; } @@ -693,9 +540,8 @@ a.kpi:hover { } .column-settings-order button { - border: 1px solid var(--panel-border-strong); - background: var(--panel-bg); - color: var(--text-strong); + border: 1px solid #c4cddc; + background: #fff; border-radius: 3px; font-size: 0.72rem; padding: 0.08rem 0.28rem; @@ -703,8 +549,8 @@ a.kpi:hover { .tree-grid-wrap { overflow: auto; - border: 1px solid var(--panel-border); - background: var(--panel-bg); + border: 1px solid #d9dee8; + background: #fff; width: 100%; max-width: 100%; min-width: 0; @@ -714,7 +560,7 @@ a.kpi:hover { .column-settings-empty { padding: 0.55rem 0.35rem; - color: var(--text-faint); + color: #6b7280; font-size: 0.76rem; } @@ -724,8 +570,8 @@ a.kpi:hover { } .column-settings-footer button.primary { - border-color: var(--accent-blue); - background: var(--accent-blue); + border-color: #2563eb; + background: #2563eb; color: #fff; } @@ -737,8 +583,8 @@ a.kpi:hover { } .project-device-sidebar { - border: 1px solid var(--panel-border); - background: var(--panel-bg); + border: 1px solid #d9dee8; + background: #fff; padding: 0.6rem; display: flex; flex-direction: column; @@ -748,17 +594,14 @@ a.kpi:hover { .project-device-sidebar h3 { margin: 0; font-size: 1rem; - color: var(--text-strong); } .project-device-sidebar input, .project-device-sidebar select { width: 100%; - border: 1px solid var(--panel-border-strong); + border: 1px solid #cfd7e5; border-radius: 4px; padding: 0.25rem 0.35rem; - background: var(--panel-bg); - color: var(--text-strong); } .project-device-list { @@ -770,9 +613,8 @@ a.kpi:hover { } .project-device-item { - border: 1px solid var(--panel-border-strong); - background: var(--panel-bg-subtle); - color: var(--text-strong); + border: 1px solid #d5ddec; + background: #f8faff; text-align: left; padding: 0.4rem; border-radius: 4px; @@ -783,8 +625,8 @@ a.kpi:hover { } .project-device-item.selected { - border-color: var(--accent-blue-strong-border); - background: var(--accent-blue-soft-bg); + border-color: #4c7dd9; + background: #edf3ff; } .project-device-item.dragging { @@ -803,13 +645,11 @@ a.kpi:hover { flex-direction: column; gap: 0.2rem; font-size: 0.82rem; - color: var(--text-strong); } .sidebar-actions button { - border: 1px solid var(--panel-border-strong); - background: var(--panel-bg); - color: var(--text-strong); + border: 1px solid #c4cddc; + background: #fff; border-radius: 4px; padding: 0.3rem 0.45rem; font-size: 0.82rem; @@ -820,19 +660,18 @@ a.kpi:hover { min-width: 0; border-collapse: collapse; font-size: 0.9rem; - color: var(--text-strong); } .tree-grid th, .tree-grid td { - border: 1px solid var(--panel-border); + border: 1px solid #e4e9f2; padding: 0.35rem 0.4rem; vertical-align: middle; } .tree-grid th { width: 1px; - background: var(--panel-bg-subtle); + background: #f4f7fb; position: sticky; top: 0; z-index: 2; @@ -855,9 +694,8 @@ a.kpi:hover { .tree-grid .header-filter-btn { display: block; width: 100%; - border: 1px solid var(--panel-border-strong); - background: var(--panel-bg); - color: var(--text-strong); + border: 1px solid #c4cddc; + background: #fff; border-radius: 3px; font-size: 0.75rem; padding: 0.15rem 0.35rem; @@ -874,7 +712,7 @@ a.kpi:hover { border: 0; background: transparent; padding: 0; - color: var(--text-strong); + color: #1f2937; font: inherit; font-size: 0.78rem; font-weight: 600; @@ -895,7 +733,7 @@ a.kpi:hover { max-height: calc(100vh - 7rem); overflow: auto; border-radius: 0.5rem; - box-shadow: 0 0.75rem 2rem var(--shadow-drawer); + box-shadow: 0 0.75rem 2rem rgba(31, 41, 55, 0.22); } .project-device-drawer-header { @@ -906,14 +744,13 @@ a.kpi:hover { } .project-device-drawer-header span { - color: var(--text-faint); + color: #6b7280; font-size: 0.75rem; } .project-device-drawer-header button { - border: 1px solid var(--panel-border-strong); - background: var(--panel-bg); - color: var(--text-strong); + border: 1px solid #c4cddc; + background: #fff; border-radius: 4px; padding: 0.25rem 0.4rem; font-size: 0.78rem; @@ -925,24 +762,23 @@ a.kpi:hover { } .tree-grid .sort-indicator { - color: var(--accent-blue); + color: #2563eb; font-size: 0.85rem; } .tree-grid .header-filter-btn.active { - border-color: var(--accent-blue); - color: var(--accent-blue); - background: var(--accent-blue-soft-bg); + border-color: #2563eb; + color: #2563eb; + background: #eff6ff; } .tree-grid .header-filter-menu { position: absolute; z-index: 7; margin-top: 0.2rem; - border: 1px solid var(--panel-border); - background: var(--panel-bg); - color: var(--text-strong); - box-shadow: 0 6px 16px var(--shadow-menu); + border: 1px solid #cfd7e5; + background: #fff; + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12); width: 300px; padding: 0.55rem; text-align: left; @@ -966,7 +802,7 @@ a.kpi:hover { .tree-grid .header-filter-close { border: 0; background: transparent; - color: var(--text-muted); + color: #4b5563; padding: 0.05rem 0.2rem; font-size: 1rem; line-height: 1; @@ -981,14 +817,13 @@ a.kpi:hover { .tree-grid .header-filter-selection-actions span { margin-left: auto; - color: var(--text-muted); + color: #4b5563; } .tree-grid .header-filter-selection-actions button, .tree-grid .header-filter-footer button { - border: 1px solid var(--panel-border-strong); - background: var(--panel-bg); - color: var(--text-strong); + border: 1px solid #c4cddc; + background: #fff; border-radius: 3px; padding: 0.18rem 0.35rem; font-size: 0.72rem; @@ -997,7 +832,7 @@ a.kpi:hover { .tree-grid .header-filter-explanation { margin-bottom: 0.4rem; - color: var(--text-muted); + color: #4b5563; font-size: 0.72rem; line-height: 1.3; white-space: normal; @@ -1005,11 +840,9 @@ a.kpi:hover { .tree-grid .header-filter-search { width: 100%; - border: 1px solid var(--panel-border-strong); + border: 1px solid #c4cddc; border-radius: 4px; padding: 0.3rem 0.4rem; - background: var(--panel-bg); - color: var(--text-strong); font-size: 0.76rem; } @@ -1019,7 +852,7 @@ a.kpi:hover { gap: 0.15rem; max-height: 210px; overflow: auto; - border: 1px solid var(--panel-border); + border: 1px solid #e1e6ef; border-radius: 4px; padding: 0.25rem; } @@ -1033,7 +866,7 @@ a.kpi:hover { border-radius: 3px; background: transparent; padding: 0.25rem 0.3rem; - color: var(--text-strong); + color: #1f2937; font-weight: 400; font-size: 0.75rem; text-align: left; @@ -1041,17 +874,17 @@ a.kpi:hover { } .tree-grid .header-filter-item:hover { - background: var(--surface-hover); + background: #f3f4f6; } .tree-grid .header-filter-item.selected { - border-color: var(--accent-blue-border); - background: var(--accent-blue-soft-bg); + border-color: #bfdbfe; + background: #eff6ff; } .tree-grid .header-filter-empty { padding: 0.45rem 0.25rem; - color: var(--text-faint); + color: #6b7280; font-size: 0.74rem; } @@ -1064,8 +897,8 @@ a.kpi:hover { } .tree-grid .header-filter-footer button.primary { - border-color: var(--accent-blue); - background: var(--accent-blue); + border-color: #2563eb; + background: #2563eb; color: #fff; } @@ -1075,7 +908,7 @@ a.kpi:hover { } .tree-grid .header-filter-warning { - color: var(--grid-warn-strong); + color: #9a3412; font-size: 0.7rem; white-space: normal; } @@ -1085,25 +918,25 @@ a.kpi:hover { } .tree-grid .section-row td { - background: var(--surface-header-row); + background: #e8eef8; font-weight: 600; } .tree-grid .structure-component-row td { padding: 0.38rem 0.6rem; - border-bottom-color: var(--panel-border); + border-bottom-color: #d8dee9; } .tree-grid .structure-component-row.headerComponent td { - background: var(--surface-component-header); + background: #e2e8f0; } .tree-grid .structure-component-row.groupComponent td { - background: var(--surface-component-group); + background: #f8fafc; } .tree-grid .structure-component-row.footerComponent td { - background: var(--surface-component-footer); + background: #f1f5f9; } .tree-grid .structure-component-content { @@ -1119,7 +952,7 @@ a.kpi:hover { } .tree-grid .structure-component-protection { - color: var(--text-subtle); + color: #475569; font-size: 0.78rem; } @@ -1136,7 +969,7 @@ a.kpi:hover { } .tree-grid .structure-component-fixed { - color: var(--text-faint); + color: #64748b; font-size: 0.75rem; } @@ -1157,33 +990,32 @@ a.kpi:hover { } .tree-grid .section-actions button { - border: 1px solid var(--panel-border-strong); - background: var(--panel-bg); - color: var(--text-strong); + border: 1px solid #c4cddc; + background: #fff; padding: 0.2rem 0.45rem; border-radius: 3px; font-size: 0.78rem; } .tree-grid .summary-row td { - background: var(--panel-bg-subtle); + background: #f3f7fd; } .tree-grid .device-row td:first-child { - color: var(--text-faint); + color: #6b7280; } .tree-grid .empty-circuit-row td { - background: var(--panel-bg-subtle); + background: #f8fbff; } .tree-grid tr.row-selected td { - background: var(--surface-selected); + background: #eaf1ff; } .tree-grid .placeholder-row td { - background: var(--panel-bg-subtle); - color: var(--text-faint); + background: #f7f7f7; + color: #6b7280; font-style: italic; } @@ -1200,12 +1032,8 @@ a.kpi:hover { box-shadow 0.12s ease; } -:root[data-bs-theme="dark"] .tree-grid .cell-protection-trigger { - color: var(--color-accent-pale); -} - .tree-grid .cell-protection-trigger:hover { - background: rgba(63, 166, 107, 0.15); + background: #eaf6f0; box-shadow: inset 0 0 0 1px var(--color-signal); } @@ -1226,7 +1054,7 @@ a.kpi:hover { } .tree-grid .cell-selected { - outline: 2px solid var(--accent-blue-strong-border); + outline: 2px solid #4c7dd9; outline-offset: -2px; } @@ -1237,30 +1065,28 @@ a.kpi:hover { } .tree-grid .section-title span { - color: var(--text-subtle); + color: #475569; font-size: 0.78rem; font-weight: 500; } .tree-grid .cell-invalid { - outline: 2px solid var(--grid-danger); + outline: 2px solid #c2410c; outline-offset: -2px; - background: var(--grid-warn-bg) !important; + background: #fff7ed !important; } .tree-grid .cell-invalid input { - border-color: var(--grid-danger); + border-color: #c2410c; } .tree-grid input, .tree-grid select { width: 100%; min-width: 5rem; - border: 1px solid var(--input-border); + border: 1px solid #9fb6e0; border-radius: 2px; padding: 0.2rem 0.3rem; - background: var(--panel-bg); - color: var(--text-strong); } .tree-grid .action-cell { @@ -1269,27 +1095,26 @@ a.kpi:hover { } .tree-grid .action-cell button { - border: 1px solid var(--panel-border-strong); - background: var(--panel-bg); - color: var(--text-strong); + border: 1px solid #c4cddc; + background: #fff; padding: 0.2rem 0.45rem; border-radius: 3px; font-size: 0.78rem; } .tree-grid .drop-target-active { - box-shadow: inset 0 0 0 2px var(--accent-blue-strong-border); - background: var(--accent-blue-soft-bg) !important; + box-shadow: inset 0 0 0 2px #4c7dd9; + background: #eef4ff !important; } .tree-grid .drop-target-invalid { - box-shadow: inset 0 0 0 2px var(--grid-warn); - background: var(--grid-warn-bg) !important; + box-shadow: inset 0 0 0 2px #d97706; + background: #fff7ed !important; } .tree-grid .drop-target-confirm { - box-shadow: inset 0 0 0 2px var(--grid-warn); - background: var(--grid-warn-bg) !important; + box-shadow: inset 0 0 0 2px #d97706; + background: #fffbeb !important; } .tree-grid tr.circuit-insert-before td, @@ -1303,7 +1128,7 @@ a.kpi:hover { left: -1px; right: -1px; top: -2px; - border-top: 4px solid var(--accent-blue); + border-top: 4px solid #2563eb; pointer-events: none; } @@ -1316,7 +1141,7 @@ a.kpi:hover { height: 0; border-top: 7px solid transparent; border-bottom: 7px solid transparent; - border-left: 10px solid var(--accent-blue); + border-left: 10px solid #2563eb; pointer-events: none; } @@ -1326,7 +1151,7 @@ a.kpi:hover { left: -1px; right: -1px; bottom: -2px; - border-bottom: 4px solid var(--accent-blue); + border-bottom: 4px solid #2563eb; pointer-events: none; } @@ -1339,13 +1164,13 @@ a.kpi:hover { height: 0; border-top: 7px solid transparent; border-bottom: 7px solid transparent; - border-left: 10px solid var(--accent-blue); + border-left: 10px solid #2563eb; pointer-events: none; } .drop-hint { font-size: 0.75rem; - color: var(--accent-blue-marker); + color: #1f4ea3; font-weight: 600; } @@ -1353,22 +1178,21 @@ a.kpi:hover { padding: 0.5rem 0.75rem; border-radius: 4px; border: 1px solid transparent; - color: var(--text-strong); } .notice.info { - background: var(--notice-info-bg); - border-color: var(--notice-info-border); + background: #ebf3ff; + border-color: #bad1f7; } .notice.error { - background: var(--notice-error-bg); - border-color: var(--notice-error-border); + background: #fdecec; + border-color: #f5b5b5; } .notice.warning { - background: var(--notice-warning-bg); - border-color: var(--notice-warning-border); + background: #fff7ed; + border-color: #fdba74; } .editor-error-notice { @@ -1379,13 +1203,12 @@ a.kpi:hover { } .notice.muted { - background: var(--notice-muted-bg); - border-color: var(--notice-muted-border); - color: var(--text-muted); + background: #f6f6f6; + border-color: #e4e4e4; } .todo-hint { - color: var(--text-faint); + color: #6b7280; font-size: 0.8rem; margin: 0; } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index cd2407b..01df63e 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,5 +1,4 @@ import type { Metadata } from "next"; -import Script from "next/script"; import "bootstrap/dist/css/bootstrap.min.css"; import "./globals.css"; import { AppShell } from "../frontend/components/AppShell"; @@ -9,33 +8,10 @@ export const metadata: Metadata = { description: "Leistungsbilanz für elektrische Verbraucher und Stromkreislisten", }; -// Keep this key literal in sync with THEME_STORAGE_KEY in ThemeToggle.tsx. -// It must stay inline (not imported) so it runs before hydration and never -// flashes the wrong theme on load. -const THEME_INIT_SCRIPT = ` -(function () { - try { - var stored = localStorage.getItem("leistungsbilanz:theme"); - var theme = - stored === "dark" || stored === "light" - ? stored - : window.matchMedia("(prefers-color-scheme: dark)").matches - ? "dark" - : "light"; - document.documentElement.setAttribute("data-bs-theme", theme); - } catch (error) { - // Storage/matchMedia unavailable: fall back to the default light theme. - } -})(); -`; - export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { return ( - + - {children} diff --git a/src/app/projects/[projectId]/page.tsx b/src/app/projects/[projectId]/page.tsx index fa342ea..c675340 100644 --- a/src/app/projects/[projectId]/page.tsx +++ b/src/app/projects/[projectId]/page.tsx @@ -17,13 +17,13 @@ import { deleteDistributionBoard, disconnectProjectDeviceRows, exportProjectTransfer, - getProject, getProjectDeviceSyncPreview, listCircuitLists, listDistributionBoards, listFloors, listGlobalDevices, listProjectDevices, + listProjects, listRooms, importProjectTransfer, synchronizeProjectDeviceRows, @@ -129,7 +129,7 @@ export default function ProjectDetailPage() { return; } Promise.all([ - getProject(projectId), + listProjects(), listDistributionBoards(projectId), listCircuitLists(projectId), listFloors(projectId), @@ -138,7 +138,7 @@ export default function ProjectDetailPage() { listGlobalDevices(), ]) .then(([ - currentProject, + projects, distributionBoards, loadedCircuitLists, loadedFloors, @@ -146,6 +146,7 @@ export default function ProjectDetailPage() { loadedProjectDevices, loadedGlobalDevices, ]) => { + const currentProject = projects.find((item) => item.id === projectId) ?? null; setProject(currentProject); setBoards(distributionBoards); setCircuitLists(loadedCircuitLists); diff --git a/src/app/web-health/route.ts b/src/app/web-health/route.ts deleted file mode 100644 index ce6f2c4..0000000 --- a/src/app/web-health/route.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NextResponse } from "next/server"; - -// Liveness probe for the web container itself. The "/health" path is -// rewritten to the API in next.config.mjs, so it cannot answer for this -// process. Kept as a route handler so a probe does not render a page. -export const dynamic = "force-dynamic"; - -export function GET() { - return NextResponse.json({ ok: true }); -} diff --git a/src/db/migrations/0006_damp_skrulls.sql b/src/db/migrations/0006_damp_skrulls.sql deleted file mode 100644 index 3f6badf..0000000 --- a/src/db/migrations/0006_damp_skrulls.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE INDEX `circuit_device_rows_circuit_id_idx` ON `circuit_device_rows` (`circuit_id`);--> statement-breakpoint -CREATE INDEX `circuits_section_id_idx` ON `circuits` (`section_id`); \ No newline at end of file diff --git a/src/db/migrations/meta/0006_snapshot.json b/src/db/migrations/meta/0006_snapshot.json deleted file mode 100644 index 84f5229..0000000 --- a/src/db/migrations/meta/0006_snapshot.json +++ /dev/null @@ -1,2419 +0,0 @@ -{ - "version": "6", - "dialect": "sqlite", - "id": "b25e1a40-c400-4848-bc24-a04bda7a8209", - "prevId": "b79934fe-5d05-49da-b7c4-95c954e5fec2", - "tables": { - "circuit_device_rows": { - "name": "circuit_device_rows", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "level": { - "name": "level", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number_snapshot": { - "name": "room_number_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_name_snapshot": { - "name": "room_name_snapshot", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "manual_quantity": { - "name": "manual_quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuit_device_rows_circuit_id_idx": { - "name": "circuit_device_rows_circuit_id_idx", - "columns": [ - "circuit_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "circuit_device_rows_circuit_id_circuits_id_fk": { - "name": "circuit_device_rows_circuit_id_circuits_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_device_rows_linked_project_device_id_project_devices_id_fk": { - "name": "circuit_device_rows_linked_project_device_id_project_devices_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "project_devices", - "columnsFrom": [ - "linked_project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "circuit_device_rows_room_id_rooms_id_fk": { - "name": "circuit_device_rows_room_id_rooms_id_fk", - "tableFrom": "circuit_device_rows", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_list_equipment_identifiers": { - "name": "circuit_list_equipment_identifiers", - "columns": { - "owner_type": { - "name": "owner_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "owner_id": { - "name": "owner_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_list_equipment_identifiers_owner_unique": { - "name": "circuit_list_equipment_identifiers_owner_unique", - "columns": [ - "owner_type", - "owner_id" - ], - "isUnique": true - }, - "circuit_list_equipment_identifiers_list_identifier_unique": { - "name": "circuit_list_equipment_identifiers_list_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_list_equipment_identifiers_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_list_equipment_identifiers", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_lists": { - "name": "circuit_lists", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "circuit_lists_distribution_board_id_unique": { - "name": "circuit_lists_distribution_board_id_unique", - "columns": [ - "distribution_board_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_lists_project_id_projects_id_fk": { - "name": "circuit_lists_project_id_projects_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuit_lists_distribution_board_id_distribution_boards_id_fk": { - "name": "circuit_lists_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "circuit_lists", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_protection_devices": { - "name": "circuit_protection_devices", - "columns": { - "circuit_id": { - "name": "circuit_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "rated_current_a": { - "name": "rated_current_a", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "fuse_utilization_category": { - "name": "fuse_utilization_category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trip_characteristic": { - "name": "trip_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_type": { - "name": "rcd_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rated_residual_current_ma": { - "name": "rated_residual_current_ma", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "circuit_protection_devices_circuit_id_circuits_id_fk": { - "name": "circuit_protection_devices_circuit_id_circuits_id_fk", - "tableFrom": "circuit_protection_devices", - "tableTo": "circuits", - "columnsFrom": [ - "circuit_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuit_sections": { - "name": "circuit_sections", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "key": { - "name": "key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "prefix": { - "name": "prefix", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "group_number": { - "name": "group_number", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuit_sections_list_key_unique": { - "name": "circuit_sections_list_key_unique", - "columns": [ - "circuit_list_id", - "key" - ], - "isUnique": true - }, - "circuit_sections_list_prefix_unique": { - "name": "circuit_sections_list_prefix_unique", - "columns": [ - "circuit_list_id", - "prefix" - ], - "isUnique": true - }, - "circuit_sections_list_category_group_unique": { - "name": "circuit_sections_list_category_group_unique", - "columns": [ - "circuit_list_id", - "category", - "group_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuit_sections_circuit_list_id_circuit_lists_id_fk": { - "name": "circuit_sections_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuit_sections", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "circuits": { - "name": "circuits", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "cable_type": { - "name": "cable_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_cross_section": { - "name": "cable_cross_section", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cable_length": { - "name": "cable_length", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_assignment": { - "name": "rcd_assignment", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "terminal_designation": { - "name": "terminal_designation", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage": { - "name": "voltage", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "control_requirement": { - "name": "control_requirement", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "status": { - "name": "status", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_reserve": { - "name": "is_reserve", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "circuits_section_id_idx": { - "name": "circuits_section_id_idx", - "columns": [ - "section_id" - ], - "isUnique": false - }, - "circuits_list_equipment_identifier_unique": { - "name": "circuits_list_equipment_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - } - }, - "foreignKeys": { - "circuits_circuit_list_id_circuit_lists_id_fk": { - "name": "circuits_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "circuits_section_id_circuit_sections_id_fk": { - "name": "circuits_section_id_circuit_sections_id_fk", - "tableFrom": "circuits", - "tableTo": "circuit_sections", - "columnsFrom": [ - "section_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_board_component_protection_devices": { - "name": "distribution_board_component_protection_devices", - "columns": { - "component_id": { - "name": "component_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "rated_current_a": { - "name": "rated_current_a", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "fuse_utilization_category": { - "name": "fuse_utilization_category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "trip_characteristic": { - "name": "trip_characteristic", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rcd_type": { - "name": "rcd_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "rated_residual_current_ma": { - "name": "rated_residual_current_ma", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk": { - "name": "distribution_board_component_protection_devices_component_id_distribution_board_components_id_fk", - "tableFrom": "distribution_board_component_protection_devices", - "tableTo": "distribution_board_components", - "columnsFrom": [ - "component_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_board_components": { - "name": "distribution_board_components", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "circuit_list_id": { - "name": "circuit_list_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "section_id": { - "name": "section_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "equipment_identifier": { - "name": "equipment_identifier", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "role": { - "name": "role", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "placement": { - "name": "placement", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": { - "distribution_board_components_list_identifier_unique": { - "name": "distribution_board_components_list_identifier_unique", - "columns": [ - "circuit_list_id", - "equipment_identifier" - ], - "isUnique": true - }, - "distribution_board_components_section_role_unique": { - "name": "distribution_board_components_section_role_unique", - "columns": [ - "section_id", - "role" - ], - "isUnique": true - } - }, - "foreignKeys": { - "distribution_board_components_circuit_list_id_circuit_lists_id_fk": { - "name": "distribution_board_components_circuit_list_id_circuit_lists_id_fk", - "tableFrom": "distribution_board_components", - "tableTo": "circuit_lists", - "columnsFrom": [ - "circuit_list_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "distribution_board_components_section_id_circuit_sections_id_fk": { - "name": "distribution_board_components_section_id_circuit_sections_id_fk", - "tableFrom": "distribution_board_components", - "tableTo": "circuit_sections", - "columnsFrom": [ - "section_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "distribution_boards": { - "name": "distribution_boards", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "supply_type": { - "name": "supply_type", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - } - }, - "indexes": {}, - "foreignKeys": { - "distribution_boards_project_id_projects_id_fk": { - "name": "distribution_boards_project_id_projects_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "distribution_boards_floor_id_floors_id_fk": { - "name": "distribution_boards_floor_id_floors_id_fk", - "tableFrom": "distribution_boards", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "external_csv_configurations": { - "name": "external_csv_configurations", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "configuration_version": { - "name": "configuration_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "configuration": { - "name": "configuration", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "external_csv_configurations_project_id_unique": { - "name": "external_csv_configurations_project_id_unique", - "columns": [ - "project_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "external_csv_configurations_project_id_projects_id_fk": { - "name": "external_csv_configurations_project_id_projects_id_fk", - "tableFrom": "external_csv_configurations", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "external_import_batches": { - "name": "external_import_batches", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_id": { - "name": "source_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "import_kind": { - "name": "import_kind", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "imported_at_iso": { - "name": "imported_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "file_name": { - "name": "file_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sha256": { - "name": "sha256", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "applied_project_revision": { - "name": "applied_project_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "configuration_version": { - "name": "configuration_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "configuration_snapshot": { - "name": "configuration_snapshot", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "original_bytes": { - "name": "original_bytes", - "type": "blob", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "document": { - "name": "document", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "external_import_batches_project_revision_idx": { - "name": "external_import_batches_project_revision_idx", - "columns": [ - "project_id", - "applied_project_revision" - ], - "isUnique": false - }, - "external_import_batches_source_imported_idx": { - "name": "external_import_batches_source_imported_idx", - "columns": [ - "source_id", - "imported_at_iso" - ], - "isUnique": false - } - }, - "foreignKeys": { - "external_import_batches_project_id_projects_id_fk": { - "name": "external_import_batches_project_id_projects_id_fk", - "tableFrom": "external_import_batches", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "external_import_batches_source_id_external_model_sources_id_fk": { - "name": "external_import_batches_source_id_external_model_sources_id_fk", - "tableFrom": "external_import_batches", - "tableTo": "external_model_sources", - "columnsFrom": [ - "source_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "external_model_objects": { - "name": "external_model_objects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_id": { - "name": "source_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "ifc_guid": { - "name": "ifc_guid", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "last_seen_import_batch_id": { - "name": "last_seen_import_batch_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "last_accepted_import_batch_id": { - "name": "last_accepted_import_batch_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "accepted_source_values": { - "name": "accepted_source_values", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "planning_values": { - "name": "planning_values", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "overridden_fields": { - "name": "overridden_fields", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "external_room_mapping_id": { - "name": "external_room_mapping_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "distribution_board_id": { - "name": "distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "linked_project_device_id": { - "name": "linked_project_device_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "circuit_device_row_id": { - "name": "circuit_device_row_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "presence_status": { - "name": "presence_status", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'present'" - } - }, - "indexes": { - "external_model_objects_source_ifc_guid_unique": { - "name": "external_model_objects_source_ifc_guid_unique", - "columns": [ - "source_id", - "ifc_guid" - ], - "isUnique": true - }, - "external_model_objects_project_presence_idx": { - "name": "external_model_objects_project_presence_idx", - "columns": [ - "project_id", - "presence_status" - ], - "isUnique": false - }, - "external_model_objects_distribution_board_idx": { - "name": "external_model_objects_distribution_board_idx", - "columns": [ - "distribution_board_id" - ], - "isUnique": false - }, - "external_model_objects_circuit_device_row_idx": { - "name": "external_model_objects_circuit_device_row_idx", - "columns": [ - "circuit_device_row_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "external_model_objects_project_id_projects_id_fk": { - "name": "external_model_objects_project_id_projects_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "external_model_objects_source_id_external_model_sources_id_fk": { - "name": "external_model_objects_source_id_external_model_sources_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "external_model_sources", - "columnsFrom": [ - "source_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk": { - "name": "external_model_objects_last_seen_import_batch_id_external_import_batches_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "external_import_batches", - "columnsFrom": [ - "last_seen_import_batch_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk": { - "name": "external_model_objects_last_accepted_import_batch_id_external_import_batches_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "external_import_batches", - "columnsFrom": [ - "last_accepted_import_batch_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "restrict", - "onUpdate": "no action" - }, - "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk": { - "name": "external_model_objects_external_room_mapping_id_external_room_mappings_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "external_room_mappings", - "columnsFrom": [ - "external_room_mapping_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "external_model_objects_distribution_board_id_distribution_boards_id_fk": { - "name": "external_model_objects_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "distribution_boards", - "columnsFrom": [ - "distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "external_model_objects_linked_project_device_id_project_devices_id_fk": { - "name": "external_model_objects_linked_project_device_id_project_devices_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "project_devices", - "columnsFrom": [ - "linked_project_device_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk": { - "name": "external_model_objects_circuit_device_row_id_circuit_device_rows_id_fk", - "tableFrom": "external_model_objects", - "tableTo": "circuit_device_rows", - "columnsFrom": [ - "circuit_device_row_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "external_model_sources": { - "name": "external_model_sources", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_type": { - "name": "source_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'revit_csv'" - } - }, - "indexes": { - "external_model_sources_project_type_unique": { - "name": "external_model_sources_project_type_unique", - "columns": [ - "project_id", - "source_type" - ], - "isUnique": true - } - }, - "foreignKeys": { - "external_model_sources_project_id_projects_id_fk": { - "name": "external_model_sources_project_id_projects_id_fk", - "tableFrom": "external_model_sources", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "external_room_mappings": { - "name": "external_room_mappings", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_id": { - "name": "source_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "normalized_source_room_key": { - "name": "normalized_source_room_key", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_floor_name": { - "name": "source_floor_name", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source_room_number": { - "name": "source_room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_room_name": { - "name": "source_room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_id": { - "name": "room_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "default_distribution_board_id": { - "name": "default_distribution_board_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "external_room_mappings_source_key_unique": { - "name": "external_room_mappings_source_key_unique", - "columns": [ - "source_id", - "normalized_source_room_key" - ], - "isUnique": true - }, - "external_room_mappings_project_room_idx": { - "name": "external_room_mappings_project_room_idx", - "columns": [ - "project_id", - "room_id" - ], - "isUnique": false - } - }, - "foreignKeys": { - "external_room_mappings_project_id_projects_id_fk": { - "name": "external_room_mappings_project_id_projects_id_fk", - "tableFrom": "external_room_mappings", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "external_room_mappings_source_id_external_model_sources_id_fk": { - "name": "external_room_mappings_source_id_external_model_sources_id_fk", - "tableFrom": "external_room_mappings", - "tableTo": "external_model_sources", - "columnsFrom": [ - "source_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "external_room_mappings_room_id_rooms_id_fk": { - "name": "external_room_mappings_room_id_rooms_id_fk", - "tableFrom": "external_room_mappings", - "tableTo": "rooms", - "columnsFrom": [ - "room_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - }, - "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk": { - "name": "external_room_mappings_default_distribution_board_id_distribution_boards_id_fk", - "tableFrom": "external_room_mappings", - "tableTo": "distribution_boards", - "columnsFrom": [ - "default_distribution_board_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "floors": { - "name": "floors", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": { - "floors_project_id_projects_id_fk": { - "name": "floors_project_id_projects_id_fk", - "tableFrom": "floors", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "global_devices": { - "name": "global_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "installed_power_per_unit_kw": { - "name": "installed_power_per_unit_kw", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "demand_factor": { - "name": "demand_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "phase_count": { - "name": "phase_count", - "type": "integer", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "power_factor": { - "name": "power_factor", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "note": { - "name": "note", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_change_sets": { - "name": "project_change_sets", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_revision_id": { - "name": "project_revision_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "command_type": { - "name": "command_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_schema_version": { - "name": "payload_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "forward_payload_json": { - "name": "forward_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "inverse_payload_json": { - "name": "inverse_payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_change_sets_revision_unique": { - "name": "project_change_sets_revision_unique", - "columns": [ - "project_revision_id" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_change_sets_project_revision_id_project_revisions_id_fk": { - "name": "project_change_sets_project_revision_id_project_revisions_id_fk", - "tableFrom": "project_change_sets", - "tableTo": "project_revisions", - "columnsFrom": [ - "project_revision_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_devices": { - "name": "project_devices", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "display_name": { - "name": "display_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "phase_type": { - "name": "phase_type", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'single_phase'" - }, - "connection_kind": { - "name": "connection_kind", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "cost_group": { - "name": "cost_group", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "category": { - "name": "category", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "quantity": { - "name": "quantity", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "power_per_unit": { - "name": "power_per_unit", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - }, - "simultaneity_factor": { - "name": "simultaneity_factor", - "type": "real", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 1 - }, - "cos_phi": { - "name": "cos_phi", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "remark": { - "name": "remark", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "voltage_v": { - "name": "voltage_v", - "type": "real", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "project_devices_project_id_projects_id_fk": { - "name": "project_devices_project_id_projects_id_fk", - "tableFrom": "project_devices", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_history_stack_entries": { - "name": "project_history_stack_entries", - "columns": { - "change_set_id": { - "name": "change_set_id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "stack": { - "name": "stack", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "position": { - "name": "position", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": { - "project_history_stack_entries_position_unique": { - "name": "project_history_stack_entries_position_unique", - "columns": [ - "project_id", - "stack", - "position" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_history_stack_entries_change_set_id_project_change_sets_id_fk": { - "name": "project_history_stack_entries_change_set_id_project_change_sets_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "project_change_sets", - "columnsFrom": [ - "change_set_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "project_history_stack_entries_project_id_projects_id_fk": { - "name": "project_history_stack_entries_project_id_projects_id_fk", - "tableFrom": "project_history_stack_entries", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": { - "project_history_stack_entries_stack_check": { - "name": "project_history_stack_entries_stack_check", - "value": "\"project_history_stack_entries\".\"stack\" in ('undo', 'redo')" - } - } - }, - "project_revisions": { - "name": "project_revisions", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "revision_number": { - "name": "revision_number", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "actor_id": { - "name": "actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "source": { - "name": "source", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_revisions_project_number_unique": { - "name": "project_revisions_project_number_unique", - "columns": [ - "project_id", - "revision_number" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_revisions_project_id_projects_id_fk": { - "name": "project_revisions_project_id_projects_id_fk", - "tableFrom": "project_revisions", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "project_snapshots": { - "name": "project_snapshots", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "source_revision": { - "name": "source_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "schema_version": { - "name": "schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "kind": { - "name": "kind", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'named'" - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "payload_json": { - "name": "payload_json", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "payload_sha256": { - "name": "payload_sha256", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at_iso": { - "name": "created_at_iso", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_by_actor_id": { - "name": "created_by_actor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - } - }, - "indexes": { - "project_snapshots_project_created_idx": { - "name": "project_snapshots_project_created_idx", - "columns": [ - "project_id", - "created_at_iso" - ], - "isUnique": false - }, - "project_snapshots_project_kind_revision_idx": { - "name": "project_snapshots_project_kind_revision_idx", - "columns": [ - "project_id", - "kind", - "source_revision" - ], - "isUnique": false - }, - "project_snapshots_project_name_unique": { - "name": "project_snapshots_project_name_unique", - "columns": [ - "project_id", - "name" - ], - "isUnique": true - } - }, - "foreignKeys": { - "project_snapshots_project_id_projects_id_fk": { - "name": "project_snapshots_project_id_projects_id_fk", - "tableFrom": "project_snapshots", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "projects": { - "name": "projects", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "internal_project_number": { - "name": "internal_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "external_project_number": { - "name": "external_project_number", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "building_owner": { - "name": "building_owner", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "is_public_building": { - "name": "is_public_building", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": false - }, - "single_phase_voltage_v": { - "name": "single_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 230 - }, - "three_phase_voltage_v": { - "name": "three_phase_voltage_v", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 400 - }, - "enabled_distribution_board_supply_types": { - "name": "enabled_distribution_board_supply_types", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": "'[\"AV\",\"SV\",\"EV\",\"USV\",\"MSR\",\"SiBe\"]'" - }, - "current_revision": { - "name": "current_revision", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false, - "default": 0 - } - }, - "indexes": {}, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - }, - "rooms": { - "name": "rooms", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true, - "autoincrement": false - }, - "project_id": { - "name": "project_id", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "floor_id": { - "name": "floor_id", - "type": "text", - "primaryKey": false, - "notNull": false, - "autoincrement": false - }, - "room_number": { - "name": "room_number", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "room_name": { - "name": "room_name", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - } - }, - "indexes": {}, - "foreignKeys": { - "rooms_project_id_projects_id_fk": { - "name": "rooms_project_id_projects_id_fk", - "tableFrom": "rooms", - "tableTo": "projects", - "columnsFrom": [ - "project_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "rooms_floor_id_floors_id_fk": { - "name": "rooms_floor_id_floors_id_fk", - "tableFrom": "rooms", - "tableTo": "floors", - "columnsFrom": [ - "floor_id" - ], - "columnsTo": [ - "id" - ], - "onDelete": "set null", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "checkConstraints": {} - } - }, - "views": {}, - "enums": {}, - "_meta": { - "schemas": {}, - "tables": {}, - "columns": {} - }, - "internal": { - "indexes": {} - } -} \ No newline at end of file diff --git a/src/db/migrations/meta/_journal.json b/src/db/migrations/meta/_journal.json index eedfc7a..5a65548 100644 --- a/src/db/migrations/meta/_journal.json +++ b/src/db/migrations/meta/_journal.json @@ -43,13 +43,6 @@ "when": 1785687503453, "tag": "0005_stale_gorilla_man", "breakpoints": true - }, - { - "idx": 6, - "version": "6", - "when": 1786043080323, - "tag": "0006_damp_skrulls", - "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/repositories/circuit-device-row.persistence.ts b/src/db/repositories/circuit-device-row.persistence.ts index 4a32623..c075f61 100644 --- a/src/db/repositories/circuit-device-row.persistence.ts +++ b/src/db/repositories/circuit-device-row.persistence.ts @@ -43,6 +43,12 @@ export interface CircuitDeviceRowPatchInput { overriddenFields?: string | null; } +export interface CircuitDeviceRowCreateInput extends CircuitDeviceRowUpdateInput { + circuitId: string; + linkedProjectDeviceId?: string; + sortOrder: number; +} + export function toCircuitDeviceRowUpdateValues(input: CircuitDeviceRowUpdateInput) { return { linkedProjectDeviceId: input.linkedProjectDeviceId ?? null, @@ -99,3 +105,15 @@ export function toCircuitDeviceRowPatchValues(input: CircuitDeviceRowPatchInput) return values; } + +export function toCircuitDeviceRowCreateValues( + id: string, + input: CircuitDeviceRowCreateInput +) { + return { + id, + circuitId: input.circuitId, + sortOrder: input.sortOrder, + ...toCircuitDeviceRowUpdateValues(input), + }; +} diff --git a/src/db/repositories/circuit-project-command.repository.ts b/src/db/repositories/circuit-project-command.repository.ts index bbd6543..6f04ffb 100644 --- a/src/db/repositories/circuit-project-command.repository.ts +++ b/src/db/repositories/circuit-project-command.repository.ts @@ -1,4 +1,4 @@ -import { and, eq } from "drizzle-orm"; +import { and, eq, ne } from "drizzle-orm"; import { assertCircuitUpdateProjectCommand, createCircuitUpdateProjectCommand, @@ -21,7 +21,6 @@ import { import { executeProjectCommandTransactionWithAppliedForward } from "./project-command-transaction.persistence.js"; import { resolveCircuitVoltage } from "./project-voltage.persistence.js"; import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; -import { assertEquipmentIdentifierAvailable } from "./equipment-identifier-uniqueness.persistence.js"; type CircuitRow = typeof circuits.$inferSelect; @@ -167,12 +166,20 @@ export class CircuitProjectCommandRepository ) { return; } - assertEquipmentIdentifierAvailable( - database, - circuit.circuitListId, - equipmentIdentifier, - circuit.id - ); + const duplicate = database + .select({ id: circuits.id }) + .from(circuits) + .where( + and( + eq(circuits.circuitListId, circuit.circuitListId), + eq(circuits.equipmentIdentifier, equipmentIdentifier), + ne(circuits.id, circuit.id) + ) + ) + .get(); + if (duplicate) { + throw new Error("Duplicate equipmentIdentifier in circuit list."); + } } } diff --git a/src/db/repositories/circuit-structure-project-command.repository.ts b/src/db/repositories/circuit-structure-project-command.repository.ts index 9b8941d..454028e 100644 --- a/src/db/repositories/circuit-structure-project-command.repository.ts +++ b/src/db/repositories/circuit-structure-project-command.repository.ts @@ -27,7 +27,6 @@ import { } from "./circuit-device-row-structure.persistence.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; import { resolveCircuitVoltage } from "./project-voltage.persistence.js"; -import { assertEquipmentIdentifierAvailable } from "./equipment-identifier-uniqueness.persistence.js"; export class CircuitStructureProjectCommandRepository implements CircuitStructureProjectCommandStore @@ -104,11 +103,24 @@ export class CircuitStructureProjectCommandRepository if (existingCircuit) { throw new Error("Circuit id already exists."); } - assertEquipmentIdentifierAvailable( - database, - snapshot.circuitListId, - snapshot.equipmentIdentifier - ); + const duplicateIdentifier = database + .select({ id: circuits.id }) + .from(circuits) + .where( + and( + eq(circuits.circuitListId, snapshot.circuitListId), + eq( + circuits.equipmentIdentifier, + snapshot.equipmentIdentifier + ) + ) + ) + .get(); + if (duplicateIdentifier) { + throw new Error( + "Duplicate equipmentIdentifier in circuit list." + ); + } if (snapshot.deviceRows.length > 0) { const rowIds = snapshot.deviceRows.map((row) => row.id); diff --git a/src/db/repositories/distribution-board-component-structure-project-command.repository.ts b/src/db/repositories/distribution-board-component-structure-project-command.repository.ts index 3ea4291..2dc3bba 100644 --- a/src/db/repositories/distribution-board-component-structure-project-command.repository.ts +++ b/src/db/repositories/distribution-board-component-structure-project-command.repository.ts @@ -22,7 +22,6 @@ import { circuitSections } from "../schema/circuit-sections.js"; import { distributionBoardComponentProtectionDevices } from "../schema/distribution-board-component-protection-devices.js"; import { distributionBoardComponents } from "../schema/distribution-board-components.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; -import { assertEquipmentIdentifierAvailable } from "./equipment-identifier-uniqueness.persistence.js"; export class DistributionBoardComponentStructureProjectCommandRepository implements DistributionBoardComponentStructureProjectCommandStore @@ -95,11 +94,6 @@ export class DistributionBoardComponentStructureProjectCommandRepository if (existing) { throw new Error("Distribution-board component id already exists."); } - assertEquipmentIdentifierAvailable( - database, - snapshot.component.circuitListId, - snapshot.component.equipmentIdentifier - ); database .insert(distributionBoardComponents) .values(snapshot.component) @@ -193,17 +187,6 @@ export class DistributionBoardComponentStructureProjectCommandRepository "Distribution-board component changed before update." ); } - if ( - target.component.equipmentIdentifier !== - expected.component.equipmentIdentifier - ) { - assertEquipmentIdentifierAvailable( - database, - expected.component.circuitListId, - target.component.equipmentIdentifier, - expected.component.id - ); - } database .update(distributionBoardComponents) .set(target.component) diff --git a/src/db/repositories/equipment-identifier-uniqueness.persistence.ts b/src/db/repositories/equipment-identifier-uniqueness.persistence.ts deleted file mode 100644 index 43b63ad..0000000 --- a/src/db/repositories/equipment-identifier-uniqueness.persistence.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { eq } from "drizzle-orm"; -import type { AppDatabase } from "../database-context.js"; -import { circuitListEquipmentIdentifiers } from "../schema/circuit-list-equipment-identifiers.js"; - -export function normalizeEquipmentIdentifier(value: string): string { - return value.trim().toLowerCase(); -} - -/** - * The DB-level normalized unique index uses SQLite's built-in lower(), - * which only folds ASCII a-z and leaves German characters (Ä/Ö/Ü/ß/…) - * untouched, so it alone would let e.g. "Ä1" and "ä1" coexist. This check - * normalizes with JS's Unicode-aware toLowerCase() against every - * identifier already registered for the circuit list (circuits and - * distribution-board components share one BMK namespace via - * circuit_list_equipment_identifiers), catching what the DB index cannot. - */ -export function assertEquipmentIdentifierAvailable( - database: AppDatabase, - circuitListId: string, - equipmentIdentifier: string, - excludeOwnerId?: string -): void { - const candidate = normalizeEquipmentIdentifier(equipmentIdentifier); - const existing = database - .select({ - ownerId: circuitListEquipmentIdentifiers.ownerId, - equipmentIdentifier: circuitListEquipmentIdentifiers.equipmentIdentifier, - }) - .from(circuitListEquipmentIdentifiers) - .where(eq(circuitListEquipmentIdentifiers.circuitListId, circuitListId)) - .all(); - const duplicate = existing.some( - (row) => - row.ownerId !== excludeOwnerId && - normalizeEquipmentIdentifier(row.equipmentIdentifier) === candidate - ); - if (duplicate) { - throw new Error("Duplicate equipmentIdentifier in circuit list."); - } -} diff --git a/src/db/repositories/external-initial-import-project-command.repository.ts b/src/db/repositories/external-initial-import-project-command.repository.ts index 1f2281e..9d2895a 100644 --- a/src/db/repositories/external-initial-import-project-command.repository.ts +++ b/src/db/repositories/external-initial-import-project-command.repository.ts @@ -212,9 +212,7 @@ function replaceExternalState( if (target.roomMappings.length) { database.insert(externalRoomMappings).values(target.roomMappings).run(); } - if (target.objects.length) { - database.insert(externalModelObjects).values(target.objects).run(); - } + database.insert(externalModelObjects).values(target.objects).run(); } function decodeCanonicalBase64(value: string) { diff --git a/src/db/repositories/external-object-new-circuit-project-command.repository.ts b/src/db/repositories/external-object-new-circuit-project-command.repository.ts index bea64ff..c25ac76 100644 --- a/src/db/repositories/external-object-new-circuit-project-command.repository.ts +++ b/src/db/repositories/external-object-new-circuit-project-command.repository.ts @@ -32,7 +32,6 @@ import { loadExpectedExternalObjectTransitions, snapshotsEqual, } from "./external-object-assignment.persistence.js"; -import { assertEquipmentIdentifierAvailable } from "./equipment-identifier-uniqueness.persistence.js"; export class ExternalObjectNewCircuitProjectCommandRepository implements ExternalObjectNewCircuitProjectCommandStore @@ -91,11 +90,12 @@ export class ExternalObjectNewCircuitProjectCommandRepository .where(eq(circuits.id, circuit.id)).get()) { throw new Error("External circuit id already exists."); } - assertEquipmentIdentifierAvailable( - database, - circuit.circuitListId, - circuit.equipmentIdentifier - ); + if (database.select({ id: circuits.id }).from(circuits).where(and( + eq(circuits.circuitListId, circuit.circuitListId), + eq(circuits.equipmentIdentifier, circuit.equipmentIdentifier) + )).get()) { + throw new Error("Duplicate equipmentIdentifier in circuit list."); + } if (database.select({ id: circuitDeviceRows.id }).from(circuitDeviceRows) .where(eq(circuitDeviceRows.id, row.id)).get()) { throw new Error("External device-row id already exists."); diff --git a/src/db/repositories/project-device-row-sync-project-command.repository.ts b/src/db/repositories/project-device-row-sync-project-command.repository.ts index eb722d7..da17458 100644 --- a/src/db/repositories/project-device-row-sync-project-command.repository.ts +++ b/src/db/repositories/project-device-row-sync-project-command.repository.ts @@ -14,7 +14,6 @@ import type { AppDatabase } from "../database-context.js"; import { circuitDeviceRows } from "../schema/circuit-device-rows.js"; import { circuitLists } from "../schema/circuit-lists.js"; import { circuits } from "../schema/circuits.js"; -import { externalModelObjects } from "../schema/external-model-objects.js"; import { projectDevices } from "../schema/project-devices.js"; import { executeProjectCommandTransaction } from "./project-command-transaction.persistence.js"; import { updateDerivedCircuitVoltage } from "./project-voltage.persistence.js"; @@ -120,32 +119,9 @@ export class ProjectDeviceRowSyncProjectCommandRepository ); for (const assignment of input.command.payload.rows) { - const values: typeof assignment.target & { - manualQuantity?: number; - } = { ...assignment.target }; - if (assignment.target.quantity !== assignment.expected.quantity) { - const externalTotal = tx - .select({ planningValues: externalModelObjects.planningValues }) - .from(externalModelObjects) - .where( - eq(externalModelObjects.circuitDeviceRowId, assignment.rowId) - ) - .all() - .reduce( - (sum, object) => sum + object.planningValues.effectiveQuantity, - 0 - ); - const manualQuantity = assignment.target.quantity - externalTotal; - if (manualQuantity < 0) { - throw new Error( - "Synchronized quantity is below the total quantity of linked external objects." - ); - } - values.manualQuantity = manualQuantity; - } const updated = tx .update(circuitDeviceRows) - .set(values) + .set(assignment.target) .where(eq(circuitDeviceRows.id, assignment.rowId)) .run(); if (updated.changes !== 1) { diff --git a/src/db/schema/circuit-device-rows.ts b/src/db/schema/circuit-device-rows.ts index f718c5d..a5da01e 100644 --- a/src/db/schema/circuit-device-rows.ts +++ b/src/db/schema/circuit-device-rows.ts @@ -1,38 +1,35 @@ -import { index, integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core"; +import { integer, real, sqliteTable, text } from "drizzle-orm/sqlite-core"; import { circuits } from "./circuits.js"; import { projectDevices } from "./project-devices.js"; import { rooms } from "./rooms.js"; -export const circuitDeviceRows = sqliteTable( - "circuit_device_rows", - { - id: text("id").primaryKey(), - circuitId: text("circuit_id") - .notNull() - .references(() => circuits.id, { onDelete: "cascade" }), - linkedProjectDeviceId: text("linked_project_device_id").references(() => projectDevices.id, { - onDelete: "set null", - }), - sortOrder: integer("sort_order").notNull().default(0), - name: text("name").notNull(), - displayName: text("display_name").notNull(), - phaseType: text("phase_type"), - connectionKind: text("connection_kind"), - costGroup: text("cost_group"), - category: text("category"), - level: text("level"), - roomId: text("room_id").references(() => rooms.id, { - onDelete: "set null", - }), - roomNumberSnapshot: text("room_number_snapshot"), - roomNameSnapshot: text("room_name_snapshot"), - quantity: integer("quantity").notNull(), - manualQuantity: integer("manual_quantity").notNull().default(0), - powerPerUnit: real("power_per_unit").notNull(), - simultaneityFactor: real("simultaneity_factor").notNull(), - cosPhi: real("cos_phi"), - remark: text("remark"), - overriddenFields: text("overridden_fields"), - }, - (table) => [index("circuit_device_rows_circuit_id_idx").on(table.circuitId)] -); +export const circuitDeviceRows = sqliteTable("circuit_device_rows", { + id: text("id").primaryKey(), + circuitId: text("circuit_id") + .notNull() + .references(() => circuits.id, { onDelete: "cascade" }), + linkedProjectDeviceId: text("linked_project_device_id").references(() => projectDevices.id, { + onDelete: "set null", + }), + sortOrder: integer("sort_order").notNull().default(0), + name: text("name").notNull(), + displayName: text("display_name").notNull(), + phaseType: text("phase_type"), + connectionKind: text("connection_kind"), + costGroup: text("cost_group"), + category: text("category"), + level: text("level"), + roomId: text("room_id").references(() => rooms.id, { + onDelete: "set null", + }), + roomNumberSnapshot: text("room_number_snapshot"), + roomNameSnapshot: text("room_name_snapshot"), + quantity: integer("quantity").notNull(), + manualQuantity: integer("manual_quantity").notNull().default(0), + powerPerUnit: real("power_per_unit").notNull(), + simultaneityFactor: real("simultaneity_factor").notNull(), + cosPhi: real("cos_phi"), + remark: text("remark"), + overriddenFields: text("overridden_fields"), +}); + diff --git a/src/db/schema/circuits.ts b/src/db/schema/circuits.ts index 6e3ed3e..c0c733c 100644 --- a/src/db/schema/circuits.ts +++ b/src/db/schema/circuits.ts @@ -1,4 +1,4 @@ -import { index, integer, real, sqliteTable, text, unique } from "drizzle-orm/sqlite-core"; +import { integer, real, sqliteTable, text, unique } from "drizzle-orm/sqlite-core"; import { circuitLists } from "./circuit-lists.js"; import { circuitSections } from "./circuit-sections.js"; @@ -26,9 +26,6 @@ export const circuits = sqliteTable( isReserve: integer("is_reserve").notNull().default(0), remark: text("remark"), }, - (table) => [ - unique("circuits_list_equipment_identifier_unique").on(table.circuitListId, table.equipmentIdentifier), - index("circuits_section_id_idx").on(table.sectionId), - ] + (table) => [unique("circuits_list_equipment_identifier_unique").on(table.circuitListId, table.equipmentIdentifier)] ); diff --git a/src/domain/models/circuit-device-row-project-command.model.ts b/src/domain/models/circuit-device-row-project-command.model.ts index 8e949a1..825701f 100644 --- a/src/domain/models/circuit-device-row-project-command.model.ts +++ b/src/domain/models/circuit-device-row-project-command.model.ts @@ -144,9 +144,6 @@ function assertCircuitDeviceRowUpdateFieldValue( if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { throw new Error(`${field} must be a non-negative finite number.`); } - if (field === "simultaneityFactor" && value > 1) { - throw new Error("simultaneityFactor must not exceed 1."); - } return; } if (field === "cosPhi") { diff --git a/src/domain/models/circuit-device-row-structure-project-command.model.ts b/src/domain/models/circuit-device-row-structure-project-command.model.ts index 2464a27..d6e3cf6 100644 --- a/src/domain/models/circuit-device-row-structure-project-command.model.ts +++ b/src/domain/models/circuit-device-row-structure-project-command.model.ts @@ -134,9 +134,6 @@ export function assertCircuitDeviceRowInsertProjectCommand( row.simultaneityFactor, "row.simultaneityFactor" ); - if (row.simultaneityFactor > 1) { - throw new Error("row.simultaneityFactor must not exceed 1."); - } if (row.cosPhi !== null) { assertFiniteNumber(row.cosPhi, "row.cosPhi"); if (row.cosPhi <= 0) { diff --git a/src/domain/models/circuit-device-row.model.ts b/src/domain/models/circuit-device-row.model.ts new file mode 100644 index 0000000..f18c63c --- /dev/null +++ b/src/domain/models/circuit-device-row.model.ts @@ -0,0 +1,24 @@ +export interface CircuitDeviceRow { + id: string; + circuitId: string; + linkedProjectDeviceId?: string; + sortOrder: number; + name: string; + displayName: string; + phaseType?: string; + connectionKind?: string; + costGroup?: string; + category?: string; + level?: string; + roomId?: string; + roomNumberSnapshot?: string; + roomNameSnapshot?: string; + quantity: number; + manualQuantity: number; + powerPerUnit: number; + simultaneityFactor: number; + cosPhi?: number; + remark?: string; + overriddenFields?: string; +} + diff --git a/src/domain/models/circuit-protection-project-command.model.ts b/src/domain/models/circuit-protection-project-command.model.ts index 0c8a19e..2e8e6f4 100644 --- a/src/domain/models/circuit-protection-project-command.model.ts +++ b/src/domain/models/circuit-protection-project-command.model.ts @@ -71,33 +71,15 @@ export function assertCircuitProtectionUpdateProjectCommand( if (target !== null) { assertCircuitProtectionSnapshot(target, circuitId); } - if (circuitProtectionSnapshotsEqual(expected, target)) { + if (JSON.stringify(expected) === JSON.stringify(target)) { throw new Error("Circuit protection update must change state."); } } -function circuitProtectionSnapshotsEqual( - left: CircuitProtectionSnapshot | null, - right: CircuitProtectionSnapshot | null -): boolean { - if (left === null || right === null) { - return left === right; - } - return ( - left.circuitId === right.circuitId && - left.type === right.type && - left.ratedCurrentA === right.ratedCurrentA && - left.fuseUtilizationCategory === right.fuseUtilizationCategory && - left.tripCharacteristic === right.tripCharacteristic && - left.rcdType === right.rcdType && - left.ratedResidualCurrentMa === right.ratedResidualCurrentMa - ); -} - export function assertCircuitProtectionSnapshot( value: unknown, circuitId: string -): asserts value is CircuitProtectionSnapshot { +) { if ( !isPlainObject(value) || Object.keys(value).length !== 7 || diff --git a/src/domain/models/circuit-section.model.ts b/src/domain/models/circuit-section.model.ts new file mode 100644 index 0000000..bba2c4e --- /dev/null +++ b/src/domain/models/circuit-section.model.ts @@ -0,0 +1,9 @@ +export interface CircuitSection { + id: string; + circuitListId: string; + key: string; + displayName: string; + prefix: string; + sortOrder: number; +} + diff --git a/src/domain/models/circuit.model.ts b/src/domain/models/circuit.model.ts new file mode 100644 index 0000000..3efe12c --- /dev/null +++ b/src/domain/models/circuit.model.ts @@ -0,0 +1,19 @@ +export interface Circuit { + id: string; + circuitListId: string; + sectionId: string; + equipmentIdentifier: string; + displayName?: string; + sortOrder: number; + cableType?: string; + cableCrossSection?: string; + cableLength?: number; + rcdAssignment?: string; + terminalDesignation?: string; + voltage?: number; + controlRequirement?: string; + status?: string; + isReserve: boolean; + remark?: string; +} + diff --git a/src/domain/models/project-state-snapshot.model.ts b/src/domain/models/project-state-snapshot.model.ts index 7172cb7..060c795 100644 --- a/src/domain/models/project-state-snapshot.model.ts +++ b/src/domain/models/project-state-snapshot.model.ts @@ -130,7 +130,7 @@ const circuitDeviceRowSchema = z.preprocess( quantity: finiteNumberSchema.nonnegative(), manualQuantity: finiteNumberSchema.nonnegative(), powerPerUnit: finiteNumberSchema.nonnegative(), - simultaneityFactor: finiteNumberSchema.min(0).max(1), + simultaneityFactor: finiteNumberSchema.nonnegative(), cosPhi: finiteNumberSchema.positive().nullable(), remark: nullableStringSchema, overriddenFields: nullableStringSchema, diff --git a/src/frontend/components/Sidebar.tsx b/src/frontend/components/Sidebar.tsx index b3cbb4f..f58c4f6 100644 --- a/src/frontend/components/Sidebar.tsx +++ b/src/frontend/components/Sidebar.tsx @@ -5,7 +5,6 @@ import { usePathname } from "next/navigation"; import { useEffect, useState } from "react"; import { listProjects } from "../utils/api"; import type { ProjectDto } from "../types"; -import { ThemeToggle } from "./ThemeToggle"; const PROJECT_SECTIONS = [ { id: "verlauf", label: "Verlauf", icon: "↺" }, @@ -136,9 +135,6 @@ export function Sidebar({ isCollapsed, onToggleCollapsed }: SidebarProps) { )} -
- -
); } diff --git a/src/frontend/components/ThemeToggle.tsx b/src/frontend/components/ThemeToggle.tsx deleted file mode 100644 index 355e0e2..0000000 --- a/src/frontend/components/ThemeToggle.tsx +++ /dev/null @@ -1,63 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; - -export const THEME_STORAGE_KEY = "leistungsbilanz:theme"; - -type Theme = "light" | "dark"; - -function applyTheme(theme: Theme) { - document.documentElement.setAttribute("data-bs-theme", theme); -} - -export function ThemeToggle() { - const [theme, setTheme] = useState("light"); - - // The inline script in layout.tsx already applied the persisted/system - // theme before hydration; read it back so the toggle starts in sync - // instead of flashing to "light" first. - useEffect(() => { - const current = document.documentElement.getAttribute("data-bs-theme"); - setTheme(current === "dark" ? "dark" : "light"); - }, []); - - useEffect(() => { - function handleStorage(event: StorageEvent) { - if ( - event.key === THEME_STORAGE_KEY && - (event.newValue === "dark" || event.newValue === "light") - ) { - setTheme(event.newValue); - applyTheme(event.newValue); - } - } - window.addEventListener("storage", handleStorage); - return () => window.removeEventListener("storage", handleStorage); - }, []); - - function toggleTheme() { - const next: Theme = theme === "dark" ? "light" : "dark"; - setTheme(next); - applyTheme(next); - try { - localStorage.setItem(THEME_STORAGE_KEY, next); - } catch { - // Private browsing or disabled storage: theme still applies for - // this page load, just without persistence across reloads. - } - } - - return ( - - ); -} diff --git a/src/frontend/components/circuit-tree-editor.tsx b/src/frontend/components/circuit-tree-editor.tsx index 03aa38d..b63cd8e 100644 --- a/src/frontend/components/circuit-tree-editor.tsx +++ b/src/frontend/components/circuit-tree-editor.tsx @@ -268,10 +268,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str const [editingCell, setEditingCell] = useState(null); const [activeSectionId, setActiveSectionId] = useState(null); const [isSaving, setIsSaving] = useState(false); - // Synchronous re-entry guard: isSaving is React state and only reflects - // reality after the next render, so a second click/drop fired within the - // same tick could otherwise race past it and double-submit a command. - const commandInFlightRef = useRef(false); const [componentEditorIntent, setComponentEditorIntent] = useState(null); const [circuitGroupEditorIntent, setCircuitGroupEditorIntent] = @@ -727,27 +723,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str ); }, [data]); - // Clears the sidebar's target selection once it no longer resolves to a - // real section/circuit (e.g. deleted, moved or renumbered elsewhere) - // instead of silently holding a stale id after a tree reload. - useEffect(() => { - if (!data) { - return; - } - if ( - targetSectionId && - !data.sections.some((section) => section.id === targetSectionId) - ) { - setTargetSectionId(null); - } - if ( - targetCircuitId && - !circuitOptions.some((option) => option.id === targetCircuitId) - ) { - setTargetCircuitId(null); - } - }, [data, circuitOptions, targetSectionId, targetCircuitId]); - const allCircuits = useMemo( () => data?.sections.flatMap((section) => section.circuits) ?? [], [data] @@ -1071,10 +1046,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str // Runs a normal command. The server records it in project-wide history. async function runCommand(command: HistoryCommand) { - if (commandInFlightRef.current) { - return; - } - commandInFlightRef.current = true; try { setError(null); setIsSaving(true); @@ -1085,7 +1056,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str await loadTree({ showLoading: false }); setError(message); } finally { - commandInFlightRef.current = false; setIsSaving(false); } } @@ -1093,10 +1063,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str // Applies the next eligible project-wide history operation. Selection is only // a best-effort local hint; command eligibility and data changes stay server-owned. async function applyHistory(mode: "undo" | "redo") { - if (commandInFlightRef.current) { - return; - } - commandInFlightRef.current = true; try { setError(null); setHistoryBusy(true); @@ -1122,7 +1088,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str await loadTree({ showLoading: false }); setError(message); } finally { - commandInFlightRef.current = false; setIsSaving(false); setHistoryBusy(false); } @@ -1779,7 +1744,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str if (!section) { throw new Error("Bereich wurde nicht gefunden."); } - const next = await getNextCircuitIdentifier(projectId, sectionId); + const next = await getNextCircuitIdentifier(sectionId); const sortOrder = section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10; const isDeviceField = deviceFieldKeys.has(key); @@ -1968,7 +1933,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str await runCommand({ label: "Stromkreis hinzufügen", redo: async () => { - const next = await getNextCircuitIdentifier(projectId, sectionId); + const next = await getNextCircuitIdentifier(sectionId); const sortOrder = getInsertionSortOrder(section.circuits, afterCircuitId); const circuit = createCircuitSnapshot({ sectionId, @@ -2162,7 +2127,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str if (!section) { throw new Error("Der Zielbereich ist ungültig."); } - const next = await getNextCircuitIdentifier(projectId, sectionId); + const next = await getNextCircuitIdentifier(sectionId); const sortOrder = section.circuits.length > 0 ? Math.max(...section.circuits.map((circuit) => circuit.sortOrder)) + 10 : 10; const circuit = createCircuitSnapshot( @@ -2573,7 +2538,7 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str await runCommand({ label: newCircuitLabel, redo: async () => { - const next = await getNextCircuitIdentifier(projectId, intent.sectionId); + const next = await getNextCircuitIdentifier(intent.sectionId); const sortOrder = intent.targetCircuitId && intent.placement ? getAdjacentInsertionSortOrder( @@ -3734,7 +3699,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str type="button" tabIndex={-1} disabled={ - isSaving || !buildCircuitGroupReorderAssignments( data.sections, section.id, @@ -3752,7 +3716,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str type="button" tabIndex={-1} disabled={ - isSaving || !buildCircuitGroupReorderAssignments( data.sections, section.id, @@ -3770,7 +3733,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str type="button" tabIndex={-1} disabled={ - isSaving || hasActiveSortOrFilter || !section.category || !canRenumberCircuitGroups( @@ -3796,7 +3758,6 @@ export function CircuitTreeEditor(props: { projectId: string; circuitListId: str - ) : null} diff --git a/src/frontend/components/form-modal.tsx b/src/frontend/components/form-modal.tsx index d893690..05e18eb 100644 --- a/src/frontend/components/form-modal.tsx +++ b/src/frontend/components/form-modal.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { type FormEvent, type ReactNode, useEffect, useRef } from "react"; +import React, { type FormEvent, type ReactNode } from "react"; interface FormModalProps { children: ReactNode; @@ -14,9 +14,6 @@ interface FormModalProps { title: string; } -const FOCUSABLE_SELECTOR = - 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'; - export function FormModal({ children, description, @@ -28,56 +25,11 @@ export function FormModal({ submitLabel, title, }: FormModalProps) { - const dialogRef = useRef(null); - - // Focuses the dialog on open and returns focus to the element that - // triggered it on close, so keyboard users never lose their place in the - // grid behind the backdrop. - useEffect(() => { - const previouslyFocused = document.activeElement as HTMLElement | null; - const firstFocusable = - dialogRef.current?.querySelector(FOCUSABLE_SELECTOR); - firstFocusable?.focus(); - return () => { - previouslyFocused?.focus(); - }; - }, []); - - function handleKeyDown(event: React.KeyboardEvent) { - if (event.key === "Escape") { - if (!isSaving) { - event.stopPropagation(); - onClose(); - } - return; - } - if (event.key !== "Tab" || !dialogRef.current) { - return; - } - const focusable = Array.from( - dialogRef.current.querySelectorAll(FOCUSABLE_SELECTOR) - ); - if (focusable.length === 0) { - return; - } - const first = focusable[0]; - const last = focusable[focusable.length - 1]; - if (event.shiftKey && document.activeElement === first) { - event.preventDefault(); - last.focus(); - } else if (!event.shiftKey && document.activeElement === last) { - event.preventDefault(); - first.focus(); - } - } - return ( <>
diff --git a/src/frontend/components/project-settings-modal.tsx b/src/frontend/components/project-settings-modal.tsx index 4af751c..77295df 100644 --- a/src/frontend/components/project-settings-modal.tsx +++ b/src/frontend/components/project-settings-modal.tsx @@ -7,7 +7,6 @@ import { distributionBoardSupplyTypes, type DistributionBoardSupplyType, } from "../../shared/constants/distribution-board"; -import { FormModal } from "./form-modal"; export interface ProjectSettingsInput { name: string; @@ -132,278 +131,319 @@ export function ProjectSettingsModal({ } return ( - -
-
- - setName(event.target.value)} - required - value={name} - /> -
-
- - - setInternalProjectNumber(event.target.value) - } - value={internalProjectNumber} - /> -
-
- - - setExternalProjectNumber(event.target.value) - } - value={externalProjectNumber} - /> -
-
-
- - Verwendete Netzarten - -

- Nur ausgewählte Netzarten stehen bei Verteilungen zur - Auswahl. Bereits verwendete Netzarten können nicht - deaktiviert werden. -

-
- {distributionBoardSupplyTypes.map((supplyType) => ( -
+ <> +
+
+
+
+
+

+ Projekteinstellungen +

+

+ Stammdaten und elektrische Standardwerte des Projekts +

+
+
+
+
+
+ + setName(event.target.value)} + required + value={name} + /> +
+
+ + + setInternalProjectNumber(event.target.value) + } + value={internalProjectNumber} + /> +
+
+ + + setExternalProjectNumber(event.target.value) + } + value={externalProjectNumber} + /> +
+
+
+ + Verwendete Netzarten + +

+ Nur ausgewählte Netzarten stehen bei Verteilungen zur + Auswahl. Bereits verwendete Netzarten können nicht + deaktiviert werden. +

+
+ {distributionBoardSupplyTypes.map((supplyType) => ( +
+ +
+ ))} +
+ {enabledDistributionBoardSupplyTypes.length === 0 ? ( +
+ Mindestens eine Netzart muss aktiviert sein. +
+ ) : null} +
+
+
+ + setBuildingOwner(event.target.value)} + value={buildingOwner} + /> +
+
+ +