diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..8e8433e
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,7 @@
+node_modules
+.git
+*.log
+.DS_Store
+docker-data
+.env
+data
diff --git a/.gitignore b/.gitignore
index fb47ca2..f38cb21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
node_modules/
data/
+docker-data/
.env
*.log
.DS_Store
diff --git a/Anlagenliste ITT.xlsx b/Anlagenliste ITT.xlsx
deleted file mode 100644
index 8eb00d8..0000000
Binary files a/Anlagenliste ITT.xlsx and /dev/null differ
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..d71bca8
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,18 @@
+# SDS CRM — Node 22 (built-in node:sqlite)
+FROM node:22-bookworm-slim
+
+WORKDIR /app
+
+COPY package.json package-lock.json ./
+RUN npm ci --omit=dev
+
+COPY server ./server
+COPY public ./public
+COPY database ./database
+
+ENV NODE_ENV=production
+ENV PORT=8888
+
+EXPOSE 8888
+
+CMD ["node", "server/index.js"]
diff --git a/database/init.sql b/database/init.sql
index f120a06..e9e4af6 100644
--- a/database/init.sql
+++ b/database/init.sql
@@ -7,6 +7,9 @@ CREATE TABLE IF NOT EXISTS "machines" (
"typ" TEXT NOT NULL,
"seriennummer" TEXT NOT NULL,
"standort" TEXT NOT NULL,
+ "list_status" TEXT NOT NULL DEFAULT '' CHECK (
+ "list_status" IN ('', 'PRUEFEN', 'VERSCHROTTET', 'SN_GEAENDERT', 'IN_BEARBEITUNG', 'UPDATE_RAUS')
+ ),
"extras" TEXT,
"created_at" TEXT NOT NULL DEFAULT (datetime('now')),
"updated_at" TEXT NOT NULL DEFAULT (datetime('now'))
@@ -19,6 +22,8 @@ CREATE TABLE IF NOT EXISTS "tickets" (
"description" TEXT NOT NULL,
"status" TEXT NOT NULL DEFAULT 'OPEN' CHECK ("status" IN ('OPEN', 'WAITING', 'DONE')),
"priority" TEXT NOT NULL DEFAULT 'MEDIUM' CHECK ("priority" IN ('LOW', 'MEDIUM', 'HIGH')),
+ "sla_days" INTEGER,
+ "sla_anchor_at" TEXT,
"created_at" TEXT NOT NULL DEFAULT (datetime('now')),
"updated_at" TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY ("machine_id") REFERENCES "machines" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
@@ -27,12 +32,13 @@ CREATE TABLE IF NOT EXISTS "tickets" (
CREATE TABLE IF NOT EXISTS "events" (
"id" TEXT NOT NULL PRIMARY KEY,
"ticket_id" TEXT NOT NULL,
- "type" TEXT NOT NULL CHECK ("type" IN ('NOTE', 'CALL', 'REMOTE', 'PART', 'SYSTEM')),
+ "type" TEXT NOT NULL CHECK ("type" IN ('NOTE', 'CALL', 'REMOTE', 'PART', 'SYSTEM', 'ATTACHMENT')),
"description" TEXT NOT NULL,
"callback_number" TEXT,
"teamviewer_id" TEXT,
"article_number" TEXT,
"remote_duration_seconds" INTEGER,
+ "teamviewer_notes" TEXT,
"created_at" TEXT NOT NULL DEFAULT (datetime('now')),
FOREIGN KEY ("ticket_id") REFERENCES "tickets" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
@@ -43,6 +49,19 @@ CREATE INDEX IF NOT EXISTS "tickets_priority_idx" ON "tickets" ("priority");
CREATE INDEX IF NOT EXISTS "events_ticket_id_idx" ON "events" ("ticket_id");
CREATE INDEX IF NOT EXISTS "events_created_at_idx" ON "events" ("created_at");
+CREATE TABLE IF NOT EXISTS "ticket_attachments" (
+ "id" TEXT NOT NULL PRIMARY KEY,
+ "event_id" TEXT NOT NULL,
+ "original_name" TEXT NOT NULL,
+ "stored_path" TEXT NOT NULL,
+ "mime_type" TEXT,
+ "size_bytes" INTEGER NOT NULL,
+ "created_at" TEXT NOT NULL DEFAULT (datetime('now')),
+ FOREIGN KEY ("event_id") REFERENCES "events" ("id") ON DELETE CASCADE ON UPDATE CASCADE
+);
+
+CREATE INDEX IF NOT EXISTS "ticket_attachments_event_idx" ON "ticket_attachments" ("event_id");
+
CREATE TABLE IF NOT EXISTS "users" (
"id" TEXT NOT NULL PRIMARY KEY,
"username" TEXT NOT NULL UNIQUE,
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..e155e96
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,17 @@
+services:
+ crm:
+ build: .
+ image: sds-crm:latest
+ restart: unless-stopped
+ ports:
+ - "${PORT:-8888}:8888"
+ environment:
+ PORT: ${PORT:-8888}
+ # Persistente SQLite-Datei und Uploads auf dem Host (Volume unten)
+ SQLITE_PATH: /data/crm.db
+ UPLOAD_DIR: /data/uploads
+ NODE_ENV: production
+ SESSION_SECRET: ${SESSION_SECRET:-}
+ volumes:
+ # Host-Verzeichnis: hier liegt die Datenbank (und uploads/) dauerhaft
+ - ${CRM_DATA_DIR:-./docker-data}:/data
diff --git a/package-lock.json b/package-lock.json
index 80b1752..f7e4b73 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,6 +14,7 @@
"express": "^4.21.2",
"express-session": "^1.19.0",
"ldapjs": "^3.0.7",
+ "multer": "^2.0.0-rc.3",
"xlsx": "^0.18.5"
},
"engines": {
@@ -115,6 +116,24 @@
"deprecated": "This package has been decomissioned. See https://github.com/ldapjs/node-ldapjs/blob/8ffd0bc9c149088a10ec4c1ec6a18450f76ad05d/README.md",
"license": "MIT"
},
+ "node_modules/@tokenizer/token": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
+ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
+ "license": "MIT"
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "license": "MIT",
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
"node_modules/abstract-logging": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
@@ -143,6 +162,15 @@
"node": ">=0.8"
}
},
+ "node_modules/append-field": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-2.0.0.tgz",
+ "integrity": "sha512-yUPXgerKgcuwakzrRyklfhX+Ma2aYYMjb+BO2RPUwq+tk928V/i5DFWcCUS3hQhj468N+Ktmwb0tfbEtmfC6WA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
@@ -170,6 +198,38 @@
"node": ">= 0.6"
}
},
+ "node_modules/base32-encode": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/base32-encode/-/base32-encode-2.0.0.tgz",
+ "integrity": "sha512-mlmkfc2WqdDtMl/id4qm3A7RjW6jxcbAoMjdRmsPiwQP0ufD4oXItYMnPgVHe80lnAIy+1xwzhHE1s4FoIceSw==",
+ "license": "MIT",
+ "dependencies": {
+ "to-data-view": "^2.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
"node_modules/bcrypt": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
@@ -208,6 +268,41 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/busboy": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.3.1.tgz",
+ "integrity": "sha512-y7tTxhGKXcyBxRKAni+awqx8uqaJKrSFSNFSeRG5CsWNdmy2BIK+6VGWEW7TZnIO/533mtMEA4rOevQV815YJw==",
+ "dependencies": {
+ "dicer": "0.3.0"
+ },
+ "engines": {
+ "node": ">=4.5.0"
+ }
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -367,6 +462,17 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/dicer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.3.0.tgz",
+ "integrity": "sha512-MdceRRWqltEG2dZqO769g27N/3PXfcKl04VhYnBlo2YhH7zPi88VebsjTKclaOyiuMaGU72hTfw3VkUitGcVCA==",
+ "dependencies": {
+ "streamsearch": "0.1.2"
+ },
+ "engines": {
+ "node": ">=4.5.0"
+ }
+ },
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -399,6 +505,15 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
"license": "MIT"
},
+ "node_modules/encode-utf8": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encode-utf8/-/encode-utf8-2.0.0.tgz",
+ "integrity": "sha512-3EyMFxZj1/7oMotElDQUEQcP7N4TIe1aJ0m1uBDoyQ8I2LBHhBsXx8P3KsPbqNlGzG+NYxFwEauUwMPHZg3YDQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
"node_modules/encodeurl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
@@ -453,6 +568,24 @@
"node": ">= 0.6"
}
},
+ "node_modules/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
"node_modules/express": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
@@ -531,6 +664,23 @@
],
"license": "MIT"
},
+ "node_modules/file-type": {
+ "version": "16.5.4",
+ "resolved": "https://registry.npmjs.org/file-type/-/file-type-16.5.4.tgz",
+ "integrity": "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-web-to-node-stream": "^3.0.0",
+ "strtok3": "^6.2.4",
+ "token-types": "^4.1.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/file-type?sponsor=1"
+ }
+ },
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
@@ -549,6 +699,15 @@
"node": ">= 0.8"
}
},
+ "node_modules/fmix": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fmix/-/fmix-1.0.0.tgz",
+ "integrity": "sha512-PIaqOGvVH5P+R92Ywy5PumsNEHvondVQh42SGOmkA9A0ZTFbfguzZpjZ/Gy3WVRUqT9Ia8k5tWlJeiZQzRHA7g==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -576,6 +735,18 @@
"node": ">= 0.6"
}
},
+ "node_modules/fs-temp": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/fs-temp/-/fs-temp-2.0.1.tgz",
+ "integrity": "sha512-WYE7cUGOA0xRKsiYxNf/+WDuj0T20OtX85bVhsXpY+wJmjrRIUQftTI6JF9PVpo2RpGkh0l8lZsV8zszisYQ0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "random-path": "^1.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -634,6 +805,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/has-own-property": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-own-property/-/has-own-property-2.0.0.tgz",
+ "integrity": "sha512-oupojxEPq/nfAshi0hOFDjpmYO4JXtymFyPC9YSBmFqGamZ2zUgrCkYInt5tf7f1j6iumlzJBpfLNRxZIQCM0w==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -690,6 +870,26 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -803,6 +1003,39 @@
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
+ "node_modules/multer": {
+ "version": "2.0.0-rc.3",
+ "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.0-rc.3.tgz",
+ "integrity": "sha512-wg2wN2xaS8Xrowj9y25jCVvhUQtIXNzVgWd1VIb7kdwilzpnA/teiZbh0KcOfd3y+RR1DiCXIzmfnWeky1i0Ag==",
+ "deprecated": "You should upgrade to 3.0.0-alpha.1 version",
+ "license": "MIT",
+ "dependencies": {
+ "append-field": "^2.0.0",
+ "busboy": "^0.3.1",
+ "bytes": "^3.1.0",
+ "fs-temp": "^2.0.0",
+ "has-own-property": "^2.0.0",
+ "on-finished": "^2.3.0",
+ "stream-file-type": "^0.6.1",
+ "type-is": "^1.6.18"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/murmur-32": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/murmur-32/-/murmur-32-1.0.0.tgz",
+ "integrity": "sha512-l6QKUGWXzZWHash7lmZzycpIifOrnc3PeMaoFrMv90c+xqOyOSzJU0q2T/1d15MzAdEWTpCh1paC855APqt1Gw==",
+ "license": "MIT",
+ "dependencies": {
+ "encode-utf8": "^2.0.0",
+ "fmix": "^1.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
"node_modules/negotiator": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
@@ -898,6 +1131,19 @@
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
+ "node_modules/peek-readable": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/peek-readable/-/peek-readable-4.1.0.tgz",
+ "integrity": "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
"node_modules/precond": {
"version": "0.2.3",
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
@@ -906,6 +1152,15 @@
"node": ">= 0.6"
}
},
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
"node_modules/process-warning": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.2.tgz",
@@ -949,6 +1204,19 @@
"node": ">= 0.8"
}
},
+ "node_modules/random-path": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/random-path/-/random-path-1.0.0.tgz",
+ "integrity": "sha512-I6FGG7uFR3tZqHP7TzcP3Ikt5IyVEG59u7KTjIIjizcdPY6MDjD9CbbKqE+znIv4mrDF6HMlBshoemk0oRRwsQ==",
+ "license": "MIT",
+ "dependencies": {
+ "base32-encode": "^2.0.0",
+ "murmur-32": "^1.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
"node_modules/range-parser": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
@@ -973,6 +1241,38 @@
"node": ">= 0.8"
}
},
+ "node_modules/readable-stream": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
+ "integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
+ "license": "MIT",
+ "dependencies": {
+ "abort-controller": "^3.0.0",
+ "buffer": "^6.0.3",
+ "events": "^3.3.0",
+ "process": "^0.11.10",
+ "string_decoder": "^1.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/readable-web-to-node-stream": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.4.tgz",
+ "integrity": "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw==",
+ "license": "MIT",
+ "dependencies": {
+ "readable-stream": "^4.7.0"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -1143,6 +1443,61 @@
"node": ">= 0.8"
}
},
+ "node_modules/stream-file-type": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/stream-file-type/-/stream-file-type-0.6.1.tgz",
+ "integrity": "sha512-//KIXMQan4ow4gD//dfPu15nhH/sFdt41PzAOpD9EBFUBy/MtFjocTPO8v1dTOJnyi47TlPo6Qj+67sSE1lJKA==",
+ "license": "MIT",
+ "dependencies": {
+ "file-type": "^16.0.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
+ "node_modules/streamsearch": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz",
+ "integrity": "sha512-jos8u++JKm0ARcSUTAZXOVC0mSox7Bhn6sBgty73P1f3JGf7yG2clTbBNHUdde/kdvP2FESam+vM6l8jBrNxHA==",
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/strtok3": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-6.3.0.tgz",
+ "integrity": "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tokenizer/token": "^0.3.0",
+ "peek-readable": "^4.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/to-data-view": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/to-data-view/-/to-data-view-2.0.0.tgz",
+ "integrity": "sha512-RGEM5KqlPHr+WVTPmGNAXNeFEmsBnlkxXaIfEpUYV0AST2Z5W1EGq9L/MENFrMMmL2WQr1wjkmZy/M92eKhjYA==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ }
+ },
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -1152,6 +1507,23 @@
"node": ">=0.6"
}
},
+ "node_modules/token-types": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/token-types/-/token-types-4.2.1.tgz",
+ "integrity": "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@tokenizer/token": "^0.3.0",
+ "ieee754": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
diff --git a/package.json b/package.json
index b079063..494f01e 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"express": "^4.21.2",
"express-session": "^1.19.0",
"ldapjs": "^3.0.7",
+ "multer": "^2.0.0-rc.3",
"xlsx": "^0.18.5"
}
}
diff --git a/public/bootstrap.html b/public/bootstrap.html
new file mode 100644
index 0000000..d8f94be
--- /dev/null
+++ b/public/bootstrap.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+ Erster Administrator — SDS CRM
+
+
+
+
+
+
+ Lade …
+
+
Erster Administrator
+
+ Es ist noch kein Benutzer angelegt. Legen Sie das erste Admin-Konto an (min. 8 Zeichen Passwort).
+
+
+
+
+
+
+
+
+
diff --git a/public/css/pages/auth.css b/public/css/pages/auth.css
new file mode 100644
index 0000000..1d6fbb4
--- /dev/null
+++ b/public/css/pages/auth.css
@@ -0,0 +1 @@
+/* Login / Bootstrap – bei Bedarf seiten-spezifische Styles */
diff --git a/public/css/pages/detail.css b/public/css/pages/detail.css
new file mode 100644
index 0000000..e082daa
--- /dev/null
+++ b/public/css/pages/detail.css
@@ -0,0 +1 @@
+/* Maschinen- und Ticket-Detail – bei Bedarf seiten-spezifische Styles */
diff --git a/public/css/pages/machines.css b/public/css/pages/machines.css
new file mode 100644
index 0000000..e79fe27
--- /dev/null
+++ b/public/css/pages/machines.css
@@ -0,0 +1,127 @@
+/* Farben zentral — gleiche Werte für Zeilen und Legende */
+.machines-overview {
+ --mrow-none: rgba(240, 243, 246, 0.12);
+ --mrow-pruefen: rgba(229, 57, 53, 0.45);
+ --mrow-verschrottet: rgba(206, 147, 216, 0.45);
+ --mrow-sn: rgba(102, 187, 106, 0.4);
+ --mrow-bearbeitung: rgba(255, 235, 59, 0.35);
+ --mrow-update: rgba(38, 198, 218, 0.4);
+}
+
+.machines-overview-header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 1rem 1.5rem;
+}
+
+.machines-overview-title h2 {
+ margin-bottom: 0.35rem;
+}
+
+/* Legende oben rechts */
+.machine-legend {
+ flex-shrink: 0;
+ max-width: min(100%, 16rem);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 0.55rem 0.75rem 0.6rem;
+ background: var(--bg-card);
+ font-size: 0.82rem;
+ line-height: 1.35;
+}
+
+.machine-legend-title {
+ font-weight: 600;
+ color: var(--text-sub);
+ margin-bottom: 0.4rem;
+ font-size: 0.78rem;
+ letter-spacing: 0.02em;
+ text-transform: uppercase;
+}
+
+.machine-legend-list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+}
+
+.machine-legend-list li {
+ display: flex;
+ align-items: center;
+ gap: 0.45rem;
+ margin: 0.15rem 0;
+ color: var(--text);
+}
+
+.machine-legend-swatch {
+ flex-shrink: 0;
+ width: 1rem;
+ height: 0.75rem;
+ border-radius: 3px;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
+}
+
+.machine-legend-swatch.machine-row--none {
+ background: var(--mrow-none);
+}
+
+.machine-legend-swatch.machine-row--pruefen {
+ background: var(--mrow-pruefen);
+}
+
+.machine-legend-swatch.machine-row--verschrottet {
+ background: var(--mrow-verschrottet);
+}
+
+.machine-legend-swatch.machine-row--sn {
+ background: var(--mrow-sn);
+}
+
+.machine-legend-swatch.machine-row--bearbeitung {
+ background: var(--mrow-bearbeitung);
+}
+
+.machine-legend-swatch.machine-row--update {
+ background: var(--mrow-update);
+}
+
+/* Maschinenliste: ganze Zeile gemäß Listen-Status */
+#machine-table tbody tr.machine-row--none {
+ background: var(--mrow-none);
+}
+
+#machine-table tbody tr.machine-row--pruefen {
+ background: var(--mrow-pruefen);
+}
+
+#machine-table tbody tr.machine-row--verschrottet {
+ background: var(--mrow-verschrottet);
+}
+
+#machine-table tbody tr.machine-row--sn {
+ background: var(--mrow-sn);
+}
+
+#machine-table tbody tr.machine-row--bearbeitung {
+ background: var(--mrow-bearbeitung);
+}
+
+#machine-table tbody tr.machine-row--update {
+ background: var(--mrow-update);
+}
+
+#machine-table tbody tr.machine-row--pruefen a,
+#machine-table tbody tr.machine-row--verschrottet a,
+#machine-table tbody tr.machine-row--sn a,
+#machine-table tbody tr.machine-row--bearbeitung a,
+#machine-table tbody tr.machine-row--update a {
+ color: inherit;
+ font-weight: 600;
+}
+
+#machine-table tbody tr.machine-row--bearbeitung a {
+ color: var(--accent-hi);
+}
diff --git a/public/css/pages/options.css b/public/css/pages/options.css
new file mode 100644
index 0000000..81a28cd
--- /dev/null
+++ b/public/css/pages/options.css
@@ -0,0 +1 @@
+/* Optionen – bei Bedarf seiten-spezifische Styles */
diff --git a/public/css/pages/start.css b/public/css/pages/start.css
new file mode 100644
index 0000000..4dec9a8
--- /dev/null
+++ b/public/css/pages/start.css
@@ -0,0 +1 @@
+/* Start – bei Bedarf seiten-spezifische Styles */
diff --git a/public/css/pages/tickets.css b/public/css/pages/tickets.css
new file mode 100644
index 0000000..8d83208
--- /dev/null
+++ b/public/css/pages/tickets.css
@@ -0,0 +1,37 @@
+/* Tickets – Neues Ticket: volle Breite für Titel & Beschreibung */
+
+.ticket-new-form .ticket-form-machine label {
+ max-width: 36rem;
+}
+
+.ticket-new-form .ticket-form-machine select {
+ width: 100%;
+ min-width: 0;
+}
+
+.ticket-new-form .ticket-field-full {
+ width: 100%;
+}
+
+.ticket-new-form .ticket-field-full input,
+.ticket-new-form .ticket-field-full textarea {
+ width: 100%;
+ min-width: 0;
+ max-width: 100%;
+}
+
+.ticket-new-form textarea.ticket-desc {
+ resize: vertical;
+ min-height: 8rem;
+ display: block;
+}
+
+.ticket-row-overdue {
+ background-color: rgba(180, 40, 40, 0.12);
+ box-shadow: inset 3px 0 0 0 #b42828;
+}
+
+.ticket-row-overdue a {
+ color: #8b1e1e;
+ font-weight: 600;
+}
diff --git a/public/css/pages/users.css b/public/css/pages/users.css
new file mode 100644
index 0000000..1c3f960
--- /dev/null
+++ b/public/css/pages/users.css
@@ -0,0 +1 @@
+/* Benutzer – bei Bedarf seiten-spezifische Styles */
diff --git a/public/css/style.css b/public/css/style.css
index 4f861df..d67f41b 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -1,866 +1,1038 @@
-:root {
- --bg: #0d1117;
- --bg-card: #161b22;
- --bg-raised: #1c2330;
- --bg-hover: #212836;
- --border: #30363d;
- --border-hi: #444c56;
- --text: #e6edf3;
- --text-sub: #8b949e;
- --text-muted:#6e7681;
- --accent: #2f81f7;
- --accent-hi: #58a6ff;
- --green: #238636;
- --green-fg: #3fb950;
- --amber: #9e6a03;
- --amber-fg: #d29922;
- --red: #b91c1c;
- --red-fg: #f85149;
-
- font-family: 'Segoe UI', system-ui, sans-serif;
- font-size: 14px;
- line-height: 1.55;
- color: var(--text);
- background: var(--bg);
-}
-
-* { box-sizing: border-box; }
-
-body { margin: 0; }
-
-a {
- color: var(--accent-hi);
- text-decoration: none;
-}
-a:hover { text-decoration: underline; }
-
-/* ── Header ──────────────────────────────────────── */
-.header {
- background: #010409;
- border-bottom: 1px solid var(--border);
- padding: 0 1.5rem;
- height: 52px;
- display: flex;
- align-items: center;
- gap: 2rem;
- position: sticky;
- top: 0;
- z-index: 100;
-}
-
-.header h1 {
- margin: 0;
- font-size: 1rem;
- font-weight: 600;
- color: var(--text);
- letter-spacing: 0.02em;
- white-space: nowrap;
-}
-
-.header nav { display: flex; gap: 0.25rem; }
-
-.header nav a {
- color: var(--text-sub);
- padding: 0.35rem 0.75rem;
- border-radius: 6px;
- font-size: 0.9rem;
- transition: color 0.1s, background 0.1s;
-}
-
-.header nav a:hover {
- color: var(--text);
- background: var(--bg-raised);
- text-decoration: none;
-}
-
-.header h1 a {
- color: inherit;
-}
-
-#main-nav {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 0.75rem 1rem;
-}
-
-.nav-user {
- font-size: 0.85rem;
-}
-
-.btn-nav-logout {
- padding: 0.3rem 0.65rem;
- font-size: 0.85rem;
-}
-
-.auth-panel {
- max-width: 28rem;
-}
-
-.row-inline {
- flex-direction: row;
- align-items: center;
- gap: 0.5rem;
-}
-
-.users-table .users-actions {
- white-space: nowrap;
-}
-
-.users-table .users-actions button {
- margin-right: 0.35rem;
- margin-bottom: 0.25rem;
-}
-
-.options-page .options-section-title {
- margin: 0 0 0.5rem;
- font-size: 1.05rem;
- font-weight: 600;
- color: var(--text);
- text-transform: none;
- letter-spacing: normal;
-}
-
-.options-page .options-section .muted code {
- font-size: 0.85em;
-}
-
-/* LDAP-Synchronisation (Referenz-Layout) */
-.ldap-section {
- padding: 0;
- overflow: hidden;
-}
-
-.ldap-section-toggle {
- width: 100%;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 1rem;
- padding: 0.85rem 1.25rem;
- margin: 0;
- border: none;
- border-bottom: 1px solid var(--border);
- background: var(--bg-raised);
- color: var(--text);
- font: inherit;
- font-size: 1rem;
- font-weight: 600;
- cursor: pointer;
- text-align: left;
-}
-
-.ldap-section-toggle:hover {
- background: var(--bg-hover);
-}
-
-.ldap-section-heading {
- flex: 1;
-}
-
-.ldap-chevron {
- font-size: 0.75rem;
- color: var(--text-muted);
- flex-shrink: 0;
-}
-
-.ldap-section-body {
- padding: 1rem 1.25rem 1.25rem;
-}
-
-.ldap-section-body[hidden] {
- display: none !important;
-}
-
-.ldap-subtitle {
- margin: 0 0 1rem;
- font-size: 0.95rem;
- font-weight: 600;
- color: var(--text-sub);
-}
-
-.ldap-sync-check {
- margin-bottom: 1rem;
-}
-
-.form-grid-2 {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 0.75rem 1rem;
- margin-bottom: 0.75rem;
-}
-
-@media (max-width: 720px) {
- .form-grid-2 {
- grid-template-columns: 1fr;
- }
-}
-
-.form-grid-ldap-attr {
- display: grid;
- grid-template-columns: 1fr 1fr 1fr;
- gap: 0.75rem 1rem;
- margin-bottom: 0.75rem;
-}
-
-@media (max-width: 900px) {
- .form-grid-ldap-attr {
- grid-template-columns: 1fr;
- }
-}
-
-.options-page label.full-width {
- display: flex;
- flex-direction: column;
- gap: 0.35rem;
- margin-bottom: 0.75rem;
-}
-
-.ldap-filter-ta {
- font-family: ui-monospace, monospace;
- font-size: 0.82rem;
- line-height: 1.45;
- min-height: 5rem;
-}
-
-.ldap-hint {
- margin: -0.35rem 0 0.75rem;
- font-size: 0.82rem;
-}
-
-.options-actions {
- display: flex;
- justify-content: flex-start;
-}
-
-.btn-config-save {
- padding: 0.5rem 1.25rem;
- font-weight: 600;
-}
-
-/* LDAP Sync-Panel (Optionen) */
-.sync-panel .sync-actions {
- margin-bottom: 0.75rem;
-}
-
-.btn-ldap-sync-now {
- padding: 0.5rem 1.1rem;
- font-weight: 600;
-}
-
-.sync-last-line {
- margin: 0 0 1rem;
- font-size: 0.92rem;
-}
-
-.sync-log-title {
- margin: 0 0 0.65rem;
- font-size: 0.95rem;
- font-weight: 600;
- color: var(--text-sub);
- text-transform: none;
- letter-spacing: normal;
-}
-
-.sync-log-table-wrap {
- overflow-x: auto;
-}
-
-table.sync-log-table {
- font-size: 0.88rem;
-}
-
-table.sync-log-table th,
-table.sync-log-table td {
- padding: 0.45rem 0.65rem;
- border-bottom: 1px solid var(--border);
- vertical-align: top;
-}
-
-table.sync-log-table th {
- font-weight: 600;
- color: var(--text-sub);
- text-align: left;
-}
-
-table.sync-log-table td.num {
- text-align: right;
- font-variant-numeric: tabular-nums;
-}
-
-.sync-status-badge {
- display: inline-block;
- padding: 0.15rem 0.55rem;
- border-radius: 999px;
- font-size: 0.82rem;
- font-weight: 600;
-}
-
-.sync-status-badge.sync-status-ok {
- background: rgba(46, 160, 67, 0.25);
- color: #3fb950;
- border: 1px solid rgba(63, 185, 80, 0.45);
-}
-
-.sync-status-badge.sync-status-err {
- background: rgba(248, 81, 73, 0.15);
- color: var(--red, #f85149);
- border: 1px solid rgba(248, 81, 73, 0.35);
-}
-
-.tv-device-row {
- display: flex;
- flex-wrap: wrap;
- align-items: flex-end;
- gap: 0.75rem;
-}
-
-.tv-device-row label {
- flex: 1;
- min-width: 12rem;
-}
-
-.tv-conn-hint {
- margin: 0.25rem 0 0;
- font-size: 0.85rem;
-}
-
-/* ── Layout ──────────────────────────────────────── */
-.main {
- padding: 1.5rem 2rem;
- max-width: 100%;
-}
-
-/* ── Card ────────────────────────────────────────── */
-.card {
- background: var(--bg-card);
- border: 1px solid var(--border);
- border-radius: 8px;
- padding: 1rem 1.25rem;
- margin-bottom: 1rem;
-}
-
-/* ── Stack / Row ─────────────────────────────────── */
-.stack {
- display: flex;
- flex-direction: column;
- gap: 0.85rem;
-}
-
-.row {
- display: flex;
- gap: 0.75rem;
- flex-wrap: wrap;
- align-items: flex-end;
-}
-
-/* ── Typography ─────────────────────────────────── */
-h2 { margin: 0 0 0.5rem; font-size: 1.25rem; }
-h3, h4 { margin: 0 0 0.5rem; font-size: 0.95rem; color: var(--text-sub); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
-
-.muted { color: var(--text-muted); font-size: 0.9rem; }
-.error { color: var(--red-fg); font-size: 0.9rem; }
-
-/* ── Forms ───────────────────────────────────────── */
-label {
- display: flex;
- flex-direction: column;
- gap: 0.3rem;
- font-size: 0.85rem;
- font-weight: 500;
- color: var(--text-sub);
-}
-
-input, select, textarea, button { font: inherit; }
-
-input, select, textarea {
- padding: 0.45rem 0.65rem;
- background: var(--bg);
- border: 1px solid var(--border);
- border-radius: 6px;
- color: var(--text);
- min-width: 180px;
- transition: border-color 0.1s;
-}
-
-input:focus, select:focus, textarea:focus {
- outline: none;
- border-color: var(--accent);
-}
-
-textarea { min-height: 72px; resize: vertical; }
-
-button {
- padding: 0.45rem 0.9rem;
- border-radius: 6px;
- border: 1px solid var(--accent);
- background: var(--accent);
- color: #fff;
- cursor: pointer;
- font-weight: 500;
- transition: opacity 0.12s;
-}
-button:hover { opacity: 0.85; }
-button.secondary {
- background: transparent;
- color: var(--accent-hi);
- border-color: var(--border-hi);
-}
-button.secondary:hover { background: var(--bg-hover); opacity: 1; }
-button.danger { border-color: var(--red); background: var(--red); color: #fff; }
-button:disabled { opacity: 0.4; cursor: not-allowed; }
-
-/* ── Table (global) ──────────────────────────────── */
-table {
- width: 100%;
- border-collapse: collapse;
- font-size: 0.92rem;
-}
-
-th, td {
- text-align: left;
- padding: 0.55rem 0.6rem;
- border-bottom: 1px solid var(--border);
-}
-
-th {
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- color: var(--text-sub);
- font-weight: 600;
-}
-
-/* ── Badge ───────────────────────────────────────── */
-.badge {
- display: inline-block;
- padding: 0.15rem 0.55rem;
- border-radius: 999px;
- font-size: 0.72rem;
- font-weight: 600;
- letter-spacing: 0.03em;
- background: var(--bg-raised);
- color: var(--text-sub);
- border: 1px solid var(--border-hi);
-}
-
-/* ── Timeline ────────────────────────────────────── */
-.timeline {
- border-left: 2px solid var(--border);
- margin-left: 0.35rem;
- padding-left: 1rem;
-}
-.timeline-item { margin-bottom: 1rem; }
-.timeline-item time { font-size: 0.78rem; color: var(--text-muted); }
-
-/* ── Table-Wrap ──────────────────────────────────── */
-.table-wrap {
- overflow-x: auto;
- -webkit-overflow-scrolling: touch;
-}
-
-/* ── Extras / Anlagen ────────────────────────────── */
-.extras-table { font-size: 0.88rem; }
-.extras-table th {
- vertical-align: top;
- font-weight: 600;
- text-transform: none;
- letter-spacing: normal;
- color: var(--text-sub);
- border-right: 1px solid var(--border);
-}
-/* Nur Key-Value-Extras: feste Schlüsselspalte (nicht Anlagenliste mit 3 Spalten) */
-.extras-table:not(.anlagen-voll) th {
- width: 11rem;
-}
-
-.extras-cell-input {
- width: 100%;
- min-width: 0;
- box-sizing: border-box;
- font: inherit;
- font-size: 0.88rem;
- padding: 0.35rem 0.45rem;
- border-radius: 4px;
- border: 1px solid var(--border);
- background: var(--bg);
- color: var(--text);
-}
-.extras-table th .extras-cell-input {
- font-weight: 500;
- text-transform: none;
- letter-spacing: normal;
-}
-
-.machine-detail-card-title {
- margin-top: 0;
- font-size: 1rem;
- font-weight: 600;
-}
-
-.machine-detail-actions {
- flex-wrap: wrap;
- align-items: center;
- gap: 0.75rem;
-}
-/* Anlagenliste: Gruppe + Beschreibung schmal aber lesbar, „Wert“ der Rest.
- Kein width:100% auf der letzten Zelle — das drückt Spalte 2 auf Mindestbreite. */
-.anlagen-voll {
- width: 100%;
- table-layout: fixed;
-}
-/* Spaltenbreiten über colgroup: 1+2 kompakt, 3 bekommt den Rest (kein width:100% auf Zellen) */
-.anlagen-voll col.anlagen-col-gruppe {
- width: 10rem;
-}
-.anlagen-voll col.anlagen-col-beschr {
- width: 22%;
- min-width: 11rem;
-}
-.anlagen-voll col.anlagen-col-wert {
- width: auto;
-}
-.anlagen-voll:not(.gruppiert) thead th {
- text-transform: none;
- letter-spacing: normal;
- font-size: 0.75rem;
- vertical-align: bottom;
-}
-.anlagen-voll:not(.gruppiert) thead th:nth-child(1),
-.anlagen-voll:not(.gruppiert) tbody td:nth-child(1) {
- vertical-align: top;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- font-size: 0.85rem;
-}
-.anlagen-voll:not(.gruppiert) thead th:nth-child(2),
-.anlagen-voll:not(.gruppiert) tbody th:nth-child(2) {
- vertical-align: top;
- max-width: 28rem;
- white-space: normal;
- word-break: break-word;
- hyphens: auto;
-}
-.anlagen-voll:not(.gruppiert) thead th:nth-child(3),
-.anlagen-voll:not(.gruppiert) tbody td:nth-child(3) {
- vertical-align: top;
- word-break: break-word;
-}
-
-/* Gruppierte Anlagenliste: Box pro Gruppe, Kopfzeile = Gruppenname */
-.anlagen-voll.gruppiert col.anlagen-col-beschr {
- width: 22%;
- min-width: 11rem;
-}
-.anlagen-voll.gruppiert col.anlagen-col-wert {
- width: auto;
-}
-.anlagen-voll.gruppiert thead th {
- text-transform: none;
- letter-spacing: normal;
- font-size: 0.75rem;
- vertical-align: bottom;
-}
-.anlagen-voll.gruppiert thead th:first-child,
-.anlagen-voll.gruppiert tbody.anlagen-gruppe tr:not(.anlagen-gruppe-kopf) th {
- vertical-align: top;
- max-width: 28rem;
- white-space: normal;
- word-break: break-word;
- hyphens: auto;
-}
-.anlagen-voll.gruppiert thead th:last-child,
-.anlagen-voll.gruppiert tbody.anlagen-gruppe tr:not(.anlagen-gruppe-kopf) td {
- vertical-align: top;
- word-break: break-word;
-}
-.anlagen-voll.gruppiert tbody.anlagen-gruppe-spacer td {
- height: 0.85rem;
- padding: 0 !important;
- border: none !important;
- background: transparent !important;
-}
-.anlagen-voll.gruppiert tr.anlagen-gruppe-kopf td {
- font-weight: 600;
- font-size: 0.8rem;
- text-transform: none;
- letter-spacing: 0.02em;
- color: var(--text);
- background: var(--bg-raised);
- border: 1px solid var(--border-hi);
- border-bottom: 1px solid var(--border);
- padding: 0.5rem 0.65rem;
-}
-.anlagen-voll.gruppiert tbody.anlagen-gruppe tr.anlagen-gruppe-kopf td:first-child {
- border-radius: 8px 8px 0 0;
-}
-.anlagen-voll.gruppiert tbody.anlagen-gruppe tr:not(.anlagen-gruppe-kopf) th,
-.anlagen-voll.gruppiert tbody.anlagen-gruppe tr:not(.anlagen-gruppe-kopf) td {
- border-left: 1px solid var(--border-hi);
- border-right: 1px solid var(--border-hi);
-}
-.anlagen-voll.gruppiert tbody.anlagen-gruppe tr:not(.anlagen-gruppe-kopf) th {
- border-right: 1px solid var(--border);
-}
-.anlagen-voll.gruppiert tbody.anlagen-gruppe tr:not(.anlagen-gruppe-kopf):last-child th:first-child {
- border-bottom: 1px solid var(--border-hi);
- border-bottom-left-radius: 8px;
-}
-.anlagen-voll.gruppiert tbody.anlagen-gruppe tr:not(.anlagen-gruppe-kopf):last-child td:last-child {
- border-bottom: 1px solid var(--border-hi);
- border-bottom-right-radius: 8px;
-}
-
-.main:has(.machines-overview) { max-width: 100%; }
-
-code {
- font-size: 0.85em;
- background: var(--bg-raised);
- color: var(--text-sub);
- padding: 0.1em 0.35em;
- border-radius: 4px;
-}
-
-/* ═══════════════════════════════════════════════════
- Startseite — Offene Tickets
-════════════════════════════════════════════════════ */
-.home-open-tickets { gap: 1rem; }
-
-.home-kpi-bar {
- display: flex;
- align-items: center;
- gap: 1.25rem;
- flex-wrap: wrap;
-}
-
-.home-kpi-pills {
- display: flex;
- align-items: center;
- gap: 0.5rem;
- flex-wrap: wrap;
-}
-
-.kpi-pill .badge {
- font-size: 0.8rem;
- padding: 0.2rem 0.65rem;
-}
-
-.home-open-tickets h2 {
- font-size: 1.1rem;
- color: var(--text);
- font-weight: 600;
- letter-spacing: 0;
- text-transform: none;
- margin: 0;
-}
-
-.home-ticket-list {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-}
-
-.home-ticket-card {
- padding: 0;
- overflow: hidden;
-}
-
-.home-ticket-top {
- display: flex;
- flex-wrap: wrap;
- align-items: flex-start;
- justify-content: space-between;
- gap: 0.75rem 1rem;
- padding: 0.85rem 1.1rem;
- border-bottom: 1px solid var(--border);
- background: var(--bg-raised);
-}
-
-.home-ticket-top-inner {
- flex: 1;
- min-width: 12rem;
-}
-
-.home-ticket-titleline {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 0.5rem 0.75rem;
- font-size: 0.95rem;
- margin-bottom: 0.35rem;
-}
-
-.home-ticket-titleline > a {
- font-weight: 600;
-}
-
-.home-ticket-meta-row {
- font-size: 0.8rem;
- color: var(--text-muted);
- display: flex;
- flex-wrap: wrap;
- gap: 0.35rem 1rem;
-}
-
-.home-ticket-open {
- display: inline-block;
- padding: 0.35rem 0.75rem;
- border: 1px solid var(--accent);
- border-radius: 6px;
- font-size: 0.82rem;
- font-weight: 500;
- color: var(--accent-hi);
- transition: background 0.1s;
- flex-shrink: 0;
- text-decoration: none;
-}
-
-.home-ticket-open:hover {
- background: rgba(47, 129, 247, 0.12);
- text-decoration: none;
-}
-
-.home-ticket-context {
- padding: 0.85rem 1.1rem;
- border-bottom: 1px solid var(--border);
-}
-
-.home-ticket-context-h {
- margin: 0 0 0.4rem;
- font-size: 0.82rem;
-}
-
-.home-ticket-desc {
- white-space: pre-wrap;
- word-break: break-word;
- color: var(--text-sub);
- font-size: 0.88rem;
- line-height: 1.55;
- margin: 0;
-}
-
-.home-ticket-machine {
- margin: 0.65rem 0 0;
- font-size: 0.85rem;
- color: var(--text-sub);
-}
-
-.home-ticket-events {
- padding: 0.75rem 1.1rem 1rem;
- display: flex;
- flex-direction: column;
- gap: 0.65rem;
-}
-
-.home-ticket-no-events {
- margin: 0;
- font-size: 0.88rem;
-}
-
-.home-event-box {
- border: 1px solid var(--border);
- border-radius: 6px;
- padding: 0.65rem 0.85rem;
- background: var(--bg);
- font-size: 0.88rem;
-}
-
-.home-event-box-header {
- display: flex;
- flex-wrap: wrap;
- align-items: center;
- gap: 0.5rem 0.75rem;
- margin-bottom: 0.45rem;
-}
-
-.home-event-box-time {
- font-size: 0.78rem;
- color: var(--text-muted);
-}
-
-.home-event-box-body :last-child {
- margin-bottom: 0;
-}
-
-/* Status badge variants */
-.badge-open { background: rgba(35, 134, 54, 0.15); color: var(--green-fg); border-color: rgba(63, 185, 80, 0.3); }
-.badge-waiting { background: rgba(158, 106, 3, 0.15); color: var(--amber-fg); border-color: rgba(210, 153, 34, 0.3); }
-.badge-done { background: rgba(110, 118, 129, 0.12); color: var(--text-muted); border-color: var(--border); }
-
-/* Priority badge variants */
-.badge-high { background: rgba(185, 28, 28, 0.18); color: var(--red-fg); border-color: rgba(248, 81, 73, 0.35); }
-.badge-medium { background: rgba(158, 106, 3, 0.15); color: var(--amber-fg); border-color: rgba(210, 153, 34, 0.3); }
-.badge-low { background: rgba(35, 134, 54, 0.1); color: var(--green-fg); border-color: rgba(63, 185, 80, 0.2); }
-
-/* Ticket-Detail: Ereignisse als Tabelle (neueste zuerst) */
-.events-table {
- font-size: 0.9rem;
-}
-
-.events-table thead th {
- background: var(--bg-raised);
- white-space: nowrap;
-}
-
-.events-table-time {
- color: var(--text-sub);
- font-variant-numeric: tabular-nums;
- white-space: nowrap;
- width: 1%;
- vertical-align: top;
-}
-
-.events-table-desc {
- color: var(--text-sub);
- vertical-align: top;
- line-height: 1.5;
-}
-
-.event-inhalt-block {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.event-inhalt-label {
- margin: 0;
- font-size: 0.72rem;
- text-transform: uppercase;
- letter-spacing: 0.06em;
- color: var(--text-muted);
- font-weight: 600;
-}
-
-.event-inhalt-text {
- white-space: pre-wrap;
- word-break: break-word;
- margin: 0;
-}
-
-.event-inhalt-meta {
- margin: 0;
- font-size: 0.88rem;
-}
-
-.event-artnr {
- font-size: 0.95em;
-}
-
-.event-tv-placeholder {
- font-size: 0.85em;
-}
-
-.ev-field-group[hidden] {
- display: none !important;
-}
-
-.event-type-badge {
- white-space: nowrap;
-}
-
-.event-type-note { background: rgba(88, 166, 255, 0.12); color: var(--accent-hi); border-color: rgba(88, 166, 255, 0.35); }
-.event-type-call { background: rgba(163, 113, 247, 0.14); color: #d2a8ff; border-color: rgba(163, 113, 247, 0.35); }
-.event-type-remote { background: rgba(35, 134, 54, 0.15); color: var(--green-fg); border-color: rgba(63, 185, 80, 0.3); }
-.event-type-part { background: rgba(210, 153, 34, 0.14); color: var(--amber-fg); border-color: rgba(210, 153, 34, 0.35); }
-.event-type-system { background: rgba(110, 118, 129, 0.18); color: var(--text-muted); border-color: var(--border-hi); }
+:root {
+ --bg: #0d1117;
+ --bg-card: #161b22;
+ --bg-raised: #1c2330;
+ --bg-hover: #212836;
+ --border: #30363d;
+ --border-hi: #444c56;
+ --text: #e6edf3;
+ --text-sub: #8b949e;
+ --text-muted:#6e7681;
+ --accent: #2f81f7;
+ --accent-hi: #58a6ff;
+ --green: #238636;
+ --green-fg: #3fb950;
+ --amber: #9e6a03;
+ --amber-fg: #d29922;
+ --red: #b91c1c;
+ --red-fg: #f85149;
+
+ font-family: 'Segoe UI', system-ui, sans-serif;
+ font-size: 14px;
+ line-height: 1.55;
+ color: var(--text);
+ background: var(--bg);
+}
+
+* { box-sizing: border-box; }
+
+body { margin: 0; }
+
+a {
+ color: var(--accent-hi);
+ text-decoration: none;
+}
+a:hover { text-decoration: underline; }
+
+/* ── Header ──────────────────────────────────────── */
+.header {
+ background: #010409;
+ border-bottom: 1px solid var(--border);
+ padding: 0 1.5rem;
+ height: 52px;
+ display: flex;
+ align-items: center;
+ gap: 2rem;
+ position: sticky;
+ top: 0;
+ z-index: 100;
+}
+
+.header h1 {
+ margin: 0;
+ font-size: 1rem;
+ font-weight: 600;
+ color: var(--text);
+ letter-spacing: 0.02em;
+ white-space: nowrap;
+}
+
+.header nav { display: flex; gap: 0.25rem; }
+
+.header nav a {
+ color: var(--text-sub);
+ padding: 0.35rem 0.75rem;
+ border-radius: 6px;
+ font-size: 0.9rem;
+ transition: color 0.1s, background 0.1s;
+}
+
+.header nav a:hover {
+ color: var(--text);
+ background: var(--bg-raised);
+ text-decoration: none;
+}
+
+.header nav a.nav-active {
+ color: var(--text);
+ background: var(--bg-raised);
+ font-weight: 600;
+}
+
+.header h1 a {
+ color: inherit;
+}
+
+#main-nav {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.75rem 1rem;
+}
+
+.nav-user {
+ font-size: 0.85rem;
+}
+
+.btn-nav-logout {
+ padding: 0.3rem 0.65rem;
+ font-size: 0.85rem;
+}
+
+.auth-panel {
+ max-width: 28rem;
+}
+
+.row-inline {
+ flex-direction: row;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.users-table .users-actions {
+ white-space: nowrap;
+}
+
+.users-table .users-actions button {
+ margin-right: 0.35rem;
+ margin-bottom: 0.25rem;
+}
+
+.options-page .options-section-title {
+ margin: 0 0 0.5rem;
+ font-size: 1.05rem;
+ font-weight: 600;
+ color: var(--text);
+ text-transform: none;
+ letter-spacing: normal;
+}
+
+.options-page .options-section .muted code {
+ font-size: 0.85em;
+}
+
+/* LDAP-Synchronisation (Referenz-Layout) */
+.ldap-section {
+ padding: 0;
+ overflow: hidden;
+}
+
+.ldap-section-toggle {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: 0.85rem 1.25rem;
+ margin: 0;
+ border: none;
+ border-bottom: 1px solid var(--border);
+ background: var(--bg-raised);
+ color: var(--text);
+ font: inherit;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ text-align: left;
+}
+
+.ldap-section-toggle:hover {
+ background: var(--bg-hover);
+}
+
+.ldap-section-heading {
+ flex: 1;
+}
+
+.ldap-chevron {
+ font-size: 0.75rem;
+ color: var(--text-muted);
+ flex-shrink: 0;
+}
+
+.ldap-section-body {
+ padding: 1rem 1.25rem 1.25rem;
+}
+
+.ldap-section-body[hidden] {
+ display: none !important;
+}
+
+.ldap-subtitle {
+ margin: 0 0 1rem;
+ font-size: 0.95rem;
+ font-weight: 600;
+ color: var(--text-sub);
+}
+
+.ldap-sync-check {
+ margin-bottom: 1rem;
+}
+
+.form-grid-2 {
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ gap: 0.75rem 1rem;
+ margin-bottom: 0.75rem;
+}
+
+@media (max-width: 720px) {
+ .form-grid-2 {
+ grid-template-columns: 1fr;
+ }
+}
+
+.form-grid-ldap-attr {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 0.75rem 1rem;
+ margin-bottom: 0.75rem;
+}
+
+@media (max-width: 900px) {
+ .form-grid-ldap-attr {
+ grid-template-columns: 1fr;
+ }
+}
+
+.options-page label.full-width {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+ margin-bottom: 0.75rem;
+}
+
+.ldap-filter-ta {
+ font-family: ui-monospace, monospace;
+ font-size: 0.82rem;
+ line-height: 1.45;
+ min-height: 5rem;
+}
+
+.ldap-hint {
+ margin: -0.35rem 0 0.75rem;
+ font-size: 0.82rem;
+}
+
+.options-actions {
+ display: flex;
+ justify-content: flex-start;
+}
+
+.btn-config-save {
+ padding: 0.5rem 1.25rem;
+ font-weight: 600;
+}
+
+/* LDAP Sync-Panel (Optionen) */
+.sync-panel .sync-actions {
+ margin-bottom: 0.75rem;
+}
+
+.btn-ldap-sync-now {
+ padding: 0.5rem 1.1rem;
+ font-weight: 600;
+}
+
+.sync-last-line {
+ margin: 0 0 1rem;
+ font-size: 0.92rem;
+}
+
+.sync-log-title {
+ margin: 0 0 0.65rem;
+ font-size: 0.95rem;
+ font-weight: 600;
+ color: var(--text-sub);
+ text-transform: none;
+ letter-spacing: normal;
+}
+
+.sync-log-table-wrap {
+ overflow-x: auto;
+}
+
+table.sync-log-table {
+ font-size: 0.88rem;
+}
+
+table.sync-log-table th,
+table.sync-log-table td {
+ padding: 0.45rem 0.65rem;
+ border-bottom: 1px solid var(--border);
+ vertical-align: top;
+}
+
+table.sync-log-table th {
+ font-weight: 600;
+ color: var(--text-sub);
+ text-align: left;
+}
+
+table.sync-log-table td.num {
+ text-align: right;
+ font-variant-numeric: tabular-nums;
+}
+
+.sync-status-badge {
+ display: inline-block;
+ padding: 0.15rem 0.55rem;
+ border-radius: 999px;
+ font-size: 0.82rem;
+ font-weight: 600;
+}
+
+.sync-status-badge.sync-status-ok {
+ background: rgba(46, 160, 67, 0.25);
+ color: #3fb950;
+ border: 1px solid rgba(63, 185, 80, 0.45);
+}
+
+.sync-status-badge.sync-status-err {
+ background: rgba(248, 81, 73, 0.15);
+ color: var(--red, #f85149);
+ border: 1px solid rgba(248, 81, 73, 0.35);
+}
+
+.tv-device-row {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-end;
+ gap: 0.75rem;
+}
+
+.tv-device-row label {
+ flex: 1;
+ min-width: 12rem;
+}
+
+.tv-conn-hint {
+ margin: 0.25rem 0 0;
+ font-size: 0.85rem;
+}
+
+/* ── Layout ──────────────────────────────────────── */
+.main {
+ padding: 1.5rem 2rem;
+ max-width: 100%;
+}
+
+/* ── Card ────────────────────────────────────────── */
+.card {
+ background: var(--bg-card);
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ padding: 1rem 1.25rem;
+ margin-bottom: 1rem;
+}
+
+/* ── Stack / Row ─────────────────────────────────── */
+.stack {
+ display: flex;
+ flex-direction: column;
+ gap: 0.85rem;
+}
+
+/* [hidden] muss trotz .stack gelten (sonst bleibt z. B. #form-m sichtbar) */
+.stack[hidden] {
+ display: none !important;
+}
+
+.row {
+ display: flex;
+ gap: 0.75rem;
+ flex-wrap: wrap;
+ align-items: flex-end;
+}
+
+/* ── Typography ─────────────────────────────────── */
+h2 { margin: 0 0 0.5rem; font-size: 1.25rem; }
+h3, h4 { margin: 0 0 0.5rem; font-size: 0.95rem; color: var(--text-sub); font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; }
+
+.muted { color: var(--text-muted); font-size: 0.9rem; }
+.error { color: var(--red-fg); font-size: 0.9rem; }
+
+/* ── Forms ───────────────────────────────────────── */
+label {
+ display: flex;
+ flex-direction: column;
+ gap: 0.3rem;
+ font-size: 0.85rem;
+ font-weight: 500;
+ color: var(--text-sub);
+}
+
+input, select, textarea, button { font: inherit; }
+
+input, select, textarea {
+ padding: 0.45rem 0.65rem;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ color: var(--text);
+ min-width: 180px;
+ transition: border-color 0.1s;
+}
+
+input:focus, select:focus, textarea:focus {
+ outline: none;
+ border-color: var(--accent);
+}
+
+textarea { min-height: 72px; resize: vertical; }
+
+button {
+ padding: 0.45rem 0.9rem;
+ border-radius: 6px;
+ border: 1px solid var(--accent);
+ background: var(--accent);
+ color: #fff;
+ cursor: pointer;
+ font-weight: 500;
+ transition: opacity 0.12s;
+}
+button:hover { opacity: 0.85; }
+button.secondary {
+ background: transparent;
+ color: var(--accent-hi);
+ border-color: var(--border-hi);
+}
+button.secondary:hover { background: var(--bg-hover); opacity: 1; }
+button.danger { border-color: var(--red); background: var(--red); color: #fff; }
+button:disabled { opacity: 0.4; cursor: not-allowed; }
+
+/* ── Table (global) ──────────────────────────────── */
+table {
+ width: 100%;
+ border-collapse: collapse;
+ font-size: 0.92rem;
+}
+
+th, td {
+ text-align: left;
+ padding: 0.55rem 0.6rem;
+ border-bottom: 1px solid var(--border);
+}
+
+th {
+ font-size: 0.75rem;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ color: var(--text-sub);
+ font-weight: 600;
+}
+
+/* ── Badge ───────────────────────────────────────── */
+.badge {
+ display: inline-block;
+ padding: 0.15rem 0.55rem;
+ border-radius: 999px;
+ font-size: 0.72rem;
+ font-weight: 600;
+ letter-spacing: 0.03em;
+ background: var(--bg-raised);
+ color: var(--text-sub);
+ border: 1px solid var(--border-hi);
+}
+
+/* ── Timeline ────────────────────────────────────── */
+.timeline {
+ border-left: 2px solid var(--border);
+ margin-left: 0.35rem;
+ padding-left: 1rem;
+}
+.timeline-item { margin-bottom: 1rem; }
+.timeline-item time { font-size: 0.78rem; color: var(--text-muted); }
+
+/* ── Table-Wrap ──────────────────────────────────── */
+.table-wrap {
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+}
+
+/* ── Extras / Anlagen ────────────────────────────── */
+.extras-table { font-size: 0.88rem; }
+.extras-table th {
+ vertical-align: top;
+ font-weight: 600;
+ text-transform: none;
+ letter-spacing: normal;
+ color: var(--text-sub);
+ border-right: 1px solid var(--border);
+}
+/* Nur Key-Value-Extras: feste Schlüsselspalte (nicht Anlagenliste mit 3 Spalten) */
+.extras-table:not(.anlagen-voll) th {
+ width: 11rem;
+}
+
+.extras-cell-input {
+ width: 100%;
+ min-width: 0;
+ box-sizing: border-box;
+ font: inherit;
+ font-size: 0.88rem;
+ padding: 0.35rem 0.45rem;
+ border-radius: 4px;
+ border: 1px solid var(--border);
+ background: var(--bg);
+ color: var(--text);
+}
+.extras-table th .extras-cell-input {
+ font-weight: 500;
+ text-transform: none;
+ letter-spacing: normal;
+}
+
+.machine-detail-card-title {
+ margin-top: 0;
+ font-size: 1rem;
+ font-weight: 600;
+}
+
+.machine-detail-actions {
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.75rem;
+}
+
+.machine-detail-card-title + .machine-detail-actions {
+ margin-bottom: 0.75rem;
+}
+/* Anlagenliste: Gruppe + Beschreibung schmal aber lesbar, „Wert“ der Rest.
+ Kein width:100% auf der letzten Zelle — das drückt Spalte 2 auf Mindestbreite. */
+.anlagen-voll {
+ width: 100%;
+ table-layout: fixed;
+}
+/* Spaltenbreiten über colgroup: 1+2 kompakt, 3 bekommt den Rest (kein width:100% auf Zellen) */
+.anlagen-voll col.anlagen-col-gruppe {
+ width: 10rem;
+}
+.anlagen-voll col.anlagen-col-beschr {
+ width: 22%;
+ min-width: 11rem;
+}
+.anlagen-voll col.anlagen-col-wert {
+ width: auto;
+}
+.anlagen-voll:not(.gruppiert) thead th {
+ text-transform: none;
+ letter-spacing: normal;
+ font-size: 0.75rem;
+ vertical-align: bottom;
+}
+.anlagen-voll:not(.gruppiert) thead th:nth-child(1),
+.anlagen-voll:not(.gruppiert) tbody td:nth-child(1) {
+ vertical-align: top;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ font-size: 0.85rem;
+}
+.anlagen-voll:not(.gruppiert) thead th:nth-child(2),
+.anlagen-voll:not(.gruppiert) tbody th:nth-child(2) {
+ vertical-align: top;
+ max-width: 28rem;
+ white-space: normal;
+ word-break: break-word;
+ hyphens: auto;
+}
+.anlagen-voll:not(.gruppiert) thead th:nth-child(3),
+.anlagen-voll:not(.gruppiert) tbody td:nth-child(3) {
+ vertical-align: top;
+ word-break: break-word;
+}
+
+/* Gruppierte Anlagenliste: Box pro Gruppe, Kopfzeile = Gruppenname */
+.anlagen-voll.gruppiert col.anlagen-col-beschr {
+ width: 22%;
+ min-width: 11rem;
+}
+.anlagen-voll.gruppiert col.anlagen-col-wert {
+ width: auto;
+}
+.anlagen-voll.gruppiert thead th {
+ text-transform: none;
+ letter-spacing: normal;
+ font-size: 0.75rem;
+ vertical-align: bottom;
+}
+.anlagen-voll.gruppiert thead th:first-child,
+.anlagen-voll.gruppiert tbody.anlagen-gruppe tr:not(.anlagen-gruppe-kopf) th {
+ vertical-align: top;
+ max-width: 28rem;
+ white-space: normal;
+ word-break: break-word;
+ hyphens: auto;
+}
+.anlagen-voll.gruppiert thead th:last-child,
+.anlagen-voll.gruppiert tbody.anlagen-gruppe tr:not(.anlagen-gruppe-kopf) td {
+ vertical-align: top;
+ word-break: break-word;
+}
+.anlagen-voll.gruppiert tbody.anlagen-gruppe-spacer td {
+ height: 0.85rem;
+ padding: 0 !important;
+ border: none !important;
+ background: transparent !important;
+}
+.anlagen-voll.gruppiert tr.anlagen-gruppe-kopf td {
+ font-weight: 600;
+ font-size: 0.8rem;
+ text-transform: none;
+ letter-spacing: 0.02em;
+ color: var(--text);
+ background: var(--bg-raised);
+ border: 1px solid var(--border-hi);
+ border-bottom: 1px solid var(--border);
+ padding: 0.5rem 0.65rem;
+}
+.anlagen-voll.gruppiert tbody.anlagen-gruppe tr.anlagen-gruppe-kopf td:first-child {
+ border-radius: 8px 8px 0 0;
+}
+.anlagen-voll.gruppiert tbody.anlagen-gruppe tr:not(.anlagen-gruppe-kopf) th,
+.anlagen-voll.gruppiert tbody.anlagen-gruppe tr:not(.anlagen-gruppe-kopf) td {
+ border-left: 1px solid var(--border-hi);
+ border-right: 1px solid var(--border-hi);
+}
+.anlagen-voll.gruppiert tbody.anlagen-gruppe tr:not(.anlagen-gruppe-kopf) th {
+ border-right: 1px solid var(--border);
+}
+.anlagen-voll.gruppiert tbody.anlagen-gruppe tr:not(.anlagen-gruppe-kopf):last-child th:first-child {
+ border-bottom: 1px solid var(--border-hi);
+ border-bottom-left-radius: 8px;
+}
+.anlagen-voll.gruppiert tbody.anlagen-gruppe tr:not(.anlagen-gruppe-kopf):last-child td:last-child {
+ border-bottom: 1px solid var(--border-hi);
+ border-bottom-right-radius: 8px;
+}
+
+.main:has(.machines-overview) { max-width: 100%; }
+
+code {
+ font-size: 0.85em;
+ background: var(--bg-raised);
+ color: var(--text-sub);
+ padding: 0.1em 0.35em;
+ border-radius: 4px;
+}
+
+/* ═══════════════════════════════════════════════════
+ Startseite — Offene Tickets
+════════════════════════════════════════════════════ */
+.home-open-tickets { gap: 1rem; }
+
+.home-kpi-bar {
+ display: flex;
+ align-items: center;
+ gap: 1.25rem;
+ flex-wrap: wrap;
+}
+
+.home-kpi-pills {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.kpi-pill .badge {
+ font-size: 0.8rem;
+ padding: 0.2rem 0.65rem;
+}
+
+.home-open-tickets h2 {
+ font-size: 1.1rem;
+ color: var(--text);
+ font-weight: 600;
+ letter-spacing: 0;
+ text-transform: none;
+ margin: 0;
+}
+
+.home-ticket-list {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.home-ticket-card {
+ padding: 0;
+ overflow: hidden;
+}
+
+.home-ticket-head {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 0.75rem 1rem;
+ padding: 0.85rem 1.1rem;
+ border-bottom: 1px solid var(--border);
+ background: var(--bg-raised);
+}
+
+.home-ticket-head.home-ticket-head-overdue {
+ background-color: rgba(180, 40, 40, 0.12);
+ box-shadow: inset 3px 0 0 0 #b42828;
+}
+
+.home-ticket-collapse-btn {
+ flex-shrink: 0;
+ margin: 0;
+ padding: 0.2rem 0.35rem 0 0;
+ border: none;
+ background: transparent;
+ color: var(--text-muted);
+ font-size: 0.75rem;
+ line-height: 1.2;
+ cursor: pointer;
+ align-self: flex-start;
+}
+
+.home-ticket-collapse-btn:hover {
+ color: var(--accent-hi);
+}
+
+.home-ticket-head-main {
+ flex: 1;
+ min-width: 12rem;
+}
+
+.home-ticket-collapsible[hidden] {
+ display: none !important;
+}
+
+.home-ticket-titleline {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem 0.75rem;
+ font-size: 0.95rem;
+ margin-bottom: 0.35rem;
+}
+
+.home-ticket-titleline > a {
+ font-weight: 600;
+}
+
+.home-ticket-meta-row {
+ font-size: 0.8rem;
+ color: var(--text-muted);
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.35rem 1rem;
+}
+
+.home-ticket-open {
+ display: inline-block;
+ padding: 0.35rem 0.75rem;
+ border: 1px solid var(--accent);
+ border-radius: 6px;
+ font-size: 0.82rem;
+ font-weight: 500;
+ color: var(--accent-hi);
+ transition: background 0.1s;
+ flex-shrink: 0;
+ text-decoration: none;
+}
+
+.home-ticket-open:hover {
+ background: rgba(47, 129, 247, 0.12);
+ text-decoration: none;
+}
+
+.home-ticket-context {
+ padding: 0.85rem 1.1rem;
+ border-bottom: 1px solid var(--border);
+}
+
+.home-ticket-context-h {
+ margin: 0 0 0.4rem;
+ font-size: 0.82rem;
+}
+
+.home-ticket-desc {
+ white-space: pre-wrap;
+ word-break: break-word;
+ color: var(--text-sub);
+ font-size: 0.88rem;
+ line-height: 1.55;
+ margin: 0;
+}
+
+.home-ticket-machine {
+ margin: 0.65rem 0 0;
+ font-size: 0.85rem;
+ color: var(--text-sub);
+}
+
+.home-ticket-events {
+ padding: 0.75rem 1.1rem 1rem;
+ display: flex;
+ flex-direction: column;
+ gap: 0.65rem;
+}
+
+.home-ticket-no-events {
+ margin: 0;
+ font-size: 0.88rem;
+}
+
+.home-event-box {
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ padding: 0.65rem 0.85rem;
+ background: var(--bg);
+ font-size: 0.88rem;
+}
+
+.home-event-box-header {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ gap: 0.5rem 0.75rem;
+ margin-bottom: 0.45rem;
+}
+
+.home-event-box-time {
+ font-size: 0.78rem;
+ color: var(--text-muted);
+}
+
+.home-event-box-body :last-child {
+ margin-bottom: 0;
+}
+
+/* Status badge variants */
+.badge-open { background: rgba(35, 134, 54, 0.15); color: var(--green-fg); border-color: rgba(63, 185, 80, 0.3); }
+.badge-waiting { background: rgba(158, 106, 3, 0.15); color: var(--amber-fg); border-color: rgba(210, 153, 34, 0.3); }
+.badge-done { background: rgba(110, 118, 129, 0.12); color: var(--text-muted); border-color: var(--border); }
+
+/* Priority badge variants */
+.badge-high { background: rgba(185, 28, 28, 0.18); color: var(--red-fg); border-color: rgba(248, 81, 73, 0.35); }
+.badge-medium { background: rgba(158, 106, 3, 0.15); color: var(--amber-fg); border-color: rgba(210, 153, 34, 0.3); }
+.badge-low { background: rgba(35, 134, 54, 0.1); color: var(--green-fg); border-color: rgba(63, 185, 80, 0.2); }
+
+/* Ticket-Detail: Ereignisse als Tabelle (neueste zuerst) */
+.events-table {
+ font-size: 0.9rem;
+}
+
+.events-table thead th {
+ background: var(--bg-raised);
+ white-space: nowrap;
+}
+
+.events-table-time {
+ color: var(--text-sub);
+ font-variant-numeric: tabular-nums;
+ white-space: nowrap;
+ width: 1%;
+ vertical-align: top;
+}
+
+.events-table-desc {
+ color: var(--text-sub);
+ vertical-align: top;
+ line-height: 1.5;
+}
+
+.event-inhalt-block {
+ display: flex;
+ flex-direction: column;
+ gap: 0.5rem;
+}
+
+.event-inhalt-label {
+ margin: 0;
+ font-size: 0.72rem;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--text-muted);
+ font-weight: 600;
+}
+
+.event-inhalt-text {
+ white-space: pre-wrap;
+ word-break: break-word;
+ margin: 0;
+}
+
+.event-inhalt-meta {
+ margin: 0;
+ font-size: 0.88rem;
+}
+
+.event-artnr {
+ font-size: 0.95em;
+}
+
+.event-tv-placeholder {
+ font-size: 0.85em;
+}
+
+.ev-field-group[hidden] {
+ display: none !important;
+}
+
+.event-type-badge {
+ white-space: nowrap;
+}
+
+.event-type-note { background: rgba(88, 166, 255, 0.12); color: var(--accent-hi); border-color: rgba(88, 166, 255, 0.35); }
+.event-type-call { background: rgba(163, 113, 247, 0.14); color: #d2a8ff; border-color: rgba(163, 113, 247, 0.35); }
+.event-type-remote { background: rgba(35, 134, 54, 0.15); color: var(--green-fg); border-color: rgba(63, 185, 80, 0.3); }
+.event-type-part { background: rgba(210, 153, 34, 0.14); color: var(--amber-fg); border-color: rgba(210, 153, 34, 0.35); }
+.event-type-attachment { background: rgba(244, 143, 177, 0.12); color: #f48fb1; border-color: rgba(244, 143, 177, 0.35); }
+.event-type-system { background: rgba(110, 118, 129, 0.18); color: var(--text-muted); border-color: var(--border-hi); }
+
+.event-attachment-list {
+ margin: 0.5rem 0 0;
+ padding-left: 1.25rem;
+}
+.event-attachment-list a {
+ font-weight: 500;
+}
+
+/* Vorschau-Modal — kein display auf dem Basis-Selektor: sonst überschreibt flex das UA- display:none und der Dialog ist immer sichtbar */
+.attachment-preview-dialog {
+ border: 1px solid var(--border-hi, #444);
+ border-radius: 10px;
+ padding: 0;
+ max-width: min(960px, 96vw);
+ width: 96vw;
+ background: var(--bg, #0d1117);
+ color: var(--text, #e6edf3);
+ box-shadow: 0 16px 48px rgba(0, 0, 0, 0.45);
+}
+.attachment-preview-dialog[open] {
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+ height: min(90vh, 900px);
+ max-height: min(90vh, 900px);
+}
+.attachment-preview-dialog::backdrop {
+ background: rgba(0, 0, 0, 0.55);
+}
+.attachment-preview-inner {
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ min-height: 0;
+ overflow: hidden;
+}
+.attachment-preview-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem;
+ padding: 0.65rem 1rem;
+ border-bottom: 1px solid var(--border-hi, #444);
+ flex-shrink: 0;
+}
+.attachment-preview-title {
+ margin: 0;
+ font-size: 1rem;
+ font-weight: 600;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.attachment-preview-close {
+ flex-shrink: 0;
+ width: 2.25rem;
+ height: 2.25rem;
+ border: none;
+ border-radius: 6px;
+ background: var(--border-hi, #444);
+ color: inherit;
+ font-size: 1.35rem;
+ line-height: 1;
+ cursor: pointer;
+}
+.attachment-preview-close:hover {
+ filter: brightness(1.15);
+}
+.attachment-preview-body {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+ justify-content: center;
+}
+/* PDF: kein Scroll am äußeren Container — nur der Browser-PDF-Viewer im iframe scrollt */
+.attachment-preview-body--embed {
+ overflow: hidden;
+ padding: 0;
+}
+/* Bild, Text, Video, Fehler: ein gemeinsamer Scrollbereich */
+.attachment-preview-body--scroll {
+ overflow: auto;
+ padding: 0.75rem 1rem;
+ align-items: center;
+}
+.attachment-preview-img {
+ max-width: 100%;
+ max-height: min(75vh, 800px);
+ height: auto;
+ object-fit: contain;
+}
+.attachment-preview-iframe {
+ flex: 1;
+ min-height: 0;
+ width: 100%;
+ height: 100%;
+ border: none;
+ border-radius: 0;
+ background: #fff;
+}
+.attachment-preview-video {
+ max-width: 100%;
+ max-height: min(75vh, 800px);
+}
+.attachment-preview-audio {
+ width: 100%;
+ min-width: 280px;
+}
+.attachment-preview-text {
+ margin: 0;
+ align-self: stretch;
+ white-space: pre-wrap;
+ word-break: break-word;
+ font-size: 0.85rem;
+ line-height: 1.45;
+ max-height: min(75vh, 800px);
+ overflow: auto;
+ text-align: left;
+}
+.attachment-preview-footer {
+ padding: 0.65rem 1rem;
+ border-top: 1px solid var(--border-hi, #444);
+ flex-shrink: 0;
+}
+.attachment-preview-loading {
+ margin: 0;
+}
diff --git a/public/index.html b/public/index.html
index f095543..bbc8753 100644
--- a/public/index.html
+++ b/public/index.html
@@ -4,14 +4,12 @@
SDS CRM
-
+
+
-
-
-
+ Weiter zur Startseite