ThysToys Logistics¶
Overzicht¶
Het ThysToys Logistieke portaal is een webapplicatie voor het uitscannen van bestellingen in het magazijn van ThysToys. Het systeem bepaalt automatisch de juiste vervoerder en genereert verzendlabels voor de bestellingen.
Doel van het portaal
Het logistics portaal stroomlijnt het verzendproces door:
- Automatische selectie van de juiste vervoerder op basis van productkenmerken
- Genereren van verzendlabels via barcode scanning
- Real-time koppeling met FileMaker voor productinformatie
- Ondersteuning voor meerdere vervoerders (PostNL, DPD, Flevobird)
| Aspect | Details |
|---|---|
| Type | Webapplicatie |
| Repository | thystoys/thystoys-logistics |
Servers¶
ThysToys maakt gebruik van 2 fysieke Intel NUC servers die als fail-over configuratie zijn opgezet. Mocht één server uitvallen, dan kan de andere direct worden ingezet.
Status: Actief in productie
Deze server draait momenteel de productieomgeving van ThysToys Logistics.
Status: Passief
Locatie: Fysiek op kantoor bij Flooris
Deze server ligt stand-by als fail-over en kan worden ingezet bij problemen met TY07WEB002.
Server Provisioning
De volledige server provisioning documentatie is beschikbaar in Notion:
Vervoerders¶
Het systeem ondersteunt meerdere vervoerders die automatisch worden geselecteerd op basis van product- en bestelkenmerken.
| Vervoerder | Gebruik | Selectie logica |
|---|---|---|
| PostNL | Standaard vervoerder | Standaard voor alle reguliere bestellingen |
| DPD | Internationale verzending | Bestellingen naar Duitsland |
| Flevobird | Speciaal transport | Groot speelgoed dat niet met reguliere vervoerders kan |
Vervoerder logica
De vervoerder wordt bepaald door logica in FileMaker op productniveau, vanuit het Product Management systeem.
Omgevingen¶
Local Development¶
Container platform: OrbStack
URL: http://logistics.thystoys.localhost
# Start local development environment
docker-compose up -d
Productie¶
Omgevingscode: TTLOG Prod
URL: https://logistics.thystoys.nl
Netwerk configuratie
Deze hostname resolved naar een Local LAN IP address van de actieve NUC server. De applicatie is alleen toegankelijk vanuit het lokale netwerk van ThysToys.
Health monitoring¶
Health monitoring gaat op een iets minder "standaard" methode, de server is namelijk zelf verantwoordelijk voor het melden van zijn health status bij onze Uptime Kuma instance.
De reden hiervoor is zoals eerder al is aangegeven, de productie server is niet beschikbaar vanaf het publieke internet, waardoor Uptime Kuma dus ook niet bij de applicatie kan komen.
De health check wordt ieder uur uitgevoerd door de Laravel scheduler, hiermee wordt ook direct de health status van de scheduler bepaald.
Tijdens het uitvoeren van ReportHealthStatusJob wordt er bijvoorbeeld een HTTP request uitgevoerd naar de NGINX server op de server zelf, waardoor we toch bepaalde health checks kunnen uitvoeren, ondanks Uptime Kuma er niet zelf bij kan.
Tests in de health check¶
| Test naam | Doel |
|---|---|
testNginxConnection |
Testen of de NGINX leeft, of de applicatie bereikbaar is via NGINX en of de front-end assets beschikbaar zijn |
testDatabaseConnection |
Testen of diverse database tabellen leesbaar zijn, waarmee ook direct de MySQL/MariaDB verbinding getest wordt |
Wat als een health check faalt?¶
Als een health check faalt, zal dit direct gemeld worden aan Uptime Kuma.
Omdat deze niet direct aangeeft wat er fout is maar alleen dat er iets fout is, wordt de health status ook weggeschreven naar de Laravel logs. Hierin is het volgende te zien:
- Simpele health status (Zijn alle checks geslaagd?)
- Geslaagde checks
- Gefaalde checks
- Gefaalde check foutmeldingen + stack trace
Voorbeeld log
[2026-03-25 14:45:11] local.INFO: [ReportHealthStatusJob] Determined isHealthy status: NO (Passed checks: [testDatabaseConnection], Failed checks: [testNginxConnection]) {"failedCheckExceptions":{"testNginxConnection":{"message":"cURL error 7: Failed to connect to thystoys-logistics.localhost port 80 after 0 ms: Could not connect to server (see https://curl.haxx.se/libcurl/c/libcurl-errors.html) for http://thystoys-logistics.localhost/login","trace":[{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Support/helpers.php","line":338,"function":"Illuminate\\Http\\Client\\{closure}","class":"Illuminate\\Http\\Client\\PendingRequest","type":"->","args":[1]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Http/Client/PendingRequest.php","line":905,"function":"retry","args":[0,{"Closure":[]},100,{"Closure":[]}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Http/Client/PendingRequest.php","line":772,"function":"send","class":"Illuminate\\Http\\Client\\PendingRequest","type":"->","args":["GET","http://ttlog-nginx/home",{"json":null}]},{"file":"/var/www/html/app/Jobs/ReportHealthStatusJob.php","line":68,"function":"get","class":"Illuminate\\Http\\Client\\PendingRequest","type":"->","args":["/home"]},{"file":"/var/www/html/app/Jobs/ReportHealthStatusJob.php","line":32,"function":"testNginxConnection","class":"App\\Jobs\\ReportHealthStatusJob","type":"->","args":[]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php","line":36,"function":"handle","class":"App\\Jobs\\ReportHealthStatusJob","type":"->","args":[]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Container/Util.php","line":43,"function":"Illuminate\\Container\\{closure}","class":"Illuminate\\Container\\BoundMethod","type":"::","args":[]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php","line":95,"function":"unwrapIfClosure","class":"Illuminate\\Container\\Util","type":"::","args":[{"Closure":[]}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php","line":35,"function":"callBoundMethod","class":"Illuminate\\Container\\BoundMethod","type":"::","args":[{"Illuminate\\Foundation\\Application":{"contextual":[],"contextualAttributes":[]}},[{"App\\Jobs\\ReportHealthStatusJob":{"job":[],"connection":"sync","queue":null,"delay":null,"afterCommit":null,"middleware":[],"chained":[],"chainConnection":null,"chainQueue":null,"chainCatchCallbacks":null}},"handle"],{"Closure":[]}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Container/Container.php","line":696,"function":"call","class":"Illuminate\\Container\\BoundMethod","type":"::","args":[{"Illuminate\\Foundation\\Application":{"contextual":[],"contextualAttributes":[]}},[{"App\\Jobs\\ReportHealthStatusJob":{"job":[],"connection":"sync","queue":null,"delay":null,"afterCommit":null,"middleware":[],"chained":[],"chainConnection":null,"chainQueue":null,"chainCatchCallbacks":null}},"handle"],[],null]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php","line":126,"function":"call","class":"Illuminate\\Container\\Container","type":"->","args":[[{"App\\Jobs\\ReportHealthStatusJob":{"job":[],"connection":"sync","queue":null,"delay":null,"afterCommit":null,"middleware":[],"chained":[],"chainConnection":null,"chainQueue":null,"chainCatchCallbacks":null}},"handle"]]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php","line":170,"function":"Illuminate\\Bus\\{closure}","class":"Illuminate\\Bus\\Dispatcher","type":"->","args":[{"App\\Jobs\\ReportHealthStatusJob":{"job":[],"connection":"sync","queue":null,"delay":null,"afterCommit":null,"middleware":[],"chained":[],"chainConnection":null,"chainQueue":null,"chainCatchCallbacks":null}}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php","line":127,"function":"Illuminate\\Pipeline\\{closure}","class":"Illuminate\\Pipeline\\Pipeline","type":"->","args":[{"App\\Jobs\\ReportHealthStatusJob":{"job":[],"connection":"sync","queue":null,"delay":null,"afterCommit":null,"middleware":[],"chained":[],"chainConnection":null,"chainQueue":null,"chainCatchCallbacks":null}}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php","line":130,"function":"then","class":"Illuminate\\Pipeline\\Pipeline","type":"->","args":[{"Closure":[]}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php","line":126,"function":"dispatchNow","class":"Illuminate\\Bus\\Dispatcher","type":"->","args":[{"App\\Jobs\\ReportHealthStatusJob":{"job":[],"connection":"sync","queue":null,"delay":null,"afterCommit":null,"middleware":[],"chained":[],"chainConnection":null,"chainQueue":null,"chainCatchCallbacks":null}},false]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php","line":170,"function":"Illuminate\\Queue\\{closure}","class":"Illuminate\\Queue\\CallQueuedHandler","type":"->","args":[{"App\\Jobs\\ReportHealthStatusJob":{"job":[],"connection":"sync","queue":null,"delay":null,"afterCommit":null,"middleware":[],"chained":[],"chainConnection":null,"chainQueue":null,"chainCatchCallbacks":null}}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Pipeline/Pipeline.php","line":127,"function":"Illuminate\\Pipeline\\{closure}","class":"Illuminate\\Pipeline\\Pipeline","type":"->","args":[{"App\\Jobs\\ReportHealthStatusJob":{"job":[],"connection":"sync","queue":null,"delay":null,"afterCommit":null,"middleware":[],"chained":[],"chainConnection":null,"chainQueue":null,"chainCatchCallbacks":null}}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php","line":121,"function":"then","class":"Illuminate\\Pipeline\\Pipeline","type":"->","args":[{"Closure":[]}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Queue/CallQueuedHandler.php","line":69,"function":"dispatchThroughMiddleware","class":"Illuminate\\Queue\\CallQueuedHandler","type":"->","args":[{"Illuminate\\Queue\\Jobs\\SyncJob":[]},{"App\\Jobs\\ReportHealthStatusJob":{"job":[],"connection":"sync","queue":null,"delay":null,"afterCommit":null,"middleware":[],"chained":[],"chainConnection":null,"chainQueue":null,"chainCatchCallbacks":null}}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Queue/Jobs/Job.php","line":102,"function":"call","class":"Illuminate\\Queue\\CallQueuedHandler","type":"->","args":[{"Illuminate\\Queue\\Jobs\\SyncJob":[]},{"commandName":"App\\Jobs\\ReportHealthStatusJob","command":"O:30:\"App\\Jobs\\ReportHealthStatusJob\":1:{s:10:\"connection\";s:4:\"sync\";}"}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Queue/SyncQueue.php","line":76,"function":"fire","class":"Illuminate\\Queue\\Jobs\\Job","type":"->","args":[]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Queue/SyncQueue.php","line":56,"function":"executeJob","class":"Illuminate\\Queue\\SyncQueue","type":"->","args":[{"App\\Jobs\\ReportHealthStatusJob":{"job":null,"connection":"sync","queue":null,"delay":null,"afterCommit":null,"middleware":[],"chained":[],"chainConnection":null,"chainQueue":null,"chainCatchCallbacks":null}},"",null]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php","line":244,"function":"push","class":"Illuminate\\Queue\\SyncQueue","type":"->","args":[{"App\\Jobs\\ReportHealthStatusJob":{"job":null,"connection":"sync","queue":null,"delay":null,"afterCommit":null,"middleware":[],"chained":[],"chainConnection":null,"chainQueue":null,"chainCatchCallbacks":null}},"",null]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php","line":228,"function":"pushCommandToQueue","class":"Illuminate\\Bus\\Dispatcher","type":"->","args":[{"Illuminate\\Queue\\SyncQueue":[]},{"App\\Jobs\\ReportHealthStatusJob":{"job":null,"connection":"sync","queue":null,"delay":null,"afterCommit":null,"middleware":[],"chained":[],"chainConnection":null,"chainQueue":null,"chainCatchCallbacks":null}}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Bus/Dispatcher.php","line":95,"function":"dispatchToQueue","class":"Illuminate\\Bus\\Dispatcher","type":"->","args":[{"App\\Jobs\\ReportHealthStatusJob":{"job":null,"connection":"sync","queue":null,"delay":null,"afterCommit":null,"middleware":[],"chained":[],"chainConnection":null,"chainQueue":null,"chainCatchCallbacks":null}}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Bus/Dispatchable.php","line":76,"function":"dispatchSync","class":"Illuminate\\Bus\\Dispatcher","type":"->","args":[{"App\\Jobs\\ReportHealthStatusJob":{"job":null,"connection":"sync","queue":null,"delay":null,"afterCommit":null,"middleware":[],"chained":[],"chainConnection":null,"chainQueue":null,"chainCatchCallbacks":null}}]},{"file":"/var/www/html/app/Console/Commands/HealthReportHealthStatusCommand.php","line":29,"function":"dispatchSync","class":"App\\Jobs\\ReportHealthStatusJob","type":"::","args":[]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php","line":36,"function":"handle","class":"App\\Console\\Commands\\HealthReportHealthStatusCommand","type":"->","args":[]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Container/Util.php","line":43,"function":"Illuminate\\Container\\{closure}","class":"Illuminate\\Container\\BoundMethod","type":"::","args":[]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php","line":95,"function":"unwrapIfClosure","class":"Illuminate\\Container\\Util","type":"::","args":[{"Closure":[]}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php","line":35,"function":"callBoundMethod","class":"Illuminate\\Container\\BoundMethod","type":"::","args":[{"Illuminate\\Foundation\\Application":{"contextual":[],"contextualAttributes":[]}},[{"App\\Console\\Commands\\HealthReportHealthStatusCommand":[]},"handle"],{"Closure":[]}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Container/Container.php","line":696,"function":"call","class":"Illuminate\\Container\\BoundMethod","type":"::","args":[{"Illuminate\\Foundation\\Application":{"contextual":[],"contextualAttributes":[]}},[{"App\\Console\\Commands\\HealthReportHealthStatusCommand":[]},"handle"],[],null]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Console/Command.php","line":213,"function":"call","class":"Illuminate\\Container\\Container","type":"->","args":[[{"App\\Console\\Commands\\HealthReportHealthStatusCommand":[]},"handle"]]},{"file":"/var/www/html/vendor/symfony/console/Command/Command.php","line":279,"function":"execute","class":"Illuminate\\Console\\Command","type":"->","args":[{"Symfony\\Component\\Console\\Input\\ArgvInput":"'health:report-health-status'"},{"Illuminate\\Console\\OutputStyle":[]}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Console/Command.php","line":182,"function":"run","class":"Symfony\\Component\\Console\\Command\\Command","type":"->","args":[{"Symfony\\Component\\Console\\Input\\ArgvInput":"'health:report-health-status'"},{"Illuminate\\Console\\OutputStyle":[]}]},{"file":"/var/www/html/vendor/symfony/console/Application.php","line":1094,"function":"run","class":"Illuminate\\Console\\Command","type":"->","args":[{"Symfony\\Component\\Console\\Input\\ArgvInput":"'health:report-health-status'"},{"Symfony\\Component\\Console\\Output\\ConsoleOutput":[]}]},{"file":"/var/www/html/vendor/symfony/console/Application.php","line":342,"function":"doRunCommand","class":"Symfony\\Component\\Console\\Application","type":"->","args":[{"App\\Console\\Commands\\HealthReportHealthStatusCommand":[]},{"Symfony\\Component\\Console\\Input\\ArgvInput":"'health:report-health-status'"},{"Symfony\\Component\\Console\\Output\\ConsoleOutput":[]}]},{"file":"/var/www/html/vendor/symfony/console/Application.php","line":193,"function":"doRun","class":"Symfony\\Component\\Console\\Application","type":"->","args":[{"Symfony\\Component\\Console\\Input\\ArgvInput":"'health:report-health-status'"},{"Symfony\\Component\\Console\\Output\\ConsoleOutput":[]}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php","line":198,"function":"run","class":"Symfony\\Component\\Console\\Application","type":"->","args":[{"Symfony\\Component\\Console\\Input\\ArgvInput":"'health:report-health-status'"},{"Symfony\\Component\\Console\\Output\\ConsoleOutput":[]}]},{"file":"/var/www/html/vendor/laravel/framework/src/Illuminate/Foundation/Application.php","line":1235,"function":"handle","class":"Illuminate\\Foundation\\Console\\Kernel","type":"->","args":[{"Symfony\\Component\\Console\\Input\\ArgvInput":"'health:report-health-status'"},{"Symfony\\Component\\Console\\Output\\ConsoleOutput":[]}]},{"file":"/var/www/html/artisan","line":13,"function":"handleCommand","class":"Illuminate\\Foundation\\Application","type":"->","args":[{"Symfony\\Component\\Console\\Input\\ArgvInput":"'health:report-health-status'"}]}]}}}
[2026-03-25 15:06:32] local.INFO: [ReportHealthStatusJob] Determined isHealthy status: YES (Passed checks: [testNginxConnection,testDatabaseConnection], Failed checks: []) {"failedCheckExceptions":[]}
Als de hele scheduler or server down gaat, zal Uptime Kuma ook geen pings meer ontvangen en zal hij na een aantal gemiste pings ook een melding versturen naar de monitoring channels dat de health check/server niet meer lijkt te reageren.
Bekende Issues¶
Bekende issues bij ThysToys Logistics, met een verwijzing naar het bijbehorende KB-artikel voor de volledige oorzaak en oplossing.
| Issue | ClickUp ticket | Datum | KB-artikel |
|---|---|---|---|
| SSL-certificaat foutmelding ("Niet beveiligd") op uitscan PC's | TT-1394 | 27 november 2025 | KB: "Niet beveiligd" SSL foutmelding |
Barcode Cheatsheet¶
Debug Mode in Development
Bij een debug build van de front-end verschijnt een invoerveld waarmee je handmatig barcodes kunt invoeren. Zo heb je tijdens ontwikkeling/testen geen fysieke scanner nodig.
Debug Mode in Productie
Ook in een productie build kun je handmatig barcodes invoeren door de geheime debug code in te voeren:
Code: AABBCC + Enter
Dit activeert het handmatige invoerveld.
Barcode Acties¶
| Actie | Barcode Formaat | Toelichting |
|---|---|---|
| Printer selecteren | PRINTER-{printer_id} |
Selecteer een specifieke printer voor het afdrukken van labels |
| Inloggen | LT-{login_token} |
Login token moet 15 karakters lang zijn |
| Uitloggen | LOGOUT |
Gebruiker uitloggen uit het systeem |
| Label selecteren | LABEL |
Lijkt momenteel niet in gebruik te zijn |
| Order selecteren | {order_token} |
Order token moet 10 karakters lang zijn, geen barcode prefix vereist |
Voorbeeld barcodes
PRINTER-12 # Selecteer printer met ID 12
LT-ABC123456789012 # Login met token (15 chars)
LOGOUT # Uitloggen
AB12345678 # Order selecteren (10 chars)
Veelgestelde Vragen¶
Hoe weet ik welke NUC momenteel actief is?
Je kunt dit controleren door te kijken welke server toegankelijk is via https://logistics.thystoys.nl. Momenteel is TY07WEB002 de actieve server.
Wat te doen bij uitval van de actieve NUC?
- Controleer of TY07WEB001 beschikbaar is
- Update de DNS/network configuratie om naar TY07WEB001 te wijzen
- Raadpleeg de Notion KB voor volledige fail-over procedure
Kan ik de applicatie van buitenaf benaderen?
Nee, de productie URL resolved naar een Local LAN IP address. De applicatie is alleen toegankelijk vanuit het lokale netwerk van ThysToys.