diff --git a/CLAUDE.md b/CLAUDE.md index b694b29..5847129 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ git push origin main # auto-deploys via Portainer webhook - **Urban Switch** (urbanism) - **Studii de Teren** (geotechnics) -It runs on an on-premise Ubuntu server at `10.10.10.166`, containerized with Docker, managed via Portainer, served by Nginx Proxy Manager. +It runs on two on-premise servers, containerized with Docker, managed via Portainer CE. ### Stack @@ -35,7 +35,8 @@ It runs on an on-premise Ubuntu server at `10.10.10.166`, containerized with Doc | Storage | `DatabaseStorageAdapter` (PostgreSQL) — localStorage fallback available | | File Storage | MinIO (10.10.10.166:9002 API / 9003 UI) — client configured, adapter pending | | Auth | NextAuth v4 + Authentik OIDC (auth.beletage.ro) | -| Deploy | Docker multi-stage, Portainer CE, Nginx Proxy Manager | +| Proxy | Traefik v3 on `10.10.10.199` (proxy server), SSL via Let's Encrypt | +| Deploy | Docker multi-stage, Portainer CE on `10.10.10.166` (satra) | | Repo | Gitea at `https://git.beletage.ro/gitadmin/ArchiTools` | | Language | Code in **English**, UI in **Romanian** | @@ -104,9 +105,9 @@ legacy/ # Original HTML tools for reference | 4 | **Registratura** | `/registratura` | 0.5.0 | CRUD registry, dynamic doc types, bidirectional Address Book, threads, backdating, **legal deadline tracking** (6 categories, 18 types), recipient registration, document expiry, **NAS network path attachments** (A/O/P/T drives, copy-to-clipboard), **detail sheet side panel**, **configurable column visibility**, **QuickLook attachment preview** (images: zoom/pan, PDFs: native viewer, multi-file navigation), **email notifications** (Brevo SMTP daily digest, per-user preferences), **compact registry numbers** (single-letter company badge + direction arrow + plain number) | | 5 | **Tag Manager** | `/tag-manager` | 0.2.0 | CRUD tags, category/scope/color, US/SDT seeds, mandatory categories, **ManicTime bidirectional sync** | | 6 | **IT Inventory** | `/it-inventory` | 0.2.0 | Dynamic equipment types, rented status (purple pulse), **42U rack visualization**, type/status/company filters | -| 7 | **Address Book** | `/address-book` | 0.1.1 | CRUD contacts, card grid, vCard export, Registratura reverse lookup, **dynamic types (creatable)**, **alphabetically sorted type dropdown** | +| 7 | **Address Book** | `/address-book` | 0.2.0 | CRUD contacts (person OR institution), card grid, vCard export, Registratura reverse lookup, **dynamic types (creatable)**, **name OR company required** (flexible validation), **ContactPerson with department field**, **quick contact from Registratura** (persons + institutions) | | 8 | **Password Vault** | `/password-vault` | 0.4.0 | CRUD credentials, 9 categorii cu iconițe, **WiFi QR code real**, context-aware form, strength meter, company scope, **AES-256-GCM encryption**, **utilizatori multipli per intrare** (VaultUser[]: username/password/email/notes, colapsibil în form, badge în list) | -| 9 | **Mini Utilities** | `/mini-utilities` | 0.3.0 | Text case, char counter, percentage, **TVA calculator (cotă configurabilă: 5/9/19/21% + custom)**, area converter, U→R, num→text, artifact cleaner, MDLPA, **extreme PDF compression (GS+qpdf)**, PDF unlock, **DWG→DXF**, OCR, color palette, **paste on all drop zones**, **thermal drag-drop reorder**, **Calculator scară desen** (real cm↔desen mm, 7 preseturi 1:20..1:5000 + custom) | +| 9 | **Mini Utilities** | `/mini-utilities` | 0.4.0 | Text case, char counter, percentage, **TVA calculator (cotă configurabilă: 5/9/19/21% + custom)**, area converter, U→R, num→text, artifact cleaner, MDLPA, **PDF compression** (qpdf local lossless + iLovePDF API cloud lossy, streaming upload for large files), PDF unlock, **DWG→DXF**, OCR, color palette, **paste on all drop zones**, **thermal drag-drop reorder**, **Calculator scară desen** (real cm↔desen mm, 7 preseturi 1:20..1:5000 + custom) | | 10 | **Prompt Generator** | `/prompt-generator` | 0.2.0 | Template-driven prompt builder, **18 templates** (14 text + 4 image), search bar, target type filter | | 11 | **Digital Signatures** | `/digital-signatures` | 0.1.0 | CRUD assets, drag-and-drop file upload, tag chips | | 12 | **Word Templates** | `/word-templates` | 0.1.0 | Template library, 8 categories, version tracking, .docx placeholder auto-detection | @@ -137,6 +138,51 @@ Key files: - `components/notification-preferences.tsx` — Bell button + dialog with per-type toggles - `components/registry-table.tsx` — `CompactNumber` component: single-letter company badge (B/U/S/G), direction arrow (↓ intrat / ↑ iesit), plain number +### Address Book — Flexible Contact Model + +The Address Book supports both persons and institutions: + +- **Flexible validation**: either `name` OR `company` required (not both mandatory) +- **Auto-type detection**: when only company is set via quick-create, type defaults to "institution" +- **ContactPerson sub-entities**: each has `name`, `department`, `role`, `email`, `phone` +- **Quick contact creation from Registratura**: inline dialog with name + company + phone + email +- **Display logic**: if no name, company shows as primary; if both, shows "Name (Company)" +- **Creatable types**: dropdown with defaults (client/supplier/institution/collaborator/internal) + user-created custom types + +Key files: + +- `modules/address-book/types.ts` — `AddressContact`, `ContactPerson` interfaces +- `modules/address-book/components/address-book-module.tsx` — Full UI (cards, detail dialog, form) +- `modules/address-book/hooks/use-contacts.ts` — Storage hook with search/filter +- `modules/address-book/services/vcard-export.ts` — vCard 3.0 export +- `modules/registratura/components/quick-contact-dialog.tsx` — Quick create from registry + +### PDF Compression — Dual Mode (Local + Cloud) + +Two compression routes, both with streaming upload support for large files (tested up to 287MB): + +- **Local (qpdf)**: lossless structural optimization — stream compression, object dedup, linearization. Safe, no font corruption. Typical reduction: 3-15%. +- **Cloud (iLovePDF API)**: lossy image re-compression via iLovePDF REST API. Levels: extreme/recommended/low. Typical reduction: 50-91%. Requires `ILOVEPDF_PUBLIC_KEY` env var. + +**Architecture** (zero-memory for any file size): +1. `parseMultipartUpload()` streams request body to disk (constant 64KB memory) +2. Scans raw file for multipart boundaries using `findInFile()` with 64KB sliding window +3. Stream-copies PDF bytes to separate file +4. Route handler processes (qpdf exec or iLovePDF API) and streams response back + +**Critical gotchas**: +- Middleware body buffering: `api/compress-pdf` routes are **excluded from middleware matcher** (middleware buffers entire body at 10MB default) +- Auth: route-level `requireAuth()` instead of middleware (in `auth-check.ts`) +- Unicode filenames: `Content-Disposition` header uses `encodeURIComponent()` to avoid ByteString errors with Romanian chars (Ș, Ț, etc.) +- Ghostscript `-sDEVICE=pdfwrite` destroys font encodings — **never use GS for compression**, only qpdf + +Key files: + +- `app/api/compress-pdf/parse-upload.ts` — Streaming multipart parser (zero memory) +- `app/api/compress-pdf/extreme/route.ts` — qpdf local compression +- `app/api/compress-pdf/cloud/route.ts` — iLovePDF API integration +- `app/api/compress-pdf/auth-check.ts` — Shared auth for routes excluded from middleware + ### Email Notifications (Brevo SMTP) Platform-level notification service for daily email digests: @@ -185,19 +231,18 @@ Key files: ## Infrastructure -### Server: `10.10.10.166` (Ubuntu) +### Server: `satra` — 10.10.10.166 (Ubuntu, app server) | Service | Port | Purpose | | ----------------------- | ---------------------- | ----------------------------------- | | **ArchiTools** | 3000 | This app (tools.beletage.ro) | | **Gitea** | 3002 | Git hosting (git.beletage.ro) | | **PostgreSQL** | 5432 | App database (Prisma ORM) | -| **Portainer** | 9000 | Docker management | -| **Nginx Proxy Manager** | 81 (admin) | Reverse proxy + SSL termination | +| **Portainer CE** | 9000 | Docker management + deploy | | **Uptime Kuma** | 3001 | Service monitoring | | **MinIO** | 9002 (API) / 9003 (UI) | Object storage | | **Authentik** | 9100 | SSO (auth.beletage.ro) — **active** | -| **N8N** | 5678 | Workflow automation (daily digest cron) | +| **N8N** | 5678 | Workflow automation (daily digest) | | **Stirling PDF** | 8087 | PDF tools | | **IT-Tools** | 8085 | Developer utilities | | **FileBrowser** | 8086 | File management | @@ -205,21 +250,37 @@ Key files: | **Dozzle** | 9999 | Docker log viewer | | **CrowdSec** | 8088 | Security | +### Server: `proxy` — 10.10.10.199 (Traefik reverse proxy) + +| Config | Path / Value | +| ----------------------- | ---------------------------------------- | +| **Static config** | `/opt/traefik/traefik.yml` | +| **Dynamic configs** | `/opt/traefik/dynamic/` (file provider, `watch: true`) | +| **ArchiTools route** | `/opt/traefik/dynamic/tools.yml` | +| **SSL** | Let's Encrypt ACME, HTTP challenge | +| **Timeouts** | `readTimeout: 600s`, `writeTimeout: 600s`, `idleTimeout: 600s` on `websecure` entrypoint | +| **Response forwarding** | `flushInterval: 100ms` (streaming support) | + +**IMPORTANT**: Default Traefik v2.11+ has 60s `readTimeout` — breaks large file uploads. Must set explicitly in static config. + ### Deployment Pipeline ``` git push origin main → Gitea webhook fires - → Portainer CE detects new commit - → Manual "Pull and redeploy" in Portainer (CE doesn't auto-rebuild) - → Docker multi-stage build (~1-2 min) + → Portainer CE: Stacks → architools → "Pull and redeploy" + → Toggle "Re-pull image and redeploy" ON → click "Update" + → Portainer re-clones git repo + Docker multi-stage build (~2 min) → Container starts on :3000 - → Nginx Proxy Manager routes to tools.beletage.ro + → Traefik routes tools.beletage.ro → http://10.10.10.166:3000 ``` +**Portainer CE deploy**: NOT automatic. Must manually click "Pull and redeploy" in Portainer UI after each push. The stack is configured from git repo `http://10.10.10.166:3002/gitadmin/ArchiTools`. + ### Docker -- `Dockerfile`: 3-stage build (deps → builder → runner), `node:20-alpine`, non-root user +- `Dockerfile`: 3-stage build (deps → builder → runner), `node:22-alpine`, non-root user +- Runner stage installs: `gdal gdal-tools ghostscript qpdf` (for PDF compression, GIS) - `Dockerfile` includes `npx prisma generate` before build step - `docker-compose.yml`: single service, port 3000, **all env vars hardcoded** (Portainer CE can't inject env vars) - `output: 'standalone'` in `next.config.ts` is **required** @@ -275,6 +336,15 @@ src/modules// └── index.ts # Public exports ``` +### Middleware & Large Upload Routes + +- Next.js middleware buffers the **entire request body** even if it only reads cookies/headers +- Default middleware body limit is 10MB — any upload route handling large files MUST be excluded +- Excluded routes pattern in `src/middleware.ts` matcher: `api/auth|api/notifications/digest|api/compress-pdf` +- Excluded routes handle auth via `requireAuth()` helper (`src/app/api/compress-pdf/auth-check.ts`) +- To add a new large-upload route: (1) add to middleware matcher exclusion, (2) add `requireAuth()` call in route handler +- `next.config.ts` has `experimental: { middlewareClientMaxBodySize: '500mb' }` but this is unreliable with `output: 'standalone'` + ### eTerra / External API Rules - **ArcGIS REST API** has `maxRecordCount=1000` — always paginate with `resultOffset`/`resultRecordCount` @@ -319,6 +389,8 @@ src/modules// | **PostGIS** | ✅ Active | `GisFeature` model, geometry storage, spatial queries, used by ParcelSync | | **Email Notifications** | ✅ Implemented | Brevo SMTP daily digest, `/api/notifications/digest` + `/preferences`, N8N cron trigger | | **N8N automations** | ✅ Active (digest cron) | Daily digest cron `0 8 * * 1-5`, Bearer token auth, future: backups, workflows | +| **iLovePDF API** | ✅ Active | Cloud PDF compression, `ILOVEPDF_PUBLIC_KEY` env, free tier 250 files/month | +| **qpdf** | ✅ Active | Local lossless PDF optimization, installed in Docker image (`apk add qpdf`) | ---