feat(20-windows-client): aplikacja C# WinForms do zdalnego druku etykiet

- System tray app z NotifyIcon + ContextMenuStrip
- Polling API orderPRO (GET /api/print/jobs/pending)
- Pobieranie etykiet PDF i druk przez PdfiumViewer
- Formularz ustawień: URL API, klucz, drukarka, interwał
- Okno logów z historią (ciemny motyw, Consolas)
- Self-contained .NET 8 publish (win-x64)
- Milestone v0.7 Zdalne drukowanie etykiet — COMPLETE

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-22 22:49:28 +01:00
parent 02d06298ea
commit 5fef42ba12
1003 changed files with 5928 additions and 24 deletions

View File

@@ -0,0 +1,324 @@
---
phase: 20-windows-client
plan: 01
type: execute
wave: 1
depends_on: ["18-01", "19-01"]
files_modified:
- clients/windows/OrderPROPrint/OrderPROPrint.sln
- clients/windows/OrderPROPrint/OrderPROPrint.csproj
- clients/windows/OrderPROPrint/Program.cs
- clients/windows/OrderPROPrint/TrayApplicationContext.cs
- clients/windows/OrderPROPrint/Services/PrintApiClient.cs
- clients/windows/OrderPROPrint/Services/PrintService.cs
- clients/windows/OrderPROPrint/Services/PollingService.cs
- clients/windows/OrderPROPrint/Forms/SettingsForm.cs
- clients/windows/OrderPROPrint/Forms/SettingsForm.Designer.cs
- clients/windows/OrderPROPrint/Models/PrintJob.cs
- clients/windows/OrderPROPrint/Models/AppSettings.cs
- clients/windows/OrderPROPrint/Properties/Resources.resx
- clients/windows/OrderPROPrint/app.config
autonomous: false
---
<objective>
## Goal
Stworzyć aplikację C# WinForms działającą w system tray, która odpytuje API orderPRO o zlecenia wydruku, pobiera etykiety i drukuje je na drukarce termicznej Xprinter XP-420B (format A6).
## Purpose
Użytkownik może zlecić wydruk etykiety z przeglądarki (faza 19) i etykieta automatycznie wydrukuje się na drukarce podłączonej do jego komputera — bez ręcznego pobierania pliku i drukowania.
## Output
- Aplikacja WinForms (.NET 8) w `clients/windows/OrderPROPrint/`
- System tray icon z menu kontekstowym
- Polling API co N sekund (konfigurowalne)
- Automatyczny druk etykiet A6 na wybranej drukarce
- Formularz ustawień (URL API, klucz API, drukarka, interwał)
</objective>
<context>
## Project Context
@.paul/PROJECT.md
@.paul/ROADMAP.md
@.paul/STATE.md
## Prior Work
@.paul/phases/18-print-queue-backend/18-01-SUMMARY.md
- API endpoints (API key auth via X-Api-Key header, SHA-256):
- GET /api/print/jobs/pending — lista zleceń do wydruku (JSON array)
- GET /api/print/jobs/{id}/download — pobieranie pliku etykiety (binary)
- POST /api/print/jobs/{id}/complete — oznaczenie jako wydrukowane
- Auth: header X-Api-Key z raw key (serwer hashuje SHA-256 i porównuje)
@.paul/phases/19-ui-integration/19-01-SUMMARY.md
- UI tworzy print jobs przez POST /api/print/jobs i /api/print/jobs/bulk
- Status "pending" = gotowe do pobrania przez Windows Client
## Source Files
@src/Modules/Printing/PrintApiController.php — API endpoints consumed by this client
@src/Modules/Printing/ApiKeyMiddleware.php — auth pattern (X-Api-Key header)
</context>
<skills>
## Required Skills (from SPECIAL-FLOWS.md)
| Skill | Priority | When to Invoke | Loaded? |
|-------|----------|----------------|---------|
| sonar-scanner | required | Po APPLY, przed UNIFY | ○ |
## Skill Invocation Checklist
- [ ] sonar-scanner loaded (run before UNIFY) — uwaga: skanuje tylko PHP, C# poza zakresem
</skills>
<acceptance_criteria>
## AC-1: Aplikacja uruchamia się w system tray
```gherkin
Given aplikacja OrderPROPrint jest uruchomiona
When użytkownik patrzy na system tray (obszar powiadomień)
Then widzi ikonę aplikacji OrderPROPrint
And po kliknięciu prawym przyciskiem widzi menu: "Ustawienia", "Wstrzymaj/Wznów", "O programie", "Zamknij"
And dwuklik otwiera formularz ustawień
```
## AC-2: Konfiguracja połączenia z API
```gherkin
Given użytkownik otworzył formularz ustawień
When wpisuje URL API (np. https://orderpro.projectpro.pl), klucz API i wybiera drukarkę
Then ustawienia zapisują się w pliku konfiguracyjnym (app.config lub JSON)
And po ponownym uruchomieniu ustawienia są zapamiętane
And przycisk "Testuj połączenie" weryfikuje poprawność URL + klucza
```
## AC-3: Polling i pobieranie zleceń
```gherkin
Given aplikacja ma poprawną konfigurację API
When polling timer odpytuje GET /api/print/jobs/pending
Then pobiera listę zleceń ze statusem "pending"
And dla każdego zlecenia pobiera etykietę GET /api/print/jobs/{id}/download
And po wydruku oznacza POST /api/print/jobs/{id}/complete
And ikona tray pokazuje liczbę przetworzonych zleceń (tooltip lub balloon)
```
## AC-4: Drukowanie etykiety A6 na drukarce termicznej
```gherkin
Given pobrano plik etykiety (PDF)
When aplikacja wysyła do wybranej drukarki
Then etykieta drukuje się w formacie A6 (105×148mm)
And orientacja i marginesy są poprawne dla drukarki termicznej
And w razie błędu druku zlecenie nie jest oznaczane jako complete
```
## AC-5: Obsługa błędów i odporność
```gherkin
Given aplikacja jest uruchomiona
When API jest niedostępne lub klucz nieprawidłowy
Then ikona tray zmienia się na stan "error" (np. czerwona)
And tooltip pokazuje ostatni błąd
And polling kontynuuje próby (nie crash)
And po przywróceniu połączenia wraca do normalnego działania
```
</acceptance_criteria>
<tasks>
<task type="auto">
<name>Task 1: Projekt C# WinForms + system tray + ustawienia</name>
<files>
clients/windows/OrderPROPrint/OrderPROPrint.sln,
clients/windows/OrderPROPrint/OrderPROPrint.csproj,
clients/windows/OrderPROPrint/Program.cs,
clients/windows/OrderPROPrint/TrayApplicationContext.cs,
clients/windows/OrderPROPrint/Forms/SettingsForm.cs,
clients/windows/OrderPROPrint/Forms/SettingsForm.Designer.cs,
clients/windows/OrderPROPrint/Models/AppSettings.cs,
clients/windows/OrderPROPrint/Properties/Resources.resx
</files>
<action>
1. Utwórz projekt .NET 8 WinForms w `clients/windows/OrderPROPrint/`:
- `dotnet new winforms -n OrderPROPrint`
- Target: net8.0-windows
- NuGet: System.Text.Json (wbudowane), żadnych zewnętrznych zależności
2. Program.cs:
- Application.Run(new TrayApplicationContext())
- Nie pokazuj głównego okna — tylko tray
3. TrayApplicationContext (dziedziczy ApplicationContext):
- NotifyIcon z ikoną (embedded resource lub SystemIcons.Application)
- ContextMenuStrip z pozycjami:
- "Ustawienia" → otwiera SettingsForm
- "Wstrzymaj" / "Wznów" → toggle polling
- separator
- "O programie" → MessageBox z wersją
- "Zamknij" → Application.Exit()
- DoubleClick na ikonę → otwiera SettingsForm
- Tooltip: "OrderPRO Print — Oczekiwanie" (aktualizowany przez PollingService)
4. AppSettings (model):
- ApiUrl (string), ApiKey (string), PrinterName (string), PollIntervalSeconds (int, default 10)
- Zapis/odczyt do JSON: %APPDATA%/OrderPROPrint/settings.json
- Metoda Load() i Save() — statyczne
5. SettingsForm (WinForms designer):
- Pola: txtApiUrl, txtApiKey, cmbPrinter, nudPollInterval
- cmbPrinter: wypełniony z System.Drawing.Printing.PrinterSettings.InstalledPrinters
- Przycisk "Testuj połączenie" — GET /api/print/jobs/pending z podanym URL+key
- Sukces: zielony label "Połączono ✓"
- Błąd: czerwony label z komunikatem
- Przycisk "Zapisz" — AppSettings.Save() + zamknij formularz
- Przycisk "Anuluj" — zamknij bez zapisu
Avoid: Nie używaj WPF ani MAUI — czysty WinForms dla prostoty
Avoid: Nie dodawaj auto-start na tym etapie (to future enhancement)
</action>
<verify>
- `dotnet build` przechodzi bez błędów
- Aplikacja uruchamia się i pojawia się ikona w system tray
- Menu kontekstowe działa
- Formularz ustawień otwiera się i zamyka
- Lista drukarek się ładuje
- Ustawienia zapisują się do %APPDATA%/OrderPROPrint/settings.json
</verify>
<done>AC-1 satisfied (system tray), AC-2 satisfied (konfiguracja)</done>
</task>
<task type="auto">
<name>Task 2: API client + polling + drukowanie</name>
<files>
clients/windows/OrderPROPrint/Services/PrintApiClient.cs,
clients/windows/OrderPROPrint/Services/PrintService.cs,
clients/windows/OrderPROPrint/Services/PollingService.cs,
clients/windows/OrderPROPrint/Models/PrintJob.cs,
clients/windows/OrderPROPrint/TrayApplicationContext.cs
</files>
<action>
1. PrintJob (model):
- Id (int), OrderId (int), PackageId (int), LabelPath (string), Status (string)
- Deserializacja z JSON response GET /api/print/jobs/pending
2. PrintApiClient:
- Constructor: (string apiUrl, string apiKey)
- HttpClient z default header X-Api-Key
- async Task<List<PrintJob>> GetPendingJobsAsync()
- GET {apiUrl}/api/print/jobs/pending
- Deserializuj JSON response → List<PrintJob>
- async Task<byte[]> DownloadLabelAsync(int jobId)
- GET {apiUrl}/api/print/jobs/{jobId}/download
- Return raw bytes (PDF)
- async Task<bool> MarkCompleteAsync(int jobId)
- POST {apiUrl}/api/print/jobs/{jobId}/complete
- Return true jeśli 200
- async Task<bool> TestConnectionAsync()
- GET {apiUrl}/api/print/jobs/pending (sprawdza czy nie 401)
- Obsługa błędów: HttpRequestException, TaskCanceledException (timeout)
3. PrintService:
- static void PrintPdf(byte[] pdfBytes, string printerName)
- Zapisz PDF do temp file → drukuj przez Process.Start z parametrami drukarki
- Alternatywa: użyj System.Drawing.Printing.PrintDocument z renderowaniem PDF
- Preferowane podejście: zapisz temp PDF, użyj SumatraPDF CLI (portable) do silent print:
`SumatraPDF.exe -print-to "PrinterName" -silent label.pdf`
- Jeśli SumatraPDF niedostępny: fallback na ShellExecute "print" verb
- Ustaw rozmiar papieru: A6 (105×148mm) lub custom size z drukarki
- Po wydruku: usuń temp file
4. PollingService:
- Constructor: (PrintApiClient client, PrintService printService, Action<string> onStatusUpdate, Action<string> onError)
- System.Threading.Timer z interwałem z AppSettings
- Na każdy tick:
a. GetPendingJobsAsync()
b. Dla każdego job: DownloadLabelAsync → PrintPdf → MarkCompleteAsync
c. Aktualizuj status (callback onStatusUpdate)
d. Przy błędzie: callback onError, nie przerywaj pętli
- Start() / Stop() / IsRunning property
- Mutex/lock żeby nie nakładały się dwa ticki
5. Integracja w TrayApplicationContext:
- Po załadowaniu ustawień: utwórz PrintApiClient + PollingService
- onStatusUpdate → aktualizuj tooltip ikony ("Wydrukowano 3 etykiety")
- onError → zmień ikonę na error state, tooltip z komunikatem
- "Wstrzymaj" → PollingService.Stop(), zmień tekst menu na "Wznów"
- Przy zmianie ustawień → restart PollingService z nowymi parametrami
Avoid: Nie blokuj UI thread — cała komunikacja HTTP i drukowanie async
Avoid: Nie ignoruj błędów — każdy failed print musi być widoczny
Avoid: Nie oznaczaj jako complete jeśli druk nie powiódł się
</action>
<verify>
- `dotnet build` przechodzi
- Aplikacja łączy się z API (test z prawdziwym kluczem)
- Polling pobiera pending jobs
- Etykieta drukuje się na wskazanej drukarce
- Po wydruku job oznaczany jako complete w UI orderPRO
- Błąd API nie crashuje aplikacji
</verify>
<done>AC-3 satisfied (polling), AC-4 satisfied (drukowanie), AC-5 satisfied (obsługa błędów)</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
Aplikacja Windows OrderPROPrint — system tray, polling API, drukowanie etykiet A6.
</what-built>
<how-to-verify>
1. Otwórz `clients/windows/OrderPROPrint/` w Visual Studio lub uruchom `dotnet run`
2. Sprawdź: ikona pojawia się w system tray
3. Prawy klik → "Ustawienia" → wpisz URL + klucz API + wybierz drukarkę
4. Kliknij "Testuj połączenie" → powinno być "Połączono ✓"
5. Zapisz ustawienia, zamknij formularz
6. W orderPRO: zlecij wydruk etykiety (przycisk "Drukuj" w widoku przesyłki)
7. Poczekaj na polling (domyślnie 10s)
8. Sprawdź: etykieta wydrukowana na drukarce Xprinter
9. Sprawdź: w orderPRO kolejka wydruku → status "completed"
10. Sprawdź: tooltip ikony pokazuje liczbę wydrukowanych
11. Odłącz internet → sprawdź czy ikona zmienia się na "error"
12. Podłącz z powrotem → polling powinien wrócić do normy
</how-to-verify>
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
</task>
</tasks>
<boundaries>
## DO NOT CHANGE
- src/Modules/Printing/* (backend API gotowe z fazy 18-19)
- database/migrations/* (schemat stabilny)
- Endpointy API — klient konsumuje istniejące, nie modyfikuje serwera
## SCOPE LIMITS
- Nie dodawaj auto-start z Windows (future enhancement)
- Nie buduj instalatora MSI/Setup (na razie `dotnet publish`)
- Nie dodawaj auto-update mechanizmu
- Nie modyfikuj kodu PHP — to jest czysty C# project
- Drukowanie tylko PDF — nie konwertuj do ZPL/EPL (drukarki termiczne z Windows driver)
</boundaries>
<verification>
Before declaring plan complete:
- [ ] `dotnet build` przechodzi bez błędów i ostrzeżeń
- [ ] Aplikacja uruchamia się w system tray (brak głównego okna)
- [ ] Formularz ustawień: URL, klucz API, drukarka, interwał
- [ ] "Testuj połączenie" działa z prawdziwym API
- [ ] Polling pobiera pending jobs z API
- [ ] Etykieta PDF drukuje się na wybranej drukarce
- [ ] Po wydruku job oznaczany jako complete
- [ ] Błędy nie crashują aplikacji
- [ ] Tooltip aktualizuje się z informacjami o statusie
- [ ] Brak natywnych alert() — to WinForms, więc MessageBox jest OK
- [ ] Wszystkie acceptance criteria spełnione
</verification>
<success_criteria>
- Wszystkie 5 AC spełnione
- Aplikacja stabilna — działa ciągle w tle bez crash
- Komunikacja z API poprawna (auth, polling, download, complete)
- Druk na drukarce termicznej w formacie A6
- Kod czytelny, SRP, async/await
</success_criteria>
<output>
After completion, create `.paul/phases/20-windows-client/20-01-SUMMARY.md`
</output>

View File

@@ -0,0 +1,149 @@
---
phase: 20-windows-client
plan: 01
subsystem: desktop-app
tags: [csharp, winforms, printing, system-tray, pdfium, thermal-printer]
requires:
- phase: 18-print-queue-backend
provides: REST API (pending, download, complete), API key auth
- phase: 19-ui-integration
provides: UI creating print jobs (single + bulk)
provides:
- Windows tray application polling API and printing labels
- Self-contained .exe with PdfiumViewer for PDF rendering
affects: []
tech-stack:
added: [".NET 8 WinForms", "PdfiumViewer 2.13.0", "pdfium native x64"]
patterns: [polling-service, tray-application-context, synchronization-context-ui-updates]
key-files:
created:
- clients/windows/OrderPROPrint/OrderPROPrint.sln
- clients/windows/OrderPROPrint/OrderPROPrint.csproj
- clients/windows/OrderPROPrint/Program.cs
- clients/windows/OrderPROPrint/TrayApplicationContext.cs
- clients/windows/OrderPROPrint/Models/AppSettings.cs
- clients/windows/OrderPROPrint/Models/PrintJob.cs
- clients/windows/OrderPROPrint/Services/PrintApiClient.cs
- clients/windows/OrderPROPrint/Services/PrintService.cs
- clients/windows/OrderPROPrint/Services/PollingService.cs
- clients/windows/OrderPROPrint/Forms/SettingsForm.cs
- clients/windows/OrderPROPrint/Forms/SettingsForm.Designer.cs
- clients/windows/OrderPROPrint/Forms/LogForm.cs
- clients/windows/OrderPROPrint/Forms/LogForm.Designer.cs
key-decisions:
- "PdfiumViewer instead of SumatraPDF — zero external dependencies, native PDF rendering"
- "Directory publish instead of single-file — pdfium.dll native not compatible with PublishSingleFile"
- "SynchronizationContext.Post for thread-safe tray icon updates from polling timer"
- "Option C (single client) — multi-client support deferred"
patterns-established:
- "C# desktop app lives in clients/windows/ subdirectory of orderPRO repo"
- "PendingJobsResponse wrapper for API JSON deserialization ({jobs: [...]})"
duration: ~2h
started: 2026-03-22T20:30:00Z
completed: 2026-03-22T22:45:00Z
---
# Phase 20 Plan 01: Windows Client Summary
**Aplikacja C# WinForms w system tray — polling API orderPRO, automatyczne pobieranie i drukowanie etykiet PDF na drukarce termicznej via PdfiumViewer.**
## Performance
| Metric | Value |
|--------|-------|
| Duration | ~2h |
| Started | 2026-03-22 |
| Completed | 2026-03-22 |
| Tasks | 3 (2 auto + 1 checkpoint) |
| Files created | 13 |
## Acceptance Criteria Results
| Criterion | Status | Notes |
|-----------|--------|-------|
| AC-1: Aplikacja w system tray | Pass | NotifyIcon + ContextMenuStrip z Ustawienia, Logi, Wstrzymaj, O programie, Zamknij |
| AC-2: Konfiguracja API | Pass | SettingsForm z URL, klucz, drukarka, interwał + Test połączenia |
| AC-3: Polling i pobieranie | Pass | Timer co N sekund, download label, mark complete |
| AC-4: Drukowanie A6 | Pass | PdfiumViewer CreatePrintDocument z PaperSize A6 (105x148mm) |
| AC-5: Obsługa błędów | Pass | Error icon, tooltip, logi — polling kontynuuje |
## Accomplishments
- Standalone .exe (self-contained .NET 8, ~170MB z runtime)
- System tray z menu kontekstowym i dwuklikiem na ustawienia
- Polling z konfigurowalnym interwałem (5-300s)
- PdfiumViewer renderuje PDF i drukuje bez zewnętrznych narzędzi
- Okno logów z historią (ciemny motyw, Consolas, kopiowanie)
- Thread-safe UI updates via SynchronizationContext
- Settings w %APPDATA%/OrderPROPrint/settings.json
## Deviations from Plan
### Summary
| Type | Count | Impact |
|------|-------|--------|
| Scope additions | 1 | Okno logów (request użytkownika) |
| Auto-fixed | 3 | JSON deserialization, pdfium.dll, NotifyIcon API |
**Total impact:** Lepsza diagnostyka dzięki logom, stabilniejszy druk.
### Auto-fixed Issues
**1. JSON deserialization mismatch**
- **Issue:** API zwraca `{"jobs": [...]}`, klient deserializował jako `List<PrintJob>`
- **Fix:** Dodano `PendingJobsResponse` wrapper, poprawiono property names
- **Files:** Models/PrintJob.cs, Services/PrintApiClient.cs
**2. pdfium.dll missing in publish**
- **Issue:** `PublishSingleFile` nie dołącza natywnej DLL z NuGet
- **Fix:** Directory publish + MSBuild target `CopyPdfiumNative`
- **Files:** OrderPROPrint.csproj
**3. NotifyIcon API incompatibility**
- **Issue:** `NotifyIcon.IsDisposed` i `.Invoke()` nie istnieją w .NET 8
- **Fix:** SynchronizationContext.Post + własny `_isDisposed` flag
- **Files:** TrayApplicationContext.cs
### Scope Additions
**1. Okno logów (LogForm)**
- **Requested by:** użytkownik podczas testów
- **Purpose:** diagnostyka błędów druku
- **Files:** Forms/LogForm.cs, Forms/LogForm.Designer.cs
## Decisions Made
| Decision | Rationale | Impact |
|----------|-----------|--------|
| PdfiumViewer zamiast SumatraPDF | Zero external deps, native rendering | Większy .exe ale prostsze wdrożenie |
| Directory publish zamiast single-file | pdfium.dll native incompatible | Cały folder publish/ do skopiowania |
| Opcja C — jeden klient | Multi-client wymaga zmian backendu | Dla wielu komputerów trzeba dodać api_key_id |
| .NET 8 SDK zainstalowany via winget | Potrzebny do budowania projektu | Nowa zależność dev na maszynie |
## SonarQube Scan
**Not applicable** — sonar-scanner skonfigurowany tylko dla PHP. Projekt C# jest poza zakresem skanowania.
## Next Phase Readiness
**Ready:**
- Milestone v0.7 kompletny — wszystkie 3 fazy zakończone
- Pełny flow: zlecenie z UI → API → Windows Client → drukarka
**Concerns:**
- Multi-client: wielu klientów drukuje duplikaty (opcja A/B do rozważenia w przyszłości)
- Rozmiar .exe: 170MB (self-contained z runtime .NET)
**Blockers:**
- None
---
*Phase: 20-windows-client, Plan: 01*
*Completed: 2026-03-22*