Compare commits
477 Commits
main
...
033e74adda
| Author | SHA1 | Date | |
|---|---|---|---|
| 033e74adda | |||
| ccc66ebd0f | |||
| c660f62120 | |||
| ad74825781 | |||
| f1e0f36cca | |||
| 526ad966c4 | |||
| 99501fb924 | |||
| 2fdc89565c | |||
| 427c0580c4 | |||
| 47a9af26fe | |||
| 42d2d7dc0e | |||
| 6e0868582a | |||
| 7a6bb4552e | |||
| 0ecdff4d7d | |||
| 0ea4efdc43 | |||
| c854b0bf30 | |||
| a26ff30cd8 | |||
| e9504e21e7 | |||
| 822dcdcfb9 | |||
| 78f9bd3f7a | |||
| 4aea8b94c3 | |||
| 79d620d9c1 | |||
| 8d821a6802 | |||
| 363163f741 | |||
| 384ea95fe5 | |||
| 2a5071b15a | |||
| f2055d59eb | |||
| b91c9d0fd8 | |||
| 29a9e2b63b | |||
| 5264cf54ac | |||
| f002ad867a | |||
| cb09651d8d | |||
| b59599cb92 | |||
| 154d7060f8 | |||
| a34bf43756 | |||
| 2c96b9c5a5 | |||
| d45cefbc20 | |||
| 4fd2eb9c40 | |||
| 8697810127 | |||
| 358cd906ae | |||
| be4a5caaec | |||
| f2adc21fea | |||
| 7239ad92e4 | |||
| 347319aee3 | |||
| 21e2bc2755 | |||
| c699d2ade8 | |||
| 51e0ae95b1 | |||
| 45ca4f7327 | |||
| 38ccdd058b | |||
| 7f47821a7f | |||
| f150cfd740 | |||
| 00da371dfb | |||
| c56fb6b571 | |||
| 0328a4586a | |||
| d73209a150 | |||
| 4bcc2152ab | |||
| ab4055f2a5 | |||
| d6582dd767 | |||
| 3d5bec4ef8 | |||
| 5963a9280c | |||
| cacfce4d15 | |||
| 5400fd7ad5 | |||
| 0d0dc33e84 | |||
| 0e2e4a36be | |||
| 5403418c42 | |||
| ab0f892bc1 | |||
| 69874742f8 | |||
| 736f7bba88 | |||
| ff4328f264 | |||
| 71f5763f7b | |||
| 5a4de421ce | |||
| 19bab852de | |||
| 8a2429827c | |||
| 3594dc69e8 | |||
| 25e0c5389c | |||
| 520052e71a | |||
| da9cad1513 | |||
| b44c8d453a | |||
| bbbdc4d2ae | |||
| b15d98f6e9 | |||
| f36cbcc207 | |||
| 76764eb4c3 | |||
| 0bd0120ec2 | |||
| 266c07d820 | |||
| cc34acac3e | |||
| 31b8378b87 | |||
| 33ff46744f | |||
| c44d8e172d | |||
| bddb326e18 | |||
| 81cecad668 | |||
| 7950315291 | |||
| 9da30ac2e8 | |||
| fb1ccf91b9 | |||
| 42bed16e25 | |||
| 30cbc18b3a | |||
| b6705e84a7 | |||
| d26fe6dcef | |||
| 63bf57e720 | |||
| 1240ffd03b | |||
| 7a893dfdcb | |||
| beb91bf5c3 | |||
| 9bdd725691 | |||
| 821a5f85de | |||
| b667a856d4 | |||
| f6fb607008 | |||
| ee6c2d7420 | |||
| 9e7b5bc0b9 | |||
| ba12c46c88 | |||
| d99cddf5b5 | |||
| df32bf516b | |||
| 151f605eb0 | |||
| 4347a0858d | |||
| 8196f8a955 | |||
| e9bfa3dc1c | |||
| 88006be691 | |||
| fe23742912 | |||
| 6abc0dd772 | |||
| 655a78392b | |||
| 10f03e151d | |||
| 4b85ea3d2d | |||
| 8bed6e2984 | |||
| 9c1d3bc04c | |||
| 8df587f9e2 | |||
| 3796bc2953 | |||
| 2278dfa714 | |||
| 1a5c69fcfb | |||
| a671ae392d | |||
| 4c58d175a0 | |||
| bc655f0e06 | |||
| 22bcf01fa8 | |||
| bf8a3386d7 | |||
| d182231448 | |||
| 0a32ae77cd | |||
| 98c95483d8 | |||
| bcde1da84f | |||
| 6157d7e27d | |||
| 0cfa6a691b | |||
| 14470da7dc | |||
| 85ac33c334 | |||
| 9e38a488c8 | |||
| ed6283b9e1 | |||
| 2d2e8552f0 | |||
| 25ed99b356 | |||
| 6cc7dc87ad | |||
| 697abc99fa | |||
| bace26c084 | |||
| 274f3d5795 | |||
| 168d2fce6e | |||
| 6dcd8b1863 | |||
| 81ce9d263d | |||
| 6455be81bd | |||
| 9cde630562 | |||
| 48d101e139 | |||
| 167e9a40c3 | |||
| f9d3f10eae | |||
| 6d9bceb63f | |||
| e29e84898b | |||
| 1ccabbedcd | |||
| 24febf4c95 | |||
| 5fc7cc9604 | |||
| 941f1d819b | |||
| 58c47fa8f7 | |||
| ea392af094 | |||
| 0ac22d346f | |||
| 26ffc4421a | |||
| 7caa37378b | |||
| 227a88b24b | |||
| 0fb469c9b0 | |||
| 5b3445c2dc | |||
| 716de8a503 | |||
| 817d0e814b | |||
| 75d5e2b72d | |||
| 30aaf141c7 | |||
| c7ba7a9cc5 | |||
| 1c68e6b724 | |||
| cc3c405473 | |||
| ff70338b21 | |||
| bb3b842be1 | |||
| 9c6a6a841a | |||
| b7b913035e | |||
| 454e9ee3c9 | |||
| 01846d488b | |||
| 80b2b1d097 | |||
| f5755993b5 | |||
| 0f56102030 | |||
| 60d846baa9 | |||
| a28b910d4d | |||
| 4aeefb2b83 | |||
| 24c09d7891 | |||
| 77eabe7e18 | |||
| e4073e01ad | |||
| 248da3412c | |||
| c93ea4284d | |||
| 7c68ce61f2 | |||
| f6dd37b458 | |||
| bb54a8779e | |||
| 6e14f48770 | |||
| 4d24e3a657 | |||
| f33ccf730a | |||
| 8824b1c9c8 | |||
| 571c24f250 | |||
| b03af21e97 | |||
| b1e102ca5d | |||
| 8b40be7909 | |||
| 655459a46b | |||
| 5fca7792a2 | |||
| 30b6ffcc20 | |||
| 7f66f66cfa | |||
| d0de3cb92e | |||
| c893574cb1 | |||
| eb2dd03ef9 | |||
| b322d0c173 | |||
| 54ae136f0d | |||
| 00e1e88dd9 | |||
| 3984e218db | |||
| d9c3c8d07c | |||
| c6a0d59c29 | |||
| 9592e2b062 | |||
| d522cbb49d | |||
| 8d7bc2e97c | |||
| 44017a768b | |||
| 683d073b6e | |||
| cb939f2197 | |||
| 0e71899c57 | |||
| 6b82f2b629 | |||
| 9ba5f26efc | |||
| 82f2143dd1 | |||
| 4e49dd18a1 | |||
| 252021acee | |||
| eae321b364 | |||
| 5a2682c835 | |||
| 34f537238e | |||
| 64df33f0fa | |||
| 94ab3350ec | |||
| aa162dcad3 | |||
| c42e57494a | |||
| 582af62fcb | |||
| 743c0e8772 | |||
| d4c39d7d44 | |||
| e60188f043 | |||
| ca4f1ba1c0 | |||
| 5fe823f52a | |||
| 1969610130 | |||
| 2bf52b35fe | |||
| f01881a6ce | |||
| a185c6eb11 | |||
| a8450fc0c6 | |||
| 0f5275b870 | |||
| 4f37811dcc | |||
| d7eced3e77 | |||
| 6a5c1e844d | |||
| 5dc44e571f | |||
| 2b1a9a456b | |||
| bf5d7aaed2 | |||
| e166248c0d | |||
| cba4ea52e8 | |||
| 0f14f7ac3d | |||
| 2d26cedaa3 | |||
| d5aed2140e | |||
| cfc5efb556 | |||
| 898a5459fa | |||
| 3b7bcb7940 | |||
| 2aaff0088e | |||
| e9bbc196f7 | |||
| 20818beb3a | |||
| 6aa69cb68b | |||
| a021d3d15c | |||
| bb61caed6d | |||
| d3ab03da7e | |||
| 5869f88c1a | |||
| 50c76b67c7 | |||
| f4edcc2d44 | |||
| 35ef3a7cf8 | |||
| 4783971000 | |||
| c085b1e4d5 | |||
| 46b08b29b9 | |||
| 5cc41f9a2d | |||
| edec670ee0 | |||
| 41e5a4021b | |||
| 9c608cbf71 | |||
| 543952dbf8 | |||
| 2f7819e309 | |||
| 7799cbce80 | |||
| 0284ea8726 | |||
| 743bf0660c | |||
| df4b591be4 | |||
| 86e0743cbb | |||
| aaf91ea15e | |||
| cb71e9d294 | |||
| 75148b2718 | |||
| 81b4eee1e8 | |||
| 0fbda27609 | |||
| 3562d55a12 | |||
| 6224a25c38 | |||
| 63b1c563c1 | |||
| 76f86e87c1 | |||
| 8c458f4953 | |||
| d704e343fc | |||
| 4882da0d35 | |||
| 1908a6441d | |||
| a4735818fb | |||
| 4fb3d3c8a0 | |||
| 30dc99e4e0 | |||
| 9fea18b215 | |||
| 75c15c14c4 | |||
| b27b00f59c | |||
| 1637d4bd91 | |||
| 8114a8c645 | |||
| 0b7d20d946 | |||
| 849e24092e | |||
| 6fcaf3f65c | |||
| dce0046e63 | |||
| 02b5769049 | |||
| f125617af0 | |||
| d9e5df07bf | |||
| 7996c746c3 | |||
| f679eb3624 | |||
| 7ad44544cf | |||
| 669bcd93ab | |||
| aee45e29fd | |||
| 42e0d7b35e | |||
| f6c9875320 | |||
| 05f3b678c4 | |||
| eb718021fd | |||
| 01b4d0f973 | |||
| c29494dc0d | |||
| 809a37a410 | |||
| 232e3f3260 | |||
| b2657f5d52 | |||
| cee0e1fa7d | |||
| 7dea2de7f3 | |||
| 4db753d34a | |||
| e0e99ba6f5 | |||
| ace2213cc4 | |||
| 7e6c5cc189 | |||
| 7c644c941a | |||
| 11a242d70d | |||
| 9f665fc3b8 | |||
| 03bcc1a939 | |||
| 68b2cbb0ee | |||
| b009ac845f | |||
| cfd84b773f | |||
| 8038f03406 | |||
| 6c3c318f86 | |||
| 8dfcffc92b | |||
| 9ecacdab50 | |||
| 44fb50b11e | |||
| 23c4d21f44 | |||
| 6f77bccd85 | |||
| be336a51ab | |||
| ac2e2fcfe9 | |||
| 9dbb194c8a | |||
| 0aacb18aaa | |||
| e3a1636018 | |||
| 55bb2589a4 | |||
| 05d99e9e7d | |||
| 7e0a2f5e4f | |||
| 84c174ca09 | |||
| a9d3d0038f | |||
| 003d88587a | |||
| 69ff646689 | |||
| 1511340f00 | |||
| 62accb5a86 | |||
| 8c935c6101 | |||
| f6bdf2906f | |||
| dff3a23c04 | |||
| 966c121cbf | |||
| da50782ffc | |||
| 6919de096a | |||
| 8892b36ae5 | |||
| 8a08147265 | |||
| 52c182cb5f | |||
| 9cef3964e9 | |||
| cf0fb724a2 | |||
| bbb893dd6c | |||
| 724f152d70 | |||
| 27be8241bf | |||
| d27e437ba6 | |||
| f5253b29f4 | |||
| 0141a243ce | |||
| a0e1b8c0eb | |||
| 45fb45845a | |||
| 409db82368 | |||
| 30d761f899 | |||
| 70636f6ac5 | |||
| 59392a723c | |||
| c782492ab5 | |||
| 844af30b18 | |||
| 6fded3993a | |||
| f26d6bd4f3 | |||
| 2621cc0d8d | |||
| a8238dc9ba | |||
| 49d35f080d | |||
| 189a52b3cd | |||
| 3f8ce5daf7 | |||
| 087ba1126e | |||
| db4e9612a0 | |||
| cb4917c536 | |||
| 9f32eb5439 | |||
| f596b46364 | |||
| 117da523d2 | |||
| c2901dc0a9 | |||
| 8c2a8a7998 | |||
| 1dc74947f4 | |||
| f63e793c88 | |||
| 29a84b899d | |||
| be706a70f8 | |||
| 474b3e762c | |||
| f793d4cce6 | |||
| c3f46cd184 | |||
| 6bf336356d | |||
| 55699da42c | |||
| 053f184a33 | |||
| 6541cb2adf | |||
| 7dca84947e | |||
| 45fd6fda08 | |||
| 31e80fb386 | |||
| 7ea28cc6c0 | |||
| c0faa398b8 | |||
| 19be1f0d03 | |||
| c43d3225e3 | |||
| 7125d15b3f | |||
| 4b7cf171c8 | |||
| 59fdedfaa0 | |||
| 71d249d8bf | |||
| e496a62b36 | |||
| 0bfef0806b | |||
| 5c69388f1c | |||
| 7ed0388acb | |||
| 3aa0c7d77a | |||
| 77aa277347 | |||
| 2fff1ca8a8 | |||
| e58929d9a0 | |||
| 90560ecd2c | |||
| b07953fb7d | |||
| 01ef3c5a42 | |||
| 2aed851224 | |||
| c56fcfbd14 | |||
| ca2020b9c6 | |||
| c87212d54a | |||
| db22d47900 | |||
| 143485e107 | |||
| c1d4b24418 | |||
| 9655d4fa05 | |||
| 4efe452f1c | |||
| cb21a85736 | |||
| d2b70e5883 | |||
| 1a065b649c | |||
| 34c58c3755 | |||
| 37d8a414d3 | |||
| 7f4f232c32 | |||
| d6f257bcc6 | |||
| 3109f4d5ff | |||
| 235b33ae08 | |||
| 2d135b7068 | |||
| 8831320a4c | |||
| 000d409e4d | |||
| 160124a184 | |||
| 26dad422ec | |||
| e59cbade53 | |||
| 6423886930 | |||
| 6adf09faa0 | |||
| d7f3920763 | |||
| 3af92ebf71 | |||
| 5ab90830a0 | |||
| 4f72919269 | |||
| f2c9dcc900 | |||
| b4ec792cc0 | |||
| 9b3f48defe | |||
| 5edc90bd4d | |||
| d140251aa0 | |||
| e7fb2df5c7 | |||
| f27fd3f6da | |||
| d3e2b106af | |||
| 769d2059ca | |||
| 53349fae83 | |||
| d8eb1559c8 |
166
.env.example
Normal file
166
.env.example
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
# FEDEO Selfhosting
|
||||||
|
DOMAIN=app.example.com
|
||||||
|
CONTACT_EMAIL=admin@deine-domain.de
|
||||||
|
|
||||||
|
DB_NAME=fedeo
|
||||||
|
DB_USER=fedeo
|
||||||
|
DB_PASSWORD=change-this-db-password
|
||||||
|
DATABASE_URL=postgres://fedeo:change-this-db-password@db:5432/fedeo
|
||||||
|
|
||||||
|
MINIO_ROOT_USER=fedeo-minio
|
||||||
|
MINIO_ROOT_PASSWORD=change-this-minio-password
|
||||||
|
MINIO_BUCKET=fedeo
|
||||||
|
|
||||||
|
HOST=0.0.0.0
|
||||||
|
PORT=3100
|
||||||
|
FEDEO_RUN_MIGRATIONS=true
|
||||||
|
COOKIE_SECRET=change-this-cookie-secret
|
||||||
|
JWT_SECRET=change-this-jwt-secret
|
||||||
|
ENCRYPTION_KEY=change-this-encryption-key
|
||||||
|
|
||||||
|
MAILER_SMTP_HOST=smtp.example.com
|
||||||
|
MAILER_SMTP_PORT=587
|
||||||
|
MAILER_SMTP_SSL=false
|
||||||
|
MAILER_SMTP_USER=mailer@example.com
|
||||||
|
MAILER_SMTP_PASS=change-this-mail-password
|
||||||
|
MAILER_FROM=FEDEO <no-reply@example.com>
|
||||||
|
|
||||||
|
# Desktop Push per Web Push. Schlüssel können mit
|
||||||
|
# `npx web-push generate-vapid-keys` erzeugt werden.
|
||||||
|
WEB_PUSH_PUBLIC_KEY=replace-this-web-push-public-key
|
||||||
|
WEB_PUSH_PRIVATE_KEY=replace-this-web-push-private-key
|
||||||
|
WEB_PUSH_SUBJECT=mailto:admin@example.com
|
||||||
|
|
||||||
|
S3_ENDPOINT=http://minio:9000
|
||||||
|
S3_REGION=eu-central-1
|
||||||
|
S3_ACCESS_KEY=fedeo-minio
|
||||||
|
S3_SECRET_KEY=change-this-minio-password
|
||||||
|
S3_BUCKET=fedeo
|
||||||
|
|
||||||
|
# Datei-Backend. S3 bleibt aktuell der Standard; Seafile kann als externer
|
||||||
|
# Dateidienst angebunden werden, sobald der Backend-Umbau aktiviert ist.
|
||||||
|
FEDEO_FILE_BACKEND=s3
|
||||||
|
|
||||||
|
# Externer Seafile-Dienst, nicht Teil des Standard-Compose-Stacks.
|
||||||
|
SEAFILE_BASE_URL=https://files.example.com
|
||||||
|
SEAFILE_INTERNAL_URL=https://files.example.com
|
||||||
|
SEAFILE_ADMIN_EMAIL=admin@example.com
|
||||||
|
SEAFILE_ADMIN_PASSWORD=change-this-seafile-admin-password
|
||||||
|
|
||||||
|
M2M_API_KEY=change-this-m2m-key
|
||||||
|
API_BASE_URL=https://app.example.com/backend
|
||||||
|
GOCARDLESS_BASE_URL=https://api.gocardless.com
|
||||||
|
GOCARDLESS_SECRET_ID=replace-this
|
||||||
|
GOCARDLESS_SECRET_KEY=replace-this
|
||||||
|
|
||||||
|
DOKUBOX_IMAP_HOST=imap.example.com
|
||||||
|
DOKUBOX_IMAP_PORT=993
|
||||||
|
DOKUBOX_IMAP_SECURE=true
|
||||||
|
DOKUBOX_IMAP_USER=dokubox@example.com
|
||||||
|
DOKUBOX_IMAP_PASSWORD=change-this-imap-password
|
||||||
|
|
||||||
|
OPENAI_API_KEY=replace-this
|
||||||
|
STIRLING_API_KEY=replace-this
|
||||||
|
NUXT_PUBLIC_PDF_LICENSE=replace-with-your-pdf-license
|
||||||
|
|
||||||
|
# Interner Prometheus Node Exporter für die Admin-Systemstatusseite.
|
||||||
|
NODE_EXPORTER_URL=http://node-exporter:9100
|
||||||
|
|
||||||
|
# Lokaler Asterisk-Test für SIP/Voice. Aktivieren, wenn das Compose-Profil
|
||||||
|
# `telephony-dev` genutzt wird.
|
||||||
|
TELEPHONY_ENABLED=false
|
||||||
|
ASTERISK_IMAGE=andrius/asterisk:20
|
||||||
|
TELEPHONY_ASTERISK_HTTP_URL=http://asterisk-dev:8088/ws
|
||||||
|
TELEPHONY_ASTERISK_WS_URL=ws://localhost:8088/ws
|
||||||
|
TELEPHONY_ASTERISK_GENERATED_DIR=/var/lib/fedeo/asterisk/generated
|
||||||
|
TELEPHONY_ASTERISK_AMI_HOST=asterisk-dev
|
||||||
|
TELEPHONY_ASTERISK_AMI_PORT=5038
|
||||||
|
TELEPHONY_ASTERISK_AMI_USER=fedeo
|
||||||
|
TELEPHONY_ASTERISK_AMI_PASSWORD=fedeo-ami-dev
|
||||||
|
TELEPHONY_SIP_DOMAIN=localhost
|
||||||
|
TELEPHONY_TEST_EXTENSION=1001
|
||||||
|
TELEPHONY_TEST_PASSWORD=fedeo-test-1001
|
||||||
|
TELEPHONY_TEST_EXTENSION_2=1002
|
||||||
|
TELEPHONY_TEST_PASSWORD_2=fedeo-test-1002
|
||||||
|
TELEPHONY_ECHO_EXTENSION=600
|
||||||
|
TELEPHONY_DEV_WS_PORT=8088
|
||||||
|
TELEPHONY_DEV_AMI_PORT=5038
|
||||||
|
TELEPHONY_DEV_SIP_PORT=5060
|
||||||
|
TELEPHONY_DEV_RTP_MIN_PORT=10000
|
||||||
|
TELEPHONY_DEV_RTP_MAX_PORT=10100
|
||||||
|
TELEPHONY_ASTERISK_EXTERNAL_SIGNALING_ADDRESS=
|
||||||
|
TELEPHONY_ASTERISK_EXTERNAL_MEDIA_ADDRESS=
|
||||||
|
|
||||||
|
# Externe Telefonie über Telekom/tel.t-online.de. Keine echten Zugangsdaten
|
||||||
|
# einchecken. SIP-ID ist in der Regel die Rufnummer mit Vorwahl ohne Leerzeichen
|
||||||
|
# und ohne Sonderzeichen, z. B. 0301234567. Wenn dein Anschluss noch die
|
||||||
|
# Internet-Zugangsdaten als Auth-User nutzt, kann TELEPHONY_TELEKOM_AUTH_USER
|
||||||
|
# aus Anschlusskennung + Zugangsnummer + # + Mitbenutzernummer + @t-online.de
|
||||||
|
# gebildet werden.
|
||||||
|
TELEPHONY_EXTERNAL_PROVIDER=
|
||||||
|
TELEPHONY_EXTERNAL_ENABLED=false
|
||||||
|
TELEPHONY_EXTERNAL_INBOUND_EXTENSION=1001
|
||||||
|
TELEPHONY_TELEKOM_ENABLED=false
|
||||||
|
TELEPHONY_TELEKOM_REGISTRAR=tel.t-online.de
|
||||||
|
TELEPHONY_TELEKOM_SIP_USER=
|
||||||
|
TELEPHONY_TELEKOM_AUTH_USER=
|
||||||
|
TELEPHONY_TELEKOM_PASSWORD=
|
||||||
|
TELEPHONY_TELEKOM_CALLER_ID=
|
||||||
|
TELEPHONY_TELEKOM_INBOUND_EXTENSION=1001
|
||||||
|
TELEPHONY_TELEKOM_OUTBOUND_PREFIX=0
|
||||||
|
|
||||||
|
# Optionaler Erststart-Bootstrap. Wenn gesetzt, werden Admin, Mandant,
|
||||||
|
# Admin-Rolle und Grunddaten beim Backend-Start idempotent angelegt.
|
||||||
|
FEDEO_BOOTSTRAP_ADMIN_EMAIL=admin@example.com
|
||||||
|
FEDEO_BOOTSTRAP_ADMIN_PASSWORD=change-this-admin-password
|
||||||
|
FEDEO_BOOTSTRAP_ADMIN_FIRST_NAME=Admin
|
||||||
|
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME=Benutzer
|
||||||
|
FEDEO_BOOTSTRAP_TENANT_NAME=Mein Unternehmen
|
||||||
|
FEDEO_BOOTSTRAP_TENANT_SHORT=MEIN
|
||||||
|
FEDEO_BOOTSTRAP_MATRIX=true
|
||||||
|
|
||||||
|
# FEDEO Matrix-Kommunikation
|
||||||
|
#
|
||||||
|
# Diese Werte werden von docker-compose.selfhost.yml für den integrierten
|
||||||
|
# Matrix-Stack gelesen. Für produktive Systeme müssen alle Geheimnisse ersetzt
|
||||||
|
# werden.
|
||||||
|
|
||||||
|
MATRIX_SERVER_NAME=app.example.com
|
||||||
|
|
||||||
|
MATRIX_POSTGRES_DB=synapse
|
||||||
|
MATRIX_POSTGRES_USER=synapse
|
||||||
|
MATRIX_POSTGRES_PASSWORD=change-this-matrix-db-password
|
||||||
|
|
||||||
|
MATRIX_TURN_SHARED_SECRET=change-this-turn-secret
|
||||||
|
|
||||||
|
LIVEKIT_KEY=fedeo-livekit
|
||||||
|
LIVEKIT_SECRET=change-this-livekit-secret-please-replace
|
||||||
|
|
||||||
|
# Backend-Integration im Selfhost-Stack
|
||||||
|
MATRIX_HOMESERVER_URL=http://matrix-synapse:8008
|
||||||
|
MATRIX_RTC_HOST=app.example.com
|
||||||
|
MATRIX_RTC_JWT_URL=https://app.example.com/livekit/jwt
|
||||||
|
MATRIX_LIVEKIT_URL=wss://app.example.com/livekit/sfu
|
||||||
|
MATRIX_REGISTRATION_SHARED_SECRET=change-this-matrix-registration-secret
|
||||||
|
MATRIX_SERVICE_USER_LOCALPART=fedeo_service
|
||||||
|
NUXT_PUBLIC_MATRIX_ELEMENT_URL=https://app.example.com/element
|
||||||
|
|
||||||
|
# Lokale Matrix-Entwicklung
|
||||||
|
MATRIX_DEV_SYNAPSE_PORT=8008
|
||||||
|
MATRIX_DEV_ELEMENT_PORT=8080
|
||||||
|
MATRIX_DEV_RTC_JWT_PORT=8081
|
||||||
|
MATRIX_DEV_LIVEKIT_PORT=7880
|
||||||
|
MATRIX_DEV_LIVEKIT_TCP_PORT=7881
|
||||||
|
MATRIX_DEV_LIVEKIT_RTC_MIN_PORT=50000
|
||||||
|
MATRIX_DEV_LIVEKIT_RTC_MAX_PORT=50100
|
||||||
|
MATRIX_DEV_LIVEKIT_NODE_IP=127.0.0.1
|
||||||
|
MATRIX_DEV_TURN_PORT=3478
|
||||||
|
MATRIX_DEV_TURN_MIN_PORT=49160
|
||||||
|
MATRIX_DEV_TURN_MAX_PORT=49200
|
||||||
|
|
||||||
|
# Lokale Backend-Integration gegen den Matrix-Entwicklungsstack
|
||||||
|
# MATRIX_HOMESERVER_URL=http://localhost:8008
|
||||||
|
# MATRIX_RTC_JWT_URL=http://localhost:8081
|
||||||
|
# MATRIX_LIVEKIT_URL=ws://localhost:7880
|
||||||
|
# MATRIX_REGISTRATION_SHARED_SECRET=copy-from-matrix-dev-synapse-homeserver-yaml
|
||||||
|
# NUXT_PUBLIC_MATRIX_ELEMENT_URL=http://localhost:8080
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
name: Build and Push Docker Images
|
name: Build and Push Docker Images
|
||||||
run-name: Build Backend & Frontend by @${{ github.actor }}
|
run-name: Build Backend, Frontend, Website & Docs by @${{ github.actor }}
|
||||||
|
|
||||||
on: [push]
|
on: [push]
|
||||||
|
|
||||||
@@ -8,12 +8,38 @@ env:
|
|||||||
# Wenn du die Gitea-interne Registry nutzt, ist es meist einfach der Hostname deiner Gitea-Instanz.
|
# Wenn du die Gitea-interne Registry nutzt, ist es meist einfach der Hostname deiner Gitea-Instanz.
|
||||||
# Beispiel: gitea.deine-domain.de
|
# Beispiel: gitea.deine-domain.de
|
||||||
REGISTRY_HOST: git.federspiel.tech
|
REGISTRY_HOST: git.federspiel.tech
|
||||||
# Der Name des Repos (z.B. user/repo)
|
# Der Name des Repos (z.B. user/repo).
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
# Explizit in lowercase gesetzt, damit es exakt zu den Compose-Imagepfaden passt.
|
||||||
|
IMAGE_NAME: flfeders/fedeo
|
||||||
ACTOR: flfeders
|
ACTOR: flfeders
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
# verify-docs-sync:
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# steps:
|
||||||
|
# - name: Check out repository code
|
||||||
|
# uses: actions/checkout@v3
|
||||||
|
#
|
||||||
|
# - name: Prüfe Node-Version
|
||||||
|
# uses: actions/setup-node@v4
|
||||||
|
# with:
|
||||||
|
# node-version: 20
|
||||||
|
#
|
||||||
|
# - name: Synchronisiere Funktionsdokumentation
|
||||||
|
# run: node docs/scripts/sync-funktionsdoku.mjs
|
||||||
|
#
|
||||||
|
# - name: Breche ab, wenn Doku nicht aktuell committed ist
|
||||||
|
# run: |
|
||||||
|
# if [ -n "$(git status --porcelain docs/)" ]; then
|
||||||
|
# echo "Die generierte Dokumentation ist nicht aktuell."
|
||||||
|
# echo "Bitte lokal ausführen: node docs/scripts/sync-funktionsdoku.mjs"
|
||||||
|
# echo "Danach die Änderungen committen."
|
||||||
|
# git status --short docs/
|
||||||
|
# exit 1
|
||||||
|
# fi
|
||||||
|
|
||||||
build-backend:
|
build-backend:
|
||||||
|
#needs: verify-docs-sync
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository code
|
- name: Check out repository code
|
||||||
@@ -46,6 +72,7 @@ jobs:
|
|||||||
labels: ${{ steps.meta-backend.outputs.labels }}
|
labels: ${{ steps.meta-backend.outputs.labels }}
|
||||||
|
|
||||||
build-frontend:
|
build-frontend:
|
||||||
|
#needs: verify-docs-sync
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repository code
|
- name: Check out repository code
|
||||||
@@ -75,3 +102,68 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
tags: ${{ steps.meta-frontend.outputs.tags }}
|
tags: ${{ steps.meta-frontend.outputs.tags }}
|
||||||
labels: ${{ steps.meta-frontend.outputs.labels }}
|
labels: ${{ steps.meta-frontend.outputs.labels }}
|
||||||
|
|
||||||
|
build-docs:
|
||||||
|
needs: verify-docs-sync
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY_HOST }}
|
||||||
|
username: ${{ env.ACTOR }}
|
||||||
|
password: ${{ vars.CI_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docs
|
||||||
|
id: meta-docs
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY_HOST }}/${{ env.IMAGE_NAME }}/docs
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
|
- name: Build and push Docs
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./docs-site/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta-docs.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta-docs.outputs.labels }}
|
||||||
|
|
||||||
|
build-website:
|
||||||
|
#needs: verify-docs-sync
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Check out repository code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Log in to Docker Registry
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY_HOST }}
|
||||||
|
username: ${{ env.ACTOR }}
|
||||||
|
password: ${{ vars.CI_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Website
|
||||||
|
id: meta-website
|
||||||
|
uses: docker/metadata-action@v4
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY_HOST }}/${{ env.IMAGE_NAME }}/website
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
|
||||||
|
|
||||||
|
- name: Build and push Website
|
||||||
|
uses: docker/build-push-action@v4
|
||||||
|
with:
|
||||||
|
context: ./website
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta-website.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta-website.outputs.labels }}
|
||||||
|
|||||||
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.env
|
||||||
|
node_modules/
|
||||||
|
.nuxt/
|
||||||
|
.output/
|
||||||
|
|
||||||
|
# Lokale Runtime-Daten und generierte Konfigurationen
|
||||||
|
matrix/postgres/
|
||||||
|
matrix/synapse/
|
||||||
|
matrix/dev/postgres/
|
||||||
|
matrix/dev/synapse/
|
||||||
8
.idea/.gitignore
generated
vendored
Normal file
8
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
12
.idea/FEDEO.iml
generated
Normal file
12
.idea/FEDEO.iml
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/temp" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/tmp" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/FEDEO.iml" filepath="$PROJECT_DIR$/.idea/FEDEO.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
615
README.md
615
README.md
@@ -1,109 +1,536 @@
|
|||||||
|
# FEDEO Hosting Guide
|
||||||
|
|
||||||
|
Diese Anleitung beschreibt ein produktionsnahes Self-Hosting von FEDEO mit Docker Compose, Traefik, PostgreSQL und optionalem S3-kompatiblem Objektspeicher via MinIO.
|
||||||
|
|
||||||
|
## Architektur
|
||||||
|
|
||||||
# Docker Compose Setup
|
Der Stack besteht aus:
|
||||||
|
|
||||||
## ENV Vars
|
- `frontend`: Nuxt-Frontend auf Port `3000`
|
||||||
|
- `backend`: Node/Fastify-API auf Port `3100`
|
||||||
|
- `db`: PostgreSQL
|
||||||
|
- `traefik`: Reverse Proxy mit automatischen Let's-Encrypt-Zertifikaten
|
||||||
|
- optional `minio`: S3-kompatibler Objektspeicher fur Dateiuploads
|
||||||
|
|
||||||
- DOMAIN
|
Die Konfiguration erfolgt uber Umgebungsvariablen beziehungsweise eine `.env`-Datei im Deploy-Verzeichnis.
|
||||||
- PDF_LICENSE
|
|
||||||
- DB_PASS
|
|
||||||
- DB_USER
|
|
||||||
- CONTACT_EMAIL
|
|
||||||
|
|
||||||
## Docker Compose File
|
## Voraussetzungen
|
||||||
~~~
|
|
||||||
|
Vor dem Deployment sollten folgende Punkte erfullt sein:
|
||||||
|
|
||||||
|
- Ein Linux-Server oder VPS mit offentlichen Ports `80` und `443`
|
||||||
|
- Docker Engine inkl. Compose Plugin
|
||||||
|
- Eine Domain, die auf den Server zeigt, z. B. `app.example.com`
|
||||||
|
- Optional: SMTP-Zugang fur E-Mails
|
||||||
|
- Optional: S3-Bucket oder MinIO fur Dateispeicher
|
||||||
|
|
||||||
|
Empfohlen:
|
||||||
|
|
||||||
|
- mindestens 2 vCPU
|
||||||
|
- mindestens 4 GB RAM
|
||||||
|
- SSD-Speicher fur PostgreSQL und Dateiuploads
|
||||||
|
|
||||||
|
## DNS und Netzwerk
|
||||||
|
|
||||||
|
Lege mindestens einen A- oder AAAA-Record an:
|
||||||
|
|
||||||
|
- `app.example.com -> <SERVER-IP>`
|
||||||
|
|
||||||
|
Traefik terminiert TLS direkt im Compose-Stack. Es ist kein zusatzlicher Reverse Proxy davor erforderlich.
|
||||||
|
|
||||||
|
## Benotigte Backend-Umgebungsvariablen
|
||||||
|
|
||||||
|
Das Backend erwartet mindestens diese Umgebungsvariablen:
|
||||||
|
|
||||||
|
- `COOKIE_SECRET`
|
||||||
|
- `JWT_SECRET`
|
||||||
|
- `PORT`
|
||||||
|
- `HOST`
|
||||||
|
- `DATABASE_URL`
|
||||||
|
- `S3_BUCKET`
|
||||||
|
- `ENCRYPTION_KEY`
|
||||||
|
- `MAILER_SMTP_HOST`
|
||||||
|
- `MAILER_SMTP_PORT`
|
||||||
|
- `MAILER_SMTP_SSL`
|
||||||
|
- `MAILER_SMTP_USER`
|
||||||
|
- `MAILER_SMTP_PASS`
|
||||||
|
- `MAILER_FROM`
|
||||||
|
- `S3_ENDPOINT`
|
||||||
|
- `S3_REGION`
|
||||||
|
- `S3_ACCESS_KEY`
|
||||||
|
- `S3_SECRET_KEY`
|
||||||
|
- `M2M_API_KEY`
|
||||||
|
- `API_BASE_URL`
|
||||||
|
- `GOCARDLESS_BASE_URL`
|
||||||
|
- `GOCARDLESS_SECRET_ID`
|
||||||
|
- `GOCARDLESS_SECRET_KEY`
|
||||||
|
- `DOKUBOX_IMAP_HOST`
|
||||||
|
- `DOKUBOX_IMAP_PORT`
|
||||||
|
- `DOKUBOX_IMAP_SECURE`
|
||||||
|
- `DOKUBOX_IMAP_USER`
|
||||||
|
- `DOKUBOX_IMAP_PASSWORD`
|
||||||
|
- `OPENAI_API_KEY`
|
||||||
|
- `STIRLING_API_KEY`
|
||||||
|
|
||||||
|
Minimal wichtige Werte fur den ersten Start:
|
||||||
|
|
||||||
|
- `HOST=0.0.0.0`
|
||||||
|
- `PORT=3100`
|
||||||
|
- `DATABASE_URL=postgres://fedeo:<starkes-passwort>@db:5432/fedeo`
|
||||||
|
- `API_BASE_URL=https://app.example.com/backend`
|
||||||
|
|
||||||
|
Wenn du MinIO verwendest, setze zusatzlich:
|
||||||
|
|
||||||
|
- `S3_ENDPOINT=http://minio:9000`
|
||||||
|
- `S3_REGION=eu-central-1`
|
||||||
|
- `S3_ACCESS_KEY=<MINIO_ROOT_USER>`
|
||||||
|
- `S3_SECRET_KEY=<MINIO_ROOT_PASSWORD>`
|
||||||
|
- `S3_BUCKET=fedeo`
|
||||||
|
|
||||||
|
## Deploy-Struktur
|
||||||
|
|
||||||
|
Deploye den Stack in einem eigenen Betriebsverzeichnis. Der Selfhost-Installer lädt dafür nur die benötigten Betriebsdateien und klont nicht das komplette Repository.
|
||||||
|
|
||||||
|
Beispiel für die manuelle Vorbereitung:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /opt/fedeo/scripts
|
||||||
|
curl -fsSL https://git.federspiel.tech/flfeders/FEDEO/raw/branch/dev/docker-compose.selfhost.yml -o /opt/fedeo/docker-compose.yml
|
||||||
|
curl -fsSL https://git.federspiel.tech/flfeders/FEDEO/raw/branch/dev/.env.example -o /opt/fedeo/.env.example
|
||||||
|
curl -fsSL https://git.federspiel.tech/flfeders/FEDEO/raw/branch/dev/scripts/selfhost-setup.sh -o /opt/fedeo/scripts/selfhost-setup.sh
|
||||||
|
chmod +x /opt/fedeo/scripts/selfhost-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Verzeichnisstruktur sollte dann mindestens so aussehen:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/opt/fedeo/
|
||||||
|
docker-compose.yml
|
||||||
|
.env
|
||||||
|
scripts/
|
||||||
|
traefik/
|
||||||
|
letsencrypt/
|
||||||
|
logs/
|
||||||
|
postgres/
|
||||||
|
minio/
|
||||||
|
```
|
||||||
|
|
||||||
|
Danach:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p /opt/fedeo/traefik/letsencrypt
|
||||||
|
mkdir -p /opt/fedeo/traefik/logs
|
||||||
|
mkdir -p /opt/fedeo/postgres
|
||||||
|
mkdir -p /opt/fedeo/minio
|
||||||
|
touch /opt/fedeo/traefik/letsencrypt/acme.json
|
||||||
|
chmod 600 /opt/fedeo/traefik/letsencrypt/acme.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Als Startpunkt kannst du die Beispielumgebung kopieren:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Ersetze anschließend alle Platzhalter und passe mindestens `DOMAIN`, `CONTACT_EMAIL`, Datenbank-, Secret-, SMTP- und S3-Werte an. Seafile ist kein Teil des Standard-Stacks; wenn FEDEO später Seafile als File-Backend nutzen soll, zeigst du die Seafile-Variablen auf einen externen Seafile-Dienst.
|
||||||
|
|
||||||
|
Alternativ kannst du die Konfiguration geführt erzeugen lassen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/selfhost-setup.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Auf einem frischen Server kannst du die Betriebsdateien und die Konfiguration direkt per One-Liner vorbereiten:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://git.federspiel.tech/flfeders/FEDEO/raw/branch/dev/scripts/selfhost-install.sh | bash
|
||||||
|
```
|
||||||
|
|
||||||
|
Der schnelle One-Liner mit direktem Stack-Start:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://git.federspiel.tech/flfeders/FEDEO/raw/branch/dev/scripts/selfhost-install.sh | bash -s -- --simple --start
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Installer prüft Basispakete, installiert Docker auf Wunsch über das offizielle Docker-Installationsscript, lädt nur die Selfhost-Dateien nach `/opt/fedeo` und startet anschließend den geführten Setup-Assistenten.
|
||||||
|
|
||||||
|
Für den schnellen Standardpfad:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/selfhost-setup.sh --simple
|
||||||
|
```
|
||||||
|
|
||||||
|
Für mehr Rückfragen zu SMTP, API-Schlüsseln und optionalen Diensten:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/selfhost-setup.sh --advanced
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Assistent erklärt zuerst die Selfhost-Verzeichnisstruktur, schreibt anschließend `.env`, legt persistente Verzeichnisse inklusive `traefik/letsencrypt/acme.json` an und kann den Stack optional direkt starten.
|
||||||
|
|
||||||
|
## Beispiel `.env`
|
||||||
|
|
||||||
|
Diese Datei liegt neben der `docker-compose.yml`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
DOMAIN=app.example.com
|
||||||
|
CONTACT_EMAIL=admin@deine-domain.de
|
||||||
|
|
||||||
|
DB_NAME=fedeo
|
||||||
|
DB_USER=fedeo
|
||||||
|
DB_PASSWORD=change-this-db-password
|
||||||
|
DATABASE_URL=postgres://fedeo:change-this-db-password@db:5432/fedeo
|
||||||
|
|
||||||
|
MINIO_ROOT_USER=fedeo-minio
|
||||||
|
MINIO_ROOT_PASSWORD=change-this-minio-password
|
||||||
|
MINIO_BUCKET=fedeo
|
||||||
|
|
||||||
|
HOST=0.0.0.0
|
||||||
|
PORT=3100
|
||||||
|
COOKIE_SECRET=change-this-cookie-secret
|
||||||
|
JWT_SECRET=change-this-jwt-secret
|
||||||
|
ENCRYPTION_KEY=change-this-encryption-key
|
||||||
|
|
||||||
|
MAILER_SMTP_HOST=smtp.example.com
|
||||||
|
MAILER_SMTP_PORT=587
|
||||||
|
MAILER_SMTP_SSL=false
|
||||||
|
MAILER_SMTP_USER=mailer@example.com
|
||||||
|
MAILER_SMTP_PASS=change-this-mail-password
|
||||||
|
MAILER_FROM=FEDEO <no-reply@example.com>
|
||||||
|
|
||||||
|
S3_ENDPOINT=http://minio:9000
|
||||||
|
S3_REGION=eu-central-1
|
||||||
|
S3_ACCESS_KEY=fedeo-minio
|
||||||
|
S3_SECRET_KEY=change-this-minio-password
|
||||||
|
S3_BUCKET=fedeo
|
||||||
|
|
||||||
|
FEDEO_FILE_BACKEND=s3
|
||||||
|
SEAFILE_BASE_URL=https://files.example.com
|
||||||
|
SEAFILE_INTERNAL_URL=https://files.example.com
|
||||||
|
SEAFILE_ADMIN_EMAIL=admin@example.com
|
||||||
|
SEAFILE_ADMIN_PASSWORD=change-this-seafile-admin-password
|
||||||
|
|
||||||
|
M2M_API_KEY=change-this-m2m-key
|
||||||
|
API_BASE_URL=https://app.example.com/backend
|
||||||
|
GOCARDLESS_BASE_URL=https://api.gocardless.com
|
||||||
|
GOCARDLESS_SECRET_ID=replace-this
|
||||||
|
GOCARDLESS_SECRET_KEY=replace-this
|
||||||
|
|
||||||
|
DOKUBOX_IMAP_HOST=imap.example.com
|
||||||
|
DOKUBOX_IMAP_PORT=993
|
||||||
|
DOKUBOX_IMAP_SECURE=true
|
||||||
|
DOKUBOX_IMAP_USER=dokubox@example.com
|
||||||
|
DOKUBOX_IMAP_PASSWORD=change-this-imap-password
|
||||||
|
|
||||||
|
OPENAI_API_KEY=replace-this
|
||||||
|
STIRLING_API_KEY=replace-this
|
||||||
|
|
||||||
|
NUXT_PUBLIC_PDF_LICENSE=replace-with-your-pdf-license
|
||||||
|
|
||||||
|
FEDEO_BOOTSTRAP_ADMIN_EMAIL=admin@example.com
|
||||||
|
FEDEO_BOOTSTRAP_ADMIN_PASSWORD=change-this-admin-password
|
||||||
|
FEDEO_BOOTSTRAP_ADMIN_FIRST_NAME=Admin
|
||||||
|
FEDEO_BOOTSTRAP_ADMIN_LAST_NAME=Benutzer
|
||||||
|
FEDEO_BOOTSTRAP_TENANT_NAME=Mein Unternehmen
|
||||||
|
FEDEO_BOOTSTRAP_TENANT_SHORT=MEIN
|
||||||
|
|
||||||
|
MATRIX_SERVER_NAME=app.example.com
|
||||||
|
MATRIX_POSTGRES_DB=synapse
|
||||||
|
MATRIX_POSTGRES_USER=synapse
|
||||||
|
MATRIX_POSTGRES_PASSWORD=change-this-matrix-db-password
|
||||||
|
MATRIX_TURN_SHARED_SECRET=change-this-turn-secret
|
||||||
|
MATRIX_HOMESERVER_URL=http://matrix-synapse:8008
|
||||||
|
MATRIX_RTC_HOST=app.example.com
|
||||||
|
MATRIX_RTC_JWT_URL=https://app.example.com/livekit/jwt
|
||||||
|
MATRIX_LIVEKIT_URL=wss://app.example.com/livekit/sfu
|
||||||
|
MATRIX_REGISTRATION_SHARED_SECRET=change-this-matrix-registration-secret
|
||||||
|
MATRIX_SERVICE_USER_LOCALPART=fedeo_service
|
||||||
|
LIVEKIT_KEY=fedeo-livekit
|
||||||
|
LIVEKIT_SECRET=change-this-livekit-secret-please-replace
|
||||||
|
NUXT_PUBLIC_MATRIX_ELEMENT_URL=https://app.example.com/element
|
||||||
|
```
|
||||||
|
|
||||||
|
Die `FEDEO_BOOTSTRAP_*`-Werte sind für den ersten Start gedacht. Wenn `FEDEO_BOOTSTRAP_ADMIN_EMAIL` und `FEDEO_BOOTSTRAP_ADMIN_PASSWORD` gesetzt sind, legt das Backend idempotent einen Admin-Benutzer, einen ersten Mandanten, eine Administrator-Rolle und grundlegende Stammdaten an. Nach erfolgreichem Erstzugriff solltest du das Bootstrap-Passwort aus der `.env` entfernen oder ändern.
|
||||||
|
|
||||||
|
## Docker Compose mit optionalem S3 und Matrix
|
||||||
|
|
||||||
|
Die Selfhost-Konfiguration wird im Betriebsverzeichnis als `docker-compose.yml` abgelegt. Sie startet MinIO standardmäßig mit. Wenn du stattdessen AWS S3, Hetzner Object Storage, Backblaze B2 S3 oder einen anderen externen S3-Dienst nutzen willst, kannst du die Services `minio` und `createbuckets` entfernen und nur die entsprechenden S3-Umgebungsvariablen auf den externen Anbieter zeigen lassen.
|
||||||
|
|
||||||
|
Seafile wird bewusst nicht im Standard-Compose-Stack gestartet. FEDEO kann später gegen einen extern betriebenen Seafile-Dienst sprechen; dafür bleiben `SEAFILE_BASE_URL`, `SEAFILE_INTERNAL_URL`, `SEAFILE_ADMIN_EMAIL` und `SEAFILE_ADMIN_PASSWORD` als generische Anbindungswerte vorgesehen. `FEDEO_FILE_BACKEND=s3` bleibt der Standard, bis die Backend-Integration für Seafile vollständig umgesetzt ist.
|
||||||
|
|
||||||
|
Der Matrix-Stack ist im Selfhost-Compose direkt enthalten. Er umfasst Synapse, eine eigene PostgreSQL-Datenbank für Synapse, Redis, `.well-known/matrix`, coturn, LiveKit, den LiveKit-JWT-Service und Element Web. Das einfache Selfhost-Setup nutzt nur `DOMAIN`: Synapse läuft unter `https://DOMAIN/_matrix`, Matrix-Well-Known unter `https://DOMAIN/.well-known/matrix`, LiveKit unter `https://DOMAIN/livekit/sfu`, der JWT-Service unter `https://DOMAIN/livekit/jwt` und Element Web unter `https://DOMAIN/element`.
|
||||||
|
|
||||||
|
Das Backend führt beim Containerstart standardmäßig `npm run migrate` aus. Setze `FEDEO_RUN_MIGRATIONS=false`, wenn du Migrationen bewusst manuell ausführen möchtest.
|
||||||
|
|
||||||
|
```yaml
|
||||||
services:
|
services:
|
||||||
frontend:
|
|
||||||
image: git.federspiel.tech/flfeders/fedeo/frontend:main
|
|
||||||
restart: always
|
|
||||||
environment:
|
|
||||||
- NUXT_PUBLIC_API_BASE=https://${DOMAIN}/backend
|
|
||||||
- NUXT_PUBLIC_PDF_LICENSE=${PDF_LICENSE}
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.docker.network=traefik"
|
|
||||||
- "traefik.port=3000"
|
|
||||||
# Middlewares
|
|
||||||
- "traefik.http.middlewares.fedeo-frontend-redirect-web-secure.redirectscheme.scheme=https"
|
|
||||||
# Web Entrypoint
|
|
||||||
- "traefik.http.routers.fedeo-frontend.middlewares=fedeo-frontend-redirect-web-secure"
|
|
||||||
- "traefik.http.routers.fedeo-frontend.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)"
|
|
||||||
- "traefik.http.routers.fedeo-frontend.entrypoints=web"
|
|
||||||
# Web Secure Entrypoint
|
|
||||||
- "traefik.http.routers.fedeo-frontend-secure.rule=Host(`${DOMAIN}`) && PathPrefix(`/`)"
|
|
||||||
- "traefik.http.routers.fedeo-frontend-secure.entrypoints=web-secured" #
|
|
||||||
- "traefik.http.routers.fedeo-frontend-secure.tls.certresolver=mytlschallenge"
|
|
||||||
backend:
|
|
||||||
image: git.federspiel.tech/flfeders/fedeo/backend:main
|
|
||||||
restart: always
|
|
||||||
environment:
|
|
||||||
- INFISICAL_CLIENT_ID=
|
|
||||||
- INFISICAL_CLIENT_SECRET=
|
|
||||||
- NODE_ENV=production
|
|
||||||
networks:
|
|
||||||
- traefik
|
|
||||||
labels:
|
|
||||||
- "traefik.enable=true"
|
|
||||||
- "traefik.docker.network=traefik"
|
|
||||||
- "traefik.port=3100"
|
|
||||||
# Middlewares
|
|
||||||
- "traefik.http.middlewares.fedeo-backend-redirect-web-secure.redirectscheme.scheme=https"
|
|
||||||
- "traefik.http.middlewares.fedeo-backend-strip.stripprefix.prefixes=/backend"
|
|
||||||
# Web Entrypoint
|
|
||||||
- "traefik.http.routers.fedeo-backend.middlewares=fedeo-backend-redirect-web-secure"
|
|
||||||
- "traefik.http.routers.fedeo-backend.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)"
|
|
||||||
- "traefik.http.routers.fedeo-backend.entrypoints=web"
|
|
||||||
# Web Secure Entrypoint
|
|
||||||
- "traefik.http.routers.fedeo-backend-secure.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)"
|
|
||||||
- "traefik.http.routers.fedeo-backend-secure.entrypoints=web-secured" #
|
|
||||||
- "traefik.http.routers.fedeo-backend-secure.tls.certresolver=mytlschallenge"
|
|
||||||
- "traefik.http.routers.fedeo-backend-secure.middlewares=fedeo-backend-strip"
|
|
||||||
# db:
|
|
||||||
# image: postgres
|
|
||||||
# restart: always
|
|
||||||
# shm_size: 128mb
|
|
||||||
# environment:
|
|
||||||
# POSTGRES_PASSWORD:
|
|
||||||
# POSTGRES_USER:
|
|
||||||
# POSTGRES_DB:
|
|
||||||
# volumes:
|
|
||||||
# - ./pg-data:/var/lib/postgresql/data
|
|
||||||
# ports:
|
|
||||||
# - "5432:5432"
|
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v2.11
|
image: traefik:v2.11
|
||||||
|
container_name: fedeo-traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
container_name: traefik
|
|
||||||
command:
|
command:
|
||||||
- "--api.insecure=false"
|
- --api.insecure=false
|
||||||
- "--api.dashboard=false"
|
- --api.dashboard=false
|
||||||
- "--api.debug=false"
|
- --providers.docker=true
|
||||||
- "--providers.docker=true"
|
- --providers.docker.exposedbydefault=false
|
||||||
- "--providers.docker.exposedbydefault=false"
|
- --entrypoints.web.address=:80
|
||||||
- "--providers.docker.network=traefik"
|
- --entrypoints.websecure.address=:443
|
||||||
- "--entrypoints.web.address=:80"
|
- --entrypoints.web.http.redirections.entrypoint.to=websecure
|
||||||
- "--entrypoints.web-secured.address=:443"
|
- --entrypoints.web.http.redirections.entrypoint.scheme=https
|
||||||
- "--accesslog=true"
|
- --certificatesresolvers.letsencrypt.acme.tlschallenge=true
|
||||||
- "--accesslog.filepath=/logs/access.log"
|
- --certificatesresolvers.letsencrypt.acme.email=${CONTACT_EMAIL}
|
||||||
- "--accesslog.bufferingsize=5000"
|
- --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
|
||||||
- "--accesslog.fields.defaultMode=keep"
|
- --accesslog=true
|
||||||
- "--accesslog.fields.headers.defaultMode=keep"
|
- --accesslog.filepath=/logs/access.log
|
||||||
- "--certificatesresolvers.mytlschallenge.acme.tlschallenge=true" #
|
|
||||||
- "--certificatesresolvers.mytlschallenge.acme.email=${CONTACT_EMAIL}"
|
|
||||||
- "--certificatesresolvers.mytlschallenge.acme.storage=/letsencrypt/acme.json"
|
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- "80:80"
|
||||||
- 443:443
|
- "443:443"
|
||||||
volumes:
|
volumes:
|
||||||
- "./traefik/letsencrypt:/letsencrypt" # <== Volume for certs (TLS)
|
- ./traefik/letsencrypt:/letsencrypt
|
||||||
- "/var/run/docker.sock:/var/run/docker.sock:ro"
|
- ./traefik/logs:/logs
|
||||||
- "./traefik/logs:/logs"
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
networks:
|
networks:
|
||||||
- traefik
|
- web
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:16
|
||||||
|
container_name: fedeo-db
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: ${DB_NAME}
|
||||||
|
POSTGRES_USER: ${DB_USER}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- ./postgres:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USER} -d ${DB_NAME}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
networks:
|
networks:
|
||||||
traefik:
|
- internal
|
||||||
external: false
|
|
||||||
~~~
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
container_name: fedeo-minio
|
||||||
|
restart: unless-stopped
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||||
|
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||||
|
volumes:
|
||||||
|
- ./minio:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
createbuckets:
|
||||||
|
image: minio/mc:latest
|
||||||
|
container_name: fedeo-minio-init
|
||||||
|
depends_on:
|
||||||
|
minio:
|
||||||
|
condition: service_healthy
|
||||||
|
entrypoint: >
|
||||||
|
/bin/sh -c "
|
||||||
|
mc alias set local http://minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD};
|
||||||
|
mc mb --ignore-existing local/${MINIO_BUCKET};
|
||||||
|
mc anonymous set private local/${MINIO_BUCKET};
|
||||||
|
exit 0;
|
||||||
|
"
|
||||||
|
restart: "no"
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
backend:
|
||||||
|
image: git.federspiel.tech/flfeders/fedeo/backend:dev
|
||||||
|
container_name: fedeo-backend
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
db:
|
||||||
|
condition: service_healthy
|
||||||
|
minio:
|
||||||
|
condition: service_healthy
|
||||||
|
createbuckets:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
HOST: ${HOST}
|
||||||
|
PORT: ${PORT}
|
||||||
|
COOKIE_SECRET: ${COOKIE_SECRET}
|
||||||
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
|
ENCRYPTION_KEY: ${ENCRYPTION_KEY}
|
||||||
|
DATABASE_URL: ${DATABASE_URL}
|
||||||
|
MAILER_SMTP_HOST: ${MAILER_SMTP_HOST}
|
||||||
|
MAILER_SMTP_PORT: ${MAILER_SMTP_PORT}
|
||||||
|
MAILER_SMTP_SSL: ${MAILER_SMTP_SSL}
|
||||||
|
MAILER_SMTP_USER: ${MAILER_SMTP_USER}
|
||||||
|
MAILER_SMTP_PASS: ${MAILER_SMTP_PASS}
|
||||||
|
MAILER_FROM: ${MAILER_FROM}
|
||||||
|
S3_ENDPOINT: ${S3_ENDPOINT}
|
||||||
|
S3_REGION: ${S3_REGION}
|
||||||
|
S3_ACCESS_KEY: ${S3_ACCESS_KEY}
|
||||||
|
S3_SECRET_KEY: ${S3_SECRET_KEY}
|
||||||
|
S3_BUCKET: ${S3_BUCKET}
|
||||||
|
M2M_API_KEY: ${M2M_API_KEY}
|
||||||
|
API_BASE_URL: ${API_BASE_URL}
|
||||||
|
GOCARDLESS_BASE_URL: ${GOCARDLESS_BASE_URL}
|
||||||
|
GOCARDLESS_SECRET_ID: ${GOCARDLESS_SECRET_ID}
|
||||||
|
GOCARDLESS_SECRET_KEY: ${GOCARDLESS_SECRET_KEY}
|
||||||
|
DOKUBOX_IMAP_HOST: ${DOKUBOX_IMAP_HOST}
|
||||||
|
DOKUBOX_IMAP_PORT: ${DOKUBOX_IMAP_PORT}
|
||||||
|
DOKUBOX_IMAP_SECURE: ${DOKUBOX_IMAP_SECURE}
|
||||||
|
DOKUBOX_IMAP_USER: ${DOKUBOX_IMAP_USER}
|
||||||
|
DOKUBOX_IMAP_PASSWORD: ${DOKUBOX_IMAP_PASSWORD}
|
||||||
|
OPENAI_API_KEY: ${OPENAI_API_KEY}
|
||||||
|
STIRLING_API_KEY: ${STIRLING_API_KEY}
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.fedeo-backend.rule=Host(`${DOMAIN}`) && PathPrefix(`/backend`)
|
||||||
|
- traefik.http.routers.fedeo-backend.entrypoints=websecure
|
||||||
|
- traefik.http.routers.fedeo-backend.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.middlewares.fedeo-backend-strip.stripprefix.prefixes=/backend
|
||||||
|
- traefik.http.routers.fedeo-backend.middlewares=fedeo-backend-strip
|
||||||
|
- traefik.http.services.fedeo-backend.loadbalancer.server.port=3100
|
||||||
|
- traefik.docker.network=fedeo_web
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
- internal
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
image: git.federspiel.tech/flfeders/fedeo/frontend:dev
|
||||||
|
container_name: fedeo-frontend
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
NUXT_PUBLIC_API_BASE: https://${DOMAIN}/backend
|
||||||
|
NUXT_PUBLIC_PDF_LICENSE: ${NUXT_PUBLIC_PDF_LICENSE}
|
||||||
|
labels:
|
||||||
|
- traefik.enable=true
|
||||||
|
- traefik.http.routers.fedeo-frontend.rule=Host(`${DOMAIN}`)
|
||||||
|
- traefik.http.routers.fedeo-frontend.entrypoints=websecure
|
||||||
|
- traefik.http.routers.fedeo-frontend.tls.certresolver=letsencrypt
|
||||||
|
- traefik.http.services.fedeo-frontend.loadbalancer.server.port=3000
|
||||||
|
- traefik.docker.network=fedeo_web
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
name: fedeo_web
|
||||||
|
driver: bridge
|
||||||
|
internal:
|
||||||
|
name: fedeo_internal
|
||||||
|
driver: bridge
|
||||||
|
```
|
||||||
|
|
||||||
|
## Externe S3-Provider statt MinIO
|
||||||
|
|
||||||
|
Wenn du keinen lokalen MinIO-Container betreiben willst:
|
||||||
|
|
||||||
|
1. Entferne die Services `minio` und `createbuckets` aus der Compose-Datei.
|
||||||
|
2. Entferne im Backend `depends_on` fur `minio` und `createbuckets`.
|
||||||
|
3. Trage in `.env` die Zugangsdaten des externen S3-Dienstes ein.
|
||||||
|
|
||||||
|
Beispiel fur die relevanten Werte:
|
||||||
|
|
||||||
|
```env
|
||||||
|
S3_ENDPOINT=https://s3.eu-central-1.amazonaws.com
|
||||||
|
S3_REGION=eu-central-1
|
||||||
|
S3_ACCESS_KEY=...
|
||||||
|
S3_SECRET_KEY=...
|
||||||
|
S3_BUCKET=fedeo
|
||||||
|
```
|
||||||
|
|
||||||
|
Hinweis: Das Backend nutzt `forcePathStyle: true`. Das funktioniert sauber mit MinIO und vielen S3-kompatiblen Providern. Bei reinem AWS S3 kann je nach Endpoint-Setup ein abweichendes Verhalten sinnvoll sein. Falls du AWS S3 einsetzen willst, sollte die S3-Initialisierung im Backend gegen den konkreten Zielprovider getestet werden.
|
||||||
|
|
||||||
|
## Start des Stacks
|
||||||
|
|
||||||
|
Im Deploy-Verzeichnis:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --env-file /opt/fedeo/.env -f /opt/fedeo/docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Synapse erzeugt `matrix/synapse/homeserver.yaml` beim ersten Start automatisch und aktualisiert die für FEDEO relevanten Werte aus der `.env`. `MATRIX_REGISTRATION_SHARED_SECRET` muss in der `.env` gesetzt und geheim bleiben, weil FEDEO damit Matrix-Nutzer provisioniert.
|
||||||
|
|
||||||
|
Danach Status prufen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --env-file /opt/fedeo/.env -f /opt/fedeo/docker-compose.yml ps
|
||||||
|
docker compose --env-file /opt/fedeo/.env -f /opt/fedeo/docker-compose.yml logs -f traefik
|
||||||
|
docker compose --env-file /opt/fedeo/.env -f /opt/fedeo/docker-compose.yml logs -f backend
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn du Migrationen manuell ausführen möchtest:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose --env-file /opt/fedeo/.env -f /opt/fedeo/docker-compose.yml run --rm backend npm run migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Funktionsprufung
|
||||||
|
|
||||||
|
Nach dem ersten Start sollten mindestens diese Checks erfolgreich sein:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -I https://app.example.com
|
||||||
|
curl https://app.example.com/backend/health
|
||||||
|
```
|
||||||
|
|
||||||
|
Erwartung:
|
||||||
|
|
||||||
|
- Frontend liefert `200` oder `302`
|
||||||
|
- Backend liefert JSON wie `{"status":"ok"}`
|
||||||
|
|
||||||
|
Wenn der Bootstrap aktiviert ist, kannst du dich danach mit `FEDEO_BOOTSTRAP_ADMIN_EMAIL` und `FEDEO_BOOTSTRAP_ADMIN_PASSWORD` anmelden. Die Mandantensperre wird über `locked` gesteuert; `hasActiveLicense` wird nicht mehr für den Selfhost-Zugriff ausgewertet.
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
|
||||||
|
Bei neuen Versionen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -fsSL https://git.federspiel.tech/flfeders/FEDEO/raw/branch/dev/docker-compose.selfhost.yml -o /opt/fedeo/docker-compose.yml
|
||||||
|
curl -fsSL https://git.federspiel.tech/flfeders/FEDEO/raw/branch/dev/.env.example -o /opt/fedeo/.env.example
|
||||||
|
curl -fsSL https://git.federspiel.tech/flfeders/FEDEO/raw/branch/dev/scripts/selfhost-setup.sh -o /opt/fedeo/scripts/selfhost-setup.sh
|
||||||
|
chmod +x /opt/fedeo/scripts/selfhost-setup.sh
|
||||||
|
docker compose --env-file /opt/fedeo/.env -f /opt/fedeo/docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Backend-Container wendet Datenbankmigrationen beim Start automatisch an. Bei kritischen Updates sollte vorher ein Backup von `./postgres` und `./minio` erstellt werden.
|
||||||
|
|
||||||
|
Die Selfhost-Compose-Datei nutzt vorgebaute Images. Dadurch braucht der Server keinen Repository-Checkout und keine lokalen Build-Kontexte.
|
||||||
|
|
||||||
|
## Backup-Empfehlung
|
||||||
|
|
||||||
|
Regelmassig sichern:
|
||||||
|
|
||||||
|
- `./postgres`
|
||||||
|
- `./minio` falls MinIO lokal genutzt wird
|
||||||
|
- `./matrix/postgres` falls Matrix lokal betrieben wird
|
||||||
|
- `./matrix/synapse` falls Matrix lokal betrieben wird
|
||||||
|
- `./traefik/letsencrypt/acme.json`
|
||||||
|
- deine `.env`
|
||||||
|
- deine dokumentierten Secret-Werte aus der `.env` oder deinem Secret-Management
|
||||||
|
|
||||||
|
## Bekannte Betriebsbesonderheiten
|
||||||
|
|
||||||
|
- Das Backend startet nur sauber, wenn alle Pflichtvariablen gesetzt sind.
|
||||||
|
- Ohne korrekt gesetzte S3-Secrets funktionieren Dateiuploads und dateibasierte Funktionen nicht.
|
||||||
|
- Fur die Frontend-PDF-Funktion wird eine gueltige `NUXT_PUBLIC_PDF_LICENSE` benotigt.
|
||||||
|
- PostgreSQL ist im Projekt vorgesehen; andere SQL-Datenbanken sind in dieser Compose-Datei nicht berucksichtigt.
|
||||||
|
|
||||||
|
## Optional: Nur mit bestehender externer Infrastruktur
|
||||||
|
|
||||||
|
Wenn bereits vorhanden:
|
||||||
|
|
||||||
|
- externer Reverse Proxy
|
||||||
|
- externer PostgreSQL-Server
|
||||||
|
- externer S3-Speicher
|
||||||
|
- externe Zertifikatsverwaltung
|
||||||
|
|
||||||
|
dann konnen `traefik`, `db` und `minio` aus dem Stack entfernt werden. In diesem Fall mussen die zugehorigen Hostnamen und Zugangsdaten in der `.env` beziehungsweise im Frontend-Environment auf die externe Infrastruktur zeigen.
|
||||||
|
|||||||
6
agents/fedeo-device-agent/.dockerignore
Normal file
6
agents/fedeo-device-agent/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
.venv-opencv
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
14
agents/fedeo-device-agent/.env.example
Normal file
14
agents/fedeo-device-agent/.env.example
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
FEDEO_URL=https://fedeo.example.com
|
||||||
|
FEDEO_AGENT_TOKEN=fedeo_agent_REPLACE_ME
|
||||||
|
FEDEO_POLL_SECONDS=5
|
||||||
|
FEDEO_WORK_DIR=/tmp/fedeo-device-agent
|
||||||
|
FEDEO_SCANNER_NAME=
|
||||||
|
FEDEO_PRINTER_NAME=
|
||||||
|
FEDEO_SCAN_FORMAT=pdf
|
||||||
|
FEDEO_SCAN_RESOLUTION=300
|
||||||
|
FEDEO_SCAN_MODE=Color
|
||||||
|
FEDEO_SCAN_SOURCE=
|
||||||
|
FEDEO_SCAN_POSTPROCESS=false
|
||||||
|
FEDEO_SCAN_POSTPROCESS_PROFILE=document
|
||||||
|
FEDEO_SCAN_POSTPROCESS_PYTHON=
|
||||||
|
FEDEO_SCAN_POSTPROCESS_STRICT=false
|
||||||
6
agents/fedeo-device-agent/.gitignore
vendored
Normal file
6
agents/fedeo-device-agent/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
dist
|
||||||
|
node_modules
|
||||||
|
.venv-opencv
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
45
agents/fedeo-device-agent/Dockerfile
Normal file
45
agents/fedeo-device-agent/Dockerfile
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
FROM node:20-bookworm-slim AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json tsconfig.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY src ./src
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:20-bookworm-slim AS runtime
|
||||||
|
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
ENV FEDEO_WORK_DIR=/work
|
||||||
|
ENV FEDEO_SCAN_POSTPROCESS=true
|
||||||
|
ENV FEDEO_SCAN_POSTPROCESS_PROFILE=receipt
|
||||||
|
ENV FEDEO_SCAN_POSTPROCESS_PYTHON=/opt/fedeo-device-agent/.venv-opencv/bin/python
|
||||||
|
ENV FEDEO_SCAN_POSTPROCESS_STRICT=false
|
||||||
|
|
||||||
|
WORKDIR /opt/fedeo-device-agent
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
ca-certificates \
|
||||||
|
cups-client \
|
||||||
|
libgomp1 \
|
||||||
|
python3 \
|
||||||
|
python3-pip \
|
||||||
|
python3-venv \
|
||||||
|
sane-utils \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY requirements-opencv.txt ./
|
||||||
|
RUN python3 -m venv .venv-opencv \
|
||||||
|
&& .venv-opencv/bin/python -m pip install --no-cache-dir --upgrade pip \
|
||||||
|
&& .venv-opencv/bin/python -m pip install --no-cache-dir -r requirements-opencv.txt \
|
||||||
|
&& .venv-opencv/bin/python -c "import cv2, PIL, numpy"
|
||||||
|
|
||||||
|
COPY --from=build /app/dist ./dist
|
||||||
|
COPY scripts ./scripts
|
||||||
|
COPY package.json ./
|
||||||
|
|
||||||
|
RUN mkdir -p /work
|
||||||
|
|
||||||
|
CMD ["node", "dist/main.js"]
|
||||||
133
agents/fedeo-device-agent/README.md
Normal file
133
agents/fedeo-device-agent/README.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# FEDEO Geräte-Agent
|
||||||
|
|
||||||
|
Der FEDEO Geräte-Agent läuft lokal auf macOS, Linux oder Raspberry Pi OS. Er holt instanzweite Scan-Aufträge von FEDEO ab, führt sie auf einem lokal angeschlossenen Scanner aus und lädt das Ergebnis wieder in FEDEO hoch.
|
||||||
|
|
||||||
|
Der Agent ist nicht an einen Mandanten gebunden. Jeder Auftrag enthält seinen Tenant selbst.
|
||||||
|
|
||||||
|
## Voraussetzungen
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install node sane-backends
|
||||||
|
scanimage -L
|
||||||
|
```
|
||||||
|
|
||||||
|
Drucken nutzt später das macOS-Drucksystem/CUPS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lpstat -p
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linux und Raspberry Pi OS
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y nodejs npm sane-utils cups
|
||||||
|
scanimage -L
|
||||||
|
lpstat -p
|
||||||
|
```
|
||||||
|
|
||||||
|
## Konfiguration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env
|
||||||
|
```
|
||||||
|
|
||||||
|
Wichtige Werte:
|
||||||
|
|
||||||
|
```env
|
||||||
|
FEDEO_URL=https://deine-fedeo-instanz
|
||||||
|
FEDEO_AGENT_TOKEN=fedeo_agent_...
|
||||||
|
FEDEO_SCANNER_NAME=
|
||||||
|
FEDEO_POLL_SECONDS=5
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn `FEDEO_SCANNER_NAME` leer bleibt, verwendet `scanimage` den Standard-Scanner.
|
||||||
|
|
||||||
|
## Entwicklung
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## OpenCV-Nachbearbeitung
|
||||||
|
|
||||||
|
Für automatischen Zuschnitt, leichte Entzerrung, Rotation und Kontrastkorrektur kann die OpenCV-Pipeline aktiviert werden.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run setup:opencv
|
||||||
|
```
|
||||||
|
|
||||||
|
Konfiguration:
|
||||||
|
|
||||||
|
```env
|
||||||
|
FEDEO_SCAN_POSTPROCESS=true
|
||||||
|
FEDEO_SCAN_POSTPROCESS_PROFILE=receipt
|
||||||
|
FEDEO_SCAN_POSTPROCESS_PYTHON=/pfad/zum/agent/.venv-opencv/bin/python
|
||||||
|
FEDEO_SCAN_POSTPROCESS_STRICT=false
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn `FEDEO_SCAN_POSTPROCESS_PYTHON` leer bleibt, verwendet der Agent automatisch `.venv-opencv/bin/python`, sofern diese Umgebung existiert. Falls OpenCV nicht installiert ist und `FEDEO_SCAN_POSTPROCESS_STRICT=false` gesetzt ist, lädt der Agent den Rohscan hoch, statt den Auftrag komplett fehlschlagen zu lassen.
|
||||||
|
|
||||||
|
Profile:
|
||||||
|
|
||||||
|
- `receipt`: Bons und schmale Belege werden bevorzugt hochkant zugeschnitten und kontrastiert.
|
||||||
|
- `document`: allgemeine Dokumente mit Farberhalt und moderater Verbesserung.
|
||||||
|
- `raw`: Zuschnitt/Entzerrung ohne starke Kontrastkorrektur.
|
||||||
|
|
||||||
|
## Container-Betrieb
|
||||||
|
|
||||||
|
Auf Linux und Raspberry Pi OS kann der Agent komplett im Container laufen. Dadurch bleiben Node.js, Python, OpenCV und SANE im Image. Auf dem Host werden dann nur Docker und Zugriff auf den USB-Scanner benötigt.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
nano .env
|
||||||
|
docker compose -f docker-compose.example.yml up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn FEDEO lokal auf dem Docker-Host läuft, verwende im Container nicht `localhost`, sondern:
|
||||||
|
|
||||||
|
```env
|
||||||
|
FEDEO_URL=http://host.docker.internal:3100
|
||||||
|
```
|
||||||
|
|
||||||
|
Scanner im Container prüfen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose -f docker-compose.example.yml run --rm fedeo-device-agent scanimage -L
|
||||||
|
```
|
||||||
|
|
||||||
|
Wenn der Scanner nicht sichtbar ist, hilft je nach Gerät/Host manchmal `privileged: true` im Compose-Beispiel. Auf macOS ist Docker dafür nur eingeschränkt geeignet, weil Docker Desktop USB-Scanner normalerweise nicht direkt an Linux-Container durchreichen kann. Für macOS bleibt deshalb der native Agent oder später eine signierte App der bessere Weg.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## FEDEO-Endpunkte
|
||||||
|
|
||||||
|
Der Agent nutzt:
|
||||||
|
|
||||||
|
- `POST /instance-agent/heartbeat`
|
||||||
|
- `GET /instance-agent/scan-jobs/next`
|
||||||
|
- `POST /instance-agent/scan-jobs/:id/status`
|
||||||
|
- `POST /instance-agent/scan-jobs/:id/upload`
|
||||||
|
|
||||||
|
## macOS Autostart
|
||||||
|
|
||||||
|
Die Vorlage liegt unter `system/macos/com.fedeo.device-agent.plist`. Nach Anpassung der Pfade kann sie als LaunchAgent installiert werden:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p ~/Library/LaunchAgents
|
||||||
|
cp system/macos/com.fedeo.device-agent.plist ~/Library/LaunchAgents/
|
||||||
|
launchctl load ~/Library/LaunchAgents/com.fedeo.device-agent.plist
|
||||||
|
```
|
||||||
|
|
||||||
|
## Linux Autostart
|
||||||
|
|
||||||
|
Die Vorlage liegt unter `system/linux/fedeo-device-agent.service`.
|
||||||
28
agents/fedeo-device-agent/docker-compose.example.yml
Normal file
28
agents/fedeo-device-agent/docker-compose.example.yml
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
services:
|
||||||
|
fedeo-device-agent:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
image: fedeo-device-agent:local
|
||||||
|
container_name: fedeo-device-agent
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
FEDEO_WORK_DIR: /work
|
||||||
|
FEDEO_SCAN_POSTPROCESS: "true"
|
||||||
|
FEDEO_SCAN_POSTPROCESS_PROFILE: receipt
|
||||||
|
FEDEO_SCAN_POSTPROCESS_PYTHON: /opt/fedeo-device-agent/.venv-opencv/bin/python
|
||||||
|
FEDEO_SCAN_POSTPROCESS_STRICT: "false"
|
||||||
|
volumes:
|
||||||
|
- fedeo-device-agent-work:/work
|
||||||
|
# Optional fuer CUPS-Druck ueber den Host:
|
||||||
|
# - /var/run/cups/cups.sock:/var/run/cups/cups.sock
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
devices:
|
||||||
|
- /dev/bus/usb:/dev/bus/usb
|
||||||
|
# Falls SANE den Scanner trotz devices-Mapping nicht sieht, testweise aktivieren:
|
||||||
|
# privileged: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
fedeo-device-agent-work:
|
||||||
26
agents/fedeo-device-agent/package.json
Normal file
26
agents/fedeo-device-agent/package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "@fedeo/device-agent",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "Lokaler FEDEO Druck- und Scan-Agent für macOS, Linux und Raspberry Pi OS.",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/main.js",
|
||||||
|
"bin": {
|
||||||
|
"fedeo-device-agent": "dist/main.js"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.json",
|
||||||
|
"dev": "tsx src/main.ts",
|
||||||
|
"start": "node dist/main.js",
|
||||||
|
"setup:opencv": "sh scripts/setup-opencv.sh"
|
||||||
|
},
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.3.0",
|
||||||
|
"tsx": "^4.20.5",
|
||||||
|
"typescript": "^5.9.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
}
|
||||||
3
agents/fedeo-device-agent/requirements-opencv.txt
Normal file
3
agents/fedeo-device-agent/requirements-opencv.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
opencv-python-headless>=4.9
|
||||||
|
Pillow>=10.0
|
||||||
|
numpy>=1.26
|
||||||
219
agents/fedeo-device-agent/scripts/opencv_postprocess.py
Normal file
219
agents/fedeo-device-agent/scripts/opencv_postprocess.py
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import argparse
|
||||||
|
import math
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
def order_points(points):
|
||||||
|
rect = np.zeros((4, 2), dtype="float32")
|
||||||
|
point_sum = points.sum(axis=1)
|
||||||
|
point_diff = np.diff(points, axis=1)
|
||||||
|
|
||||||
|
rect[0] = points[np.argmin(point_sum)]
|
||||||
|
rect[2] = points[np.argmax(point_sum)]
|
||||||
|
rect[1] = points[np.argmin(point_diff)]
|
||||||
|
rect[3] = points[np.argmax(point_diff)]
|
||||||
|
return rect
|
||||||
|
|
||||||
|
|
||||||
|
def four_point_transform(image, points):
|
||||||
|
rect = order_points(points)
|
||||||
|
top_left, top_right, bottom_right, bottom_left = rect
|
||||||
|
|
||||||
|
width_a = np.linalg.norm(bottom_right - bottom_left)
|
||||||
|
width_b = np.linalg.norm(top_right - top_left)
|
||||||
|
max_width = int(max(width_a, width_b))
|
||||||
|
|
||||||
|
height_a = np.linalg.norm(top_right - bottom_right)
|
||||||
|
height_b = np.linalg.norm(top_left - bottom_left)
|
||||||
|
max_height = int(max(height_a, height_b))
|
||||||
|
|
||||||
|
destination = np.array([
|
||||||
|
[0, 0],
|
||||||
|
[max_width - 1, 0],
|
||||||
|
[max_width - 1, max_height - 1],
|
||||||
|
[0, max_height - 1],
|
||||||
|
], dtype="float32")
|
||||||
|
|
||||||
|
matrix = cv2.getPerspectiveTransform(rect, destination)
|
||||||
|
return cv2.warpPerspective(image, matrix, (max_width, max_height), borderValue=(255, 255, 255))
|
||||||
|
|
||||||
|
|
||||||
|
def rotate_bound(image, angle):
|
||||||
|
height, width = image.shape[:2]
|
||||||
|
center = (width / 2, height / 2)
|
||||||
|
matrix = cv2.getRotationMatrix2D(center, angle, 1.0)
|
||||||
|
cos = abs(matrix[0, 0])
|
||||||
|
sin = abs(matrix[0, 1])
|
||||||
|
|
||||||
|
new_width = int((height * sin) + (width * cos))
|
||||||
|
new_height = int((height * cos) + (width * sin))
|
||||||
|
|
||||||
|
matrix[0, 2] += (new_width / 2) - center[0]
|
||||||
|
matrix[1, 2] += (new_height / 2) - center[1]
|
||||||
|
|
||||||
|
return cv2.warpAffine(image, matrix, (new_width, new_height), borderValue=(255, 255, 255))
|
||||||
|
|
||||||
|
|
||||||
|
def deskew_by_text_angle(image):
|
||||||
|
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
||||||
|
inverted = cv2.bitwise_not(gray)
|
||||||
|
threshold = cv2.threshold(inverted, 0, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)[1]
|
||||||
|
coordinates = np.column_stack(np.where(threshold > 0))
|
||||||
|
|
||||||
|
if len(coordinates) < 500:
|
||||||
|
return image
|
||||||
|
|
||||||
|
angle = cv2.minAreaRect(coordinates)[-1]
|
||||||
|
if angle < -45:
|
||||||
|
angle = -(90 + angle)
|
||||||
|
else:
|
||||||
|
angle = -angle
|
||||||
|
|
||||||
|
if abs(angle) < 0.2 or abs(angle) > 8:
|
||||||
|
return image
|
||||||
|
|
||||||
|
return rotate_bound(image, angle)
|
||||||
|
|
||||||
|
|
||||||
|
def find_document_contour(image, profile):
|
||||||
|
ratio = image.shape[0] / 900.0
|
||||||
|
resized = cv2.resize(image, (int(image.shape[1] / ratio), 900))
|
||||||
|
gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY)
|
||||||
|
gray = cv2.GaussianBlur(gray, (5, 5), 0)
|
||||||
|
|
||||||
|
edges = cv2.Canny(gray, 45, 140)
|
||||||
|
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (7, 7))
|
||||||
|
edges = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, kernel)
|
||||||
|
|
||||||
|
contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||||
|
contours = sorted(contours, key=cv2.contourArea, reverse=True)[:8]
|
||||||
|
|
||||||
|
min_area = resized.shape[0] * resized.shape[1] * (0.03 if profile == "receipt" else 0.12)
|
||||||
|
|
||||||
|
for contour in contours:
|
||||||
|
if cv2.contourArea(contour) < min_area:
|
||||||
|
continue
|
||||||
|
|
||||||
|
perimeter = cv2.arcLength(contour, True)
|
||||||
|
approx = cv2.approxPolyDP(contour, 0.025 * perimeter, True)
|
||||||
|
if len(approx) == 4:
|
||||||
|
return approx.reshape(4, 2) * ratio
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def trim_light_border(image):
|
||||||
|
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
||||||
|
mask = cv2.threshold(gray, 245, 255, cv2.THRESH_BINARY_INV)[1]
|
||||||
|
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (9, 9))
|
||||||
|
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
|
||||||
|
|
||||||
|
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
|
||||||
|
if not contours:
|
||||||
|
return image
|
||||||
|
|
||||||
|
contour = max(contours, key=cv2.contourArea)
|
||||||
|
if cv2.contourArea(contour) < image.shape[0] * image.shape[1] * 0.02:
|
||||||
|
return image
|
||||||
|
|
||||||
|
x, y, width, height = cv2.boundingRect(contour)
|
||||||
|
padding = max(12, int(min(width, height) * 0.025))
|
||||||
|
x = max(0, x - padding)
|
||||||
|
y = max(0, y - padding)
|
||||||
|
width = min(image.shape[1] - x, width + padding * 2)
|
||||||
|
height = min(image.shape[0] - y, height + padding * 2)
|
||||||
|
return image[y:y + height, x:x + width]
|
||||||
|
|
||||||
|
|
||||||
|
def enhance_receipt(image):
|
||||||
|
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
||||||
|
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
|
||||||
|
gray = clahe.apply(gray)
|
||||||
|
gray = cv2.fastNlMeansDenoising(gray, None, 8, 7, 21)
|
||||||
|
gray = cv2.normalize(gray, None, 0, 255, cv2.NORM_MINMAX)
|
||||||
|
return cv2.cvtColor(gray, cv2.COLOR_GRAY2BGR)
|
||||||
|
|
||||||
|
|
||||||
|
def enhance_document(image):
|
||||||
|
lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)
|
||||||
|
l_channel, a_channel, b_channel = cv2.split(lab)
|
||||||
|
clahe = cv2.createCLAHE(clipLimit=1.6, tileGridSize=(8, 8))
|
||||||
|
l_channel = clahe.apply(l_channel)
|
||||||
|
return cv2.cvtColor(cv2.merge((l_channel, a_channel, b_channel)), cv2.COLOR_LAB2BGR)
|
||||||
|
|
||||||
|
|
||||||
|
def auto_rotate_profile(image, profile):
|
||||||
|
height, width = image.shape[:2]
|
||||||
|
|
||||||
|
if profile == "receipt" and width > height:
|
||||||
|
return cv2.rotate(image, cv2.ROTATE_90_CLOCKWISE)
|
||||||
|
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
def postprocess(input_path, output_path, profile):
|
||||||
|
image = cv2.imread(str(input_path), cv2.IMREAD_COLOR)
|
||||||
|
if image is None:
|
||||||
|
raise RuntimeError(f"OpenCV konnte {input_path} nicht lesen")
|
||||||
|
|
||||||
|
contour = find_document_contour(image, profile)
|
||||||
|
if contour is not None:
|
||||||
|
processed = four_point_transform(image, contour.astype("float32"))
|
||||||
|
else:
|
||||||
|
processed = trim_light_border(image)
|
||||||
|
|
||||||
|
processed = deskew_by_text_angle(processed)
|
||||||
|
processed = trim_light_border(processed)
|
||||||
|
processed = auto_rotate_profile(processed, profile)
|
||||||
|
|
||||||
|
if profile == "receipt":
|
||||||
|
processed = enhance_receipt(processed)
|
||||||
|
elif profile != "raw":
|
||||||
|
processed = enhance_document(processed)
|
||||||
|
|
||||||
|
save_output(processed, output_path)
|
||||||
|
|
||||||
|
|
||||||
|
def save_output(image, output_path):
|
||||||
|
suffix = output_path.suffix.lower()
|
||||||
|
|
||||||
|
if suffix == ".pdf":
|
||||||
|
rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
|
||||||
|
pil_image = Image.fromarray(rgb)
|
||||||
|
if pil_image.mode != "RGB":
|
||||||
|
pil_image = pil_image.convert("RGB")
|
||||||
|
pil_image.save(output_path, "PDF", resolution=300.0)
|
||||||
|
return
|
||||||
|
|
||||||
|
if suffix in {".jpg", ".jpeg"}:
|
||||||
|
cv2.imwrite(str(output_path), image, [cv2.IMWRITE_JPEG_QUALITY, 92])
|
||||||
|
return
|
||||||
|
|
||||||
|
if suffix == ".png":
|
||||||
|
cv2.imwrite(str(output_path), image, [cv2.IMWRITE_PNG_COMPRESSION, 3])
|
||||||
|
return
|
||||||
|
|
||||||
|
if suffix in {".tif", ".tiff"}:
|
||||||
|
cv2.imwrite(str(output_path), image)
|
||||||
|
return
|
||||||
|
|
||||||
|
raise RuntimeError(f"Nicht unterstütztes Ausgabeformat: {suffix}")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="FEDEO Scan-Nachbearbeitung mit OpenCV")
|
||||||
|
parser.add_argument("--input", required=True)
|
||||||
|
parser.add_argument("--output", required=True)
|
||||||
|
parser.add_argument("--profile", default="document", choices=["document", "receipt", "raw"])
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
postprocess(Path(args.input), Path(args.output), args.profile)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
26
agents/fedeo-device-agent/scripts/setup-opencv.sh
Normal file
26
agents/fedeo-device-agent/scripts/setup-opencv.sh
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
||||||
|
AGENT_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
|
||||||
|
VENV_DIR="${FEDEO_SCAN_POSTPROCESS_VENV:-$AGENT_DIR/.venv-opencv}"
|
||||||
|
PYTHON_BIN="${PYTHON:-python3}"
|
||||||
|
|
||||||
|
echo "FEDEO OpenCV-Umgebung wird vorbereitet"
|
||||||
|
echo "Agent: $AGENT_DIR"
|
||||||
|
echo "Venv: $VENV_DIR"
|
||||||
|
|
||||||
|
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
|
||||||
|
echo "Fehler: $PYTHON_BIN wurde nicht gefunden." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
"$PYTHON_BIN" -m venv "$VENV_DIR"
|
||||||
|
"$VENV_DIR/bin/python" -m pip install --upgrade pip
|
||||||
|
"$VENV_DIR/bin/python" -m pip install -r "$AGENT_DIR/requirements-opencv.txt"
|
||||||
|
"$VENV_DIR/bin/python" -c "import cv2, PIL, numpy; print('OpenCV OK')"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Fertig. Verwende in .env:"
|
||||||
|
echo "FEDEO_SCAN_POSTPROCESS=true"
|
||||||
|
echo "FEDEO_SCAN_POSTPROCESS_PYTHON=$VENV_DIR/bin/python"
|
||||||
67
agents/fedeo-device-agent/src/api.ts
Normal file
67
agents/fedeo-device-agent/src/api.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { readFile } from "node:fs/promises"
|
||||||
|
import { basename } from "node:path"
|
||||||
|
import { AgentConfig, AgentHeartbeat, NextScanJobResponse, ScanResult } from "./types.js"
|
||||||
|
|
||||||
|
export class FedeoApi {
|
||||||
|
constructor(private readonly config: AgentConfig) {}
|
||||||
|
|
||||||
|
private url(path: string) {
|
||||||
|
return `${this.config.fedeoUrl}${path}`
|
||||||
|
}
|
||||||
|
|
||||||
|
private headers(extra?: HeadersInit): HeadersInit {
|
||||||
|
return {
|
||||||
|
"X-Agent-Token": this.config.agentToken,
|
||||||
|
...extra,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async request<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||||
|
const response = await fetch(this.url(path), {
|
||||||
|
...init,
|
||||||
|
headers: this.headers(init.headers),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const body = await response.text().catch(() => "")
|
||||||
|
throw new Error(`${init.method || "GET"} ${path} fehlgeschlagen: ${response.status} ${body}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json() as T
|
||||||
|
}
|
||||||
|
|
||||||
|
heartbeat(payload: AgentHeartbeat) {
|
||||||
|
return this.request<{ status: string; pendingScanJobs: number }>("/instance-agent/heartbeat", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
nextScanJob() {
|
||||||
|
return this.request<NextScanJobResponse>("/instance-agent/scan-jobs/next")
|
||||||
|
}
|
||||||
|
|
||||||
|
updateScanJobStatus(jobId: string, status: "running" | "failed" | "canceled", message?: string) {
|
||||||
|
return this.request(`/instance-agent/scan-jobs/${jobId}/status`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ status, message }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadScan(jobId: string, result: ScanResult) {
|
||||||
|
const form = new FormData()
|
||||||
|
const fileBuffer = await readFile(result.path)
|
||||||
|
const file = new File([fileBuffer], result.filename || basename(result.path), {
|
||||||
|
type: result.mimeType,
|
||||||
|
})
|
||||||
|
|
||||||
|
form.append("file", file)
|
||||||
|
|
||||||
|
return this.request(`/instance-agent/scan-jobs/${jobId}/upload`, {
|
||||||
|
method: "POST",
|
||||||
|
body: form,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
48
agents/fedeo-device-agent/src/commands.ts
Normal file
48
agents/fedeo-device-agent/src/commands.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { spawn } from "node:child_process"
|
||||||
|
|
||||||
|
export type CommandResult = {
|
||||||
|
stdout: string
|
||||||
|
stderr: string
|
||||||
|
code: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const commandExists = (command: string) =>
|
||||||
|
new Promise<boolean>((resolve) => {
|
||||||
|
const child = spawn("sh", ["-lc", `command -v ${command}`])
|
||||||
|
child.on("error", () => resolve(false))
|
||||||
|
child.on("close", (code) => resolve(code === 0))
|
||||||
|
})
|
||||||
|
|
||||||
|
export const runCommand = (
|
||||||
|
command: string,
|
||||||
|
args: string[],
|
||||||
|
options: { timeoutMs?: number } = {}
|
||||||
|
) =>
|
||||||
|
new Promise<CommandResult>((resolve, reject) => {
|
||||||
|
const child = spawn(command, args, {
|
||||||
|
stdio: ["ignore", "pipe", "pipe"],
|
||||||
|
})
|
||||||
|
|
||||||
|
const stdout: Buffer[] = []
|
||||||
|
const stderr: Buffer[] = []
|
||||||
|
|
||||||
|
const timeout = options.timeoutMs
|
||||||
|
? setTimeout(() => {
|
||||||
|
child.kill("SIGTERM")
|
||||||
|
reject(new Error(`${command} wurde nach ${options.timeoutMs} ms beendet`))
|
||||||
|
}, options.timeoutMs)
|
||||||
|
: null
|
||||||
|
|
||||||
|
child.stdout.on("data", (chunk) => stdout.push(Buffer.from(chunk)))
|
||||||
|
child.stderr.on("data", (chunk) => stderr.push(Buffer.from(chunk)))
|
||||||
|
child.on("error", reject)
|
||||||
|
child.on("close", (code) => {
|
||||||
|
if (timeout) clearTimeout(timeout)
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
stdout: Buffer.concat(stdout).toString("utf8"),
|
||||||
|
stderr: Buffer.concat(stderr).toString("utf8"),
|
||||||
|
code: code ?? 0,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
68
agents/fedeo-device-agent/src/config.ts
Normal file
68
agents/fedeo-device-agent/src/config.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import path from "node:path"
|
||||||
|
import os from "node:os"
|
||||||
|
import { existsSync } from "node:fs"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
import { AgentConfig } from "./types.js"
|
||||||
|
import { loadDotEnv } from "./env.js"
|
||||||
|
|
||||||
|
const currentFile = fileURLToPath(import.meta.url)
|
||||||
|
const agentRoot = path.resolve(path.dirname(currentFile), "..")
|
||||||
|
|
||||||
|
const optional = (value: string | undefined) => {
|
||||||
|
const trimmed = value?.trim()
|
||||||
|
return trimmed ? trimmed : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberFromEnv = (value: string | undefined, fallback: number) => {
|
||||||
|
if (!value) return fallback
|
||||||
|
|
||||||
|
const parsed = Number(value)
|
||||||
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
const scanFormatFromEnv = (value: string | undefined): AgentConfig["scanFormat"] => {
|
||||||
|
if (value === "png" || value === "tiff" || value === "pdf") return value
|
||||||
|
return "pdf"
|
||||||
|
}
|
||||||
|
|
||||||
|
const booleanFromEnv = (value: string | undefined, fallback: boolean) => {
|
||||||
|
if (!value) return fallback
|
||||||
|
return ["1", "true", "yes", "ja", "on"].includes(value.trim().toLowerCase())
|
||||||
|
}
|
||||||
|
|
||||||
|
const postprocessProfileFromEnv = (value: string | undefined): AgentConfig["postprocessProfile"] => {
|
||||||
|
if (value === "document" || value === "receipt" || value === "raw") return value
|
||||||
|
return "document"
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultPostprocessPython = () => {
|
||||||
|
const localVenvPython = path.join(agentRoot, ".venv-opencv", "bin", "python")
|
||||||
|
return existsSync(localVenvPython) ? localVenvPython : "python3"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadConfig = (): AgentConfig => {
|
||||||
|
loadDotEnv(process.env.FEDEO_AGENT_ENV || ".env")
|
||||||
|
|
||||||
|
const fedeoUrl = optional(process.env.FEDEO_URL)
|
||||||
|
const agentToken = optional(process.env.FEDEO_AGENT_TOKEN)
|
||||||
|
|
||||||
|
if (!fedeoUrl) throw new Error("FEDEO_URL fehlt")
|
||||||
|
if (!agentToken) throw new Error("FEDEO_AGENT_TOKEN fehlt")
|
||||||
|
|
||||||
|
return {
|
||||||
|
fedeoUrl: fedeoUrl.replace(/\/+$/, ""),
|
||||||
|
agentToken,
|
||||||
|
pollSeconds: numberFromEnv(process.env.FEDEO_POLL_SECONDS, 5),
|
||||||
|
workDir: optional(process.env.FEDEO_WORK_DIR) || path.join(os.tmpdir(), "fedeo-device-agent"),
|
||||||
|
scannerName: optional(process.env.FEDEO_SCANNER_NAME),
|
||||||
|
printerName: optional(process.env.FEDEO_PRINTER_NAME),
|
||||||
|
scanFormat: scanFormatFromEnv(process.env.FEDEO_SCAN_FORMAT),
|
||||||
|
scanResolution: numberFromEnv(process.env.FEDEO_SCAN_RESOLUTION, 300),
|
||||||
|
scanMode: optional(process.env.FEDEO_SCAN_MODE) || "Color",
|
||||||
|
scanSource: optional(process.env.FEDEO_SCAN_SOURCE),
|
||||||
|
scanPostprocess: booleanFromEnv(process.env.FEDEO_SCAN_POSTPROCESS, false),
|
||||||
|
postprocessProfile: postprocessProfileFromEnv(process.env.FEDEO_SCAN_POSTPROCESS_PROFILE),
|
||||||
|
postprocessPython: optional(process.env.FEDEO_SCAN_POSTPROCESS_PYTHON) || defaultPostprocessPython(),
|
||||||
|
postprocessStrict: booleanFromEnv(process.env.FEDEO_SCAN_POSTPROCESS_STRICT, false),
|
||||||
|
}
|
||||||
|
}
|
||||||
32
agents/fedeo-device-agent/src/env.ts
Normal file
32
agents/fedeo-device-agent/src/env.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { readFileSync, existsSync } from "node:fs"
|
||||||
|
|
||||||
|
const parseEnvLine = (line: string) => {
|
||||||
|
const trimmed = line.trim()
|
||||||
|
if (!trimmed || trimmed.startsWith("#")) return null
|
||||||
|
|
||||||
|
const separator = trimmed.indexOf("=")
|
||||||
|
if (separator === -1) return null
|
||||||
|
|
||||||
|
const key = trimmed.slice(0, separator).trim()
|
||||||
|
let value = trimmed.slice(separator + 1).trim()
|
||||||
|
|
||||||
|
if (
|
||||||
|
(value.startsWith("\"") && value.endsWith("\"")) ||
|
||||||
|
(value.startsWith("'") && value.endsWith("'"))
|
||||||
|
) {
|
||||||
|
value = value.slice(1, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { key, value }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadDotEnv = (path = ".env") => {
|
||||||
|
if (!existsSync(path)) return
|
||||||
|
|
||||||
|
const content = readFileSync(path, "utf8")
|
||||||
|
for (const line of content.split(/\r?\n/)) {
|
||||||
|
const parsed = parseEnvLine(line)
|
||||||
|
if (!parsed) continue
|
||||||
|
if (process.env[parsed.key] === undefined) process.env[parsed.key] = parsed.value
|
||||||
|
}
|
||||||
|
}
|
||||||
30
agents/fedeo-device-agent/src/logger.ts
Normal file
30
agents/fedeo-device-agent/src/logger.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
const timestamp = () => new Date().toISOString()
|
||||||
|
|
||||||
|
export const log = {
|
||||||
|
info(message: string, meta?: unknown) {
|
||||||
|
if (meta === undefined) {
|
||||||
|
console.log(`[${timestamp()}] INFO ${message}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[${timestamp()}] INFO ${message}`, meta)
|
||||||
|
},
|
||||||
|
|
||||||
|
warn(message: string, meta?: unknown) {
|
||||||
|
if (meta === undefined) {
|
||||||
|
console.warn(`[${timestamp()}] WARN ${message}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.warn(`[${timestamp()}] WARN ${message}`, meta)
|
||||||
|
},
|
||||||
|
|
||||||
|
error(message: string, meta?: unknown) {
|
||||||
|
if (meta === undefined) {
|
||||||
|
console.error(`[${timestamp()}] ERROR ${message}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(`[${timestamp()}] ERROR ${message}`, meta)
|
||||||
|
},
|
||||||
|
}
|
||||||
93
agents/fedeo-device-agent/src/main.ts
Normal file
93
agents/fedeo-device-agent/src/main.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
import os from "node:os"
|
||||||
|
import { FedeoApi } from "./api.js"
|
||||||
|
import { loadConfig } from "./config.js"
|
||||||
|
import { log } from "./logger.js"
|
||||||
|
import { listPrinters } from "./print/cups.js"
|
||||||
|
import { hasSane, listScanners, runScan } from "./scan/sane.js"
|
||||||
|
|
||||||
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
|
||||||
|
const stringifyError = (error: unknown) => {
|
||||||
|
if (error instanceof Error) return error.message
|
||||||
|
return String(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
const config = loadConfig()
|
||||||
|
const api = new FedeoApi(config)
|
||||||
|
|
||||||
|
log.info("FEDEO Geräte-Agent startet", {
|
||||||
|
platform: process.platform,
|
||||||
|
workDir: config.workDir,
|
||||||
|
pollSeconds: config.pollSeconds,
|
||||||
|
})
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const scannerNames = await listScanners()
|
||||||
|
const printerNames = await listPrinters()
|
||||||
|
const scanAvailable = await hasSane()
|
||||||
|
|
||||||
|
const heartbeat = await api.heartbeat({
|
||||||
|
capabilities: {
|
||||||
|
scan: scanAvailable,
|
||||||
|
print: printerNames.length > 0,
|
||||||
|
platform: process.platform,
|
||||||
|
},
|
||||||
|
scannerNames,
|
||||||
|
printerNames,
|
||||||
|
debugInfo: {
|
||||||
|
hostname: os.hostname(),
|
||||||
|
release: os.release(),
|
||||||
|
arch: os.arch(),
|
||||||
|
node: process.version,
|
||||||
|
uptimeSeconds: Math.round(os.uptime()),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (heartbeat.pendingScanJobs > 0) {
|
||||||
|
log.info(`${heartbeat.pendingScanJobs} Scan-Auftrag/Aufträge warten`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = await api.nextScanJob()
|
||||||
|
if (!next.job) {
|
||||||
|
await sleep(config.pollSeconds * 1000)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Scan-Auftrag wird ausgeführt", {
|
||||||
|
jobId: next.job.id,
|
||||||
|
tenantId: next.job.tenantId,
|
||||||
|
scannerName: next.job.scannerName || config.scannerName || "default",
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.updateScanJobStatus(next.job.id, "running")
|
||||||
|
const scanResult = await runScan(config, next.job)
|
||||||
|
await api.uploadScan(next.job.id, scanResult)
|
||||||
|
|
||||||
|
log.info("Scan-Auftrag abgeschlossen", {
|
||||||
|
jobId: next.job.id,
|
||||||
|
file: scanResult.filename,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
const message = stringifyError(error)
|
||||||
|
log.error("Scan-Auftrag fehlgeschlagen", {
|
||||||
|
jobId: next.job.id,
|
||||||
|
message,
|
||||||
|
})
|
||||||
|
|
||||||
|
await api.updateScanJobStatus(next.job.id, "failed", message)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
log.error("Agent-Schleife fehlgeschlagen", stringifyError(error))
|
||||||
|
await sleep(config.pollSeconds * 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
log.error("Agent konnte nicht gestartet werden", stringifyError(error))
|
||||||
|
process.exit(1)
|
||||||
|
})
|
||||||
15
agents/fedeo-device-agent/src/print/cups.ts
Normal file
15
agents/fedeo-device-agent/src/print/cups.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { commandExists, runCommand } from "../commands.js"
|
||||||
|
|
||||||
|
export const hasCups = () => commandExists("lpstat")
|
||||||
|
|
||||||
|
export const listPrinters = async () => {
|
||||||
|
if (!await hasCups()) return []
|
||||||
|
|
||||||
|
const result = await runCommand("lpstat", ["-p"], { timeoutMs: 10_000 })
|
||||||
|
if (result.code !== 0) return []
|
||||||
|
|
||||||
|
return result.stdout
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line.match(/^printer\s+(\S+)/)?.[1])
|
||||||
|
.filter((printer): printer is string => Boolean(printer))
|
||||||
|
}
|
||||||
66
agents/fedeo-device-agent/src/scan/postprocess.ts
Normal file
66
agents/fedeo-device-agent/src/scan/postprocess.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import path from "node:path"
|
||||||
|
import { fileURLToPath } from "node:url"
|
||||||
|
import { AgentConfig, ScanResult } from "../types.js"
|
||||||
|
import { commandExists, runCommand } from "../commands.js"
|
||||||
|
|
||||||
|
const currentFile = fileURLToPath(import.meta.url)
|
||||||
|
const agentRoot = path.resolve(path.dirname(currentFile), "../..")
|
||||||
|
const postprocessScript = path.join(agentRoot, "scripts/opencv_postprocess.py")
|
||||||
|
|
||||||
|
const extensionMimeTypes: Record<string, string> = {
|
||||||
|
".pdf": "application/pdf",
|
||||||
|
".png": "image/png",
|
||||||
|
".tif": "image/tiff",
|
||||||
|
".tiff": "image/tiff",
|
||||||
|
".jpg": "image/jpeg",
|
||||||
|
".jpeg": "image/jpeg",
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureOutputExtension = (filename: string, format: AgentConfig["scanFormat"]) => {
|
||||||
|
const ext = path.extname(filename)
|
||||||
|
if (ext) return filename
|
||||||
|
return `${filename}.${format}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasOpenCvPostprocessRuntime = async (config: AgentConfig) => {
|
||||||
|
if (!await commandExists(config.postprocessPython)) return false
|
||||||
|
|
||||||
|
const result = await runCommand(config.postprocessPython, [
|
||||||
|
"-c",
|
||||||
|
"import cv2, PIL, numpy",
|
||||||
|
], { timeoutMs: 10_000 })
|
||||||
|
|
||||||
|
return result.code === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export const postprocessScan = async (
|
||||||
|
config: AgentConfig,
|
||||||
|
inputPath: string,
|
||||||
|
outputFilename: string,
|
||||||
|
outputFormat: AgentConfig["scanFormat"],
|
||||||
|
profile: AgentConfig["postprocessProfile"]
|
||||||
|
): Promise<ScanResult> => {
|
||||||
|
const filename = ensureOutputExtension(outputFilename, outputFormat)
|
||||||
|
const outputPath = path.join(config.workDir, filename)
|
||||||
|
|
||||||
|
const result = await runCommand(config.postprocessPython, [
|
||||||
|
postprocessScript,
|
||||||
|
"--input",
|
||||||
|
inputPath,
|
||||||
|
"--output",
|
||||||
|
outputPath,
|
||||||
|
"--profile",
|
||||||
|
profile,
|
||||||
|
], { timeoutMs: 5 * 60 * 1000 })
|
||||||
|
|
||||||
|
if (result.code !== 0) {
|
||||||
|
throw new Error(result.stderr || `OpenCV-Nachbearbeitung wurde mit Code ${result.code} beendet`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const extension = path.extname(outputPath).toLowerCase()
|
||||||
|
return {
|
||||||
|
path: outputPath,
|
||||||
|
filename,
|
||||||
|
mimeType: extensionMimeTypes[extension] || "application/octet-stream",
|
||||||
|
}
|
||||||
|
}
|
||||||
149
agents/fedeo-device-agent/src/scan/sane.ts
Normal file
149
agents/fedeo-device-agent/src/scan/sane.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { mkdirSync } from "node:fs"
|
||||||
|
import path from "node:path"
|
||||||
|
import { AgentConfig, ScanJob, ScanResult } from "../types.js"
|
||||||
|
import { commandExists, runCommand } from "../commands.js"
|
||||||
|
import { hasOpenCvPostprocessRuntime, postprocessScan } from "./postprocess.js"
|
||||||
|
import { log } from "../logger.js"
|
||||||
|
|
||||||
|
const mimeTypes = {
|
||||||
|
pdf: "application/pdf",
|
||||||
|
png: "image/png",
|
||||||
|
tiff: "image/tiff",
|
||||||
|
}
|
||||||
|
|
||||||
|
const stringSetting = (settings: Record<string, unknown> | undefined, key: string) => {
|
||||||
|
const value = settings?.[key]
|
||||||
|
return typeof value === "string" && value.trim() ? value.trim() : undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const numberSetting = (settings: Record<string, unknown> | undefined, key: string) => {
|
||||||
|
const value = settings?.[key]
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) return value
|
||||||
|
if (typeof value === "string" && value.trim()) {
|
||||||
|
const parsed = Number(value)
|
||||||
|
if (Number.isFinite(parsed)) return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const booleanSetting = (settings: Record<string, unknown> | undefined, key: string, fallback: boolean) => {
|
||||||
|
const value = settings?.[key]
|
||||||
|
if (typeof value === "boolean") return value
|
||||||
|
if (typeof value === "string") return ["1", "true", "yes", "ja", "on"].includes(value.trim().toLowerCase())
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileSetting = (
|
||||||
|
settings: Record<string, unknown> | undefined,
|
||||||
|
fallback: AgentConfig["postprocessProfile"]
|
||||||
|
): AgentConfig["postprocessProfile"] => {
|
||||||
|
const value = settings?.postprocessProfile
|
||||||
|
if (value === "document" || value === "receipt" || value === "raw") return value
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
const ensureFilenameExtension = (filename: string, format: AgentConfig["scanFormat"]) => {
|
||||||
|
const ext = path.extname(filename)
|
||||||
|
if (!ext) return `${filename}.${format}`
|
||||||
|
|
||||||
|
const expectedExt = `.${format}`
|
||||||
|
if (ext.toLowerCase() === expectedExt) return filename
|
||||||
|
return `${filename.slice(0, -ext.length)}${expectedExt}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const fallbackRawResult = (scanOutputPath: string, jobId: string): ScanResult => ({
|
||||||
|
path: scanOutputPath,
|
||||||
|
filename: `${jobId}.raw.png`,
|
||||||
|
mimeType: "image/png",
|
||||||
|
})
|
||||||
|
|
||||||
|
export const hasSane = () => commandExists("scanimage")
|
||||||
|
|
||||||
|
export const listScanners = async () => {
|
||||||
|
if (!await hasSane()) return []
|
||||||
|
|
||||||
|
const result = await runCommand("scanimage", ["-L"], { timeoutMs: 10_000 })
|
||||||
|
if (result.code !== 0) return []
|
||||||
|
|
||||||
|
return result.stdout
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter((line) => line.startsWith("device `"))
|
||||||
|
.map((line) => line.match(/device `([^']+)'/)?.[1])
|
||||||
|
.filter((device): device is string => Boolean(device))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const runScan = async (config: AgentConfig, job: ScanJob): Promise<ScanResult> => {
|
||||||
|
if (!await hasSane()) {
|
||||||
|
throw new Error("scanimage ist nicht installiert oder nicht im PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync(config.workDir, { recursive: true })
|
||||||
|
|
||||||
|
const settings = job.settings || {}
|
||||||
|
const format = stringSetting(settings, "format") as AgentConfig["scanFormat"] | undefined || config.scanFormat
|
||||||
|
const resolution = numberSetting(settings, "resolution") || config.scanResolution
|
||||||
|
const mode = stringSetting(settings, "mode") || config.scanMode
|
||||||
|
const source = stringSetting(settings, "source") || config.scanSource
|
||||||
|
const scannerName = job.scannerName || config.scannerName
|
||||||
|
const filename = ensureFilenameExtension(job.requestedFilename || `${job.id}.${format}`, format)
|
||||||
|
const outputPath = path.join(config.workDir, filename)
|
||||||
|
const shouldPostprocess = booleanSetting(settings, "postprocess", config.scanPostprocess)
|
||||||
|
const postprocessProfile = profileSetting(settings, config.postprocessProfile)
|
||||||
|
const scanFormat = shouldPostprocess ? "png" : format
|
||||||
|
const scanOutputPath = shouldPostprocess
|
||||||
|
? path.join(config.workDir, `${job.id}.raw.png`)
|
||||||
|
: outputPath
|
||||||
|
|
||||||
|
const args = [
|
||||||
|
"--format",
|
||||||
|
scanFormat,
|
||||||
|
"--resolution",
|
||||||
|
String(resolution),
|
||||||
|
"--mode",
|
||||||
|
mode,
|
||||||
|
"--output-file",
|
||||||
|
scanOutputPath,
|
||||||
|
]
|
||||||
|
|
||||||
|
if (source) args.push("--source", source)
|
||||||
|
if (scannerName) args.push("--device-name", scannerName)
|
||||||
|
|
||||||
|
const result = await runCommand("scanimage", args, { timeoutMs: 5 * 60 * 1000 })
|
||||||
|
|
||||||
|
if (result.code !== 0) {
|
||||||
|
throw new Error(result.stderr || `scanimage wurde mit Code ${result.code} beendet`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldPostprocess) {
|
||||||
|
if (!await hasOpenCvPostprocessRuntime(config)) {
|
||||||
|
const message = "OpenCV-Nachbearbeitung ist aktiviert, aber python3 mit cv2, Pillow und numpy ist nicht verfügbar"
|
||||||
|
if (config.postprocessStrict) throw new Error(message)
|
||||||
|
|
||||||
|
log.warn(`${message}. Rohscan wird ohne Korrektur hochgeladen.`, {
|
||||||
|
jobId: job.id,
|
||||||
|
python: config.postprocessPython,
|
||||||
|
})
|
||||||
|
return fallbackRawResult(scanOutputPath, job.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await postprocessScan(config, scanOutputPath, filename, format, postprocessProfile)
|
||||||
|
} catch (error) {
|
||||||
|
if (config.postprocessStrict) throw error
|
||||||
|
|
||||||
|
log.warn("OpenCV-Nachbearbeitung fehlgeschlagen. Rohscan wird ohne Korrektur hochgeladen.", {
|
||||||
|
jobId: job.id,
|
||||||
|
error: error instanceof Error ? error.message : String(error),
|
||||||
|
})
|
||||||
|
return fallbackRawResult(scanOutputPath, job.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
path: outputPath,
|
||||||
|
filename,
|
||||||
|
mimeType: mimeTypes[format] || "application/octet-stream",
|
||||||
|
}
|
||||||
|
}
|
||||||
48
agents/fedeo-device-agent/src/types.ts
Normal file
48
agents/fedeo-device-agent/src/types.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
export type AgentConfig = {
|
||||||
|
fedeoUrl: string
|
||||||
|
agentToken: string
|
||||||
|
pollSeconds: number
|
||||||
|
workDir: string
|
||||||
|
scannerName?: string
|
||||||
|
printerName?: string
|
||||||
|
scanFormat: "pdf" | "png" | "tiff"
|
||||||
|
scanResolution: number
|
||||||
|
scanMode: string
|
||||||
|
scanSource?: string
|
||||||
|
scanPostprocess: boolean
|
||||||
|
postprocessProfile: "document" | "receipt" | "raw"
|
||||||
|
postprocessPython: string
|
||||||
|
postprocessStrict: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AgentHeartbeat = {
|
||||||
|
capabilities: {
|
||||||
|
scan: boolean
|
||||||
|
print: boolean
|
||||||
|
platform: NodeJS.Platform
|
||||||
|
}
|
||||||
|
scannerNames: string[]
|
||||||
|
printerNames: string[]
|
||||||
|
debugInfo: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScanJob = {
|
||||||
|
id: string
|
||||||
|
tenantId: number
|
||||||
|
agentId: string
|
||||||
|
status: string
|
||||||
|
scannerName?: string | null
|
||||||
|
requestedFilename?: string | null
|
||||||
|
settings?: Record<string, unknown>
|
||||||
|
target?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NextScanJobResponse = {
|
||||||
|
job: ScanJob | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScanResult = {
|
||||||
|
path: string
|
||||||
|
filename: string
|
||||||
|
mimeType: string
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=FEDEO Geräte-Agent
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
EnvironmentFile=/etc/fedeo-device-agent/config.env
|
||||||
|
WorkingDirectory=/opt/fedeo-device-agent
|
||||||
|
ExecStart=/usr/bin/node /opt/fedeo-device-agent/dist/main.js
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
User=fedeo-agent
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
||||||
|
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>Label</key>
|
||||||
|
<string>com.fedeo.device-agent</string>
|
||||||
|
|
||||||
|
<key>ProgramArguments</key>
|
||||||
|
<array>
|
||||||
|
<string>/usr/local/bin/node</string>
|
||||||
|
<string>/opt/fedeo-device-agent/dist/main.js</string>
|
||||||
|
</array>
|
||||||
|
|
||||||
|
<key>EnvironmentVariables</key>
|
||||||
|
<dict>
|
||||||
|
<key>FEDEO_AGENT_ENV</key>
|
||||||
|
<string>/opt/fedeo-device-agent/.env</string>
|
||||||
|
</dict>
|
||||||
|
|
||||||
|
<key>RunAtLoad</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<key>KeepAlive</key>
|
||||||
|
<true/>
|
||||||
|
|
||||||
|
<key>StandardOutPath</key>
|
||||||
|
<string>/tmp/fedeo-device-agent.log</string>
|
||||||
|
|
||||||
|
<key>StandardErrorPath</key>
|
||||||
|
<string>/tmp/fedeo-device-agent.err.log</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
17
agents/fedeo-device-agent/tsconfig.json
Normal file
17
agents/fedeo-device-agent/tsconfig.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"lib": ["ES2022", "DOM"],
|
||||||
|
"types": ["node"],
|
||||||
|
"typeRoots": ["../../backend/node_modules/@types"],
|
||||||
|
"rootDir": "src",
|
||||||
|
"outDir": "dist",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"]
|
||||||
|
}
|
||||||
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
@@ -3,3 +3,4 @@ node_modules
|
|||||||
.env
|
.env
|
||||||
|
|
||||||
/src/generated/prisma
|
/src/generated/prisma
|
||||||
|
/dist/
|
||||||
|
|||||||
3
backend/.secretlintrc.json
Normal file
3
backend/.secretlintrc.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"rules": []
|
||||||
|
}
|
||||||
@@ -1,11 +1,21 @@
|
|||||||
FROM node:20-alpine
|
FROM node:20-bookworm-slim
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
poppler-utils \
|
||||||
|
tesseract-ocr \
|
||||||
|
tesseract-ocr-deu \
|
||||||
|
tesseract-ocr-eng \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Package-Dateien
|
# Package-Dateien
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Dev + Prod Dependencies (für TS-Build nötig)
|
# Dev + Prod Dependencies (für TS-Build nötig).
|
||||||
RUN npm install
|
# Sharp benötigt im Linux-Container native optionale Pakete, auch wenn das Lockfile auf macOS erzeugt wurde.
|
||||||
|
RUN npm install --include=optional \
|
||||||
|
&& npm install --include=optional --os=linux --cpu=x64 sharp
|
||||||
|
|
||||||
# Restlicher Sourcecode
|
# Restlicher Sourcecode
|
||||||
COPY . .
|
COPY . .
|
||||||
@@ -16,5 +26,5 @@ RUN npm run build
|
|||||||
# Port freigeben
|
# Port freigeben
|
||||||
EXPOSE 3100
|
EXPOSE 3100
|
||||||
|
|
||||||
# Start der App
|
# Migrationen ausführen und App starten
|
||||||
CMD ["node", "dist/src/index.js"]
|
CMD ["sh", "./docker-entrypoint.sh"]
|
||||||
|
|||||||
@@ -1,13 +1,33 @@
|
|||||||
import { drizzle } from "drizzle-orm/node-postgres"
|
// src/db/index.ts
|
||||||
import { Pool } from "pg"
|
import { drizzle } from "drizzle-orm/node-postgres";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
import * as schema from "./schema";
|
||||||
import {secrets} from "../src/utils/secrets";
|
import {secrets} from "../src/utils/secrets";
|
||||||
import * as schema from "./schema"
|
|
||||||
|
|
||||||
|
console.log("[DB INIT] 1. Suche Connection String...");
|
||||||
|
|
||||||
|
const fallbackConnectionString = "postgres://postgres:wJw7aNpEBJdcxgoct6GXNpvY4Cn6ECqu@fedeo-db-001.vpn.internal:5432/fedeo"
|
||||||
|
|
||||||
|
// Checken woher die URL kommt
|
||||||
|
let connectionString = process.env.DATABASE_URL || secrets.DATABASE_URL || fallbackConnectionString;
|
||||||
|
if (process.env.DATABASE_URL) {
|
||||||
|
console.log("[DB INIT] -> Gefunden in process.env.DATABASE_URL");
|
||||||
|
} else if (secrets.DATABASE_URL) {
|
||||||
|
console.log("[DB INIT] -> Gefunden in secrets.DATABASE_URL");
|
||||||
|
} else if (connectionString) {
|
||||||
|
console.log("[DB INIT] -> Nutze Fallback aus dem Projekt");
|
||||||
|
} else {
|
||||||
|
console.error("[DB INIT] ❌ KEIN CONNECTION STRING GEFUNDEN! .env nicht geladen?");
|
||||||
|
}
|
||||||
|
|
||||||
export const pool = new Pool({
|
export const pool = new Pool({
|
||||||
connectionString: secrets.DATABASE_URL,
|
connectionString,
|
||||||
max: 10, // je nach Last
|
max: 10,
|
||||||
})
|
});
|
||||||
|
|
||||||
export const db = drizzle(pool , {schema})
|
// TEST: Ist die DB wirklich da?
|
||||||
|
pool.query('SELECT NOW()')
|
||||||
|
.then(res => console.log(`[DB INIT] ✅ VERBINDUNG ERFOLGREICH! Zeit auf DB: ${res.rows[0].now}`))
|
||||||
|
.catch(err => console.error(`[DB INIT] ❌ VERBINDUNGSFEHLER:`, err.message));
|
||||||
|
|
||||||
|
export const db = drizzle(pool, { schema });
|
||||||
|
|||||||
2
backend/db/migrations/0003_woozy_adam_destine.sql
Normal file
2
backend/db/migrations/0003_woozy_adam_destine.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- No-op migration: Datei war im Journal referenziert, aber fehlte im Repository.
|
||||||
|
SELECT 1;
|
||||||
2
backend/db/migrations/0004_stormy_onslaught.sql
Normal file
2
backend/db/migrations/0004_stormy_onslaught.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
-- No-op migration: Datei war im Journal referenziert, aber fehlte im Repository.
|
||||||
|
SELECT 1;
|
||||||
95
backend/db/migrations/0005_green_shinobi_shaw.sql
Normal file
95
backend/db/migrations/0005_green_shinobi_shaw.sql
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
CREATE TABLE "m2m_api_keys" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"tenant_id" bigint NOT NULL,
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
"created_by" uuid,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"key_prefix" text NOT NULL,
|
||||||
|
"key_hash" text NOT NULL,
|
||||||
|
"active" boolean DEFAULT true NOT NULL,
|
||||||
|
"last_used_at" timestamp with time zone,
|
||||||
|
"expires_at" timestamp with time zone,
|
||||||
|
CONSTRAINT "m2m_api_keys_key_hash_unique" UNIQUE("key_hash")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "serialtypes" (
|
||||||
|
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "serialtypes_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"intervall" text,
|
||||||
|
"icon" text,
|
||||||
|
"tenant" bigint NOT NULL,
|
||||||
|
"archived" boolean DEFAULT false NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone,
|
||||||
|
"updated_by" uuid
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "serial_executions" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"tenant" bigint NOT NULL,
|
||||||
|
"execution_date" timestamp NOT NULL,
|
||||||
|
"status" text DEFAULT 'draft',
|
||||||
|
"created_by" text,
|
||||||
|
"created_at" timestamp DEFAULT now(),
|
||||||
|
"summary" text
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "public_links" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"token" text NOT NULL,
|
||||||
|
"tenant" integer NOT NULL,
|
||||||
|
"default_profile" uuid,
|
||||||
|
"is_protected" boolean DEFAULT false NOT NULL,
|
||||||
|
"pin_hash" text,
|
||||||
|
"config" jsonb DEFAULT '{}'::jsonb,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"active" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now(),
|
||||||
|
"updated_at" timestamp DEFAULT now(),
|
||||||
|
CONSTRAINT "public_links_token_unique" UNIQUE("token")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "wiki_pages" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"tenant_id" bigint NOT NULL,
|
||||||
|
"parent_id" uuid,
|
||||||
|
"title" text NOT NULL,
|
||||||
|
"content" jsonb,
|
||||||
|
"is_folder" boolean DEFAULT false NOT NULL,
|
||||||
|
"sort_order" integer DEFAULT 0 NOT NULL,
|
||||||
|
"entity_type" text,
|
||||||
|
"entity_id" bigint,
|
||||||
|
"entity_uuid" uuid,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone,
|
||||||
|
"created_by" uuid,
|
||||||
|
"updated_by" uuid
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "projects" ALTER COLUMN "active_phase" SET DEFAULT 'Erstkontakt';--> statement-breakpoint
|
||||||
|
ALTER TABLE "createddocuments" ADD COLUMN "serialexecution" uuid;--> statement-breakpoint
|
||||||
|
ALTER TABLE "devices" ADD COLUMN "last_seen" timestamp with time zone;--> statement-breakpoint
|
||||||
|
ALTER TABLE "devices" ADD COLUMN "last_debug_info" jsonb;--> statement-breakpoint
|
||||||
|
ALTER TABLE "files" ADD COLUMN "size" bigint;--> statement-breakpoint
|
||||||
|
ALTER TABLE "staff_time_events" ADD COLUMN "related_event_id" uuid;--> statement-breakpoint
|
||||||
|
ALTER TABLE "m2m_api_keys" ADD CONSTRAINT "m2m_api_keys_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "m2m_api_keys" ADD CONSTRAINT "m2m_api_keys_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "m2m_api_keys" ADD CONSTRAINT "m2m_api_keys_created_by_auth_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id") ON DELETE set null ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "staff_time_events" ADD CONSTRAINT "staff_time_events_related_event_id_staff_time_events_id_fk" FOREIGN KEY ("related_event_id") REFERENCES "public"."staff_time_events"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "serialtypes" ADD CONSTRAINT "serialtypes_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "serialtypes" ADD CONSTRAINT "serialtypes_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "serial_executions" ADD CONSTRAINT "serial_executions_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "public_links" ADD CONSTRAINT "public_links_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "public_links" ADD CONSTRAINT "public_links_default_profile_auth_profiles_id_fk" FOREIGN KEY ("default_profile") REFERENCES "public"."auth_profiles"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "wiki_pages" ADD CONSTRAINT "wiki_pages_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "wiki_pages" ADD CONSTRAINT "wiki_pages_parent_id_wiki_pages_id_fk" FOREIGN KEY ("parent_id") REFERENCES "public"."wiki_pages"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "wiki_pages" ADD CONSTRAINT "wiki_pages_created_by_auth_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "wiki_pages" ADD CONSTRAINT "wiki_pages_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
|
CREATE INDEX "wiki_pages_tenant_idx" ON "wiki_pages" USING btree ("tenant_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "wiki_pages_parent_idx" ON "wiki_pages" USING btree ("parent_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "wiki_pages_entity_int_idx" ON "wiki_pages" USING btree ("tenant_id","entity_type","entity_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "wiki_pages_entity_uuid_idx" ON "wiki_pages" USING btree ("tenant_id","entity_type","entity_uuid");--> statement-breakpoint
|
||||||
|
ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_serialexecution_serial_executions_id_fk" FOREIGN KEY ("serialexecution") REFERENCES "public"."serial_executions"("id") ON DELETE no action ON UPDATE no action;
|
||||||
1
backend/db/migrations/0006_nifty_price_lock.sql
Normal file
1
backend/db/migrations/0006_nifty_price_lock.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "services" ADD COLUMN "priceUpdateLocked" boolean DEFAULT false NOT NULL;
|
||||||
1
backend/db/migrations/0007_bright_default_tax_type.sql
Normal file
1
backend/db/migrations/0007_bright_default_tax_type.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "customers" ADD COLUMN "customTaxType" text;
|
||||||
16
backend/db/migrations/0008_quick_contracttypes.sql
Normal file
16
backend/db/migrations/0008_quick_contracttypes.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
CREATE TABLE "contracttypes" (
|
||||||
|
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "contracttypes_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"tenant" bigint NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"paymentType" text,
|
||||||
|
"recurring" boolean DEFAULT false NOT NULL,
|
||||||
|
"archived" boolean DEFAULT false NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone,
|
||||||
|
"updated_by" uuid
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "contracttypes" ADD CONSTRAINT "contracttypes_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "contracttypes" ADD CONSTRAINT "contracttypes_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "contracts" ADD COLUMN "contracttype" bigint;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "contracts" ADD CONSTRAINT "contracts_contracttype_contracttypes_id_fk" FOREIGN KEY ("contracttype") REFERENCES "public"."contracttypes"("id") ON DELETE no action ON UPDATE no action;
|
||||||
3
backend/db/migrations/0010_sudden_billing_interval.sql
Normal file
3
backend/db/migrations/0010_sudden_billing_interval.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "contracttypes" ADD COLUMN "billingInterval" text;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "contracts" ADD COLUMN "billingInterval" text;
|
||||||
16
backend/db/migrations/0011_mighty_member_bankaccounts.sql
Normal file
16
backend/db/migrations/0011_mighty_member_bankaccounts.sql
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
CREATE TABLE "entitybankaccounts" (
|
||||||
|
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "entitybankaccounts_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"tenant" bigint NOT NULL,
|
||||||
|
"iban_encrypted" jsonb NOT NULL,
|
||||||
|
"bic_encrypted" jsonb NOT NULL,
|
||||||
|
"bank_name_encrypted" jsonb NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"updated_at" timestamp with time zone,
|
||||||
|
"updated_by" uuid,
|
||||||
|
"archived" boolean DEFAULT false NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "entitybankaccounts" ADD CONSTRAINT "entitybankaccounts_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "entitybankaccounts" ADD CONSTRAINT "entitybankaccounts_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
|
||||||
73
backend/db/migrations/0012_shiny_customer_inventory.sql
Normal file
73
backend/db/migrations/0012_shiny_customer_inventory.sql
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
CREATE TABLE "customerspaces" (
|
||||||
|
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "customerspaces_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"type" text NOT NULL,
|
||||||
|
"tenant" bigint NOT NULL,
|
||||||
|
"customer" bigint NOT NULL,
|
||||||
|
"spaceNumber" text NOT NULL,
|
||||||
|
"parentSpace" bigint,
|
||||||
|
"infoData" jsonb DEFAULT '{"zip":"","city":"","streetNumber":""}'::jsonb NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"archived" boolean DEFAULT false NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone,
|
||||||
|
"updated_by" uuid
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "customerinventoryitems" (
|
||||||
|
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "customerinventoryitems_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"tenant" bigint NOT NULL,
|
||||||
|
"customer" bigint NOT NULL,
|
||||||
|
"customerspace" bigint,
|
||||||
|
"customerInventoryId" text NOT NULL,
|
||||||
|
"serialNumber" text,
|
||||||
|
"quantity" bigint DEFAULT 0 NOT NULL,
|
||||||
|
"manufacturer" text,
|
||||||
|
"manufacturerNumber" text,
|
||||||
|
"purchaseDate" date,
|
||||||
|
"purchasePrice" double precision DEFAULT 0,
|
||||||
|
"currentValue" double precision,
|
||||||
|
"product" bigint,
|
||||||
|
"archived" boolean DEFAULT false NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone,
|
||||||
|
"updated_by" uuid
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "customerspaces" ADD CONSTRAINT "customerspaces_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "customerspaces" ADD CONSTRAINT "customerspaces_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "customerspaces" ADD CONSTRAINT "customerspaces_parentSpace_customerspaces_id_fk" FOREIGN KEY ("parentSpace") REFERENCES "public"."customerspaces"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "customerspaces" ADD CONSTRAINT "customerspaces_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_customerspace_customerspaces_id_fk" FOREIGN KEY ("customerspace") REFERENCES "public"."customerspaces"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_product_products_id_fk" FOREIGN KEY ("product") REFERENCES "public"."products"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "customerinventoryitems_tenant_customerInventoryId_idx" ON "customerinventoryitems" USING btree ("tenant","customerInventoryId");
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "historyitems" ADD COLUMN "customerspace" bigint;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "historyitems" ADD COLUMN "customerinventoryitem" bigint;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_customerspace_customerspaces_id_fk" FOREIGN KEY ("customerspace") REFERENCES "public"."customerspaces"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_customerinventoryitem_customerinventoryitems_id_fk" FOREIGN KEY ("customerinventoryitem") REFERENCES "public"."customerinventoryitems"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "tenants" ALTER COLUMN "numberRanges" SET DEFAULT '{"vendors":{"prefix":"","suffix":"","nextNumber":10000},"customers":{"prefix":"","suffix":"","nextNumber":10000},"products":{"prefix":"AT-","suffix":"","nextNumber":1000},"quotes":{"prefix":"AN-","suffix":"","nextNumber":1000},"confirmationOrders":{"prefix":"AB-","suffix":"","nextNumber":1000},"invoices":{"prefix":"RE-","suffix":"","nextNumber":1000},"spaces":{"prefix":"LP-","suffix":"","nextNumber":1000},"customerspaces":{"prefix":"KLP-","suffix":"","nextNumber":1000},"inventoryitems":{"prefix":"IA-","suffix":"","nextNumber":1000},"customerinventoryitems":{"prefix":"KIA-","suffix":"","nextNumber":1000},"projects":{"prefix":"PRJ-","suffix":"","nextNumber":1000},"costcentres":{"prefix":"KST-","suffix":"","nextNumber":1000}}'::jsonb;
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "tenants"
|
||||||
|
SET "numberRanges" = COALESCE("numberRanges", '{}'::jsonb) || jsonb_build_object(
|
||||||
|
'customerspaces', COALESCE("numberRanges"->'customerspaces', '{"prefix":"KLP-","suffix":"","nextNumber":1000}'::jsonb),
|
||||||
|
'customerinventoryitems', COALESCE("numberRanges"->'customerinventoryitems', '{"prefix":"KIA-","suffix":"","nextNumber":1000}'::jsonb)
|
||||||
|
);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "customerinventoryitems" ADD COLUMN "vendor" bigint;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "customerinventoryitems" ADD CONSTRAINT "customerinventoryitems_vendor_vendors_id_fk" FOREIGN KEY ("vendor") REFERENCES "public"."vendors"("id") ON DELETE no action ON UPDATE no action;
|
||||||
20
backend/db/migrations/0014_smart_memberrelations.sql
Normal file
20
backend/db/migrations/0014_smart_memberrelations.sql
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
CREATE TABLE "memberrelations" (
|
||||||
|
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "memberrelations_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"tenant" bigint NOT NULL,
|
||||||
|
"type" text NOT NULL,
|
||||||
|
"billingInterval" text NOT NULL,
|
||||||
|
"billingAmount" double precision DEFAULT 0 NOT NULL,
|
||||||
|
"archived" boolean DEFAULT false NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone,
|
||||||
|
"updated_by" uuid
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "customers" ADD COLUMN "memberrelation" bigint;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "memberrelations" ADD CONSTRAINT "memberrelations_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "memberrelations" ADD CONSTRAINT "memberrelations_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "customers" ADD CONSTRAINT "customers_memberrelation_memberrelations_id_fk" FOREIGN KEY ("memberrelation") REFERENCES "public"."memberrelations"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
ALTER TABLE "historyitems" ADD COLUMN "memberrelation" bigint;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_memberrelation_memberrelations_id_fk" FOREIGN KEY ("memberrelation") REFERENCES "public"."memberrelations"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
ALTER TABLE "customers" ADD COLUMN IF NOT EXISTS "memberrelation" bigint;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_constraint
|
||||||
|
WHERE conname = 'customers_memberrelation_memberrelations_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "customers"
|
||||||
|
ADD CONSTRAINT "customers_memberrelation_memberrelations_id_fk"
|
||||||
|
FOREIGN KEY ("memberrelation")
|
||||||
|
REFERENCES "public"."memberrelations"("id")
|
||||||
|
ON DELETE no action
|
||||||
|
ON UPDATE no action;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
UPDATE "customers"
|
||||||
|
SET "memberrelation" = ("infoData"->>'memberrelation')::bigint
|
||||||
|
WHERE
|
||||||
|
"memberrelation" IS NULL
|
||||||
|
AND "type" = 'Mitglied'
|
||||||
|
AND jsonb_typeof(COALESCE("infoData", '{}'::jsonb)) = 'object'
|
||||||
|
AND COALESCE("infoData", '{}'::jsonb) ? 'memberrelation'
|
||||||
|
AND ("infoData"->>'memberrelation') ~ '^[0-9]+$';
|
||||||
|
|
||||||
|
UPDATE "customers"
|
||||||
|
SET "infoData" = COALESCE("infoData", '{}'::jsonb) - 'memberrelation'
|
||||||
|
WHERE
|
||||||
|
"type" = 'Mitglied'
|
||||||
|
AND jsonb_typeof(COALESCE("infoData", '{}'::jsonb)) = 'object'
|
||||||
|
AND COALESCE("infoData", '{}'::jsonb) ? 'memberrelation';
|
||||||
1
backend/db/migrations/0017_slow_the_hood.sql
Normal file
1
backend/db/migrations/0017_slow_the_hood.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
-- Absichtlich leer: Die Objekte aus dieser generierten Migration existieren bereits in früheren Migrationen.
|
||||||
3
backend/db/migrations/0018_account_chart.sql
Normal file
3
backend/db/migrations/0018_account_chart.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "accounts" ADD COLUMN "accountChart" text DEFAULT 'skr03' NOT NULL;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "tenants" ADD COLUMN "accountChart" text DEFAULT 'skr03' NOT NULL;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "createddocuments"
|
||||||
|
ALTER COLUMN "customSurchargePercentage" TYPE double precision
|
||||||
|
USING "customSurchargePercentage"::double precision;
|
||||||
1
backend/db/migrations/0020_file_extracted_text.sql
Normal file
1
backend/db/migrations/0020_file_extracted_text.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "files" ADD COLUMN "extracted_text" text;
|
||||||
2
backend/db/migrations/0021_admin_user_flag.sql
Normal file
2
backend/db/migrations/0021_admin_user_flag.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "auth_users"
|
||||||
|
ADD COLUMN "is_admin" boolean NOT NULL DEFAULT false;
|
||||||
2
backend/db/migrations/0022_task_dependencies.sql
Normal file
2
backend/db/migrations/0022_task_dependencies.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "tasks"
|
||||||
|
ADD COLUMN "dependency_ids" jsonb NOT NULL DEFAULT '[]'::jsonb;
|
||||||
2
backend/db/migrations/0023_tax_evaluation_period.sql
Normal file
2
backend/db/migrations/0023_tax_evaluation_period.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "tenants"
|
||||||
|
ADD COLUMN "taxEvaluationPeriod" text DEFAULT 'monthly' NOT NULL;
|
||||||
37
backend/db/migrations/0024_tenant_branches.sql
Normal file
37
backend/db/migrations/0024_tenant_branches.sql
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
CREATE TABLE "branches" (
|
||||||
|
"id" bigint GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"tenant" bigint NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"number" text,
|
||||||
|
"description" text,
|
||||||
|
"archived" boolean DEFAULT false NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone,
|
||||||
|
"updated_by" uuid
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "branches" ADD CONSTRAINT "branches_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "branches" ADD CONSTRAINT "branches_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "costcentres" ADD COLUMN "branch" bigint;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "costcentres" ADD CONSTRAINT "costcentres_branch_branches_id_fk" FOREIGN KEY ("branch") REFERENCES "public"."branches"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth_profiles" ADD COLUMN "branch_id" bigint;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth_profiles" ADD CONSTRAINT "auth_profiles_branch_id_branches_id_fk" FOREIGN KEY ("branch_id") REFERENCES "public"."branches"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "auth_profile_branches" (
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"profile_id" uuid NOT NULL,
|
||||||
|
"branch_id" bigint NOT NULL,
|
||||||
|
"created_by" uuid,
|
||||||
|
CONSTRAINT "auth_profile_branches_profile_id_branch_id_pk" PRIMARY KEY("profile_id","branch_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth_profile_branches" ADD CONSTRAINT "auth_profile_branches_profile_id_auth_profiles_id_fk" FOREIGN KEY ("profile_id") REFERENCES "public"."auth_profiles"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth_profile_branches" ADD CONSTRAINT "auth_profile_branches_branch_id_branches_id_fk" FOREIGN KEY ("branch_id") REFERENCES "public"."branches"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth_profile_branches" ADD CONSTRAINT "auth_profile_branches_created_by_auth_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
ALTER TABLE "statementallocations"
|
||||||
|
ADD COLUMN "booking_mode" text DEFAULT 'expense' NOT NULL,
|
||||||
|
ADD COLUMN "depreciation_months" integer,
|
||||||
|
ADD COLUMN "depreciation_start_date" text,
|
||||||
|
ADD COLUMN "depreciation_label" text,
|
||||||
|
ADD COLUMN "depreciation_group" text;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "statementallocations"
|
||||||
|
ADD COLUMN "depreciation_method" text,
|
||||||
|
ADD COLUMN "residual_value" double precision;
|
||||||
2
backend/db/migrations/0027_product_supplier_link.sql
Normal file
2
backend/db/migrations/0027_product_supplier_link.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "products"
|
||||||
|
ADD COLUMN "supplierLink" text;
|
||||||
31
backend/db/migrations/0028_teams.sql
Normal file
31
backend/db/migrations/0028_teams.sql
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
CREATE TABLE "teams" (
|
||||||
|
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "teams_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"tenant" bigint NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"branch" bigint,
|
||||||
|
"archived" boolean DEFAULT false NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone,
|
||||||
|
"updated_by" uuid
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "auth_profile_teams" (
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"profile_id" uuid NOT NULL,
|
||||||
|
"team_id" bigint NOT NULL,
|
||||||
|
"created_by" uuid,
|
||||||
|
CONSTRAINT "auth_profile_teams_profile_id_team_id_pk" PRIMARY KEY("profile_id","team_id")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "teams" ADD CONSTRAINT "teams_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "teams" ADD CONSTRAINT "teams_branch_branches_id_fk" FOREIGN KEY ("branch") REFERENCES "public"."branches"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "teams" ADD CONSTRAINT "teams_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth_profile_teams" ADD CONSTRAINT "auth_profile_teams_profile_id_auth_profiles_id_fk" FOREIGN KEY ("profile_id") REFERENCES "public"."auth_profiles"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth_profile_teams" ADD CONSTRAINT "auth_profile_teams_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth_profile_teams" ADD CONSTRAINT "auth_profile_teams_created_by_auth_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
|
||||||
1
backend/db/migrations/0029_events_quick.sql
Normal file
1
backend/db/migrations/0029_events_quick.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "events" ADD COLUMN "quick" boolean DEFAULT false NOT NULL;
|
||||||
10
backend/db/migrations/0030_manual_statementallocations.sql
Normal file
10
backend/db/migrations/0030_manual_statementallocations.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
ALTER TABLE "statementallocations" ALTER COLUMN "bs_id" DROP NOT NULL;
|
||||||
|
ALTER TABLE "statementallocations" ADD COLUMN "manual_booking_date" text;
|
||||||
|
ALTER TABLE "statementallocations" ADD COLUMN "contra_account" bigint;
|
||||||
|
ALTER TABLE "statementallocations" ADD COLUMN "contra_ownaccount" uuid;
|
||||||
|
ALTER TABLE "statementallocations" ADD COLUMN "contra_customer" bigint;
|
||||||
|
ALTER TABLE "statementallocations" ADD COLUMN "contra_vendor" bigint;
|
||||||
|
ALTER TABLE "statementallocations" ADD CONSTRAINT "statementallocations_contra_account_accounts_id_fk" FOREIGN KEY ("contra_account") REFERENCES "public"."accounts"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
ALTER TABLE "statementallocations" ADD CONSTRAINT "statementallocations_contra_ownaccount_ownaccounts_id_fk" FOREIGN KEY ("contra_ownaccount") REFERENCES "public"."ownaccounts"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
ALTER TABLE "statementallocations" ADD CONSTRAINT "statementallocations_contra_customer_customers_id_fk" FOREIGN KEY ("contra_customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
ALTER TABLE "statementallocations" ADD CONSTRAINT "statementallocations_contra_vendor_vendors_id_fk" FOREIGN KEY ("contra_vendor") REFERENCES "public"."vendors"("id") ON DELETE no action ON UPDATE no action;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "statementallocations" ADD COLUMN "datev_tax_key" text;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "statementallocations" ADD COLUMN "manual_invoice_side" text;
|
||||||
2
backend/db/migrations/0033_costcentres_parent.sql
Normal file
2
backend/db/migrations/0033_costcentres_parent.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "costcentres" ADD COLUMN "parent_costcentre" uuid;
|
||||||
|
ALTER TABLE "costcentres" ADD CONSTRAINT "costcentres_parent_costcentre_costcentres_id_fk" FOREIGN KEY ("parent_costcentre") REFERENCES "public"."costcentres"("id") ON DELETE no action ON UPDATE no action;
|
||||||
8
backend/db/migrations/0034_events_color.sql
Normal file
8
backend/db/migrations/0034_events_color.sql
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
ALTER TABLE "events" ADD COLUMN "color" text;
|
||||||
|
|
||||||
|
UPDATE "events" AS e
|
||||||
|
SET "color" = COALESCE(t."calendarConfig"->'quickEntry'->>'color', '#2563eb')
|
||||||
|
FROM "tenants" AS t
|
||||||
|
WHERE e."tenant" = t."id"
|
||||||
|
AND e."quick" = true
|
||||||
|
AND e."color" IS NULL;
|
||||||
2
backend/db/migrations/0034_profile_availability_note.sql
Normal file
2
backend/db/migrations/0034_profile_availability_note.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "auth_profiles"
|
||||||
|
ADD COLUMN IF NOT EXISTS "availability_note" text;
|
||||||
3
backend/db/migrations/0035_contract_history.sql
Normal file
3
backend/db/migrations/0035_contract_history.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "historyitems" ADD COLUMN "contract" bigint;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_contract_contracts_id_fk" FOREIGN KEY ("contract") REFERENCES "public"."contracts"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
1
backend/db/migrations/0036_allowed_contracttypes.sql
Normal file
1
backend/db/migrations/0036_allowed_contracttypes.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "contracts" ADD COLUMN "allowedContracttypes" jsonb DEFAULT '[]'::jsonb NOT NULL;
|
||||||
53
backend/db/migrations/0037_outgoing_sepa_mandates.sql
Normal file
53
backend/db/migrations/0037_outgoing_sepa_mandates.sql
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
CREATE TABLE "outgoingsepamandates" (
|
||||||
|
"id" bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY (sequence name "outgoingsepamandates_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 9223372036854775807 START WITH 1 CACHE 1),
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"tenant" bigint NOT NULL,
|
||||||
|
"customer" bigint NOT NULL,
|
||||||
|
"bankaccount" bigint NOT NULL,
|
||||||
|
"reference" text NOT NULL,
|
||||||
|
"status" text DEFAULT 'Entwurf' NOT NULL,
|
||||||
|
"mandate_type" text DEFAULT 'CORE' NOT NULL,
|
||||||
|
"sequence_type" text DEFAULT 'RCUR' NOT NULL,
|
||||||
|
"signed_at" timestamp with time zone,
|
||||||
|
"valid_from" timestamp with time zone,
|
||||||
|
"valid_until" timestamp with time zone,
|
||||||
|
"default_mandate" boolean DEFAULT false NOT NULL,
|
||||||
|
"notes" text,
|
||||||
|
"archived" boolean DEFAULT false NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone,
|
||||||
|
"updated_by" uuid
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "outgoingsepamandates" ADD CONSTRAINT "outgoingsepamandates_tenant_tenants_id_fk" FOREIGN KEY ("tenant") REFERENCES "public"."tenants"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "outgoingsepamandates" ADD CONSTRAINT "outgoingsepamandates_customer_customers_id_fk" FOREIGN KEY ("customer") REFERENCES "public"."customers"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "outgoingsepamandates" ADD CONSTRAINT "outgoingsepamandates_bankaccount_entitybankaccounts_id_fk" FOREIGN KEY ("bankaccount") REFERENCES "public"."entitybankaccounts"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "outgoingsepamandates" ADD CONSTRAINT "outgoingsepamandates_updated_by_auth_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "contracts" ADD COLUMN "outgoingsepamandate" bigint;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "contracts" ADD CONSTRAINT "contracts_outgoingsepamandate_outgoingsepamandates_id_fk" FOREIGN KEY ("outgoingsepamandate") REFERENCES "public"."outgoingsepamandates"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "createddocuments" ADD COLUMN "outgoingsepamandate" bigint;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_outgoingsepamandate_outgoingsepamandates_id_fk" FOREIGN KEY ("outgoingsepamandate") REFERENCES "public"."outgoingsepamandates"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "historyitems" ADD COLUMN "outgoingsepamandate" bigint;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "historyitems" ADD CONSTRAINT "historyitems_outgoingsepamandate_outgoingsepamandates_id_fk" FOREIGN KEY ("outgoingsepamandate") REFERENCES "public"."outgoingsepamandates"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "tenants" ALTER COLUMN "numberRanges" SET DEFAULT '{"vendors":{"prefix":"","suffix":"","nextNumber":10000},"customers":{"prefix":"","suffix":"","nextNumber":10000},"products":{"prefix":"AT-","suffix":"","nextNumber":1000},"quotes":{"prefix":"AN-","suffix":"","nextNumber":1000},"costEstimates":{"prefix":"KS-","suffix":"","nextNumber":1000},"confirmationOrders":{"prefix":"AB-","suffix":"","nextNumber":1000},"invoices":{"prefix":"RE-","suffix":"","nextNumber":1000},"deliveryNotes":{"prefix":"LS-","suffix":"","nextNumber":1000},"packingSlips":{"prefix":"PS-","suffix":"","nextNumber":1000},"spaces":{"prefix":"LP-","suffix":"","nextNumber":1000},"customerspaces":{"prefix":"KLP-","suffix":"","nextNumber":1000},"inventoryitems":{"prefix":"IA-","suffix":"","nextNumber":1000},"customerinventoryitems":{"prefix":"KIA-","suffix":"","nextNumber":1000},"projects":{"prefix":"PRJ-","suffix":"","nextNumber":1000},"costcentres":{"prefix":"KST-","suffix":"","nextNumber":1000},"outgoingsepamandates":{"prefix":"SEPA-","suffix":"","nextNumber":1000}}'::jsonb;
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "tenants"
|
||||||
|
SET "numberRanges" = COALESCE("numberRanges", '{}'::jsonb) || jsonb_build_object(
|
||||||
|
'outgoingsepamandates',
|
||||||
|
COALESCE("numberRanges"->'outgoingsepamandates', '{"prefix":"SEPA-","suffix":"","nextNumber":1000}'::jsonb)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
UPDATE "tenants"
|
||||||
|
SET "features" = COALESCE("features", '{}'::jsonb) || jsonb_build_object(
|
||||||
|
'outgoingsepamandates',
|
||||||
|
COALESCE("features"->'outgoingsepamandates', 'true'::jsonb)
|
||||||
|
);
|
||||||
43
backend/db/migrations/0038_communication_rooms.sql
Normal file
43
backend/db/migrations/0038_communication_rooms.sql
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
CREATE TABLE "communication_rooms" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"tenant_id" bigint NOT NULL,
|
||||||
|
"key" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"topic" text,
|
||||||
|
"type" text DEFAULT 'room' NOT NULL,
|
||||||
|
"entity_type" text,
|
||||||
|
"entity_id" bigint,
|
||||||
|
"entity_uuid" uuid,
|
||||||
|
"matrix_room_id" text,
|
||||||
|
"matrix_alias" text,
|
||||||
|
"parent_space_room_id" text,
|
||||||
|
"archived" boolean DEFAULT false NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone,
|
||||||
|
"created_by" uuid,
|
||||||
|
"updated_by" uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "communication_rooms"
|
||||||
|
ADD CONSTRAINT "communication_rooms_tenant_id_tenants_id_fk"
|
||||||
|
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id")
|
||||||
|
ON DELETE cascade ON UPDATE no action;
|
||||||
|
|
||||||
|
ALTER TABLE "communication_rooms"
|
||||||
|
ADD CONSTRAINT "communication_rooms_created_by_auth_users_id_fk"
|
||||||
|
FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id")
|
||||||
|
ON DELETE no action ON UPDATE no action;
|
||||||
|
|
||||||
|
ALTER TABLE "communication_rooms"
|
||||||
|
ADD CONSTRAINT "communication_rooms_updated_by_auth_users_id_fk"
|
||||||
|
FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id")
|
||||||
|
ON DELETE no action ON UPDATE no action;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX "communication_rooms_tenant_key_idx"
|
||||||
|
ON "communication_rooms" USING btree ("tenant_id", "key");
|
||||||
|
|
||||||
|
CREATE INDEX "communication_rooms_tenant_idx"
|
||||||
|
ON "communication_rooms" USING btree ("tenant_id");
|
||||||
|
|
||||||
|
CREATE INDEX "communication_rooms_entity_idx"
|
||||||
|
ON "communication_rooms" USING btree ("tenant_id", "entity_type", "entity_id", "entity_uuid");
|
||||||
5
backend/db/migrations/0038_events_state.sql
Normal file
5
backend/db/migrations/0038_events_state.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE "events" ADD COLUMN "state" text DEFAULT 'Final' NOT NULL;
|
||||||
|
|
||||||
|
UPDATE "events"
|
||||||
|
SET "state" = 'Final'
|
||||||
|
WHERE "state" IS NULL;
|
||||||
1
backend/db/migrations/0039_events_repeat_interval.sql
Normal file
1
backend/db/migrations/0039_events_repeat_interval.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "events" ADD COLUMN "repeatInterval" text DEFAULT 'Keine Wiederholung' NOT NULL;
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
CREATE TABLE "notification_push_subscriptions" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"tenant_id" bigint NOT NULL,
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
"endpoint" text NOT NULL,
|
||||||
|
"p256dh" text NOT NULL,
|
||||||
|
"auth" text NOT NULL,
|
||||||
|
"user_agent" text,
|
||||||
|
"device_label" text,
|
||||||
|
"meta" jsonb,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"last_seen_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"disabled_at" timestamp with time zone,
|
||||||
|
CONSTRAINT "notification_push_subscriptions_endpoint_key" UNIQUE("endpoint")
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "notification_push_subscriptions"
|
||||||
|
ADD CONSTRAINT "notification_push_subscriptions_tenant_id_tenants_id_fk"
|
||||||
|
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id")
|
||||||
|
ON DELETE cascade ON UPDATE cascade;
|
||||||
|
|
||||||
|
ALTER TABLE "notification_push_subscriptions"
|
||||||
|
ADD CONSTRAINT "notification_push_subscriptions_user_id_auth_users_id_fk"
|
||||||
|
FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id")
|
||||||
|
ON DELETE cascade ON UPDATE cascade;
|
||||||
|
|
||||||
|
INSERT INTO "notifications_event_types" (
|
||||||
|
"event_key",
|
||||||
|
"display_name",
|
||||||
|
"description",
|
||||||
|
"category",
|
||||||
|
"severity",
|
||||||
|
"allowed_channels"
|
||||||
|
) VALUES
|
||||||
|
('system.test_push', 'Test-Push', 'Testet Desktop-Benachrichtigungen für den angemeldeten Nutzer.', 'system', 'info', '["inapp", "push"]'::jsonb),
|
||||||
|
('communication.message.new', 'Neue Chatnachricht', 'Benachrichtigt über relevante neue Chatnachrichten.', 'communication', 'info', '["inapp", "push"]'::jsonb),
|
||||||
|
('communication.call.started', 'Besprechung gestartet', 'Benachrichtigt Raumteilnehmer über gestartete Audio- oder Videoanrufe.', 'communication', 'info', '["inapp", "push"]'::jsonb),
|
||||||
|
('communication.call.missed', 'Verpasster Anruf', 'Benachrichtigt über verpasste Besprechungen.', 'communication', 'warning', '["inapp", "push"]'::jsonb),
|
||||||
|
('communication.room.invited', 'Raumeinladung', 'Benachrichtigt über Einladungen in Kommunikationsräume.', 'communication', 'info', '["inapp", "push"]'::jsonb)
|
||||||
|
ON CONFLICT ("event_key") DO UPDATE SET
|
||||||
|
"display_name" = EXCLUDED."display_name",
|
||||||
|
"description" = EXCLUDED."description",
|
||||||
|
"category" = EXCLUDED."category",
|
||||||
|
"severity" = EXCLUDED."severity",
|
||||||
|
"allowed_channels" = EXCLUDED."allowed_channels",
|
||||||
|
"is_active" = true;
|
||||||
6
backend/db/migrations/0040_filetag_system_types.sql
Normal file
6
backend/db/migrations/0040_filetag_system_types.sql
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
ALTER TABLE "filetags" ADD COLUMN "isSystemUsed" boolean DEFAULT false NOT NULL;
|
||||||
|
|
||||||
|
UPDATE "filetags"
|
||||||
|
SET "isSystemUsed" = true
|
||||||
|
WHERE COALESCE("createddocumenttype", '') <> ''
|
||||||
|
OR COALESCE("incomingDocumentType", '') <> '';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "auth_profiles"
|
||||||
|
ADD COLUMN "calendar_subscription_token" text;
|
||||||
2
backend/db/migrations/0042_profile_availability_note.sql
Normal file
2
backend/db/migrations/0042_profile_availability_note.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "auth_profiles"
|
||||||
|
ADD COLUMN IF NOT EXISTS "availability_note" text;
|
||||||
58
backend/db/migrations/0043_communication_rooms.sql
Normal file
58
backend/db/migrations/0043_communication_rooms.sql
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "communication_rooms" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"tenant_id" bigint NOT NULL,
|
||||||
|
"key" text NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"topic" text,
|
||||||
|
"type" text DEFAULT 'room' NOT NULL,
|
||||||
|
"entity_type" text,
|
||||||
|
"entity_id" bigint,
|
||||||
|
"entity_uuid" uuid,
|
||||||
|
"matrix_room_id" text,
|
||||||
|
"matrix_alias" text,
|
||||||
|
"parent_space_room_id" text,
|
||||||
|
"archived" boolean DEFAULT false NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone,
|
||||||
|
"created_by" uuid,
|
||||||
|
"updated_by" uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'communication_rooms_tenant_id_tenants_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "communication_rooms"
|
||||||
|
ADD CONSTRAINT "communication_rooms_tenant_id_tenants_id_fk"
|
||||||
|
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id")
|
||||||
|
ON DELETE cascade ON UPDATE no action;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'communication_rooms_created_by_auth_users_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "communication_rooms"
|
||||||
|
ADD CONSTRAINT "communication_rooms_created_by_auth_users_id_fk"
|
||||||
|
FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id")
|
||||||
|
ON DELETE no action ON UPDATE no action;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'communication_rooms_updated_by_auth_users_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "communication_rooms"
|
||||||
|
ADD CONSTRAINT "communication_rooms_updated_by_auth_users_id_fk"
|
||||||
|
FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id")
|
||||||
|
ON DELETE no action ON UPDATE no action;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "communication_rooms_tenant_key_idx"
|
||||||
|
ON "communication_rooms" USING btree ("tenant_id", "key");
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "communication_rooms_tenant_idx"
|
||||||
|
ON "communication_rooms" USING btree ("tenant_id");
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "communication_rooms_entity_idx"
|
||||||
|
ON "communication_rooms" USING btree ("tenant_id", "entity_type", "entity_id", "entity_uuid");
|
||||||
57
backend/db/migrations/0044_telephony_calls.sql
Normal file
57
backend/db/migrations/0044_telephony_calls.sql
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "telephony_calls" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"tenant_id" bigint NOT NULL,
|
||||||
|
"direction" text NOT NULL,
|
||||||
|
"status" text DEFAULT 'ringing' NOT NULL,
|
||||||
|
"local_extension" text,
|
||||||
|
"remote_number" text,
|
||||||
|
"remote_display_name" text,
|
||||||
|
"sip_call_id" text,
|
||||||
|
"started_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"answered_at" timestamp with time zone,
|
||||||
|
"ended_at" timestamp with time zone,
|
||||||
|
"duration_seconds" integer,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone,
|
||||||
|
"created_by" uuid,
|
||||||
|
"updated_by" uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'telephony_calls_tenant_id_tenants_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "telephony_calls"
|
||||||
|
ADD CONSTRAINT "telephony_calls_tenant_id_tenants_id_fk"
|
||||||
|
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id")
|
||||||
|
ON DELETE cascade ON UPDATE no action;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'telephony_calls_created_by_auth_users_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "telephony_calls"
|
||||||
|
ADD CONSTRAINT "telephony_calls_created_by_auth_users_id_fk"
|
||||||
|
FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id")
|
||||||
|
ON DELETE no action ON UPDATE no action;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'telephony_calls_updated_by_auth_users_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "telephony_calls"
|
||||||
|
ADD CONSTRAINT "telephony_calls_updated_by_auth_users_id_fk"
|
||||||
|
FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id")
|
||||||
|
ON DELETE no action ON UPDATE no action;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "telephony_calls_tenant_started_idx"
|
||||||
|
ON "telephony_calls" USING btree ("tenant_id", "started_at");
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "telephony_calls_created_by_idx"
|
||||||
|
ON "telephony_calls" USING btree ("tenant_id", "created_by");
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "telephony_calls_sip_call_idx"
|
||||||
|
ON "telephony_calls" USING btree ("tenant_id", "sip_call_id");
|
||||||
50
backend/db/migrations/0045_telephony_trunks.sql
Normal file
50
backend/db/migrations/0045_telephony_trunks.sql
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "telephony_trunks" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"tenant_id" bigint NOT NULL,
|
||||||
|
"provider" text DEFAULT 'telekom' NOT NULL,
|
||||||
|
"enabled" boolean DEFAULT false NOT NULL,
|
||||||
|
"registrar" text DEFAULT 'tel.t-online.de' NOT NULL,
|
||||||
|
"sip_user" text,
|
||||||
|
"auth_user" text,
|
||||||
|
"password" text,
|
||||||
|
"caller_id" text,
|
||||||
|
"inbound_extension" text DEFAULT '1001' NOT NULL,
|
||||||
|
"outbound_prefix" text DEFAULT '0' NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone,
|
||||||
|
"created_by" uuid,
|
||||||
|
"updated_by" uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'telephony_trunks_tenant_id_tenants_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "telephony_trunks"
|
||||||
|
ADD CONSTRAINT "telephony_trunks_tenant_id_tenants_id_fk"
|
||||||
|
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id")
|
||||||
|
ON DELETE cascade ON UPDATE no action;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'telephony_trunks_created_by_auth_users_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "telephony_trunks"
|
||||||
|
ADD CONSTRAINT "telephony_trunks_created_by_auth_users_id_fk"
|
||||||
|
FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id")
|
||||||
|
ON DELETE no action ON UPDATE no action;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'telephony_trunks_updated_by_auth_users_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "telephony_trunks"
|
||||||
|
ADD CONSTRAINT "telephony_trunks_updated_by_auth_users_id_fk"
|
||||||
|
FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id")
|
||||||
|
ON DELETE no action ON UPDATE no action;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "telephony_trunks_tenant_provider_idx"
|
||||||
|
ON "telephony_trunks" USING btree ("tenant_id", "provider");
|
||||||
3
backend/db/migrations/0046_telephony_trunk_nat.sql
Normal file
3
backend/db/migrations/0046_telephony_trunk_nat.sql
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "telephony_trunks" ADD COLUMN IF NOT EXISTS "external_signaling_address" text;
|
||||||
|
ALTER TABLE "telephony_trunks" ADD COLUMN IF NOT EXISTS "external_media_address" text;
|
||||||
|
ALTER TABLE "telephony_trunks" ADD COLUMN IF NOT EXISTS "local_networks" text;
|
||||||
92
backend/db/migrations/0047_telephony_extensions.sql
Normal file
92
backend/db/migrations/0047_telephony_extensions.sql
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
CREATE TABLE IF NOT EXISTS "telephony_extensions" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"tenant_id" bigint NOT NULL,
|
||||||
|
"target_type" text NOT NULL,
|
||||||
|
"target_user_id" uuid,
|
||||||
|
"target_team_id" bigint,
|
||||||
|
"target_branch_id" bigint,
|
||||||
|
"extension" text NOT NULL,
|
||||||
|
"display_name" text,
|
||||||
|
"sip_username" text,
|
||||||
|
"sip_password" text,
|
||||||
|
"enabled" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone,
|
||||||
|
"created_by" uuid,
|
||||||
|
"updated_by" uuid
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE "telephony_trunks"
|
||||||
|
ADD COLUMN IF NOT EXISTS "default_route_extension_id" uuid;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'telephony_extensions_tenant_id_tenants_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "telephony_extensions"
|
||||||
|
ADD CONSTRAINT "telephony_extensions_tenant_id_tenants_id_fk"
|
||||||
|
FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id")
|
||||||
|
ON DELETE cascade ON UPDATE no action;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'telephony_extensions_target_user_id_auth_users_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "telephony_extensions"
|
||||||
|
ADD CONSTRAINT "telephony_extensions_target_user_id_auth_users_id_fk"
|
||||||
|
FOREIGN KEY ("target_user_id") REFERENCES "public"."auth_users"("id")
|
||||||
|
ON DELETE cascade ON UPDATE no action;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'telephony_extensions_target_team_id_teams_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "telephony_extensions"
|
||||||
|
ADD CONSTRAINT "telephony_extensions_target_team_id_teams_id_fk"
|
||||||
|
FOREIGN KEY ("target_team_id") REFERENCES "public"."teams"("id")
|
||||||
|
ON DELETE cascade ON UPDATE no action;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'telephony_extensions_target_branch_id_branches_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "telephony_extensions"
|
||||||
|
ADD CONSTRAINT "telephony_extensions_target_branch_id_branches_id_fk"
|
||||||
|
FOREIGN KEY ("target_branch_id") REFERENCES "public"."branches"("id")
|
||||||
|
ON DELETE cascade ON UPDATE no action;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'telephony_extensions_created_by_auth_users_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "telephony_extensions"
|
||||||
|
ADD CONSTRAINT "telephony_extensions_created_by_auth_users_id_fk"
|
||||||
|
FOREIGN KEY ("created_by") REFERENCES "public"."auth_users"("id")
|
||||||
|
ON DELETE no action ON UPDATE no action;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'telephony_extensions_updated_by_auth_users_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "telephony_extensions"
|
||||||
|
ADD CONSTRAINT "telephony_extensions_updated_by_auth_users_id_fk"
|
||||||
|
FOREIGN KEY ("updated_by") REFERENCES "public"."auth_users"("id")
|
||||||
|
ON DELETE no action ON UPDATE no action;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1 FROM pg_constraint WHERE conname = 'telephony_trunks_default_route_extension_id_telephony_extensions_id_fk'
|
||||||
|
) THEN
|
||||||
|
ALTER TABLE "telephony_trunks"
|
||||||
|
ADD CONSTRAINT "telephony_trunks_default_route_extension_id_telephony_extensions_id_fk"
|
||||||
|
FOREIGN KEY ("default_route_extension_id") REFERENCES "public"."telephony_extensions"("id")
|
||||||
|
ON DELETE set null ON UPDATE no action;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "telephony_extensions_tenant_extension_idx"
|
||||||
|
ON "telephony_extensions" USING btree ("tenant_id", "extension");
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS "telephony_extensions_tenant_target_idx"
|
||||||
|
ON "telephony_extensions" USING btree ("tenant_id", "target_type");
|
||||||
19
backend/db/migrations/0048_mobile_push_devices.sql
Normal file
19
backend/db/migrations/0048_mobile_push_devices.sql
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
CREATE TABLE "notification_mobile_push_devices" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"tenant_id" bigint NOT NULL,
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
"local_device_id" text NOT NULL,
|
||||||
|
"central_device_id" text NOT NULL,
|
||||||
|
"platform" text NOT NULL,
|
||||||
|
"provider_token_preview" text,
|
||||||
|
"device_label" text,
|
||||||
|
"meta" jsonb,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"last_seen_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"disabled_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "notification_mobile_push_devices" ADD CONSTRAINT "notification_mobile_push_devices_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "notification_mobile_push_devices" ADD CONSTRAINT "notification_mobile_push_devices_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "notification_mobile_push_devices_user_device_key" ON "notification_mobile_push_devices" USING btree ("tenant_id","user_id","local_device_id");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "notification_mobile_push_devices_central_device_key" ON "notification_mobile_push_devices" USING btree ("central_device_id");
|
||||||
106
backend/db/migrations/0049_email_cache.sql
Normal file
106
backend/db/migrations/0049_email_cache.sql
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
CREATE TABLE "email_mailboxes" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"tenant_id" bigint NOT NULL,
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
"account_id" uuid NOT NULL,
|
||||||
|
"path" text NOT NULL,
|
||||||
|
"delimiter" text,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"special_use" text,
|
||||||
|
"flags" jsonb,
|
||||||
|
"exists" integer DEFAULT 0 NOT NULL,
|
||||||
|
"unseen" integer DEFAULT 0 NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "email_messages" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"tenant_id" bigint NOT NULL,
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
"account_id" uuid NOT NULL,
|
||||||
|
"mailbox_id" uuid NOT NULL,
|
||||||
|
"mailbox_path" text NOT NULL,
|
||||||
|
"uid" bigint NOT NULL,
|
||||||
|
"email_id" text,
|
||||||
|
"message_id" text,
|
||||||
|
"in_reply_to" text,
|
||||||
|
"thread_id" text,
|
||||||
|
"subject" text,
|
||||||
|
"from" jsonb,
|
||||||
|
"to" jsonb,
|
||||||
|
"cc" jsonb,
|
||||||
|
"bcc" jsonb,
|
||||||
|
"reply_to" jsonb,
|
||||||
|
"preview" text,
|
||||||
|
"flags" jsonb,
|
||||||
|
"seen" boolean DEFAULT false NOT NULL,
|
||||||
|
"flagged" boolean DEFAULT false NOT NULL,
|
||||||
|
"has_attachments" boolean DEFAULT false NOT NULL,
|
||||||
|
"size" bigint,
|
||||||
|
"sent_at" timestamp with time zone,
|
||||||
|
"received_at" timestamp with time zone,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "email_message_bodies" (
|
||||||
|
"message_id" uuid PRIMARY KEY NOT NULL,
|
||||||
|
"text" text,
|
||||||
|
"html" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "email_attachments" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"message_id" uuid NOT NULL,
|
||||||
|
"filename" text,
|
||||||
|
"content_type" text,
|
||||||
|
"content_id" text,
|
||||||
|
"disposition" text,
|
||||||
|
"size" bigint,
|
||||||
|
"checksum" text,
|
||||||
|
"storage_key" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "email_sync_state" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"tenant_id" bigint NOT NULL,
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
"account_id" uuid NOT NULL,
|
||||||
|
"mailbox_id" uuid NOT NULL,
|
||||||
|
"mailbox_path" text NOT NULL,
|
||||||
|
"uid_validity" bigint,
|
||||||
|
"highest_uid" bigint DEFAULT 0 NOT NULL,
|
||||||
|
"mod_seq" text,
|
||||||
|
"last_synced_at" timestamp with time zone,
|
||||||
|
"sync_error" text,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "email_mailboxes" ADD CONSTRAINT "email_mailboxes_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "email_mailboxes" ADD CONSTRAINT "email_mailboxes_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "email_mailboxes" ADD CONSTRAINT "email_mailboxes_account_id_user_credentials_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."user_credentials"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "email_messages" ADD CONSTRAINT "email_messages_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "email_messages" ADD CONSTRAINT "email_messages_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "email_messages" ADD CONSTRAINT "email_messages_account_id_user_credentials_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."user_credentials"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "email_messages" ADD CONSTRAINT "email_messages_mailbox_id_email_mailboxes_id_fk" FOREIGN KEY ("mailbox_id") REFERENCES "public"."email_mailboxes"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "email_message_bodies" ADD CONSTRAINT "email_message_bodies_message_id_email_messages_id_fk" FOREIGN KEY ("message_id") REFERENCES "public"."email_messages"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "email_attachments" ADD CONSTRAINT "email_attachments_message_id_email_messages_id_fk" FOREIGN KEY ("message_id") REFERENCES "public"."email_messages"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "email_sync_state" ADD CONSTRAINT "email_sync_state_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "email_sync_state" ADD CONSTRAINT "email_sync_state_user_id_auth_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."auth_users"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "email_sync_state" ADD CONSTRAINT "email_sync_state_account_id_user_credentials_id_fk" FOREIGN KEY ("account_id") REFERENCES "public"."user_credentials"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
ALTER TABLE "email_sync_state" ADD CONSTRAINT "email_sync_state_mailbox_id_email_mailboxes_id_fk" FOREIGN KEY ("mailbox_id") REFERENCES "public"."email_mailboxes"("id") ON DELETE cascade ON UPDATE cascade;--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "email_mailboxes_account_path_key" ON "email_mailboxes" USING btree ("account_id","path");--> statement-breakpoint
|
||||||
|
CREATE INDEX "email_mailboxes_tenant_account_idx" ON "email_mailboxes" USING btree ("tenant_id","account_id");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "email_messages_mailbox_uid_key" ON "email_messages" USING btree ("mailbox_id","uid");--> statement-breakpoint
|
||||||
|
CREATE INDEX "email_messages_account_mailbox_idx" ON "email_messages" USING btree ("account_id","mailbox_path");--> statement-breakpoint
|
||||||
|
CREATE INDEX "email_messages_received_idx" ON "email_messages" USING btree ("received_at");--> statement-breakpoint
|
||||||
|
CREATE INDEX "email_messages_message_id_idx" ON "email_messages" USING btree ("message_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "email_messages_thread_idx" ON "email_messages" USING btree ("thread_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "email_attachments_message_idx" ON "email_attachments" USING btree ("message_id");--> statement-breakpoint
|
||||||
|
CREATE UNIQUE INDEX "email_sync_state_mailbox_key" ON "email_sync_state" USING btree ("account_id","mailbox_path");--> statement-breakpoint
|
||||||
|
CREATE INDEX "email_sync_state_tenant_account_idx" ON "email_sync_state" USING btree ("tenant_id","account_id");
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE "createddocuments" ADD COLUMN "costcentre" uuid;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "services" ADD COLUMN "costcentre" uuid;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "createddocuments" ADD CONSTRAINT "createddocuments_costcentre_costcentres_id_fk" FOREIGN KEY ("costcentre") REFERENCES "public"."costcentres"("id") ON DELETE no action ON UPDATE no action;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "services" ADD CONSTRAINT "services_costcentre_costcentres_id_fk" FOREIGN KEY ("costcentre") REFERENCES "public"."costcentres"("id") ON DELETE no action ON UPDATE no action;
|
||||||
43
backend/db/migrations/0051_instance_scan_agents.sql
Normal file
43
backend/db/migrations/0051_instance_scan_agents.sql
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
CREATE TABLE "instance_agents" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"name" text NOT NULL,
|
||||||
|
"description" text,
|
||||||
|
"token_prefix" text NOT NULL,
|
||||||
|
"token_hash" text NOT NULL,
|
||||||
|
"active" boolean DEFAULT true NOT NULL,
|
||||||
|
"capabilities" jsonb DEFAULT '{"scan":true,"print":false}'::jsonb NOT NULL,
|
||||||
|
"scanner_names" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||||
|
"printer_names" jsonb DEFAULT '[]'::jsonb NOT NULL,
|
||||||
|
"last_seen_at" timestamp with time zone,
|
||||||
|
"last_debug_info" jsonb,
|
||||||
|
CONSTRAINT "instance_agents_token_hash_unique" UNIQUE("token_hash")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "instance_agent_scan_jobs" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"tenant_id" bigint NOT NULL,
|
||||||
|
"agent_id" uuid NOT NULL,
|
||||||
|
"requested_by" uuid,
|
||||||
|
"status" text DEFAULT 'pending' NOT NULL,
|
||||||
|
"scanner_name" text,
|
||||||
|
"requested_filename" text,
|
||||||
|
"settings" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
"target" jsonb DEFAULT '{}'::jsonb NOT NULL,
|
||||||
|
"agent_message" text,
|
||||||
|
"attempts" integer DEFAULT 0 NOT NULL,
|
||||||
|
"claimed_at" timestamp with time zone,
|
||||||
|
"finished_at" timestamp with time zone,
|
||||||
|
"file_id" uuid,
|
||||||
|
CONSTRAINT "instance_agent_scan_jobs_tenant_id_tenants_id_fk" FOREIGN KEY ("tenant_id") REFERENCES "public"."tenants"("id") ON DELETE cascade ON UPDATE cascade,
|
||||||
|
CONSTRAINT "instance_agent_scan_jobs_agent_id_instance_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."instance_agents"("id") ON DELETE cascade ON UPDATE cascade,
|
||||||
|
CONSTRAINT "instance_agent_scan_jobs_requested_by_auth_users_id_fk" FOREIGN KEY ("requested_by") REFERENCES "public"."auth_users"("id") ON DELETE set null ON UPDATE cascade,
|
||||||
|
CONSTRAINT "instance_agent_scan_jobs_file_id_files_id_fk" FOREIGN KEY ("file_id") REFERENCES "public"."files"("id") ON DELETE set null ON UPDATE cascade
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX "instance_agent_scan_jobs_agent_status_idx" ON "instance_agent_scan_jobs" USING btree ("agent_id","status","created_at");
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX "instance_agent_scan_jobs_tenant_idx" ON "instance_agent_scan_jobs" USING btree ("tenant_id","created_at");
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE "instance_agents" ADD COLUMN "preferred_scanner_name" text;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "instance_agents" ADD COLUMN "scan_defaults" jsonb DEFAULT '{"format":"pdf","resolution":300,"mode":"Color","source":null}'::jsonb NOT NULL;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "instance_agents" ALTER COLUMN "scan_defaults" SET DEFAULT '{"format":"pdf","resolution":300,"mode":"Color","source":null,"postprocess":false,"postprocessProfile":"document"}'::jsonb;
|
||||||
10685
backend/db/migrations/meta/0005_snapshot.json
Normal file
10685
backend/db/migrations/meta/0005_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
11525
backend/db/migrations/meta/0017_snapshot.json
Normal file
11525
backend/db/migrations/meta/0017_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
11813
backend/db/migrations/meta/0025_snapshot.json
Normal file
11813
backend/db/migrations/meta/0025_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -36,6 +36,328 @@
|
|||||||
"when": 1765716877146,
|
"when": 1765716877146,
|
||||||
"tag": "0004_stormy_onslaught",
|
"tag": "0004_stormy_onslaught",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1771096926109,
|
||||||
|
"tag": "0005_green_shinobi_shaw",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 6,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1772000000000,
|
||||||
|
"tag": "0006_nifty_price_lock",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 7,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1772000100000,
|
||||||
|
"tag": "0007_bright_default_tax_type",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 8,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773000000000,
|
||||||
|
"tag": "0008_quick_contracttypes",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 9,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773000100000,
|
||||||
|
"tag": "0009_heavy_contract_contracttype",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 10,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773000200000,
|
||||||
|
"tag": "0010_sudden_billing_interval",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 11,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773000300000,
|
||||||
|
"tag": "0011_mighty_member_bankaccounts",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 12,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773000400000,
|
||||||
|
"tag": "0012_shiny_customer_inventory",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 13,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773000500000,
|
||||||
|
"tag": "0013_brisk_customer_inventory_vendor",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 14,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773000600000,
|
||||||
|
"tag": "0014_smart_memberrelations",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 15,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773000700000,
|
||||||
|
"tag": "0015_wise_memberrelation_history",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 16,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773000800000,
|
||||||
|
"tag": "0016_fix_memberrelation_column_usage",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 17,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1771704862789,
|
||||||
|
"tag": "0017_slow_the_hood",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 18,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773000900000,
|
||||||
|
"tag": "0018_account_chart",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 19,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773489600000,
|
||||||
|
"tag": "0019_custom_surcharge_percentage_decimal",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 20,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773572400000,
|
||||||
|
"tag": "0020_file_extracted_text",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 21,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773835200000,
|
||||||
|
"tag": "0021_admin_user_flag",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 22,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1773925200000,
|
||||||
|
"tag": "0022_task_dependencies",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 23,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1774080000000,
|
||||||
|
"tag": "0023_tax_evaluation_period",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 24,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1774393200000,
|
||||||
|
"tag": "0024_tenant_branches",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 25,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1774393201000,
|
||||||
|
"tag": "0025_statementallocation_depreciation",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 26,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1774393202000,
|
||||||
|
"tag": "0026_statementallocation_depreciation_method",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 27,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1774602000000,
|
||||||
|
"tag": "0027_product_supplier_link",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 28,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1776124800000,
|
||||||
|
"tag": "0028_teams",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 29,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1776211200000,
|
||||||
|
"tag": "0029_events_quick",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 30,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1776297600000,
|
||||||
|
"tag": "0030_manual_statementallocations",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 31,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1776298200000,
|
||||||
|
"tag": "0031_manual_statementallocations_tax_key",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 32,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1776298800000,
|
||||||
|
"tag": "0032_manual_statementallocations_invoice_side",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 33,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1777003200000,
|
||||||
|
"tag": "0033_costcentres_parent",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 34,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1777420800000,
|
||||||
|
"tag": "0034_events_color",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 35,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1778191200000,
|
||||||
|
"tag": "0035_contract_history",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 36,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1778194800000,
|
||||||
|
"tag": "0036_allowed_contracttypes",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 37,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1778840100000,
|
||||||
|
"tag": "0037_outgoing_sepa_mandates",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 38,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1779158400000,
|
||||||
|
"tag": "0038_events_state",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 39,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1779840000000,
|
||||||
|
"tag": "0039_events_repeat_interval",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 40,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1779141600000,
|
||||||
|
"tag": "0040_filetag_system_types",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 41,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780149600000,
|
||||||
|
"tag": "0041_profile_calendar_subscription",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 42,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780153200000,
|
||||||
|
"tag": "0042_profile_availability_note",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 43,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780156800000,
|
||||||
|
"tag": "0043_communication_rooms",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 44,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780160400000,
|
||||||
|
"tag": "0044_telephony_calls",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 45,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780164000000,
|
||||||
|
"tag": "0045_telephony_trunks",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 46,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780167600000,
|
||||||
|
"tag": "0046_telephony_trunk_nat",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 47,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780171200000,
|
||||||
|
"tag": "0047_telephony_extensions",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 48,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780174800000,
|
||||||
|
"tag": "0048_mobile_push_devices",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 49,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780178400000,
|
||||||
|
"tag": "0049_email_cache",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 50,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1780261200000,
|
||||||
|
"tag": "0050_outgoing_document_costcentres",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,7 @@ export const accounts = pgTable("accounts", {
|
|||||||
|
|
||||||
number: text("number").notNull(),
|
number: text("number").notNull(),
|
||||||
label: text("label").notNull(),
|
label: text("label").notNull(),
|
||||||
|
accountChart: text("accountChart").notNull().default("skr03"),
|
||||||
|
|
||||||
description: text("description"),
|
description: text("description"),
|
||||||
})
|
})
|
||||||
|
|||||||
30
backend/db/schema/auth_profile_branches.ts
Normal file
30
backend/db/schema/auth_profile_branches.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { pgTable, uuid, bigint, timestamp } from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { authProfiles } from "./auth_profiles"
|
||||||
|
import { branches } from "./branches"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const authProfileBranches = pgTable(
|
||||||
|
"auth_profile_branches",
|
||||||
|
{
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
profile_id: uuid("profile_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => authProfiles.id, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
branch_id: bigint("branch_id", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => branches.id, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
created_by: uuid("created_by").references(() => authUsers.id),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
primaryKey: [table.profile_id, table.branch_id],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type AuthProfileBranch = typeof authProfileBranches.$inferSelect
|
||||||
|
export type NewAuthProfileBranch = typeof authProfileBranches.$inferInsert
|
||||||
30
backend/db/schema/auth_profile_teams.ts
Normal file
30
backend/db/schema/auth_profile_teams.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { pgTable, uuid, bigint, timestamp } from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { authProfiles } from "./auth_profiles"
|
||||||
|
import { teams } from "./teams"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const authProfileTeams = pgTable(
|
||||||
|
"auth_profile_teams",
|
||||||
|
{
|
||||||
|
created_at: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
profile_id: uuid("profile_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => authProfiles.id, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
team_id: bigint("team_id", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => teams.id, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
created_by: uuid("created_by").references(() => authUsers.id),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
primaryKey: [table.profile_id, table.team_id],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type AuthProfileTeam = typeof authProfileTeams.$inferSelect
|
||||||
|
export type NewAuthProfileTeam = typeof authProfileTeams.$inferInsert
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
jsonb,
|
jsonb,
|
||||||
} from "drizzle-orm/pg-core"
|
} from "drizzle-orm/pg-core"
|
||||||
import { authUsers } from "./auth_users"
|
import { authUsers } from "./auth_users"
|
||||||
|
import { branches } from "./branches"
|
||||||
|
|
||||||
export const authProfiles = pgTable("auth_profiles", {
|
export const authProfiles = pgTable("auth_profiles", {
|
||||||
id: uuid("id").primaryKey().defaultRandom(),
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
@@ -18,6 +19,8 @@ export const authProfiles = pgTable("auth_profiles", {
|
|||||||
|
|
||||||
tenant_id: bigint("tenant_id", { mode: "number" }).notNull(),
|
tenant_id: bigint("tenant_id", { mode: "number" }).notNull(),
|
||||||
|
|
||||||
|
branch_id: bigint("branch_id", { mode: "number" }).references(() => branches.id),
|
||||||
|
|
||||||
created_at: timestamp("created_at", { withTimezone: true })
|
created_at: timestamp("created_at", { withTimezone: true })
|
||||||
.notNull()
|
.notNull()
|
||||||
.defaultNow(),
|
.defaultNow(),
|
||||||
@@ -60,6 +63,7 @@ export const authProfiles = pgTable("auth_profiles", {
|
|||||||
|
|
||||||
email: text("email"),
|
email: text("email"),
|
||||||
token_id: text("token_id"),
|
token_id: text("token_id"),
|
||||||
|
calendar_subscription_token: text("calendar_subscription_token"),
|
||||||
|
|
||||||
weekly_working_days: doublePrecision("weekly_working_days"),
|
weekly_working_days: doublePrecision("weekly_working_days"),
|
||||||
|
|
||||||
@@ -71,6 +75,7 @@ export const authProfiles = pgTable("auth_profiles", {
|
|||||||
contract_type: text("contract_type"),
|
contract_type: text("contract_type"),
|
||||||
position: text("position"),
|
position: text("position"),
|
||||||
qualification: text("qualification"),
|
qualification: text("qualification"),
|
||||||
|
availability_note: text("availability_note"),
|
||||||
|
|
||||||
address_street: text("address_street"),
|
address_street: text("address_street"),
|
||||||
address_zip: text("address_zip"),
|
address_zip: text("address_zip"),
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export const authUsers = pgTable("auth_users", {
|
|||||||
|
|
||||||
multiTenant: boolean("multi_tenant").notNull().default(true),
|
multiTenant: boolean("multi_tenant").notNull().default(true),
|
||||||
must_change_password: boolean("must_change_password").notNull().default(false),
|
must_change_password: boolean("must_change_password").notNull().default(false),
|
||||||
|
is_admin: boolean("is_admin").notNull().default(false),
|
||||||
|
|
||||||
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
|
||||||
|
|||||||
37
backend/db/schema/branches.ts
Normal file
37
backend/db/schema/branches.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
bigint,
|
||||||
|
timestamp,
|
||||||
|
text,
|
||||||
|
boolean,
|
||||||
|
uuid,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const branches = pgTable("branches", {
|
||||||
|
id: bigint("id", { mode: "number" })
|
||||||
|
.primaryKey()
|
||||||
|
.generatedByDefaultAsIdentity(),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
|
||||||
|
tenant: bigint("tenant", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id),
|
||||||
|
|
||||||
|
name: text("name").notNull(),
|
||||||
|
number: text("number"),
|
||||||
|
description: text("description"),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type Branch = typeof branches.$inferSelect
|
||||||
|
export type NewBranch = typeof branches.$inferInsert
|
||||||
57
backend/db/schema/communication_rooms.ts
Normal file
57
backend/db/schema/communication_rooms.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
pgTable,
|
||||||
|
uuid,
|
||||||
|
bigint,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
boolean,
|
||||||
|
uniqueIndex,
|
||||||
|
index,
|
||||||
|
} from "drizzle-orm/pg-core"
|
||||||
|
|
||||||
|
import { tenants } from "./tenants"
|
||||||
|
import { authUsers } from "./auth_users"
|
||||||
|
|
||||||
|
export const communicationRooms = pgTable(
|
||||||
|
"communication_rooms",
|
||||||
|
{
|
||||||
|
id: uuid("id").primaryKey().defaultRandom(),
|
||||||
|
|
||||||
|
tenantId: bigint("tenant_id", { mode: "number" })
|
||||||
|
.notNull()
|
||||||
|
.references(() => tenants.id, { onDelete: "cascade" }),
|
||||||
|
|
||||||
|
key: text("key").notNull(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
topic: text("topic"),
|
||||||
|
type: text("type").notNull().default("room"),
|
||||||
|
|
||||||
|
entityType: text("entity_type"),
|
||||||
|
entityId: bigint("entity_id", { mode: "number" }),
|
||||||
|
entityUuid: uuid("entity_uuid"),
|
||||||
|
|
||||||
|
matrixRoomId: text("matrix_room_id"),
|
||||||
|
matrixAlias: text("matrix_alias"),
|
||||||
|
parentSpaceRoomId: text("parent_space_room_id"),
|
||||||
|
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true })
|
||||||
|
.notNull()
|
||||||
|
.defaultNow(),
|
||||||
|
updatedAt: timestamp("updated_at", { withTimezone: true }),
|
||||||
|
createdBy: uuid("created_by").references(() => authUsers.id),
|
||||||
|
updatedBy: uuid("updated_by").references(() => authUsers.id),
|
||||||
|
},
|
||||||
|
(table) => ({
|
||||||
|
tenantKeyIdx: uniqueIndex("communication_rooms_tenant_key_idx")
|
||||||
|
.on(table.tenantId, table.key),
|
||||||
|
tenantIdx: index("communication_rooms_tenant_idx")
|
||||||
|
.on(table.tenantId),
|
||||||
|
entityIdx: index("communication_rooms_entity_idx")
|
||||||
|
.on(table.tenantId, table.entityType, table.entityId, table.entityUuid),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
export type CommunicationRoom = typeof communicationRooms.$inferSelect
|
||||||
|
export type NewCommunicationRoom = typeof communicationRooms.$inferInsert
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user