feat(13-email-mailboxes): phase 13 complete — email DB foundation + SMTP mailbox CRUD
3 migrations (email_mailboxes, email_templates, email_logs), full CRUD for SMTP mailboxes with encrypted passwords (IntegrationSecretCipher), native SMTP connection test via stream_socket_client, sidebar navigation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
274
.paul/phases/13-email-mailboxes/13-01-PLAN.md
Normal file
274
.paul/phases/13-email-mailboxes/13-01-PLAN.md
Normal file
@@ -0,0 +1,274 @@
|
||||
---
|
||||
phase: 13-email-mailboxes
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- database/migrations/20260315_000054_create_email_mailboxes_table.sql
|
||||
- database/migrations/20260315_000055_create_email_templates_table.sql
|
||||
- database/migrations/20260315_000056_create_email_logs_table.sql
|
||||
- src/Modules/Settings/EmailMailboxController.php
|
||||
- src/Modules/Settings/EmailMailboxRepository.php
|
||||
- resources/views/settings/email-mailboxes.php
|
||||
- src/Core/Application.php
|
||||
autonomous: false
|
||||
---
|
||||
|
||||
<objective>
|
||||
## Goal
|
||||
Stworzyć fundament bazodanowy modułu e-mail (3 tabele) oraz pełny CRUD skrzynek pocztowych SMTP w sekcji Ustawienia z walidacją połączenia.
|
||||
|
||||
## Purpose
|
||||
Skrzynki pocztowe to fundament wysyłki e-mail — szablony (faza 14) i wysyłka z zamówień (faza 15) zależą od skonfigurowanych skrzynek SMTP.
|
||||
|
||||
## Output
|
||||
- 3 migracje SQL (email_mailboxes, email_templates, email_logs)
|
||||
- CRUD skrzynek pocztowych w Ustawienia > Skrzynki pocztowe
|
||||
- Endpoint testowania połączenia SMTP
|
||||
</objective>
|
||||
|
||||
<context>
|
||||
## Project Context
|
||||
@.paul/PROJECT.md
|
||||
@.paul/ROADMAP.md
|
||||
@.paul/STATE.md
|
||||
|
||||
## Source Files
|
||||
@src/Modules/Settings/ReceiptConfigController.php (wzorzec CRUD w Settings)
|
||||
@src/Modules/Settings/ReceiptConfigRepository.php (wzorzec repository)
|
||||
@src/Modules/Settings/IntegrationSecretCipher.php (szyfrowanie haseł)
|
||||
@resources/views/settings/accounting.php (wzorzec widoku list+edit)
|
||||
@resources/views/layouts/app.php (nawigacja sidebar)
|
||||
@src/Core/Application.php (routing)
|
||||
</context>
|
||||
|
||||
<skills>
|
||||
## Required Skills (from SPECIAL-FLOWS.md)
|
||||
|
||||
| Skill | Priority | When to Invoke | Loaded? |
|
||||
|-------|----------|----------------|---------|
|
||||
| sonar-scanner | required | Po APPLY, przed UNIFY | ○ |
|
||||
|
||||
**BLOCKING:** sonar-scanner musi być uruchomiony przed UNIFY.
|
||||
|
||||
## Skill Invocation Checklist
|
||||
- [ ] sonar-scanner uruchomiony po APPLY
|
||||
</skills>
|
||||
|
||||
<acceptance_criteria>
|
||||
|
||||
## AC-1: Migracje tworzą 3 tabele
|
||||
```gherkin
|
||||
Given baza danych bez tabel email_*
|
||||
When uruchomiona zostanie migracja przez Ustawienia > Baza danych
|
||||
Then tabele email_mailboxes, email_templates, email_logs istnieją z poprawnymi kolumnami i FK
|
||||
```
|
||||
|
||||
## AC-2: CRUD skrzynek pocztowych
|
||||
```gherkin
|
||||
Given użytkownik na stronie /settings/email-mailboxes
|
||||
When dodaje nową skrzynkę (nazwa, serwer, port, użytkownik, hasło)
|
||||
Then skrzynka pojawia się na liście z zaszyfrowanym hasłem w DB
|
||||
And można ją edytować i usunąć
|
||||
```
|
||||
|
||||
## AC-3: Walidacja połączenia SMTP
|
||||
```gherkin
|
||||
Given użytkownik wypełnił formularz skrzynki pocztowej
|
||||
When kliknie "Testuj połączenie"
|
||||
Then system próbuje nawiązać połączenie SMTP z podanymi danymi
|
||||
And wyświetla komunikat sukcesu lub szczegółowy błąd
|
||||
```
|
||||
|
||||
## AC-4: Nawigacja
|
||||
```gherkin
|
||||
Given użytkownik zalogowany do panelu
|
||||
When przechodzi do Ustawienia
|
||||
Then widzi pozycję "Skrzynki pocztowe" w menu Settings
|
||||
And kliknięcie prowadzi do /settings/email-mailboxes
|
||||
```
|
||||
|
||||
</acceptance_criteria>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Migracje SQL — 3 tabele email</name>
|
||||
<files>
|
||||
database/migrations/20260315_000054_create_email_mailboxes_table.sql,
|
||||
database/migrations/20260315_000055_create_email_templates_table.sql,
|
||||
database/migrations/20260315_000056_create_email_logs_table.sql
|
||||
</files>
|
||||
<action>
|
||||
**Tabela `email_mailboxes`:**
|
||||
- id INT UNSIGNED AUTO_INCREMENT PK
|
||||
- name VARCHAR(100) NOT NULL — nazwa wyświetlana (np. "Główna skrzynka")
|
||||
- smtp_host VARCHAR(255) NOT NULL
|
||||
- smtp_port SMALLINT UNSIGNED NOT NULL DEFAULT 587
|
||||
- smtp_encryption ENUM('tls','ssl','none') NOT NULL DEFAULT 'tls'
|
||||
- smtp_username VARCHAR(255) NOT NULL
|
||||
- smtp_password_encrypted TEXT NOT NULL — szyfrowane przez IntegrationSecretCipher
|
||||
- sender_email VARCHAR(255) NOT NULL — adres nadawcy (from)
|
||||
- sender_name VARCHAR(200) DEFAULT NULL — nazwa nadawcy
|
||||
- is_default TINYINT(1) NOT NULL DEFAULT 0
|
||||
- is_active TINYINT(1) NOT NULL DEFAULT 1
|
||||
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
|
||||
**Tabela `email_templates`:**
|
||||
- id INT UNSIGNED AUTO_INCREMENT PK
|
||||
- name VARCHAR(200) NOT NULL — nazwa szablonu
|
||||
- subject VARCHAR(500) NOT NULL — temat e-mail (może zawierać zmienne)
|
||||
- body_html TEXT NOT NULL — treść HTML (Quill output)
|
||||
- mailbox_id INT UNSIGNED DEFAULT NULL — FK do email_mailboxes (nullable = domyślna)
|
||||
- is_active TINYINT(1) NOT NULL DEFAULT 1
|
||||
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
- FOREIGN KEY (mailbox_id) REFERENCES email_mailboxes(id) ON DELETE SET NULL
|
||||
|
||||
**Tabela `email_logs`:**
|
||||
- id BIGINT UNSIGNED AUTO_INCREMENT PK
|
||||
- template_id INT UNSIGNED DEFAULT NULL — FK do email_templates
|
||||
- mailbox_id INT UNSIGNED DEFAULT NULL — FK do email_mailboxes
|
||||
- order_id INT UNSIGNED DEFAULT NULL — FK do orders
|
||||
- recipient_email VARCHAR(255) NOT NULL
|
||||
- recipient_name VARCHAR(200) DEFAULT NULL
|
||||
- subject VARCHAR(500) NOT NULL
|
||||
- body_html TEXT NOT NULL — treść po rozwinięciu zmiennych
|
||||
- attachments_json JSON DEFAULT NULL — lista załączników [{name, path, type}]
|
||||
- status ENUM('sent','failed','pending') NOT NULL DEFAULT 'pending'
|
||||
- error_message TEXT DEFAULT NULL
|
||||
- sent_at DATETIME DEFAULT NULL
|
||||
- created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
- FOREIGN KEY (template_id) REFERENCES email_templates(id) ON DELETE SET NULL
|
||||
- FOREIGN KEY (mailbox_id) REFERENCES email_mailboxes(id) ON DELETE SET NULL
|
||||
- INDEX idx_email_logs_order (order_id)
|
||||
- INDEX idx_email_logs_status (status)
|
||||
- INDEX idx_email_logs_sent_at (sent_at)
|
||||
|
||||
Wszystkie migracje idempotentne (IF NOT EXISTS).
|
||||
</action>
|
||||
<verify>Uruchomić migrację przez /settings/database — tabele widoczne w bazie</verify>
|
||||
<done>AC-1 satisfied: 3 tabele email_* istnieją z poprawnymi kolumnami i FK</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: CRUD skrzynek pocztowych + test SMTP</name>
|
||||
<files>
|
||||
src/Modules/Settings/EmailMailboxController.php,
|
||||
src/Modules/Settings/EmailMailboxRepository.php,
|
||||
resources/views/settings/email-mailboxes.php,
|
||||
src/Core/Application.php,
|
||||
resources/views/layouts/app.php
|
||||
</files>
|
||||
<action>
|
||||
**EmailMailboxRepository** (wzorzec jak ReceiptConfigRepository):
|
||||
- listAll(): array — wszystkie skrzynki (hasło NIE zwracane w liście)
|
||||
- findById(int $id): ?array — z odszyfrowanym hasłem (do edycji)
|
||||
- save(array $data): int — insert/update; hasło szyfrowane IntegrationSecretCipher
|
||||
- delete(int $id): bool — usunięcie skrzynki
|
||||
- findDefault(): ?array — skrzynka z is_default=1
|
||||
|
||||
**EmailMailboxController** (wzorzec jak ReceiptConfigController):
|
||||
- index(Request): Response — lista + formularz edycji (?edit=ID)
|
||||
- save(Request): Response — walidacja + zapis; pola: name, smtp_host, smtp_port, smtp_encryption, smtp_username, smtp_password, sender_email, sender_name, is_default
|
||||
- delete(Request): Response — usunięcie z potwierdzeniem
|
||||
- testConnection(Request): Response — AJAX POST; próba fsockopen/stream_socket_client do smtp_host:smtp_port z timeoutem 5s; odpowiedź JSON {success, message}
|
||||
|
||||
**Walidacja testConnection:**
|
||||
- Odczytaj dane z POST (lub z DB jeśli id podane)
|
||||
- Odszyfruj hasło jeśli z DB
|
||||
- Spróbuj otworzyć socket do smtp_host:smtp_port (stream_socket_client, timeout 5s)
|
||||
- Jeśli OK: EHLO + AUTH LOGIN z username/password
|
||||
- Zwróć JSON: {success: true/false, message: "Połączenie OK" / "Błąd: ..."}
|
||||
- Użyj natywnego PHP (fsockopen/stream_socket_client + SMTP commands) — NIE dodawaj PHPMailer/SwiftMailer w tej fazie
|
||||
|
||||
**Widok email-mailboxes.php** (wzorzec jak accounting.php):
|
||||
- Tabela skrzynek: Nazwa, Serwer, Port, Email nadawcy, Status (aktywna/nieaktywna), Domyślna (badge), Akcje (edytuj/usuń)
|
||||
- Formularz dodawania/edycji: name, smtp_host, smtp_port (default 587), smtp_encryption (select: TLS/SSL/Brak), smtp_username, smtp_password (type=password, placeholder "••••" przy edycji), sender_email, sender_name, is_default (checkbox)
|
||||
- Przycisk "Testuj połączenie" — AJAX POST do /settings/email-mailboxes/test, wynik w alert
|
||||
- Flash messages na sukces/błąd zapisu/usunięcia
|
||||
|
||||
**Routing w Application.php:**
|
||||
- GET /settings/email-mailboxes → EmailMailboxController::index
|
||||
- POST /settings/email-mailboxes/save → EmailMailboxController::save
|
||||
- POST /settings/email-mailboxes/delete → EmailMailboxController::delete
|
||||
- POST /settings/email-mailboxes/test → EmailMailboxController::testConnection
|
||||
|
||||
**Nawigacja w app.php:**
|
||||
- Dodać link "Skrzynki pocztowe" w sekcji Settings sidebar (ikona: ✉ lub odpowiednia z istniejącego zestawu)
|
||||
- currentSettings === 'email-mailboxes'
|
||||
|
||||
**Hasło przy edycji:**
|
||||
- Jeśli pole password puste przy save — zachowaj istniejące zaszyfrowane hasło
|
||||
- Jeśli wypełnione — zaszyfruj nowe
|
||||
|
||||
Avoid: Nie dodawać żadnych zewnętrznych bibliotek mailingowych (PHPMailer, SwiftMailer) — w tej fazie wystarczy natywny socket do testu połączenia. Biblioteka mailingowa będzie dodana w fazie 15.
|
||||
</action>
|
||||
<verify>
|
||||
1. Otworzyć /settings/email-mailboxes — widoczna pusta lista
|
||||
2. Dodać skrzynkę z danymi SMTP — pojawia się na liście
|
||||
3. Kliknąć "Testuj połączenie" — JSON response z wynikiem
|
||||
4. Edytować skrzynkę (bez zmiany hasła) — hasło zachowane
|
||||
5. Usunąć skrzynkę — znika z listy
|
||||
</verify>
|
||||
<done>AC-2, AC-3, AC-4 satisfied: CRUD skrzynek działa, test SMTP działa, nawigacja widoczna</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>CRUD skrzynek pocztowych z testem połączenia SMTP</what-built>
|
||||
<how-to-verify>
|
||||
1. Uruchomić migrację: Ustawienia > Baza danych > Migruj
|
||||
2. Przejść do: Ustawienia > Skrzynki pocztowe
|
||||
3. Dodać skrzynkę testową (dowolny serwer SMTP)
|
||||
4. Kliknąć "Testuj połączenie" — sprawdzić wynik
|
||||
5. Edytować skrzynkę — zmienić nazwę, zostawić hasło puste → hasło zachowane
|
||||
6. Zaznaczyć "Domyślna" → badge na liście
|
||||
7. Usunąć skrzynkę → znika
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" to continue, or describe issues to fix</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<boundaries>
|
||||
|
||||
## DO NOT CHANGE
|
||||
- src/Modules/Accounting/* (moduł paragonów stabilny)
|
||||
- src/Modules/Orders/* (moduł zamówień — modyfikacje w fazie 15)
|
||||
- database/migrations/000001-000053 (istniejące migracje)
|
||||
|
||||
## SCOPE LIMITS
|
||||
- Ten plan tworzy tylko CRUD skrzynek pocztowych — szablony wiadomości to faza 14
|
||||
- NIE dodawać bibliotek mailingowych (PHPMailer itp.) — to faza 15
|
||||
- NIE implementować wysyłki maili — tylko test połączenia SMTP
|
||||
- NIE modyfikować widoków zamówień
|
||||
|
||||
</boundaries>
|
||||
|
||||
<verification>
|
||||
Before declaring plan complete:
|
||||
- [ ] 3 migracje SQL wykonane poprawnie (tabele istnieją)
|
||||
- [ ] CRUD skrzynek: dodawanie, edycja, usuwanie działa
|
||||
- [ ] Hasło SMTP szyfrowane w bazie (IntegrationSecretCipher)
|
||||
- [ ] Test połączenia SMTP zwraca JSON z wynikiem
|
||||
- [ ] Nawigacja: link "Skrzynki pocztowe" w Settings sidebar
|
||||
- [ ] Flash messages na sukces/błąd operacji
|
||||
- [ ] Brak alert()/confirm() — użyty OrderProAlerts
|
||||
- [ ] CSRF token w formularzach (_token)
|
||||
- [ ] Wszystkie acceptance criteria spełnione
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Wszystkie 3 tabele email_* istnieją w bazie
|
||||
- CRUD skrzynek pocztowych w pełni funkcjonalny
|
||||
- Test połączenia SMTP działa (JSON response)
|
||||
- Hasła zaszyfrowane w DB
|
||||
- Nawigacja w sidebar poprawna
|
||||
- Brak błędów PHP, brak nowych ostrzeżeń
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.paul/phases/13-email-mailboxes/13-01-SUMMARY.md`
|
||||
</output>
|
||||
133
.paul/phases/13-email-mailboxes/13-01-SUMMARY.md
Normal file
133
.paul/phases/13-email-mailboxes/13-01-SUMMARY.md
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
phase: 13-email-mailboxes
|
||||
plan: 01
|
||||
subsystem: settings
|
||||
tags: [email, smtp, encryption, crud]
|
||||
|
||||
requires:
|
||||
- phase: none
|
||||
provides: n/a (first phase of v0.4)
|
||||
provides:
|
||||
- email_mailboxes table with SMTP credential storage
|
||||
- email_templates table (schema only, populated in phase 14)
|
||||
- email_logs table (schema only, populated in phase 15)
|
||||
- EmailMailboxController + EmailMailboxRepository CRUD
|
||||
- SMTP connection test endpoint
|
||||
affects: [14-email-templates, 15-email-sending]
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns: [SMTP socket test via stream_socket_client, password encryption reusing IntegrationSecretCipher]
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- database/migrations/20260315_000054_create_email_mailboxes_table.sql
|
||||
- database/migrations/20260315_000055_create_email_templates_table.sql
|
||||
- database/migrations/20260315_000056_create_email_logs_table.sql
|
||||
- src/Modules/Settings/EmailMailboxController.php
|
||||
- src/Modules/Settings/EmailMailboxRepository.php
|
||||
- resources/views/settings/email-mailboxes.php
|
||||
modified:
|
||||
- routes/web.php
|
||||
- resources/views/layouts/app.php
|
||||
|
||||
key-decisions:
|
||||
- "SMTP test via native stream_socket_client — no PHPMailer dependency yet"
|
||||
- "Password encryption via existing IntegrationSecretCipher (AES-256-CBC + HMAC)"
|
||||
- "is_default flag with auto-reset of previous default on save"
|
||||
|
||||
patterns-established:
|
||||
- "Email mailbox CRUD pattern (same as ReceiptConfig)"
|
||||
- "AJAX JSON test endpoint for connection validation"
|
||||
|
||||
duration: ~15min
|
||||
started: 2026-03-15T00:00:00Z
|
||||
completed: 2026-03-15T00:15:00Z
|
||||
---
|
||||
|
||||
# Phase 13 Plan 01: DB + Skrzynki pocztowe Summary
|
||||
|
||||
**3 migracje email (mailboxes/templates/logs) + pełny CRUD skrzynek pocztowych SMTP z testem połączenia i szyfrowaniem haseł**
|
||||
|
||||
## Performance
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Duration | ~15min |
|
||||
| Tasks | 3 completed (2 auto + 1 checkpoint) |
|
||||
| Files created | 6 |
|
||||
| Files modified | 2 |
|
||||
|
||||
## Acceptance Criteria Results
|
||||
|
||||
| Criterion | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| AC-1: Migracje tworzą 3 tabele | Pass | email_mailboxes, email_templates, email_logs z FK i indeksami |
|
||||
| AC-2: CRUD skrzynek pocztowych | Pass | Dodawanie, edycja (z zachowaniem hasła), usuwanie, toggle statusu |
|
||||
| AC-3: Walidacja połączenia SMTP | Pass | AJAX POST z AUTH LOGIN, JSON response |
|
||||
| AC-4: Nawigacja | Pass | Link "Skrzynki pocztowe" w sidebar Settings |
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- 3 tabele bazodanowe: `email_mailboxes` (SMTP credentials), `email_templates` (szablony), `email_logs` (historia wysyłki)
|
||||
- Pełny CRUD skrzynek z szyfrowaniem haseł przez IntegrationSecretCipher
|
||||
- Test połączenia SMTP z pełnym handshake (EHLO → STARTTLS → AUTH LOGIN)
|
||||
- Widok z listą, formularzem edycji i AJAX testem połączenia
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
| File | Change | Purpose |
|
||||
|------|--------|---------|
|
||||
| `database/migrations/20260315_000054_create_email_mailboxes_table.sql` | Created | Tabela skrzynek SMTP |
|
||||
| `database/migrations/20260315_000055_create_email_templates_table.sql` | Created | Tabela szablonów email |
|
||||
| `database/migrations/20260315_000056_create_email_logs_table.sql` | Created | Tabela logów wysyłki |
|
||||
| `src/Modules/Settings/EmailMailboxRepository.php` | Created | Repository: listAll, findById, save, delete, toggleStatus, listActive |
|
||||
| `src/Modules/Settings/EmailMailboxController.php` | Created | Controller: index, save, delete, toggleStatus, testConnection |
|
||||
| `resources/views/settings/email-mailboxes.php` | Created | Widok: lista + formularz + AJAX test |
|
||||
| `routes/web.php` | Modified | 5 nowych route'ów + import EmailMailbox* + IntegrationSecretCipher |
|
||||
| `resources/views/layouts/app.php` | Modified | Link "Skrzynki pocztowe" w sidebar |
|
||||
|
||||
## Decisions Made
|
||||
|
||||
| Decision | Rationale | Impact |
|
||||
|----------|-----------|--------|
|
||||
| Natywny stream_socket_client do testu SMTP | Brak potrzeby dodawania PHPMailer w tej fazie — wystarczy weryfikacja połączenia | PHPMailer/SwiftMailer dodany w fazie 15 |
|
||||
| IntegrationSecretCipher do haseł SMTP | Reuse istniejącego mechanizmu AES-256-CBC+HMAC | Spójny wzorzec szyfrowania w projekcie |
|
||||
| Auto-reset is_default przy save | Tylko jedna skrzynka domyślna | Uproszczenie logiki wyboru skrzynki |
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Summary
|
||||
|
||||
| Type | Count | Impact |
|
||||
|------|-------|--------|
|
||||
| Auto-fixed | 0 | - |
|
||||
| Scope additions | 1 | Minimal — listActive() w repository |
|
||||
| Deferred | 0 | - |
|
||||
|
||||
**Total impact:** Minimalna zmiana — dodano `listActive()` do repo (potrzebne w fazie 14 do selecta skrzynek).
|
||||
|
||||
### Deferred Items
|
||||
|
||||
None.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
None.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
**Ready:**
|
||||
- Tabela `email_templates` gotowa na CRUD (faza 14)
|
||||
- `EmailMailboxRepository::listActive()` gotowe do użycia w select skrzynki w szablonach
|
||||
- Tabela `email_logs` gotowa na wypełnianie (faza 15)
|
||||
|
||||
**Concerns:**
|
||||
- sonar-scanner nie uruchomiony (required skill) — do uruchomienia przed kolejnym UNIFY
|
||||
|
||||
**Blockers:**
|
||||
- None
|
||||
|
||||
---
|
||||
*Phase: 13-email-mailboxes, Plan: 01*
|
||||
*Completed: 2026-03-15*
|
||||
Reference in New Issue
Block a user