From e50156455ded010932fd77eb8daadadd2142251c Mon Sep 17 00:00:00 2001 From: "Sebastian BURGIN-FIX (ext)" Date: Sat, 1 Aug 2026 09:45:43 +0200 Subject: [PATCH 1/6] Updated --- .env.example | 8 +- .gitignore | 3 + README.md | 20 +- app/Actions/LlmUsageStatsAction.php | 98 ++- app/Actions/ViewDataAction.php | 57 +- app/Checks/FilesystemsDefaultCheck.php | 33 - app/Console/Commands/ImportCommand.php | 8 + app/Console/Commands/ImportNewsCommand.php | 57 +- .../Commands/SyncRepositoriesCommand.php | 18 +- app/DTO/ContactDTO.php | 9 +- app/DTO/LlmUsageFilterDTO.php | 89 +++ app/Enums/CacheKeyEnum.php | 4 +- app/Enums/ContactSectionEnum.php | 8 +- app/Helpers/Facades/HelperBank.php | 20 - app/Helpers/Facades/HelperDate.php | 2 + app/Helpers/Facades/HelperDevice.php | 20 - app/Helpers/Facades/HelperFile.php | 22 - app/Helpers/Facades/HelperMarkdown.php | 20 - app/Helpers/Facades/HelperMoney.php | 24 - app/Helpers/Facades/HelperNumber.php | 3 +- app/Helpers/Facades/HelperPhone.php | 20 - app/Helpers/HelperBank.php | 15 - app/Helpers/HelperDevice.php | 18 - app/Helpers/HelperFile.php | 25 - app/Helpers/HelperMarkdown.php | 15 - app/Helpers/HelperMoney.php | 38 - app/Helpers/HelperPhone.php | 19 - .../AboutUs/AboutUsIndexController.php | 9 +- app/Http/Controllers/Ai/AiIndexController.php | 23 +- .../Ai/AiLlmAnalyticsIndexController.php | 101 +-- .../Controllers/Ai/AiLlmIndexController.php | 1 + .../CoWorking/CoWorkingIndexController.php | 25 +- .../Contact/ContactIndexController.php | 19 +- .../Demo/FlowsLayoutDemoController.php | 221 ------ .../Network/NetworkIndexController.php | 11 +- .../Controllers/News/NewsShowController.php | 25 +- .../OpenSource/OpenSoruceShowController.php | 44 -- .../OpenSource/OpenSourceIndexController.php | 17 +- .../OpenSource/OpenSourceShowController.php | 23 + .../Products/ProductsIndexController.php | 12 +- .../Products/ProductsShowController.php | 21 +- .../Services/ServicesShowController.php | 12 +- .../TechnologiesIndexController.php | 12 +- .../TechnologiesShowController.php | 12 +- app/Http/Middleware/SecurityHeaders.php | 1 + app/Jobs/Network/SendNetworkInviteJob.php | 27 + app/Jobs/Network/SendNetworkManageLinkJob.php | 28 + app/Markdown/CodeTheme.php | 66 ++ app/Markdown/NewsMarkdown.php | 31 +- app/Models/AiModel.php | 4 +- app/Models/News.php | 20 + app/Observers/ContentCacheObserver.php | 4 +- app/Observers/NetworkObserver.php | 15 +- app/Observers/NetworkUserObserver.php | 6 +- app/Observers/NewsObserver.php | 4 +- app/Providers/AppServiceProvider.php | 28 +- app/Security/Presets/MyCspPreset.php | 9 +- app/Seo/SchemaNodes.php | 2 +- app/Support/NewsImage.php | 6 +- app/Support/PageNavigation.php | 39 +- app/Support/ResponseCacheFlusher.php | 59 ++ bootstrap/app.php | 1 - composer.json | 4 +- composer.lock | 122 ++-- config/company.php | 4 +- config/health.php | 2 +- database/files/ai_models/deepseek-ocr.yaml | 4 +- .../files/ai_models/deepseek-v4-flash.yaml | 4 +- database/files/ai_models/gemma4-31b.yaml | 4 +- database/files/ai_models/kimi-linear-48b.yaml | 4 +- database/files/ai_models/qwen3-6-35b.yaml | 4 +- database/files/ai_models/qwen3-coder-30b.yaml | 4 +- .../files/ai_models/qwen3-embedding-8b.yaml | 4 +- database/files/ai_models/qwen3-vl-32b.yaml | 4 +- database/files/ai_models/qwen3-vl-8b.yaml | 4 +- .../de_CH/2025-04-06-docuware-7-12-is-here.md | 5 +- .../de_CH/2025-09-15-docuware-7-13-is-here.md | 5 +- .../de_CH/2026-05-12-docuware-7-14-is-here.md | 5 +- .../de_CH/2026-07-28-bausteine-styleguide.md | 9 +- .../2026-07-29-llm-gateway-open-source.md | 191 ----- .../2026-07-31-llm-gateway-open-source.md | 164 +++++ .../en_CH/2025-04-06-docuware-7-12-is-here.md | 5 +- .../en_CH/2025-09-15-docuware-7-13-is-here.md | 5 +- .../en_CH/2026-05-12-docuware-7-14-is-here.md | 5 +- .../en_CH/2026-07-28-bausteine-styleguide.md | 9 +- .../2026-07-29-llm-gateway-open-source.md | 182 ----- .../2026-07-31-llm-gateway-open-source.md | 159 +++++ database/files/products/de_CH/flows.md | 4 +- .../services/de_CH/dms-ecm-consulting.md | 6 +- .../de_CH/individuelle-softwareentwicklung.md | 6 +- .../services/de_CH/konzeption-prototyping.md | 7 +- .../files/services/de_CH/open-source-erp.md | 7 +- .../services/en_CH/dms-ecm-consulting.md | 5 +- .../en_CH/individuelle-softwareentwicklung.md | 5 +- .../services/en_CH/konzeption-prototyping.md | 6 +- .../files/services/en_CH/open-source-erp.md | 5 +- .../files/team/sebastian-buergin-fix.yaml | 1 + ...1_01_000010_create_auth_provider_table.php | 5 + .../2026_07_28_150010_create_news_table.php | 4 + .../2026_08_01_090000_add_listing_indexes.php | 82 +++ lang/de_CH.json | 33 +- lang/de_CH/components.php | 22 +- lang/en_CH.json | 33 +- lang/en_CH/components.php | 24 +- prompts/illustration-news-card.md | 152 ++++ prompts/illustration-news.md | 250 +++++++ prompts/illustration-services.md | 643 +++++++++++++++++ prompts/images-news-square.md | 120 ++++ prompts/images-news.md | 12 +- public/fonts/poppins/poppins-regular.woff2 | Bin 7884 -> 0 bytes public/images/illustrations.json | 109 +++ .../images/news/bausteine-styleguide-card.svg | 75 ++ public/images/news/bausteine-styleguide.png | Bin 0 -> 124086 bytes public/images/news/bausteine-styleguide.svg | 169 +++++ .../news/docuware-7-12-is-here-card.svg | 71 ++ public/images/news/docuware-7-12-is-here.png | Bin 0 -> 99193 bytes public/images/news/docuware-7-12-is-here.svg | 114 +++ .../news/docuware-7-13-is-here-card.svg | 59 ++ public/images/news/docuware-7-13-is-here.png | Bin 0 -> 105399 bytes public/images/news/docuware-7-13-is-here.svg | 123 ++++ .../news/docuware-7-14-is-here-card.svg | 66 ++ public/images/news/docuware-7-14-is-here.png | Bin 0 -> 109413 bytes public/images/news/docuware-7-14-is-here.svg | 125 ++++ .../news/llm-gateway-open-source-card.svg | 89 +++ .../images/news/llm-gateway-open-source.png | Bin 0 -> 99101 bytes .../images/news/llm-gateway-open-source.svg | 155 ++++ .../bausteine-styleguide-square.svg | 60 ++ .../placeholders/docuware-7-12-square.svg | 63 ++ .../placeholders/docuware-7-13-square.svg | 78 ++ .../placeholders/docuware-7-14-square.svg | 73 ++ .../llm-gateway-open-source-square.svg | 83 +++ .../services/dms-ecm-consulting-card.svg | 69 ++ public/images/services/dms-ecm-consulting.png | Bin 0 -> 144605 bytes public/images/services/dms-ecm-consulting.svg | 139 ++++ .../individuelle-softwareentwicklung-card.svg | 73 ++ .../individuelle-softwareentwicklung.png | Bin 0 -> 114840 bytes .../individuelle-softwareentwicklung.svg | 142 ++++ .../services/konzeption-prototyping-card.svg | 56 ++ .../services/konzeption-prototyping.png | Bin 0 -> 123997 bytes .../services/konzeption-prototyping.svg | 139 ++++ .../images/services/open-source-erp-card.svg | 79 +++ public/images/services/open-source-erp.png | Bin 0 -> 111054 bytes public/images/services/open-source-erp.svg | 128 ++++ reports/frontend.md | 305 ++++++++ reports/longterm.md | 144 ++++ resources/css/app.css | 462 ++++++++++-- .../poppins-300-normal-latin-ext.woff2 | Bin 0 -> 5524 bytes .../poppins/poppins-300-normal-latin.woff2 | Bin 0 -> 7840 bytes .../poppins-400-normal-latin-ext.woff2 | Bin .../poppins/poppins-400-normal-latin.woff2 | Bin .../poppins-500-normal-latin-ext.woff2 | Bin 0 -> 5484 bytes .../poppins/poppins-500-normal-latin.woff2 | Bin 0 -> 7748 bytes .../poppins-600-normal-latin-ext.woff2 | Bin .../poppins/poppins-600-normal-latin.woff2 | Bin .../poppins-700-normal-latin-ext.woff2 | Bin .../poppins/poppins-700-normal-latin.woff2 | Bin resources/fonts/poppins/poppins-regular.woff2 | Bin 7884 -> 0 bytes resources/js/app.js | 100 ++- resources/views/app/about-us/index.blade.php | 18 +- resources/views/app/ai/index.blade.php | 14 +- .../views/app/ai/llm/analytics.blade.php | 71 +- resources/views/app/ai/llm/index.blade.php | 6 +- .../views/app/co-working/index.blade.php | 8 +- resources/views/app/contact/index.blade.php | 37 +- resources/views/app/jobs/index.blade.php | 23 +- .../views/app/legal/imprint/index.blade.php | 7 +- .../views/app/legal/privacy/index.blade.php | 2 +- .../views/app/legal/terms/index.blade.php | 2 +- resources/views/app/media/index.blade.php | 4 +- resources/views/app/network/index.blade.php | 4 +- resources/views/app/network/manage.blade.php | 13 +- .../app/network/pages/baselhack.blade.php | 35 +- resources/views/app/network/request.blade.php | 7 +- resources/views/app/news/index.blade.php | 71 +- resources/views/app/news/show.blade.php | 168 ++--- resources/views/app/products/index.blade.php | 2 +- resources/views/app/services/index.blade.php | 29 +- .../views/app/technologies/index.blade.php | 2 +- .../components/ai-llm/archive-table.blade.php | 25 +- .../views/components/breadcrumbs.blade.php | 14 +- .../components/card/item-card-body.blade.php | 7 +- .../views/components/card/linkable.blade.php | 5 +- .../views/components/card/nav-card.blade.php | 3 +- .../components/card/person-card.blade.php | 2 +- .../views/components/card/stat-card.blade.php | 6 +- resources/views/components/demo/bar.blade.php | 15 - resources/views/components/explore.blade.php | 2 +- .../views/components/form/combobox.blade.php | 4 +- .../views/components/form/field.blade.php | 4 - .../views/components/form/file.blade.php | 18 +- .../views/components/form/input.blade.php | 9 +- resources/views/components/h1.blade.php | 5 +- resources/views/components/h2.blade.php | 10 +- resources/views/components/h3.blade.php | 5 +- .../components/illustration-row.blade.php | 60 ++ resources/views/components/intro.blade.php | 141 +++- .../views/components/layout/lead.blade.php | 5 + .../components/layout/page-header.blade.php | 30 +- .../components/layout/page-note.blade.php | 1 + resources/views/components/nav/link.blade.php | 9 +- .../components/nav/locale-switch.blade.php | 5 +- .../views/components/news/avatar.blade.php | 10 +- .../views/components/news/byline.blade.php | 20 - .../views/components/news/card-meta.blade.php | 54 ++ .../views/components/news/card.blade.php | 82 +-- .../views/components/news/latest.blade.php | 22 +- .../views/components/news/lead.blade.php | 42 ++ .../views/components/news/list.blade.php | 43 ++ .../components/news/series-nav.blade.php | 19 +- .../news/table-of-contents.blade.php | 8 +- .../views/components/next-page.blade.php | 6 +- .../views/components/opening-hours.blade.php | 8 +- .../views/components/ui/arrow-link.blade.php | 4 +- resources/views/components/ui/badge.blade.php | 11 +- .../views/components/ui/button.blade.php | 6 +- resources/views/components/ui/link.blade.php | 4 +- .../views/components/ui/pagination.blade.php | 66 ++ resources/views/components/ui/row.blade.php | 8 +- .../components/ui/social-links.blade.php | 9 +- resources/views/components/ui/table.blade.php | 11 + .../views/components/ui/table/cell.blade.php | 12 + .../views/components/ui/table/row.blade.php | 11 + resources/views/demo/_app_layout.blade.php | 6 - resources/views/demo/_layout.blade.php | 24 - resources/views/demo/flows/index.blade.php | 41 -- resources/views/demo/flows/v2/index.blade.php | 44 -- .../flows/v2/variants/blueprint.blade.php | 87 --- .../flows/v2/variants/data-bars.blade.php | 94 --- .../flows/v2/variants/doc-stack.blade.php | 85 --- .../flows/v2/variants/dot-halftone.blade.php | 105 --- .../flows/v2/variants/flow-diagram.blade.php | 97 --- .../flows/v2/variants/isometric.blade.php | 112 --- .../flows/v2/variants/line-icons.blade.php | 93 --- .../flows/v2/variants/network-nodes.blade.php | 102 --- .../flows/v2/variants/organic-blobs.blade.php | 79 --- .../flows/v2/variants/stamp-seal.blade.php | 92 --- .../flows/variants/before-after.blade.php | 83 --- .../views/demo/flows/variants/bento.blade.php | 75 -- .../flows/variants/big-statement.blade.php | 70 -- .../demo/flows/variants/docs-split.blade.php | 82 --- .../demo/flows/variants/editorial.blade.php | 64 -- .../demo/flows/variants/journey.blade.php | 85 --- .../flows/variants/saas-landing.blade.php | 104 --- .../demo/flows/variants/swiss-grid.blade.php | 78 -- .../flows/variants/tabs-dashboard.blade.php | 82 --- .../demo/flows/variants/terminal.blade.php | 81 --- .../errors/partials/_error-page.blade.php | 5 +- .../views/layouts/_partials/_footer.blade.php | 16 +- .../_partials/_footer/labels.blade.php | 4 +- .../layouts/_partials/_navigation.blade.php | 4 +- .../_partials/_navigation_mobile.blade.php | 49 +- resources/views/layouts/app.blade.php | 24 +- resources/views/markdown/video.blade.php | 4 +- .../vendor/pagination/tailwind.blade.php | 1 + routes/console.php | 4 +- routes/web.php | 46 +- scripts/check-illustrations.py | 669 ++++++++++++++++++ scripts/make-news-square.py | 335 +++++++++ scripts/render-news-og.sh | 22 +- tests/Feature/Components/IntroTabsTest.php | 175 +++++ .../Controllers/LocaleRouteConstraintTest.php | 33 + .../Network/NetworkRequestControllerTest.php | 16 + tests/Feature/News/ImportNewsCommandTest.php | 41 ++ .../Seeders/ServicesTableSeederTest.php | 2 +- tests/Helper/HelperBankClassTest.php | 14 - tests/Helper/HelperDeviceClassTest.php | 11 - tests/Helper/HelperFileClassTest.php | 54 -- tests/Helper/HelperMarkdownClassTest.php | 14 - tests/Helper/HelperMoneyClassTest.php | 43 -- tests/Helper/HelperPhoneClassTest.php | 12 - tests/Unit/Actions/ViewDataActionTest.php | 8 +- tests/Unit/Checks/HealthChecksTest.php | 17 - tests/Unit/DTO/ContactDTOTest.php | 5 +- tests/Unit/Enums/CacheKeyEnumTest.php | 14 +- tests/Unit/Enums/ContactSectionEnumTest.php | 11 +- tests/Unit/Markdown/NewsMarkdownTest.php | 33 + tests/Unit/Models/AiModelModelTest.php | 12 +- tests/Unit/Support/PageNavigationTest.php | 50 +- .../Unit/Support/ResponseCacheFlusherTest.php | 44 ++ todos/news-konzept.md | 10 +- 280 files changed, 8195 insertions(+), 4139 deletions(-) delete mode 100644 app/Checks/FilesystemsDefaultCheck.php create mode 100644 app/DTO/LlmUsageFilterDTO.php delete mode 100644 app/Helpers/Facades/HelperBank.php delete mode 100644 app/Helpers/Facades/HelperDevice.php delete mode 100644 app/Helpers/Facades/HelperFile.php delete mode 100644 app/Helpers/Facades/HelperMarkdown.php delete mode 100644 app/Helpers/Facades/HelperMoney.php delete mode 100644 app/Helpers/Facades/HelperPhone.php delete mode 100644 app/Helpers/HelperBank.php delete mode 100644 app/Helpers/HelperDevice.php delete mode 100644 app/Helpers/HelperFile.php delete mode 100644 app/Helpers/HelperMarkdown.php delete mode 100755 app/Helpers/HelperMoney.php delete mode 100755 app/Helpers/HelperPhone.php delete mode 100644 app/Http/Controllers/Demo/FlowsLayoutDemoController.php delete mode 100644 app/Http/Controllers/OpenSource/OpenSoruceShowController.php create mode 100644 app/Http/Controllers/OpenSource/OpenSourceShowController.php create mode 100644 app/Markdown/CodeTheme.php create mode 100644 app/Support/ResponseCacheFlusher.php delete mode 100644 database/files/news/de_CH/2026-07-29-llm-gateway-open-source.md create mode 100644 database/files/news/de_CH/2026-07-31-llm-gateway-open-source.md delete mode 100644 database/files/news/en_CH/2026-07-29-llm-gateway-open-source.md create mode 100644 database/files/news/en_CH/2026-07-31-llm-gateway-open-source.md create mode 100644 database/migrations/2026_08_01_090000_add_listing_indexes.php create mode 100644 prompts/illustration-news-card.md create mode 100644 prompts/illustration-news.md create mode 100644 prompts/illustration-services.md create mode 100644 prompts/images-news-square.md delete mode 100644 public/fonts/poppins/poppins-regular.woff2 create mode 100644 public/images/illustrations.json create mode 100644 public/images/news/bausteine-styleguide-card.svg create mode 100644 public/images/news/bausteine-styleguide.png create mode 100644 public/images/news/bausteine-styleguide.svg create mode 100644 public/images/news/docuware-7-12-is-here-card.svg create mode 100644 public/images/news/docuware-7-12-is-here.png create mode 100644 public/images/news/docuware-7-12-is-here.svg create mode 100644 public/images/news/docuware-7-13-is-here-card.svg create mode 100644 public/images/news/docuware-7-13-is-here.png create mode 100644 public/images/news/docuware-7-13-is-here.svg create mode 100644 public/images/news/docuware-7-14-is-here-card.svg create mode 100644 public/images/news/docuware-7-14-is-here.png create mode 100644 public/images/news/docuware-7-14-is-here.svg create mode 100644 public/images/news/llm-gateway-open-source-card.svg create mode 100644 public/images/news/llm-gateway-open-source.png create mode 100644 public/images/news/llm-gateway-open-source.svg create mode 100644 public/images/news/placeholders/bausteine-styleguide-square.svg create mode 100644 public/images/news/placeholders/docuware-7-12-square.svg create mode 100644 public/images/news/placeholders/docuware-7-13-square.svg create mode 100644 public/images/news/placeholders/docuware-7-14-square.svg create mode 100644 public/images/news/placeholders/llm-gateway-open-source-square.svg create mode 100644 public/images/services/dms-ecm-consulting-card.svg create mode 100644 public/images/services/dms-ecm-consulting.png create mode 100644 public/images/services/dms-ecm-consulting.svg create mode 100644 public/images/services/individuelle-softwareentwicklung-card.svg create mode 100644 public/images/services/individuelle-softwareentwicklung.png create mode 100644 public/images/services/individuelle-softwareentwicklung.svg create mode 100644 public/images/services/konzeption-prototyping-card.svg create mode 100644 public/images/services/konzeption-prototyping.png create mode 100644 public/images/services/konzeption-prototyping.svg create mode 100644 public/images/services/open-source-erp-card.svg create mode 100644 public/images/services/open-source-erp.png create mode 100644 public/images/services/open-source-erp.svg create mode 100644 reports/frontend.md create mode 100644 reports/longterm.md create mode 100644 resources/fonts/poppins/poppins-300-normal-latin-ext.woff2 create mode 100644 resources/fonts/poppins/poppins-300-normal-latin.woff2 rename {public => resources}/fonts/poppins/poppins-400-normal-latin-ext.woff2 (100%) rename {public => resources}/fonts/poppins/poppins-400-normal-latin.woff2 (100%) create mode 100644 resources/fonts/poppins/poppins-500-normal-latin-ext.woff2 create mode 100644 resources/fonts/poppins/poppins-500-normal-latin.woff2 rename {public => resources}/fonts/poppins/poppins-600-normal-latin-ext.woff2 (100%) rename {public => resources}/fonts/poppins/poppins-600-normal-latin.woff2 (100%) rename {public => resources}/fonts/poppins/poppins-700-normal-latin-ext.woff2 (100%) rename {public => resources}/fonts/poppins/poppins-700-normal-latin.woff2 (100%) delete mode 100644 resources/fonts/poppins/poppins-regular.woff2 delete mode 100644 resources/views/components/demo/bar.blade.php create mode 100644 resources/views/components/illustration-row.blade.php create mode 100644 resources/views/components/layout/lead.blade.php create mode 100644 resources/views/components/layout/page-note.blade.php delete mode 100644 resources/views/components/news/byline.blade.php create mode 100644 resources/views/components/news/card-meta.blade.php create mode 100644 resources/views/components/news/lead.blade.php create mode 100644 resources/views/components/news/list.blade.php create mode 100644 resources/views/components/ui/pagination.blade.php create mode 100644 resources/views/components/ui/table.blade.php create mode 100644 resources/views/components/ui/table/cell.blade.php create mode 100644 resources/views/components/ui/table/row.blade.php delete mode 100644 resources/views/demo/_app_layout.blade.php delete mode 100644 resources/views/demo/_layout.blade.php delete mode 100644 resources/views/demo/flows/index.blade.php delete mode 100644 resources/views/demo/flows/v2/index.blade.php delete mode 100644 resources/views/demo/flows/v2/variants/blueprint.blade.php delete mode 100644 resources/views/demo/flows/v2/variants/data-bars.blade.php delete mode 100644 resources/views/demo/flows/v2/variants/doc-stack.blade.php delete mode 100644 resources/views/demo/flows/v2/variants/dot-halftone.blade.php delete mode 100644 resources/views/demo/flows/v2/variants/flow-diagram.blade.php delete mode 100644 resources/views/demo/flows/v2/variants/isometric.blade.php delete mode 100644 resources/views/demo/flows/v2/variants/line-icons.blade.php delete mode 100644 resources/views/demo/flows/v2/variants/network-nodes.blade.php delete mode 100644 resources/views/demo/flows/v2/variants/organic-blobs.blade.php delete mode 100644 resources/views/demo/flows/v2/variants/stamp-seal.blade.php delete mode 100644 resources/views/demo/flows/variants/before-after.blade.php delete mode 100644 resources/views/demo/flows/variants/bento.blade.php delete mode 100644 resources/views/demo/flows/variants/big-statement.blade.php delete mode 100644 resources/views/demo/flows/variants/docs-split.blade.php delete mode 100644 resources/views/demo/flows/variants/editorial.blade.php delete mode 100644 resources/views/demo/flows/variants/journey.blade.php delete mode 100644 resources/views/demo/flows/variants/saas-landing.blade.php delete mode 100644 resources/views/demo/flows/variants/swiss-grid.blade.php delete mode 100644 resources/views/demo/flows/variants/tabs-dashboard.blade.php delete mode 100644 resources/views/demo/flows/variants/terminal.blade.php create mode 100644 resources/views/vendor/pagination/tailwind.blade.php create mode 100755 scripts/check-illustrations.py create mode 100755 scripts/make-news-square.py create mode 100644 tests/Feature/Components/IntroTabsTest.php create mode 100644 tests/Feature/Controllers/LocaleRouteConstraintTest.php delete mode 100644 tests/Helper/HelperBankClassTest.php delete mode 100644 tests/Helper/HelperDeviceClassTest.php delete mode 100644 tests/Helper/HelperFileClassTest.php delete mode 100644 tests/Helper/HelperMarkdownClassTest.php delete mode 100644 tests/Helper/HelperMoneyClassTest.php delete mode 100644 tests/Helper/HelperPhoneClassTest.php create mode 100644 tests/Unit/Support/ResponseCacheFlusherTest.php diff --git a/.env.example b/.env.example index 0955855..12542ad 100644 --- a/.env.example +++ b/.env.example @@ -32,7 +32,10 @@ SESSION_DOMAIN=web.codebar.test BROADCAST_CONNECTION=log FILESYSTEM_DISK=local -QUEUE_CONNECTION=sync +# Not "sync": the network profile link is sent from a queued job, and running it inside +# the request makes the deliberately constant response observable by its timing. +# Needs `php artisan queue:work` (see README). +QUEUE_CONNECTION=database MAIL_MAILER=log MAIL_HOST=localhost @@ -62,3 +65,6 @@ LARAVEL_DEFAULT_FATHOM_SITE_ID=WECFOADW LITELLM_URL=https://llm.codebar.net LITELLM_MASTER_KEY= GITHUB_TOKEN= + +# Where a failing health check reports to. Unset means no notification is sent. +HEALTH_NOTIFICATION_EMAIL= diff --git a/.gitignore b/.gitignore index ea4c8f9..d123952 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,6 @@ auth.json # Lighthouse audit reports (regenerate via tests/lighthouse/run.sh) /tests/lighthouse/reports/ --cache-directory + +# Generated Lighthouse runs — keep run.sh and pages.json, not their output. +tests/lighthouse/reports/ diff --git a/README.md b/README.md index 99b754e..84ac4fd 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ The site ships in German (`de_CH`) and English (`en_CH`) under distinct, fully t - **Structured data** — a single `@graph` JSON-LD payload (`App\Seo\SchemaGraph` / `App\Seo\SchemaNodes`) built from `config/company.php`, the one source of truth for the company's name, addresses, phone and `sameAs` profiles. Every page ships `Organization`, `WebSite`, `WebPage` and `BreadcrumbList` nodes; content pages add `Service`, `Person`, `BlogPosting`, etc. - **Sitemap** — `App\Sitemap\SitemapBuilder` + `App\Http\Controllers\Sitemap\SitemapController` build `/sitemap.xml` from the same models the site renders, cached 24h via `Cache::remember`. -- **Response cache** — `spatie/laravel-responsecache` caches full HTTP responses for up to 7 days. `App\Observers\SitemapCacheObserver` drops the sitemap's own data cache on every relevant model save, and `responsecache:clear` runs hourly via the scheduler (`routes/console.php`) as a backstop for the full-page cache layer — these are two independent caches and both need clearing after a bulk content change (`php artisan responsecache:clear` after a fresh `db:seed`, for instance). +- **Response cache** — `spatie/laravel-responsecache` caches full HTTP responses for up to 7 days. Invalidation is event-driven, not scheduled: `App\Observers\SitemapCacheObserver` drops the sitemap's data cache and the content observers drop both the listing caches and the rendered pages on every relevant model save. Clearing goes through `App\Support\ResponseCacheFlusher`, which collapses the flush to once per import — the response cache has no per-URL invalidation, so an import that saves 50 rows would otherwise clear the whole site 50 times. - **Social images** — article heroes that are local SVG placeholders (no real photography yet) are not usable as `og:image` — social crawlers don't render SVG. `App\Support\NewsImage::ogImage()` falls back to a same-named `.png` rendered from the SVG when one exists, or the site's default share image otherwise. - **Tests** — `tests/Feature/Seo/` asserts on the actual rendered JSON-LD and meta tags (not just "does it look right"), and `tests/lighthouse/` audits real Lighthouse scores against the built (`npm run build`) output, not the Vite dev server. @@ -100,6 +100,17 @@ valet open > Set `valetTls: 'your-domain.test'` below `refresh: true` in `vite.config.js` if you use `valet secure` / `herd secure`. +### Background work + +`QUEUE_CONNECTION=database`, so a worker has to be running for anything queued to happen — the network profile link, the LLM usage sync. Deliberately not `sync`: running the mail job inside the request makes the intentionally constant response of `network.request.store` observable by its timing. + +```bash +php artisan queue:work # network profile mails, LLM usage sync +php artisan schedule:work # hourly LLM sync, health checks +``` + +Without the scheduler, `health:check` never runs and `/health` reports stale results. A dispatched job with no worker is a silent black hole — `JobsCheck` exists to catch exactly that, and reports through `/health`. + ## Testing & code quality ```bash @@ -115,8 +126,11 @@ The site runs on [Laravel Cloud](https://cloud.laravel.com), behind Cloudflare. - `CSP_ENABLED=true` — enables Content-Security-Policy enforcement via Spatie CSP middleware - `FPH_ENABLED=true` — enables Permissions-Policy headers +- `HEALTH_NOTIFICATION_EMAIL` — where a failing health check reports to; unset means no notification is sent + +HSTS, COOP, CORP, `X-Content-Type-Options`, `Referrer-Policy` and `X-Frame-Options` are applied automatically by the `SecurityHeaders` middleware on every web response. -HSTS, COOP, `X-Content-Type-Options`, `Referrer-Policy` and `X-Frame-Options` are applied automatically by the `SecurityHeaders` middleware on every web response. +`/health` returns the Spatie Health JSON result set. It is rate limited and never response-cached; it needs `schedule:work` running to hold current results. **Lighthouse note:** deprecated-API warnings for `/cdn-cgi/challenge-platform/scripts/jsd/main.js` come from Cloudflare's bot protection, injected at the edge — not from application code. Run Lighthouse in incognito without extensions for an accurate score, and prefer `tests/lighthouse/` (which audits the built output) over ad-hoc runs against the dev server. @@ -124,7 +138,7 @@ HSTS, COOP, `X-Content-Type-Options`, `Referrer-Policy` and `X-Frame-Options` ar - **Content & storage** — `spatie/laravel-translatable`, `codebar-ag/laravel-flysystem-cloudinary` (editorial images), `league/flysystem-aws-s3-v3` (DigitalOcean Spaces for other assets), `symfony/yaml` - **SEO** — `spatie/laravel-sitemap`, `spatie/laravel-responsecache` -- **Security & health** — `spatie/laravel-csp`, `spatie/laravel-honeypot`, `spatie/laravel-permission`, `spatie/laravel-health`, `spatie/security-advisories-health-check`, `mazedlx/laravel-feature-policy` +- **Security & health** — `spatie/laravel-csp`, `spatie/laravel-honeypot` (network request form), `spatie/laravel-permission`, `spatie/laravel-health`, `spatie/security-advisories-health-check`, `mazedlx/laravel-feature-policy` - **Ops** — `laravel/nightwatch` (observability), `symfony/postmark-mailer` - **Analytics** — [Fathom](https://usefathom.com) (privacy-friendly, no cookie banner) diff --git a/app/Actions/LlmUsageStatsAction.php b/app/Actions/LlmUsageStatsAction.php index 89ce355..9bde376 100644 --- a/app/Actions/LlmUsageStatsAction.php +++ b/app/Actions/LlmUsageStatsAction.php @@ -10,7 +10,6 @@ use Closure; use DateTimeInterface; use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Eloquent\Collection as EloquentCollection; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; @@ -44,16 +43,18 @@ public function hasOtherModels(): bool } /** - * @return Collection + * The calendar years usage has been recorded in, oldest first. + * + * @return Collection */ public function years(): Collection { return $this->remember('years', function () { return AiModelDailyUsage::query() - ->orderBy('date') - ->get(['date']) - ->map(fn (AiModelDailyUsage $row): string => $row->date->format('Y')) - ->unique() + ->selectRaw('DISTINCT EXTRACT(YEAR FROM date)::int AS year') + ->orderBy('year') + ->pluck('year') + ->map(fn (mixed $year): string => $this->toYear($year)) ->values(); }); } @@ -78,31 +79,33 @@ private function toDate(mixed $value): ?Carbon } /** - * @return Collection, completion_tokens: int<0, max>, total_tokens: int<0, max>, requests: int<0, max>}> + * @return Collection */ public function monthlyBreakdown(?string $year, ?string $month, ?string $model): Collection { $suffix = 'breakdown_'.($year ?? 'all').'_'.($month ?? 'all').'_'.($model ?? 'all'); return $this->remember($suffix, function () use ($year, $month, $model) { - return AiModelDailyUsage::query() - ->when($model === self::OTHER_MODEL, fn (Builder $query) => $query->whereNull('ai_model_id')) - ->when($model && $model !== self::OTHER_MODEL, fn (Builder $query) => $query->where('model', $model)) + // Grouped in the database rather than in PHP: this table grows by one row + // per model per day forever, and the page only ever shows the totals. + return $this->filtered($model) ->when($year, fn (Builder $query) => $query->whereYear('date', $year)) ->when($month, fn (Builder $query) => $query->whereMonth('date', $month)) - ->orderBy('date') + ->selectRaw("to_char(date, 'YYYY-MM') AS label") + ->selectRaw('COALESCE(SUM(prompt_tokens), 0)::bigint AS prompt_tokens') + ->selectRaw('COALESCE(SUM(completion_tokens), 0)::bigint AS completion_tokens') + ->selectRaw('COALESCE(SUM(total_tokens), 0)::bigint AS total_tokens') + ->selectRaw('COALESCE(SUM(requests), 0)::bigint AS requests') + ->groupByRaw("to_char(date, 'YYYY-MM')") + ->orderByRaw("to_char(date, 'YYYY-MM')") ->get() - ->groupBy(fn (AiModelDailyUsage $row) => $row->date->format('Y-m')) - ->map( - /** @param EloquentCollection $rows */ - fn (EloquentCollection $rows, string $label) => [ - 'label' => $label, - 'prompt_tokens' => $rows->sum(fn (AiModelDailyUsage $row): int => $row->prompt_tokens), - 'completion_tokens' => $rows->sum(fn (AiModelDailyUsage $row): int => $row->completion_tokens), - 'total_tokens' => $rows->sum(fn (AiModelDailyUsage $row): int => $row->total_tokens), - 'requests' => $rows->sum(fn (AiModelDailyUsage $row): int => $row->requests), - ] - ) + ->map(fn (AiModelDailyUsage $row): array => [ + 'label' => $this->toString($row->getAttribute('label')), + 'prompt_tokens' => $this->toInt($row->getAttribute('prompt_tokens')), + 'completion_tokens' => $this->toInt($row->getAttribute('completion_tokens')), + 'total_tokens' => $this->toInt($row->getAttribute('total_tokens')), + 'requests' => $this->toInt($row->getAttribute('requests')), + ]) ->values(); }); } @@ -137,21 +140,56 @@ public function totalSummary(?string $model = null): array private function summary(string $suffix, ?CarbonImmutable $from, ?string $model = null): array { return $this->remember($suffix.'_'.($model ?? 'all'), function () use ($from, $model) { - $rows = AiModelDailyUsage::query() + $row = $this->filtered($model) ->when($from, fn (Builder $query) => $query->where('date', '>=', $from)) - ->when($model === self::OTHER_MODEL, fn (Builder $query) => $query->whereNull('ai_model_id')) - ->when($model && $model !== self::OTHER_MODEL, fn (Builder $query) => $query->where('model', $model)) - ->get(); + ->selectRaw('COALESCE(SUM(prompt_tokens), 0)::bigint AS prompt_tokens') + ->selectRaw('COALESCE(SUM(completion_tokens), 0)::bigint AS completion_tokens') + ->selectRaw('COALESCE(SUM(total_tokens), 0)::bigint AS total_tokens') + ->selectRaw('COALESCE(SUM(requests), 0)::bigint AS requests') + ->first(); return [ - 'prompt_tokens' => $rows->sum(fn (AiModelDailyUsage $row): int => $row->prompt_tokens), - 'completion_tokens' => $rows->sum(fn (AiModelDailyUsage $row): int => $row->completion_tokens), - 'total_tokens' => $rows->sum(fn (AiModelDailyUsage $row): int => $row->total_tokens), - 'requests' => $rows->sum(fn (AiModelDailyUsage $row): int => $row->requests), + 'prompt_tokens' => $this->toInt($row?->getAttribute('prompt_tokens')), + 'completion_tokens' => $this->toInt($row?->getAttribute('completion_tokens')), + 'total_tokens' => $this->toInt($row?->getAttribute('total_tokens')), + 'requests' => $this->toInt($row?->getAttribute('requests')), ]; }); } + /** + * Aggregate columns come back untyped — PostgreSQL hands SUM() over a bigint back as + * a string, and a grouped row carries no cast from the model. + */ + private function toInt(mixed $value): int + { + return is_numeric($value) ? (int) $value : 0; + } + + private function toString(mixed $value): string + { + return is_string($value) ? $value : ''; + } + + /** The filter compares against the query string, so a year travels as text. */ + private function toYear(mixed $value): string + { + return (string) $this->toInt($value); + } + + /** + * The model filter every aggregate shares: a named model, everything that is not a + * known model ("other"), or no filter at all. + * + * @return Builder + */ + private function filtered(?string $model): Builder + { + return AiModelDailyUsage::query() + ->when($model === self::OTHER_MODEL, fn (Builder $query) => $query->whereNull('ai_model_id')) + ->when($model !== null && $model !== self::OTHER_MODEL, fn (Builder $query) => $query->where('model', $model)); + } + /** * @template TValue * diff --git a/app/Actions/ViewDataAction.php b/app/Actions/ViewDataAction.php index 1b01c91..c30b2d6 100644 --- a/app/Actions/ViewDataAction.php +++ b/app/Actions/ViewDataAction.php @@ -10,6 +10,7 @@ use App\Enums\ContactSectionEnum; use App\Models\AiModel; use App\Models\Contact; +use App\Models\Network; use App\Models\News; use App\Models\OpenSource; use App\Models\Product; @@ -125,7 +126,13 @@ public function openSource(string $locale): Collection }); } - public function contacts(string $locale): \stdClass + /** + * The published team, grouped by the section each person appears in. Every section + * is present, empty ones included, so a caller never has to guard the lookup. + * + * @return Collection> + */ + public function contacts(string $locale): Collection { $key = CacheKeyEnum::CONTACTS_PUBLISHED->forLocale($locale); @@ -138,21 +145,39 @@ public function contacts(string $locale): \stdClass ->orderBy('name') ->get(); - return (object) collect([ - ContactSectionEnum::EMPLOYEES, - ContactSectionEnum::COLLABORATIONS, - ContactSectionEnum::BOARD_MEMBERS, - ])->mapWithKeys(function (string $section) use ($publishedContacts, $locale): array { - $contacts = $publishedContacts - ->filter(function (Contact $contact) use ($section): bool { - $sections = $contact->sections ?? []; - - return array_key_exists($section, $sections); - }) - ->map(fn (Contact $contact) => ContactDTO::fromModel($contact, $section, $locale)); - - return [$section => $contacts->values()]; - })->all(); + return collect(ContactSectionEnum::cases()) + ->mapWithKeys(fn (ContactSectionEnum $section): array => [ + $section->value => $publishedContacts + ->filter(fn (Contact $contact): bool => array_key_exists($section->value, $contact->sections ?? [])) + ->map(fn (Contact $contact): ContactDTO => ContactDTO::fromModel($contact, $section, $locale)) + ->values(), + ]); + }); + } + + /** + * @return Collection + */ + public function contactsInSection(string $locale, ContactSectionEnum $section): Collection + { + /** @var Collection $contacts */ + $contacts = $this->contacts($locale)->get($section->value, new Collection); + + return $contacts; + } + + /** + * @return Collection + */ + public function networks(): Collection + { + return Cache::rememberForever(CacheKeyEnum::NETWORKS_PUBLISHED->value, function () { + return Network::query() + ->published() + ->active() + ->with('publishedUsers') + ->orderBy('sort') + ->get(); }); } } diff --git a/app/Checks/FilesystemsDefaultCheck.php b/app/Checks/FilesystemsDefaultCheck.php deleted file mode 100644 index 02c5cb8..0000000 --- a/app/Checks/FilesystemsDefaultCheck.php +++ /dev/null @@ -1,33 +0,0 @@ -shortSummary("invalid filesystems default: {$filesystemsDefault}"); - - if ($defaultDisk === $fallbackDisk) { - return $result->failed(); - } - - return $result->ok(); - } -} diff --git a/app/Console/Commands/ImportCommand.php b/app/Console/Commands/ImportCommand.php index 12df437..e364f94 100644 --- a/app/Console/Commands/ImportCommand.php +++ b/app/Console/Commands/ImportCommand.php @@ -4,9 +4,12 @@ namespace App\Console\Commands; +use App\Support\ResponseCacheFlusher; use Illuminate\Console\Command; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Yaml; @@ -24,6 +27,11 @@ abstract class ImportCommand extends Command /** Directory under database/ this importer reads when --path is not given. */ abstract protected function defaultPath(): string; + protected function execute(InputInterface $input, OutputInterface $output): int + { + return ResponseCacheFlusher::batch(fn (): int => parent::execute($input, $output)); + } + protected function basePath(): string { $override = $this->option('path'); diff --git a/app/Console/Commands/ImportNewsCommand.php b/app/Console/Commands/ImportNewsCommand.php index 601971e..070eff7 100644 --- a/app/Console/Commands/ImportNewsCommand.php +++ b/app/Console/Commands/ImportNewsCommand.php @@ -27,6 +27,9 @@ class ImportNewsCommand extends ImportCommand protected $description = 'Import news articles from database/files/news/{locale}/*.md'; + /** @var array|null */ + private ?array $contactIds = null; + public function handle(NewsMarkdown $markdown): int { // PARSE_DATETIME: without it a bare `published_at: 2026-07-28` arrives as a Unix timestamp. @@ -42,13 +45,13 @@ public function handle(NewsMarkdown $markdown): int } $dryRun = $this->isDryRun(); - $only = $this->option('key'); + $only = $this->nullableString($this->option('key')); $imported = 0; $skipped = 0; foreach ($documents as $key => $localeDocuments) { - if (is_string($only) && $only !== '' && $only !== $key) { + if ($only !== null && $only !== $key) { continue; } @@ -81,6 +84,12 @@ public function handle(NewsMarkdown $markdown): int $imported++; } + // Skipped on a single-key run: that import sees one article and must never + // conclude the rest are orphans. + if (! $dryRun && $only === null) { + $this->removeOrphans(News::query(), 'key', array_keys($documents)); + } + if (! $dryRun && $imported > 0) { $this->linkRelatedArticles($documents); NewsObserver::flush(); @@ -106,7 +115,7 @@ protected function defaultPath(): string */ protected function checkFileName(string $path, string $key, array $front): void { - $publishedAt = $this->publishedAt($front['published_at'] ?? null); + $publishedAt = $this->date($front['published_at'] ?? null); if ($publishedAt === null) { return; @@ -150,7 +159,9 @@ private function store(string $key, array $localeDocuments, NewsMarkdown $markdo $news->fill([ ...$translated, 'hero_image' => $this->nullableString($primary['hero'] ?? null), - 'published_at' => $this->publishedAt($primary['published_at'] ?? null), + 'thumb_image' => $this->nullableString($primary['thumb'] ?? null), + 'published_at' => $this->date($primary['published_at'] ?? null), + 'revised_at' => $this->date($primary['updated_at'] ?? null), // Defaults to true: an article with a date is live unless it says otherwise. 'published' => (bool) ($primary['published'] ?? true), 'author' => $this->nullableString($primary['author_name'] ?? null), @@ -189,7 +200,7 @@ private function existing(string $key, array $slugs): ?News })->first(); } - private function publishedAt(mixed $value): ?Carbon + private function date(mixed $value): ?Carbon { if ($value === null || $value === '') { return null; @@ -225,12 +236,7 @@ private function resolveContactId(mixed $value): ?int $needle = mb_strtolower($this->string($value)); - $match = Contact::all()->first(function (Contact $contact) use ($needle): bool { - $icons = $contact->icons; - $email = is_array($icons) && isset($icons['email']) ? $this->string($icons['email']) : ''; - - return mb_strtolower($email) === $needle || mb_strtolower($contact->name) === $needle; - }); + $match = $this->contactIdsByIdentifier()[$needle] ?? null; if ($match === null) { $this->components->warn(sprintf( @@ -241,7 +247,34 @@ private function resolveContactId(mixed $value): ?int return null; } - return $match->id; + return $match; + } + + /** + * Every contact addressable by name or by the email in its icons, resolved once per + * run rather than once per article. + * + * @return array + */ + private function contactIdsByIdentifier(): array + { + if ($this->contactIds !== null) { + return $this->contactIds; + } + + $identifiers = []; + + foreach (Contact::all() as $contact) { + $email = $this->string(data_get($contact->icons, 'email')); + + if ($email !== '') { + $identifiers[mb_strtolower($email)] = $contact->id; + } + + $identifiers[mb_strtolower($contact->name)] = $contact->id; + } + + return $this->contactIds = $identifiers; } /** diff --git a/app/Console/Commands/SyncRepositoriesCommand.php b/app/Console/Commands/SyncRepositoriesCommand.php index 2c746f4..228564c 100644 --- a/app/Console/Commands/SyncRepositoriesCommand.php +++ b/app/Console/Commands/SyncRepositoriesCommand.php @@ -6,6 +6,7 @@ use App\Models\OpenSource; use Illuminate\Console\Command; +use Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; @@ -65,7 +66,8 @@ private function fetchAllRepositories(): ?array } do { - $response = Http::withHeaders($headers) + $response = $this->client() + ->withHeaders($headers) ->accept('application/vnd.github+json') ->get(sprintf('https://api.github.com/orgs/%s/repos', self::ORG), [ 'type' => 'public', @@ -181,9 +183,21 @@ private function syncRepository(array $repo): void $this->line(sprintf(' %s %s downloads', $entry->title, number_format($downloads))); } + /** + * A sync that walks every repository makes one call per repository, so a slow + * endpoint must fail fast rather than hold the command open on the default timeout. + */ + private function client(): PendingRequest + { + return Http::connectTimeout(5) + ->timeout(20) + ->retry(times: 2, sleepMilliseconds: 500, throw: false); + } + private function fetchPackagistDownloads(string $fullName): int { - $response = Http::accept('application/json') + $response = $this->client() + ->accept('application/json') ->get(sprintf('https://packagist.org/packages/%s.json', $fullName)); if ($response->failed()) { diff --git a/app/DTO/ContactDTO.php b/app/DTO/ContactDTO.php index 994fabd..9eb3f94 100644 --- a/app/DTO/ContactDTO.php +++ b/app/DTO/ContactDTO.php @@ -4,6 +4,7 @@ namespace App\DTO; +use App\Enums\ContactSectionEnum; use App\Models\Contact; use Illuminate\Support\Arr; @@ -14,18 +15,20 @@ class ContactDTO */ public function __construct( public readonly string $locale, - public readonly string $section, + public readonly ContactSectionEnum $section, + public readonly string $key, public readonly string $name, public readonly ?string $role, public readonly string $image, public readonly array $icons, ) {} - public static function fromModel(Contact $contact, string $section, string $locale): self + public static function fromModel(Contact $contact, ContactSectionEnum $section, string $locale): self { - $role = Arr::get($contact->sections ?? [], "$section.role.$locale"); + $role = Arr::get($contact->sections ?? [], "{$section->value}.role.{$locale}"); return new self( + key: $contact->key, name: $contact->name, role: is_string($role) ? $role : null, locale: $locale, diff --git a/app/DTO/LlmUsageFilterDTO.php b/app/DTO/LlmUsageFilterDTO.php new file mode 100644 index 0000000..9cee549 --- /dev/null +++ b/app/DTO/LlmUsageFilterDTO.php @@ -0,0 +1,89 @@ + $years + * @param Collection $monthOptions + * @param Collection $models + */ + public static function resolve( + Request $request, + Collection $years, + Collection $monthOptions, + Collection $models, + string $otherLabel, + bool $hasOtherModels, + ): self { + return new self( + year: $years->first(fn (string $option): bool => $option === $request->query('year')), + month: self::month($monthOptions, (string) $request->query('month')), + model: self::model($models, $otherLabel, (string) $request->query('model'), $hasOtherModels), + ); + } + + /** + * Accepts both the URL form ("05") and the localized label ("Mai"), so a month can + * be linked to either way. + * + * @param Collection $monthOptions + */ + private static function month(Collection $monthOptions, string $input): ?string + { + if ($monthOptions->has($input)) { + return $input; + } + + $key = $monthOptions->search(fn (string $label): bool => strcasecmp($label, $input) === 0); + + return $key === false ? null : $key; + } + + /** + * @param Collection $models + */ + private static function model(Collection $models, string $otherLabel, string $input, bool $hasOther): ?string + { + if ($hasOther && (strcasecmp($input, $otherLabel) === 0 || strcasecmp($input, LlmUsageStatsAction::OTHER_MODEL) === 0)) { + return LlmUsageStatsAction::OTHER_MODEL; + } + + return $models->first(fn (string $option): bool => strcasecmp($option, $input) === 0); + } + + /** + * @return array + */ + public function toQuery(): array + { + return array_filter([ + 'year' => $this->year, + 'month' => $this->month, + 'model' => $this->model, + ], fn (?string $value): bool => $value !== null); + } + + public function modelLabel(string $otherLabel): ?string + { + return $this->model === LlmUsageStatsAction::OTHER_MODEL ? $otherLabel : $this->model; + } +} diff --git a/app/Enums/CacheKeyEnum.php b/app/Enums/CacheKeyEnum.php index ac9ce20..9145962 100644 --- a/app/Enums/CacheKeyEnum.php +++ b/app/Enums/CacheKeyEnum.php @@ -19,8 +19,6 @@ */ enum CacheKeyEnum: string { - const string VALID_FILESYSTEMS_DEFAULT = 'valid_filesystems_defaullt'; - case CONTACTS_PUBLISHED = 'contacts_published'; case NEWS_PUBLISHED = 'news_published'; @@ -33,6 +31,8 @@ enum CacheKeyEnum: string case OPEN_SOURCE_PUBLISHED = 'open_source_published'; + case NETWORKS_PUBLISHED = 'networks_published'; + case AI_MODELS_ACTIVE = 'ai_models_active'; case AI_MODELS_ARCHIVED = 'ai_models_archived'; diff --git a/app/Enums/ContactSectionEnum.php b/app/Enums/ContactSectionEnum.php index 116d1fb..c43606e 100644 --- a/app/Enums/ContactSectionEnum.php +++ b/app/Enums/ContactSectionEnum.php @@ -6,9 +6,7 @@ enum ContactSectionEnum: string { - const string EMPLOYEES = 'employees'; - - const string COLLABORATIONS = 'collaborations'; - - const string BOARD_MEMBERS = 'board_members'; + case EMPLOYEES = 'employees'; + case COLLABORATIONS = 'collaborations'; + case BOARD_MEMBERS = 'board_members'; } diff --git a/app/Helpers/Facades/HelperBank.php b/app/Helpers/Facades/HelperBank.php deleted file mode 100644 index b9cfa7f..0000000 --- a/app/Helpers/Facades/HelperBank.php +++ /dev/null @@ -1,20 +0,0 @@ -format('Ymd_').$name; - } - - public function nameWithDateTime(string $name): string - { - return now()->format('YmdHm_').$name; - } -} diff --git a/app/Helpers/HelperMarkdown.php b/app/Helpers/HelperMarkdown.php deleted file mode 100644 index 3521fef..0000000 --- a/app/Helpers/HelperMarkdown.php +++ /dev/null @@ -1,15 +0,0 @@ -markdown()->toString(); - } -} diff --git a/app/Helpers/HelperMoney.php b/app/Helpers/HelperMoney.php deleted file mode 100755 index fc9af9b..0000000 --- a/app/Helpers/HelperMoney.php +++ /dev/null @@ -1,38 +0,0 @@ - substr($number, 0, 3).' (0) '.substr($number, 3, 2).' '.substr($number, 5, 3).' '.substr($number, 8, 2).' '.substr($number, 10, 2), - default => chunk_split($number, 3, ' '), - }; - } -} diff --git a/app/Http/Controllers/AboutUs/AboutUsIndexController.php b/app/Http/Controllers/AboutUs/AboutUsIndexController.php index 5f442e2..9df5950 100644 --- a/app/Http/Controllers/AboutUs/AboutUsIndexController.php +++ b/app/Http/Controllers/AboutUs/AboutUsIndexController.php @@ -12,7 +12,6 @@ use App\Seo\SchemaNodes; use Illuminate\Support\Collection; use Illuminate\View\View; -use stdClass; class AboutUsIndexController extends Controller { @@ -36,15 +35,13 @@ public function __invoke(ViewDataAction $viewData): View * Flattens the section-keyed contact groups into one list — the schema * cares about the people, not about which block they render in. * + * @param Collection> $contacts * @return Collection */ - private function flatten(stdClass $contacts): Collection + private function flatten(Collection $contacts): Collection { /** @var Collection $flattened */ - $flattened = collect((array) $contacts) - ->flatMap(fn (mixed $group): array => $group instanceof Collection ? $group->all() : []) - ->filter(fn (mixed $contact): bool => $contact instanceof ContactDTO) - ->values(); + $flattened = $contacts->flatMap(fn (Collection $group): array => $group->all())->values(); return $flattened; } diff --git a/app/Http/Controllers/Ai/AiIndexController.php b/app/Http/Controllers/Ai/AiIndexController.php index 94ce290..6e42721 100644 --- a/app/Http/Controllers/Ai/AiIndexController.php +++ b/app/Http/Controllers/Ai/AiIndexController.php @@ -6,16 +6,37 @@ use App\Actions\LlmUsageStatsAction; use App\Actions\PageAction; +use App\Actions\ViewDataAction; use App\Http\Controllers\Controller; +use App\Models\News; +use App\Models\NewsTag; use Illuminate\View\View; class AiIndexController extends Controller { - public function __invoke(LlmUsageStatsAction $stats): View + /** + * The topics this page collects its articles from. «KI» in the German files and + * «AI» in the English ones become two separate tags on import, so both keys count. + * + * @var list + */ + private const array TOPIC_KEYS = ['ki', 'ai']; + + public function __invoke(LlmUsageStatsAction $stats, ViewDataAction $viewData): View { + $locale = app()->getLocale(); + return view('app.ai.index')->with([ 'page' => (new PageAction(locale: null, routeName: 'ai.index'))->default(), 'llmSummary' => $stats->currentMonthSummary(), + 'hasUsage' => $stats->totalSummary()['requests'] > 0, + // Every article on the topic, not a teaser of the newest few: this is the + // page about it, and the news index stays one click away for the rest. + 'news' => $viewData->news($locale) + ->filter(fn (News $entry): bool => $entry->newsTags->contains( + fn (NewsTag $tag): bool => in_array($tag->key, self::TOPIC_KEYS, true), + )) + ->values(), ]); } } diff --git a/app/Http/Controllers/Ai/AiLlmAnalyticsIndexController.php b/app/Http/Controllers/Ai/AiLlmAnalyticsIndexController.php index ab73cf5..96d91ea 100644 --- a/app/Http/Controllers/Ai/AiLlmAnalyticsIndexController.php +++ b/app/Http/Controllers/Ai/AiLlmAnalyticsIndexController.php @@ -6,6 +6,7 @@ use App\Actions\LlmUsageStatsAction; use App\Actions\PageAction; +use App\DTO\LlmUsageFilterDTO; use App\Helpers\Facades\HelperDate; use App\Http\Controllers\Controller; use Illuminate\Http\Request; @@ -22,78 +23,78 @@ public function __invoke(Request $request, LlmUsageStatsAction $stats): View { $models = $stats->models(); $years = $stats->years(); - $monthOptions = collect(range(1, 12))->mapWithKeys(fn (int $month) => [ - str_pad((string) $month, 2, '0', STR_PAD_LEFT) => HelperDate::monthName($month), - ]); + $monthOptions = $this->monthOptions(); + $hasOtherModels = $stats->hasOtherModels(); $otherLabel = __('components.ai_llm_analytics.filter.other_models'); - $modelOptions = $stats->hasOtherModels() ? $models->concat([$otherLabel]) : $models; + $otherLabel = is_string($otherLabel) ? $otherLabel : LlmUsageStatsAction::OTHER_MODEL; - $year = $years->first(fn (string $option) => $option === $request->query('year')); - $month = $this->resolveMonth($monthOptions, (string) $request->query('month')); - $model = $this->resolveModel($models, $otherLabel, (string) $request->query('model'), $stats->hasOtherModels()); + $filter = LlmUsageFilterDTO::resolve($request, $years, $monthOptions, $models, $otherLabel, $hasOtherModels); - $breakdown = $stats->monthlyBreakdown($year, $month, $model)->reverse()->values(); - - $page = Paginator::resolveCurrentPage(); - - $periods = new LengthAwarePaginator( - items: $breakdown->forPage($page, self::PER_PAGE)->values(), - total: $breakdown->count(), - perPage: self::PER_PAGE, - currentPage: $page, - options: [ - 'path' => Paginator::resolveCurrentPath(), - 'query' => array_filter(['year' => $year, 'month' => $month, 'model' => $model]), - ], - ); + $breakdown = $stats->monthlyBreakdown($filter->year, $filter->month, $filter->model)->reverse()->values(); return view('app.ai.llm.analytics')->with([ 'page' => (new PageAction(locale: null, routeName: 'ai.llm.analytics.index'))->default(), - 'monthSummary' => $stats->currentMonthSummary($model), - 'yearSummary' => $stats->currentYearSummary($model), - 'totalSummary' => $stats->totalSummary($model), - 'periods' => $periods, - 'grandTotal' => [ - 'prompt_tokens' => $breakdown->sum(fn (array $row): int => $row['prompt_tokens']), - 'completion_tokens' => $breakdown->sum(fn (array $row): int => $row['completion_tokens']), - 'total_tokens' => $breakdown->sum(fn (array $row): int => $row['total_tokens']), - 'requests' => $breakdown->sum(fn (array $row): int => $row['requests']), - ], - 'modelOptions' => $modelOptions, + 'monthSummary' => $stats->currentMonthSummary($filter->model), + 'yearSummary' => $stats->currentYearSummary($filter->model), + 'totalSummary' => $stats->totalSummary($filter->model), + 'periods' => $this->paginate($breakdown, $filter), + 'grandTotal' => $this->grandTotal($breakdown), + 'modelOptions' => $hasOtherModels ? $models->concat([$otherLabel]) : $models, 'years' => $years, 'monthOptions' => $monthOptions, - 'year' => $year, - 'month' => $month, - 'model' => $model, - 'modelLabel' => $model === LlmUsageStatsAction::OTHER_MODEL ? $otherLabel : $model, + 'year' => $filter->year, + 'month' => $filter->month, + 'model' => $filter->model, + 'modelLabel' => $filter->modelLabel($otherLabel), 'lastSyncedAt' => $stats->lastSyncedAt(), ]); } /** - * @param Collection $models + * @return Collection */ - private function resolveModel(Collection $models, string $otherLabel, string $input, bool $hasOther): ?string + private function monthOptions(): Collection { - if ($hasOther && (strcasecmp($input, $otherLabel) === 0 || strcasecmp($input, LlmUsageStatsAction::OTHER_MODEL) === 0)) { - return LlmUsageStatsAction::OTHER_MODEL; - } - - return $models->first(fn (string $option) => strcasecmp($option, $input) === 0); + return collect(range(1, 12))->mapWithKeys(fn (int $month): array => [ + str_pad((string) $month, 2, '0', STR_PAD_LEFT) => HelperDate::monthName($month), + ]); } /** - * @param Collection $monthOptions + * The breakdown is already one row per month, so it is paginated in memory rather + * than re-queried per page. + * + * @param Collection $breakdown + * @return LengthAwarePaginator */ - private function resolveMonth(Collection $monthOptions, string $input): ?string + private function paginate(Collection $breakdown, LlmUsageFilterDTO $filter): LengthAwarePaginator { - if ($monthOptions->has($input)) { - return $input; - } + $page = Paginator::resolveCurrentPage(); - $key = $monthOptions->search(fn (string $label) => strcasecmp($label, $input) === 0); + return new LengthAwarePaginator( + items: $breakdown->forPage($page, self::PER_PAGE)->values(), + total: $breakdown->count(), + perPage: self::PER_PAGE, + currentPage: $page, + options: [ + 'path' => Paginator::resolveCurrentPath(), + 'query' => $filter->toQuery(), + ], + ); + } - return $key === false ? null : $key; + /** + * @param Collection $breakdown + * @return array + */ + private function grandTotal(Collection $breakdown): array + { + return [ + 'prompt_tokens' => $breakdown->sum(fn (array $row): int => $row['prompt_tokens']), + 'completion_tokens' => $breakdown->sum(fn (array $row): int => $row['completion_tokens']), + 'total_tokens' => $breakdown->sum(fn (array $row): int => $row['total_tokens']), + 'requests' => $breakdown->sum(fn (array $row): int => $row['requests']), + ]; } } diff --git a/app/Http/Controllers/Ai/AiLlmIndexController.php b/app/Http/Controllers/Ai/AiLlmIndexController.php index b6df9bd..6c5ad76 100644 --- a/app/Http/Controllers/Ai/AiLlmIndexController.php +++ b/app/Http/Controllers/Ai/AiLlmIndexController.php @@ -19,6 +19,7 @@ public function __invoke(LlmUsageStatsAction $stats, ViewDataAction $viewData): 'groups' => $viewData->aiModelGroups(), 'archive' => $viewData->aiModelArchive(), 'llmSummary' => $stats->currentMonthSummary(), + 'hasUsage' => $stats->totalSummary()['requests'] > 0, ]); } } diff --git a/app/Http/Controllers/CoWorking/CoWorkingIndexController.php b/app/Http/Controllers/CoWorking/CoWorkingIndexController.php index 8c7ba5a..af45cc1 100644 --- a/app/Http/Controllers/CoWorking/CoWorkingIndexController.php +++ b/app/Http/Controllers/CoWorking/CoWorkingIndexController.php @@ -4,37 +4,14 @@ namespace App\Http\Controllers\CoWorking; -use App\Actions\PageAction; use App\Http\Controllers\Controller; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Str; -use Illuminate\View\View; class CoWorkingIndexController extends Controller { - public function __invoke(): View|RedirectResponse + public function __invoke(): RedirectResponse { return redirect()->route(Str::slug(app()->getLocale()).'.start.index'); - - /* return view('app.co-working.index')->with([ - 'page' => (new PageAction(locale: null, routeName: 'co-working.index'))->default(), - 'services' => $this->services(), - 'pricing' => [ - 'name' => __('Single workstation'), - 'price_chf' => 750, - 'period' => __('month'), - ], - 'optionalServices' => [ - ['name' => __('Monitor & video camera'), 'price' => __('On request')], - ['name' => __('Parking with EV charging'), 'price' => __('from CHF 250.00 / month')], - ['name' => __('Fibre upgrade up to 5 Gbit/s'), 'price' => __('On request')], - ['name' => __('Static public IP'), 'price' => __('On request')], - ], - 'rentalConditions' => [ - 'minimum_months' => 12, - 'notice_months' => 3, - 'deposit_text' => __('3 months\' rent per workstation'), - ], - ]);*/ } } diff --git a/app/Http/Controllers/Contact/ContactIndexController.php b/app/Http/Controllers/Contact/ContactIndexController.php index 5538d0d..aae301d 100644 --- a/app/Http/Controllers/Contact/ContactIndexController.php +++ b/app/Http/Controllers/Contact/ContactIndexController.php @@ -5,19 +5,36 @@ namespace App\Http\Controllers\Contact; use App\Actions\PageAction; +use App\Actions\ViewDataAction; +use App\DTO\ContactDTO; +use App\Enums\ContactSectionEnum; use App\Http\Controllers\Controller; use App\Seo\SchemaNodes; use Illuminate\View\View; class ContactIndexController extends Controller { - public function __invoke(): View + public function __invoke(ViewDataAction $viewData): View { return view('app.contact.index')->with([ 'page' => (new PageAction(locale: null, routeName: 'contact.index'))->default(), 'openingHours' => config('company.opening_hours'), 'locations' => config('company.locations'), + 'contactPerson' => $this->contactPerson($viewData), 'schema' => SchemaNodes::locations(), ]); } + + private function contactPerson(ViewDataAction $viewData): ?ContactDTO + { + $key = config('company.contact_person'); + + if (! is_string($key) || $key === '') { + return null; + } + + return $viewData + ->contactsInSection(app()->getLocale(), ContactSectionEnum::EMPLOYEES) + ->firstWhere('key', $key); + } } diff --git a/app/Http/Controllers/Demo/FlowsLayoutDemoController.php b/app/Http/Controllers/Demo/FlowsLayoutDemoController.php deleted file mode 100644 index 7b83abd..0000000 --- a/app/Http/Controllers/Demo/FlowsLayoutDemoController.php +++ /dev/null @@ -1,221 +0,0 @@ - - */ - public static function variants(): array - { - return [ - 'saas-landing' => [ - 'view' => 'demo.flows.variants.saas-landing', - 'title' => 'Klassische SaaS-Landingpage', - 'description' => 'Grosser Gradient-Hero, Feature-Grid, sticky CTA-Leiste — vertraute Marketing-Rhythmik.', - ], - 'editorial' => [ - 'view' => 'demo.flows.variants.editorial', - 'title' => 'Editorial / Long-Form', - 'description' => 'Magazin-artige einspaltige Leseerfahrung mit Initiale, Zwischentiteln und viel Weissraum.', - ], - 'bento' => [ - 'view' => 'demo.flows.variants.bento', - 'title' => 'Bento-Grid', - 'description' => 'Asymmetrisches Kachelraster im Stil moderner Produktseiten (Apple/Linear).', - ], - 'terminal' => [ - 'view' => 'demo.flows.variants.terminal', - 'title' => 'Terminal / Dev-Konsole', - 'description' => 'Dunkles, monospace-geprägtes Layout mit Terminal-Fenster-Chrome — technische Zielgruppe.', - ], - 'big-statement' => [ - 'view' => 'demo.flows.variants.big-statement', - 'title' => 'Grosse Aussagen / Scroll-Story', - 'description' => 'Überdimensionierte Typografie, eine Kernaussage pro Abschnitt, filmisches Scrollen.', - ], - 'docs-split' => [ - 'view' => 'demo.flows.variants.docs-split', - 'title' => 'Split Sticky Docs', - 'description' => 'Linke fixierte Mini-Navigation, rechts scrollender Inhalt — wie moderne Doku-Seiten.', - ], - 'journey' => [ - 'view' => 'demo.flows.variants.journey', - 'title' => 'Nummerierte Reise', - 'description' => 'Alles als durchnummerierte Schritte mit verbindender Zeitlinie erzählt.', - ], - 'before-after' => [ - 'view' => 'demo.flows.variants.before-after', - 'title' => 'Vorher / Nachher', - 'description' => 'Kontrastreiche Zweispalten-Gegenüberstellung als Erzählprinzip.', - ], - 'swiss-grid' => [ - 'view' => 'demo.flows.variants.swiss-grid', - 'title' => 'Swiss Minimalist Grid', - 'description' => 'Strenges Raster, feine Linien, Kapitälchen-Labels — reduziert und hochwertig.', - ], - 'tabs-dashboard' => [ - 'view' => 'demo.flows.variants.tabs-dashboard', - 'title' => 'Interaktives Tab-Dashboard', - 'description' => 'Abschnitte als Tabs/Accordion erkundbar — produkttour-artiges Gefühl.', - ], - ]; - } - - /** - * @return array - */ - public static function content(): array - { - return [ - 'headline' => 'Dokumentenprozesse, gesteuert von Agenten. Auf Infrastruktur, die du kontrollierst.', - 'subheadline' => 'Flows ist eine Orchestrierungsplattform für dokumentenbasierte Prozesse — verbinde dein DMS und lass KI-Agenten Dokumente automatisch extrahieren, validieren und verarbeiten.', - 'problem' => [ - 'heading' => 'Extraktionstools lesen Dokumente. Sie verstehen dein Geschäft nicht.', - 'intro' => 'Zwei Probleme halten Dokumentenprozesse manuell.', - 'paragraphs' => [ - 'Das erste ist der Aufwand: Dein DMS speichert Dokumente zuverlässig, aber die Arbeit drumherum – Lesen, Indexieren, Validieren, Übertragen von Daten in andere Systeme – hängt weiterhin von Personen ab, die Dateien öffnen und Daten eintippen. Das skaliert nur durch zusätzliches Personal.', - 'Das zweite Problem liegt tiefer: Klassische Dokumentenextraktion liefert Felder, nicht mehr. Sie versteht den Inhalt nicht und kann das Ergebnis nicht gegen deine eigenen Daten prüfen – existiert dieser Lieferant, stimmt der Betrag mit der Bestellung überein, ist das die richtige Kostenstelle? Selbst mit einem Extraktionstool muss also weiterhin eine Person jedes Ergebnis prüfen, bevor es vertrauenswürdig ist.', - ], - ], - 'features' => [ - 'heading' => 'Orchestrierte Agenten-Workflows – extrahieren, verstehen, validieren, zurückschreiben.', - 'intro' => 'Flows verbindet sich mit deinem bestehenden DMS. Keine Migration; dein Archiv bleibt, wo es ist. Darauf konfigurierst du Workflows, die auslösen, sobald ein Dokument eintrifft.', - 'items' => [ - ['title' => 'Agentische Extraktion mit Validierung', 'description' => 'Definiere das Schema, das du brauchst. Agenten extrahieren die Daten, validieren sie gegen deine eigenen Systeme und Daten und schreiben geprüfte Ergebnisse zurück – strukturiert und passend zu deinen Feldnamen.'], - ['title' => 'Multi-Agenten-Workflow-Orchestrierung', 'description' => 'Für komplexe Prozesse verkettest du spezialisierte Agenten – Klassifizierung, Validierung, Anreicherung, Routing. Die Orchestrierung basiert auf dem Microsoft Agent Framework, und MCP-Tool-Integrationen verbinden deine Workflows direkt mit deinen Geschäftssystemen und Daten.'], - ['title' => 'Prompt- und Agenten-Engineering', 'description' => 'Verbessere Prompts anhand echter Produktionsdaten, generiere sie aus dem Wissen in deinem Datenpool oder DMS und verwalte Prompts und Schemas in einer versionierten, wiederverwendbaren Knowledge-Bibliothek.'], - ['title' => 'Modell- und Anbieterfreiheit', 'description' => 'Führe Workflows bei dem Anbieter aus, der zu deinen Anforderungen an Genauigkeit, Kosten und Datenresidenz passt – und vergleiche denselben Workflow über mehrere Modelle hinweg, um zu sehen, welches bei deinen Dokumenten am besten abschneidet. Ein Wechsel bedeutet keinen Neuaufbau. Eigene lokale Modelle (Bring-your-own) folgen als Nächstes.'], - ['title' => 'Vollständige Nachvollziehbarkeit', 'description' => 'Jeder Lauf wird lückenlos protokolliert: Dokument, Modell, Tokens, Dauer, Ergebnis.'], - ], - ], - 'deployment' => [ - 'heading' => 'Drei Wege, Flows zu betreiben. Alle isoliert.', - 'intro' => 'Jede Bereitstellung erhält ihre eigene Datenbank, Schlüsselverwaltung, Speicher und Endpunkte – automatisch bereitgestellt, mit Firewalling und automatischer Rotation der Secrets. Du entscheidest, wo sie läuft, und wählst bei jeder Option die Azure-Region deiner Bereitstellung, sofern diese die benötigten Dienste unterstützt.', - 'options' => [ - ['title' => 'Auf deiner eigenen Azure-Infrastruktur', 'description' => 'Flows wird in deine bestehende Azure-Umgebung integriert. Dein Abonnement, deine Governance, deine Kontrolle.'], - ['title' => 'Dediziertes Azure-Abonnement', 'description' => 'Ein vollständig isoliertes Abonnement, das für dich betrieben wird – maximale Trennung, ohne dass du es selbst betreiben musst.'], - ['title' => 'Gemeinsames Abonnement, dedizierte Ressourcengruppe', 'description' => 'Deine eigene Ressourcengruppe und Bereitstellungen innerhalb eines gemeinsamen Abonnements – isolierte Ressourcen bei geringerem Fussabdruck.'], - ], - ], - 'cta' => [ - 'heading' => 'Interessiert?', - 'body' => 'Wir zeigen dir Flows anhand deiner eigenen Dokumente.', - 'buttonLabel' => 'Kontaktiere uns', - ], - ]; - } - - public function index(): View - { - return view('demo.flows.index', [ - 'variants' => self::variants(), - ]); - } - - public function show(string $variant): View - { - if (! array_key_exists($variant, self::variants())) { - throw new NotFoundHttpException; - } - - return view(self::variants()[$variant]['view'], [ - 'content' => self::content(), - 'variantTitle' => self::variants()[$variant]['title'], - ]); - } - - /** - * @return array - */ - public static function v2Variants(): array - { - return [ - 'flow-diagram' => [ - 'view' => 'demo.flows.v2.variants.flow-diagram', - 'title' => 'Fluss-Diagramm', - 'description' => 'Dokument → Agent → geprüftes Ergebnis als technisches Node-Diagramm im Hero.', - ], - 'line-icons' => [ - 'view' => 'demo.flows.v2.variants.line-icons', - 'title' => 'Line-Icon-Set', - 'description' => 'Ein eigenes, konsistentes Strich-Icon für jedes Feature und jede Deployment-Option.', - ], - 'blueprint' => [ - 'view' => 'demo.flows.v2.variants.blueprint', - 'title' => 'Blueprint', - 'description' => 'Millimeterpapier-Textur, Massketten und Eckmarken — technische Zeichnung.', - ], - 'isometric' => [ - 'view' => 'demo.flows.v2.variants.isometric', - 'title' => 'Isometrisch', - 'description' => 'Isometrische Dokumentenstapel- und Server-Illustrationen für Hero & Deployment.', - ], - 'organic-blobs' => [ - 'view' => 'demo.flows.v2.variants.organic-blobs', - 'title' => 'Organische Blobs', - 'description' => 'Weiche Verlaufsflächen in Markenfarben hinter den nummerierten Sektionen.', - ], - 'dot-halftone' => [ - 'view' => 'demo.flows.v2.variants.dot-halftone', - 'title' => 'Halbton-Raster', - 'description' => 'Punktraster-Illustration im Hero, Plakat-artige Druck-Ästhetik.', - ], - 'network-nodes' => [ - 'view' => 'demo.flows.v2.variants.network-nodes', - 'title' => 'Netzwerk-Knoten', - 'description' => 'Verbundene Knoten als Sinnbild für Multi-Agenten-Orchestrierung.', - ], - 'stamp-seal' => [ - 'view' => 'demo.flows.v2.variants.stamp-seal', - 'title' => 'Stempel/Siegel', - 'description' => 'Kreisrunde Stempelmarken mit perforiertem Rand markieren jede Sektion.', - ], - 'data-bars' => [ - 'view' => 'demo.flows.v2.variants.data-bars', - 'title' => 'Daten-Balken', - 'description' => 'Abstrakte Balken-/Wellenformen als Sinnbild für Dokumentenverarbeitung.', - ], - 'doc-stack' => [ - 'view' => 'demo.flows.v2.variants.doc-stack', - 'title' => 'Dokumentenstapel', - 'description' => 'Stilisierte Dokumente mit Eselsohr als durchgehendes Bildmotiv.', - ], - ]; - } - - public function indexV2(): View - { - return view('demo.flows.v2.index', [ - 'variants' => self::v2Variants(), - ]); - } - - public function showV2(string $variant): View - { - if (! array_key_exists($variant, self::v2Variants())) { - throw new NotFoundHttpException; - } - - $product = Product::where('locale', 'de_CH')->where('slug', 'flows')->first(); - - $page = $product - ? (new PageAction)->product(product: $product) - : (new PageAction(locale: null, routeName: 'products.index'))->default(); - - return view(self::v2Variants()[$variant]['view'], [ - 'content' => self::content(), - 'variantTitle' => self::v2Variants()[$variant]['title'], - 'page' => $page, - ]); - } -} diff --git a/app/Http/Controllers/Network/NetworkIndexController.php b/app/Http/Controllers/Network/NetworkIndexController.php index 6e08dd0..7401b5d 100644 --- a/app/Http/Controllers/Network/NetworkIndexController.php +++ b/app/Http/Controllers/Network/NetworkIndexController.php @@ -5,22 +5,17 @@ namespace App\Http\Controllers\Network; use App\Actions\PageAction; +use App\Actions\ViewDataAction; use App\Enums\NetworkCategoryEnum; use App\Http\Controllers\Controller; -use App\Models\Network; use Illuminate\Support\Collection; use Illuminate\View\View; class NetworkIndexController extends Controller { - public function __invoke(): View + public function __invoke(ViewDataAction $viewData): View { - $networks = Network::query() - ->published() - ->active() - ->with('publishedUsers') - ->orderBy('sort') - ->get(); + $networks = $viewData->networks(); $groups = collect(NetworkCategoryEnum::cases()) ->mapWithKeys(fn (NetworkCategoryEnum $category): array => [ diff --git a/app/Http/Controllers/News/NewsShowController.php b/app/Http/Controllers/News/NewsShowController.php index fbaa5f0..5ee7d22 100644 --- a/app/Http/Controllers/News/NewsShowController.php +++ b/app/Http/Controllers/News/NewsShowController.php @@ -6,8 +6,10 @@ use App\Actions\PageAction; use App\DTO\ContactDTO; +use App\Enums\ContactSectionEnum; use App\Http\Controllers\Controller; use App\Markdown\NewsMarkdown; +use App\Models\Contact; use App\Models\News; use App\Seo\SchemaNodes; use Illuminate\Database\Eloquent\Collection; @@ -15,7 +17,7 @@ class NewsShowController extends Controller { - private const int RELATED_ARTICLES = 3; + private const int RELATED_ARTICLES = 2; private const int RELATED_CANDIDATE_POOL = 30; @@ -31,7 +33,11 @@ public function __invoke(string $locale, News $news, NewsMarkdown $markdown): Vi $page = (new PageAction($locale))->news(news: $news, withReferences: true); $body = is_string($news->content) ? $news->content : ''; - $author = $news->authorContact; + + $authors = collect([$news->authorContact]) + ->filter() + ->map(fn (Contact $contact): ContactDTO => ContactDTO::fromModel($contact, ContactSectionEnum::EMPLOYEES, $locale)) + ->values(); return view('app.news.show')->with([ 'page' => $page, @@ -41,10 +47,7 @@ public function __invoke(string $locale, News $news, NewsMarkdown $markdown): Vi 'tags' => collect($news->tags), 'content' => $markdown->toHtml($body), 'headings' => $markdown->headings($body), - 'authorName' => $news->authorName(), - 'authorRole' => $author !== null ? ContactDTO::fromModel($author, 'employees', $locale)->role : null, - 'authorImage' => $author?->image, - 'authorLinkedin' => $author !== null && is_array($author->icons) ? ($author->icons['linkedin'] ?? null) : null, + 'authors' => $authors, 'series' => $news->series, 'seriesParts' => $news->seriesParts(), 'related' => $this->relatedArticles($news), @@ -60,7 +63,12 @@ public function __invoke(string $locale, News $news, NewsMarkdown $markdown): Vi */ private function relatedArticles(News $news): Collection { - $curated = $news->relatedArticles()->published()->get(); + // The footer rows are the same rows as the index and the start page — series + // chip, author picture and all — so they need the same relations loaded. + // Model::shouldBeStrict() turns a lazy load into an exception locally. + $related = ['series', 'authorContact']; + + $curated = $news->relatedArticles()->with($related)->published()->get(); if ($curated->count() >= self::RELATED_ARTICLES) { return $curated->take(self::RELATED_ARTICLES); @@ -69,10 +77,11 @@ private function relatedArticles(News $news): Collection $tags = $this->tagStrings($news); $byTag = News::query() + ->with($related) ->published() // Only the card fields — `content` holds the full article body in both // languages, and scoring by tag overlap never looks at it. - ->select(['id', 'key', 'slug', 'title', 'teaser', 'hero_image', 'published_at', 'reading_minutes', 'tags', 'series_id', 'contact_id']) + ->select(['id', 'key', 'slug', 'title', 'teaser', 'hero_image', 'thumb_image', 'published_at', 'reading_minutes', 'tags', 'series_id', 'contact_id']) ->whereKeyNot($news->getKey()) ->whereNotIn('id', $curated->modelKeys()) ->when($news->series_id !== null, fn ($query) => $query->where(function ($inner) use ($news) { diff --git a/app/Http/Controllers/OpenSource/OpenSoruceShowController.php b/app/Http/Controllers/OpenSource/OpenSoruceShowController.php deleted file mode 100644 index 43f6900..0000000 --- a/app/Http/Controllers/OpenSource/OpenSoruceShowController.php +++ /dev/null @@ -1,44 +0,0 @@ -route(Str::slug(app()->getLocale()).'.start.index'); - - /* // `sync:repositories` creates an entry per GitHub repository but only - // fills title and teaser — content is written by hand. Without it there - // is no page here worth having, let alone indexing, so serve a 404 - // rather than a near-empty URL. - if (! $openSource->hasWrittenContent()) { - throw new NotFoundHttpException; - } - - $page = (new PageAction(locale: $locale))->openSource(openSource: $openSource, withReferences: true); - - return view('app.open-source.show')->with([ - 'page' => $page, - 'name' => $openSource->title, - 'teaser' => $openSource->teaser, - 'content' => Str::of($openSource->content ?? '')->markdown(), - 'tags' => $openSource->tags, - 'link' => $openSource->link, - 'schema' => SchemaNodes::softwareSourceCode($openSource, $page, $locale), - ]);*/ - } -} diff --git a/app/Http/Controllers/OpenSource/OpenSourceIndexController.php b/app/Http/Controllers/OpenSource/OpenSourceIndexController.php index c74eb24..d201bf6 100644 --- a/app/Http/Controllers/OpenSource/OpenSourceIndexController.php +++ b/app/Http/Controllers/OpenSource/OpenSourceIndexController.php @@ -7,25 +7,16 @@ use App\Http\Controllers\Controller; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Str; -use Illuminate\View\View; class OpenSourceIndexController extends Controller { /** - * Disabled until the listing actually has entries. `sync:repositories` is - * not scheduled, so this page rendered its intro and nothing else — an - * empty URL that search engines read as thin content. Restore the body - * below once repositories are synced and written up. + * Disabled until the listing actually has entries. `sync:repositories` is not + * scheduled, so this page rendered its intro and nothing else — an empty URL that + * search engines read as thin content. */ - public function __invoke(): View|RedirectResponse + public function __invoke(): RedirectResponse { return redirect()->route(Str::slug(app()->getLocale()).'.start.index'); - - /* $locale = app()->getLocale(); - - return view('app.open-source.index')->with([ - 'page' => (new PageAction(locale: null, routeName: 'open-source.index'))->default(), - 'openSource' => (new ViewDataAction)->openSource($locale), - ]);*/ } } diff --git a/app/Http/Controllers/OpenSource/OpenSourceShowController.php b/app/Http/Controllers/OpenSource/OpenSourceShowController.php new file mode 100644 index 0000000..3aa14dc --- /dev/null +++ b/app/Http/Controllers/OpenSource/OpenSourceShowController.php @@ -0,0 +1,23 @@ +route(Str::slug(app()->getLocale()).'.start.index'); + } +} diff --git a/app/Http/Controllers/Products/ProductsIndexController.php b/app/Http/Controllers/Products/ProductsIndexController.php index 4c43b2f..355374d 100644 --- a/app/Http/Controllers/Products/ProductsIndexController.php +++ b/app/Http/Controllers/Products/ProductsIndexController.php @@ -4,24 +4,14 @@ namespace App\Http\Controllers\Products; -use App\Actions\PageAction; -use App\Actions\ViewDataAction; use App\Http\Controllers\Controller; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Str; -use Illuminate\View\View; class ProductsIndexController extends Controller { - public function __invoke(): View|RedirectResponse + public function __invoke(): RedirectResponse { return redirect()->route(Str::slug(app()->getLocale()).'.start.index'); - - /* $locale = app()->getLocale(); - - return view('app.products.index')->with([ - 'page' => (new PageAction(locale: null, routeName: 'products.index'))->default(), - 'products' => (new ViewDataAction)->products($locale), - ]);*/ } } diff --git a/app/Http/Controllers/Products/ProductsShowController.php b/app/Http/Controllers/Products/ProductsShowController.php index 4ed7b56..1438e22 100644 --- a/app/Http/Controllers/Products/ProductsShowController.php +++ b/app/Http/Controllers/Products/ProductsShowController.php @@ -4,34 +4,15 @@ namespace App\Http\Controllers\Products; -use App\Actions\PageAction; use App\Http\Controllers\Controller; use App\Models\Product; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Str; -use Illuminate\View\View; class ProductsShowController extends Controller { - public function __invoke(string $locale, Product $product): View|RedirectResponse + public function __invoke(string $locale, Product $product): RedirectResponse { return redirect()->route(Str::slug(app()->getLocale()).'.start.index'); - - /* return view('app.products.show')->with([ - 'page' => (new PageAction(locale: $locale))->product(product: $product), - 'name' => $product->name, - 'headline' => $product->headline, - 'teaser' => $product->teaser, - 'content' => Str::of($product->content ?? '')->markdown(), - 'tags' => $product->tags, - 'featuresHeading' => $product->features_heading, - 'featuresIntro' => $product->features_intro, - 'features' => $product->features, - 'deploymentHeading' => $product->deployment_heading, - 'deploymentIntro' => $product->deployment_intro, - 'deploymentOptions' => $product->deployment_options, - 'ctaHeading' => $product->cta_heading, - 'ctaBody' => $product->cta_body, - ]);*/ } } diff --git a/app/Http/Controllers/Services/ServicesShowController.php b/app/Http/Controllers/Services/ServicesShowController.php index 5223388..d4ee60c 100644 --- a/app/Http/Controllers/Services/ServicesShowController.php +++ b/app/Http/Controllers/Services/ServicesShowController.php @@ -4,25 +4,15 @@ namespace App\Http\Controllers\Services; -use App\Actions\PageAction; use App\Http\Controllers\Controller; use App\Models\Service; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Str; -use Illuminate\View\View; class ServicesShowController extends Controller { - public function __invoke(string $locale, Service $service): View|RedirectResponse + public function __invoke(string $locale, Service $service): RedirectResponse { return redirect()->route(Str::slug(app()->getLocale()).'.start.index'); - - /* return view('app.services.show')->with([ - 'page' => (new PageAction(locale: $locale, routeName: null))->service(service: $service, withReferences: true), - 'name' => $service->name, - 'teaser' => $service->teaser, - 'content' => Str::of($service->content ?? '')->markdown(), - 'tags' => $service->tags, - ]);*/ } } diff --git a/app/Http/Controllers/Technologies/TechnologiesIndexController.php b/app/Http/Controllers/Technologies/TechnologiesIndexController.php index 9efd78a..2c9782b 100644 --- a/app/Http/Controllers/Technologies/TechnologiesIndexController.php +++ b/app/Http/Controllers/Technologies/TechnologiesIndexController.php @@ -4,24 +4,14 @@ namespace App\Http\Controllers\Technologies; -use App\Actions\PageAction; -use App\Actions\ViewDataAction; use App\Http\Controllers\Controller; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Str; -use Illuminate\View\View; class TechnologiesIndexController extends Controller { - public function __invoke(): View|RedirectResponse + public function __invoke(): RedirectResponse { return redirect()->route(Str::slug(app()->getLocale()).'.start.index'); - - /* $locale = app()->getLocale(); - - return view('app.technologies.index')->with([ - 'page' => (new PageAction(locale: null, routeName: 'technologies.index'))->default(), - 'technologies' => (new ViewDataAction)->technologies($locale), - ]);*/ } } diff --git a/app/Http/Controllers/Technologies/TechnologiesShowController.php b/app/Http/Controllers/Technologies/TechnologiesShowController.php index 32f6c1a..4a0d250 100644 --- a/app/Http/Controllers/Technologies/TechnologiesShowController.php +++ b/app/Http/Controllers/Technologies/TechnologiesShowController.php @@ -4,25 +4,15 @@ namespace App\Http\Controllers\Technologies; -use App\Actions\PageAction; use App\Http\Controllers\Controller; use App\Models\Technology; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Str; -use Illuminate\View\View; class TechnologiesShowController extends Controller { - public function __invoke(string $locale, Technology $technology): View|RedirectResponse + public function __invoke(string $locale, Technology $technology): RedirectResponse { return redirect()->route(Str::slug(app()->getLocale()).'.start.index'); - - /* return view('app.technologies.show')->with([ - 'page' => (new PageAction(locale: $locale))->technology(technology: $technology), - 'name' => $technology->title, - 'teaser' => $technology->teaser, - 'content' => Str::of($technology->content ?? '')->markdown(), - 'tags' => $technology->tags, - ]);*/ } } diff --git a/app/Http/Middleware/SecurityHeaders.php b/app/Http/Middleware/SecurityHeaders.php index dc780b3..3c177a1 100644 --- a/app/Http/Middleware/SecurityHeaders.php +++ b/app/Http/Middleware/SecurityHeaders.php @@ -32,6 +32,7 @@ public function handle(Request $request, Closure $next): Response } $response->headers->set('Cross-Origin-Opener-Policy', 'same-origin'); + $response->headers->set('Cross-Origin-Resource-Policy', 'same-origin'); $response->headers->set('X-Content-Type-Options', 'nosniff'); $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); $response->headers->set('X-Frame-Options', 'DENY'); diff --git a/app/Jobs/Network/SendNetworkInviteJob.php b/app/Jobs/Network/SendNetworkInviteJob.php index d40e9fd..3599cb4 100644 --- a/app/Jobs/Network/SendNetworkInviteJob.php +++ b/app/Jobs/Network/SendNetworkInviteJob.php @@ -8,18 +8,45 @@ use App\Notifications\NetworkInviteNotification; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\URL; use Illuminate\Support\Str; +use Throwable; class SendNetworkInviteJob implements ShouldQueue { use Queueable; + public int $tries = 3; + + public int $timeout = 30; + public function __construct( public string $email, public string $locale, ) {} + /** + * @return array + */ + public function backoff(): array + { + return [60, 300]; + } + + /** + * Invites go out in bulk from an interactive command, so a failure that only shows + * up in the failed_jobs table is a partner nobody notices was never invited. + */ + public function failed(Throwable $exception): void + { + Log::error('Failed to send a network invite.', [ + 'email_sha256' => hash('sha256', $this->email), + 'locale' => $this->locale, + 'exception' => $exception->getMessage(), + ]); + } + public function handle(): void { $networkUser = NetworkUser::query() diff --git a/app/Jobs/Network/SendNetworkManageLinkJob.php b/app/Jobs/Network/SendNetworkManageLinkJob.php index 395d511..29a69bb 100644 --- a/app/Jobs/Network/SendNetworkManageLinkJob.php +++ b/app/Jobs/Network/SendNetworkManageLinkJob.php @@ -8,18 +8,46 @@ use App\Notifications\NetworkManageLinkNotification; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Queue\Queueable; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\URL; use Illuminate\Support\Str; +use Throwable; class SendNetworkManageLinkJob implements ShouldQueue { use Queueable; + public int $tries = 3; + + public int $timeout = 30; + public function __construct( public string $email, public string $locale, ) {} + /** + * @return array + */ + public function backoff(): array + { + return [60, 300]; + } + + /** + * The requester is told nothing either way, so a silent failure here looks to them + * like a link that never arrives. Log it with a hashed address — the plain one is + * exactly the kind of thing that should not end up in a log file. + */ + public function failed(Throwable $exception): void + { + Log::error('Failed to send a network manage link.', [ + 'email_sha256' => hash('sha256', $this->email), + 'locale' => $this->locale, + 'exception' => $exception->getMessage(), + ]); + } + public function handle(): void { $networkUser = NetworkUser::query() diff --git a/app/Markdown/CodeTheme.php b/app/Markdown/CodeTheme.php new file mode 100644 index 0000000..5695909 --- /dev/null +++ b/app/Markdown/CodeTheme.php @@ -0,0 +1,66 @@ + on purpose —
 is the scroll container, and
+ * a button positioned inside it scrolls away with the first long line. Both labels
+ * are rendered server-side and read back by Alpine, because a translated string
+ * belongs in lang/, not in a bundle.
+ */
+final readonly class CodeTheme implements WebTheme
+{
+    use EscapesWebTheme;
+
+    private CssTheme $tokens;
+
+    public function __construct()
+    {
+        $this->tokens = new CssTheme;
+    }
+
+    public function before(TokenType $tokenType): string
+    {
+        return $this->tokens->before($tokenType);
+    }
+
+    public function after(TokenType $tokenType): string
+    {
+        return $this->tokens->after($tokenType);
+    }
+
+    public function preBefore(Highlighter $highlighter): string
+    {
+        $language = $highlighter->getCurrentLanguage()?->getName() ?? 'txt';
+
+        return '
' + .'' + .'
';
+    }
+
+    public function preAfter(Highlighter $highlighter): string
+    {
+        // The label is in the markup, not only in x-text, so the button reads correctly
+        // before Alpine boots. Without JavaScript it stays a button that does nothing —
+        // hence hidden until the component initialises.
+        return '
' + .'' + .'
'; + } +} diff --git a/app/Markdown/NewsMarkdown.php b/app/Markdown/NewsMarkdown.php index 39f1359..17e06d2 100644 --- a/app/Markdown/NewsMarkdown.php +++ b/app/Markdown/NewsMarkdown.php @@ -7,12 +7,16 @@ use Illuminate\Support\Facades\View; use Illuminate\Support\Str; use League\CommonMark\Environment\Environment; +use League\CommonMark\Event\DocumentParsedEvent; use League\CommonMark\Exception\CommonMarkException; use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension; +use League\CommonMark\Extension\CommonMark\Node\Inline\Link; use League\CommonMark\Extension\GithubFlavoredMarkdownExtension; use League\CommonMark\MarkdownConverter; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Yaml; +use Tempest\Highlight\CommonMark\HighlightExtension; +use Tempest\Highlight\Highlighter; /** * Renders an article body: GitHub-flavoured Markdown plus fenced block directives. @@ -245,6 +249,22 @@ private function parseYamlList(string $body): array return $items; } + private function openLinksInNewTab(DocumentParsedEvent $event): void + { + $walker = $event->getDocument()->walker(); + + while ($step = $walker->next()) { + $node = $step->getNode(); + + if (! $step->isEntering() || ! $node instanceof Link) { + continue; + } + + $node->data->set('attributes/target', '_blank'); + $node->data->set('attributes/rel', 'noopener noreferrer'); + } + } + private function convert(string $markdown): string { $environment = (new Environment([ @@ -254,7 +274,16 @@ private function convert(string $markdown): string 'allow_unsafe_links' => false, ])) ->addExtension(new CommonMarkCoreExtension) - ->addExtension(new GithubFlavoredMarkdownExtension); + ->addExtension(new GithubFlavoredMarkdownExtension) + // Highlighting happens here, at render time, and emits nothing but + // — no client-side highlighter, no inline styles, + // so the code block costs no JavaScript and needs no CSP exception. + // The colours of those classes live in resources/css/app.css. + ->addExtension(new HighlightExtension(new Highlighter(new CodeTheme))); + + // Every link in an article leaves the article: a reader who follows a source + // keeps the text they were reading open behind it. + $environment->addEventListener(DocumentParsedEvent::class, $this->openLinksInNewTab(...)); $converter = new MarkdownConverter($environment); diff --git a/app/Models/AiModel.php b/app/Models/AiModel.php index 424ac19..f53a811 100644 --- a/app/Models/AiModel.php +++ b/app/Models/AiModel.php @@ -53,7 +53,9 @@ public function dailyUsages(): HasMany public function localizedRole(): ?string { - return $this->role[substr(app()->getLocale(), 0, 2)] ?? null; + $role = data_get($this->role, app()->getLocale()); + + return is_string($role) ? $role : null; } public function licenseLabel(): ?string diff --git a/app/Models/News.php b/app/Models/News.php index 3068330..e481d7e 100644 --- a/app/Models/News.php +++ b/app/Models/News.php @@ -34,9 +34,11 @@ class News extends Model implements HasTranslatedRouteKey 'teaser', 'content', 'hero_image', + 'thumb_image', 'hero_caption', 'hero_alt', 'published_at', + 'revised_at', 'published', 'author', 'contact_id', @@ -50,6 +52,7 @@ class News extends Model implements HasTranslatedRouteKey protected $casts = [ 'tags' => 'json', 'published_at' => 'datetime', + 'revised_at' => 'datetime', 'featured' => 'boolean', 'published' => 'boolean', ]; @@ -93,6 +96,23 @@ public function authorName(): ?string return is_string($this->author) && $this->author !== '' ? $this->author : null; } + /** + * The chips a card shows: the series first, then every tag. One list rather than a + * single topic, because an article can sit in a series and still carry two subjects — + * and because the start page and the news index have to agree on what they label. + * + * @return array + */ + public function topics(): array + { + $series = $this->series?->title; + + return array_values(array_unique(array_filter(array_merge( + is_string($series) ? [$series] : [], + is_array($this->tags) ? array_filter($this->tags, is_string(...)) : [], + )))); + } + /** * @return BelongsTo */ diff --git a/app/Observers/ContentCacheObserver.php b/app/Observers/ContentCacheObserver.php index b773a9d..db5032b 100644 --- a/app/Observers/ContentCacheObserver.php +++ b/app/Observers/ContentCacheObserver.php @@ -10,9 +10,9 @@ use App\Models\Product; use App\Models\Service; use App\Models\Technology; +use App\Support\ResponseCacheFlusher; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Cache; -use Spatie\ResponseCache\Facades\ResponseCache; /** * Drops the cached listing of a content type whenever one of its rows changes. @@ -43,7 +43,7 @@ public static function flush(Model $model): void Cache::forget($key); } - ResponseCache::clear(); + ResponseCacheFlusher::flush(); } /** diff --git a/app/Observers/NetworkObserver.php b/app/Observers/NetworkObserver.php index edad25a..49c21a3 100644 --- a/app/Observers/NetworkObserver.php +++ b/app/Observers/NetworkObserver.php @@ -4,18 +4,27 @@ namespace App\Observers; +use App\Enums\CacheKeyEnum; use App\Models\Network; -use Spatie\ResponseCache\Facades\ResponseCache; +use App\Support\ResponseCacheFlusher; +use Illuminate\Support\Facades\Cache; class NetworkObserver { public function saved(Network $network): void { - ResponseCache::clear(); + self::flush(); } public function deleted(Network $network): void { - ResponseCache::clear(); + self::flush(); + } + + public static function flush(): void + { + Cache::forget(CacheKeyEnum::NETWORKS_PUBLISHED->value); + + ResponseCacheFlusher::flush(); } } diff --git a/app/Observers/NetworkUserObserver.php b/app/Observers/NetworkUserObserver.php index a48e32f..cdbe99b 100644 --- a/app/Observers/NetworkUserObserver.php +++ b/app/Observers/NetworkUserObserver.php @@ -5,17 +5,17 @@ namespace App\Observers; use App\Models\NetworkUser; -use Spatie\ResponseCache\Facades\ResponseCache; class NetworkUserObserver { + /** The cached network listing eager loads publishedUsers, so a person's edit stales it. */ public function saved(NetworkUser $networkUser): void { - ResponseCache::clear(); + NetworkObserver::flush(); } public function deleted(NetworkUser $networkUser): void { - ResponseCache::clear(); + NetworkObserver::flush(); } } diff --git a/app/Observers/NewsObserver.php b/app/Observers/NewsObserver.php index 0bf1bea..2acecb8 100644 --- a/app/Observers/NewsObserver.php +++ b/app/Observers/NewsObserver.php @@ -6,8 +6,8 @@ use App\Enums\CacheKeyEnum; use App\Models\News; +use App\Support\ResponseCacheFlusher; use Illuminate\Support\Facades\Cache; -use Spatie\ResponseCache\Facades\ResponseCache; /** * The published-news list is cached forever per locale and the rendered HTML is cached @@ -32,6 +32,6 @@ public static function flush(): void Cache::forget($key); } - ResponseCache::clear(); + ResponseCacheFlusher::flush(); } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index a44b0cb..b5c4cc4 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,7 +5,6 @@ namespace App\Providers; use App\Checks\FailedJobsCheck; -use App\Checks\FilesystemsDefaultCheck; use App\Checks\JobsCheck; use App\Models\AiModel; use App\Models\Network; @@ -23,22 +22,17 @@ use App\Observers\SitemapCacheObserver; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\ServiceProvider; -use Spatie\Health\Checks\Check; use Spatie\Health\Checks\Checks\CacheCheck; use Spatie\Health\Checks\Checks\DebugModeCheck; use Spatie\Health\Checks\Checks\EnvironmentCheck; use Spatie\Health\Checks\Checks\OptimizedAppCheck; +use Spatie\Health\Checks\Checks\ScheduleCheck; use Spatie\Health\Facades\Health; use Spatie\SecurityAdvisoriesHealthCheck\SecurityAdvisoriesCheck; use Spatie\Translatable\Translatable; class AppServiceProvider extends ServiceProvider { - public function register(): void - { - // - } - public function boot(): void { $this->multilanguage(); @@ -67,15 +61,24 @@ public function boot(): void $environmentCheck = EnvironmentCheck::new(); $environmentCheck->if(app()->isProduction()); + $jobsCheck = JobsCheck::new(); + $jobsCheck->everyFiveMinutes(); + + $scheduleCheck = ScheduleCheck::new(); + $scheduleCheck->everyFiveMinutes(); + + $advisoriesCheck = SecurityAdvisoriesCheck::new(); + $advisoriesCheck->lastDayOfMonth(); + Health::checks([ DebugModeCheck::new(), CacheCheck::new(), OptimizedAppCheck::new(), $environmentCheck, - self::asCheck(FilesystemsDefaultCheck::new()->everyFiveMinutes()), - self::asCheck(JobsCheck::new()->everyFiveMinutes()), + $jobsCheck, + $scheduleCheck, FailedJobsCheck::new(), - self::asCheck(SecurityAdvisoriesCheck::new()->lastDayOfMonth()), + $advisoriesCheck, ]); } @@ -87,9 +90,4 @@ private function multilanguage(): void Technology::registerLocalizedBinding('technology'); OpenSource::registerLocalizedBinding('openSource'); } - - private static function asCheck(Check $check): Check - { - return $check; - } } diff --git a/app/Security/Presets/MyCspPreset.php b/app/Security/Presets/MyCspPreset.php index 369a2bd..e3c48f6 100644 --- a/app/Security/Presets/MyCspPreset.php +++ b/app/Security/Presets/MyCspPreset.php @@ -37,14 +37,19 @@ public function configure(Policy $policy): void $styleSources = array_filter([ Keyword::SELF, - Keyword::UNSAFE_INLINE, $cdnHost ?: null, ]); - $policy->add(Directive::STYLE, $styleSources); + // Stylesheets are Vite-built files, never inline +``` + +The source is `resources/fonts/poppins/poppins-600-normal-latin.woff2` — 8 KB, so the payload is +about 11 KB of base64 and no subsetting is needed. One weight, 600, one class. (`scripts/make-news-hero.py` +still points at `public/fonts/poppins/`, which no longer exists. That script is v1 and is stale; +take the path from here.) + +**German only.** The headline is language-bound, so strictly this file is per locale — but we only +ever post in German, so only the German one gets drawn, and there is no locale suffix on the name. +Fall back to the English title only where an article has no German one. If that ever changes, the +second file is `-en.svg` and nothing else about this document moves. + +The palette holds otherwise: ink for the headline, the accent gradient on the rule and inside the +drawing, `#500472` nowhere — with the tag pills gone, this family has no use for it. + +## 4. The motif is re-composed, not cropped + +Take the article's approved hero and rebuild it into the band. **It is the same sentence with the +same objects and the same act** — a LinkedIn asset is never a place to invent a second idea for one +article — but the hero has 486 units of height for its drawing and this band has 265. + +The story still runs **left → right**, exactly as the hero does: + +``` +x 70–500 before the unresolved thing +x 524–576 arrow one arrowhead +x 596–714 act the chip, on the glow +x 730–782 arrow the second arrowhead +x 790–1124 after the working system +``` + +Two arrowheads, still — §8 of `illustration-services.md` applies unchanged, and so does the ban on +crossing connectors. All three stages share one optical axis; in the reference that is y 168. + +**Drop one object rather than shrink three.** The gateway hero carries a queue panel, a model chip +*and* a database cylinder; this one keeps the first two and lets the panel say what the cylinder +said. Three surfaces in the output half is the ceiling. If everything feels essential, the hero is +doing too much and the LinkedIn asset is the wrong place to discover that. + +Strokes, against the hero: + +| | hero | here | +|---|---|---| +| Outer surfaces | 3 | 3.2 | +| Inner detail, small glyphs | 2.5 | 2.6 | +| Connectors (`opacity 0.55`) | 3.5 | 3.2 | +| Arrow shaft | 4 | 4.2 | +| Squiggle, heading / body / secondary | 5.5 / 4.5 / 3.5 | 4.5 / 4 / 2.4 | + +Scaled groups need the division done by hand: `` with +`stroke-width="2.9"` renders at 2.5, not 2.9. + +## 5. The title block + +**Bottom-anchored, left-aligned.** The last baseline is fixed and the block grows *upward* — a +two-line headline and a three-line headline end on the same line, and the motif band gives up the +difference. Anchoring the top instead makes every article's picture end somewhere else, which is +exactly what a feed makes visible. + +| Element | Position | Notes | +|---|---|---| +| Accent rule | x 70, y 340, 100×7, `rx 3.5` | `url(#accent)`, the block's hook | +| Headline | x 70, baselines 566 / 492 / 418 | 60 px, leading 74, column **850** wide | +| Logo | x 940, y 525, 190×40.35 | `codebar-logo-colored.svg`, inlined as a nested `` | + +The logo sits bottom-right, its centre on the last line's x-height. The headline column stops at +x 920 so the two never meet — that is what makes 850 the column and not the full 1060. + +Two rungs, and the first one that fits wins: + +| Lines | Size / leading | Baselines | Band ends | Rule at | +|---|---|---|---|---| +| up to 3 | 60 / 74 | 566, 492, 418 | 305 | 340 | +| 4 | 52 / 64 | 566, 502, 438, 374 | 270 | 300 | + +**Five lines is not a rung.** A headline that needs one is too long for a feed — shorten it for the +post, or drop the subtitle half of a colon headline. + +SVG does not wrap text, so the lines are wrapped by hand into ``s and every one carries its +own `x="70"`. Measure rather than guess: + +```python +from fontTools.ttLib import TTFont +f = TTFont('resources/fonts/poppins/poppins-600-normal-latin.woff2') +cmap, hmtx, upem = f.getBestCmap(), f['hmtx'], f['head'].unitsPerEm +width = lambda s, px, ls=0.0: sum(hmtx[cmap[ord(c)]][0] / upem * px + for c in s if ord(c) in cmap) + ls * max(len(s) - 1, 0) +``` + +Break where the sentence breaks — after a colon, before a verb — not wherever 850 units run out. +«Lokale LLMs betreiben: / begrenzte Ressourcen / orchestrieren» reads; the same three lines broken +mechanically do not. + +## 6. Where files go + +``` +public/images/social/linkedin/.svg 1200×627 authored +public/images/social/linkedin/.png 1200×627 rendered, never hand-edited +``` + +`` is the article's `key:` — the same string in every locale, and the name the hero and the card +already use. + +**Not in `public/images/news/`.** `scripts/render-news-og.sh` with no arguments globs that directory +and re-renders everything in it at 1200×630. Three units of stretch would not be visible, but the +resulting PNG would sit next to the hero's own and there would be two candidates for one `og:image`. +The separate directory is the guard. + +Nothing in the app reads these files. There is no front matter to wire and no import to run: the +asset is uploaded to LinkedIn by hand, and the repository is where it is kept so the next post can +start from it. + +## 7. Rendering + +```bash +rsvg-convert -w 1200 -h 627 \ + public/images/social/linkedin/.svg \ + -o public/images/social/linkedin/.png +``` + +Not `render-news-og.sh` — that script is hard-wired to 1200×**630** because `config/seo.php` declares +those numbers for `og:image`, and it is right to be. Upload the **PNG**; LinkedIn does not accept SVG. + +Then look at it at feed size before you believe it: + +```bash +rsvg-convert -w 552 -h 288 .svg -o /tmp/check.png +``` + +## 8. The gate does not cover this family + +`scripts/check-illustrations.py` would fail every file here on Gate 4 — `` exists, the canvas +is not 1600×840 — and it does not look in this directory. That is deliberate, and it moves the +burden onto the hero: **only draw a LinkedIn asset for an article whose hero already passes.** The +provenance, the act and the set were checked there; this file inherits them and must not add an +object the hero does not have. + +## 9. Before you post + +- [ ] Derived from a hero that passes `scripts/check-illustrations.py news`, same objects, same act. +- [ ] The story runs left → right on one axis, exactly two arrowheads, no crossing connectors. +- [ ] At most three surfaces in the output half; something from the hero was dropped, not shrunk. +- [ ] Headline bottom-anchored: last baseline on 566, lines stacked upward, wrapped at the sense. +- [ ] Headline column stops at x 920; the logo is bottom-right and nothing reaches into it. +- [ ] Nothing in the picture the post text should be carrying — no teaser, no URL, no tags, no date. +- [ ] Poppins 600 embedded as base64 — check the **PNG**, not the SVG: Helvetica in the render means + the `@font-face` did not resolve. +- [ ] Rendered at 552 px: the headline reads, the motif still tells its story. +- [ ] German, unless the article has no German title. +- [ ] The PNG is 1200×627 and sits next to the SVG, and neither is in `public/images/news/`. diff --git a/prompts/illustration-services.md b/prompts/illustration-services.md index 2be9678..d8b6dae 100644 --- a/prompts/illustration-services.md +++ b/prompts/illustration-services.md @@ -19,6 +19,7 @@ other three only add what is specific to their subject: an article, an index row | `illustration-news.md` | an article | 1600×840 hero, displayed cropped | | `illustration-news-card.md` | an index row | 344×344 | | `illustration-seo-card.md` | a page — the picture its link travels with | 1200×630, no card | +| `illustration-linkedin.md` | an article, uploaded to a LinkedIn post | 1200×627, **carries type** | Two older families still exist and are not this: the generated news placeholders in `images-news.md` / `images-news-square.md` (v1 — a title in Poppins, `make-news-hero.py`, one @@ -155,6 +156,11 @@ load-bearing twice over: `` is not ``: it renders no glyphs, it is what a screen reader announces, and every drawing in this family has one. Write it in German, describing the change. +The one place this rule is off is `illustration-linkedin.md` — a picture uploaded to a feed has no +headline next to it, so it carries its own, with Poppins embedded as base64 to survive +`rsvg-convert`. It stays a single file only because we post in German and draw nothing else. That +family lives in its own directory and outside the gate. Nothing here changes. + Squiggle rules: ragged right edge, never justified; word lengths mixed 1–4 waves within a line; line length varies between lines; the last line of a block is short. A block of identical-length lines reads as a barcode, not as text. diff --git a/public/images/illustrations.json b/public/images/illustrations.json index b5997e9..db7e950 100644 --- a/public/images/illustrations.json +++ b/public/images/illustrations.json @@ -48,15 +48,15 @@ "news/llm-gateway-open-source": { "sentence": "agents, processes and people all call one in-house machine at once until its memory is full → queueing → a gateway that stores every request and feeds the model one at a time", "act": "queue", - "actSource": "wandelt jede Anfrage in einen asynchronen Auftrag um", + "actSource": "Wir behandeln deshalb jede Anfrage als asynchronen Auftrag", "objects": { - "agent and team requests": "Dazu kommen die Anfragen aus dem Team und aus laufenden Prozessen.", - "request lanes": "Am Ende zeigt alles auf dieselbe Maschine.", - "single in-house machine": "Die Modelle laufen bei uns im Haus auf einer einzelnen Maschine", - "full memory bar": "mit 128 GB Unified Memory", + "agent and team requests": "Agenten und Benutzer, die mit den Modellen arbeiten", + "request lanes": "Ohne Orchestrierung schlagen Anfragen fehl", + "single in-house machine": "ein dediziertes MacBook mit M5 Max", + "full memory bar": "128 GB Unified Memory", "stored queue": "Die Anfragen werden gesammelt und nach den vorhandenen Ressourcen abgearbeitet", - "job database": "weil jeder Auftrag in der Datenbank liegt", - "model chip": "Ein Modell muss geladen sein, bevor es antworten kann", + "job database": "legt jeden Auftrag in einer Datenbank-Queue ab", + "model chip": "Ein Modell muss im Arbeitsspeicher geladen sein, bevor es antworten kann", "fork mark": "Der Code liegt öffentlich auf GitHub" } }, diff --git a/public/images/social/linkedin/llm-gateway-open-source.png b/public/images/social/linkedin/llm-gateway-open-source.png new file mode 100644 index 0000000000000000000000000000000000000000..701e137ff61acac061526934eac2b4a719aee1f4 GIT binary patch literal 128066 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 z zRTiu->izwlV|H97`*0F@rDP{zZGrzW6|df;Czwf`oHgZs$agaA`kgc@x)m&zBBDqx zX~4JsH|niv^>wS`ae3&CvTvJZzV-`{?-EC~L^TD_$Nj2+Aysz_deYtPb zvQa8KO*M(H!(I_nmCvdtO1mX^gx#^@Ix5@mWmQAsuTe{jlg7)|TfRI$NJ(l5v9_JW z$7{se8MJ@>fwUy*U9 z#lif`j9W;hpK<%@#Lb3+L&WGyPHtO@17#=nP`*A1{@M+oE|WIfauEp(&b`}1RgDPt zHX(J;@SVUHl7z~M>!E+udge*u30`M2ILk4WTBs{x8Q_9Ga5;Ui)E~jO@P`Q&QW*%A@S2U zxaRd1GAZx{x9w45b(mLA_2YgD?k0)v^*04;tr)I3zRkkBH_L-xJy?q@r?l|60Jja# z!$ifeH(Z^LD1bG`p^w$Rf671KN-?dP#0mgFcRuj-@GU@$?vvudTLBNZ_THk}|K=`j zWxGQDW}SwN+UhePNcrXn527!xsVIs05S2{LP7_@s0md9GG0hYSI~@w5ZD2vHl?T~H z#RqYyD5vDhP>)a%r70rABJsj0x8TiustuIEX2ltzbps_Z+J8xoik17ts}RL_$-ZZ2BXS(E*p*(ju^zCc;}6LyF- zW)5>kn

{&fIvF<~9FGEe(SeHZ(H_)vX^gvM>j;2w<)|U#!2*LrOdp@Zx7B-;O8g z86q@J%#0v+Klk0$sq|5SP5IUOl&p@MZ%jEi3sW7J_do)vC2Pc~AE4Y~5iZGCUy~Pm zd+qjvky;08i+v()i^IXSU>Lr$HG!G-drk( z_9ed-53Nkb_!HUzkAn0R^_||@RgeG+e1JRCJWUwJ;(#d$t%!*1VHwL(5z#QdB9!wU z4c2{l;Hy72Y~+5a!$g@%aW7K#x7i41$IJ+rbfX>2aOmjv0kI>D+85>m72n*7%Tetk zd>pDM$_i+adEl=svT20DNn+-c@4M>)2jqY3&d|Kp(e1bJ{w?|H$pN2xDFVa%|};i}u2ZV1Zg zCF2U|PTX+pe>0=61oUO{h5c@M>@vna@sn6Z=m(Z@hk;b=1hQ)P^BM3+lxu9wbEM2@ zj}_8J<{$uo1c(faIyw^Sf5j=aNu5 z)Ui!vydV0+mhi$xqQq?dD}uS5+7m&qWqQnHL#2Z=BMLxqf0m6@8xV0>UxW99)0_EB zo&-c&i^$sY*~_w8;#m;3;~!_XXOW-tZ{(7Kq7Hh;)Nhw(*YY5o9Kc|fc|k14MFF~2No=d`ZhHzQQibF8$!E>nzx&;`Jk{zUJLKIB?;s?&BsBig+0y}e@6z#6)h+;+D zg&5@6CuZgtP^4g8c><)hDW+($eA_f;p&^XCkI};76BFBv;|J!*f>*V;u@Acl#2&)F zStnL5a+M!}Um#X9Ivu8b7Gz21jfeRL+Y*1yeerJU)hY6@+dSl{)9C1u4ZpL*jlC#V zq~lLY^q6Lp2+TV{ho7az<2i_EU;kD{kdCMjfd6J(mgjycLMroq^ZC%i^agb^bK{@8 zJ?iWmrns5D%R2z4eVe5mInC(F6VaK`qPG!dN~Q%KCBcUWX%;-2a;SU+o}h_g5Tf4W zM-=6;(9j_2<6CaV`mc*8WYZE9k?+BmkF!WoVGvw9PE*65(|(M9#z;lCr@TjX9wOj{ zb4FA~mbl2g)@4AH4eFhUDr6b;zTBF%X=Z96FOnwAxK(WXK?K1en!In@GB*1 z3GjvN9LPonnV{JMVPoD8^b19ipxe^Id#PWxFsDyP2Qx6$_n3bFS*>?yNdNgqi%tJZ zc{BS_y~`q2VyTvsH=W>B2b#j=9ot#U=Gmf;ce8;ZXR*(#dMI&gU#`^o^eP^NqE@{Q z6e&R3O&>xQ0rUX>tJ?Rk0}=l^;(sc?-cV{0#;27F7mvV2LV_^oQUosY%M^yJWgNH9 zB0E7g`?gpdm8ot}I9{{}zPi1@UF=U6M%(dsGs~EHXy5eI%&%6w_llbNa0= zcKP|iVAAr02aPitK|zfnp&kDj4V6jI_o!d#Opv+Q@O&~im+Z~>=3wHn#kq$dYnvO9 zdD@0E^#ApJ8z#0E-dZ8sK~}tF?H)6l>7$bx2$7DN(hLH+^~mwg57iaEt12bEt4 zQ9VfG2H9zrO0D9Sj%JPW2mmyM;92CC}eVNZk&(#EY9uaFo zdJnJ}J3$~?pAIfi7DMxgOP!R&4?$$P|Y~>PlqmBJnPYLlde7{(<5Mes1f4VjX{h5NmCB>2PPt+&`1}= zmZYh^R}db?&_C98&HpwBs$ZRGfOProIv+j=y**5&GWq@Xhxzz5@%oQ8vSy0{glFc1Jb|SM15WDLWF;ZnRA-I)b zCPrCVFBlUy%fs=LTsbGeY!(K9DtYG9n2VB>fQygJF9qDQzD)gKH26@QB^_ckn{&rO z24MBiQ7P>N01$6KgKPn{XDyK!0s4S0X&-{e{zFo9uYKV)=0ar!KGOo6J+%)iIO!OA zthv_!vZ0_PJy4M)qA{x*uLJw`BWiaeNeeF|RNS~iFGGc%X9n;Mf7mRGz5Z(MLIXGq zlZER0K3K%^oi81$29_#OMvk&$zVo@8baqd7`yBwS&ZeRWaROhEzPwfzjFNwNMgTO+P}VMIJmzEa4Cb6EG$$l={QgL}WgLeEREP?i#-w|uxjKm}9j<3Do=J0*OBJ$`X@Aj@@| zt$vt9&5B@w8ji-XlL(K*GnrRR{Q3s{T{n;VliSW;E3$-M9%Oboj(+kZ zrv&OY*({GxGrg=V+GXKUr9DdZdrj~Kffbjn$};Cl;{YLC_7%MAZ}RHYi^BOcN95@B z{{`D`RRu4yI~T1!Vw*qwB{>PJ@R8R#g9yIUhtz*&OToNsuqmhnKz)2@EaxXQ);3w< z=?#!SM$|JPfDu|PDKuQRmLR&vU&t6uQY#Kq)>w@#j#F$cS(z~Pl+Ll4|0EDlNk{-k zNs!%DK+5wy1a%BcO$~XQmeu}i@I&Gk9UQFHIDdxTs0pF>ebfCl!62fLBkKg`5!Qmb zC3y#APXuAao4amIv#_KK^GH!{)^|C|urB4{XPpg}PH{0KHY?CN#Skry38ck@8iE&~ zcpMZ2QZ`Z((|#^_EK)k{c`%9W&_!QmdrGG`N*yjS?Q2ShKF!3FU2f-7`ZjpC3ST9( z@)FY;S;aPvQj4GSzH#^&u(?2nPyx|SB%((Gs- z!96Ls^mW_P$G9LIuB|j$7q{^w()_o4$ho32Dv1}UY(ak9#^$!$|*xQpfrGq{U{3EUaEj9n! z0tV_jS@cm%*o3Ug%&;d9 z4vt9^P3(l@5%oebf$ zhUDpv)>^8@s;+2%rAA4DX8Gb+Ui#NU=-Us6ex{JQ<+a;I$6e<|rx)aM{&e2%4!Nf6 zGX1#hh5VtwUXJ_X$2$_YrEN%i^4{|>d-EVrVO2cFVsLWBkj{U#r}A zuBis$&zvoamvYCJ#a)z>g$!|5VqSke7U3SfK2HDcbBf2i7A(S`JT8I~(b3=clx{`bs3o?F=k~>HqlT<>g=JF#KqTp~W z!eu4PQ+yFj4Yy7nryOX3S&T23e|F*_VW#b;k`8g3 zy{1*W#Ad!<4Rg(%uB&9BM5usJBBx8pc0k*lqT}t~H37GE*TqBG76>DJ$_*;7?i}pu zd^{@J>dPT>IFi(UIAY{H-u_=e}==P!fj}Q_8FNgom`j!udqe90ODo zT#(^>td59Ui#$NXM|sAw2KUu(;qM3mZB&8TR1O4aBcdrF>JSH=A#dq2d1Z4ef=?u0 zPqg+#em|i)tuX?icpVsx+2Skqk7LV`3d_7?eBLlx*lk-_41kA{%+H+ zYmD(pDtjxRq{DO|Vrc)DNo?i)krdN$^Iw866gfKmd$*?~I+Ihu6&tAX-?uC`$OI!x@jBECgR2WXlzq&MM_`qafgss|)V zkkFuEqFDFCNV|(8Fcre82;syk(@7n_WM2j5K<9chb94{pKDlG`oIFi#ufx`!3y6XAl`1cCPh8tjh1O$JI z$yMVs7sa^614eBMnxd9$1C^eI-}hRg9+(@a#OHb|-_PK+?yVJ9$Z;H)brkmSIG+k0 z|E;Djd@j6RP9Zb7in||&>#eXCo>T3qj0Uv!i`9mH*CTQ-qTk0-1R7k9KblskFdknjrncfR&owK9xiD%kdMQ2meOtYqS8j;0RVKG<{e9{ z51%HiRUNjwFFFY#W6X@S!$bW&5PF%>eOXj@2BgDNQU6$dY^5KA_M^axuh5Ekulggh z*F|LyPA7hQj^mScP>b@$M4ZPtu?38dTo zrj@^0Lz5`k#PJ>|a)bBI#@nevi+xF4zo7)IqxbearS;kSN&?`}4aLNu;IN;pwdTI* z!#Z?v*Y);dZRquz>odzfmG|pSPPylHkxcTggqdY8-ec0R z-$v%dwVAy|h`At!@8Ru*{=>uDp_KckL8EW2RT(=ov%^~LkndhE%Im#?AOC_aOZuY$ z_5&SZ{vAMf1&lImSJDGy8755WCa9hj+}2w(3& zm5ZE!A*@i=18ve`_JOWOaVsq>uwQ&MlZP4ktMG4PoRKo_H6Fz^N>UOETqx1_pd5_% zXjMr~%RlIjO;qEE}{n|@R6}(-FD7A|jVW&{H zeTNrovaK%2$k><}=2CUrlXt>BN9Lr7t3Y7Px?f7@FFivFiiclrynz6^8Woz86&VE+ zf1+eu$O{HifcIrV7hzHxbUeo6It9({%f6>A!JDCBw4n@qUqR0_3laQ#{ZDpL6o=Bk z$hPML^LX=VMa;>_vB-0GJ;D0s6)Jlz&kYz#KZLVT)FnUXBJ=^u(v%Qzro$H5;0!)e zNj#9CmM;netUGMz;eeR(Pd<%9&wEM(e_aj@BRC*mJ$UX%sY7!rEwB)Xw;iK{CN9d`y$WVARr#E{0*-?b(kNWg z;Q&hn!-G4pOPII?pD_#-z_9BcU?U~oX?6mB0R^T#!1M=j$)e%a{pk6J;&(me8w5DA zGx&%ABPiU3TI7$27(_e{tgvw(em8;3r|P)~0E2L{5n>^TioyI?!2I{dR+;+JAv;eG z5z`1a(8lnjn5`$8B0*cPJob2b6t&1mm>;nnjtIriMZ^m*4&MW5p~;mK<9tN_4_jXy zROJ_Td*~84bazWPNFTaEP(Zr7Tco?YI|Y$Oy1S&2?(PoB`}({0&U}A-XGRC-0M5K; z?`N-Pt@W(EA+-AmOc&06@Rn%9tGse#K}6XZ*4w9NG}@)wq2KfQRtM2x5Jw8>)=ycY zwZr63`K!xdt35#uAL@sonRPDrP9d+iqWeFM9lL+DJ3TM^@t%gg3dGU)cL2503(ypl zB7vo<04!An`N)c$Yhf5}PXCK1_Z7#ZvhU%H4@VlEkH%zAuT;z!W^i#L>ZdOgX^{}KW*P{nXZIvt0GV=$oa=Z>MsJCVH}K__Y)f(!Cs_U8p=*Z1L?_Xp z+QLhimFG}#ow|DjuR)Lc%Ifc4Xb6kk?Lc|bEp~$qWF_E2(e=| z8h+I)Q6ixj8Jf^ z^LNKVR6Z$>!m(?Wz}f55dF$Uj`{(C<5q$ub!=QN_mh0XA_-8mqKtTAeid14t_1$jN z^82YjJd}aKp6>p?QsB9#l>=}7JLC3H`EC1Y?Y?8}wU$Rw2QUrk)tU^;(q-Qiv%VZl zi+mkk`0thbvX~CIJNJSAjd!ksGrb}E=o$0R#E;d*K!*JK8iammIUbxa%K*A<_55%l z#5}XJa4*~vG-$&-2tZ6=l&c6T=(Cy~=!i0$qVi;m5B2z9LjqyrXv9Jwfj+EAus2?? zQf_-j*Vu$S3@Y@9h_wjD5@L`BSc*ecDGhY)a2iCL1y#cwGYN~Ik6%8_h%>CBhKmr< zbt1_`>eCesV4o4~G-)Cv_4Z6scK3kVohJD38p^@tN`iv#=uhZg3};kR@~K3@_OT%P z?O~ts)(nnZFc^jb8p8x1K4N|<|6MJcjY4q8>pNAc1uDAEutcPAEtaoQqKF4KM;upB z1=tk3%i%l5k~qeH(g|T6UVSyc&EUzrU}AFw=Aw^2a-wHQa86P@4@t9J#1{kx z2EP5xeY*_dZS(n|ZSn(~GRNzT|LOETVMa;7+s4M`bmJp! zr8)!#p}8f}ASpo{I>)jf+)*+eGJ#QIQ^^I-5RPs=XQRU88Sx9@FUtGSbVf!F)(Z}OZbcOQYQ{#tn>~Jf-X`P zAGuAQ(m<=Xh-Z=j~3L+A*(Ljejkbe<{Z+1W;IPoNkRvT5t ztiD5R>d*5+EDB35jGusnMd^-@2_4`ZXKL640%MRgzpL}83*nraM$sH>Fl1uR`WPAc zer;x|2$MUD=-pOF^39|GH~fX|KC|H0!^ze4)dxT_(x@U`{~%{;>F@__v69t_*z@40 z2)|G0a2Pgs`iHr|T(2{K>-i`y2WuEB>20P5V6YAV^j!Ilh2%-Y?I8X2U zPaA#rrA1nnJ734lg>AX`m~RGnJ1)uIF4ge#pYcEr``PZV^S<8a(e~eF)OkltmhRsB z=KcJerlml8m*=BhjR1$2P_UyuMXLUq`#eLdiOm7KPj=_|=13W=Oi>+|M%t?fs}*)P z%SA7&XH5v(c@$nXb#9AKhZ%BmB-* zKye4`yY_w}dytGJ4XxqxK_AAMC(|_P)4PtBtnBr*Wr$*lZ86{iJFs-9_oo6Jb0)91 zw!C%=*SpSWLbZdVB#H@Ug2-P@f15c2zTM+L71lhNw{^F+m5h~5)14l-<@$gR$9%r3 zpX=9$`xWOHzAePJFYr?)8J8o=qNg*BEZ-}I77Ctq5UE7hNTZtQLJZT>`(Y*H{%Q&h zDU+Bqj^N1<*Ax7NU2ywONLmR%n9W+$KMPrT;8D)b4o_r_KOv+W1G`dZpW-SJR>o1vo7 zChO?W+{f%#_jL|c>E_!1f{*}$DIWOZQS#or@|X4en)8Iv>#hFVEe(&o10fQI$X!k6 zRh92eAz6ifr;pXLF^3}Sr^UeF#?#a89c=9RgtAIO_{b9U1Es%=HnOSbpnj4=emvM3P}d z1dHg;uNb?5C}m#wp$sG~kah(}MTc!s#XMT~Tsxw^%E;Ew^j~n5+{S}FL#6~__o0np zio`|ROv=|k#qGZqj}fdrf>mY}@C}o(39c%{z_{gbh>hS<6MBvQ#;?zmfdYQ%ajZe& zQnxQZ?d|#&Fn+u_m)#%?nsEG2Z&&8gAJ-CGlZ9M`S<6a-U+9~iGI^z@M+f z+IGk$duPQwMWti(Z;s=GDiBS$$3X!~KPxBGRFvRQujKOk2C%E#WH%ZKqVztK!&2kv zunv8+Q#T`2X|sq9m}Q=7wpz%K^9}P27#zfwj*sy;z|LXE z601hd6jj7a7c$5FEg zQ!=ALcSO`lQ0|}qSNs((v#qr7b?o+u&i7oO*ymxhp|`Jo=47!BUF6w81nvd_fK&_l zp=Yh<9nk|OEb#ykZT|2z;iuf#=kO14TmEugQqp7ZV@Kb!i?o@Sp-T??zVte0`Qp-x z89ji(c?5dFec1Qud0Ul%ua?5%^Xe$W54Al@MO`y!44)CWE#eTtLx$vB;}D-yaA#&4 z54TDM=h@CX4-!I#r+YJWGdSnGy25-T|iOX<)m zGsLC+*xzdscz;I3c$j5NaiBn%ilJd~Q1%B1L4JyP@4%cQ(QH4z&)l#O z2e-e~g253x_t_xQ_pl%Y;fO?csGTE0_tAnv@=Q!j?WO~31v;$SU;D2TX+4jTQx zbUf$9-js)X$r48G#kw`}VgdhU9vo}t^-5sjf1aamgt3gHKc6Cz&vyh$YCC&gh9$E1 z2rak>|E2e6Qc5DrXbdj2#7 z(}pza>*JuHmmOlq>m=R-dR)qCjEjT{&bUZp>?lDn^1I31Fd^mNYh#t^H<||s3Zgm*ty#m%hU=Gnl zM)rX$yRD%%gdNG0=Hr=Mx9D8^@I3<{cY!vN%}<_>*DK#oZ^*zX3z@#EH&&)>%^S`g zDbpTt%}0GPb=e+JeC==ZJUy=)B=tH;?fhnNdo*WkT(Ok~KvX=Ssg2Me?pXI};9o1U zetoVEKHFo~1{rK$ggl*hzWu85J_WR%l6qa*nWEA{^FcSu@>i2$Msu*2b43RB_7am$2tM3$vcGXHF$qqx zzl9pfg@)|w$oku(q%1KE>2B3*FA6L~f&rBPZjn7|DuMdtphmKUn^yPx%Bl&|6#|sU z59t=J(grw;u5i3#R+z}$&|vPDy&z0^B3%FE@I3NjEPXaik7e|XomwU;JSpgEk{>HS|oN!qA;a0rc+I4k8$^O7ex!oP6Zr~OynTLxt{y}dkQLS(Pj_V3&_ z<0_mqX7dNE!r|GP|7Y5AK4vCLt-ue2C;hhZQ%yPDo@*g=iPZn1W{|X8Ht>tc%b_yB zYc-~}?a(AnQLCY#+H1Z&Y{vrY@KtRUoM?Ha{yJ#Z;~Q}E6+cQy;<4g$Jpzqpy#YFW z%IIQXxOvaSe(6J;iK!|*D(>y_pTEr8&1{sQbW=P(_yv(22MtPEk{?CgeHuxOxAH>| z9G+SkVA`XmLj1HO_MufwFm+2nUml~bbWS;cQUb93aPuKUA4ucHF5mcukT_j=(8Qfo zzDW}hH$zeWhMD=1=GqJ%dWS*IMBU2}oDt}_KgdkwjT&^>Av!UJub^)Tk=ixD`5>)@ z>;8DHfN13mxqzqQY_{RVC+YF9?eCguf-$jU6$6p4RgiAAAIldYC#u{!Fq!X(aAzoF z9|4}MG!|{NlrpKYgQ&?n#GuaSU6@tVXJ9@@3ky-B;>+!I?%Q+C;OCRhHy>$Y$G)xp zNP!CLEt)av3e6fLKn@`#CeAtv8RHTH#v||7yv{i%1naDBkM?tY4S!_T38COIe`z|7 zbXx^ng)zd92a=JvyGp6jrTqx^ZS(3WD!Fpd&?uXc&G+F#L)isNU=@{S*?jwys)PfS-U>-80C|d;5Ut$oa zw380X^cJIff+Sc1qtfI0giwYC1XC%huC@f1E?WfIza_~hAE=NizHjuWvLRQs5i&7s zQeU9+LnD?9tC&vvp#TX_khIL7O-l?_o^|Y)E@Zz1rk$=g(e>k$4S{s4j{q*VKZdpS zWma~#jTLSBdV4fH+GIIXGRpe4Vw&px6+qRmLtR~6&HFjmm$7fL00UB^#EivixL3AF ztpCgEpJ$7E4mF)(sn&FKPD9^&Mv?dBfllPm!d`8rh(Y)Z4`Fnn-gQP}$8Cjqw(Fl2 zH^%p$HuO8j8V>)a3oQhGP_O+ZgwG*)jn>|aOWGaiu7S(+{%PJ5auW=egQj*E&({y6 z(fpPdCC!6RVJ9mc70}%wuMrPr$tHFKKUdVfad=tFg{?hMNtn6=jSOKxuJvT-$bHt& zV!>1&r$C@lCNceNXea|kUTc>KMfj4CT|<8JBxW>5m!oa?!3e}aIpWtV!Jx1YyJIT3 z$YsomCf<0#!3lNSE0<#!l&xnxQ1cWjs8iD|3^$Y|jKl}0XArVHk~Ua=2QVyju#ZSl zn0&}d+PYHxC&>K~vInzA97vx>MTQ#bK_J1aq25@*D~G>%z)5deOrRXd)_X_sv+Pg# zP<J3m103s$8wC+whz>->=2$ISpe-6JPGK`Ytv@stta*@RfemHbO6% zc$L$dB8@s#yCW}!6s)H)DKXssusu*EvLHT1(&G3Y;~0C%0sjr+4-eA|hkX-!2S>P5 z7Z0EA3WEwS9mfadPesNP0c{KQ7n0t_oWmEsxutqUl`^+DEGCwOVk#7%%ItncwN$o_tCwyCF#7LS}HZc`}3@M_x^gOod$H_6efO8K&+CVsMNfC_-8->NTpU_L{>=;iBmJv z14&D5eyQ59SL^)c8jy5aA2)!he{|u8JYY9?f6xA)H?%NIMFIM$-)Xlyl8i2Na{)}x zJWu^Q!`je&ABY;iU9Z31uDh)`mQv?#wSRF{&qa>yL>(Y1wMVDbRs>O7534Mw=)5l@ zV){{ncC?kOjM&GX4JC}^mj&aq+CzlRiykLIoql7#OtQC0H2)o(4I&`%Y{ZHcaaViBp6dgVP~~+te0ih&^Rq(^e@O@$U*2tUd87Yl75|iPa`5TkY^52Y}iqaUPqZ z*){{pgz~>K!6;8={aTzoz(IZz9d-ai_Apr(D@-d5aH??tb%FYJtG}+_V3RIV3(TLA zJ#>Y3yxzXiuU@$U71<^_CPu*(tE4fR=6=BOa{7J zn&brGvG>g=JDd8AAAbwTN1zKr#p8Rf#`-x9&Fl+mTu22IBsfc8vfx8_`M<@2OzM!! zSRN$s**`JM1itghCxhUM0NF$aelU=l&Tpm(A^b>)pl?(#7GbGFh?(qVSN+IsQdP2i zY%NAASz#$;G(N0Kj%W9sL2}TrzMsW()4t;!Bl)KfWp8^ta5UY z7HhXBKZ0+fP{tnIhE4}qbY;j}Rf5*WdPs)W(HyRE1R`V+B@@}|i&vX~$AzD~iFn?@bOE2i!$IK;CStC)Te5GB&&%U(EYn0b2@%oy9y9Xe zz3(S;9Ev^yM!X?$JhKIudI znU0cvU4L4Cn1`BzL zI!=J(wgOKqNYjC2b!cO!Gz%_x3J{&@!V|^3hTwDVrVfdM0^1>yJ(OKi(VL4XA2pBH-1SEP02-OLuci8eQcmw( zP)FNLe29@2WQ@T#EV;2{IJ7P+#aV*gjTSj0cIdEi`AI{X2!^u-We76l{)G%br9=|_ zz#Qrp2yHFzBv5KrsFMy<^36U@)fGGZ3nH9RUEVI>Nc=(KPTX?zgY^K=8T|cOzisO8 zl)s=7g@|1Eqdyw3RRKA>=sp+XIlEs0OW!^rLHYnx`MmDEX*#dRnkU)gci+eQDTCZm z5r4FI@4oGEzPXI^pWh?c10@HD5fEol`~>A_L~7AS&t3u|pUhhb3{D9P#(81=2ulU( zK?#+U3tzq|Gr7XUm^cHZ%=FyR@0GFqV84BbxIEGt&x1%X;G@Za?=?)4nfgcwD;e1F zB50`+8h*7z_mOisf5pg5ga4TEZvKpze{gK1MCS~_NhGd>l9E!o?W1zrnhJwM8sqR0 z_a;{yjMIvg5=Q=q9cW7O&<#NwKejyzhQ4V{romcO%~@Nf(P?$N*YDOd)??? zYVMZ(Ryv~>%Sp5_U>42xeU*NW%yLHnJ)P7|6sP)JwOxA7pm)7n+_T_h0bq>i$JVwil+f%?di?Hl+dA zo-Wk76I3oD@I9W1!-59o9~>@B{DjchOdC}e-Mf*Ucb>?4uQIm^;!mo?u-y`5DBt&z z;vm9m?KDkNC*bIq%p>WSQ>BUxU?IGeKCT{#ohuB*jbXvPuo<|(g|j72&SuHVK+$@l zL^vg~!8rKAC90&P#2aQ_#)i|*xK*C+aN3D35HV}j?$iukQN(=thn|1)u2a^*+Ba>M z1ATR1Gq*8l?ARaFL(f^_a+cseM_5W~o@_q1q(%H9+YWXjX+ck9w2Gt*YR<(V_!jTRG6n3{RX&bOK1OCVx@SJD2_SWDVvvDTDu2ebh=tG~{Ddi(e* z;Gj>Q%{H{fzOyqUeY6^v9{iv zI!rVW{Cik5(2xPg5^siFg0Ki06&iy(`)T+5taCqg{q4~HT|O?Z#hg z#tvIFX$_3PlW`V#9aermc5gq{Ra?eSq|Y8#mxm6ze|s355P5qp&~+=Lua0){UhuQ% z-%(S&D(NG|{sJg@pBDp-sxLA)Uv&+byF2@;)PAen<)PsQSBH{g1JbauQJ(bO2MmHY zIZHMdQ-64kD$l!t<*L^|=x@ezC=;_M`{@+>8R%6W7n?ZfYYwBGOnUYEnX$ghz9N6y zTbWi+dotMjP)1Zl9nNopGikbf!WdBpNNKy{3FmlZBG&$-ho@qvEzJ}A z$aRoecl6i%_4HU{LJX3Fy6_)=twe` z!B*DKv1C{KxU<)e>5VN93qdS7JW5}Xj`LGeK~5P(wi>E5!9_BaS4E94VZEfllq18$ z%rFQ{HI?4QQxp@U_+s!=SxO+Qtc=O6c;NzJQ-2hpwzl@}e0{wo!nX6PlhYL-Dy8SX zg`O@q;$dU&?e3zH3AafyH@3qCk3+%4ygeSjfx6CC*R#F< zEFvL=r*H(w%X+ODGNN;Ajwg~_IdJ^cwB8Z3L#CIYVJjub-Ql#7V+b-4JF;XNN&En! z0FhP(25fOivoknFHBs;S90a5CaC5geA0^X_=Kge^8zJih9@PZDc~6t$b_gmF;Z(D1 zX7K3lQit~-@K50vms8&LZ{p}YTus6{*Gu0s*D_rfdq!y|-1xTqv_m;4iydc-A^6(T z0UszbhMg824moArx)fxav)&mQ`nC8$=bx=eY3qzg%B@*gN?==)M_oAAnX_OhvaNc$eSq^~A7S>1Yx}L1Hds%oDkZ&yO%?_T&c!d4SjT?w2YH>ZE>Rfk} zmHqtX0FO&H{gOZO*Fq$wT?E}KY=ElY;)&-yX_2!v>lL|iU_vFsLq1JFXusxcuWOt` zKLS*kj3{fymKIiNB*A_JJkfKp$$A0Z{;SSS_5(2o2Z!0jc<0p!iYaTvB;r=>+@DWDH9+v^(%lUbHh&;iR31=HqJJKDd;S0NLfy@0#3w-!|UMODRuPF3p_Y5MrF$3xTy#Xj?@wf2*vP+>U#JwPX57c<8*ZI*j9cu!J2Tuo4Oa} z8-({gP$5#^(ii%hXpV@nDAw4x3x4`w9N4wE@m=4?WsU?3jz~dPw&Q+H1R%H30X@t9 zq*@H+@%|hjWA0yXW@3S&@9yNpA=G5Pezw?{Q@1%9dthFPqNYvhGznuk(4JwS`YcOn zC4(h-0AbprLbTBPO3EA}Q__?0QCh3`=P1Q)mQ8yP{zVWfOeN%c4N5c~(2OPV=vZLoI z1vjf~o)>`V83{okQN+bocjZs6b6tt>RvBi~%HKp2gYaV`HGQ5;JmcJYY*^pFdk=8RN?aKWXNL1kqOE`=>Ef2+o`vWWXHdr&`pN_Hts4sk$6-b7u$E2t1Z z^oI$GqX?{kKW)IPo)$e12ysB!ev;B+!dLh*{aUrFYi}1CsMJm|Jcg^RjvGOPO_s0;2*VXOf2ntxkFO^#+h?UM1%a?T;Rrv!&I)m%q zj(C+Bm}A3M?NZ&+viBFz60U#qN)|8#--*AyzLcS)#OL6J*6B3rvotM zNhv8I4RpvAy};8vkQ$635nslF3*P7zDKUF5vtMgkT>ONLA~cHhYFYX%qjWW@y;S#m zk=;~y!}#wD(S}`PJwM2?B`CS!0v9~^8iGc4hr!Q+w~5iS#MQD z0dSZP529)b`0j8f)b3>IS9wQ=Fz~8>QMJI(VVSSgZ9Q-^(@0iS70ZWPXmz(YGi%)0 zp$52_?D)N^429<}W~?J6vsulKg`!G3e&(4Q(P<@1dm9CMyJE0~-u74Ik=hZE+D+^f zcd0tUNXpFGV{*6zD3I>F7MmKhb%*9(U~5gbo~^dtAI+6tT{$gZ@fkt&hN8cn)cs1N z(|V)pPersy9-ap$`%2k+$bJ3Mn#0rG6{OY;cY>5v<_y;k5lR*^2kWwclXVQsg z!jvl@5rc&;ooc(I{1M+S6ILrT^aVy~-$K%mLr$8ef~p;*)*)uP92<@>F~ddA z)|MIcj)>>vzB6~KN?*@qhZ{+pi-!mBOx&L~t}oUYOFKGV_J!{Q@&H|L2r7zeCw329 zjVK&LK?FF4L?C4k|L4!2|D3?cJp@|+V4bTgDhNOe{xHhOuI|{_0mvfX6B_#I6PL8B z%`fu0U+WDHLiEO+yi~&-(vc~^q)H)q82#dSYIaAt#m)jB0&fWnVtfqFxrdla&7J#jf@tGmyY(x;ugctgi6a zmnZM%%OgNwD{pJlp?egcSWBCE3KAB2g5uobj`E}#PCdO?xsySa7JJwWFS-|vYk|Q8 zAHw*DxuOEMDHUUO9pu-*L)#!ajFiAk1qE3Av`@QIUTC-ZsE2H9pdC@amxx_jS((nh zeYLc-)YQ~O2VD9E(4DHJ8>i9XSajo=?k~UyPm9B*?XO?IIL&lAocHe#Ye|^t>0$l; zcw0*vt*U?&8AVhhfF1dJXb7L%3VYS(AYVFc)kT^@Pz=%YqC529PL?hwEh!0m>6M;7 z2wd@W^q!H~wOPkSyUEbM+IPkT0aA5WpvM|b9?zM`NilN?_V)@!N)$0XM|$Slhh2G9 z!}O!}j0+8dl?Ye?$6u;J6cL(fC@X?V2MP*E>`dqtFtjvPTc#EkJf61e%B{H|H?h5$JI~fhE;g^?ldB`#W+}q_=BkLO27K7h(0~H3;)_jB$Z@6_iNJhdzZ7^(Ri~wv{-v`5ntj6yb zzURKr=-@+|#cdFTMCp(&Hp62{A^s0dsIrxZjiwbeI6wm5C!)uIfmsTgi2XxNnu8OT z8CR%o$}wgfj-+va-;c7$)S+!%D@I?hd$eXPOd?2&ja79uC)L_Q=RJ z$WH`x+4-b`JG=LD*O_arN!_-)K{`eZ%0d?h3hNFXTkQrm(HHyzzv;TnkAM;;i+4qX z4vqIMBM|}4ctauy1A)4Q<(W|)J()@Cm3lRYW48xb=bHWz6oAFwHNsWGom9OJ<>@p1b+MQxa{ z$J?+I*k7bF;0x-lsj1&RaMctQ1xdxHvm3We!v~^EWU(8606pK#OuXmG4}eouMk$O2 z)Fr4y)&Ve)vIy8B|nKqGHV+{CPW^jdwu|+In#O|85V{fW13zlkr*Ajs^?TF{8nOBSsALWGIkFFJBM2M zxa{4-<+g~&+Y4h=ON5f1`^kdiVeyRG$Ar*#1PR&M*;!ebM`#0FR#rbEBO~2Umn4lN zJ{A=ftpFt&u=gKSeE#+gqVn~-LE!ZC^wasRZE7)?fB?3G( zAVIVynNj2sfLg0|?)&4}-<3OUF1K2N25hN#y(YAzM2gbdB8R*$2AnZY<SyW~Ch{H5PxwnkNAfTC%*#AX+?J~&O1M;UE6IjahD z#5cl!ix1S!sI@ivVOm272TCb~0FB!Z41p?77fc8UpVw|BWP+Ee$*~(~Vh`hwjxw0F zI}mNFux;2qF7o*6%)Msd84}beY2jUx41r@9Fj-2QnkMsoR9API*NxQoeYq``f8)29 zBo|p;Z1a@&c|8HdKSg5xZx0tJtZff~9TOndI1}lV(HuIdVEwD{aQ1ACLKwUMAP1$n zVB4424Jc?L(slLtfj7-%TqJfFsKTZQ^Wx=2tNFl8F?GVQ(Q`I)5AaHo$7d>)NN({vFg@ImzxgdwY%C7P0V5(3$mejR% z^;HAq?F%R_HMRBY+z<3MePN&HhZH#iWHG;)Lt;UX$<@g77OzXAbsOONMvhDd;2hIz zaXD=Bnarl>}a@QjIQ^@Bzum{`U?Xgya@0(s3}Jbs_h8163@ULzG}GQdsb( zCjJ6ybm)kof|Z}(2H*QnhxtZJ8^HMDVvF*pZY&KQN4V8ezON4Fz^M0|rF8$4c#nnp z-3^T4j68wBM_MWF4_qGE@I6io)%3J^@;O1|%b415B90PC@x&;L3DC>jAzT+sc)vBH zzZmX@BO@cnqfZkPz59<`F$dZ;hsx`92gYY>B!x7G@gSsqVgWas)yLah-`Bt0fx9dk z-tt7Mh~$`v=N-sp;v9Yb0vb8^F~0L?*9(&X$R(X|qnH@nFo< zD)5(LI>+rOYvaRR=OeJ#fDB>EMF^jekPrn${?DILXDzE|&(DimT3Y5=>@6f`R7?f} zYW2nL4QeB}7-BuA5XFJQS8jhN4dyk?J}XvaP|jTexdj;(X0T~?+AimaR2r>#P}k7j z6KuL5VhcD@a7Qfhp4?XFBcUr#;XZZ1NT50qWze@8XOh#IFbt|kfT9`xUcP|>e3mO0 zo4t+p>u4YmoRg75?-gaQP9evxi`k@iTsiz0=qRdGqFkWuOQiTXW|j-60Ti`fHCgH; zRDng&UE{Q1d`Z+`8m_QgxJ2gCbP2iYFVVG%;}kG=!wdjqq%sk3v`74pc`4;4_(d)x z^s-f^hJa33Z$2@Re-;UejL*z0=UOA*?(&BLLhTbb;13TES=(>6fL{O4+?>$E#(L=| ze9*t873-VTXw;9eHdahXM09<VkzTc!ujfA)vg-y~-nl3xy zFnW4v*arvH^aLrp924ERAb=nP8 zD*+4*;o;%vf(~!q_p6L@KEMbFAQgzT{kp#Bjs5m?+UP=rq5thNIMc3W>d=L9YffY0 z>v07y?A+UKs=mYrIpv?)jUB+m1F%+lZs#YguYU)#J|`iZ-blrMqkVBfTs5Sgl!AV!Sj4YWI+#5r2V=C#onA+ zsj90@l`s`c#5;a%{LunsAcjPqS8gOi=k`{#tAh7M=RsfBo#;=dLE1gxO)M4*fD}22 zc}}mE5J4IANSOC9?D!8*i?!+{P9Pgtg5BsERK1^4?PF+}x()zt4^w$)fctOxF@G0# zHYswxowPp=1)duU>aR`lQ19D0joaJzaFQnE6cmmNaruN+`Ru>hOETjviDs9C-|m*} z%QdQSTLH`35U^)^CN+e~6+hC2 z%0;PPKhg)W%gM|0+Ae+D=<-iUz??9_-`(BidUO{PGn5yy?&-91a{B1z=5~L!I>Be` z(rCB(#U+`ids}*Lq1LOrHy7)#8sldLDL70_OeG+_f<(amGpPXKQ3pL&Ah7Zc#Y1VS zOM|ewrM`ZZl9S`0F~Fiz%HlqLKfwVE1CIgjOV@`BL(soYS>V0}r~$)=l-5>OW)nH$ zi3-r7N=oDUuUB{zZg%ain}BBy0Bum9pg4f^Fg*TKCE$8QjSPnDP3V&oqX9r}W`>GO zQIc&IMtq%3*4<}r*X}ciBsRi=NUat_8jl1vnDAoim^E4DDCo@GBH0XH-j#eN8Vo_C zgxV5iait_APVq#KXcn1rr2t_ldE6(T6bF0VpJk_|9n4D?Ztx}~B-GZ`S;CfG^%k$T z-WIq0JlU|>b732&n>|`9JhI%66GFRzMQ4@5_#ucOBqXHoIZh)T1>V7M!FKLyrLBbqFyA>@cv&0m zb2~E3h`9Chz2j3-9;<-R%y7CKfUVgNL|>}r0}YLheLE_nt7=Pdl+5@9l#^6GVl+@` zc{z|na47P8K_ygfxdgOINk*vxeTOcuFXvTJgxnb)uu_te6mkT2*WdKAQ&OZ*R@|0z zg`XXQ>3sp#OzLw2NZXT5rTX?a48V0WqXl(*uLxSm=DES9LI80_Z{PW};aPO3?Szx@ z#V5gxS-_%}NU@57iX1pTHXxmu4|y;c5+}cu2TCwW213b9f38%Puz ze@4VqG_rvVUhFNf{(mokaRugZfVvwe>>aP%TGXtmN){$l((UgpDGM&c6(_b;#LS}= z5y95C$&97PKnZu}d=h`qL6{AY&PYBR{pTY{414A`9AW|59#1A?SBy;S^Yj45BzZH+ zp-8~H>^uPh*nMXX3YPKol$1X;#)Dx)XthTDBS6x0d9{OnsQ@O+q^mbzQT~@ju$OL5 z`t72sdqQnjk}&FhA0-7WNO9)M>H6#ak?!ZupM9#^N?J-f)@X{}1;=$Ax(p1`QGcg4 zH#IXgGy5T#H)*ueR0C*6x4LFa^^qDsH7Pixo{#m zMBYLGXz6LaAA|)D*dlyU)!?gv1bM#=T<5IdW5o&Rlns~C)Z z;QF2aL5$(WZsJ6A2z?0`mY;?%f5w)AjKiIww=3y5Y=u(20S~UagpBJx9ZPPM(A+No?FuBt zufc&lSdhMe;}LAcass9e+>$~lCLQu6VBN(`s8wLe5)z>b{`PMk9(Vt-QZMm*CQoFF z)JBW{(Eh|(BfLI z0u3}l^T(A>g~bX$;Y*PPumDLsx?1azy2x56~P2Kbickil zDn03M+l1k2;!cuFz`GT~BC89{6Yti#%4x0#7!>7NBEwR3IBWsc0&qIZO%5sJ_nf-< z-paZB6ehQGyB3QqvzC9rLSoBcWUlz{~qd*%-6&=YU7RV=os5_rwICFW`vAe*e~LumSche_a0$UuPMWRn)cXhi-TXN$D2p?hfe^q&ua%yQM+8 zL0UpUl$4YXLAtwQ!BNuuenjaI}#kN`WH_r~CHOLi^#TD*Wd&IX+71`NZsG zB}o+V`3qV~CK;L3$qQCF$9QiK7o-ZnTco+$HalqSvM3{13JaU zM(Gd^4Iy0^cg$*6<6Mm@S8EpfmY45#dj%r3cBcZ0#2b#2@ktn!qUMqSlH3Z_JZBuHom65t*~1ti|;{Lqj4x1-kK z34>8IpeQS!D?s?S`R|O9s0jbXYq{S_QSsIj$9#?KlD<_gqnI803N#GhZES23gs=9< zpPY_?ZT0%`T9ohQ0ywe(<>@s&eVO!eZ(rZC*Jj8tOY7zH)9Eq5M8AIh>hL3IvrM_& z8Ysxe(-(<9o{iFWj#(c=Yoj;}S=Tz@3cDUl!f^GiUGu1kU*c=?5#kb^uXw+!-d zpsjVK3z90fgPomzn-}}?$zt=V;Az|Oa1D3FWQ9iA_V6kRr==Y@w*AX$&jl6f(L|xJ zy|8Hzs(LRfWp#NuxFeg6_kj&cTSXM1bOHOn8)m;bn{jebadmV`AUk!V(Er;+?H$WN zeb~I@;<~sP?csHsd0v0xP%4?XG@$xy{`_jHB=R)#C31E?+^~kHL9!>h)*IG0)Lnmk z3&z4ql{1!zxAQKlT{TU9xp&db|K?h|RHc+!D*+A0ny)o1jO5=J{m9A|A@YuxrTJ9#{H@bcmia9vTu9)P zD`OmEOjK=WM>RMX1>5iv1JdGW;KSx`=gl#iup@9In`RA%$H%d{-9vX7UQ$VkQtCHY zjy8;oHd_o6yo$0`Sa%cnCt+OT@t0!Cw1kiEJlD3!tlJkF_GfFG zQ{9@k;GqCoS9#~8ZmQCvfLss}VU83<_c?(=$!uKsE|xAhS&-{pzNL_17d8rIR@Y41 zcWim#7nBa(7(MdyUI- z&`00z<0Nm%V^xMQqoYwsfyU%3n96n-+(6@ZG1|P>Pn)i&kybQn+8c#UU?k*zdwvbG zHp=_ViX6svFi#=HOS#nGc^ZO%#^X4Ta}Y1}3cS~O?B>3i^z^fvjd@*VT3xufg60)M zxLjDLC=IjeE9+JpmbC_P+;qvSP^*{gg(CZ}_05xwU!p5UQzd4v%->*qnW5=mT;SF5 zg@vi`mr#}SE7EYmBXZyy%7(@N5uq}SAu6fCi${bmu-C(mp^Z^lRrRHXm}hWfa;P_v zLP

5+;4ki^E6aots+p8+BaO>haw^+q_*7O&GN{WV@L(05VIyo5 zlJhwkgO<9R@5vJKXceB_NG4|_PPb#nZE0g;qh51R_|=)z>pJl$Q;BZ@kGt9i!iPZd z+>isL$XZvZ8C#Nqf;evHzb<=aWfs{yNSOO}wrQl-V)t<4OnICYBGKD3PL3ZKqq7&+ zE?z>8p%b05GY|DdG!GVG5T+4IM*18cvgzC)q#OD3c7l)faxGZ15(eIZRURs~+f7CR z!LFpF2vR2O6Qjh&c3_SXbEDFfl7Mg}RZUDVy$C^cP#(;~@?WdD)9pe~lazl_Yzh+Q zmO0>}s+JLteJ75cq!Y)HHjqx&iwb*9BUPkfB`w*2?+|8O`T+;7QGVwrzGO^p8MA-y zAhAFxbJ0JB3bQaYN&q);w;imFOZPpXDlnGCqD!SrCj4V}G|H4^<6lm?KYn2C$l*EO z@Muwu1@}hlKlg^fh(l>E3SdyClSPX^6UhHv3-QuWP`iN~zvt#oVMT(PSFc{NbeJz7 z4X--#=zI?ek&qLM^6y45!nSCulXKwy$^2N2ZJ{`Onb0n;;Wn;-yCoRqA^M}5_(epN z+I)}U^a6(loziPAt$^hoWO!tYKl*O2+|!u|-0;dRTRFJMbdpMAz149FvTgBJ+07KEO-%AF)%6ZFS@E_Ocii6K6U$d~3lF;Y6nZDj7vBQ3e`7@gS$>Cy$nZiHl z7-{41V*-wKHx0e*(?gr*>09mbBD*@tNX&7ei|GiALwcpGk~!lKP%v-c`G$v*I4VtgbqjOK zRKUbpCJwW1E!FzFfB4!DF1v5&Nbq2#E`_nBb6y>Z(f2ALvySK0j^%Mru!}```<=gl zF{OI=<#nOYWL!isiL!V3DoWJzetP@%?RR~a^kMLIJ8;TG z?>&ft)$_v31O|$u>V~n)X9?65Cm?r<0^_NhZ%;%VKyY=e?~4lx)=SqH^e2V zowl=)mdr)lo{o=Cg~jKpFT8%5I1MlOREkUH;+G|=n#R8|(*qJfHP5~F2}P^eCzNGTIH#|{Y!+e551 zjZKhq7L1bm)&_sUMo27I$}2OXvXd74Ww`(^G?lh^jf1+%vQEu`g@IjbXVN-@^t^N4 z!gM22*iX!Ca;RABSJgiCIzNqSWp~I=Xg7}dAA%T)-7q0JZ1ECoc{Tci+owrh#=ucU zEH}n43?}J_>|=3#*wEti)pTMLc1VaA)E!=UT38JkHp~B0PYP~RW7^cZlexM1%_@Vm z*UZ8U}0h5=5A8S7xBNl^bp7eU@i?Ut*!3;yy@KddyM>Qt*>vsX-NugxDZm% z1<*Eda5};z4RzaQTQ&CZB7cyvk;11H3PJi}OcuLbD3eRl*J0jJ21AJ`dg-Qxj;Vi7 z5^q=Ip-tk53PZ7z>Hv{JbzDP=HYO&!U43~rXD_EdyGhL=LriR*U$Abtd0oBu#{;a6*h_$)xw|n zRg}ugb^2Eano{j<$?6|J1&Q)<%0D#~y*lo#?R=EyjX)Y?hJnw61Ka;Lw=nUHi2gsI=5Yn1fgh2DqJ+vJw>^Y%D0VD*U`~TFlqBqYZp5eI22?;lIEHtP+ke_0XB57lqQ#Yz#oYX7ty*TzHvX04w zbTn1K*-#QLi=4s%A=(GH$cfzEY8HRM4)7y0oNV>1y}7x$ySro7`>O9(|D_8BhR1OQ{hw;w z?t6W%7E5uzsiK-064Koh>CgUfetwScq0tovz5;3Tj$z`vYkFJTl2$W0njN$#7Fs#c zevE;=)m&492?5Vf$J3o?S+0kzgoK3L{ilg|1|}xott8p1N?VELq^RDN_HY2ZkAYVt zOaVaOv}+AXH#Qz>$^3@vEm^J8wkGrt8s87~Zd9XB7`9-~;wdIiipG(Y63Y}vWR@G4 z#Y)5P;?gShzFqq9mA15u1T@nEk`b*XvE%GHrKbn$;o{a+kSl`A%NFzgv?Wg)hmN3; zs9?v)l%@4X7?kh7`s(U}b)h!wO{uw1`~KZr0e5u!x7Hvketz$zZ_Z#ek2m0^Kzpb7 z{cKHeM@R^3>c@}me`oX{Y@o?rQbI#ci&B@K#U+c3hKdRTPd8c>)zs961uBhY02hNq z?zh*f7!VL}$Y=7eTI*maMirOwZ+{PZm@PFzRmHeri9ngst?&A}23qf6w?~6GwYnz- zeWOJVE3&vMYLL0Vluv#gl5Mc%uiR^%JElNjF0A^piHx(^#D#+fF%G^_>Qcs8skKbyK7PY|oql)~{vNDZk?&ULG7nl7gQ4Yn?)Y3X(T1ZdNCOC*^ zfF}b3qZT?lX+S^I%Ma*p^!Y+U{QUFVm9^E?@(S<8o!D5}m`Dt(g)O*>W4-lAE+-(- zW5*k@TQDMi*x?ZCpNQYp@{mcWe-eDU!D^&mO_+NLDvnHe$kRJS4la)8zU>0Y@)sBW*|GR|c)t{w8Zm*|l`HU< ziX36VmV=liDHM;=9IS%H4u4MVxS zR6Qw)C+6n90rfybnI8eHs8?{}$d8e`Dw)xA(N3frQP@V)czCJjVz#LAo>})vCCt`o zRXV;FC~T56-4S_h<0Tgbs)-}FY}MEM`{so3G}kJ8?n=)?O4itplkxc(_Vb7{sW6^aF-j5$Yg73pQO{#`WLrhG3 zb;6A9&RPV2e|20OR#!7-G1Rg{YC`6-q9CTmqtblXLW+X-1|@nOX-{puMEzIQ0Zv>h zp7<-=!F0~n7;HVyFITVv?Ec2pKbK79$*^9S`Q2EvNoXa177S7{aRv123#T(E@;Z{Y zQib;^S*2FDjmY7P>c^{pf+Y}`Ma~C`fh*V?=y!l2z6Lfak#u#o#N^~QU}DpAG}u$& zv+n8Sje4($S}6VQnG6ZGUKtq>WuWb;`R<#F$lUbwB^Ux60BrT|XZ!$oMq1e+jgLw8 zT04ww$Z2Z;`=cG3Dxc3K;W9-S7dN-g)+LWxrZr2u;v+lTC; zHs-~k^B{J`fYh337eO9kI>#A4X}gvIG{R5Q@GJvj_XaFy_1M%_uv~`$y(0^%iNcH9 z$ga2{EFpshcw*JKP#zxD6cru&-Zpv;c1`O*E$@f0p7nQ)d+`qsqSYXhXsUFSmX_At zy+(|?hJu=%ol;QHcO-+eR525b{38&3AI(tAZmL)g{c8s`&4H$U{Fv)|;*HP4p!)9H zzYV~s^-AN@&VA^C?e-IyJ-`vs-3f}29Ga6l*^f#@MfIUXZ9Zr|^&L}+-gG}qDPAQt z9y)ZIo+)8jTK%=Of`K5YafK7UXi3O4*VW-p;Iq9{9Xangno1y5MM!`^CO`L%l+eiM zsD48}$I$TARO8AzS5!=x37s9!|9&$T>2q~tKPx`|gp-Y%JTPBlRsr7kxBt10y|Ke* zg0f?aMCpoAu`zHsMTxL?BcW`dmpt~kxzdiXF@Qp!!xs32L7(Fk!uQ2$eC50AGoeRA zbDflbyKZMJ52#1OTF@p#r!{*VC-&_&<#R-LN&~pfwG|b|^gN^k7wYZP7WD0J4yMp* z1U7F!CGc>~+)uSErIv9}XE(^FH@0(!Uxo9J#Y7|d+-(h_VS<>A*NpeeNEH=v*)xhA zdzZULUQ&F~f3zNLCNyQ@N-_}C)MU)C)Sj!&v?=w$$Qsa4dgWm?lJRst%-Yx22THlI zT;JtI$4<$FTAc<<;Oqs>^(HGc(EmGoxaEcY0o<3a;E0X*X_hiVj#bUj@agZYk=|I0 zP@-C>zw~`?e0uL65dFXI5!Xrg>A5WG&g*kjKZ*~OiTr(}z zs~MyOt#Q;sh9cRtplGMQM>yReR7<>G*yAVzgn(p@m4TzOq4)MS9PoiHeT&X(-;9?A z{WihrIU>T}{u(Rw72u*d|w z*rwb6AV>k4Oz}rHDTnzUaI(U!*rA((x7&Wi>lZSw+jQfJDVXb}KK>bg@^^K}7rB}S z_w#Ihq(&GWS7ZN|DURiFq--*}t3fyv=Cja*%M4w2ERhRDJr}A(lUZjoyoz$i+KIO_ z{@r|OF8X@;{vB_JVR6A47IFb*LFhSUEJ|0i^DIp5FX?-A12$*#l?TZ$`tJih4~43+>(h8vgYB)}y~AGfo-m6fN-nKy z)jmPA6^Yq+$JxNVe2)>G*q|PRENCfbSM$YWGwXsr!RNRsJuZpPebf^Z1y*i>bE#Zn zE~Eh4T9dNHY?uEjFBD`d=NWo6?T>}LDFW*3Uj2*uUay8+#^=P_H83G;x1)*VAT z8Q%Ajx96gv6w#Phn*-r~!GOj4e_4Rbc`bLP-#x6a4eNJu`!2|d`Ha6+Sa1d#lUZEd z!o6O9^LXUnyZLA5z-Z+&p_5hfR$igeL{d?NdaF_bA@jrcsC60hSC+ffF*4A}uO786 zE$6jZE4N*dj;o%h#gdV+Yx18W>l^V#8SZu?P({Q!p_L59^&vYQ7Ty22nPD9ui@4M~ zQ9Q94LLIzIaXkpx5ow}3N=I9@iN^G3B! z1MKfAw#YyJLn9+Xf2TE=t@^5YZU3xCGBHq&>}R`9N^3|g=T58vnMKHLZ{pxL`Oh(P zfgm}>*v_qF1p)U1cu20GC#r+|9e$6{)ZZSKX-(}{c1kjWC)Jp7Tf5vV#I&OhMN z4J^ksy;&=fc1UA=p@S89rA|(aThzya(FGojYpl>x+Z#JaZg0T0yYIkceMbPu) z$V~vCuRylLIUt)a{td(7#^@?F+$|#CaA#Rtb9g6vxU$^%w*g^xiT`g7)9Z>Yp&s7} z{p-BA{^^j2_=y6Oc9N&-+G0_=a{k*07J5Z_w4}Q2!qYR!uVxX1uS`G8P{Y%{pO;H5 zJiJ39@3^>)Xlh<*xw)RIm62L*9xZQwVH@NO&?!*!Y#04fmQzp|9UD6V?%of!`$3vC znqY@fgDZ=2nkZ5Q-fAWa-~_(A?i9Tm(N_U%<;B_0s<℘qQI`6)+NuyaNQ;f&Z3| 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 literal 0 HcmV?d00001 diff --git a/public/images/social/linkedin/llm-gateway-open-source.svg b/public/images/social/linkedin/llm-gateway-open-source.svg new file mode 100644 index 0000000..776e382 --- /dev/null +++ b/public/images/social/linkedin/llm-gateway-open-source.svg @@ -0,0 +1,183 @@ + + KI im eigenen Serverraum: wenn zu viele Anfragen auf einmal kommen — Anfragen laufen neu über eine Queue, die ein Gateway der Reihe nach an das Modell weitergibt + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + KI im eigenen Serverraum: + wenn zu viele Anfragen + auf einmal kommen + + + + + + + + + + + + + + + + + + diff --git a/resources/css/app.css b/resources/css/app.css index 5385635..0cc3a36 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -118,6 +118,12 @@ display: none !important; } +/* Utilities beat the user-agent rule, so an element carrying both `hidden` and a + display class would render. Anything toggled through the attribute needs this. */ +[hidden] { + display: none !important; +} + /* Tailwind v4 preflight no longer sets a pointer cursor on interactive elements */ @layer base { button:not(:disabled), @@ -407,6 +413,17 @@ body { margin-block-start: 0; } +.news-prose > hr { + height: 0; + border: 0; + border-block-start: 1px solid var(--color-border); + margin-block-start: 2.5rem; +} + +.news-prose > hr + * { + margin-block-start: 2.5rem; +} + .news-prose ul, .news-prose ol { padding-inline-start: 1.5rem; diff --git a/resources/js/app.js b/resources/js/app.js index 98ea3f6..3cfebef 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -4,6 +4,7 @@ import Alpine from '@alpinejs/csp' import focus from '@alpinejs/focus' import { introTabs } from './intro-tabs' +import { languageSuggestion } from './language-suggestion' window.Alpine = Alpine Alpine.plugin(focus) @@ -127,6 +128,8 @@ Alpine.data('combobox', () => ({ Alpine.data('introTabs', introTabs) +Alpine.data('languageSuggestion', languageSuggestion) + Alpine.data('navigation', () => ({ open: false, toggle() { diff --git a/resources/js/language-suggestion.js b/resources/js/language-suggestion.js new file mode 100644 index 0000000..6222181 --- /dev/null +++ b/resources/js/language-suggestion.js @@ -0,0 +1,85 @@ +export const LANGUAGE_SUGGESTION_KEY = 'language-suggestion' + +export const languageSuggestion = () => ({ + init() { + if (!this.consume()) return + if (this.dismissed()) return + + const wanted = this.preferred() + if (wanted === this.$root.dataset.language) return + + const option = this.$root.querySelector(`[data-language="${wanted}"]`) + if (!option) return + + option.hidden = false + this.$root.hidden = false + }, + + dismiss() { + this.$root.hidden = true + + try { + window.localStorage.setItem(LANGUAGE_SUGGESTION_KEY, '1') + } catch { + /* empty */ + } + }, + + /** + * The language of the page this visitor would rather be on: the first of + * their accepted languages the site actually speaks, and the fallback + * locale when it speaks none of them. + */ + preferred() { + const offered = [ + this.$root.dataset.language, + ...Array.from(this.$root.querySelectorAll('[data-language]')).map((option) => option.dataset.language), + ] + + for (const tag of this.accepted()) { + if (typeof tag !== 'string') continue + + const primary = tag.toLowerCase().split('-')[0] + if (offered.includes(primary)) return primary + } + + return this.$root.dataset.fallback + }, + + accepted() { + const languages = window.navigator.languages + + if (Array.isArray(languages) && languages.length > 0) return languages + + return [window.navigator.language] + }, + + /** + * Reads the marker the entry redirect leaves behind and clears it in the + * same breath: the suggestion belongs to that one arrival, not to every + * later visit to the start page. + */ + consume() { + const name = this.$root.dataset.cookie + + const found = document.cookie.split(';').some((pair) => { + const [key, ...value] = pair.trim().split('=') + + return key === name && value.join('=') !== '' + }) + + if (found) { + document.cookie = `${name}=; Max-Age=0; path=/` + } + + return found + }, + + dismissed() { + try { + return window.localStorage.getItem(LANGUAGE_SUGGESTION_KEY) !== null + } catch { + return false + } + }, +}) diff --git a/resources/views/app/news/show.blade.php b/resources/views/app/news/show.blade.php index fb8fbff..05e92e6 100644 --- a/resources/views/app/news/show.blade.php +++ b/resources/views/app/news/show.blade.php @@ -25,7 +25,7 @@ @endforeach -

+

@if($news->published_at)

+@endif diff --git a/tests/Feature/Components/LanguageSuggestionTest.php b/tests/Feature/Components/LanguageSuggestionTest.php new file mode 100644 index 0000000..d56ed6c --- /dev/null +++ b/tests/Feature/Components/LanguageSuggestionTest.php @@ -0,0 +1,113 @@ +assertOk(); + + return (string) $response->getContent(); +} + +function suggestionMarkup(string $locale): string +{ + return Str::of(startPageMarkup($locale)) + ->after('x-data="languageSuggestion"') + ->before('toString(); +} + +function suggestionCopy(string $key, string $locale): string +{ + $text = __('components.language_suggestion.'.$key, [], $locale); + + return is_string($text) ? $text : throw new RuntimeException("Missing copy: {$key} ({$locale})"); +} + +it('ships the suggestion on the start page of every locale', function (string $locale) { + expect(startPageMarkup($locale))->toContain('x-data="languageSuggestion"'); +})->with([[LocaleEnum::DE->value], [LocaleEnum::EN->value]])->group('language-suggestion'); + +it('hands the component the cookie the entry redirect leaves behind', function () { + expect(suggestionMarkup(LocaleEnum::DE->value)) + ->toContain('data-cookie="'.CookieNameEnum::ENTRY_REDIRECT->value.'"'); +})->group('language-suggestion'); + +it('names the page language and the fallback language', function () { + expect(suggestionMarkup(LocaleEnum::DE->value)) + ->toContain('data-language="de"') + ->toContain('data-fallback="en"'); +})->group('language-suggestion'); + +it('keeps the same fallback language on the English page', function () { + expect(suggestionMarkup(LocaleEnum::EN->value)) + ->toContain('data-language="en"') + ->toContain('data-fallback="en"'); +})->group('language-suggestion'); + +it('offers the English page from the German one', function () { + $markup = suggestionMarkup(LocaleEnum::DE->value); + + expect($markup)->toContain('data-language="en"'); + expect($markup)->toContain(route(Str::slug(LocaleEnum::EN->value).'.start.index')); +})->group('language-suggestion'); + +it('offers the German page from the English one', function () { + $markup = suggestionMarkup(LocaleEnum::EN->value); + + expect($markup)->toContain('data-language="de"'); + expect($markup)->toContain(route(Str::slug(LocaleEnum::DE->value).'.start.index')); +})->group('language-suggestion'); + +it('offers nothing but the other locale', function () { + $markup = suggestionMarkup(LocaleEnum::DE->value); + + expect(substr_count($markup, '
group('language-suggestion'); + +it('writes the offer in the language it is offering, so its reader can read it', function () { + $german = suggestionMarkup(LocaleEnum::DE->value); + + expect($german)->toContain(suggestionCopy('message', LocaleEnum::EN->value)); + expect($german)->toContain(suggestionCopy('action', LocaleEnum::EN->value)); + expect($german)->not->toContain(suggestionCopy('message', LocaleEnum::DE->value)); + + $english = suggestionMarkup(LocaleEnum::EN->value); + + expect($english)->toContain(suggestionCopy('message', LocaleEnum::DE->value)); + expect($english)->not->toContain(suggestionCopy('message', LocaleEnum::EN->value)); +})->group('language-suggestion'); + +it('marks the offer with the language it is written in', function () { + expect(suggestionMarkup(LocaleEnum::DE->value)) + ->toContain('lang="en-CH"') + ->toContain('hreflang="en-CH"'); +})->group('language-suggestion'); + +it('renders hidden, because the cached page cannot know who is reading it', function () { + $markup = suggestionMarkup(LocaleEnum::DE->value); + + expect(Str::before($markup, 'data-language="en"'))->toContain('hidden'); + expect(Str::after($markup, 'data-language="en"'))->toContain('hidden'); +})->group('language-suggestion'); + +it('gives the dismiss control an accessible name', function () { + expect(suggestionMarkup(LocaleEnum::DE->value)) + ->toContain('x-on:click="dismiss"') + ->toContain(suggestionCopy('dismiss', LocaleEnum::EN->value)); +})->group('language-suggestion'); + +it('stays off pages the entry redirect never lands on', function (string $route) { + $response = get(route(Str::slug(LocaleEnum::DE->value).'.'.$route)); + $response->assertOk(); + + expect((string) $response->getContent())->not->toContain('x-data="languageSuggestion"'); +})->with([['about-us.index'], ['contact.index'], ['news.index']])->group('language-suggestion'); diff --git a/tests/Feature/Controllers/EntryIndexControllerTest.php b/tests/Feature/Controllers/EntryIndexControllerTest.php index 0de971d..a49f3b5 100644 --- a/tests/Feature/Controllers/EntryIndexControllerTest.php +++ b/tests/Feature/Controllers/EntryIndexControllerTest.php @@ -2,12 +2,33 @@ declare(strict_types=1); +use App\Enums\CookieNameEnum; use App\Enums\LocaleEnum; +use App\Enums\SessionKeyEnum; use Illuminate\Support\Facades\App; +use Illuminate\Support\Facades\Config; use Illuminate\Support\Str; +use Symfony\Component\HttpFoundation\Cookie; +use Symfony\Component\HttpFoundation\Response; use function Pest\Laravel\get; +function findEntryCookie(Response $response): ?Cookie +{ + foreach ($response->headers->getCookies() as $cookie) { + if ($cookie->getName() === CookieNameEnum::ENTRY_REDIRECT->value) { + return $cookie; + } + } + + return null; +} + +function entryCookie(Response $response): Cookie +{ + return findEntryCookie($response) ?? throw new RuntimeException('The response carries no entry marker.'); +} + it('redirects to the correct localized start page', function () { $locale = LocaleEnum::DE->value; App::setLocale($locale); @@ -18,3 +39,79 @@ $response->assertRedirect($expectedRoute); })->group('entry'); + +it('redirects permanently, whatever the session language', function () { + App::setLocale(LocaleEnum::EN->value); + session([SessionKeyEnum::LANGUAGE->value => LocaleEnum::EN->value]); + + $response = get('/'); + + $response->assertStatus(301); + $response->assertRedirect(route(Str::slug(LocaleEnum::DE->value).'.start.index')); +})->group('entry'); + +it('marks the arrival with a cookie the start page can read', function () { + $cookie = entryCookie(get('/')->baseResponse); + + expect($cookie->getValue())->toBe('1'); + expect($cookie->isHttpOnly())->toBeFalse(); + expect($cookie->getPath())->toBe('/'); + expect($cookie->getSameSite())->toBe('lax'); +})->group('entry'); + +it('keeps the browser from caching the redirect, which would swallow the marker', function () { + $response = get('/'); + + expect($response->headers->get('cache-control'))->toContain('no-store'); +})->group('entry'); + +it('keeps the browser from caching the redirect even once the response cache serves it', function () { + Config::set('responsecache.enabled', true); + + get('/'); + $second = get('/'); + + $second->assertHeader('x-cache-status', 'HIT'); + + expect($second->headers->get('cache-control'))->toContain('no-store'); +})->group('entry'); + +it('leaves the marker unencrypted so JavaScript can read it', function () { + get('/')->assertPlainCookie(CookieNameEnum::ENTRY_REDIRECT->value, '1'); +})->group('entry'); + +it('lets the marker expire within minutes', function () { + $expires = entryCookie(get('/')->baseResponse)->getExpiresTime(); + + expect($expires)->toBeGreaterThan(time()); + expect($expires)->toBeLessThanOrEqual(time() + 600); +})->group('entry'); + +it('keeps handing out the marker once the response cache serves the redirect', function () { + Config::set('responsecache.enabled', true); + + $first = get('/'); + $second = get('/'); + + $second->assertRedirect(route(Str::slug(LocaleEnum::DE->value).'.start.index')); + $second->assertHeader('x-cache-status', 'HIT'); + + expect(entryCookie($first->baseResponse)->getValue())->toBe('1'); + expect(entryCookie($second->baseResponse)->getValue())->toBe('1'); +})->group('entry'); + +it('does not mark a direct visit to a localized start page', function (string $locale) { + $response = get(route(Str::slug($locale).'.start.index')); + + $response->assertOk(); + + expect(findEntryCookie($response->baseResponse))->toBeNull(); +})->with([[LocaleEnum::DE->value], [LocaleEnum::EN->value]])->group('entry'); + +it('does not mark a visit to any other page', function () { + $response = get(route(Str::slug(LocaleEnum::DE->value).'.about-us.index')); + + $response->assertOk(); + + expect(findEntryCookie($response->baseResponse))->toBeNull(); +})->group('entry'); diff --git a/tests/js/language-suggestion.test.js b/tests/js/language-suggestion.test.js new file mode 100644 index 0000000..a412f49 --- /dev/null +++ b/tests/js/language-suggestion.test.js @@ -0,0 +1,321 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { LANGUAGE_SUGGESTION_KEY, languageSuggestion } from '../../resources/js/language-suggestion.js' + +const COOKIE = 'entry_redirect' + +function mount({ language = 'de', fallback = 'en', alternates = ['en'] } = {}) { + document.body.innerHTML = ` + + ` + + const root = document.querySelector('#suggestion') + + const component = languageSuggestion() + component.$root = root + component.init() + + return { component, root } +} + +function visible() { + const root = document.querySelector('#suggestion') + if (!root || root.hidden) return null + + const shown = Array.from(root.querySelectorAll('[data-language]')).filter((option) => !option.hidden) + + return shown.map((option) => option.dataset.language) +} + +function setEntryCookie(value = '1') { + document.cookie = `${COOKIE}=${value}; path=/` +} + +function speaks(...languages) { + vi.spyOn(window.navigator, 'languages', 'get').mockReturnValue(languages) + vi.spyOn(window.navigator, 'language', 'get').mockReturnValue(languages[0]) +} + +function clearCookies() { + document.cookie.split(';').forEach((pair) => { + const name = pair.trim().split('=')[0] + if (name) document.cookie = `${name}=; Max-Age=0; path=/` + }) +} + +beforeEach(() => { + clearCookies() + window.localStorage.clear() +}) + +afterEach(() => { + document.body.innerHTML = '' + vi.restoreAllMocks() +}) + +describe('only after an arrival through the domain root', () => { + it('stays hidden without the entry cookie, however foreign the browser', () => { + speaks('en-US') + + mount() + + expect(visible()).toBeNull() + }) + + it('shows the suggestion when the entry cookie is there', () => { + setEntryCookie() + speaks('en-US') + + mount() + + expect(visible()).toEqual(['en']) + }) + + it('clears the cookie so a later visit to the start page stays quiet', () => { + setEntryCookie() + speaks('en-US') + + mount() + expect(document.cookie).not.toContain(COOKIE) + + document.body.innerHTML = '' + mount() + + expect(visible()).toBeNull() + }) + + it('clears the cookie even when it decides against suggesting anything', () => { + setEntryCookie() + speaks('de-CH') + + mount() + + expect(document.cookie).not.toContain(COOKIE) + }) + + it('ignores a cookie whose name merely ends in the one it looks for', () => { + document.cookie = `not_${COOKIE}=1; path=/` + speaks('en-US') + + mount() + + expect(visible()).toBeNull() + }) + + it('ignores an emptied cookie', () => { + setEntryCookie('') + speaks('en-US') + + mount() + + expect(visible()).toBeNull() + }) + + it('finds the cookie among others', () => { + document.cookie = 'first=a; path=/' + setEntryCookie() + document.cookie = 'last=z; path=/' + speaks('en-US') + + mount() + + expect(visible()).toEqual(['en']) + }) +}) + +describe('choosing the language to suggest', () => { + beforeEach(() => { + setEntryCookie() + }) + + it('says nothing to a German browser, which already has the page it wants', () => { + speaks('de-CH', 'de') + + mount() + + expect(visible()).toBeNull() + }) + + it('suggests English to an English browser', () => { + speaks('en-GB') + + mount() + + expect(visible()).toEqual(['en']) + }) + + it('reads the region off the tag, whatever its case', () => { + speaks('EN-us') + + mount() + + expect(visible()).toEqual(['en']) + }) + + it('falls back to English for a language the site does not speak', () => { + speaks('fr-CH') + + mount() + + expect(visible()).toEqual(['en']) + }) + + it('honours the browser order and stays put when German outranks English', () => { + speaks('de-CH', 'en-US') + + mount() + + expect(visible()).toBeNull() + }) + + it('honours the browser order and suggests English when English outranks German', () => { + speaks('en-US', 'de-CH') + + mount() + + expect(visible()).toEqual(['en']) + }) + + it('skips languages the site does not speak to reach one it does', () => { + speaks('fr-CH', 'it-CH', 'de-CH') + + mount() + + expect(visible()).toBeNull() + }) + + it('falls back to the plain tag when the browser exposes no list', () => { + vi.spyOn(window.navigator, 'languages', 'get').mockReturnValue(undefined) + vi.spyOn(window.navigator, 'language', 'get').mockReturnValue('en') + + mount() + + expect(visible()).toEqual(['en']) + }) + + it('falls back to the plain tag when the list is empty', () => { + vi.spyOn(window.navigator, 'languages', 'get').mockReturnValue([]) + vi.spyOn(window.navigator, 'language', 'get').mockReturnValue('de-CH') + + mount() + + expect(visible()).toBeNull() + }) + + it('falls back to English when the browser exposes no language at all', () => { + vi.spyOn(window.navigator, 'languages', 'get').mockReturnValue([]) + vi.spyOn(window.navigator, 'language', 'get').mockReturnValue(undefined) + + expect(() => mount()).not.toThrow() + + expect(visible()).toEqual(['en']) + }) + + it('suggests nothing on the fallback page itself to a browser it cannot place', () => { + speaks('fr-CH') + + mount({ language: 'en', alternates: ['de'] }) + + expect(visible()).toBeNull() + }) + + it('suggests German on the English page to a German browser', () => { + speaks('de-CH') + + mount({ language: 'en', alternates: ['de'] }) + + expect(visible()).toEqual(['de']) + }) + + it('reveals only the matching language when several are offered', () => { + speaks('fr-FR') + + mount({ language: 'de', alternates: ['en', 'fr'] }) + + expect(visible()).toEqual(['fr']) + }) +}) + +describe('dismissing', () => { + it('hides the suggestion and remembers the choice', () => { + setEntryCookie() + speaks('en-US') + + const { component, root } = mount() + component.dismiss() + + expect(root.hidden).toBe(true) + expect(window.localStorage.getItem(LANGUAGE_SUGGESTION_KEY)).toBe('1') + }) + + it('stays away on the next arrival through the domain root', () => { + window.localStorage.setItem(LANGUAGE_SUGGESTION_KEY, '1') + setEntryCookie() + speaks('en-US') + + mount() + + expect(visible()).toBeNull() + }) + + it('still consumes the cookie once dismissed', () => { + window.localStorage.setItem(LANGUAGE_SUGGESTION_KEY, '1') + setEntryCookie() + speaks('en-US') + + mount() + + expect(document.cookie).not.toContain(COOKIE) + }) + + it('keeps working when storage is blocked', () => { + vi.spyOn(window.localStorage.__proto__, 'getItem').mockImplementation(() => { + throw new Error('access denied') + }) + vi.spyOn(window.localStorage.__proto__, 'setItem').mockImplementation(() => { + throw new Error('access denied') + }) + setEntryCookie() + speaks('en-US') + + let mounted + expect(() => { + mounted = mount() + }).not.toThrow() + + expect(visible()).toEqual(['en']) + expect(() => mounted.component.dismiss()).not.toThrow() + expect(mounted.root.hidden).toBe(true) + }) +}) + +describe('markup it cannot act on', () => { + it('stays hidden when the page offers no alternate at all', () => { + setEntryCookie() + speaks('en-US') + + mount({ alternates: [] }) + + expect(visible()).toBeNull() + }) + + it('stays hidden when the fallback language has no link on the page', () => { + setEntryCookie() + speaks('fr-CH') + + mount({ language: 'de', fallback: 'it', alternates: ['en'] }) + + expect(visible()).toBeNull() + }) +}) From 6812a1bb7dd176db722aab5196942f9da682d80a Mon Sep 17 00:00:00 2001 From: "Sebastian BURGIN-FIX (ext)" Date: Sat, 1 Aug 2026 12:25:58 +0200 Subject: [PATCH 6/6] wip --- .../2026-07-31-llm-gateway-open-source.md | 22 +++++++++---------- .../2026-07-31-llm-gateway-open-source.md | 18 +++++++-------- 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/database/files/news/de_CH/2026-07-31-llm-gateway-open-source.md b/database/files/news/de_CH/2026-07-31-llm-gateway-open-source.md index 4d2293e..e1b85e8 100644 --- a/database/files/news/de_CH/2026-07-31-llm-gateway-open-source.md +++ b/database/files/news/de_CH/2026-07-31-llm-gateway-open-source.md @@ -26,9 +26,9 @@ ausprobiert, einiges wieder verworfen und ein paar Dinge gefunden, die im Alltag Prototypen entstehen schneller, aus Belegen werden Daten, ohne dass jemand sie abtippt, und beim Programmieren schreiben wir weniger selbst und prüfen dafür mehr. -Eine Erfahrung zieht sich für uns durch alles: Einen erheblichen Mehrwert und echte -Effizienzgewinne bringt KI dort, wo die Prozesse schon sauber sind. Wo sie es nicht sind, wird die -Qualität auch mit KI nicht besser. +Eine Erfahrung zieht sich für uns durch alles: Erheblichen Mehrwert und echte Effizienzgewinne +bringt KI dort, wo die Prozesse sauber etabliert sind und die Datenqualität stimmt. Fehlt diese +Grundlage, werden Qualität und Effizienz auch mit KI nur bedingt besser – und selten dauerhaft. Davon zeigen wir in den nächsten Wochen mehr. Den Anfang macht ein Werkzeug, das wir für uns selbst gebaut haben – weil unsere eigene Hardware an die Grenze kam. @@ -40,11 +40,11 @@ auf der unserer Kunden. Kundendaten, Verträge und interne Dokumente bleiben dam ohnehin liegen. Welches Modell rechnet, ist für uns deshalb keine rein technische Frage. Es muss zum Kunden passen und dazu, wie er mit LLMs umgehen will. -Unser Eindruck: Lokal hiess lange auch spürbar schwächer. Das hat sich für unsere Anwendungsfälle -geändert. Inzwischen reichen lokale Modelle für die meisten unserer Prozesse, und dabei gilt oft: -weniger ist mehr. Ein kleines Modell mit präziser Instruktion kommt bei uns häufig weiter als ein -grosses ohne – auch weiter als Claude oder die Modelle von OpenAI – und ist dabei deutlich -effizienter. Nicht immer, aber öfter, als wir erwartet hätten. +Unser Eindruck: Lange arbeiteten lokale Modelle Instruktionen zu unzuverlässig ab. Für unsere +Anwendungsfälle hat sich das geändert. In den meisten unserer Prozesse halten sie sich heute +verlässlich an das, was wir ihnen vorgeben, und dabei gilt oft: weniger ist mehr. Ein kleines +Modell mit präziser Instruktion kommt bei uns häufig weiter als ein grosses ohne und ist dabei +deutlich effizienter. Dafür steht bei uns ein MacBook im Serverraum. M5 Max, 128 GB Arbeitsspeicher, darauf Ollama und LiteLLM – und Zugriff für alle im Unternehmen. @@ -79,10 +79,8 @@ etwas sehr Einfaches: Es nimmt jede Anfrage entgegen, gibt sofort eine Nummer zu sich die Anfrage später wiederfinden lässt, und stellt sie in eine Warteschlange. Abgearbeitet wird sie, sobald das Modell frei ist. Die Antwort holt man anschliessend über die Nummer ab. -Zwei Dinge waren uns dabei wichtig. Erstens sollte sich am Absenden nichts ändern: Wer bisher -direkt ans Modell geschickt hat, schickt genau gleich weiter, und nur das Abholen der Antwort -kommt als zweiter Schritt dazu. Zweitens kann niemand die Warteschlange umgehen – sonst hätten wir -das Problem gleich wieder. +Wichtig war uns dabei, dass sich am Absenden nichts ändert: Wer bisher direkt ans Modell geschickt +hat, schickt genau gleich weiter, und nur das Abholen der Antwort kommt als zweiter Schritt dazu. Mitgeliefert wird die Position in der Warteschlange und eine Schätzung der Wartezeit, hochgerechnet aus den letzten tatsächlichen Antwortzeiten desselben Modells. Keine Zusage, aber diff --git a/database/files/news/en_CH/2026-07-31-llm-gateway-open-source.md b/database/files/news/en_CH/2026-07-31-llm-gateway-open-source.md index 9bf964e..b08d308 100644 --- a/database/files/news/en_CH/2026-07-31-llm-gateway-open-source.md +++ b/database/files/news/en_CH/2026-07-31-llm-gateway-open-source.md @@ -27,8 +27,8 @@ together faster, receipts turn into data without anyone typing them out, and whe write less ourselves and review more instead. One experience runs through all of it, at least for us: AI delivers real value and genuine -efficiency gains where the processes are already sound. Where they are not, AI will not make the -quality any better either. +efficiency gains where processes are properly established and the data quality holds up. Without +that foundation, quality and efficiency improve only so far with AI – and rarely for long. We will be showing more of this over the coming weeks. First up is a tool we built for ourselves, because our own hardware reached its limit. @@ -40,11 +40,10 @@ Customer data, contracts and internal documents stay where they already are. Whi computing is therefore not a purely technical question for us. It has to suit the customer and the way they want to work with LLMs. -Our impression: for a long time, local also meant noticeably weaker. For our use cases, that has -changed. Local models are now sufficient for most of our processes, and here less is often more. A -small model with a precise instruction frequently gets us further than a large one without – -further, too, than Claude or the models from OpenAI – while being considerably more efficient. Not -always, but more often than we expected. +Our impression: for a long time, local models followed instructions too unreliably. For our use +cases, that has changed. In most of our processes they now hold reliably to what we give them, and +here less is often more. A small model with a precise instruction frequently gets us further than +a large one without, while being considerably more efficient. For this we have a MacBook sitting in the server room. M5 Max, 128 GB of memory, running Ollama and LiteLLM – and open to everyone in the company. @@ -79,10 +78,9 @@ very simple: it accepts every request, immediately returns a number that the req identified by, and puts it in a queue. It gets processed as soon as the model is free. The answer is then collected using that number. -Two things mattered to us here. First, nothing about sending should change: whoever used to send +What mattered to us here was that nothing about sending should change: whoever used to send straight to the model sends exactly the same thing as before, and only collecting the answer is -added as a second step. Second, nobody can bypass the queue – otherwise we would be right back -where we started. +added as a second step. Along with the number come the position in the queue and an estimate of the waiting time, extrapolated from the most recent actual response times of the same model. Not a promise, but a