Compare commits
22 Commits
b2a267bf19
...
9c88ea603e
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c88ea603e | |||
| 08b43b427a | |||
| 67777bde5b | |||
| eec3709533 | |||
| 21085c7405 | |||
| 3a854ad500 | |||
| d05a1ad276 | |||
| 480fd207c7 | |||
| 83d2e13d14 | |||
| e93eca1384 | |||
| ec7a7753f6 | |||
| 789f60e2ca | |||
| 892d79b6c3 | |||
| c16834b153 | |||
| 0528fb0a8d | |||
| db4ddab42b | |||
| f899b3c01b | |||
| d651e0c845 | |||
| 07c61fdd09 | |||
| 46ea2dcd15 | |||
| 886ddb3208 | |||
| d327750cf3 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -6,6 +6,7 @@
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
/.local/
|
||||
/backups/
|
||||
/certbot/conf/
|
||||
/certbot/www/
|
||||
@@ -16,4 +17,3 @@
|
||||
.vscode/
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## Current phase gate
|
||||
|
||||
This project is in specification mode. Do not scaffold or implement application/deployment code until the relevant capability spec is explicitly approved by the user. Documentation, research notes, Git hygiene, and spec revisions are allowed.
|
||||
This repository is authorized for SDD Phase 4 implementation as of 2026-08-20. Follow `tasks/todo.md` in dependency order and keep the approved platform specification, adaptation contract, runbooks, and ADRs authoritative. Production operations and destructive actions retain their task-specific human approval guards.
|
||||
|
||||
## Sources of truth
|
||||
|
||||
@@ -28,7 +28,7 @@ This repository owns deployment concerns only. Backend and frontend directories
|
||||
## Operational baseline
|
||||
|
||||
- Everything runs through Docker, Docker Compose, Bash scripts, and environment files/templates.
|
||||
- The CSV-to-PostgreSQL workflow is a one-time administrative bootstrap/recovery command, never a scheduled ingestion service. ZarinPal owns future production payment writes.
|
||||
- The CSV-to-PostgreSQL workflow is a one-time administrative bootstrap/recovery command, never a scheduled ingestion service. MVP payment facts remain immutable after that import; future live writes require a new approved integration contract.
|
||||
- An idempotent authenticated start request enqueues overview, payment-performance, fees, and peer-comparison jobs for bounded parallel calculation by a separate on-demand Go worker. Redis caches/coordinates each section for 30 days, and a separate authenticated SSE request streams partial completion. This worker is event-driven, never scheduled.
|
||||
- The worker uses Asynq pinned to `v0.26.0`. Asynq is pre-`v1.0.0`; never use a floating dependency or upgrade it without compatibility review and passing queue/retry/recovery tests.
|
||||
- Each section attempt has a 60-second timeout and at most two automatic retries for transient failures using exponential backoff with jitter. Deterministic failures are not retried.
|
||||
@@ -39,4 +39,4 @@ This repository owns deployment concerns only. Backend and frontend directories
|
||||
- Health checks distinguish liveness from readiness.
|
||||
- Backups are encrypted where supported, validated, retention-aware, and paired with rehearsed restores.
|
||||
- Restore operations must validate the exact destination and require an explicit production confirmation guard.
|
||||
- CI/CD deploys immutable revisions, runs pre/post-deploy checks, and has a documented rollback.
|
||||
- Production deployment is manual and Qlockify-shaped: a trusted Docker-group user fast-forwards all three clean checkouts to latest `origin/main`, records their exact commit SHAs, builds on the server, runs pre/post-deploy checks, and records rollback SHAs. Do not add CI/CD, a runner, registry, or automated cross-repository deploy without a new approved decision.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Capability Map
|
||||
|
||||
Status: **approved Phase 0 baseline**
|
||||
Implementation authorization: **not granted**
|
||||
Implementation authorization: **granted by user on 2026-08-20**
|
||||
|
||||
## Product intent
|
||||
|
||||
@@ -13,13 +13,13 @@ The hackathon demo will seed one account per merchant. Where merchant profile fi
|
||||
|
||||
| Order | Capability | Responsibility | Depends on | MVP |
|
||||
|---:|---|---|---|:---:|
|
||||
| 1 | Data foundation | PostgreSQL model, one-time CSV bootstrap, provenance, validation, quality reporting, synthetic merchant profiles; future source writes are owned by ZarinPal | — | Yes |
|
||||
| 2 | Identity and access | Merchant credentials, sessions, tenant isolation, demo account seeding | Data foundation | Yes |
|
||||
| 3 | Analytics engine | Deterministic metrics, cohorts, trends, diagnostic drill-downs, bounded parallel on-demand section jobs, 30-day Redis cache, PostgreSQL revision invalidation | Data foundation | Yes |
|
||||
| 1 | Data foundation | PostgreSQL model, one-time CSV bootstrap, provenance, validation, quality reporting, synthetic merchant profiles; MVP payment facts are immutable afterward | — | Yes |
|
||||
| 2 | Identity and access | Replaceable demo credential adapter, sessions, tenant isolation, demo account seeding; future ZarinPal SSO is external | Data foundation | Yes |
|
||||
| 3 | Analytics engine | Deterministic metrics, cohorts, trends, diagnostic drill-downs, bounded parallel on-demand section jobs, and a 30-day Redis cache namespaced by the sole completed immutable dataset snapshot | Data foundation | Yes |
|
||||
| 4 | Merchant API | Stable authenticated interfaces and tenant-scoped SSE for partial analytics delivery | Identity and access, analytics engine | Yes |
|
||||
| 5 | Merchant dashboard | Persian RTL dashboard, responsive navigation, accessible charts/tables and explanations | Merchant API | Yes |
|
||||
| 6 | AI assistance | AvalAI-backed explanation/recommendation workflow over sanitized aggregates | Analytics engine, merchant API | No |
|
||||
| 7 | Platform operations | Compose, Nginx, CI/CD, observability, backup/restore, release and rollback | All deployables | Yes |
|
||||
| 7 | Platform operations | Compose, Nginx, manual deployment, phased observability, backup/restore, release and rollback | All deployables | Yes |
|
||||
|
||||
Build sequence: data foundation → identity and analytics → API → dashboard → platform operations → AI assistance.
|
||||
|
||||
@@ -31,8 +31,8 @@ Build sequence: data foundation → identity and analytics → API → dashboard
|
||||
|
||||
## Dataset facts that constrain the design
|
||||
|
||||
- Source grain: one payment attempt, uniquely identified by `(session_key, try_seq)`.
|
||||
- Observed volume: 2,213,289 attempts, 2,062,839 sessions, 343 merchants, 348 terminals, 5 categories.
|
||||
- Source grain: one attempt-or-no-attempt session record, uniquely identified by `(session_key, try_seq)`. A `try_seq = 0`/`NoAttempt` marker creates no normalized attempt.
|
||||
- Observed volume: 2,213,289 source records, including 263,936 no-attempt markers and 1,949,353 actual attempts; 2,062,839 sessions, 343 merchants, 348 terminals, and 5 categories.
|
||||
- Observed period: 2026-01-01 through 2026-07-01.
|
||||
- Nullability is material: response, PSP, issuer/card, verification timing, and settlement fields are incomplete.
|
||||
- Source `adjusted_fee` is the merchant-fee field for this product and is normalized once per session as a real Rial amount; the challenge source name/value was confidentiality-adjusted.
|
||||
@@ -51,12 +51,15 @@ Build sequence: data foundation → identity and analytics → API → dashboard
|
||||
9. `Paid` and `Verified` are both successful bank payments; `Paid` still requires merchant verification and `Verified` is finalized. Paid-volume and recovery metrics include both while verification metrics keep them separate.
|
||||
10. Merchant fees are real Rial amounts stored/summed once per session; repeated attempt rows never multiply a fee.
|
||||
11. MVP reporting periods are limited to daily, Saturday–Friday weekly, Jalali-month, and Persian-season views with consistent `Asia/Tehran` boundaries across database, API, and frontend.
|
||||
12. An idempotent start request enqueues overview, payment-performance, fees, and peer-comparison sections for bounded parallel calculation by a separate Go worker; Redis caches each typed section for 30 days under merchant/cohort and metric-definition revisions.
|
||||
12. An idempotent start request enqueues overview, payment-performance, fees, and peer-comparison sections for bounded parallel calculation by a separate Go worker; Redis caches each typed section for 30 days under the merchant, completed dataset snapshot, and metric-definition version.
|
||||
13. A separate authenticated SSE request exposes each section as it becomes ready. Redis and the on-demand worker remain private, and Redis is never a source of truth or backup target.
|
||||
14. Each section attempt is limited to 60 seconds and at most two transient retries with exponential backoff/jitter; deterministic errors fail immediately.
|
||||
15. The worker pins pre-1.0 Asynq `v0.26.0`, runs at 16 jobs globally and 4/merchant by default, and changes require reviewed dependency/load evidence.
|
||||
16. Start, manual-retry, and SSE limits use independent authenticated-user/merchant buckets and return `429` with `Retry-After` before work is queued. SSE heartbeats run every 15 seconds and authorized run-event replay lasts one hour.
|
||||
17. Peer outputs use the median of one independently calculated value per eligible merchant plus a direction-aware merchant performance percentile. Equal merchant weighting is intentional because transaction weighting would let the dataset's largest merchants dominate the benchmark.
|
||||
18. Authentication uses revocable server-side sessions and an opaque host-only `Secure`, `HttpOnly`, `SameSite=Lax` cookie. Passwords use Argon2id; sessions expire after 12 idle hours or seven absolute days and are revoked on logout/account disablement. Demo credentials are generated once and never stored in Git.
|
||||
19. Demo authentication stays replaceable and minimal because ZarinPal SSO will own production identity after acceptance. Failed logins are limited by username and IP without permanent lockout; mutations require CSRF and same-origin checks; each user has at most two active sessions.
|
||||
20. Demo sessions live in PostgreSQL and store only SHA-256 digests of 32-byte random tokens. Activity writes are coalesced to five-minute windows; Redis remains disposable and is not the session source of truth.
|
||||
|
||||
## Required capability specs
|
||||
|
||||
|
||||
13
README.md
13
README.md
@@ -2,7 +2,9 @@
|
||||
|
||||
This repository is the operational control plane for the hackathon project. It will own Docker Compose, Nginx, environment templates, deployment automation, backup/restore scripts, and cross-repository release orchestration. Product code belongs in the backend and frontend repositories.
|
||||
|
||||
Current state: specification and repository bootstrap only. No application or deployment implementation has been approved yet.
|
||||
Current state: specifications, operations plan, adaptation contract, runbooks, and task list are approved. Phase 4 deployment implementation is authorized.
|
||||
|
||||
For direct Windows development without Docker Compose, see [`docs/LOCAL-DEVELOPMENT.md`](docs/LOCAL-DEVELOPMENT.md). This helper is deliberately separate from the production Compose contract.
|
||||
|
||||
## Repository topology
|
||||
|
||||
@@ -11,7 +13,7 @@ zarinpal-analytics-deployment/ # this repository
|
||||
├── backend/ # ignored checkout of zarinpal-analytics-backend
|
||||
├── frontend/ # ignored checkout of zarinpal-analytics-frontend
|
||||
├── docs/
|
||||
└── (deployment assets after specification approval)
|
||||
└── (deployment assets created through the authorized task list)
|
||||
```
|
||||
|
||||
The intended production hostname is `zarinpal.amiirkhl.ir`. The deployment shape will follow the proven Qlockify structure while being reviewed and hardened for this service; it is not a byte-for-byte copy.
|
||||
@@ -24,3 +26,10 @@ The intended production hostname is `zarinpal.amiirkhl.ir`. The deployment shape
|
||||
- Raw datasets and database state are never committed.
|
||||
- Every material change is a small Conventional Commit.
|
||||
|
||||
## Platform operations documents
|
||||
|
||||
- `docs/PLATFORM-OPERATIONS-SPEC.md` defines the approved manual Qlockify-style production topology, lifecycle, observability phases, security, backup/restore, acceptance, and operator boundaries.
|
||||
- `docs/QLOCKIFY-ADAPTATION-CONTRACT.md` maps Qlockify's tracked deployment files and environment layout to the planned ZarinPal artifacts without requiring real server values during planning.
|
||||
- `docs/runbooks/FIRST-SERVER-BOOTSTRAP.md` defines the safe first-host sequence without implementing a bootstrap script.
|
||||
- `docs/runbooks/BACKUP-RESTORE.md` defines logical PostgreSQL backup, encryption/retention, guarded restore, and rehearsal.
|
||||
- `tasks/plan.md` and `tasks/todo.md` hold the approved implementation plan and dependency-ordered Phase 4 tasks.
|
||||
|
||||
53
docs/CROSS-REPOSITORY-PLAN-REVIEW.md
Normal file
53
docs/CROSS-REPOSITORY-PLAN-REVIEW.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Cross-Repository Specification and Plan Review
|
||||
|
||||
- Status: Approved — cross-repository implementation authorized 2026-08-20
|
||||
- Date: 2026-08-20
|
||||
- Scope: `zarinpal-analytics-backend`, `zarinpal-analytics-frontend`, and `zarinpal-analytics-deployment`
|
||||
|
||||
## Outcome
|
||||
|
||||
The approved product, data, analytics, identity, API, UX, and operations specifications are sufficiently resolved for implementation. The user approved all three repository plans/task lists and authorized Phase 4 implementation on 2026-08-20.
|
||||
|
||||
## Confirmed shared contracts
|
||||
|
||||
| Concern | Agreed contract | Owning repository |
|
||||
|---|---|---|
|
||||
| Dataset | One pinned archive is imported once into normalized PostgreSQL tables; raw challenge data is ignored by Git | Backend |
|
||||
| Data lifetime | MVP payment facts are immutable after the sole completed import; future replacement or live writes require a new specification | Backend |
|
||||
| Import activation | On the first server release only: start data services, migrate, import and reconcile, then activate public services | Deployment |
|
||||
| Tenant authority | Merchant context comes only from the revocable server-side demo session, never from client-supplied merchant identifiers | Backend |
|
||||
| Analytics | Day, Saturday–Friday week, Jalali month, and Persian season metrics follow catalogue `0.4.0` | Backend |
|
||||
| Async delivery | Four independently completing sections use Redis/Asynq and authenticated SSE; Asynq is pinned pre-1.0 at `v0.26.0` | Backend |
|
||||
| Cache identity | Redis keys include merchant, sole completed dataset snapshot, metric version, period, and section; PostgreSQL has no MVP invalidation triggers | Backend |
|
||||
| Payment states | `Paid` means bank payment succeeded but merchant verification did not; `Verified` completed both steps; money success includes both where specified | Backend |
|
||||
| Detail/export | Authorized details reconcile to aggregates; selected detail views support bounded Persian CSV export with the shared projection | Backend / Frontend |
|
||||
| API | REST/SSE contract is approved as OpenAPI `1.0.0`; frontend transport types will be generated from the pinned artifact | Backend / Frontend |
|
||||
| UI | SvelteKit 2, Svelte 5, TypeScript, Tailwind, RTL Persian digits, IRANYekanX Pro, responsive light/dark ZarinPal-inspired system | Frontend |
|
||||
| Deployment | Three repositories, Docker Compose, Nginx, TLS, environment files, guarded operations, encrypted PostgreSQL backup/restore | Deployment |
|
||||
|
||||
## Implementation plan map
|
||||
|
||||
1. Backend foundation: Go workspace, normalized Ent/Atlas schema, guarded one-time importer, validation/promotion/recovery, and deterministic demo accounts.
|
||||
2. Backend services: bounded demo authentication, metric query kernel, Redis/Asynq orchestration, details, exports, and provider integration.
|
||||
3. Backend HTTP surface: OpenAPI conformance, middleware, auth/period/run/SSE/retry/detail/CSV endpoints, audit, and hardening.
|
||||
4. Frontend: foundation and tokens, generated transport, shell/auth, dashboard sections, partial SSE states, detail/export workflows, responsive/accessibility/theme verification.
|
||||
5. Deployment: adapt Qlockify's Compose/Nginx/environment/manual-release/backup operations, integrate first-start ordering, verify locally with safe placeholders, then perform the separately authorized server release with real runtime values.
|
||||
|
||||
Backend provider work and frontend visual-system work can proceed in parallel after separate implementation authorization. Deployment scaffolding can also proceed in parallel once command/build contracts are stable; production bootstrap and cutover remain sequential gates.
|
||||
|
||||
## Deferred interface details (not planning blockers)
|
||||
|
||||
These are build-time choices, not unresolved product requirements:
|
||||
|
||||
- exact pinned OpenAPI generation/validation tools and repository command wrappers;
|
||||
- exact frontend/backend build and health commands after their scaffolds exist (frontend history fallback is already restricted to non-API routes);
|
||||
- exact backend/frontend command and environment-variable names after their scaffolds exist;
|
||||
- the hosting path for human-readable API problem pages.
|
||||
|
||||
Server IP/DNS state, Git credentials, ACME email, ParsPack values, operator identity, and alert destinations are runtime inputs supplied when their operations are performed. They do not block implementation planning or local placeholder-based validation. Deployment uses no CI/CD pipeline, runner, or registry.
|
||||
|
||||
No secrets, raw dataset, generated database, credentials, certificates, backups, or runtime volumes may be committed.
|
||||
|
||||
## Authorization gate
|
||||
|
||||
The user approved these plans and explicitly authorized implementation on 2026-08-20. Implementation must follow each repository's dependency-ordered task list and review checkpoints. Remote pushes remain forbidden until the user separately requests a push.
|
||||
38
docs/LOCAL-DEVELOPMENT.md
Normal file
38
docs/LOCAL-DEVELOPMENT.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Local development without Docker Compose
|
||||
|
||||
This Windows-only helper runs the API, analytics worker, and frontend directly from the three sibling repositories. PostgreSQL and Redis must already be reachable locally. It does not create databases, run migrations/imports, generate credentials, edit service-owned environment files, or start Docker.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Go and npm are on `PATH`.
|
||||
- PostgreSQL is reachable at `127.0.0.1:5432`.
|
||||
- Redis is reachable at `127.0.0.1:6379`.
|
||||
- The backend has `cmd/api` and `cmd/worker`, plus a protected backend-owned `.env`.
|
||||
- The frontend has a `package.json` `dev` script and its frontend-owned local environment file.
|
||||
- Dependencies, migrations, the one-time importer, and demo merchant creation have already completed through their owning repositories.
|
||||
|
||||
The helper only reports variable names or file paths when validation fails. It never prints resolved environment values. Runtime process metadata and logs are written under ignored `.local/`.
|
||||
|
||||
## Commands
|
||||
|
||||
Run these from the deployment repository in PowerShell:
|
||||
|
||||
```powershell
|
||||
.\scripts\local-dev.ps1 check
|
||||
.\scripts\local-dev.ps1 start
|
||||
.\scripts\local-dev.ps1 status
|
||||
.\scripts\local-dev.ps1 stop
|
||||
```
|
||||
|
||||
The default sibling locations are `../zarinpal-analytics-backend` and `../zarinpal-analytics-frontend`. Override them with `-BackendPath` and `-FrontendPath` when necessary.
|
||||
|
||||
`stop` terminates only the process trees whose PID and start time match the ignored state file. A stale or reused PID is not terminated.
|
||||
|
||||
## Verification
|
||||
|
||||
```powershell
|
||||
pwsh -NoProfile -File .\tests\local-dev.tests.ps1
|
||||
pwsh -NoProfile -File .\scripts\local-dev.ps1 check
|
||||
```
|
||||
|
||||
After `start`, open the frontend URL declared by the frontend development server. Inspect `.local/logs/*.err.log` if a process exits during or after startup.
|
||||
520
docs/PLATFORM-OPERATIONS-SPEC.md
Normal file
520
docs/PLATFORM-OPERATIONS-SPEC.md
Normal file
@@ -0,0 +1,520 @@
|
||||
# Platform Operations Capability Spec
|
||||
|
||||
- Status: Approved specification
|
||||
- Implementation authorization: Granted by user on 2026-08-20
|
||||
- Capability: Platform operations
|
||||
- Depends on: all deployable backend and frontend capability contracts
|
||||
- Last updated: 2026-08-20
|
||||
|
||||
## Confirmed intent
|
||||
|
||||
Deploy `zarinpal.amiirkhl.ir` on one Ubuntu server using the proven Qlockify operating model: three nested Git repositories, Docker Compose builds performed on the server, Nginx as the only public entry point, automatic Let's Encrypt TLS, PostgreSQL 18, Redis 7 Alpine, a Go API and event-driven worker, and a static frontend.
|
||||
|
||||
Production deployment is manual. The operator pulls the latest `main` from all three repositories, records the resulting commit hashes, builds locally on the server, runs migrations and health checks, and records the release. Rollback checks out the previously recorded hashes and rebuilds. There is no CI/CD pipeline, runner, private image registry, or cross-repository automation.
|
||||
|
||||
MVP includes health checks, structured and bounded logs, safe startup, explicit migration/import separation, rollback, and encrypted daily PostgreSQL backup/restore adapted with minimal changes from Qlockify. Prometheus and Grafana are a required follow-up phase after the core project is ready and before AI work begins.
|
||||
|
||||
## Objective
|
||||
|
||||
Provide a production-shaped hackathon platform that a trusted operator can understand and recover without hidden automation. A clean server can be prepared, the exact source revisions can be deployed, only approved traffic is publicly exposed, database changes and one-time data import are deliberate, failures leave useful evidence, and PostgreSQL can be restored from an encrypted off-host backup.
|
||||
|
||||
## Scope
|
||||
|
||||
### MVP scope
|
||||
|
||||
- Qlockify-style nested repository layout and manual server-side deployment
|
||||
- Docker Compose service topology and internal networks
|
||||
- Nginx canonical routing, security headers, TLS termination, and SSE behavior
|
||||
- PostgreSQL 18, Redis 7 Alpine, backend API, frontend, and worker boundaries
|
||||
- Liveness/readiness, startup ordering, migration, shutdown, and resource limits
|
||||
- Explicit one-time CSV import and demo-credential administrative jobs
|
||||
- Environment, secret, volume, and host exposure policy
|
||||
- Manual release records, health verification, and Git-hash rollback
|
||||
- Structured container logs, log rotation, Docker health status, and operator checks
|
||||
- Daily encrypted PostgreSQL backup, S3-compatible upload, retention, and guarded restore adapted from Qlockify
|
||||
- Safe first-server bootstrap
|
||||
- Normal frontend build consumption of the tracked licensed font assets
|
||||
|
||||
### Required post-MVP, pre-AI phase
|
||||
|
||||
- Prometheus metrics collection
|
||||
- Grafana dashboards
|
||||
- Private exporters for host/container/PostgreSQL/Redis signals
|
||||
- Symptom-based alerts and alert delivery testing
|
||||
|
||||
This observability phase must be complete before the AI capability starts. It does not block the first working MVP deployment.
|
||||
|
||||
### Out of scope
|
||||
|
||||
- CI/CD pipelines, hosted/self-hosted runners, automated deploy triggers, and private image registries
|
||||
- Kubernetes, multi-server failover, multi-region operation, or zero-downtime claims
|
||||
- Scheduled or watched CSV ingestion
|
||||
- Redis backup or treating Redis as a source of truth
|
||||
- Public database, Redis, Docker, worker, metrics, debug, or administration endpoints
|
||||
- Backend/frontend application implementation in this repository
|
||||
- Private font injection or font copies in the deployment repository
|
||||
- AI implementation
|
||||
|
||||
## Approved assumptions and decisions
|
||||
|
||||
1. The target is a new dedicated Ubuntu 24.04 LTS server with 2 CPU cores, 2 GB RAM, Docker already installed, user `ubuntu`, and a bootstrap-created 2 GB swap file.
|
||||
2. The trusted `ubuntu` user belongs to the `docker` group and runs Docker Compose. Docker-group membership is root-equivalent, so access to this account is restricted to trusted operators.
|
||||
3. The deployment repository contains ignored nested checkouts of the backend and frontend repositories, matching ADR 0001 and Qlockify.
|
||||
4. The operator manually runs `git pull --ff-only origin main` in all three repositories and enters HTTPS credentials interactively. The deployment command performs no pull or credential handling; it verifies and records the three commits before building.
|
||||
5. Application images are built on the production server from those recorded checkouts. They are not published to a registry.
|
||||
6. Rollback checks out the previous recorded hashes and rebuilds. Database rollback remains constrained by migration compatibility.
|
||||
7. Nginx obtains and renews a Let's Encrypt certificate automatically through Certbot using HTTP-01.
|
||||
8. Infrastructure images are pinned to `postgres:18.4-alpine3.24`, `redis:7.4.10-alpine3.21`, `nginx:1.29.8-alpine3.23`, and `certbot/certbot:v5.7.0`.
|
||||
9. Redis uses a named volume, AOF `everysec`, and `noeviction` for routine restart continuity. It remains disposable cache/queue/coordination state and is excluded from backup; PostgreSQL remains authoritative.
|
||||
10. Daily encrypted PostgreSQL backups and tested restores are MVP requirements.
|
||||
11. Qlockify backup/restore/upload scripts are the implementation reference and will receive only necessary project path, service, credential, and safety changes.
|
||||
12. Production `.env` files, secrets, TLS keys, Redis data, raw CSV, credentials, logs, and font assets are excluded from database backup packages.
|
||||
13. Prometheus/Grafana are required after MVP readiness and before AI implementation. MVP readiness requires application flows, tenant isolation, migration/import, TLS, manual health/log checks, rollback, encrypted backup, and clean restore to pass.
|
||||
14. Frontend commit `64f5ac2` owns `static/fonts/IRANYekanXVFaNumVF.woff2` and `static/fonts/FontLicense.txt`; deployment uses the normal frontend build context without another asset path.
|
||||
|
||||
Any change to these decisions requires a spec revision before implementation.
|
||||
|
||||
## Repository and server layout
|
||||
|
||||
```text
|
||||
/home/ubuntu/zarinpal-analytics-deployment/
|
||||
├── .git/ deployment repository
|
||||
├── backend/ ignored checkout of backend repository
|
||||
│ └── .git/
|
||||
├── frontend/ ignored checkout of frontend repository
|
||||
│ └── .git/
|
||||
├── docs/
|
||||
├── backups/ ignored runtime output
|
||||
└── deployment assets only after explicit implementation authorization
|
||||
```
|
||||
|
||||
Each checkout keeps its own Git history and remote. Deployment operations verify the expected repository, branch, remote, clean state, and resolved absolute path before pulling, checking out, or building.
|
||||
|
||||
Production configuration and secrets live outside public Git. Runtime volumes, backups, certificate data, raw CSV, and generated credentials remain ignored.
|
||||
|
||||
## Runtime architecture
|
||||
|
||||
```text
|
||||
Internet
|
||||
|
|
||||
| TCP 80/443 only
|
||||
v
|
||||
Nginx + Certbot
|
||||
|
|
||||
+------------------------+
|
||||
| private app network |
|
||||
v v
|
||||
frontend static server backend API
|
||||
|
|
||||
+---------+---------+
|
||||
| private data net |
|
||||
v v
|
||||
PostgreSQL <------- Redis
|
||||
^ ^
|
||||
| |
|
||||
one-off jobs worker
|
||||
migrate / import / admin
|
||||
```
|
||||
|
||||
Nginx is the only public application entry point. The worker is event-driven and never scheduled. It uses Asynq pinned to `v0.26.0`, explicitly a pre-1.0 dependency. Redis loss causes controlled job/cache reconciliation and recomputation, not loss of authoritative payment or session data.
|
||||
|
||||
## Docker Compose topology
|
||||
|
||||
| Service | Responsibility | Networks | State | Host exposure |
|
||||
|---|---|---|---|---|
|
||||
| `nginx` | TLS, canonical routing, limits, static/API/SSE proxy | `edge`, `app` | certificate mounts only | 80/443 |
|
||||
| `certbot` | Initial issuance and renewal | `edge` | ACME/certificate volume | none |
|
||||
| `frontend` | Serve static SvelteKit build | `app` | none | none |
|
||||
| `backend` | Go API, sessions, authorization, analytics control | `app`, `data` | none | none |
|
||||
| `worker` | On-demand Asynq analytics sections | `data` | none | none |
|
||||
| `postgres` | Authoritative database | `data` | `postgres_data` | none |
|
||||
| `redis` | Queue, cache, rate-limit, replay coordination | `data` | named AOF volume, excluded from backup | none |
|
||||
| `migrate` | Versioned schema migration | `data` | none | one-off only |
|
||||
| `import` | One-time CSV bootstrap/recovery | `data` | exact read-only input bind | one-off profile only |
|
||||
| `admin` | Demo credentials and approved maintenance | `data` | explicit output bind if requested | one-off profile only |
|
||||
|
||||
Post-MVP adds `prometheus`, `grafana`, and approved exporters on a separate private monitoring network. Grafana binds only to loopback/private operator access unless a later ADR approves a protected public route.
|
||||
|
||||
### Image and build policy
|
||||
|
||||
- PostgreSQL, Redis, Nginx, and Certbot use the exact approved tags above; no `latest` tag.
|
||||
- Backend and worker build from the same recorded backend commit and deployment Dockerfile/context contract.
|
||||
- Frontend builds from the recorded frontend commit and normal build context, including its tracked font/license inputs.
|
||||
- The deployment script records resulting local image identifiers and build timestamps for diagnosis, but Git commit hashes are the reproducible source reference.
|
||||
- Build failure stops before replacing healthy running services.
|
||||
|
||||
### Network and container policy
|
||||
|
||||
- Only Nginx publishes ports 80 and 443.
|
||||
- PostgreSQL, Redis, backend, frontend, worker, and later monitoring services do not publish internet-facing ports.
|
||||
- Frontend and Nginx cannot connect directly to PostgreSQL or Redis.
|
||||
- Containers run as non-root where their official/application image supports it, drop unnecessary capabilities, set `no-new-privileges`, and use read-only filesystems where practical.
|
||||
- CPU, memory, PID, database-pool, and log limits are explicit. Initial values are validated against the actual server and import/load tests.
|
||||
- Docker logs use bounded rotation so a noisy service cannot fill the server disk.
|
||||
|
||||
Initial runtime limits for the 2-GB server are:
|
||||
|
||||
| Service | Memory limit |
|
||||
|---|---:|
|
||||
| PostgreSQL | 512 MB |
|
||||
| Redis | 256 MB container limit; 192 MB `maxmemory` target to leave process/AOF overhead |
|
||||
| Backend API | 256 MB |
|
||||
| Worker | 512 MB |
|
||||
| Frontend | 96 MB |
|
||||
| Nginx | 96 MB |
|
||||
|
||||
Certbot is one-shot/periodic rather than continuously memory-resident. Worker concurrency remains 16 globally and 4 per merchant. If measured load exceeds this server, upgrade server capacity rather than silently lowering the approved concurrency.
|
||||
|
||||
## Manual deployment and release records
|
||||
|
||||
### Normal deployment
|
||||
|
||||
1. Confirm exact server, deployment root, environment, available disk, recent backup, Docker health, and operator identity.
|
||||
2. Acquire a deployment lock so backup/restore/import/deploy operations cannot conflict unsafely.
|
||||
3. Before running the deployment command, the operator manually executes `git pull --ff-only origin main` in each repository and enters HTTPS credentials interactively; no Git credential is stored by deployment.
|
||||
4. Verify all three repositories exist at their exact expected paths, use the approved HTTPS remotes, are on `main`, match `origin/main`, and contain no unexplained local changes.
|
||||
5. Record the deployment, backend, and frontend commit hashes under the ignored `releases/` directory before building.
|
||||
6. Validate required configuration/secret files without printing values.
|
||||
7. Render and validate Docker Compose configuration.
|
||||
8. Build candidate application/Nginx images on the server.
|
||||
9. Start/verify PostgreSQL and Redis.
|
||||
10. Run the dedicated migration service once and require success.
|
||||
11. Replace backend/worker, then frontend, then validate/reload Nginx.
|
||||
12. Run public and private health/smoke checks and observe for 15 minutes.
|
||||
13. Write a non-secret release record and mark it active only after checks pass.
|
||||
|
||||
The future deployment script adapts Qlockify's manual `deploy.sh` structure but must not use unconditional `git reset --hard`; a dirty or divergent checkout stops and asks the operator to resolve it.
|
||||
|
||||
### Release record
|
||||
|
||||
Each server-side release record contains:
|
||||
|
||||
- release id and UTC timestamp
|
||||
- deployment, backend, and frontend Git commit hashes
|
||||
- Git branch and remote identity for each checkout
|
||||
- resulting local image ids/tags
|
||||
- migration version
|
||||
- non-secret configuration checksum
|
||||
- operator identity
|
||||
- preflight backup id/age
|
||||
- pre/post health results
|
||||
- previous release id
|
||||
- outcome and rollback reason if applicable
|
||||
|
||||
It contains no secret values, `.env` content, raw CSV path/content, credentials, license text, font copy, database rows, or TLS private material.
|
||||
|
||||
### Rollback
|
||||
|
||||
- Select the previous known-good release record explicitly.
|
||||
- Verify the three target hashes exist, repository paths/remotes are correct, and working trees are clean.
|
||||
- Check out the previous deployment/backend/frontend hashes and rebuild locally.
|
||||
- Do not automatically reverse database migrations. Normal migrations use expand/contract compatibility so the previous application can run against the expanded schema.
|
||||
- Run the same health/smoke checks and record rollback as a new operational event.
|
||||
- Target application-only rollback time is 15 minutes under normal Git/build conditions.
|
||||
|
||||
## Startup, migration, import, and shutdown
|
||||
|
||||
### Startup order
|
||||
|
||||
1. PostgreSQL and Redis start and become healthy.
|
||||
2. The one-off migration completes successfully.
|
||||
3. Backend and worker start and become ready.
|
||||
4. Frontend becomes healthy.
|
||||
5. Nginx routes production traffic only when required upstreams are ready.
|
||||
|
||||
Compose health/dependency conditions prevent common startup races, but the manual deployment operation still checks every step explicitly.
|
||||
|
||||
### Migration contract
|
||||
|
||||
- Application startup never performs migrations or automatic schema creation.
|
||||
- Migrations run from the recorded candidate backend revision under one migration lock.
|
||||
- Changes are backward-compatible through expand/deploy/contract sequencing.
|
||||
- Destructive or long-locking migrations require explicit approval, a fresh verified backup, measured evidence on production-shaped data, and a separate recovery plan.
|
||||
- Migration failure stops deployment before candidate backend/worker replacement.
|
||||
- Database down-migration is never automatic.
|
||||
|
||||
### One-time CSV import
|
||||
|
||||
- Import is an explicit operator command/profile, never normal startup, cron, a scheduler, watcher, or deploy trigger.
|
||||
- On the first server release only, tracked `scripts/first-start.sh` orchestrates PostgreSQL/Redis readiness, the one-off migration, the importer profile, reconciliation, and then public application startup. Later ordinary deploys/restarts never invoke it.
|
||||
- The operator provides an exact resolved path outside Git; only that file is mounted read-only.
|
||||
- Import cannot overlap restore, backup, or another import and starts only after migration succeeds.
|
||||
- The backend importer pins the approved checksum/schema/filename/counts, enforces resource caps, streams data, reconciles counts, removes temporary raw staging, and records incomplete failure safely according to the approved backend import specifications.
|
||||
- An identical completed import is idempotent and does not duplicate business rows.
|
||||
- Import failure prevents the first public application activation. Recovery restarts from the immutable archive; it never resumes guessed partial CSV/database state.
|
||||
- Demo credential generation is a separate one-time `admin` job whose plaintext output never enters Compose logs, Git, backups, or shell history by default.
|
||||
|
||||
### Graceful shutdown
|
||||
|
||||
- Nginx stops accepting new traffic before backend termination.
|
||||
- Backend drains within a bounded timeout.
|
||||
- Worker stops fetching new jobs; active attempts remain bounded by their existing 60-second timeout and retry rules.
|
||||
- PostgreSQL stops last. Forced termination is recorded as a deployment failure.
|
||||
|
||||
## Health and readiness
|
||||
|
||||
Health responses are generic and never reveal versions, addresses, schema details, credentials, tenant data, or stack traces.
|
||||
|
||||
| Component | Liveness | Readiness |
|
||||
|---|---|---|
|
||||
| Nginx | process/config responds | TLS virtual host can serve frontend and proxy backend |
|
||||
| Frontend | local static sentinel | index and required built assets/font are readable |
|
||||
| Backend | process responds without dependency calls | expected migration, PostgreSQL, and Redis capabilities are available |
|
||||
| Worker | process probe | PostgreSQL/Redis available, queue consumer registered, heartbeat current |
|
||||
| PostgreSQL | process | authenticated `pg_isready` for configured database/user |
|
||||
| Redis | process | authenticated `PING` and required queue/cache commands |
|
||||
|
||||
Initial health timing target is 10-second interval, 3-second timeout, 30-second start period, and five failures before unhealthy, revised only from measured startup evidence. Dependency failure changes readiness but does not deliberately create liveness restart storms.
|
||||
|
||||
## Nginx, domain, SSE, and TLS
|
||||
|
||||
### Routing
|
||||
|
||||
- Port 80 serves `/.well-known/acme-challenge/` and redirects everything else to `https://zarinpal.amiirkhl.ir$request_uri`.
|
||||
- The certificate and application serve only `zarinpal.amiirkhl.ir`; there is no `www` or alternate domain. Unknown hosts are rejected by a default server.
|
||||
- `/api/` proxies only to the backend with host, scheme, safe client-address chain, and request id.
|
||||
- `/` serves/proxies the frontend; SPA fallback never captures API errors.
|
||||
- Fingerprinted assets receive long immutable caching; HTML/mutable manifests revalidate.
|
||||
- Public metrics, debug, profiling, database, Redis, worker, Docker, or administration routes do not exist.
|
||||
|
||||
### SSE
|
||||
|
||||
- The approved exact analytics SSE route gets a dedicated location.
|
||||
- Upstream HTTP/1.1 is used; `Cookie`, `Last-Event-ID`, request id, host, scheme, and client-forwarding headers are preserved.
|
||||
- Proxy buffering, caching, compression, and transformations are disabled for the route.
|
||||
- Read timeout is safely above the 15-second heartbeat; initial target is 75 seconds.
|
||||
- Every connection/replay is authenticated by the backend. Nginx does not bypass session or tenant checks.
|
||||
|
||||
### TLS
|
||||
|
||||
- Certbot obtains the certificate through HTTP-01 after DNS and port checks.
|
||||
- Staging issuance/renewal is tested before production issuance.
|
||||
- Nginx may temporarily use a self-signed certificate only during isolated first bootstrap; it is never production-ready.
|
||||
- TLS 1.2/1.3 only, with reviewed security headers.
|
||||
- HSTS is enabled only after production issuance and renewal are proven. Preload requires separate approval.
|
||||
- Certificate validity/expiry is checked daily in MVP operations; Prometheus alerting is added in the required post-MVP observability phase.
|
||||
|
||||
## Configuration and secrets
|
||||
|
||||
- A committed safe example names required variables and contains placeholders only.
|
||||
- Follow the Qlockify environment layout: deployment `.env` in the deployment root plus backend/frontend `.env` files in their nested repository roots. All are Git-ignored, mode 0600, and excluded from backups.
|
||||
- Secrets are mounted only into the services that need them and never passed in command-line arguments or embedded in images.
|
||||
- Frontend receives no production secret.
|
||||
- Compose validation and diagnostics must not print resolved secret values.
|
||||
- Backup object-store credentials and encryption passphrase are read from the protected deployment `.env`, following the Qlockify script interface with ZarinPal-specific ParsPack bucket/prefix and separate credentials.
|
||||
- If a secret reaches Git/logs, rotate/revoke it first, then clean history/evidence as a separate incident action.
|
||||
|
||||
### Licensed font
|
||||
|
||||
Frontend commit `64f5ac2` tracks `static/fonts/IRANYekanXVFaNumVF.woff2` and `static/fonts/FontLicense.txt`. The normal frontend server build includes them. Deployment adds no fetch, injection, credential, duplicate binary, or license copy. Smoke checks verify the emitted font resource and content type without reproducing license text.
|
||||
|
||||
## MVP logging and operator checks
|
||||
|
||||
### Logs
|
||||
|
||||
- Backend and worker emit structured JSON to stdout with UTC timestamp, level, service, release id, request id, analytics run/section/job ids where applicable, safe error class, and duration.
|
||||
- Nginx access logs include request id, safe route, status, bytes, request/upstream duration, and upstream status.
|
||||
- Never log passwords, hashes, cookies, raw tokens, payer-card keys, raw CSV rows, database/Redis URLs, backup keys, `.env` contents, or full merchant payloads.
|
||||
- Docker log rotation is explicit; initial target is 10 MiB per file and three files per container, adjusted from incident/disk evidence.
|
||||
- Production debug logging is off by default and time-bounded when enabled.
|
||||
|
||||
### MVP manual checks
|
||||
|
||||
The operator can check:
|
||||
|
||||
- Compose health and restart counts
|
||||
- Nginx/backend/worker/PostgreSQL/Redis logs
|
||||
- public TLS/canonical redirect and generic health
|
||||
- disk space/inodes and database/backup size
|
||||
- Redis memory and queue health
|
||||
- certificate days remaining and renewal result
|
||||
- last verified backup age and retention count
|
||||
- current/previous release records
|
||||
|
||||
An operator checklist/runbook provides exact commands after implementation approval.
|
||||
|
||||
## Required post-MVP observability phase
|
||||
|
||||
Before AI implementation begins, add private Prometheus/Grafana and approved exporters/instrumentation for:
|
||||
|
||||
- HTTP RED metrics with bounded route/status labels and latency histograms
|
||||
- worker queue depth/oldest age, section duration, retries/failures, concurrency, cache hit/miss, and SSE connection/replay behavior
|
||||
- PostgreSQL availability, connections, locks, query duration aggregates, database/volume size
|
||||
- Redis availability, memory, eviction, persistence, connections, and queue state
|
||||
- host/container CPU, memory, filesystem/inodes, OOM/restarts, and network
|
||||
- backup success/age/duration/size/remote verification and restore-rehearsal age
|
||||
- TLS expiry and renewal status
|
||||
|
||||
Grafana and Prometheus remain private. Alerts are symptom-based, actionable, link to runbooks, and use only page/ticket severity. Merchant/user/request/card ids never become metric labels.
|
||||
|
||||
Initial targets to validate:
|
||||
|
||||
| Objective | Target |
|
||||
|---|---|
|
||||
| Public availability | 99.5% over rolling 7 days excluding approved maintenance |
|
||||
| Unexpected API 5xx | under 1% over 5 minutes under acceptance traffic |
|
||||
| Non-analytics API latency | p95 under 500 ms under acceptance load |
|
||||
| Analytics start | p95 under 1 second |
|
||||
| SSE | connection p95 under 2 seconds; healthy heartbeat gap at most 20 seconds |
|
||||
| Section jobs | queue delay p95 under 5 seconds; every attempt at most 60 seconds |
|
||||
| Capacity | at least 20% and 10 GiB free on critical filesystems |
|
||||
| Backup | verified encrypted off-host backup every 24 hours |
|
||||
| Restore | clean rehearsal within two hours |
|
||||
| TLS | valid automatically renewable certificate |
|
||||
|
||||
## Backup and restore
|
||||
|
||||
### Qlockify adaptation boundary
|
||||
|
||||
Reuse and minimally adapt these read-only reference scripts after implementation approval:
|
||||
|
||||
- `qlockify-deployment/scripts/backup.sh`
|
||||
- `qlockify-deployment/scripts/backup-upload-s3.sh`
|
||||
- `qlockify-deployment/scripts/restore.sh`
|
||||
- `qlockify-deployment/scripts/restore-from-s3.sh`
|
||||
|
||||
Necessary changes include deployment root, archive/prefix names, Compose service names, protected credentials, PostgreSQL-only contents, manifest/checksums, exact target validation, and production confirmation guards. The core `pg_dump`/archive, OpenSSL encryption, rclone S3-compatible upload, local/remote retention, download/decrypt, and restore flow remains recognizable.
|
||||
|
||||
### Backup contract
|
||||
|
||||
- Daily at `02:00 UTC`, create a PostgreSQL plain-SQL logical dump using the matching PostgreSQL 18 client, following Qlockify's `pg_dump` flow.
|
||||
- Backup package contains database archive plus non-secret manifest/checksums only.
|
||||
- Encrypt before durable local/remote storage using Qlockify's OpenSSL AES-256-CBC with PBKDF2 and 200,000 iterations; pair it with recorded SHA-256 checksums because CBC encryption is not itself authenticated.
|
||||
- Upload to private S3-compatible storage over TLS and verify the remote object.
|
||||
- Retain the latest three verified encrypted local backups and seven verified encrypted remote backups.
|
||||
- Delete stale backups only after a new remote backup is verified; never delete the last known-good copy.
|
||||
- Exclude `.env`, secrets, Redis, TLS keys, raw CSV, credentials, logs, monitoring state, font binary, and `FontLicense.txt`.
|
||||
- A dump is successful only after plain-SQL header/size/checksum validation, package-content inspection, encryption, upload, remote verification, and manifest completion.
|
||||
|
||||
### Restore contract
|
||||
|
||||
- Default restore target is a new empty non-production database; plain SQL is restored with `psql -X --set ON_ERROR_STOP=on` and a reviewed transaction strategy.
|
||||
- Validate explicit backup id, checksums, manifest, package contents, plain-SQL header, PostgreSQL compatibility, free space, and exact resolved destination before mutation.
|
||||
- Production restore additionally requires environment, host, database, backup id, typed confirmation, maintenance mode, exclusive lock, and named operator.
|
||||
- Never pipe an unchecked dump into the active database or restore over a running destination in place.
|
||||
- Restore into an empty database/cluster, validate counts/invariants/tenant isolation/login/cold Redis analytics/SSE, then switch/reopen traffic.
|
||||
- Rehearse monthly and before destructive migrations; record achieved RPO/RTO.
|
||||
|
||||
## Safe first-server bootstrap
|
||||
|
||||
1. Record provider/instance, OS, network, capacity, recovery console, operators, DNS, and rollback window.
|
||||
2. Prove two key-only operator logins and provider console recovery before disabling root/password SSH.
|
||||
3. Configure default-deny firewall: operator-allowlisted SSH plus public 80/443 only.
|
||||
4. Patch the OS, enable time sync/security updates/log rotation/disk monitoring, and reboot-test access.
|
||||
5. Verify the preinstalled Docker Engine/Compose installation, record its source/version, and ensure only the trusted `ubuntu` user has the required Docker-group access.
|
||||
6. Clone the deployment repository and backend/frontend nested repositories into exact validated paths.
|
||||
7. Create protected configuration/secret paths outside Git and validate permissions.
|
||||
8. Validate Compose and local builds without starting public traffic.
|
||||
9. Point DNS, test ACME staging, issue production TLS, and verify canonical routing.
|
||||
10. Run tracked `scripts/first-start.sh`: start data services, migrate, import/reconcile the pinned archive, then start API/worker/frontend/Nginx and run health checks.
|
||||
11. Run separate demo credential generation, authenticate as at least two merchants, and verify tenant isolation.
|
||||
12. Complete encrypted remote backup and clean restore rehearsal before handoff.
|
||||
|
||||
## Threats and failure behavior
|
||||
|
||||
| Failure/threat | Required behavior |
|
||||
|---|---|
|
||||
| Wrong server/path/repository | Stop before Git, build, volume, or database mutation |
|
||||
| Dirty/diverged checkout | Stop and ask operator; never unconditional hard reset |
|
||||
| Pull/build failure | Keep current containers/release; record evidence |
|
||||
| Missing/malformed secret/config | Fail before mutation without printing value |
|
||||
| PostgreSQL/Redis unhealthy | Do not start/route dependent candidate services |
|
||||
| Migration failure | Stop release; no candidate backend/worker; no automatic down migration |
|
||||
| Import failure | Mark incomplete; keep normal startup independent; no scheduling |
|
||||
| Frontend/font failure | Keep previous frontend; no deployment-side asset workaround |
|
||||
| Worker/Redis loss | Preserve PostgreSQL; reconcile/recompute idempotently |
|
||||
| Certificate failure | Production readiness fails; do not serve plaintext application traffic |
|
||||
| Backup/upload failure | Preserve all prior verified copies and alert/operator-block destructive work |
|
||||
| Restore target mismatch | Abort before database mutation |
|
||||
| Disk/log exhaustion risk | Bounded logs/resources and stop deploy before critical free-space floor |
|
||||
| Spoofed operator | Key-only restricted SSH, named release record, minimal Docker access |
|
||||
| Secret disclosure | File permissions, log allowlists, no env in backup; rotate first if exposed |
|
||||
|
||||
## Always / Ask / Never boundaries
|
||||
|
||||
### Always
|
||||
|
||||
- Verify exact server, paths, repository remotes/branches/cleanliness, and record all three commit hashes before build.
|
||||
- Build from the recorded checkouts and record the resulting release.
|
||||
- Keep Nginx as the only public application entry point and use valid HTTPS.
|
||||
- Separate migrations, API startup, worker startup, CSV import, and credential generation.
|
||||
- Keep Asynq pinned to pre-1.0 `v0.26.0` until compatibility/recovery tests approve a change.
|
||||
- Bound resources, timeouts, retries, logs, and backup retention.
|
||||
- Back up PostgreSQL encrypted off-host and rehearse restore.
|
||||
- Use the ordinary frontend build context for the tracked font/license.
|
||||
|
||||
### Ask first
|
||||
|
||||
- Expose a new port, hostname, admin/debug/docs/metrics route, or third-party endpoint.
|
||||
- Change PostgreSQL/Redis major version, backup provider/encryption, retention, RPO/RTO, or restore behavior.
|
||||
- Run a destructive/non-compatible migration, production restore, or replacement import.
|
||||
- Change the manual latest-`main` deployment rule, repository topology, deploy-user privilege, resource limits, exact image tags, or TLS/HSTS scope.
|
||||
- Add Prometheus/Grafana before MVP is ready or start AI before the observability phase is accepted.
|
||||
- Change frontend ownership/location of the licensed assets.
|
||||
|
||||
### Never
|
||||
|
||||
- Add a CI/CD pipeline, runner, automated deployment trigger, or private image registry without a new approved decision.
|
||||
- Push from an agent session or rewrite/discard user history.
|
||||
- Publish PostgreSQL, Redis, backend, worker, monitoring, or Docker daemon ports to the internet.
|
||||
- Use unconditional `git reset --hard` on a dirty/diverged server checkout.
|
||||
- Run migration or CSV import inside API startup; schedule/watch/poll the CSV.
|
||||
- Include `.env`, secrets, TLS keys, Redis, raw CSV, credentials, logs, or font/license files in database backup.
|
||||
- Log credentials, raw tokens/cookies, payer-card keys, raw payment rows, or secret connection values.
|
||||
- Automatically run down migrations or restore over production without every confirmation guard.
|
||||
- Add another font delivery/injection path or copy the assets into deployment Git.
|
||||
- Disable health/security/backup gates to finish a deployment.
|
||||
|
||||
## Measurable acceptance criteria
|
||||
|
||||
### Manual deployment and rollback
|
||||
|
||||
- All three nested repositories resolve to approved remotes, latest `origin/main`, clean working trees, and recorded commit hashes before build.
|
||||
- An induced pull/build failure leaves the active stack unchanged.
|
||||
- A release record maps the running stack to three Git hashes, local image ids, migration version, operator, backup, and health evidence without secrets.
|
||||
- Selecting the prior release hashes, rebuilding, and running checks completes application rollback within 15 minutes under normal conditions.
|
||||
- No CI/CD workflow, runner configuration, registry dependency, or automated deployment trigger exists.
|
||||
|
||||
### Topology, routing, and lifecycle
|
||||
|
||||
- External scanning finds only approved SSH plus TCP 80/443; all internal services are unreachable directly.
|
||||
- HTTP redirects to `https://zarinpal.amiirkhl.ir`; unknown hosts are rejected; TLS issuance and renewal pass.
|
||||
- `/api/` reaches only backend, application routes reach frontend, and SSE events/heartbeats are delivered without buffering.
|
||||
- Clean startup health-gates PostgreSQL/Redis, runs migration once, then starts API/worker/frontend/Nginx.
|
||||
- Normal startup/deploy never runs import/admin. Identical explicit import does not duplicate business rows.
|
||||
- Redis deletion causes safe recomputation without loss of PostgreSQL payment/session data.
|
||||
- Required font URL returns 200 with correct type, while deployment Git contains no font/license copies.
|
||||
|
||||
### Security and recovery
|
||||
|
||||
- Real secrets remain outside Git with restrictive permissions and do not appear in Compose output/logs.
|
||||
- Docker logs are structured/bounded and an induced backend failure is traceable by request/release id without sensitive fields.
|
||||
- Daily backup contains only PostgreSQL archive plus non-secret manifest/checksums, is encrypted, remotely verified, and retained three local/seven remote.
|
||||
- No excluded `.env`, secret, Redis, TLS, CSV, credential, log, monitoring, font, or license asset appears in the backup.
|
||||
- Clean restore completes within two hours and passes schema/count/tenant/login/cold-cache/worker/SSE validation.
|
||||
- Every missing production restore confirmation factor aborts before mutation.
|
||||
|
||||
### Phase ordering
|
||||
|
||||
- MVP can be operated through health, logs, release records, disk/certificate/backup checks without Prometheus/Grafana.
|
||||
- Prometheus/Grafana/exporters/dashboards/alerts are implemented and accepted after MVP readiness and before any AI implementation begins.
|
||||
|
||||
## Implementation readiness
|
||||
|
||||
No stakeholder-level Platform Operations decisions or real server values are required to plan or implement the deployment artifacts. Follow `QLOCKIFY-ADAPTATION-CONTRACT.md` for the exact source-file mapping and root/backend/frontend environment ownership.
|
||||
|
||||
Tracked examples use blank secret placeholders and safe public defaults. The real server IP/DNS state, ParsPack values, operator identity, and secret values are runtime inputs supplied only when the relevant first-server or production operation is performed. Backend/frontend-owned command and variable names are consumed from their implementation contracts during the corresponding deployment slice; they do not reopen the approved topology or block earlier independent slices.
|
||||
|
||||
Implementation remains gated by explicit stakeholder authorization. If a discovered interface would change an approved behavior rather than fill in a service-owned command/value, amend this spec first.
|
||||
|
||||
## References
|
||||
|
||||
- Docker Compose application model: <https://docs.docker.com/compose/intro/compose-application-model/>
|
||||
- Docker Compose startup order and health conditions: <https://docs.docker.com/compose/how-tos/startup-order/>
|
||||
- Docker logging configuration: <https://docs.docker.com/engine/logging/configure/>
|
||||
- Nginx proxy buffering/timeouts: <https://nginx.org/en/docs/http/ngx_http_proxy_module.html>
|
||||
- Nginx TLS module: <https://nginx.org/en/docs/http/ngx_http_ssl_module.html>
|
||||
- PostgreSQL 18 backup/restore: <https://www.postgresql.org/docs/current/backup.html>
|
||||
- PostgreSQL dump/restore guidance: <https://www.postgresql.org/docs/current/backup-dump.html>
|
||||
- Local read-only Qlockify reference: `D:/Programing/Scripts/Qlockify/qlockify-deployment`
|
||||
- Local adaptation map: `docs/QLOCKIFY-ADAPTATION-CONTRACT.md`
|
||||
134
docs/QLOCKIFY-ADAPTATION-CONTRACT.md
Normal file
134
docs/QLOCKIFY-ADAPTATION-CONTRACT.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Qlockify deployment adaptation contract
|
||||
|
||||
- Status: Approved planning baseline
|
||||
- Source: `D:/Programing/Scripts/Qlockify/qlockify-deployment` (read-only)
|
||||
- Target: `/home/ubuntu/zarinpal-analytics-deployment`
|
||||
- Implementation authorization: Granted by user on 2026-08-20
|
||||
- Last updated: 2026-08-20
|
||||
|
||||
## Purpose
|
||||
|
||||
Use Qlockify's proven single-server deployment shape and environment-file layout for ZarinPal Analytics without requiring production IPs, credentials, or operator names during planning. Real values are supplied on the server when deployment occurs; they are not planning blockers and never enter public Git.
|
||||
|
||||
## Repository layout
|
||||
|
||||
```text
|
||||
/home/ubuntu/zarinpal-analytics-deployment/
|
||||
├── .env ignored deployment/TLS/backup values
|
||||
├── backend/ ignored backend Git checkout
|
||||
│ └── .env ignored backend/runtime values
|
||||
├── frontend/ ignored frontend Git checkout
|
||||
│ └── .env ignored public build values only
|
||||
├── data/incoming/ ignored one-time CSV input
|
||||
├── data/reports/ ignored import reports
|
||||
├── releases/ ignored non-secret release records
|
||||
└── backups/ ignored encrypted local backups
|
||||
```
|
||||
|
||||
This deliberately simplifies Qlockify's extra application subdirectory layer. Each service checkout is directly under the deployment root and keeps an independent `.git` directory.
|
||||
|
||||
## File adaptation matrix
|
||||
|
||||
| Qlockify source | Planned ZarinPal artifact | Adaptation |
|
||||
|---|---|---|
|
||||
| `.env.sample` | `.env.example` | Keep TLS and ParsPack variable names; use the ZarinPal domain/prefix; remove alternate-domain and duplicate database values. |
|
||||
| `backend/.env.sample` | backend-owned `.env.example`, consumed as `backend/.env` | Replace Django, JWT, Celery, OAuth, SMS, and demo-cleanup variables with the Go API/worker/import contracts. |
|
||||
| `frontend/.env.sample` | frontend-owned `.env.example`, consumed as `frontend/.env` | Keep only public same-origin build configuration; never place a secret here. |
|
||||
| `docker-compose.yml` | `compose.yaml` | Keep single-host orchestration and nested build contexts; replace Django/Celery/media services with API, worker, migrate, importer, and admin boundaries. |
|
||||
| `backend/Dockerfile` | deployment backend image definition | Build the backend checkout once and select API/worker/migrate/import/admin commands from the same recorded backend revision. |
|
||||
| `frontend/Dockerfile` | deployment frontend image definition | Build the ordinary frontend checkout, including its tracked font and license. |
|
||||
| `nginx/*` | `nginx/*` | Keep HTTP-01, automatic renewal, temporary bootstrap certificate, and reload flow; use only `zarinpal.amiirkhl.ir` and ZarinPal routes. |
|
||||
| `scripts/deploy.sh` | `scripts/deploy.sh` plus release-record helper | Keep manual Compose build/start structure; Git pulls happen interactively before the script; remove fetch/reset and partial-repository release selection. |
|
||||
| `scripts/backup.sh` | `scripts/backup.sh` | Keep plain-SQL `pg_dump` and temporary archive structure; include PostgreSQL plus non-secret manifest/checksums only. |
|
||||
| `scripts/backup-upload-s3.sh` | same filename | Keep root `.env` loading, OpenSSL/rclone flow, ParsPack support, and 3/7 retention; retain encrypted local artifacts. |
|
||||
| `scripts/restore.sh` | same filename | Keep plain-SQL restore basis; remove env/media restore and add empty-target plus production guards. |
|
||||
| `scripts/restore-from-s3.sh` | same filename | Keep rclone/decrypt handoff; require an explicit object id for production and validate it before restore. |
|
||||
| `.gitea/workflows/*` | none | Do not port. ZarinPal deployment is manual and has no CI/CD pipeline or runner. |
|
||||
|
||||
## Environment layout
|
||||
|
||||
Future tracked examples contain placeholders or safe public defaults only. Production files are Git-ignored, owned by `ubuntu`, mode `0600`, excluded from backup, and loaded without printing resolved values.
|
||||
|
||||
### Deployment root `.env`
|
||||
|
||||
The future `.env.example` uses this deployment-owned contract:
|
||||
|
||||
```dotenv
|
||||
COMPOSE_PROJECT_NAME=zarinpal_analytics
|
||||
|
||||
SSL_CERT_MODE=letsencrypt
|
||||
LETSENCRYPT_EMAIL=
|
||||
PRIMARY_DOMAIN=zarinpal.amiirkhl.ir
|
||||
CERTBOT_STAGING=0
|
||||
|
||||
S3_BACKUP_BUCKET=
|
||||
S3_BACKUP_PREFIX=backup/zarinpal-analytics
|
||||
S3_BACKUP_ENDPOINT_URL=
|
||||
S3_BACKUP_ACCESS_KEY_ID=
|
||||
S3_BACKUP_SECRET_ACCESS_KEY=
|
||||
BACKUP_ENCRYPTION_PASSPHRASE=
|
||||
BACKUP_LOCAL_KEEP_LATEST=3
|
||||
BACKUP_REMOTE_KEEP_LATEST=7
|
||||
```
|
||||
|
||||
There is no `ALT_DOMAINS`, runner, registry, deploy-host, SSH-key, or CI variable. The endpoint, bucket, email, access keys, and passphrase may remain blank in the tracked example; the future validation command requires them only for the operation that consumes them.
|
||||
|
||||
### Backend `backend/.env`
|
||||
|
||||
The backend repository owns the complete Go application example. Deployment requires these semantic groups and consumes their eventual exact backend-owned names without maintaining a second competing template:
|
||||
|
||||
- production mode and structured-log level
|
||||
- API listen address and public origin `https://zarinpal.amiirkhl.ir`
|
||||
- `POSTGRES_HOST=postgres`, port `5432`, database/user/password
|
||||
- `REDIS_HOST=redis`, port `6379`, password, and database selection required by the backend/Asynq contract
|
||||
- session/cookie secrets and secure same-origin cookie settings
|
||||
- analytics TTL, rate limits, worker concurrency `16`, per-merchant concurrency `4`, 60-second attempt timeout, and retry policy
|
||||
- import source/report paths, schema version, readiness timeout, and bounded batch/resource settings
|
||||
|
||||
The same protected file supplies PostgreSQL, Redis, API, worker, migrate, importer, and admin services only with the variables each service needs. Qlockify-only Django, JWT, Celery, Google OAuth, SMS, demo-environment, media, and scheduled-cleanup variables are not carried over.
|
||||
|
||||
### Frontend `frontend/.env`
|
||||
|
||||
The frontend repository owns its exact public variable name. Its production value is the same-origin API prefix, initially `/api/v1`. No database, Redis, session, backup, AvalAI, or other secret is allowed in this file or the browser bundle.
|
||||
|
||||
## Manual deployment flow
|
||||
|
||||
1. The operator manually runs `git pull --ff-only origin main` in deployment, backend, and frontend checkouts and enters source-forge credentials interactively.
|
||||
2. The future deploy command acquires the operation lock and verifies exact paths, remotes, `main`, equality with `origin/main`, and clean trees. It performs no Git fetch, pull, checkout, or reset.
|
||||
3. It records all three commit hashes and validates the root/backend/frontend environment contracts without printing values.
|
||||
4. It validates Compose, builds from the three recorded revisions, starts data services, runs the one-off migration, and replaces application services only after the required gates pass.
|
||||
5. It validates private readiness plus public TLS/routing/font/SSE behavior, observes for 15 minutes, then marks the release active.
|
||||
6. Rollback selects the prior release's three hashes, rebuilds, and re-runs the same checks. It never automatically reverses a migration.
|
||||
|
||||
The ordinary deployment path never invokes CSV import or credential generation. The first server uses the separately planned `first-start.sh` path after migration and before the first public activation.
|
||||
|
||||
## Always / Ask / Never
|
||||
|
||||
### Always
|
||||
|
||||
- Adapt from the named tracked Qlockify source file and record intentional differences.
|
||||
- Keep all three real `.env` files ignored, mode `0600`, and out of backups/logs.
|
||||
- Validate only the variables needed by the requested operation.
|
||||
- Preserve exact-path guards before any mutation, cleanup, restore, or retention action.
|
||||
|
||||
### Ask first
|
||||
|
||||
- Rename a deployment-owned variable after implementation begins.
|
||||
- Add a public origin/domain, external provider, persistent data class, or secret consumer.
|
||||
- Change the manual three-repository release rule or Qlockify-derived backup encryption/retention contract.
|
||||
|
||||
### Never
|
||||
|
||||
- Port Qlockify's Gitea workflows, runner/deploy-key variables, or automated deployment.
|
||||
- Port Qlockify's destructive `git reset --hard` synchronization.
|
||||
- Copy Qlockify credentials, domains, database names, service names, or secret values.
|
||||
- Back up or restore `.env` files, Redis, TLS keys, raw CSV, credentials, logs, or frontend assets.
|
||||
- Create a second font path or private asset-injection workflow.
|
||||
|
||||
## Planning acceptance
|
||||
|
||||
- Every future deployment artifact maps to a named Qlockify source or a documented ZarinPal-only requirement.
|
||||
- Root/backend/frontend environment ownership is unambiguous and has no duplicate secret source.
|
||||
- Tracked examples can be committed with blank secret placeholders and useful safe defaults.
|
||||
- Planning and local validation do not require a server IP, real DNS access, real ParsPack values, or named operators.
|
||||
- Real values are required only at the first-server or operation-specific execution gate.
|
||||
@@ -51,3 +51,24 @@ Applied in the frontend repository's `DESIGN.md`: Persian RTL layout, right-side
|
||||
|
||||
Framework decisions remain subordinate to capability requirements. SvelteKit is used as a TypeScript application framework without assuming SEO/SSR; Go is used for the backend with architecture, DI, database access, and API choices approved through the specification process.
|
||||
|
||||
## Platform operations documentation
|
||||
|
||||
- Docker Compose application model: <https://docs.docker.com/compose/intro/compose-application-model/>
|
||||
- Docker Compose startup order: <https://docs.docker.com/compose/how-tos/startup-order/>
|
||||
- Docker Engine logging: <https://docs.docker.com/engine/logging/configure/>
|
||||
- Nginx proxy module: <https://nginx.org/en/docs/http/ngx_http_proxy_module.html>
|
||||
- Nginx TLS module: <https://nginx.org/en/docs/http/ngx_http_ssl_module.html>
|
||||
- PostgreSQL backup and restore: <https://www.postgresql.org/docs/current/backup.html>
|
||||
- PostgreSQL SQL dumps: <https://www.postgresql.org/docs/current/backup-dump.html>
|
||||
- PostgreSQL official image tags: <https://hub.docker.com/_/postgres/tags>
|
||||
- Redis official image tags: <https://hub.docker.com/_/redis/tags>
|
||||
- Nginx official image tags: <https://hub.docker.com/_/nginx/tags>
|
||||
- Certbot official image tags: <https://hub.docker.com/r/certbot/certbot/tags>
|
||||
|
||||
Applied in `PLATFORM-OPERATIONS-SPEC.md` through health-gated Compose startup, isolated networks and secrets, bounded container logs, TLS/SSE proxy behavior, logical PostgreSQL backup with guarded restore, and the approved exact image tags and resource limits.
|
||||
|
||||
## Qlockify deployment reference
|
||||
|
||||
Local read-only source: `D:/Programing/Scripts/Qlockify/qlockify-deployment`
|
||||
|
||||
Applied as the approved three-repository, manual server-build, Compose, Nginx, ACME, nested-checkout, and encrypted off-host backup reference. ZarinPal keeps that simple deployment shape while excluding environment/secrets from database backup packages, separating migrations/import from application startup, refusing destructive Git synchronization on dirty checkouts, guarding production restore, and omitting scheduled application workers.
|
||||
|
||||
@@ -15,6 +15,5 @@ Use three Git repositories: deployment, Go backend, and SvelteKit frontend. The
|
||||
|
||||
- Service changes can run focused CI and then trigger the deployment workflow.
|
||||
- Deployment remains the authoritative location for Compose, Nginx, backup/restore, and server automation.
|
||||
- A release manifest must record the exact commit SHA of each service; local nested checkouts alone are not sufficient provenance.
|
||||
- A server-side release record must capture the exact deployment, backend, and frontend commit SHAs before each build; local nested checkouts alone are not sufficient provenance.
|
||||
- Cross-repository contract changes require coordinated, backward-compatible rollout or an explicit release plan.
|
||||
|
||||
|
||||
62
docs/decisions/0002-manual-server-build-releases.md
Normal file
62
docs/decisions/0002-manual-server-build-releases.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# ADR 0002: Manual server-build releases
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-08-20
|
||||
|
||||
## Context
|
||||
|
||||
The project has three repositories but one small hackathon server. The Qlockify reference already proves a simple operational model: the deployment repository contains ignored backend/frontend checkouts, a trusted operator pulls source on the server, and Docker Compose builds and starts the stack there.
|
||||
|
||||
An earlier proposed design used CI/CD, a private image registry, immutable image digests, and cross-repository candidate events. The stakeholder explicitly rejected that complexity for this project. No CI/CD pipeline is needed.
|
||||
|
||||
The platform still needs to answer which source is running and how to return to the previous version.
|
||||
|
||||
## Decision
|
||||
|
||||
Use manual Qlockify-style deployment on the production server.
|
||||
|
||||
For a normal release, the trusted `ubuntu` user:
|
||||
|
||||
1. verifies the exact deployment/backend/frontend repository paths, remotes, branches, and clean working trees;
|
||||
2. manually runs `git pull --ff-only origin main` in each checkout, entering the source-forge username/password interactively when prompted;
|
||||
3. invokes the deployment command, which verifies that all three checkouts are clean, on `main`, and equal to `origin/main` without performing any Git pull;
|
||||
4. records the exact three resulting Git commit hashes under the ignored `/home/ubuntu/zarinpal-analytics-deployment/releases/` directory;
|
||||
5. builds application images locally with Docker Compose;
|
||||
6. runs the dedicated migration and health/smoke checks; and
|
||||
7. records the release as active only after validation passes.
|
||||
|
||||
Rollback selects the previous release record, checks out its three hashes, rebuilds locally, and repeats the checks. Database down-migration is never automatic; migrations follow expand/contract compatibility.
|
||||
|
||||
The deploy flow may adapt Qlockify's `deploy.sh`, but Git pulling is a separate operator prerequisite and the deployment command must not store source-forge credentials. It must not unconditionally `git reset --hard`. A dirty or divergent checkout stops for operator resolution so user changes are not destroyed.
|
||||
|
||||
There is no CI/CD workflow, Actions runner, automated cross-repository event, private image registry, or direct automated production mutation.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### CI/CD plus private registry
|
||||
|
||||
- Pros: build-once artifacts, automated checks, stronger image provenance.
|
||||
- Cons: more services, credentials, runner maintenance, workflow coordination, and implementation effort than the hackathon needs.
|
||||
- Rejected: stakeholder chose the simpler manual Qlockify model.
|
||||
|
||||
### Per-service manual deployment
|
||||
|
||||
- Pros: each repository can be changed independently.
|
||||
- Cons: services could be built from incompatible revisions and migration/config changes could race.
|
||||
- Rejected: the deployment repository remains the single manual operational entry point.
|
||||
|
||||
### Git submodules
|
||||
|
||||
- Pros: deployment commits can pin service revisions.
|
||||
- Cons: conflicts with accepted ADR 0001 and the Qlockify-style ignored checkout layout.
|
||||
- Rejected: server release records provide the required three-hash provenance.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The trusted deployment user and production server contain the source/build toolchain.
|
||||
- Deployment is slower and less artifact-reproducible than build-once registry delivery, but exact Git hashes and build records preserve practical traceability.
|
||||
- Source forge availability is required for new deployment and usually for rollback unless the target commits remain locally available.
|
||||
- Manual quality commands and health checks are essential because no pipeline enforces them.
|
||||
- Dirty/diverged checkout handling must fail safely rather than destroying changes.
|
||||
- The server stores active and previous non-secret release records.
|
||||
- Any future CI/CD or registry introduction requires a new approved ADR.
|
||||
47
docs/decisions/0003-consume-licensed-font-from-frontend.md
Normal file
47
docs/decisions/0003-consume-licensed-font-from-frontend.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# ADR 0003: Consume licensed font from the frontend repository
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-08-20
|
||||
|
||||
## Context
|
||||
|
||||
The frontend requires the licensed Iran Yekan X Professional Persian-number variable webfont. The approved ownership decision is implemented by frontend commit `64f5ac2`, which tracks these required files in its ordinary source tree:
|
||||
|
||||
- `static/fonts/IRANYekanXVFaNumVF.woff2`
|
||||
- `static/fonts/FontLicense.txt`
|
||||
|
||||
The deployment repository must not duplicate application assets or introduce a second delivery path.
|
||||
|
||||
## Decision
|
||||
|
||||
Treat both files as ordinary frontend-owned build inputs. Frontend CI builds them through the normal frontend context and the immutable frontend image contains the emitted font resource. Deployment consumes only that normal frontend image/build context.
|
||||
|
||||
Platform Operations does not create a separate asset-injection path, copy either file into deployment Git, or maintain font-specific credentials. Release provenance is the recorded frontend Git commit and the local image built from that checkout.
|
||||
|
||||
Frontend CI and deployment smoke checks must fail if the required source inputs are absent, the built resource is absent, or the resource is not served with the expected font MIME type. The deployment repository records no second binary or license copy.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
### Separate CI asset injection
|
||||
|
||||
- Rejected: it conflicts with the approved direct-tracking decision and creates a second source of build inputs.
|
||||
|
||||
### Copy assets into the deployment repository
|
||||
|
||||
- Pros: centralizes production build inputs.
|
||||
- Cons: duplicates proprietary files, violates repository ownership, and can diverge from frontend source.
|
||||
- Rejected: frontend owns static application assets.
|
||||
|
||||
### Runtime host mount
|
||||
|
||||
- Pros: keeps the binary outside the image.
|
||||
- Cons: runtime drift, missing-asset failure after a deploy, harder rollback/provenance, and special server bootstrap.
|
||||
- Rejected: the normal immutable frontend artifact must be self-contained.
|
||||
|
||||
## Consequences
|
||||
|
||||
- Frontend commit `64f5ac2` is the first approved provenance point for the tracked assets.
|
||||
- Frontend repository policy and license compliance govern changes to the files.
|
||||
- Deployment rollback restores the recorded frontend commit and rebuilds its font output through the normal context.
|
||||
- No separate font injection workflow, font-specific deployment secret, or deployment-repository font copy is allowed.
|
||||
- Platform smoke tests verify the emitted resource without reading or reproducing the license text.
|
||||
156
docs/runbooks/BACKUP-RESTORE.md
Normal file
156
docs/runbooks/BACKUP-RESTORE.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# PostgreSQL backup and restore runbook
|
||||
|
||||
- Status: Approved procedure specification; scripts not yet implemented
|
||||
- RPO: 24 hours
|
||||
- RTO: two hours
|
||||
- Last updated: 2026-08-20
|
||||
|
||||
This runbook adapts Qlockify's logical dump, encrypted S3-compatible upload, local/remote retention, and restore flow while removing environment files from the backup and adding exact-destination/production confirmation guards.
|
||||
|
||||
## Scope and exclusions
|
||||
|
||||
Back up authoritative PostgreSQL data and the minimum manifest required to validate it. Do not include:
|
||||
|
||||
- Redis/AOF/cache/queue data
|
||||
- `.env` or secret files
|
||||
- deployment SSH and object-store credentials
|
||||
- backup decryption key
|
||||
- TLS private keys or ACME account data
|
||||
- raw challenge CSV archives
|
||||
- demo plaintext credential exports
|
||||
- frontend font/license files
|
||||
- Prometheus/Grafana runtime volumes or container logs
|
||||
|
||||
Redis loss is recovered through empty Redis plus idempotent analytics reconciliation/recomputation, not restore.
|
||||
|
||||
## Backup package contract
|
||||
|
||||
Each encrypted artifact corresponds to one manifest with:
|
||||
|
||||
```text
|
||||
format_version
|
||||
backup_id
|
||||
created_at_utc
|
||||
completed_at_utc
|
||||
source_environment
|
||||
source_host_id
|
||||
postgres_server_version
|
||||
dump_client_version
|
||||
database_name
|
||||
database_archive_sha256
|
||||
database_archive_bytes
|
||||
schema_migration_version
|
||||
active_release_id
|
||||
deployment_sha
|
||||
backend_sha
|
||||
frontend_sha
|
||||
encryption_scheme_version
|
||||
```
|
||||
|
||||
The manifest stores no password, connection URL, secret value, license identifier/text, raw font copy, CSV path/content, or merchant data sample. A checksum of the encrypted remote object is recorded separately from the plaintext archive checksum.
|
||||
|
||||
## Scheduled backup procedure
|
||||
|
||||
Run daily at `02:00 UTC` from the `ubuntu` user's reviewed host cron entry.
|
||||
|
||||
1. Acquire a backup lock that conflicts with restore and destructive migration but need not block ordinary application reads/writes.
|
||||
2. Resolve and verify the exact production host/database identity. Validate free disk for temporary output plus the encrypted artifact.
|
||||
3. Confirm PostgreSQL health, client/server major compatibility, active three-hash release id, and migration version.
|
||||
4. Create a consistent PostgreSQL 18 plain-SQL dump using Qlockify's reviewed `pg_dump --format=plain --clean --if-exists --no-owner --no-privileges` pattern. Capture stderr and fail on a non-zero exit.
|
||||
5. Reject an empty output, verify the expected PostgreSQL dump header/format, and calculate its SHA-256.
|
||||
6. Build the non-secret manifest and calculate the plaintext archive SHA-256.
|
||||
7. Encrypt the package before durable local retention with Qlockify's `openssl enc -aes-256-cbc -salt -pbkdf2 -iter 200000` flow. The manifest records the scheme/version and SHA-256 integrity evidence because CBC is not authenticated encryption.
|
||||
8. Upload over TLS to the private S3-compatible destination. Upload to a temporary object key, verify remote size/checksum/metadata, then finalize the immutable timestamped key.
|
||||
9. Mark the backup successful only after remote verification. Record success timestamp, duration, and size in structured MVP evidence; export the same signals as metrics in the required post-MVP observability phase.
|
||||
10. Retain the newest three verified encrypted local artifacts and seven verified encrypted remote artifacts. Delete stale artifacts only after the new remote object is verified; never delete the last known-good local or remote backup.
|
||||
11. Release the lock and ensure temporary plaintext files are removed from the exact temporary directory.
|
||||
|
||||
A cleanup failure is a ticket, not a reason to mark the backup failed if the verified new copy exists. An upload/verification failure is a backup failure and preserves all prior copies.
|
||||
|
||||
## Daily verification
|
||||
|
||||
- last verified remote backup age is under 30 hours
|
||||
- archive/manifest/encrypted-object checksums and sizes are non-zero and agree
|
||||
- retention counts are at least one and no greater than approved limits after cleanup
|
||||
- backup duration/size are within expected bounds or investigated
|
||||
- no excluded filenames/classes appear in the package-content inspection
|
||||
- local backup directory and remote bucket are not publicly readable
|
||||
- decryption key is retrievable from separate escrow by authorized operators without exposing it in the check record
|
||||
|
||||
## Clean-destination restore rehearsal
|
||||
|
||||
Run monthly and before any approved destructive migration.
|
||||
|
||||
1. Select an explicit backup id, not an unexamined implicit `latest`, and record why it was chosen.
|
||||
2. Provision an empty non-production destination with compatible PostgreSQL and enough free space. Confirm its environment/hostname/database do not match production.
|
||||
3. Download the exact encrypted object to a new restrictive temporary directory.
|
||||
4. Verify remote-object checksum/size, decrypt, validate the package and plain-SQL checksum/header, and parse the manifest.
|
||||
5. Treat the dump as trusted code only if it came from the approved bucket/path and passes provenance checks. PostgreSQL warns that restore executes statements controlled by the dump source.
|
||||
6. Create a new empty database from `template0` or the equivalent approved empty-cluster procedure. Do not reuse a partially populated database.
|
||||
7. Restore with `psql -X --set ON_ERROR_STOP=on` and the reviewed transaction/owner/privilege behavior. Preserve complete stdout/stderr in a restricted evidence path without secrets.
|
||||
8. Check out/build the selected release record if needed, then run its validation suite:
|
||||
- expected migration/import manifests
|
||||
- representative table/session/attempt counts and invariants
|
||||
- at least two merchant logins and cross-tenant denial
|
||||
- payer-card keys absent from API/log output
|
||||
- Redis initially empty, analytics recomputation succeeds, four sections/SSE work
|
||||
- frontend and API compatibility for the manifest's release
|
||||
9. Record elapsed time, achieved RPO, achieved RTO, sizes/counts, validation result, operator, and remediation.
|
||||
10. Destroy rehearsal resources only after evidence is retained and the exact destination is revalidated.
|
||||
|
||||
## Production restore approval guard
|
||||
|
||||
Production restore is destructive/exclusive and always requires explicit human approval. The future implementation must require all of these independent inputs:
|
||||
|
||||
- `RESTORE_ENVIRONMENT=production`
|
||||
- exact expected host identity matching the current provider instance
|
||||
- exact expected database name
|
||||
- explicit backup id/object key (no automatic `latest`)
|
||||
- a typed confirmation phrase containing both production and the backup id
|
||||
- active maintenance mode and deployment/restore lock
|
||||
- approved incident/change record and named operator
|
||||
- current-state backup when PostgreSQL is readable and doing so will not worsen the incident
|
||||
|
||||
If any input is absent or mismatched, exit before stopping writers, dropping/renaming a database, extracting over a target, or running `psql`.
|
||||
|
||||
## Production restore sequence
|
||||
|
||||
1. Announce maintenance and stop public writes/new sessions. Preserve an operator-only health path.
|
||||
2. Acquire the exclusive deployment/restore lock; stop worker before backend so no jobs write during restore.
|
||||
3. Capture the active/previous three-hash release records, database identity, incident timestamps, and current-state backup when safe.
|
||||
4. Perform all download/decrypt/checksum/archive/provenance checks from the rehearsal procedure before mutating database state.
|
||||
5. Restore into a newly created empty database/cluster. Do not pipe an unchecked dump directly into the active production database and do not recursively delete a computed volume/path.
|
||||
6. Validate restore completeness and application invariants using the three recorded repository commits compatible with the backup manifest. Apply only explicitly approved forward migrations.
|
||||
7. Start with empty Redis. Start backend/worker, confirm readiness, idempotent stale-run reconciliation, cold-cache analytics, sessions, and SSE behavior.
|
||||
8. Switch application connectivity to the validated restored destination using the approved implementation strategy.
|
||||
9. Run public TLS/login/dashboard plus private database/queue/tenant smoke checks. Observe for 15 minutes.
|
||||
10. Reopen traffic, mark the restore complete, and schedule a fresh backup. Retain the old database until the incident owner approves disposal under a separate exact-target procedure.
|
||||
|
||||
Do not automatically restore `.env`/secrets, TLS, Redis, or frontend assets from a database backup. Reconstruct platform configuration from Git/release records and secrets from separate escrow.
|
||||
|
||||
## Failure handling
|
||||
|
||||
| Failure | Action |
|
||||
|---|---|
|
||||
| Backup lock already held | Exit without starting a second backup; alert if stale |
|
||||
| `pg_dump` non-zero, empty, or invalid plain-SQL output | Delete only the exact failed temporary artifact; keep prior backups; page on age threshold |
|
||||
| Encryption failure | Do not persist/upload plaintext; secure exact temp path and investigate |
|
||||
| Remote upload/checksum failure | Keep encrypted local artifact and all prior remote copies; bounded retry; alert |
|
||||
| Retention cleanup failure | Keep extra copies; ticket; never broaden deletion globs/paths |
|
||||
| Manifest/archive mismatch | Quarantine artifact and select another known-good backup |
|
||||
| Restore SQL/object failure | Keep traffic closed, preserve logs/destination, do not rerun blindly into partial DB |
|
||||
| Post-restore invariant failure | Do not switch/reopen; return to old database if safe or select another backup |
|
||||
| Missing decryption key | Stop; encrypted backup is unrecoverable without the separately escrowed key |
|
||||
| Wrong environment/host/database confirmation | Abort before mutation and record denied attempt |
|
||||
|
||||
## Acceptance evidence
|
||||
|
||||
- [ ] Daily backup completed and verified remotely within 30 hours
|
||||
- [ ] Backup contains PostgreSQL archive plus non-secret manifest only
|
||||
- [ ] Three local/seven remote verified encrypted artifacts retained
|
||||
- [ ] Restore rehearsal uses an explicit backup id and clean non-production database
|
||||
- [ ] Archive trust/checksum/listing/compatibility checks precede restore
|
||||
- [ ] Application, tenant, session, cold-cache, worker, and SSE validations pass
|
||||
- [ ] Achieved RPO <=24 hours and RTO <=2 hours recorded
|
||||
- [ ] Every missing production confirmation factor aborts before mutation
|
||||
- [ ] No Redis, env, secrets, TLS keys, CSV, credentials, font/license, logs, or monitoring runtime data enter backup
|
||||
157
docs/runbooks/FIRST-SERVER-BOOTSTRAP.md
Normal file
157
docs/runbooks/FIRST-SERVER-BOOTSTRAP.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# First-server bootstrap runbook
|
||||
|
||||
- Status: Approved procedure specification; commands/artifacts not yet implemented
|
||||
- Target: `zarinpal.amiirkhl.ir`
|
||||
- Owner: Platform operator
|
||||
- Last updated: 2026-08-20
|
||||
|
||||
This is the safe order for preparing a new production host. It is intentionally a human-reviewed checklist, not a copy-paste bootstrap script. Stop on any failed validation and preserve the existing server/DNS state until the failure is understood.
|
||||
|
||||
## Preconditions and recorded inputs
|
||||
|
||||
Before connecting to the host, record in the change ticket/release record:
|
||||
|
||||
- server provider, instance id, public IPv4/IPv6, region, CPU, RAM, disk type/size, and console/recovery access
|
||||
- Ubuntu 24.04 LTS image/source for the new 2-core/2-GB server, with Docker already installed and the `ubuntu` user available
|
||||
- operator names, maintenance window, rollback/abandonment condition, and emergency contact
|
||||
- DNS zone owner and current `A`/`AAAA`/CAA records for `zarinpal.amiirkhl.ir`
|
||||
- SSH source allowlist and at least two tested operator public keys
|
||||
- confirmation that the trusted `ubuntu` user has the required Docker-group access
|
||||
- approved deployment, backend, and frontend repository URLs and expected nested paths
|
||||
- object-storage bucket/endpoint, retention capability, backup credentials, and separately escrowed encryption key
|
||||
- target `main` branches and an empty location for the first three-hash release record
|
||||
- secret inventory and owner; never put values in the ticket, shell history, or this repository
|
||||
|
||||
Do not change DNS before the host passes local and direct-IP validation. Do not reuse Qlockify credentials, certificates, volumes, backup paths, or Git credentials.
|
||||
|
||||
## Phase 1: establish recoverable access
|
||||
|
||||
1. Open the provider console/recovery channel and confirm it works independently of SSH.
|
||||
2. Log in using the provider's initial method. Create named operator accounts and install only approved public keys.
|
||||
3. Confirm a second operator can log in in a new session before changing SSH configuration.
|
||||
4. Disable direct root SSH and password authentication only after key access and recovery console are proven.
|
||||
5. Restrict SSH to the operator source allowlist at the provider firewall and host firewall. Keep the current session open while testing a new one.
|
||||
6. Configure time synchronization and verify UTC plus the documented `Asia/Tehran` application assumption. The host itself should log in UTC.
|
||||
7. Apply all supported security updates and reboot if required. Re-test console and SSH access after reboot.
|
||||
|
||||
Evidence: OS release/kernel, update result, time-sync state, two successful key logins, failed password/root-login test, and firewall rules. Do not record public key private material or session output containing secrets.
|
||||
|
||||
## Phase 2: harden and size the host
|
||||
|
||||
1. Set a unique hostname and record the provider instance id alongside it.
|
||||
2. Configure default-deny inbound firewall rules. Initially allow SSH from the operator allowlist and TCP 80/443; verify from both an allowed and disallowed external source.
|
||||
3. Enable unattended security updates with a documented reboot policy and alert path.
|
||||
4. Configure disk/inode monitoring, time sync, audit/system logs, log rotation, and OOM visibility before application deployment.
|
||||
5. Create the approved Qlockify-style runtime paths with explicit `ubuntu` ownership:
|
||||
|
||||
```text
|
||||
/home/ubuntu/zarinpal-analytics-deployment/ deployment repository and root .env
|
||||
/home/ubuntu/zarinpal-analytics-deployment/backend/ ignored backend checkout and backend .env
|
||||
/home/ubuntu/zarinpal-analytics-deployment/frontend/ ignored frontend checkout and frontend .env
|
||||
/home/ubuntu/zarinpal-analytics-deployment/releases/ ignored non-secret release records
|
||||
/home/ubuntu/zarinpal-analytics-deployment/backups/ ignored encrypted local backup retention
|
||||
```
|
||||
|
||||
6. Verify every resolved absolute path is on the intended filesystem, not a symlink to a broad/unexpected target, before applying ownership or any future recursive operation.
|
||||
7. Reserve enough free space for PostgreSQL growth, one temporary logical dump, three encrypted local backups, container images, and logs while retaining the platform alert floor of at least 20% and 10 GiB free.
|
||||
8. Create and enable the approved 2 GB swap file, use restrictive permissions, persist it in `/etc/fstab`, and verify it after reboot.
|
||||
|
||||
## Phase 3: install and constrain Docker
|
||||
|
||||
1. Verify the preinstalled Docker Engine and Compose plugin came from Docker's approved Ubuntu repository; update/install only if verification fails, then record installed versions.
|
||||
2. Confirm only the trusted `ubuntu` user has the required `docker` group access. Record that this access is effectively root-equivalent and restrict the account accordingly.
|
||||
3. Configure the daemon's bounded logging policy before starting application containers.
|
||||
4. Ensure Docker starts after reboot and that no unauthenticated TCP daemon socket is enabled.
|
||||
5. Pull a harmless pinned public test image, verify its tag/digest, run it without published ports, then remove the test container.
|
||||
6. Confirm the `ubuntu` user can run Docker Compose without using a broader shared administrator account.
|
||||
|
||||
Evidence: Engine/Compose versions, daemon security configuration, socket/group ownership, deploy-user membership, log driver/options, and test-image result.
|
||||
|
||||
## Phase 4: install the deployment control plane
|
||||
|
||||
1. From the `ubuntu` account, clone the deployment repository into `/home/ubuntu/zarinpal-analytics-deployment`, then clone backend and frontend into its exact ignored nested paths. Enter the source-forge HTTPS username/password interactively; do not configure a credential helper, token, or deploy key.
|
||||
2. Confirm no Git credential was persisted in repository configuration, shell scripts, environment files, or the operating-system credential store.
|
||||
3. Validate ownership, remote URL, `main` branch, current SHA, clean status, and independent `.git` directory for every repository.
|
||||
4. Create the Qlockify-style Git-ignored environment files: deployment-root `.env` plus nested backend/frontend `.env`, all owned by `ubuntu`, mode 0600, and excluded from backup. Verify consumers receive only required values.
|
||||
5. Manually run `git pull --ff-only origin main` in each repository, entering HTTPS credentials interactively, and confirm each checkout equals `origin/main`; do not reset, build, or start yet.
|
||||
6. Record the three initial commit hashes and selected infrastructure image tags in a non-secret draft release record.
|
||||
7. Render/validate Compose configuration without printing secret values. Confirm only Nginx publishes 80/443.
|
||||
|
||||
Do not run `docker compose up`, migrations, import, credential generation, restore, or DNS cutover during this phase.
|
||||
|
||||
## Phase 5: DNS and TLS staging
|
||||
|
||||
1. Lower DNS TTL in advance if an existing record is moving. Preserve the previous values and rollback time.
|
||||
2. Point `A` (and `AAAA` only if IPv6 is actually routed/firewalled) for `zarinpal.amiirkhl.ir` after direct-IP Nginx/host validation. No `www` or alternate hostname is configured.
|
||||
3. Verify public authoritative DNS from multiple resolvers and confirm the address matches the current server.
|
||||
4. Start only the minimum edge services required for ACME HTTP-01. Verify the challenge path from an external network.
|
||||
5. Obtain a staging certificate first and exercise renewal. Then request the production certificate.
|
||||
6. Validate certificate names, chain, expiry, TLS 1.2/1.3, canonical redirects, unknown-host rejection, and renewal permissions.
|
||||
7. Enable HSTS only after successful production issuance and renewal evidence; preload remains a separate approval.
|
||||
|
||||
If issuance fails, stop repeated production requests, inspect DNS/CAA/firewall/challenge routing, and continue serving no plaintext application content.
|
||||
|
||||
## Phase 6: first database, dataset, and application release
|
||||
|
||||
1. Acquire the production deployment lock and record the operator/release id.
|
||||
2. Place the approved `challenge_data.csv.gz` at the exact Git-ignored deployment `data/incoming/` path. Verify regular-file/read-only properties, size, and pinned SHA-256; do not copy it into an image.
|
||||
3. Invoke tracked `scripts/first-start.sh`. It starts PostgreSQL and Redis, waits with bounded backoff, applies the one-off migration, runs the one-shot importer with a read-only mount, and reconciles the pinned counts before starting public services.
|
||||
4. Review the JSON/Markdown import report and durable import-run/issues metadata. Fatal validation, resource, migration, or reconciliation failure must exit nonzero before backend/frontend/Nginx activation.
|
||||
5. Verify backend and worker liveness/readiness, worker heartbeat, Asynq `v0.26.0` compatibility, queue limits, and absence of unintended published ports.
|
||||
6. Verify the frontend static sentinel plus the emitted Iran Yekan font resource. The source font/license remain frontend-owned; no deployment-side injection or copy occurs.
|
||||
7. Verify Nginx public generic health, canonical redirect, security headers, login page, SSE behavior, and that only 80/443 are public.
|
||||
8. Run demo credential generation as a separate explicit administrative job. Store one-time plaintext output outside Git/logs/backups.
|
||||
9. Authenticate as at least two merchant accounts, verify different dashboards and cross-tenant isolation, then remove or escrow plaintext credential output.
|
||||
10. Verify MVP operator checks: Compose health/restarts, structured logs, disk, certificate, queue/Redis state, and backup destination access.
|
||||
11. Record the active three-hash release only after all postflight checks pass.
|
||||
|
||||
Failure before the active-release switch restores the prior/empty known state. Do not improvise a database down migration.
|
||||
|
||||
## Phase 7: bootstrap evidence review
|
||||
|
||||
1. Confirm the sole completed import run matches the pinned checksum, schema, source/session/attempt/merchant/terminal/category counts, and contains no fatal issue.
|
||||
2. Confirm no raw staging remains and the ordinary `docker compose up -d` path cannot invoke migration/import/admin profiles.
|
||||
3. Run `first-start.sh` a second time only in the controlled acceptance rehearsal and confirm the identical dataset is a safe no-op rather than another promotion.
|
||||
4. Archive only safe non-sensitive release/import evidence; exclude the CSV, raw rows, reports containing restricted values, and credentials.
|
||||
|
||||
Never add the source archive or credentials to Git, a container image, backup package, or diagnostic bundle.
|
||||
|
||||
## Phase 8: backup and handoff gate
|
||||
|
||||
1. Run the first encrypted logical PostgreSQL backup.
|
||||
2. Verify package contents, plain-SQL header/checksum, encrypted local artifact, remote upload, remote checksum, manifest, and retention counts.
|
||||
3. Restore that backup to a clean non-production destination and complete application/tenant/analytics validation within the two-hour RTO.
|
||||
4. Perform and record the MVP manual certificate, disk, backup-age, availability, API error, SSE heartbeat, and queue-age checks.
|
||||
5. Record current release, DNS, certificate, backup id, achieved RPO/RTO, remaining exceptions, and on-call ownership.
|
||||
6. Remove temporary bootstrap credentials/files and close provider rules that are no longer needed.
|
||||
|
||||
The host is not considered MVP-ready until restore rehearsal and every manual operator check passes. Prometheus/Grafana and automated alert delivery are the required next phase before AI work.
|
||||
|
||||
## Stop/rollback conditions
|
||||
|
||||
Stop without progressing when:
|
||||
|
||||
- recovery console or second-operator SSH access is not proven
|
||||
- resolved paths, host identity, repository remotes/branches, environment, or release record do not match
|
||||
- a repository is dirty/diverged unexpectedly or a build/source dependency cannot be verified
|
||||
- unexpected ports are externally reachable
|
||||
- DNS points somewhere unexpected or certificate names/chain are invalid
|
||||
- migration, readiness, authenticated smoke, tenant isolation, worker/SSE, manual operational checks, backup, or restore rehearsal fails
|
||||
- secrets appear in output, files have broad permissions, or the deployment checkout is dirty unexpectedly
|
||||
|
||||
Rollback DNS to the recorded prior values when the new host cannot pass the cutover window. Preserve logs and manifests, revoke any exposed credential, and do not delete the failed host until evidence and required backups are secured.
|
||||
|
||||
## Completion evidence
|
||||
|
||||
- [ ] Provider/host identity and capacity recorded
|
||||
- [ ] Console plus two key-only operator paths proven
|
||||
- [ ] Firewall external tests show only approved ports
|
||||
- [ ] Docker/Compose versions and privilege model recorded
|
||||
- [ ] Three repository remotes/branches/SHAs and release record verified
|
||||
- [ ] Secrets outside Git with restrictive ownership/mode
|
||||
- [ ] TLS issuance and renewal tested
|
||||
- [ ] Migration, API, worker, frontend/font, Nginx, SSE, and tenant smoke pass
|
||||
- [ ] One-time import reconciles and demo credentials stay outside logs/Git
|
||||
- [ ] MVP manual health/log/disk/certificate/backup/queue checks pass
|
||||
- [ ] Encrypted off-host backup and clean restore rehearsal pass within RTO
|
||||
- [ ] Active/previous release ids and operator handoff recorded
|
||||
222
scripts/LocalDev.psm1
Normal file
222
scripts/LocalDev.psm1
Normal file
@@ -0,0 +1,222 @@
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Read-DotEnvFile {
|
||||
[CmdletBinding()]
|
||||
param([Parameter(Mandatory)][string]$Path)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "Environment file not found: $Path"
|
||||
}
|
||||
|
||||
$values = @{}
|
||||
$lineNumber = 0
|
||||
foreach ($rawLine in Get-Content -LiteralPath $Path) {
|
||||
$lineNumber++
|
||||
$line = $rawLine.Trim()
|
||||
if (-not $line -or $line.StartsWith('#')) { continue }
|
||||
if ($line -notmatch '^([A-Za-z_][A-Za-z0-9_]*)=(.*)$') {
|
||||
throw "invalid dotenv entry at ${Path}:$lineNumber"
|
||||
}
|
||||
|
||||
$name = $Matches[1]
|
||||
$value = $Matches[2]
|
||||
if ($values.ContainsKey($name)) {
|
||||
throw "duplicate variable '$name' in $Path"
|
||||
}
|
||||
if ($value.Length -ge 2 -and (($value[0] -eq '"' -and $value[-1] -eq '"') -or ($value[0] -eq "'" -and $value[-1] -eq "'"))) {
|
||||
$value = $value.Substring(1, $value.Length - 2)
|
||||
}
|
||||
$values[$name] = $value
|
||||
}
|
||||
return $values
|
||||
}
|
||||
|
||||
function Resolve-LocalDevCommands {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$BackendPath,
|
||||
[Parameter(Mandatory)][string]$FrontendPath
|
||||
)
|
||||
|
||||
foreach ($entrypoint in 'api', 'worker') {
|
||||
$path = Join-Path $BackendPath "cmd\$entrypoint"
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Container)) {
|
||||
throw "Backend entrypoint cmd/$entrypoint is missing under $BackendPath"
|
||||
}
|
||||
}
|
||||
|
||||
$packageJsonPath = Join-Path $FrontendPath 'package.json'
|
||||
if (-not (Test-Path -LiteralPath $packageJsonPath -PathType Leaf)) {
|
||||
throw "Frontend package.json is missing under $FrontendPath"
|
||||
}
|
||||
$packageJson = Get-Content -Raw -LiteralPath $packageJsonPath | ConvertFrom-Json
|
||||
if (-not $packageJson.scripts -or -not $packageJson.scripts.dev) {
|
||||
throw "Frontend package.json does not define the required 'dev' script"
|
||||
}
|
||||
|
||||
return @{
|
||||
Api = @{ FilePath = 'go'; Arguments = @('run', './cmd/api'); WorkingDirectory = $BackendPath }
|
||||
Worker = @{ FilePath = 'go'; Arguments = @('run', './cmd/worker'); WorkingDirectory = $BackendPath }
|
||||
Frontend = @{ FilePath = 'npm.cmd'; Arguments = @('run', 'dev', '--', '--host', '127.0.0.1'); WorkingDirectory = $FrontendPath }
|
||||
}
|
||||
}
|
||||
|
||||
function Test-TcpEndpoint {
|
||||
[CmdletBinding()]
|
||||
param([Parameter(Mandatory)][string]$HostName, [Parameter(Mandatory)][int]$Port, [int]$TimeoutMilliseconds = 1000)
|
||||
|
||||
$client = [System.Net.Sockets.TcpClient]::new()
|
||||
try {
|
||||
$task = $client.ConnectAsync($HostName, $Port)
|
||||
return $task.Wait($TimeoutMilliseconds) -and $client.Connected
|
||||
}
|
||||
catch { return $false }
|
||||
finally { $client.Dispose() }
|
||||
}
|
||||
|
||||
function Get-StatePath {
|
||||
param([string]$DeploymentPath)
|
||||
return Join-Path $DeploymentPath '.local\processes.json'
|
||||
}
|
||||
|
||||
function Get-RecordedProcesses {
|
||||
param([string]$DeploymentPath)
|
||||
$statePath = Get-StatePath -DeploymentPath $DeploymentPath
|
||||
if (-not (Test-Path -LiteralPath $statePath -PathType Leaf)) { return @() }
|
||||
$items = @(Get-Content -Raw -LiteralPath $statePath | ConvertFrom-Json)
|
||||
return $items
|
||||
}
|
||||
|
||||
function Test-RecordedProcess {
|
||||
param($Record)
|
||||
|
||||
try {
|
||||
$recordedProcessId = [int]$Record.Pid
|
||||
if ($recordedProcessId -le 0) { return $false }
|
||||
$recordedStartTime = if ($Record.StartTimeUtc -is [datetime]) {
|
||||
$Record.StartTimeUtc.ToUniversalTime()
|
||||
}
|
||||
else {
|
||||
[datetime]::Parse(
|
||||
[string]$Record.StartTimeUtc,
|
||||
[System.Globalization.CultureInfo]::InvariantCulture,
|
||||
[System.Globalization.DateTimeStyles]::RoundtripKind
|
||||
).ToUniversalTime()
|
||||
}
|
||||
}
|
||||
catch { return $false }
|
||||
|
||||
$process = Get-Process -Id $recordedProcessId -ErrorAction SilentlyContinue
|
||||
if (-not $process) { return $false }
|
||||
try {
|
||||
return $process.StartTime.ToUniversalTime().Ticks -eq $recordedStartTime.Ticks
|
||||
}
|
||||
catch { return $false }
|
||||
}
|
||||
|
||||
function Invoke-LocalDevCheck {
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$DeploymentPath,
|
||||
[Parameter(Mandatory)][string]$BackendPath,
|
||||
[Parameter(Mandatory)][string]$FrontendPath,
|
||||
[string]$PostgresHost = '127.0.0.1',
|
||||
[int]$PostgresPort = 5432,
|
||||
[string]$RedisHost = '127.0.0.1',
|
||||
[int]$RedisPort = 6379
|
||||
)
|
||||
|
||||
$null = Resolve-LocalDevCommands -BackendPath $BackendPath -FrontendPath $FrontendPath
|
||||
foreach ($tool in 'go', 'npm.cmd') {
|
||||
if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) { throw "Required command is not installed or on PATH: $tool" }
|
||||
}
|
||||
$backendEnv = Join-Path $BackendPath '.env'
|
||||
if (-not (Test-Path -LiteralPath $backendEnv -PathType Leaf)) { throw "Backend .env is missing: $backendEnv" }
|
||||
$null = Read-DotEnvFile -Path $backendEnv
|
||||
if (-not (Test-TcpEndpoint -HostName $PostgresHost -Port $PostgresPort)) { throw "PostgreSQL is not reachable at ${PostgresHost}:$PostgresPort" }
|
||||
if (-not (Test-TcpEndpoint -HostName $RedisHost -Port $RedisPort)) { throw "Redis is not reachable at ${RedisHost}:$RedisPort" }
|
||||
|
||||
[pscustomobject]@{ Go = 'ok'; Node = 'ok'; PostgreSQL = 'reachable'; Redis = 'reachable'; Contracts = 'ok' }
|
||||
}
|
||||
|
||||
function Start-LocalProcess {
|
||||
param([string]$Name, [hashtable]$Command, [hashtable]$Environment, [string]$LogDirectory)
|
||||
|
||||
$resolved = Get-Command $Command.FilePath -ErrorAction Stop
|
||||
$previous = @{}
|
||||
foreach ($entry in $Environment.GetEnumerator()) {
|
||||
$previous[$entry.Key] = [Environment]::GetEnvironmentVariable($entry.Key, 'Process')
|
||||
[Environment]::SetEnvironmentVariable($entry.Key, [string]$entry.Value, 'Process')
|
||||
}
|
||||
try {
|
||||
$stdout = Join-Path $LogDirectory "$Name.out.log"
|
||||
$stderr = Join-Path $LogDirectory "$Name.err.log"
|
||||
$process = Start-Process -FilePath $resolved.Source -ArgumentList $Command.Arguments -WorkingDirectory $Command.WorkingDirectory -RedirectStandardOutput $stdout -RedirectStandardError $stderr -WindowStyle Hidden -PassThru
|
||||
}
|
||||
finally {
|
||||
foreach ($entry in $previous.GetEnumerator()) {
|
||||
[Environment]::SetEnvironmentVariable($entry.Key, $entry.Value, 'Process')
|
||||
}
|
||||
}
|
||||
Start-Sleep -Milliseconds 750
|
||||
if ($process.HasExited) { throw "$Name exited during startup; inspect $stderr" }
|
||||
$process.Refresh()
|
||||
return [pscustomobject]@{ Name = $Name; Pid = $process.Id; StartTimeUtc = $process.StartTime.ToUniversalTime().ToString('o') }
|
||||
}
|
||||
|
||||
function Stop-RecordedProcess {
|
||||
param($Record)
|
||||
if (-not (Test-RecordedProcess $Record)) { return }
|
||||
& taskkill.exe /PID $Record.Pid /T /F | Out-Null
|
||||
}
|
||||
|
||||
function Start-LocalDev {
|
||||
[CmdletBinding()]
|
||||
param([string]$DeploymentPath, [string]$BackendPath, [string]$FrontendPath)
|
||||
|
||||
$existing = @(Get-RecordedProcesses -DeploymentPath $DeploymentPath | Where-Object { Test-RecordedProcess $_ })
|
||||
if ($existing.Count -gt 0) { throw 'Local services are already recorded as running; use status or stop first' }
|
||||
|
||||
$null = Invoke-LocalDevCheck -DeploymentPath $DeploymentPath -BackendPath $BackendPath -FrontendPath $FrontendPath
|
||||
$commands = Resolve-LocalDevCommands -BackendPath $BackendPath -FrontendPath $FrontendPath
|
||||
$backendEnvironment = Read-DotEnvFile -Path (Join-Path $BackendPath '.env')
|
||||
$runtimeDirectory = Join-Path $DeploymentPath '.local'
|
||||
$logDirectory = Join-Path $runtimeDirectory 'logs'
|
||||
New-Item -ItemType Directory -Path $logDirectory -Force | Out-Null
|
||||
$started = @()
|
||||
try {
|
||||
$started += Start-LocalProcess -Name 'api' -Command $commands.Api -Environment $backendEnvironment -LogDirectory $logDirectory
|
||||
$started += Start-LocalProcess -Name 'worker' -Command $commands.Worker -Environment $backendEnvironment -LogDirectory $logDirectory
|
||||
$started += Start-LocalProcess -Name 'frontend' -Command $commands.Frontend -Environment @{} -LogDirectory $logDirectory
|
||||
$started | ConvertTo-Json | Set-Content -LiteralPath (Get-StatePath -DeploymentPath $DeploymentPath) -Encoding utf8
|
||||
return $started
|
||||
}
|
||||
catch {
|
||||
foreach ($record in $started) {
|
||||
Stop-RecordedProcess $record
|
||||
}
|
||||
throw
|
||||
}
|
||||
}
|
||||
|
||||
function Stop-LocalDev {
|
||||
[CmdletBinding()]
|
||||
param([string]$DeploymentPath)
|
||||
|
||||
foreach ($record in @(Get-RecordedProcesses -DeploymentPath $DeploymentPath)) {
|
||||
Stop-RecordedProcess $record
|
||||
}
|
||||
Remove-Item -LiteralPath (Get-StatePath -DeploymentPath $DeploymentPath) -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
function Get-LocalDevStatus {
|
||||
[CmdletBinding()]
|
||||
param([string]$DeploymentPath)
|
||||
|
||||
return @(Get-RecordedProcesses -DeploymentPath $DeploymentPath | ForEach-Object {
|
||||
[pscustomobject]@{ Name = $_.Name; Pid = $_.Pid; Running = [bool](Test-RecordedProcess $_) }
|
||||
})
|
||||
}
|
||||
|
||||
Export-ModuleMember -Function Read-DotEnvFile, Resolve-LocalDevCommands, Test-TcpEndpoint, Invoke-LocalDevCheck, Start-LocalDev, Stop-LocalDev, Get-LocalDevStatus
|
||||
26
scripts/local-dev.ps1
Normal file
26
scripts/local-dev.ps1
Normal file
@@ -0,0 +1,26 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Position = 0)][ValidateSet('check', 'start', 'stop', 'status')][string]$Action = 'status',
|
||||
[string]$BackendPath,
|
||||
[string]$FrontendPath
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$deploymentPath = Split-Path $PSScriptRoot -Parent
|
||||
if (-not $BackendPath) { $BackendPath = Join-Path (Split-Path $deploymentPath -Parent) 'zarinpal-analytics-backend' }
|
||||
if (-not $FrontendPath) { $FrontendPath = Join-Path (Split-Path $deploymentPath -Parent) 'zarinpal-analytics-frontend' }
|
||||
|
||||
Import-Module (Join-Path $PSScriptRoot 'LocalDev.psm1') -Force
|
||||
|
||||
try {
|
||||
switch ($Action) {
|
||||
'check' { Invoke-LocalDevCheck -DeploymentPath $deploymentPath -BackendPath $BackendPath -FrontendPath $FrontendPath | Format-List }
|
||||
'start' { Start-LocalDev -DeploymentPath $deploymentPath -BackendPath $BackendPath -FrontendPath $FrontendPath | Format-Table -AutoSize }
|
||||
'stop' { Stop-LocalDev -DeploymentPath $deploymentPath; Write-Output 'Local services stopped.' }
|
||||
'status' { Get-LocalDevStatus -DeploymentPath $deploymentPath | Format-Table -AutoSize }
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Error $_
|
||||
exit 1
|
||||
}
|
||||
146
tasks/plan.md
Normal file
146
tasks/plan.md
Normal file
@@ -0,0 +1,146 @@
|
||||
# Implementation Plan: Platform Operations
|
||||
|
||||
- Status: Approved — Phase 4 implementation authorized 2026-08-20
|
||||
- Capability: Platform operations
|
||||
- Task list target: `tasks/todo.md`
|
||||
- Planning baseline: `docs/QLOCKIFY-ADAPTATION-CONTRACT.md`
|
||||
- Last updated: 2026-08-20
|
||||
|
||||
## Outcome
|
||||
|
||||
Adapt the read-only Qlockify deployment repository into a manual, production-shaped ZarinPal Analytics deployment for `zarinpal.amiirkhl.ir`. Planning does not wait for a server IP, live DNS, real credentials, or named operators. Tracked artifacts use safe defaults and blank secret placeholders; real values are supplied only when an operation needs them.
|
||||
|
||||
There is no CI/CD pipeline, runner, registry, automated Git synchronization, or private font injection. Prometheus/Grafana remain a required post-MVP, pre-AI phase.
|
||||
|
||||
The user authorized implementation of the tasks below on 2026-08-20.
|
||||
|
||||
## Dependency graph
|
||||
|
||||
```text
|
||||
approved spec + Qlockify adaptation contract + explicit build authorization
|
||||
|
|
||||
+--> environment examples/validation --> data-service Compose
|
||||
| | |
|
||||
| +--> release record/lock +--> API/worker/one-offs
|
||||
| |
|
||||
+--> frontend build --------------------------------+
|
||||
|
|
||||
Nginx routing --> Certbot/TLS
|
||||
|
|
||||
first-start --> deploy --> rollback
|
||||
| |
|
||||
+-----------+--> backup/upload
|
||||
|
|
||||
guarded restore
|
||||
|
|
||||
first-server MVP gate
|
||||
|
|
||||
Prometheus --> Grafana
|
||||
|
|
||||
AI may begin
|
||||
```
|
||||
|
||||
## Fixed architecture decisions
|
||||
|
||||
- Preserve ADR 0001's three independent repositories and direct ignored `backend/` and `frontend/` checkouts.
|
||||
- Follow ADR 0002: the operator pulls all repositories interactively before deployment; scripts verify/record but do not pull or reset Git.
|
||||
- Follow ADR 0003/frontend commit `64f5ac2`: consume the tracked font/license through the normal frontend build context.
|
||||
- Pin `postgres:18.4-alpine3.24`, `redis:7.4.10-alpine3.21`, `nginx:1.29.8-alpine3.23`, and `certbot/certbot:v5.7.0`.
|
||||
- Keep PostgreSQL authoritative. Redis uses AOF `everysec`/`noeviction` but remains disposable and excluded from backup.
|
||||
- Keep API, worker, migrate, importer, and admin commands separate; Asynq stays explicitly pre-1.0 at `v0.26.0`.
|
||||
- Reuse Qlockify's root/backend/frontend `.env` shape and backup/restore script family with the differences recorded in the adaptation contract.
|
||||
- Use structured logs and manual operator checks for MVP; add private Prometheus/Grafana only after MVP acceptance and before AI.
|
||||
|
||||
## Phase 0: planning baseline
|
||||
|
||||
- The Platform Operations spec, ADRs 0001–0003, Qlockify adaptation contract, bootstrap runbook, and backup/restore runbook define the approved behavior.
|
||||
- Runtime values such as IP, DNS access, email, ParsPack credentials, and encryption passphrase are deliberately deferred to the operation that uses them.
|
||||
- Backend/frontend command and variable names are integrated when their implementation contracts exist; earlier independent deployment work is not blocked.
|
||||
- Stakeholder approval and explicit implementation authorization were recorded on 2026-08-20.
|
||||
|
||||
Checkpoint: the plan and tasks are approved; no implementation artifact exists yet.
|
||||
|
||||
## Phase 1: configuration and runtime foundation
|
||||
|
||||
1. Create the deployment-root example and redaction-safe validation for the Qlockify-shaped three-file layout; consume backend/frontend-owned examples without duplicating them.
|
||||
2. Implement private PostgreSQL/Redis services, named volumes, health checks, authentication, resource limits, and bounded logs.
|
||||
3. Add API/worker plus separate migration/import/admin profiles from one backend revision.
|
||||
4. Add the ordinary frontend static build and font/static health checks.
|
||||
|
||||
Checkpoint: Compose renders from placeholder test values; only intended networks exist; ordinary startup cannot invoke migration/import/admin; no secret/font/runtime artifact is tracked.
|
||||
|
||||
## Phase 2: edge and first-start lifecycle
|
||||
|
||||
5. Adapt Qlockify Nginx routing for the single canonical domain, API, frontend, readiness, and exact SSE behavior.
|
||||
6. Adapt Qlockify Certbot issuance/renewal/reload for the single domain and pinned image.
|
||||
7. Add guarded first-start orchestration for data readiness, migration, one-time import/reconciliation, and first public activation.
|
||||
|
||||
Checkpoint: routing/TLS configuration validates, first-start fails closed before public activation, and later ordinary startup never imports.
|
||||
|
||||
## Phase 3: releases and rollback
|
||||
|
||||
8. Define the ignored release-record format and exclusive operation lock.
|
||||
9. Adapt Qlockify's deploy flow to verify manually pulled clean revisions, build/migrate/replace services, observe health, and activate one three-hash release.
|
||||
10. Implement prior-release checkout/rebuild rollback with migration compatibility guards.
|
||||
|
||||
Checkpoint: induced checkout/build/migration/readiness failures preserve the active stack; a prior application release is recoverable within 15 minutes under normal conditions.
|
||||
|
||||
## Phase 4: backup and restore
|
||||
|
||||
11. Adapt Qlockify's plain-SQL PostgreSQL backup to emit only database data plus a non-secret manifest/checksums.
|
||||
12. Adapt its OpenSSL/rclone upload and encrypted 3-local/7-remote retention flow for the ZarinPal ParsPack prefix.
|
||||
13. Harden local restore with explicit backup, clean-target, compatibility, and production confirmation guards.
|
||||
14. Adapt S3 download/decrypt to verify an explicit remote object and hand off to the guarded restore.
|
||||
|
||||
Checkpoint: excluded content inspection passes, failed upload/retention cannot delete the last good copy, and a clean rehearsal meets RPO 24 hours/RTO two hours.
|
||||
|
||||
## Phase 5: first-server MVP acceptance
|
||||
|
||||
15. Follow the bootstrap runbook on the prepared Ubuntu server, fill real environment values, deploy/import once, verify the application, rehearse rollback/restore, and record evidence.
|
||||
|
||||
Checkpoint: all MVP criteria pass. The stakeholder explicitly marks MVP ready before monitoring work begins.
|
||||
|
||||
## Phase 6: required observability before AI
|
||||
|
||||
16. Add private Prometheus and approved exporters/instrumentation with bounded labels, retention, and resource use.
|
||||
17. Add private Grafana dashboards, release correlation, symptom-based alerts, runbook links, and tested delivery.
|
||||
|
||||
Checkpoint: an induced failure is detected and diagnosed from telemetry; the stakeholder approves the observability gate before AI begins.
|
||||
|
||||
## Verification strategy
|
||||
|
||||
- Documentation/planning: Markdown diff checks, source mapping review, secret-pattern scan, and no implementation files.
|
||||
- Configuration: placeholder-only rendering, missing/invalid/unknown variable tests, permission/exclusion checks, and no resolved-value output.
|
||||
- Compose: configuration validation, service health, network/port inspection, resource/log limits, profiles, and restart/Redis-loss behavior.
|
||||
- Shell operations: ShellCheck plus disposable fixture tests for exact paths, locks, dirty/diverged Git, failure cleanup, retention, and restore guard matrices.
|
||||
- Edge: `nginx -t`, canonical/unknown-host behavior, security headers, TLS issuance/renewal, static/font behavior, API routing, and streamed SSE checks.
|
||||
- Release/recovery: induced deployment failures, timed prior-hash rollback, backup inspection, remote verification, and clean-database restore rehearsal.
|
||||
- MVP/observability: manual acceptance record first; monitoring target/cardinality/dashboard/alert tests only after the MVP gate.
|
||||
|
||||
## Risks and mitigations
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
|---|---|---|
|
||||
| Qlockify-specific behavior is copied blindly | Wrong services or unsafe operations | File-by-file adaptation matrix and explicit retain/change/drop review |
|
||||
| Environment ownership drifts | Secret mismatch or accidental client exposure | Root/backend/frontend ownership contract; one owner per variable; redaction-safe validation |
|
||||
| Manual releases select inconsistent source | Broken stack | Interactive pulls first, one lock, three clean SHA checks, one release record |
|
||||
| Server-side builds vary over time | Difficult reproduction | Pin infrastructure/dependency inputs and record three SHAs plus local image ids |
|
||||
| Migration prevents rollback | Extended outage | Expand/contract sequencing, preflight backup, no automatic down migration |
|
||||
| Backup contains secrets or is unusable | Disclosure or data loss | PostgreSQL-only package, encryption/checksums, explicit exclusions, clean restore rehearsal |
|
||||
| Redis loss interrupts analytics | Temporary missing results | AOF `everysec`, PostgreSQL authority, idempotent reconciliation and cold-cache tests |
|
||||
| 2-GB server exhausts memory | OOM/restarts | Approved per-service limits, 2-GB swap, bounded imports/logs, capacity upgrade when measured |
|
||||
| Monitoring is postponed too far | Weak diagnosis before AI | Hard MVP-to-observability-to-AI phase gate |
|
||||
|
||||
## Parallel and sequential work
|
||||
|
||||
- After environment/paths are fixed, PostgreSQL/Redis foundation, frontend build, Nginx fixtures, and backup fixtures can be prepared in separate focused slices.
|
||||
- Compose integration, migration/import, release activation, rollback, production restore, DNS/TLS cutover, and phase gates remain sequential.
|
||||
- Backend/frontend repositories define their application commands and variables; deployment consumes those contracts without editing those repositories in this workstream.
|
||||
|
||||
## Completion
|
||||
|
||||
- Every authorized task in `tasks/todo.md` passes its acceptance and verification.
|
||||
- The active stack maps to exact deployment/backend/frontend commits and can roll back safely.
|
||||
- Only Nginx publishes application ports; secrets/runtime/raw/font copies remain outside deployment Git.
|
||||
- Encrypted off-host backup and clean restore meet their RPO/RTO.
|
||||
- Prometheus/Grafana acceptance passes after MVP and before AI.
|
||||
348
tasks/todo.md
Normal file
348
tasks/todo.md
Normal file
@@ -0,0 +1,348 @@
|
||||
# Platform Operations implementation tasks
|
||||
|
||||
- Status: Approved — Phase 4 implementation authorized 2026-08-20
|
||||
- Canonical plan: `tasks/plan.md`
|
||||
- Adaptation baseline: `docs/QLOCKIFY-ADAPTATION-CONTRACT.md`
|
||||
|
||||
## Task 0: Approve the implementation plan
|
||||
|
||||
**Description:** Confirm that the Qlockify adaptation contract, dependency order, task boundaries, and verification gates are the correct basis for later implementation.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [x] Platform Operations specification and ADRs 0001–0003 are approved.
|
||||
- [x] Qlockify flow/environment mapping is documented without real runtime values.
|
||||
- [x] Stakeholder approved this plan/task breakdown and authorized implementation on 2026-08-20.
|
||||
|
||||
**Verification:** Documentation diff review and confirmation that no implementation artifact was created.
|
||||
|
||||
**Dependencies:** None
|
||||
|
||||
**Files likely touched:** documentation only
|
||||
|
||||
**Estimated scope:** Small
|
||||
|
||||
## Task 1: Add the deployment environment contract
|
||||
|
||||
**Description:** Add the root deployment example and redaction-safe validation for the Qlockify-shaped root/backend/frontend environment layout. Backend and frontend examples remain owned by their repositories and are consumed without duplication.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] The root example contains only deployment-owned safe defaults/blank placeholders; every service-owned variable has one documented owner.
|
||||
- [ ] Real `.env` files stay ignored, mode `0600`, excluded from backup, and absent from rendered/logged output.
|
||||
- [ ] Validation rejects missing, unknown, malformed, or client-exposed secret variables for the selected operation.
|
||||
|
||||
**Verification:** Placeholder render, negative validation matrix, Git-ignore/permission checks, and staged secret scan.
|
||||
|
||||
**Dependencies:** Task 0 plus explicit implementation authorization
|
||||
|
||||
**Files likely touched:** `.env.example`, validation helper/test, documentation; maximum 5 files
|
||||
|
||||
**Estimated scope:** Medium
|
||||
|
||||
## Task 2: Add PostgreSQL and Redis Compose foundation
|
||||
|
||||
**Description:** Define pinned private data services, health checks, named volumes, authentication, resource limits, bounded logs, and Redis AOF behavior.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] PostgreSQL and Redis become healthy using the approved pins and memory limits.
|
||||
- [ ] Neither publishes a host port; only approved services can join the private data network.
|
||||
- [ ] Redis uses AOF `everysec`, `noeviction`, 192-MB maxmemory, and remains excluded from backup.
|
||||
|
||||
**Verification:** Compose validation, health/port/network/volume/resource inspection, restart and Redis-loss tests.
|
||||
|
||||
**Dependencies:** Task 1
|
||||
|
||||
**Files likely touched:** `compose.yaml`, data config, focused validation/test files; maximum 5 files
|
||||
|
||||
**Estimated scope:** Medium
|
||||
|
||||
## Task 3: Add backend runtime boundaries
|
||||
|
||||
**Description:** Wire API and worker services plus one-off migrate/import/admin profiles from the same recorded backend checkout/image lineage.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] API and worker use private networks, health/readiness, graceful shutdown, and approved resource limits.
|
||||
- [ ] Migrate/import/admin are explicit one-off profiles and never run during ordinary startup.
|
||||
- [ ] Worker uses pre-1.0 Asynq `v0.26.0` with concurrency 16 globally and 4 per merchant.
|
||||
|
||||
**Verification:** Compose profile/startup/failure/shutdown tests and service command-contract review.
|
||||
|
||||
**Dependencies:** Task 2 and the backend-owned command contract
|
||||
|
||||
**Files likely touched:** `compose.yaml`, backend image definition, lifecycle tests/docs; maximum 5 files
|
||||
|
||||
**Estimated scope:** Medium
|
||||
|
||||
## Task 4: Add frontend production build
|
||||
|
||||
**Description:** Build and serve the direct frontend checkout using its ordinary static context and same-origin API configuration.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] Build consumes frontend commit `64f5ac2` or later and its tracked font/license normally.
|
||||
- [ ] Static sentinel, SPA fallback, and emitted font resource/content type pass.
|
||||
- [ ] Deployment Git contains no font/license copy, fetch, credentials, or alternate asset path.
|
||||
|
||||
**Verification:** Production build/static-server checks plus repository-policy scan.
|
||||
|
||||
**Dependencies:** Task 1 and the frontend-owned build contract
|
||||
|
||||
**Files likely touched:** frontend image definition, `compose.yaml`, focused health/tests; maximum 5 files
|
||||
|
||||
**Estimated scope:** Medium
|
||||
|
||||
## Checkpoint A: Runtime foundation
|
||||
|
||||
- [ ] Tasks 1–4 pass from clean nested checkouts with placeholder test values.
|
||||
- [ ] Normal startup cannot invoke migration/import/admin.
|
||||
- [ ] No secrets, runtime state, raw data, or duplicated font assets are tracked.
|
||||
|
||||
## Task 5: Adapt Qlockify Nginx routing
|
||||
|
||||
**Description:** Adapt the Qlockify template/entrypoint for the single canonical domain, default-host rejection, frontend/API routing, security headers, health, caching, and SSE.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] Only ports 80/443 publish; HTTP redirects exactly to `zarinpal.amiirkhl.ir`; unknown hosts reject.
|
||||
- [ ] API and frontend routing/fallback do not overlap and public readiness remains generic.
|
||||
- [ ] Exact SSE route preserves auth/event ids and streams heartbeats without buffering, cache, compression, or premature timeout.
|
||||
|
||||
**Verification:** `nginx -t`, route/header/default-host tests, static/font test, and streamed SSE fixture.
|
||||
|
||||
**Dependencies:** Tasks 3–4
|
||||
|
||||
**Files likely touched:** Nginx template, image/entrypoint, focused tests; maximum 5 files
|
||||
|
||||
**Estimated scope:** Medium
|
||||
|
||||
## Task 6: Adapt Qlockify Certbot lifecycle
|
||||
|
||||
**Description:** Adapt Qlockify's HTTP-01 bootstrap, pinned Certbot service, certificate watcher, renewal, and Nginx reload for the single domain.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] Missing production certificate uses only the bounded bootstrap certificate path until issuance.
|
||||
- [ ] Staging issuance, production issuance, renewal, and validated Nginx reload pass.
|
||||
- [ ] No alternate domain, local production certificate mode, private key, or ACME state enters Git.
|
||||
|
||||
**Verification:** Compose/entrypoint tests, staging ACME drill, certificate name/expiry/protocol inspection.
|
||||
|
||||
**Dependencies:** Task 5
|
||||
|
||||
**Files likely touched:** Certbot entrypoint/config, `compose.yaml`, focused tests/docs; maximum 5 files
|
||||
|
||||
**Estimated scope:** Medium
|
||||
|
||||
## Task 7: Add guarded first-start orchestration
|
||||
|
||||
**Description:** Add the explicit first-server path that starts data services, migrates, imports/reconciles the pinned CSV, and activates public services only after success.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] Input is exact/read-only and migration/import/reconciliation failures prevent public activation.
|
||||
- [ ] A completed identical import is a safe no-op; recovery never guesses a partial CSV position.
|
||||
- [ ] Ordinary deploy/start/reboot paths never invoke first-start, importer, or admin jobs.
|
||||
|
||||
**Verification:** Success, checksum/schema/count, interruption/recovery, idempotency, and ordinary-start negative tests.
|
||||
|
||||
**Dependencies:** Tasks 2–6 and approved backend importer commands
|
||||
|
||||
**Files likely touched:** `scripts/first-start.sh`, Compose profile, focused tests/docs; maximum 5 files
|
||||
|
||||
**Estimated scope:** Medium
|
||||
|
||||
## Checkpoint B: Edge and first-start
|
||||
|
||||
- [ ] Nginx/TLS/static/API/SSE checks pass.
|
||||
- [ ] First-start is fail-closed and ordinary startup never imports.
|
||||
- [ ] Only Nginx is externally reachable.
|
||||
|
||||
## Task 8: Add release records and operation lock
|
||||
|
||||
**Description:** Define ignored current/previous/timestamped release records and one exclusive lock shared by deploy, rollback, restore, migration, and import as required.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] Record captures three SHAs, local image ids, migration, backup, operator, health, outcome, and previous release without secrets.
|
||||
- [ ] Invalid/incomplete records cannot become active and current/previous updates are atomic.
|
||||
- [ ] Conflicting operations fail safely without stale-lock ambiguity.
|
||||
|
||||
**Verification:** Schema/parser tests, invalid-record matrix, lock contention/stale-owner tests, and redaction scan.
|
||||
|
||||
**Dependencies:** Task 1
|
||||
|
||||
**Files likely touched:** release schema/helper/tests/docs; maximum 5 files
|
||||
|
||||
**Estimated scope:** Medium
|
||||
|
||||
## Task 9: Adapt Qlockify manual deployment
|
||||
|
||||
**Description:** Adapt Qlockify's deploy structure to verify three manually pulled repositories, build/migrate/replace services, observe health, and activate one coherent release.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] Script performs no Git fetch/pull/reset and rejects wrong path/remote/branch/SHA/dirty state.
|
||||
- [ ] Build, migration, readiness, font, worker, or SSE failure leaves the active release unchanged and records evidence.
|
||||
- [ ] Successful deployment records all three SHAs and passes a 15-minute postflight observation before activation.
|
||||
|
||||
**Verification:** ShellCheck, disposable Git fixtures, Compose mocks/integration, and induced failure matrix.
|
||||
|
||||
**Dependencies:** Tasks 5–8
|
||||
|
||||
**Files likely touched:** `scripts/deploy.sh`, release helper, focused tests/runbook; maximum 5 files
|
||||
|
||||
**Estimated scope:** Medium
|
||||
|
||||
## Task 10: Add three-hash rollback
|
||||
|
||||
**Description:** Select the previous release record, verify its three revisions, rebuild, and run the normal postflight without automatic database down-migration.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] Exact previous hashes and clean repository guards precede checkout/build.
|
||||
- [ ] Migration incompatibility stops rollback with the current stack preserved.
|
||||
- [ ] Normal application rollback completes within 15 minutes and is recorded as a new event.
|
||||
|
||||
**Verification:** Git fixture tests, incompatible-migration negative test, and timed rollback drill.
|
||||
|
||||
**Dependencies:** Task 9
|
||||
|
||||
**Files likely touched:** rollback script/helper/tests/runbook; maximum 5 files
|
||||
|
||||
**Estimated scope:** Medium
|
||||
|
||||
## Checkpoint C: Release safety
|
||||
|
||||
- [ ] Dirty/diverged/manual-pull/build/migration/readiness failures preserve the active stack.
|
||||
- [ ] Active and previous releases map to exact three-repository revisions.
|
||||
- [ ] Timed rollback meets its target without a down migration.
|
||||
|
||||
## Task 11: Adapt Qlockify PostgreSQL backup
|
||||
|
||||
**Description:** Retain Qlockify's matching-client plain-SQL dump flow while packaging only PostgreSQL plus a non-secret release/migration/checksum manifest.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] Dump nonzero exit, empty output, invalid header, or checksum failure prevents package success.
|
||||
- [ ] Package contains only plain SQL and the approved non-secret manifest/checksums.
|
||||
- [ ] `.env`, secrets, Redis, TLS, CSV, credentials, logs, monitoring, font/license, and media are absent.
|
||||
|
||||
**Verification:** Fixture database dump, package listing/header/checksum checks, and forbidden-content scan.
|
||||
|
||||
**Dependencies:** Tasks 2, 8
|
||||
|
||||
**Files likely touched:** `scripts/backup.sh`, manifest helper/tests/runbook; maximum 5 files
|
||||
|
||||
**Estimated scope:** Medium
|
||||
|
||||
## Task 12: Adapt encrypted ParsPack upload and retention
|
||||
|
||||
**Description:** Retain Qlockify's OpenSSL AES-256-CBC PBKDF2-200k and rclone structure while keeping three encrypted local and seven encrypted remote verified backups.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] Upload uses a private temporary key and is finalized only after remote verification.
|
||||
- [ ] Failed encryption/upload/verification preserves all previous copies and no plaintext backup remains durably.
|
||||
- [ ] Retention removes only exact stale targets after new-copy verification and never deletes the last good copy.
|
||||
|
||||
**Verification:** Fake S3/ParsPack-compatible fixture, failure/retention matrix, checksum and plaintext-residue checks.
|
||||
|
||||
**Dependencies:** Task 11
|
||||
|
||||
**Files likely touched:** upload script, retention helper/tests/runbook; maximum 5 files
|
||||
|
||||
**Estimated scope:** Medium
|
||||
|
||||
## Task 13: Add guarded local restore
|
||||
|
||||
**Description:** Restore an explicitly selected verified backup into a new empty target using `psql -X --set ON_ERROR_STOP=on` and independent production guards.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] Provenance/checksum/header/version/free-space/exact-empty-target checks precede mutation.
|
||||
- [ ] Every missing production factor aborts before writers stop or a database is changed.
|
||||
- [ ] Restore never changes `.env`, Redis, TLS, CSV, credentials, logs, or frontend assets.
|
||||
|
||||
**Verification:** Disposable database restore, negative guard matrix, partial-SQL failure, and exclusion checks.
|
||||
|
||||
**Dependencies:** Task 11
|
||||
|
||||
**Files likely touched:** `scripts/restore.sh`, guard/helper/tests/runbook; maximum 5 files
|
||||
|
||||
**Estimated scope:** Medium
|
||||
|
||||
## Task 14: Adapt encrypted S3 restore handoff
|
||||
|
||||
**Description:** Download an explicit encrypted object, verify/decrypt it in a restrictive temporary directory, and hand it to the guarded local restore.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] Production requires an explicit object id; an implicit `latest` cannot select a production backup.
|
||||
- [ ] Remote identity/size/checksum and decrypted package checks pass before restore invocation.
|
||||
- [ ] Temporary encrypted/plaintext material is removed only from the exact validated temporary directory.
|
||||
|
||||
**Verification:** Fake S3 object-selection/tamper tests, decryption failure tests, and guarded handoff rehearsal.
|
||||
|
||||
**Dependencies:** Tasks 12–13
|
||||
|
||||
**Files likely touched:** `scripts/restore-from-s3.sh`, focused tests/runbook; maximum 4 files
|
||||
|
||||
**Estimated scope:** Medium
|
||||
|
||||
## Checkpoint D: Recovery
|
||||
|
||||
- [ ] Daily schedule/3-local/7-remote retention behaves safely.
|
||||
- [ ] Forbidden content is absent from every backup package.
|
||||
- [ ] Clean restore passes schema/count/tenant/login/cold-cache/worker/SSE validation within two hours.
|
||||
|
||||
## Task 15: Bootstrap and accept the MVP server
|
||||
|
||||
**Description:** Execute the reviewed runbooks on the prepared Ubuntu server, fill runtime values, perform first-start, activate the release, and rehearse rollback/restore.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] Host access/firewall/swap/Docker/DNS/TLS and only-80/443 exposure checks pass.
|
||||
- [ ] Migration/import/accounts/tenant isolation/worker/SSE/frontend font and manual health/log checks pass.
|
||||
- [ ] Three-hash release, rollback, encrypted remote backup, and clean restore meet their time/recovery targets.
|
||||
|
||||
**Verification:** Signed non-secret bootstrap/MVP acceptance record.
|
||||
|
||||
**Dependencies:** Checkpoints A–D and a prepared server/runtime values
|
||||
|
||||
**Files likely touched:** ignored server evidence/release records only
|
||||
|
||||
**Estimated scope:** Operational session
|
||||
|
||||
## Checkpoint E: MVP ready
|
||||
|
||||
- [ ] Every MVP acceptance criterion passes.
|
||||
- [ ] Active/previous releases and backup/restore are proven.
|
||||
- [ ] Stakeholder explicitly opens the observability phase.
|
||||
|
||||
## Task 16: Add private Prometheus collection
|
||||
|
||||
**Description:** Add post-MVP application, worker/SSE, PostgreSQL/Redis, host/container, backup/restore, and TLS collection with bounded labels and retention.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] Required signals and target health pass within approved resource/retention limits.
|
||||
- [ ] Prometheus/exporters are private and not routed by Nginx.
|
||||
- [ ] Merchant/user/request/card identifiers are absent from metric labels.
|
||||
|
||||
**Verification:** Target/signal/cardinality/resource tests and sensitive-label scan.
|
||||
|
||||
**Dependencies:** Checkpoint E
|
||||
|
||||
**Files likely touched:** split into focused Prometheus/exporter slices of no more than 5 files
|
||||
|
||||
**Estimated scope:** Multiple Medium slices
|
||||
|
||||
## Task 17: Add private Grafana dashboards and alerts
|
||||
|
||||
**Description:** Add post-MVP dashboards, release correlation, actionable symptom alerts, runbook links, and tested operator delivery.
|
||||
|
||||
**Acceptance criteria:**
|
||||
- [ ] Service, worker/SSE, data, host, backup, and TLS dashboards answer the approved operator questions.
|
||||
- [ ] Every page/ticket alert is actionable, bounded, linked to a runbook, and tested through an induced failure.
|
||||
- [ ] Grafana remains private and contains no sensitive labels, logs, or embedded credentials.
|
||||
|
||||
**Verification:** Dashboard review, induced failure, alert test-fire, access and redaction checks.
|
||||
|
||||
**Dependencies:** Task 16
|
||||
|
||||
**Files likely touched:** split into focused dashboard/alert slices of no more than 5 files
|
||||
|
||||
**Estimated scope:** Multiple Medium slices
|
||||
|
||||
## Final observability checkpoint before AI
|
||||
|
||||
- [ ] Prometheus/Grafana acceptance criteria pass after MVP.
|
||||
- [ ] Alert delivery and runbook ownership are proven.
|
||||
- [ ] Stakeholder explicitly approves beginning AI implementation.
|
||||
- [ ] No CI/CD pipeline or automated deployment system was introduced.
|
||||
110
tests/local-dev.tests.ps1
Normal file
110
tests/local-dev.tests.ps1
Normal file
@@ -0,0 +1,110 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$modulePath = Join-Path $PSScriptRoot '..\scripts\LocalDev.psm1'
|
||||
Import-Module $modulePath -Force
|
||||
|
||||
function Assert-Equal {
|
||||
param($Expected, $Actual, [string]$Message)
|
||||
|
||||
if ($Expected -ne $Actual) {
|
||||
throw "$Message (expected '$Expected', got '$Actual')"
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-Throws {
|
||||
param([scriptblock]$Action, [string]$Pattern, [string]$Message)
|
||||
|
||||
try {
|
||||
& $Action
|
||||
}
|
||||
catch {
|
||||
if ($_.Exception.Message -notmatch $Pattern) {
|
||||
throw "$Message (unexpected error: $($_.Exception.Message))"
|
||||
}
|
||||
return
|
||||
}
|
||||
throw "$Message (no error was thrown)"
|
||||
}
|
||||
|
||||
$fixture = Join-Path ([System.IO.Path]::GetTempPath()) ("zarinpal-local-dev-" + [guid]::NewGuid())
|
||||
New-Item -ItemType Directory -Path $fixture | Out-Null
|
||||
|
||||
try {
|
||||
$validEnv = Join-Path $fixture '.env'
|
||||
@(
|
||||
'# ignored comment'
|
||||
'PLAIN=value'
|
||||
'QUOTED="value with spaces"'
|
||||
'EMPTY='
|
||||
) | Set-Content -LiteralPath $validEnv -Encoding utf8
|
||||
|
||||
$values = Read-DotEnvFile -Path $validEnv
|
||||
Assert-Equal 'value' $values.PLAIN 'Plain dotenv values should parse'
|
||||
Assert-Equal 'value with spaces' $values.QUOTED 'Quoted dotenv values should parse'
|
||||
Assert-Equal '' $values.EMPTY 'Blank dotenv values should parse'
|
||||
|
||||
$duplicateEnv = Join-Path $fixture 'duplicate.env'
|
||||
@('KEY=one', 'KEY=two') | Set-Content -LiteralPath $duplicateEnv -Encoding utf8
|
||||
Assert-Throws { Read-DotEnvFile -Path $duplicateEnv } 'duplicate variable' 'Duplicate variables must fail closed'
|
||||
|
||||
$invalidEnv = Join-Path $fixture 'invalid.env'
|
||||
'NOT A KEY=value' | Set-Content -LiteralPath $invalidEnv -Encoding utf8
|
||||
Assert-Throws { Read-DotEnvFile -Path $invalidEnv } 'invalid dotenv entry' 'Malformed variables must fail closed'
|
||||
|
||||
$backend = Join-Path $fixture 'backend'
|
||||
$frontend = Join-Path $fixture 'frontend'
|
||||
New-Item -ItemType Directory -Path (Join-Path $backend 'cmd\api') -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path (Join-Path $backend 'cmd\worker') -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path $frontend -Force | Out-Null
|
||||
'{"scripts":{"dev":"vite"}}' | Set-Content -LiteralPath (Join-Path $frontend 'package.json') -Encoding utf8
|
||||
|
||||
$commands = Resolve-LocalDevCommands -BackendPath $backend -FrontendPath $frontend
|
||||
Assert-Equal './cmd/api' $commands.Api.Arguments[1] 'API command should follow the backend contract'
|
||||
Assert-Equal './cmd/worker' $commands.Worker.Arguments[1] 'Worker command should follow the backend contract'
|
||||
Assert-Equal 'run' $commands.Frontend.Arguments[0] 'Frontend should use its checked-in dev script'
|
||||
|
||||
$stateDirectory = Join-Path $fixture '.local'
|
||||
New-Item -ItemType Directory -Path $stateDirectory -Force | Out-Null
|
||||
$currentProcess = Get-Process -Id $PID
|
||||
@([pscustomobject]@{
|
||||
Name = 'test-host'
|
||||
Pid = $currentProcess.Id
|
||||
StartTimeUtc = $currentProcess.StartTime.ToUniversalTime().ToString('o')
|
||||
}) | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $stateDirectory 'processes.json') -Encoding utf8
|
||||
$status = @(Get-LocalDevStatus -DeploymentPath $fixture)
|
||||
Assert-Equal $true $status[0].Running 'Status should recognize a recorded live process after JSON date conversion'
|
||||
|
||||
$childTreeRoot = Start-Process -FilePath 'cmd.exe' -ArgumentList @('/c', 'powershell.exe -NoProfile -Command "Start-Sleep -Seconds 60"') -WindowStyle Hidden -PassThru
|
||||
try {
|
||||
$childTreeRoot.Refresh()
|
||||
@([pscustomobject]@{
|
||||
Name = 'test-tree'
|
||||
Pid = $childTreeRoot.Id
|
||||
StartTimeUtc = $childTreeRoot.StartTime.ToUniversalTime().ToString('o')
|
||||
}) | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $stateDirectory 'processes.json') -Encoding utf8
|
||||
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds(5)
|
||||
do {
|
||||
$child = Get-CimInstance Win32_Process -Filter "ParentProcessId=$($childTreeRoot.Id)" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if (-not $child) { Start-Sleep -Milliseconds 50 }
|
||||
} while (-not $child -and [DateTime]::UtcNow -lt $deadline)
|
||||
if (-not $child) { throw 'Test child process did not start' }
|
||||
|
||||
Stop-LocalDev -DeploymentPath $fixture
|
||||
Assert-Equal $null (Get-Process -Id $childTreeRoot.Id -ErrorAction SilentlyContinue) 'Stop should terminate the recorded wrapper process'
|
||||
Assert-Equal $null (Get-Process -Id $child.ProcessId -ErrorAction SilentlyContinue) 'Stop should terminate descendants of the recorded wrapper process'
|
||||
}
|
||||
finally {
|
||||
if (Get-Process -Id $childTreeRoot.Id -ErrorAction SilentlyContinue) {
|
||||
& taskkill.exe /PID $childTreeRoot.Id /T /F | Out-Null
|
||||
}
|
||||
}
|
||||
|
||||
Remove-Item -LiteralPath (Join-Path $backend 'cmd\worker') -Recurse
|
||||
Assert-Throws { Resolve-LocalDevCommands -BackendPath $backend -FrontendPath $frontend } 'cmd/worker' 'A missing worker entrypoint must be actionable'
|
||||
|
||||
Write-Output 'local-dev tests passed'
|
||||
}
|
||||
finally {
|
||||
Remove-Item -LiteralPath $fixture -Recurse -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
Reference in New Issue
Block a user