1693 lines
65 KiB
Markdown
1693 lines
65 KiB
Markdown
# Implementation Plan — Granular Task Breakdown
|
|
|
|
> **LEGACY (2026-08-02):** Dokumen plan ini ditulis saat infra masih Docker/Traefik. Produksi sekarang Caddy + Nix/systemd dengan port 4000-an. Gunakan hanya sebagai referensi historis.
|
|
|
|
Setiap task adalah unit kerja terkecil yang bisa dikerjakan dalam 1-4 jam. Format:
|
|
|
|
```
|
|
[ID] Task description
|
|
Files: path/to/file
|
|
Accept: criteria yang harus terpenuhi
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 1: Foundation + Document Scanner (Prioritas)
|
|
|
|
### Milestone 1.1 — Rust Backend Skeleton
|
|
|
|
**Goal**: Gateway + Worker connected ke NATS + Redis, upload flow end-to-end.
|
|
|
|
#### 1.1.1 — Init Rust Workspace
|
|
|
|
```
|
|
[1.1.1] Buat Cargo workspace dengan 4 crate: common, gateway, workers, wasm
|
|
Files:
|
|
apps/tools/backend/Cargo.toml (workspace definition)
|
|
apps/tools/backend/common/Cargo.toml (serde, uuid, chrono)
|
|
apps/tools/backend/gateway/Cargo.toml (axum, tokio, tower, async-nats, deadpool-redis, redis)
|
|
apps/tools/backend/workers/Cargo.toml (tokio, async-nats, redis, image, imageproc, lopdf, leptess, ort, rayon)
|
|
apps/tools/backend/wasm/Cargo.toml (wasm-bindgen, image, console-error-panic)
|
|
apps/tools/backend/rust-toolchain.toml (channel = "1.85")
|
|
Accept: cargo build —release works untuk semua crate (walaupun main.rs masih empty)
|
|
```
|
|
|
|
#### 1.1.2 — Common Types
|
|
|
|
```
|
|
[1.1.2] Define shared types: JobStatus enum, Job struct, Tool enum, ScanOptions, ImageOptions,
|
|
PdfOptions, UploadResponse, JobProgress
|
|
Files:
|
|
apps/tools/backend/common/src/lib.rs
|
|
apps/tools/backend/common/src/types.rs
|
|
Types:
|
|
JobStatus { Queued, Processing { stage: String, progress: u8 }, Completed, Failed(String) }
|
|
Tool { Scan, ImageCompress, ImageResize, ImageConvert, RemoveBg,
|
|
PdfMerge, PdfSplit, ImagesToPdf, PdfCompress, PdfToImages,
|
|
VideoCompress, AudioExtract, VideoTrim, GifMaker, AudioConvert }
|
|
Job { id: Uuid, tool: Tool, status: JobStatus, file_path: PathBuf,
|
|
result_path: Option<PathBuf>, file_size: u64, options: Value,
|
|
created_at: DateTime<Utc>, ttl_seconds: u64 }
|
|
JobProgress { job_id: Uuid, status: JobStatus, stage: String, progress: u8, message: String }
|
|
Accept: Semua type implements Serialize + Deserialize + Debug + Clone
|
|
```
|
|
|
|
#### 1.1.3 — Common Errors
|
|
|
|
```
|
|
[1.1.3] Define error types dengan thiserror
|
|
Files:
|
|
apps/tools/backend/common/src/error.rs
|
|
Errors:
|
|
UploadError (InvalidMime, FileTooLarge, Io, VirusDetected)
|
|
PipelineError (ImageLoad, EdgeDetection, Warp, Ocr, PdfGeneration, Timeout, Internal)
|
|
NatsError (Publish, Subscribe, JetStream, Timeout)
|
|
RedisError (Connection, Query, Serialization)
|
|
Accept: Setiap error punya Display + Source chain yang jelas
|
|
```
|
|
|
|
#### 1.1.4 — NATS Subjects & Streams
|
|
|
|
```
|
|
[1.1.4] Define NATS subject constants + stream configuration
|
|
Files:
|
|
apps/tools/backend/common/src/nats.rs
|
|
Subjects:
|
|
tools.scan.jobs → Queue for scan workers
|
|
tools.scan.progress → Fan-out progress events
|
|
tools.image.jobs
|
|
tools.image.progress
|
|
tools.pdf.jobs
|
|
tools.pdf.progress
|
|
tools.video.jobs
|
|
tools.video.progress
|
|
tools.scheduler.cleanup → Cron: cleanup expired files
|
|
Streams:
|
|
tools-jobs (max_age: 24h, storage: file)
|
|
tools-progress (max_age: 1h, storage: memory)
|
|
Accept: Unit test verifikasi format subject string
|
|
```
|
|
|
|
#### 1.1.5 — Gateway Config
|
|
|
|
```
|
|
[1.1.5] Environment-based configuration loader
|
|
Files:
|
|
apps/tools/backend/gateway/src/config.rs
|
|
Env vars:
|
|
GATEWAY_PORT (default: 3001)
|
|
NATS_URL (default: nats://localhost:4222)
|
|
REDIS_URL (default: redis://localhost:6379)
|
|
STORAGE_PATH (default: /data/tools)
|
|
MAX_FILE_SIZE_MB (default: 50)
|
|
JOB_TTL_SECONDS (default: 3600)
|
|
RATE_LIMIT_PER_MINUTE (default: 30)
|
|
RUST_LOG (default: info)
|
|
Accept: AppConfig struct dengan semua field, load dari env + fallback default
|
|
```
|
|
|
|
#### 1.1.6 — Gateway: Upload Route (POST /api/upload)
|
|
|
|
```
|
|
[1.1.6] Multipart file upload handler
|
|
Files:
|
|
apps/tools/backend/gateway/src/routes/upload.rs
|
|
apps/tools/backend/gateway/src/routes/mod.rs
|
|
Logic:
|
|
1. Extract multipart: file + tool + options
|
|
2. Validate MIME type (image/* for scan/image tools, application/pdf for pdf tools,
|
|
video/* for video tools, audio/* for audio tools)
|
|
3. Check file size < MAX_FILE_SIZE_MB
|
|
4. Scan magic bytes: verify actual content matches extension
|
|
5. Save file ke {STORAGE_PATH}/upload/{uuid}.{ext}
|
|
6. Generate job_id (Uuid v4)
|
|
7. Save job metadata ke Redis: SET job:{job_id} → JSON
|
|
8. Publish ke NATS: tools.{tool}.jobs → {job_id, file_path, options}
|
|
9. Return 202: { job_id, status: "queued", tool, ws_url }
|
|
Accept: curl upload → 202 + job_id. curl job_status → status field populated
|
|
```
|
|
|
|
#### 1.1.7 — Gateway: Job Status Route (GET /api/job/{id})
|
|
|
|
```
|
|
[1.1.7] Job status and metadata retrieval
|
|
Files:
|
|
apps/tools/backend/gateway/src/routes/job.rs
|
|
Logic:
|
|
1. Extract job_id from path
|
|
2. GET job:{job_id} from Redis
|
|
3. Return 404 kalau not found
|
|
4. Return JSON: { job_id, status, tool, progress, stage, message,
|
|
result: Option<{ download_url, file_size, file_name, preview_url }>,
|
|
created_at, error: Option<String> }
|
|
Endpoints:
|
|
GET /api/job/{id} → Single job status
|
|
GET /api/job/{id}/preview → Preview image (thumbnail)
|
|
Accept: curl GET → full job status JSON
|
|
```
|
|
|
|
#### 1.1.8 — Gateway: Download Route (GET /api/download/{id})
|
|
|
|
```
|
|
[1.1.8] File download with streaming + auto-cleanup awareness
|
|
Files:
|
|
apps/tools/backend/gateway/src/routes/download.rs
|
|
Logic:
|
|
1. Extract job_id, get status from Redis
|
|
2. Check status == completed → get result_path
|
|
3. Stream file via tokio::fs::File → axum body stream
|
|
4. Set Content-Disposition header with original filename
|
|
5. Set Content-Type based on file extension
|
|
6. Handle 404 (not found), 400 (not completed yet), 410 (expired)
|
|
Accept: curl GET /api/download/{id} → file download dengan correct headers
|
|
```
|
|
|
|
#### 1.1.9 — Gateway: Health & Metrics
|
|
|
|
```
|
|
[1.1.9] Health check + Prometheus metrics endpoint
|
|
Files:
|
|
apps/tools/backend/gateway/src/routes/health.rs
|
|
apps/tools/backend/gateway/src/metrics.rs
|
|
Endpoints:
|
|
GET /health → 200 OK (used by Traefik health check)
|
|
GET /metrics → Prometheus text format
|
|
Metrics (via custom counters, no external crate):
|
|
tools_uploaded_files_total → counter, labels: tool, status
|
|
tools_jobs_total → counter, labels: tool, status
|
|
tools_processing_duration_ms → histogram, labels: tool
|
|
tools_queue_depth → gauge, labels: tool
|
|
Accept: /health returns 200, /metrics returns prometheus-format text
|
|
```
|
|
|
|
#### 1.1.10 — Gateway: NATS Publisher
|
|
|
|
```
|
|
[1.1.10] NATS connection management + publish helpers
|
|
Files:
|
|
apps/tools/backend/gateway/src/nats/mod.rs
|
|
apps/tools/backend/gateway/src/nats/publisher.rs
|
|
Functions:
|
|
connect_nats(url: &str) → Result<Connection>
|
|
publish_job(nats: &Connection, tool: Tool, payload: &Job) → Result<()>
|
|
publish_progress(nats: &Connection, progress: &JobProgress) → Result<()>
|
|
Accept: Integration test: publish message → consume back via subscriber
|
|
```
|
|
|
|
#### 1.1.11 — Gateway: Redis Client
|
|
|
|
```
|
|
[1.1.11] Redis connection pool + job CRUD operations
|
|
Files:
|
|
apps/tools/backend/gateway/src/redis/mod.rs
|
|
apps/tools/backend/gateway/src/redis/job.rs
|
|
apps/tools/backend/gateway/src/redis/ratelimit.rs
|
|
Functions:
|
|
connect_redis(url: &str) → Result<ConnectionManager>
|
|
job_create(redis, job) → Result<()>
|
|
job_get(redis, job_id) → Result<Job>
|
|
job_update(redis, job_id, status) → Result<()>
|
|
job_delete(redis, job_id) → Result<()>
|
|
rate_limit_check(redis, ip, tool) → Result<bool> // sliding window
|
|
TTL: Set TTL JOB_TTL_SECONDS on job_create
|
|
Accept: Redis integration test: create → get → update → delete → not found
|
|
```
|
|
|
|
#### 1.1.12 — Gateway: Main Bootstrap
|
|
|
|
```
|
|
[1.1.12] Axum app assembly + graceful shutdown
|
|
Files:
|
|
apps/tools/backend/gateway/src/main.rs
|
|
Logic:
|
|
1. Load config
|
|
2. Init Redis connection pool
|
|
3. Init NATS connection
|
|
4. Build Axum router with all routes
|
|
5. Spawn NATS progress consumer (subscribe tools.*.progress, caches in Redis)
|
|
6. Start HTTP server on configured port
|
|
7. Graceful shutdown on SIGINT/SIGTERM
|
|
States: SharedState { redis, nats, config } wrapped in Arc
|
|
Accept: cargo run —bin gateway → server listening on :3001
|
|
```
|
|
|
|
#### 1.1.13 — Workers: NATS Consumer
|
|
|
|
```
|
|
[1.1.13] NATS JetStream consumer for job queues
|
|
Files:
|
|
apps/tools/backend/workers/src/main.rs
|
|
apps/tools/backend/workers/src/nats/mod.rs
|
|
apps/tools/backend/workers/src/nats/consumer.rs
|
|
Logic:
|
|
1. Connect to NATS + Redis
|
|
2. Subscribe to JetStream streams: tools.*.jobs
|
|
3. For each message:
|
|
a. Deserialize job payload
|
|
b. Match tool → dispatch to appropriate handler
|
|
c. Acknowledge after handler returns
|
|
d. NACK with delay on failure (for retry)
|
|
4. Max delivery: 3, then dead-letter
|
|
Max concurrency: configurable (TOOLS_WORKER_CONCURRENCY, default: 4)
|
|
Accept: Workers start, consume NATS messages, dispatch to tool handlers
|
|
```
|
|
|
|
#### 1.1.14 — Workers: Progress Publisher
|
|
|
|
```
|
|
[1.1.14] Progress reporting from worker to NATS → Gateway → WebSocket
|
|
Files:
|
|
apps/tools/backend/workers/src/nats/progress.rs
|
|
Functions:
|
|
report_progress(redis, nats, job_id, status, stage, progress, message)
|
|
1. Update Redis: job:{job_id} status + progress
|
|
2. Publish to NATS: tools.{tool}.progress → {job_id, status, stage, progress, message}
|
|
Helper: ProgressReporter struct yang implements Clone, bisa dipass ke pipeline
|
|
Accept: Worker updates progress → Gateway receives → WebSocket forwards
|
|
```
|
|
|
|
#### 1.1.15 — Worker: Scanner Stub
|
|
|
|
```
|
|
[1.1.15] Scanner worker yang bisa menerima job dan update progress
|
|
Files:
|
|
apps/tools/backend/workers/src/scanner/mod.rs
|
|
Logic:
|
|
1. Receive job: { job_id, file_path, options }
|
|
2. report_progress(queued → processing:10%)
|
|
3. sleep 2s (simulasi pipeline)
|
|
4. report_progress(processing:50%)
|
|
5. sleep 2s
|
|
6. Copy input file to output (simulasi hasil)
|
|
7. report_progress(completed:100%)
|
|
8. Update Redis with result_path
|
|
Accept: Upload → NATS queue → worker consume → progress update → completed
|
|
```
|
|
|
|
---
|
|
|
|
### Milestone 1.2 — Scanner Pipeline Core
|
|
|
|
**Goal**: Foto miring → lurus + bersih + hitam-putih (belum OCR/PDF).
|
|
|
|
#### 1.2.1 — Preprocess: Load & Resize
|
|
|
|
```
|
|
[1.2.1] Load image from file, resize if too large, convert to grayscale
|
|
Files:
|
|
apps/tools/backend/workers/src/scanner/preprocess.rs
|
|
Functions:
|
|
load_image(path: &Path) → Result<DynamicImage>
|
|
safe_resize(img: DynamicImage, max_dim: u32) → DynamicImage
|
|
to_grayscale(img: &DynamicImage) → GrayImage
|
|
Rules:
|
|
- Resize if max(width, height) > 2000px → scale down, preserve aspect ratio
|
|
- Lanczos3 filter untuk downscale (sharpest)
|
|
- Support input: JPEG, PNG, WebP, HEIC (if feature enabled)
|
|
Accept: Unit test: 12MP image → resize to ≤2000px, verify aspect ratio preserved
|
|
```
|
|
|
|
#### 1.2.2 — Edge Detection: Canny + Morphological Close
|
|
|
|
```
|
|
[1.2.2] Canny edge detection with morphological operations to connect broken edges
|
|
Files:
|
|
apps/tools/backend/workers/src/scanner/edge.rs
|
|
Functions:
|
|
detect_edges(img: &GrayImage) → GrayImage
|
|
morphological_close(edges: &GrayImage, kernel_size: u8) → GrayImage
|
|
Algorithm:
|
|
1. Gaussian blur (sigma=1.0) on grayscale
|
|
2. Canny with low_threshold=50, high_threshold=150
|
|
3. Morphological close: dilate → erode with 5x5 kernel
|
|
4. If edge_count < 1% of total pixels → retry Canny(20, 80)
|
|
Accept: Unit test: known test images → edge image with continuous document borders
|
|
```
|
|
|
|
#### 1.2.3 — Corner Detection: Largest Rectangle Contour
|
|
|
|
```
|
|
[1.2.3] Find 4 corners of the document from edge image
|
|
Files:
|
|
apps/tools/backend/workers/src/scanner/corners.rs
|
|
Functions:
|
|
find_contours(edges: &GrayImage) → Vec<Contour>
|
|
largest_rectangular_contour(contours: &[Contour]) -> Option<Contour>
|
|
approx_polygon(contour: &Contour, num_vertices: u32) → Option<Vec<(f64,f64)>>
|
|
order_corners(points: Vec<(f64,f64)>) -> [(f64,f64); 4]
|
|
detect_corners(img: &GrayImage) -> Result<[(f64,f64); 4], FallbackReason>
|
|
Algorithm:
|
|
1. imageproc::contours::find_contours
|
|
2. Filter by area > 20% of total image
|
|
3. Top 5 largest by contour area
|
|
4. For each: approx polygon, find 4-vertex polygon
|
|
5. Order: top-left, top-right, bottom-right, bottom-left
|
|
Accept: Unit test: 5 test images (normal, dark, angle, shadow, cluttered bg)
|
|
→ correct corners or explicit fallback
|
|
```
|
|
|
|
#### 1.2.4 — Perspective Warp: DLT Homography
|
|
|
|
```
|
|
[1.2.4] Compute homography matrix via DLT + SVD, apply perspective warp
|
|
Files:
|
|
apps/tools/backend/workers/src/scanner/warp.rs
|
|
Functions:
|
|
compute_homography(src: &[(f64,f64);4], dst: &[(f64,f64);4]) → [[f64;3];3]
|
|
invert_homography(h: &[[f64;3];3]) → [[f64;3];3]
|
|
apply_homography(h: &[[f64;3];3], x: f64, y: f64) → (f64, f64)
|
|
bilinear_interpolate(img: &GrayImage, x: f64, y: f64) → Luma<u8>
|
|
warp_perspective(img: &DynamicImage, corners: [(f64,f64);4]) → DynamicImage
|
|
Algorithm:
|
|
DLT (Direct Linear Transform):
|
|
- 4 point correspondences → 8x9 matrix A
|
|
- SVD (via ndarray + ndarray-linalg or nalgebra)
|
|
- H = last column of V, reshape to 3x3
|
|
Backward mapping:
|
|
- For each output pixel (x,y), compute source (sx,sy) via H_inv
|
|
- Bilinear interpolate from source
|
|
Accept: Unit test: 4 corners of known grid → warped image is perfectly rectangular
|
|
```
|
|
|
|
#### 1.2.5 — Shadow Removal: Illumination Correction
|
|
|
|
```
|
|
[1.2.5] Remove uneven lighting, shadows, and glare
|
|
Files:
|
|
apps/tools/backend/workers/src/scanner/shadow.rs
|
|
Functions:
|
|
gaussian_blur_large(img: &GrayImage, radius: f64) → GrayImage
|
|
subtract_background(img: &GrayImage, background: &GrayImage) -> GrayImage
|
|
apply_clahe(img: &GrayImage, tile_size: u8, clip_limit: u8) -> GrayImage
|
|
remove_shadow(img: &GrayImage) → GrayImage
|
|
Algorithm (primary):
|
|
1. Large Gaussian blur (radius = max_dim/50, min 15px) = illumination estimate
|
|
2. Subtract: pixel = max(0, original - background + mean(background))
|
|
3. CLAHE: 8x8 tiles, clip limit 3
|
|
Algorithm (fallback - Retinex):
|
|
1. log(I) = log(R) + log(L)
|
|
2. log(R) = log(I) - log(Gaussian*I)
|
|
3. exp(R), normalize to [0,255]
|
|
Accept: Unit test: image with shadow gradient → uniform illumination
|
|
```
|
|
|
|
#### 1.2.6 — Binarization: Sauvola Local Threshold
|
|
|
|
```
|
|
[1.2.6] Convert grayscale to clean black-and-white using adaptive threshold
|
|
Files:
|
|
apps/tools/backend/workers/src/scanner/binarize.rs
|
|
Functions:
|
|
compute_integral_image(img: &GrayImage) → Vec<u64>
|
|
compute_integral_image_sq(img: &GrayImage) -> Vec<u64>
|
|
local_stats(integral: &[u64], integral_sq: &[u64],
|
|
x: i32, y: i32, half_win: i32, w: i32, h: i32) -> (f64, f64)
|
|
sauvola_threshold(img: &GrayImage, window_size: u32, k: f64) -> GrayImage
|
|
otsu_threshold(img: &GrayImage) -> GrayImage // fallback
|
|
Algorithm:
|
|
Sauvola: T = m * (1 + k * (s/R - 1))
|
|
- m = local mean (from integral image)
|
|
- s = local std dev (from integral image squared)
|
|
- k = 0.2 (tunable)
|
|
- R = 128 (max std dev for 8-bit)
|
|
- window_size = max(width, height) / 30, clamped to [15, 100]
|
|
Accept: Unit test: 5 test images → binary output, text readable, background clean white
|
|
```
|
|
|
|
#### 1.2.7 — Deskew: Hough Transform Line Detection
|
|
|
|
```
|
|
[1.2.7] Detect and correct small rotation (<5°) of text lines
|
|
Files:
|
|
apps/tools/backend/workers/src/scanner/deskew.rs
|
|
Functions:
|
|
probabilistic_hough_lines(img: &GrayImage, threshold: u32,
|
|
min_line_length: f64, max_gap: f64) -> Vec<Line>
|
|
median_angle(lines: &[Line]) -> f64
|
|
rotate_image(img: &GrayImage, angle_degrees: f64) -> GrayImage
|
|
deskew(img: &GrayImage) -> GrayImage
|
|
Algorithm:
|
|
1. Probabilistic Hough line transform
|
|
2. Filter: keep lines with angle between -45° and +45° (skip vertical)
|
|
3. Compute median angle
|
|
4. If |angle| > 0.5° → rotate with Lanczos3, crop to fit
|
|
Accept: Unit test: rotated text image 3° → deskewed to <0.5° residual rotation
|
|
```
|
|
|
|
#### 1.2.8 — Image Enhancement: Sharpening + Contrast
|
|
|
|
```
|
|
[1.2.8] Apply final sharpening and contrast optimization
|
|
Files:
|
|
apps/tools/backend/workers/src/scanner/enhance.rs
|
|
Functions:
|
|
unsharp_mask(img: &GrayImage, sigma: f64, amount: f64) -> GrayImage
|
|
adjust_contrast(img: &GrayImage, factor: f64) -> GrayImage
|
|
remove_noise(img: &GrayImage, threshold: u8) -> GrayImage
|
|
enhance_final(img: &GrayImage) -> GrayImage
|
|
Algorithm (Unsharp mask):
|
|
blurred = gaussian_blur(img, sigma=1.0)
|
|
mask = img - blurred
|
|
result = img + amount * mask // amount = 1.0 (default)
|
|
Accept: Unit test: blurry text → sharpened text, verify no ringing artifacts
|
|
```
|
|
|
|
#### 1.2.9 — Pipeline Assembly
|
|
|
|
```
|
|
[1.2.9] Connect all pipeline stages with progress reporting
|
|
Files:
|
|
apps/tools/backend/workers/src/scanner/pipeline.rs
|
|
apps/tools/backend/workers/src/scanner/mod.rs (update)
|
|
Functions:
|
|
ScanPipeline::process(input_path, options, progress: ProgressReporter) → Result<ScanResult>
|
|
Stages with progress:
|
|
0% → load + preprocess
|
|
15% → edge detection
|
|
25% → corner detection
|
|
35% → perspective warp
|
|
50% → shadow removal
|
|
65% → binarization
|
|
75% → deskew
|
|
85% → final enhance
|
|
100% → complete
|
|
ScanResult:
|
|
{ output_image_path, page_count: 1, image_dimensions, processing_time_ms }
|
|
Accept: Full pipeline test with 10 diverse test images → consistent quality output
|
|
```
|
|
|
|
---
|
|
|
|
### Milestone 1.3 — Next.js Frontend Foundation
|
|
|
|
**Goal**: User bisa upload foto, lihat progress, download hasil.
|
|
|
|
#### 1.3.1 — Init Next.js App
|
|
|
|
```
|
|
[1.3.1] Create Next.js 16 app with Tailwind v4 + shadcn/ui + TypeScript strict
|
|
Files:
|
|
apps/tools/frontend/package.json
|
|
apps/tools/frontend/next.config.ts
|
|
apps/tools/frontend/tsconfig.json
|
|
apps/tools/frontend/postcss.config.mjs
|
|
apps/tools/frontend/components.json
|
|
apps/tools/frontend/biome.json
|
|
apps/tools/frontend/src/app/globals.css
|
|
Setup:
|
|
bun create next-app@latest --typescript --tailwind --eslint
|
|
bun add @shadcn/react lucide-react class-variance-authority clsx tailwind-merge framer-motion
|
|
npx shadcn@latest init
|
|
Add custom CSS variables + Twilight Terminal theme
|
|
Accept: bun dev → localhost:3002, halaman kosong dengan Tailwind + shadcn working
|
|
```
|
|
|
|
#### 1.3.2 — Root Layout + Theme Provider
|
|
|
|
```
|
|
[1.3.2] Layout dengan header, footer, theme provider, fonts
|
|
Files:
|
|
apps/tools/frontend/src/app/layout.tsx
|
|
apps/tools/frontend/src/app/providers.tsx
|
|
apps/tools/frontend/src/components/tools/header.tsx
|
|
apps/tools/frontend/src/components/tools/footer.tsx
|
|
apps/tools/frontend/src/lib/utils.ts
|
|
Features:
|
|
- Root layout with metadata (title: "Tools — Asep Haryana")
|
|
- ThemeProvider (next-themes) wrapping children
|
|
- Geist sans font (same as hub portfolio)
|
|
- Header: logo "Tools", navigation links, theme toggle, GitHub link
|
|
- Footer: copyright, powered by Rust + Next.js badge
|
|
- cn() utility from tailwind-merge
|
|
Accept: All pages render with header + footer, theme toggle works
|
|
```
|
|
|
|
#### 1.3.3 — Landing Page (/) with Tool Cards
|
|
|
|
```
|
|
[1.3.3] Card grid showing all available tools with icons
|
|
Files:
|
|
apps/tools/frontend/src/app/page.tsx
|
|
apps/tools/frontend/src/components/tools/tool-card.tsx
|
|
apps/tools/frontend/src/components/tools/tool-grid.tsx
|
|
Data:
|
|
tools = [
|
|
{ id: "scan", title: "Document Scanner", desc: "...", icon: ScanIcon, href: "/scan", phase: 1 },
|
|
{ id: "image-compress", title: "Compress Image", desc: "...", icon: ... },
|
|
...
|
|
]
|
|
Features:
|
|
- Grid responsive: 1 col mobile, 2 col tablet, 3 col desktop
|
|
- Each card: icon, title, description, link
|
|
- Phase badges: "Available", "Coming Soon"
|
|
- Framer Motion stagger animation on mount
|
|
Accept: / renders grid of tool cards, each card is clickable link
|
|
```
|
|
|
|
#### 1.3.4 — Upload Zone Component
|
|
|
|
```
|
|
[1.3.4] Drag & drop upload zone with file validation
|
|
Files:
|
|
apps/tools/frontend/src/components/tools/upload-zone.tsx
|
|
apps/tools/frontend/src/hooks/use-upload.ts
|
|
Features:
|
|
- Drag & drop area with dashed border
|
|
- Click to open file picker
|
|
- Accept attribute berdasarkan tool (image/*, application/pdf, video/*, audio/*)
|
|
- Validate: file type, max size (50MB), max count (50 for batch)
|
|
- Show file name, size, type after selection
|
|
- Error state: invalid type, too large, too many
|
|
- Drag over highlight animation
|
|
- Loading spinner during upload
|
|
- Upload progress percentage (from XHR or fetch)
|
|
- Cancel upload button
|
|
Accept: Drag image file → uploads to server → returns job_id
|
|
```
|
|
|
|
#### 1.3.5 — Progress Bar Component
|
|
|
|
```
|
|
[1.3.5] Animated progress bar with stage label from WebSocket
|
|
Files:
|
|
apps/tools/frontend/src/components/tools/progress-bar.tsx
|
|
apps/tools/frontend/src/hooks/use-job-status.ts
|
|
WebSocket hook (useJobStatus):
|
|
- Connect to /api/job/{id}/ws
|
|
- Auto-reconnect on disconnect (3 retries)
|
|
- Parse JobProgress messages
|
|
- Update state: status, progress, stage, message
|
|
- Cleanup on unmount
|
|
ProgressBar:
|
|
- Animated bar (Framer Motion width animation)
|
|
- Stage label: "Detecting edges...", "Correcting perspective...", etc.
|
|
- Percentage number
|
|
- Status badge: Processing (amber pulse), Completed (green), Failed (red)
|
|
- Error state with retry button
|
|
Accept: Upload → progress bar animates from 0-100% with stage labels
|
|
```
|
|
|
|
#### 1.3.6 — Preview Before/After Component
|
|
|
|
```
|
|
[1.3.6] Image preview with before/after comparison slider
|
|
Files:
|
|
apps/tools/frontend/src/components/tools/preview-before-after.tsx
|
|
Features:
|
|
- Two image layers: original (left) vs processed (right)
|
|
- Draggable slider divider
|
|
- Click on left = show original, click right = show processed
|
|
- Zoom: scroll to zoom, drag to pan
|
|
- File size comparison badge: "2.4 MB → 340 KB"
|
|
- Responsive: fill container width
|
|
Accept: Component renders with two image URLs, slider interaction works
|
|
```
|
|
|
|
#### 1.3.7 — Result Preview Component
|
|
|
|
```
|
|
[1.3.7] Result display: preview, download, info
|
|
Files:
|
|
apps/tools/frontend/src/components/tools/result-preview.tsx
|
|
Features:
|
|
- Show processed file preview (image or icon for PDF/video/audio)
|
|
- File info: name, size, dimensions, pages (for PDF)
|
|
- Download button with file type icon
|
|
- Download as ZIP for batch results
|
|
- Copy share link button (if applicable)
|
|
- "Process another" button → reset to upload state
|
|
- Auto-download option checkbox
|
|
Accept: Job completes → result card shows with download button → click downloads file
|
|
```
|
|
|
|
#### 1.3.8 — Scanner Page (/scan)
|
|
|
|
```
|
|
[1.3.8] Full scanner page: upload → progress → result
|
|
Files:
|
|
apps/tools/frontend/src/app/scan/page.tsx
|
|
apps/tools/frontend/src/app/scan/result/[id]/page.tsx
|
|
Page states:
|
|
1. UPLOAD: UploadZone + options (OCR toggle, enhance toggle, DPI selector)
|
|
2. PROCESSING: ProgressBar + stage label + cancel button
|
|
3. RESULT: PreviewBeforeAfter + ResultPreview + "Process Another"
|
|
4. ERROR: Error message with retry + feedback button
|
|
Options panel:
|
|
- Enable OCR (toggle, default: on)
|
|
- Auto-enhance (toggle, default: on)
|
|
- Output format (PDF, JPEG, PNG — default: PDF)
|
|
- DPI (150, 200, 300, 400 — default: 300)
|
|
Flow:
|
|
Upload → POST /api/upload → get job_id → connect WS → show progress →
|
|
complete → show result with preview + download
|
|
Accept: Full user flow: upload → progress → download PDF
|
|
```
|
|
|
|
#### 1.3.9 — API Route: Upload Proxy
|
|
|
|
```
|
|
[1.3.9] Next.js API route that proxies upload to Rust backend
|
|
Files:
|
|
apps/tools/frontend/src/app/api/upload/route.ts
|
|
Logic:
|
|
- Accept multipart/form-data from browser
|
|
- Forward to http://tools:3001/api/upload (Rust gateway)
|
|
- On 202: return { job_id, ws_url } to client
|
|
- On 4xx/5xx: return error to client
|
|
- Handle timeout, connection refused gracefully
|
|
Accept: POST via browser → proxied to Rust → returns job_id
|
|
```
|
|
|
|
#### 1.3.10 — API Route: Job Status & Download Proxy
|
|
|
|
```
|
|
[1.3.10] Proxy job status + WebSocket + download to Rust backend
|
|
Files:
|
|
apps/tools/frontend/src/app/api/job/[id]/route.ts
|
|
apps/tools/frontend/src/app/api/job/[id]/ws/route.ts
|
|
apps/tools/frontend/src/app/api/download/[id]/route.ts
|
|
Features:
|
|
GET /api/job/{id} → proxy to Rust
|
|
WS /api/job/{id}/ws → proxy WebSocket (Next.js can't do WS in app router,
|
|
so use upgrade header or direct client WS to Rust port)
|
|
GET /api/download/{id} → stream from Rust
|
|
Note: WebSocket langsung dari client ke Rust gateway port (3001),
|
|
bukan via Next.js. CORS sudah dihandle di Rust.
|
|
Accept: WS connection works: client → Rust gateway → NATS progress → browser
|
|
```
|
|
|
|
---
|
|
|
|
### Milestone 1.4 — OCR + Searchable PDF + Infrastructure
|
|
|
|
**Goal**: Output searchable PDF, realtime WebSocket progress, auto-cleanup, rate limiting.
|
|
|
|
#### 1.4.1 — Tesseract OCR Integration
|
|
|
|
```
|
|
[1.4.1] OCR text extraction from processed image
|
|
Files:
|
|
apps/tools/backend/workers/src/scanner/ocr.rs
|
|
Functions:
|
|
init_tesseract(lang: &str) -> Result<LepTess>
|
|
ocr_text(tess: &mut LepTess, img: &GrayImage) -> Result<String>
|
|
ocr_words(tess: &mut LepTess, img: &GrayImage) -> Result<Vec<OcrWord>>
|
|
ocr_with_language(img: &GrayImage, lang: &str) -> Result<OcrResult>
|
|
OcrWord: { text: String, bbox: {x,y,w,h}, confidence: i32 }
|
|
OcrResult: { full_text: String, words: Vec<OcrWord>, confidence: f32 }
|
|
Languages: "eng+ind" (English + Indonesian)
|
|
TESSDATA_PREFIX: /usr/share/tesseract-ocr/5/tessdata
|
|
PSM mode: 3 (automatic), fallback 6 (single text block)
|
|
Accept: Unit test: known text image → OCR returns text with >80% confidence
|
|
```
|
|
|
|
#### 1.4.2 — Searchable PDF Generation
|
|
|
|
```
|
|
[1.4.2] Generate PDF with visible image + invisible text layer
|
|
Files:
|
|
apps/tools/backend/workers/src/scanner/pdf.rs
|
|
Functions:
|
|
compress_image_jpeg(img: &GrayImage, quality: u8) -> Result<Vec<u8>>
|
|
generate_pdf_page(image_data: &[u8], words: &[OcrWord],
|
|
page_width_pt: f64, page_height_pt: f64) -> Result<Document>
|
|
generate_searchable_pdf(image_data: &[u8], ocr_text: &str,
|
|
words: &[OcrWord]) -> Result<Vec<u8>>
|
|
PDF structure:
|
|
- Page with MediaBox A4 (595.28 x 841.89) or fit to image aspect ratio
|
|
- Image XObject (JPEG DCTDecode, 300 DPI equivalent)
|
|
- Content stream:
|
|
1. Place image at full page: q {w} 0 0 {h} 0 0 cm /Im0 Do Q
|
|
2. Invisible text: 3 Tr (rendering mode 3 = neither fill nor stroke)
|
|
3. Each word positioned at its bbox, converted pixels → points
|
|
- Metadata: Producer, CreationDate
|
|
Accept: Generated PDF → open in browser → text is selectable + searchable
|
|
```
|
|
|
|
#### 1.4.3 — Scanner Pipeline: OCR + PDF Integration
|
|
|
|
```
|
|
[1.4.3] Connect OCR + PDF generation into the main pipeline
|
|
Files:
|
|
apps/tools/backend/workers/src/scanner/pipeline.rs (update)
|
|
Updated stages:
|
|
75% → deskew
|
|
82% → OCR
|
|
90% → PDF generation
|
|
100% → save + complete
|
|
Pipeline now returns ScanResult:
|
|
{ output_path, page_count, file_size, ocr_text, processing_time_ms }
|
|
Accept: Full pipeline test: input image → output searchable PDF file
|
|
```
|
|
|
|
#### 1.4.4 — WebSocket Progress Forwarding (Gateway)
|
|
|
|
```
|
|
[1.4.4] Gateway subscribes to NATS progress and forwards via WebSocket
|
|
Files:
|
|
apps/tools/backend/gateway/src/routes/ws.rs
|
|
Functions:
|
|
ws_handler(ws: WebSocketUpgrade, job_id: Path<String>, state: SharedState)
|
|
handle_ws(mut ws: WebSocket, job_id: String, nats: Connection, redis: ConnectionManager)
|
|
Logic:
|
|
1. Accept WebSocket upgrade
|
|
2. Subscribe to NATS: tools.*.progress.{job_id}
|
|
3. Forward each message as JSON to WebSocket client
|
|
4. On connection close: unsubscribe from NATS
|
|
5. Keepalive ping every 30 seconds
|
|
6. Send initial status from Redis on connect
|
|
Accept: Client connects via WS → receives progress messages in realtime
|
|
```
|
|
|
|
#### 1.4.5 — Auto-Cleanup Scheduler
|
|
|
|
```
|
|
[1.4.5] Scheduled cleanup of expired files and Redis keys
|
|
Files:
|
|
apps/tools/backend/workers/src/scheduler/mod.rs
|
|
apps/tools/backend/workers/src/scheduler/cleanup.rs
|
|
Logic:
|
|
1. Subscribe to NATS cron: tools.scheduler.cleanup (every 10 min)
|
|
2. Scan {STORAGE_PATH} for files older than JOB_TTL_SECONDS
|
|
3. Delete expired files
|
|
4. SCAN Redis for job:* keys with TTL expired, delete orphans
|
|
5. Log: deleted N files, freed M bytes, deleted K orphan keys
|
|
6. Prometheus: tools_cleanup_deleted_files counter
|
|
Periodic trigger: NATS cron via tools.scheduler.cleanup subject
|
|
Accept: Upload file → wait TTL → file deleted automatically
|
|
```
|
|
|
|
#### 1.4.6 — Rate Limiting
|
|
|
|
```
|
|
[1.4.6] Rate limiting per IP per tool via Redis sliding window
|
|
Files:
|
|
apps/tools/backend/gateway/src/redis/ratelimit.rs (update)
|
|
apps/tools/backend/gateway/src/routes/upload.rs (middleware)
|
|
Algorithm (Sliding Window):
|
|
Key: ratelimit:{ip}:{tool}:{minute_bucket}
|
|
- ZADD current timestamp
|
|
- ZREMRANGEBYSCORE older than 60s
|
|
- ZCOUNT → if > RATE_LIMIT_PER_MINUTE → reject
|
|
Response on reject: 429 Too Many Requests
|
|
{ error: "rate_limit_exceeded", retry_after_seconds: 60 }
|
|
Middleware: Add to upload route as tower Layer
|
|
Accept: curl 31x in 60s → 429 on 31st request
|
|
```
|
|
|
|
#### 1.4.7 — Error Handling & Validation (Gateway Middleware)
|
|
|
|
```
|
|
[1.4.7] Global error handling and input validation middleware
|
|
Files:
|
|
apps/tools/backend/gateway/src/middleware/mod.rs
|
|
apps/tools/backend/gateway/src/middleware/error_handler.rs
|
|
apps/tools/backend/gateway/src/middleware/request_id.rs
|
|
Features:
|
|
- Request ID middleware (X-Request-Id header, uuid v4)
|
|
- JSON error response format: { error: string, code: string, request_id: string }
|
|
- 400: invalid input (missing file, missing tool, invalid options)
|
|
- 404: job not found / file expired
|
|
- 413: file too large
|
|
- 429: rate limited
|
|
- 500: internal error (logged, not exposed to client)
|
|
- Panic recovery layer
|
|
Accept: curl with missing fields → 400 JSON error. curl bad tool → 400.
|
|
```
|
|
|
|
---
|
|
|
|
### Phase 1 Complete: Document Scanner MVP
|
|
|
|
**Acceptance criteria**:
|
|
1. User buka tools.asepharyana.my.id → landing page with tool cards
|
|
2. Click "Document Scanner" → halaman scan
|
|
3. Upload foto dokumen HP (miring, bayangan) → preview upload
|
|
4. Progress bar animasi: edge → warp → enhance → binarize → OCR → PDF
|
|
5. Download searchable PDF → teks bisa di-copy
|
|
6. Rate limit: 30 uploads/min/IP
|
|
7. File auto-delete after 1 hour
|
|
|
|
---
|
|
|
|
## Phase 2: Scanner Complete + Image Tools
|
|
|
|
### Milestone 2.1 — Scanner Robustness (Manual Crop + Camera + Batch)
|
|
|
|
#### 2.1.1 — Manual Crop Canvas
|
|
|
|
```
|
|
[2.1.1] Interactive canvas with 4 draggable corner handles
|
|
Files:
|
|
apps/tools/frontend/src/components/tools/crop-editor.tsx
|
|
apps/tools/frontend/src/hooks/use-crop-editor.ts
|
|
Features:
|
|
- Render uploaded image on Canvas
|
|
- 4 draggable corner handles (circular, Luma color)
|
|
- Connect corners with dashed lines
|
|
- Zoom: scroll to zoom, drag canvas to pan
|
|
- Grid overlay (rule of thirds) for alignment
|
|
- Touch support: pinch zoom, drag handles
|
|
- Double-click to auto-detect corners again
|
|
- Reset button
|
|
- Confirm button → sends corner coordinates + image to server
|
|
API: POST /api/scan/manual-crop
|
|
{ job_id, corners: [{x,y},{x,y},{x,y},{x,y}] }
|
|
Accept: User adjust corners → confirm → server applies warp with given corners
|
|
```
|
|
|
|
#### 2.1.2 — Fallback Pipeline: Auto → Manual → Process
|
|
|
|
```
|
|
[2.1.2] When auto edge detection fails, fall back to manual crop
|
|
Files:
|
|
apps/tools/backend/gateway/src/routes/upload.rs (update)
|
|
apps/tools/frontend/src/app/scan/page.tsx (update)
|
|
Flow:
|
|
1. Upload → queue job → worker coba auto-detect
|
|
2. Auto gagal → worker set status = "needs_manual_crop"
|
|
→ return corner confidence < 0.6
|
|
3. Gateway returns: { job_id, status: "needs_manual_crop",
|
|
manual_crop_url: "/scan/crop/{job_id}" }
|
|
4. Frontend redirect to CropEditor
|
|
5. User adjust corners → POST manual-crop → worker resume pipeline
|
|
Accept: Upload bad photo → auto redirect to manual crop → complete pipeline
|
|
```
|
|
|
|
#### 2.1.3 — PWA Camera Capture
|
|
|
|
```
|
|
[2.1.3] Direct camera capture from browser (PWA)
|
|
Files:
|
|
apps/tools/frontend/src/components/tools/camera-capture.tsx
|
|
apps/tools/frontend/src/hooks/use-camera.ts
|
|
Features:
|
|
- Access rear camera via getUserMedia
|
|
- Live preview in viewfinder
|
|
- Auto-focus on tap
|
|
- Capture button → freeze frame
|
|
- Aspect ratio guide overlay (A4: 1:1.414)
|
|
- Reject blurry photos (Laplacian variance check via Canvas API)
|
|
- Auto-capture when document detected (stabilize → snap)
|
|
- Switch front/rear camera
|
|
- Torch/flash toggle (if supported)
|
|
- Zoom slider (if supported)
|
|
Accept: Click "Use Camera" → camera opens → capture → photo uploaded
|
|
```
|
|
|
|
#### 2.1.4 — Batch Multi-Page
|
|
|
|
```
|
|
[2.1.4] Multiple pages → one PDF, parallel processing
|
|
Files:
|
|
apps/tools/frontend/src/components/tools/file-list.tsx
|
|
apps/tools/backend/workers/src/scanner/pipeline.rs (update)
|
|
Frontend:
|
|
- Upload multiple files (drag multiple or multi-select)
|
|
- Thumbnail list with drag-to-reorder
|
|
- Remove individual pages
|
|
- Add more pages button
|
|
- Upload all → single group job → N individual jobs
|
|
- Per-page progress status in thumbnail list
|
|
Backend:
|
|
- Group job: { group_id, job_ids: [...], total: N, completed: 0 }
|
|
- Process each page via Rayon parallel for
|
|
- Merge all PDF pages into single document via lopdf
|
|
- Report: per-page progress + overall progress % (completed/total)
|
|
Accept: Upload 5 photos → all processed in parallel → single 5-page PDF
|
|
```
|
|
|
|
#### 2.1.5 — Scan Options Panel
|
|
|
|
```
|
|
[2.1.5] Enhanced options panel for scanner
|
|
Files:
|
|
apps/tools/frontend/src/components/tools/scan-options.tsx
|
|
apps/tools/frontend/src/app/scan/page.tsx (update)
|
|
Options:
|
|
- Output: PDF, JPEG, PNG
|
|
- Quality: 1-100 (slider, default 90)
|
|
- OCR: on/off (default: on)
|
|
- Language: English, Indonesian, Both (default: Both)
|
|
- Auto-enhance: on/off (default: on)
|
|
- Color mode: Black & White, Grayscale, Color (default: B&W)
|
|
- Page size: A4, Letter, Auto-fit (default: A4)
|
|
- DPI: 150/200/300/400 (default: 300)
|
|
Accept: Change options → upload → output sesuai pilihan
|
|
```
|
|
|
|
---
|
|
|
|
### Milestone 2.2 — WASM Image Tools (Compress, Resize, Convert)
|
|
|
|
#### 2.2.1 — WASM Rust Crate
|
|
|
|
```
|
|
[2.2.1] Compile image processing to WebAssembly via wasm-pack
|
|
Files:
|
|
apps/tools/backend/wasm/Cargo.toml
|
|
apps/tools/backend/wasm/src/lib.rs
|
|
apps/tools/backend/wasm/src/compress.rs
|
|
apps/tools/backend/wasm/src/resize.rs
|
|
apps/tools/backend/wasm/src/convert.rs
|
|
Functions:
|
|
#[wasm_bindgen]
|
|
fn compress_jpeg(bytes: &[u8], quality: u8) -> Result<Vec<u8>>
|
|
fn compress_png(bytes: &[u8], effort: u8) -> Result<Vec<u8>>
|
|
fn compress_webp(bytes: &[u8], quality: u8) -> Result<Vec<u8>>
|
|
fn resize_image(bytes: &[u8], width: u32, height: u32, fit: String) -> Result<Vec<u8>>
|
|
fn convert_format(bytes: &[u8], target_format: String) -> Result<Vec<u8>>
|
|
fn get_image_info(bytes: &[u8]) -> Result<JsValue> // { width, height, format, size }
|
|
Build: wasm-pack build --release --target web
|
|
Output: pkg/ directory (wasm binary + JS glue)
|
|
Accept: wasm-pack build succeeds. Node.js test: compress results smaller than input
|
|
```
|
|
|
|
#### 2.2.2 — WASM Integration in Frontend
|
|
|
|
```
|
|
[2.2.2] Load and invoke WASM module from Next.js
|
|
Files:
|
|
apps/tools/frontend/src/lib/wasm/loader.ts
|
|
apps/tools/frontend/src/lib/wasm/image-processor.ts
|
|
Functions:
|
|
async initWasm() → WASM module instance (lazy load on first use)
|
|
async compressInBrowser(file: File, quality: number) → Blob
|
|
async resizeInBrowser(file: File, width: number, height: number) → Blob
|
|
async convertInBrowser(file: File, format: string) → Blob
|
|
Strategy:
|
|
- Dynamic import: await import('./pkg/image_wasm.js')
|
|
- Lazy init: only load when user visits image tool page
|
|
- Code splitting: WASM chunk loaded separately (~1.5MB gzipped)
|
|
- Cache: once loaded, keep in memory
|
|
- Fallback: if WASM fails → upload to server worker
|
|
Accept: Browser loads WASM → image processing runs client-side, no upload
|
|
```
|
|
|
|
#### 2.2.3 — Image Compress Page (/image/compress)
|
|
|
|
```
|
|
[2.2.3] Full compress page with quality slider and live comparison
|
|
Files:
|
|
apps/tools/frontend/src/app/image/compress/page.tsx
|
|
apps/tools/frontend/src/components/tools/quality-slider.tsx
|
|
Features:
|
|
- UploadZone (accepts image/*)
|
|
- Quality slider: 1-100, live preview update
|
|
- File size display: original vs compressed (estimated)
|
|
- PreviewBeforeAfter (original vs compressed)
|
|
- Format selector: JPEG, PNG, WebP
|
|
- Download button
|
|
- Batch mode: compress all images in folder
|
|
WASM flow: upload → load in WASM → compress → preview → download (no server)
|
|
Server fallback: upload → Rust worker compress → download
|
|
Accept: Upload photo → adjust quality → live preview → download compressed version
|
|
```
|
|
|
|
#### 2.2.4 — Image Resize Page (/image/resize)
|
|
|
|
```
|
|
[2.2.4] Resize page with dimension input and aspect ratio lock
|
|
Files:
|
|
apps/tools/frontend/src/app/image/resize/page.tsx
|
|
apps/tools/frontend/src/components/tools/dimension-input.tsx
|
|
Features:
|
|
- UploadZone
|
|
- Input: width + height, auto-fill from original
|
|
- Aspect ratio lock toggle (🔗/🔓)
|
|
- Preset sizes: 800x600, 1024x768, 1920x1080, Instagram (1080x1080), Custom
|
|
- Fit modes: exact (stretch), contain (fit within, add bg), cover (crop to fill)
|
|
- Preview: resized dimensions overlay
|
|
- Download (or ZIP for batch)
|
|
- Batch: resize all images to same dimensions
|
|
Accept: Upload photo → set 800px width → lock aspect → download resized image
|
|
```
|
|
|
|
#### 2.2.5 — Image Convert Page (/image/convert)
|
|
|
|
```
|
|
[2.2.5] Format conversion page
|
|
Files:
|
|
apps/tools/frontend/src/app/image/convert/page.tsx
|
|
Features:
|
|
- UploadZone
|
|
- From: auto-detected from file
|
|
- To: JPEG, PNG, WebP, GIF, BMP, TIFF
|
|
- Quality slider (for lossy formats)
|
|
- PreviewBeforeAfter
|
|
- Download
|
|
- Batch: convert all files in folder
|
|
Accept: Upload HEIC → convert to JPEG → download
|
|
```
|
|
|
|
#### 2.2.6 — Server Fallback for WASM
|
|
|
|
```
|
|
[2.2.6] Server-side processing when WASM unavailable
|
|
Files:
|
|
apps/tools/backend/workers/src/image/mod.rs
|
|
apps/tools/backend/workers/src/image/compress.rs
|
|
apps/tools/backend/workers/src/image/resize.rs
|
|
apps/tools/backend/workers/src/image/convert.rs
|
|
Logic:
|
|
- Same pipeline as WASM but runs natively
|
|
- Use image crate with mozjpeg feature for optimal JPEG
|
|
- Resize with Lanczos3 filter
|
|
- Convert via image crate format support
|
|
Frontend detection:
|
|
- try/catch WASM init → if fails, enable "Server Process" button
|
|
- Upload → NATS → worker → result (same as scanner flow)
|
|
Accept: Disable WASM in browser → upload fallback works → same quality output
|
|
```
|
|
|
|
---
|
|
|
|
### Milestone 2.3 — Background Removal (ONNX)
|
|
|
|
#### 2.3.1 — ONNX Model Download & Setup
|
|
|
|
```
|
|
[2.3.1] Download and package background removal model
|
|
Files:
|
|
Dockerfile (update — or runtime download script)
|
|
models/download-models.sh
|
|
Models:
|
|
- Primary: MODNet (~25MB, good quality, fast)
|
|
- Alternative: DIS_seg (~8MB, decent quality, very fast)
|
|
Setup:
|
|
- Download model file to models/
|
|
- Verify SHA256 checksum
|
|
- Load model at worker startup
|
|
- One-time init, cache in memory
|
|
ONNX session options:
|
|
- InterOpNumThreads: 2
|
|
- IntraOpNumThreads: 4
|
|
- GraphOptimizationLevel: ORT_ENABLE_ALL
|
|
Accept: Worker starts → loads ONNX model → ready for inference
|
|
```
|
|
|
|
#### 2.3.2 — Pre/Post Processing
|
|
|
|
```
|
|
[2.3.2] Image preprocessing and postprocessing for ONNX model
|
|
Files:
|
|
apps/tools/backend/workers/src/image/remove_bg.rs
|
|
Functions:
|
|
preprocess_for_model(img: &DynamicImage, target_size: u32) -> Result<Array4<f32>>
|
|
- Resize to model input size (1024x1024 for MODNet)
|
|
- Normalize: /255.0, mean=[0.5,0.5,0.5], std=[0.5,0.5,0.5]
|
|
- Convert to CHW format (ort::Tensor)
|
|
postprocess_mask(output: &ort::Tensor, original_size: (u32,u32)) -> GrayImage
|
|
- Sigmoid → threshold 0.5 → resize to original dimensions
|
|
- Apply optional smoothing (Gaussian blur sigma=1)
|
|
apply_alpha(img: &DynamicImage, mask: &GrayImage, bg_color: Option<[u8;3]>) -> DynamicImage
|
|
- If bg_color = None → RGBA with transparent background
|
|
- If bg_color = Some([r,g,b]) → composite onto solid color
|
|
Accept: Unit test: portrait photo → mask correctly separates foreground
|
|
```
|
|
|
|
#### 2.3.3 — Remove Background Page (/image/remove-bg)
|
|
|
|
```
|
|
[2.3.3] Background removal page
|
|
Files:
|
|
apps/tools/frontend/src/app/image/remove-bg/page.tsx
|
|
Features:
|
|
- UploadZone
|
|
- Processing via server worker (too heavy for WASM)
|
|
- PreviewBeforeAfter: original → transparent bg
|
|
- Background selector: transparent, white, color picker
|
|
- Download as PNG (transparent) or JPEG (with bg color)
|
|
- Download HD (full resolution) or Web (compressed)
|
|
- Batch processing
|
|
- Progress bar during inference
|
|
Accept: Upload photo → background removed → download with transparent PNG
|
|
```
|
|
|
|
---
|
|
|
|
### Phase 2 Complete: Document Scanner Robust + Image Tools
|
|
|
|
---
|
|
|
|
## Phase 3: PDF Tools
|
|
|
|
### Milestone 3.1 — PDF Worker
|
|
|
|
#### 3.1.1 — PDF Merge Worker
|
|
|
|
```
|
|
[3.1.1] Merge multiple PDF files into one
|
|
Files:
|
|
apps/tools/backend/workers/src/pdf/mod.rs
|
|
apps/tools/backend/workers/src/pdf/merge.rs
|
|
apps/tools/frontend/src/app/pdf/merge/page.tsx
|
|
Logic:
|
|
1. Upload 2+ PDF files
|
|
2. Load each via lopdf::Document::load
|
|
3. Iterate pages from each document, append to output
|
|
4. Save merged document
|
|
5. Preserve: page size (use largest MediaBox per page), fonts (embedded)
|
|
Frontend:
|
|
- UploadZone (multiple, accept .pdf)
|
|
- Drag-to-reorder uploaded files
|
|
- Remove individual files
|
|
- Download merged PDF
|
|
Accept: Upload 3 PDFs → download 1 PDF with all pages in order
|
|
```
|
|
|
|
#### 3.1.2 — PDF Split Worker
|
|
|
|
```
|
|
[3.1.2] Extract specific pages from a PDF
|
|
Files:
|
|
apps/tools/backend/workers/src/pdf/split.rs
|
|
apps/tools/frontend/src/app/pdf/split/page.tsx
|
|
Functions:
|
|
parse_page_range(input: &str, total_pages: u32) -> Result<Vec<u32>>
|
|
split_pdf(input_path: &Path, pages: &[u32]) -> Result<Vec<u8>>
|
|
Page syntax: "1-3,5,7-9,12" → [1,2,3,5,7,8,9,12]
|
|
Frontend:
|
|
- UploadZone (single PDF)
|
|
- PDF preview with page thumbnails
|
|
- Click pages to select/deselect
|
|
- Or type page range input
|
|
- Download extracted pages as single PDF
|
|
Accept: Upload 20-page PDF → extract pages 3-7 → download 5-page PDF
|
|
```
|
|
|
|
#### 3.1.3 — Images to PDF Worker
|
|
|
|
```
|
|
[3.1.3] Convert multiple images into a single PDF
|
|
Files:
|
|
apps/tools/backend/workers/src/pdf/images_to_pdf.rs
|
|
apps/tools/frontend/src/app/pdf/images-to-pdf/page.tsx
|
|
Functions:
|
|
images_to_pdf(image_paths: &[PathBuf], options: PdfOptions) -> Result<Vec<u8>>
|
|
Logic:
|
|
1. Load each image via image::open
|
|
2. Convert to JPEG (for PDF embedding)
|
|
3. Create PDF page per image
|
|
4. Fit image to A4 / Letter / Original size
|
|
5. Optional: margin, alignment
|
|
Frontend:
|
|
- UploadZone (multiple, accept image/*)
|
|
- Thumbnail grid with drag-to-reorder
|
|
- Page size: A4, Letter, Original
|
|
- Orientation: Auto, Portrait, Landscape
|
|
- Margin: 0-50mm
|
|
- Download PDF
|
|
Accept: Upload 5 photos → download 1 PDF, each photo = 1 page
|
|
```
|
|
|
|
#### 3.1.4 — PDF Compress Worker
|
|
|
|
```
|
|
[3.1.4] Reduce PDF file size by recompressing embedded images
|
|
Files:
|
|
apps/tools/backend/workers/src/pdf/compress.rs
|
|
apps/tools/frontend/src/app/pdf/compress/page.tsx
|
|
Algorithm:
|
|
1. Open PDF with lopdf
|
|
2. Find all image XObject streams
|
|
3. For each: decode → re-encode JPEG with lower quality
|
|
4. Replace stream in PDF
|
|
5. Remove unused objects
|
|
6. Linearize PDF for fast web viewing
|
|
Compression levels:
|
|
- Maximum (q=30): smallest size, visible quality loss
|
|
- Balanced (q=50): good balance (default)
|
|
- Minimal (q=80): slight size reduction, near-lossless
|
|
Frontend:
|
|
- UploadZone (single PDF)
|
|
- Compression level selector
|
|
- Preview: original size vs estimated compressed size
|
|
- Download compressed PDF
|
|
Accept: Upload 10MB PDF with images → compress → <3MB output
|
|
```
|
|
|
|
#### 3.1.5 — PDF to Images Worker
|
|
|
|
```
|
|
[3.1.5] Convert each PDF page to an image
|
|
Files:
|
|
apps/tools/backend/workers/src/pdf/to_images.rs
|
|
apps/tools/frontend/src/app/pdf/pdf-to-images/page.tsx
|
|
Functions:
|
|
pdf_page_to_image(input_path: &Path, page_num: u32, dpi: u32) -> Result<Vec<u8>>
|
|
pdf_to_images(input_path: &Path, dpi: u32, format: &str) -> Result<Vec<Vec<u8>>>
|
|
Logic:
|
|
1. Render PDF page to image (via lopdf rasterize or poppler)
|
|
2. Output format: JPEG, PNG, WebP
|
|
3. DPI: 72, 150, 200, 300 (default: 200)
|
|
Frontend:
|
|
- UploadZone (single PDF)
|
|
- DPI selector
|
|
- Format selector
|
|
- Preview: page thumbnails
|
|
- Download as ZIP (all images) or per-page
|
|
Accept: Upload PDF → download ZIP of page images
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 4: Video/Audio Tools
|
|
|
|
### Milestone 4.1 — FFmpeg Worker
|
|
|
|
#### 4.1.1 — FFmpeg Binding & Worker Setup
|
|
|
|
```
|
|
[4.1.1] Integrate FFmpeg via ffmpeg-next crate
|
|
Files:
|
|
apps/tools/backend/workers/Cargo.toml (add ffmpeg-next)
|
|
apps/tools/backend/workers/src/video/mod.rs
|
|
apps/tools/backend/workers/src/audio/mod.rs
|
|
Dockerfile (update — install ffmpeg package)
|
|
Setup:
|
|
- apt-get install ffmpeg
|
|
- ffmpeg-next version 7.x
|
|
- Initialize: ffmpeg::init() at worker startup
|
|
- Test: version check
|
|
Accept: Worker starts → ffmpeg initialized → transcoding functions available
|
|
```
|
|
|
|
#### 4.1.2 — Video Compress
|
|
|
|
```
|
|
[4.1.2] Reduce video file size by lowering bitrate and/or resolution
|
|
Files:
|
|
apps/tools/backend/workers/src/video/compress.rs
|
|
apps/tools/frontend/src/app/video/compress/page.tsx
|
|
Options:
|
|
- Target size (approximate): 10MB, 25MB, 50MB, 100MB, Custom
|
|
- Resolution: 480p, 720p, 1080p, Original
|
|
- Quality: Low, Medium, High (CRF: 28, 23, 18)
|
|
- Codec: H.264, H.265/HEVC, VP9
|
|
FFmpeg command (via ffmpeg-next API):
|
|
- Transcode video stream: libx264, crf, preset medium
|
|
- Scale if needed: scale=iw/2:ih/2
|
|
- Copy audio stream (or compress with aac)
|
|
Frontend:
|
|
- UploadZone (accept video/*, max 500MB)
|
|
- Options: target size, resolution, quality
|
|
- Progress: from FFmpeg stderr parsed progress
|
|
- Preview: thumbnail + duration, original vs estimated size
|
|
- Download compressed video
|
|
Note: Video processing is HEAVY → max 1 concurrent video job, queue via NATS
|
|
Accept: Upload 500MB 1080p video → compress → <100MB 720p output
|
|
```
|
|
|
|
#### 4.1.3 — Audio Extract
|
|
|
|
```
|
|
[4.1.3] Extract audio track from video file
|
|
Files:
|
|
apps/tools/backend/workers/src/video/audio_extract.rs
|
|
apps/tools/frontend/src/app/video/audio-extract/page.tsx
|
|
Options:
|
|
- Format: MP3, AAC, WAV, FLAC, OGG
|
|
- Quality: 128k, 192k, 320k (for lossy) / Lossless (for FLAC/WAV)
|
|
FFmpeg:
|
|
- Stream copy if format compatible, else transcode
|
|
- Extract best audio stream
|
|
Frontend:
|
|
- UploadZone (accept video/*)
|
|
- Format + quality selector
|
|
- Progress
|
|
- Download extracted audio
|
|
Accept: Upload MP4 → download MP3 with correct audio
|
|
```
|
|
|
|
#### 4.1.4 — Video Trim
|
|
|
|
```
|
|
[4.1.4] Cut a segment from a video
|
|
Files:
|
|
apps/tools/backend/workers/src/video/trim.rs
|
|
apps/tools/frontend/src/app/video/trim/page.tsx
|
|
Options:
|
|
- Start time (HH:MM:SS or seconds)
|
|
- End time / Duration
|
|
FFmpeg:
|
|
-ffmpeg -i input -ss start -to end -c copy output (fast seek)
|
|
Or re-encode for precise seeking: -ss start -i input -t duration -c libx264
|
|
Frontend:
|
|
- UploadZone (accept video/*)
|
|
- Preview player with timeline
|
|
- Set start/end via sliders or input
|
|
- Preview trim result
|
|
- Download trimmed video
|
|
Accept: Upload 10min video → trim 2:30-5:00 → download 2.5min video
|
|
```
|
|
|
|
#### 4.1.5 — GIF Maker
|
|
|
|
```
|
|
[4.1.5] Convert video segment to animated GIF
|
|
Files:
|
|
apps/tools/backend/workers/src/video/gif.rs
|
|
apps/tools/frontend/src/app/video/gif-maker/page.tsx
|
|
Options:
|
|
- Start time, Duration (max 10s for GIF)
|
|
- FPS: 10, 15, 20, 24, 30
|
|
- Width: 320, 480, 640, 800
|
|
- Dither: on/off
|
|
- Colors: 64, 128, 256
|
|
FFmpeg:
|
|
ffmpeg -i input -ss start -t duration -vf "fps=15,scale=480:-1:flags=lanczos,palettegen" palette.png
|
|
ffmpeg -i input -i palette.png -ss start -t duration -lavfi "fps=15,scale=480:-1:flags=lanczos[x];[x][1:v]paletteuse" output.gif
|
|
Frontend:
|
|
- UploadZone (accept video/*)
|
|
- Start time + duration sliders
|
|
- FPS + width + dither options
|
|
- Preview: animated GIF in browser
|
|
- Download GIF
|
|
Accept: Upload 30s video → trim 2s → download animated GIF
|
|
```
|
|
|
|
#### 4.1.6 — Audio Convert
|
|
|
|
```
|
|
[4.1.6] Convert audio between formats
|
|
Files:
|
|
apps/tools/backend/workers/src/audio/convert.rs
|
|
apps/tools/frontend/src/app/audio/convert/page.tsx
|
|
Formats:
|
|
Input: MP3, WAV, FLAC, AAC, OGG, M4A, WMA
|
|
Output: MP3, WAV, FLAC, AAC, OGG
|
|
Options:
|
|
- Bitrate: 128k, 192k, 256k, 320k
|
|
- Sample rate: 44100, 48000, 96000
|
|
- Channels: Mono, Stereo (downmix if source is 5.1)
|
|
Frontend:
|
|
- UploadZone (accept audio/*)
|
|
- From (auto-detected) → To selector
|
|
- Bitrate + sample rate options
|
|
- Download converted audio
|
|
Accept: Upload FLAC → convert to 320kbps MP3 → download
|
|
```
|
|
|
|
---
|
|
|
|
## Phase 5: Deployment & Polish
|
|
|
|
### Milestone 5.1 — Infrastructure
|
|
|
|
#### 5.1.1 — Docker Multi-Stage Build
|
|
|
|
```
|
|
[5.1.1] Production Dockerfile
|
|
Files:
|
|
infra/docker/tools.Dockerfile
|
|
Stages:
|
|
1. chef (cargo-chef install)
|
|
2. planner (recipe.json)
|
|
3. builder (cargo build —release)
|
|
4. wasm-builder (wasm-pack build)
|
|
5. frontend-builder (bun build)
|
|
6. runtime: debian:bookworm-slim + tesseract + ffmpeg + ONNX model
|
|
Runtime dependencies:
|
|
- tesseract-ocr + tessdata (eng, ind)
|
|
- ffmpeg
|
|
- ca-certificates
|
|
- libfontconfig1 (for lopdf)
|
|
Dockerignore:
|
|
- node_modules, target, .next, .git
|
|
Build: docker build -f infra/docker/tools.Dockerfile -t tools:latest .
|
|
Accept: docker build succeeds, image size <400MB
|
|
```
|
|
|
|
#### 5.1.2 — Docker Compose File
|
|
|
|
```
|
|
[5.1.2] Compose file for production deployment
|
|
Files:
|
|
infra/compose/tools.yml
|
|
Service definition:
|
|
container_name: tools
|
|
image: ghcr.io/asepharyana/asepharyana-hub/tools:sha-xxxxx
|
|
restart: always
|
|
networks: app-shared-net (alias: tools)
|
|
env_file: ../../.env
|
|
volumes: tools_data:/data/tools
|
|
ports: 3001:3001
|
|
depends_on: [redis, nats]
|
|
labels: prometheus.io/scrape=true, prometheus.io/port=3001
|
|
Volume: tools_data (docker volume)
|
|
Accept: docker compose up → tools container running, connected to redis + nats
|
|
```
|
|
|
|
#### 5.1.3 — Traefik Routing
|
|
|
|
```
|
|
[5.1.3] Add Traefik router and service for tools
|
|
Files:
|
|
infra/traefik/dynamic/apps.yaml (update)
|
|
Router:
|
|
tools:
|
|
rule: Host(`tools.asepharyana.my.id`) || Host(`tools.asepharyana.web.id`)
|
|
entryPoints: websecure
|
|
tls: {}
|
|
middlewares: common-chain@file
|
|
service: tools-service
|
|
Service:
|
|
tools-service:
|
|
loadBalancer:
|
|
servers:
|
|
- url: http://tools:3001
|
|
Accept: tools.asepharyana.my.id → loads tools frontend
|
|
```
|
|
|
|
#### 5.1.4 — CI/CD Workflow Integration
|
|
|
|
```
|
|
[5.1.4] Add tools service to existing build + deploy workflows
|
|
Files:
|
|
.github/workflows/docker-build-push.yml (update)
|
|
.github/workflows/deploy-docker.yml (check — auto-detects compose changes)
|
|
Changes:
|
|
- Detect changed service (apps/tools/**)
|
|
- Build matrix: add tools service
|
|
- Dockerfile: tools.Dockerfile
|
|
- Path: apps/tools
|
|
- Compose file: tools.yml
|
|
- Update manifest: sed image tag in compose
|
|
Accept: Push to main with apps/tools changes → CI builds + deploys tools
|
|
```
|
|
|
|
#### 5.1.5 — Environment Variables Setup
|
|
|
|
```
|
|
[5.1.5] Add tools env vars to .env.example
|
|
Files:
|
|
.env.example (update)
|
|
Vars:
|
|
# Tools
|
|
TOOLS_GATEWAY_PORT=3001
|
|
TOOLS_WORKER_CONCURRENCY=4
|
|
TOOLS_STORAGE_PATH=/data/tools
|
|
TOOLS_JOB_TTL_SECONDS=3600
|
|
TOOLS_RATE_LIMIT_PER_MINUTE=30
|
|
TOOLS_MAX_FILE_SIZE_MB=50
|
|
TOOLS_OCR_LANG=eng+ind
|
|
Accept: .env.example updated with tools section
|
|
```
|
|
|
|
---
|
|
|
|
### Milestone 5.2 — Frontend Polish
|
|
|
|
#### 5.2.1 — Theme Integration
|
|
|
|
```
|
|
[5.2.1] Apply Twilight Terminal theme consistent with portfolio hub
|
|
Files:
|
|
apps/tools/frontend/src/app/globals.css (update)
|
|
Theme vars (from hub):
|
|
--background / --foreground
|
|
--primary / --primary-foreground
|
|
--card / --card-foreground
|
|
--muted / --muted-foreground
|
|
Glass effect: .glass { backdrop-filter: blur }
|
|
Gradient text: .gradient-text
|
|
Terminal cursor blink animation
|
|
Same dark/light mode switch mechanism
|
|
Accept: tools subdomain → visual style consistent with hub portfolio
|
|
```
|
|
|
|
#### 5.2.2 — Responsive Mobile Design
|
|
|
|
```
|
|
[5.2.2] All pages responsive for mobile devices
|
|
Files: All page/component files (review)
|
|
Requirements:
|
|
- UploadZone: full-width on mobile, tap-friendly
|
|
- Tool cards: single column on mobile
|
|
- ProgressBar: always visible, top-fixed on scroll
|
|
- CropEditor: touch-drag handles, pinch-zoom
|
|
- CameraCapture: fullscreen viewfinder
|
|
- FileList: compact thumbnail list on mobile
|
|
- Buttons: min 44px touch target
|
|
- Bottom sheet instead of modal for options
|
|
- Safe area insets for notch devices
|
|
Accept: Lighthouse mobile audit >80 for all pages
|
|
```
|
|
|
|
#### 5.2.3 — Loading States & Skeleton
|
|
|
|
```
|
|
[5.2.3] Skeleton loading states for all pages
|
|
Files:
|
|
apps/tools/frontend/src/components/tools/skeleton.tsx
|
|
apps/tools/frontend/src/app/scan/page.tsx (update)
|
|
apps/tools/frontend/src/app/image/compress/page.tsx (update)
|
|
(all other tool pages)
|
|
Components:
|
|
SkeletonCard (pulse animation)
|
|
SkeletonUploadZone
|
|
SkeletonProgressBar
|
|
SkeletonPreview
|
|
Accept: All pages show skeleton while loading data/WASM
|
|
```
|
|
|
|
#### 5.2.4 — Error Boundaries
|
|
|
|
```
|
|
[5.2.4] React error boundaries per page + global
|
|
Files:
|
|
apps/tools/frontend/src/components/tools/error-boundary.tsx
|
|
apps/tools/frontend/src/app/layout.tsx (wrap with ErrorBoundary)
|
|
Each tool page: wrap with ErrorBoundary
|
|
Behavior:
|
|
- Catch React render errors
|
|
- Show friendly error message with tool name
|
|
- "Try Again" button
|
|
- "Report Issue" link (GitHub)
|
|
- Log error details to console (future: telemetry)
|
|
Accept: Force render error → error boundary shows, app doesn't crash
|
|
```
|
|
|
|
#### 5.2.5 — PWA Manifest
|
|
|
|
```
|
|
[5.2.5] Progressive Web App configuration
|
|
Files:
|
|
apps/tools/frontend/public/manifest.json
|
|
apps/tools/frontend/src/app/layout.tsx (add manifest link + meta tags)
|
|
apps/tools/frontend/public/icons/ (app icons: 192x192, 512x512)
|
|
Manifest:
|
|
name: "Tools — Asep Haryana"
|
|
short_name: "Tools"
|
|
description: "Document Scanner, Image & PDF Tools"
|
|
start_url: /
|
|
display: standalone
|
|
background_color: #0a0a1a (dark theme)
|
|
theme_color: #0a0a1a
|
|
icons: 192x192, 512x512
|
|
Accept: Lighthouse PWA audit >80
|
|
```
|
|
|
|
---
|
|
|
|
### Milestone 5.3 — Monitoring & Observability
|
|
|
|
#### 5.3.1 — Prometheus Alerts
|
|
|
|
```
|
|
[5.3.1] Alert rules for tools service
|
|
Files:
|
|
infra/otel/prometheus.yml (update — or separate alert file)
|
|
Rules:
|
|
- High error rate: rate(tools_jobs_total{status="failed"}[5m]) > 0.1
|
|
- Queue buildup: tools_queue_depth > 50
|
|
- Slow processing: tools_processing_duration_ms{quantile="0.95"} > 10000
|
|
- Low disk space: (disk_free_bytes / disk_total_bytes) < 0.1 (if node_exporter)
|
|
Accept: Rules loaded in Prometheus, alert firing correctly
|
|
```
|
|
|
|
#### 5.3.2 — Dashboard Integration
|
|
|
|
```
|
|
[5.3.2] Add tools metrics to hub dashboard
|
|
Files:
|
|
apps/hub/src/app/dashboard/page.tsx (update)
|
|
apps/hub/src/app/api/dashboard/route.ts (update)
|
|
Add to dashboard:
|
|
- Card: "Tools" service status (running/degraded/down)
|
|
- Quick stats: Total jobs today, Active jobs, Storage used
|
|
- Link to tools.asepharyana.my.id
|
|
Accept: Dashboard shows tools service status and stats
|
|
```
|
|
|
|
#### 5.3.3 — Structured Logging
|
|
|
|
```
|
|
[5.3.3] Structured JSON logging for production
|
|
Files:
|
|
apps/tools/backend/common/src/logging.rs (or in each crate)
|
|
apps/tools/backend/gateway/src/main.rs (logging init)
|
|
apps/tools/backend/workers/src/main.rs (logging init)
|
|
Config:
|
|
- Default: human-readable (development)
|
|
- JSON mode: RUST_LOG_FORMAT=json (production)
|
|
Fields per log:
|
|
- timestamp (ISO 8601)
|
|
- level (INFO, WARN, ERROR)
|
|
- service (gateway / workers / pipeline)
|
|
- request_id (if within request context)
|
|
- job_id (if within job context)
|
|
- message
|
|
- duration_ms (for completed processing)
|
|
Accept: RUST_LOG_FORMAT=json → JSON-structured log output
|
|
```
|
|
|
|
---
|
|
|
|
## Effort Summary
|
|
|
|
| Phase | Milestone | Tasks | Estimated Hours | Total Days |
|
|
|-------|-----------|-------|-----------------|------------|
|
|
| 1 | 1.1 Rust Backend Skeleton | 15 | ~45 | 6 |
|
|
| 1 | 1.2 Scanner Pipeline Core | 9 | ~50 | 7 |
|
|
| 1 | 1.3 Next.js Frontend | 10 | ~30 | 4 |
|
|
| 1 | 1.4 OCR + PDF + Infra | 7 | ~25 | 4 |
|
|
| **Phase 1 Total** | **41** | **~150** | **~21** |
|
|
| 2 | 2.1 Scanner Robustness | 5 | ~25 | 4 |
|
|
| 2 | 2.2 WASM Image Tools | 6 | ~30 | 4 |
|
|
| 2 | 2.3 Background Removal | 3 | ~15 | 2 |
|
|
| **Phase 2 Total** | **14** | **~70** | **~10** |
|
|
| 3 | 3.1 PDF Tools | 5 | ~25 | 4 |
|
|
| **Phase 3 Total** | **5** | **~25** | **~4** |
|
|
| 4 | 4.1 FFmpeg Worker | 6 | ~35 | 5 |
|
|
| **Phase 4 Total** | **6** | **~35** | **~5** |
|
|
| 5 | 5.1 Infrastructure | 5 | ~15 | 2 |
|
|
| 5 | 5.2 Frontend Polish | 5 | ~20 | 3 |
|
|
| 5 | 5.3 Monitoring | 3 | ~10 | 2 |
|
|
| **Phase 5 Total** | **13** | **~45** | **~7** |
|
|
| **Grand Total** | **79 tasks** | **~325 hours** | **~47 days** |
|
|
|
|
> **MVP** (Phase 1 only): 41 tasks, ~21 days
|
|
> **Full release** (Phase 1-5): 79 tasks, ~47 days
|
|
|
|
---
|
|
|
|
## Critical Path (Phase 1)
|
|
|
|
```
|
|
Day 1-2: 1.1.1 → 1.1.2 → 1.1.3 → 1.1.4 → 1.1.5 (workspace + common)
|
|
Day 3-5: 1.1.6 → 1.1.7 → 1.1.8 → 1.1.10 → 1.1.14 (gateway routes + redis)
|
|
Day 5-6: 1.1.9 → 1.1.11 → 1.1.12 → 1.1.13 (gateway + workers connect)
|
|
Day 7-10: 1.2.1 → 1.2.2 → 1.2.3 → 1.2.4 (edge → warp)
|
|
Day 10-12: 1.2.5 → 1.2.6 → 1.2.7 → 1.2.8 (shadow → binarize → enhance)
|
|
Day 12: 1.2.9 (pipeline assembly)
|
|
Day 13-16: 1.3.1 → 1.3.2 → 1.3.3 → 1.3.4 (frontend pages)
|
|
Day 14-17: 1.3.5 → 1.3.6 → 1.3.7 → 1.3.8 (components)
|
|
Day 15-17: 1.3.9 → 1.3.10 (API proxy routes)
|
|
Day 18-19: 1.4.1 → 1.4.2 → 1.4.3 (OCR + PDF)
|
|
Day 19: 1.4.4 (WebSocket)
|
|
Day 20: 1.4.5 → 1.4.6 → 1.4.7 (cleanup + rate limit + errors)
|
|
```
|
|
|
|
> **MVP launch**: Day ~21 — Document Scanner live di tools.asepharyana.my.id |