diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..ade45e9 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,29 @@ +{ + "skills": [ + { + "name": "clean-code", + "filePattern": ".claude/skills/clean-code.md", + "description": "Clean Code, SOLID, dan Clean Architecture principles untuk proyek ini" + }, + { + "name": "hub-rules", + "filePattern": ".claude/skills/hub-rules.md", + "description": "Aturan repository hub, submodule, infra patterns, dan arsitektur" + }, + { + "name": "commit-convention", + "filePattern": ".claude/skills/commit-convention.md", + "description": "Commit message convention — type(scope): description" + }, + { + "name": "event-driven", + "filePattern": ".claude/skills/event-driven.md", + "description": "Event-driven patterns with Dapr + NATS untuk hub services" + }, + { + "name": "deploy-workflow", + "filePattern": ".claude/skills/deploy-workflow.md", + "description": "CI/CD pipeline, Docker patterns, deployment guide" + } + ] +} diff --git a/.claude/skills/clean-code.md b/.claude/skills/clean-code.md new file mode 100644 index 0000000..1310aa6 --- /dev/null +++ b/.claude/skills/clean-code.md @@ -0,0 +1,58 @@ +--- +name: clean-code +description: Clean Code, Clean Architecture, SOLID principles untuk proyek Asepharyana Hub +--- + +# Clean Code — Asepharyana Hub + +## Prinsip Dasar + +### 1. Naming +- **Gunakan nama yang mengungkapkan intensi**: `calculateTotal`, `fetchAnimeData`, `ImageCacheRepository` — bukan `calc`, `getData`, `Repo`. +- **Boolean prefix** dengan `is`, `has`, `should`: `isHealthy`, `hasPoster`, `shouldRetry`. +- **Hindari singkatan** kecuali sangat umum (`config`, `url`, `db`). + +### 2. Fungsi +- **Satu fungsi = satu tanggung jawab**. Max 20 baris. +- **Nama fungsi sebagai kata kerja**: `validateToken()`, `cacheImage()`. +- **Parameter minimal**: max 3 parameter. Lebih dari itu bungkus jadi struct/object. + +### 3. Clean Architecture Layers + +``` +Domain → Entities, Repository traits, Error enums +Application → Use cases +Infrastructure → Implementasi konkret (SeaORM, Redis, HTTP clients) +Presentation → Axum handlers, DTOs, middleware +``` + +Dependency rule: **kode layer dalam tidak tahu tentang layer luar**. Domain gak import framework. + +### 4. Error Handling +- **Gunakan `thiserror`** untuk domain errors, bukan `anyhow` untuk library code. +- **`anyhow` hanya untuk** binary/app entry point dan test. +- **Convert domain errors ke HTTP** di presentation layer, bukan di use case. + +### 5. Testing +- **TDD mindset**: tulis test sebelum implementasi untuk logic bisnis. +- **Unit test untuk use case + domain** (tanpa infra). +- **Integration test untuk repository** (dengan test container). +- **Mock trait**, bukan struct konkret. + +## Untuk Proyek Ini + +### Scraper Service (Rust) +- `src/domain/` — tipe data murni, trait, error — **tanpa framework** +- `src/application/` — use cases, orchestrasi, **tanpa HTTP** +- `src/infrastructure/` — implementasi repository, cache, HTTP client +- `src/presentation/` — handler Axum, routing, middleware + +### Event-Driven (Dapr + NATS) +- Event schema pake CloudEvents format +- Topic naming: `hub..` (e.g. `hub.image.cached`) +- Handler hanya untuk satu tipe event, pisah file per domain + +### Infra Config (YAML) +- Satu compose file per service +- Networking via `app-shared-net` +- Image tag selalu `sha-`, bukan `latest` di production diff --git a/.claude/skills/commit-convention.md b/.claude/skills/commit-convention.md new file mode 100644 index 0000000..e3c6251 --- /dev/null +++ b/.claude/skills/commit-convention.md @@ -0,0 +1,65 @@ +--- +name: commit-convention +description: Enforce commit message convention untuk Asepharyana Hub +--- + +# Commit Convention — Asepharyana Hub + +## Format + +``` +(): + +[optional body] + +[optional footer] +``` + +## Types + +| Type | Usage | +| ---------- | ------------------------------------ | +| `feat` | Fitur baru | +| `fix` | Bug fix | +| `chore` | Maintenance, config, tooling | +| `docs` | Dokumentasi | +| `refactor` | Perubahan kode tanpa fungsional baru | +| `test` | Nambah/update test | +| `ci` | CI/CD workflows | +| `perf` | Optimasi performa | +| `style` | Formatting (tanda kutip, dll) | + +## Scopes + +| Scope | Area | +| ------------- | --------------------------------- | +| `scraper` | apps/scraper submodule | +| `infra` | infra/ (compose, traefik, docker) | +| `ci` | .github/workflows/ | +| `dapr` | Dapr config & sidecar | +| `nats` | NATS message bus | +| `docs` | Dokumentasi | +| `deps` | Dependencies | +| `scripts` | Utility scripts | +| `root` | Root config files | + +## Contoh + +``` +feat(scraper): add anime detail caching via Dapr pubsub +fix(infra): correct NATS CLI flags for JetStream +chore(deps): update biome to v2.5.3 +docs(infra): add deployment order for Dapr services +ci(deploy): add nats.yml to ALL_COMPOSE_FILES +refactor(scraper): migrate EventBus from tokio broadcast to Dapr pubsub +``` + +## Aturan + +1. **Wajib** menyertakan scope dalam tanda kurung +2. **Wajib** `Co-Authored-By` untuk commit yang digenerate AI +3. **Gunakan imperative mood**: "add" bukan "added" / "adds" +4. **Jangan capitalize** type: `feat:` bukan `Feat:` +5. **No period** di akhir subject baris +6. Body explain **why** dan **what**, bukan **how** +7. Refer issue dengan `Closes #123` atau `Fixes #123` di footer diff --git a/.claude/skills/deploy-workflow.md b/.claude/skills/deploy-workflow.md new file mode 100644 index 0000000..87f972b --- /dev/null +++ b/.claude/skills/deploy-workflow.md @@ -0,0 +1,103 @@ +--- +name: deploy-workflow +description: Panduan deploy, CI/CD, dan Docker compose patterns untuk Asepharyana Hub +--- + +# Deploy & Workflow — Asepharyana Hub + +## CI/CD Pipeline + +### Build Pipeline (`docker-build-push.yml`) +Trigger: push ke `main` yang touch `apps/**`, `infra/**`, `infra/docker/**` + +1. **changes** — detect service mana yg berubah via git diff +2. **wait-submodule-ref** — (repository_dispatch only) tunggu SHA commit fetchable +3. **build** — matrix build per service, push ke GHCR (`sha-` + `latest`) +4. **update-manifest** — update image tag di compose file, commit + push + +### Deploy Pipeline (`deploy-docker.yml`) +Trigger: build selesai, atau push ke `main` touch `infra/**` + +1. SSH ke `orangevps` (via `secrets.VPS_HOST`) +2. Sync repo (`git fetch --depth=1 + reset`) +3. Login ke GHCR +4. Deteksi compose file yg berubah +5. Pull images + restart container selektif + +### Secrets Required +| Secret | Untuk | +|--------|-------| +| `SSH_PRIVATE_KEY` | SSH ke VPS | +| `VPS_HOST` | IP/host VPS (tailscale IP) | +| `VPS_USER` | SSH user, biasanya `root` | +| `VPS_TARGET_DIR` | Lokasi repo di VPS | +| `ENV_FILE_PRODUCTION` | .env content untuk production | + +### Selective Deployment +- Hanya compose file yg berubah yang di-redeploy +- Selective: `UP_FLAGS="-d"` (tanpa `--remove-orphans`) +- Full deploy: `UP_FLAGS="-d --remove-orphans"` + +## Docker Patterns + +### Build dengan cargo-chef (Rust) +```dockerfile +FROM lukemathwalker/cargo-chef:latest-rust-1.89.0 AS chef +WORKDIR /app +FROM chef AS planner +COPY apps/scraper . +RUN cargo chef prepare --recipe-path recipe.json +FROM chef AS builder +COPY --from=planner /app/recipe.json recipe.json +RUN cargo chef cook --release --recipe-path recipe.json +COPY apps/scraper . +RUN cargo build --release +``` + +### Runtime minimal untuk Rust binary +```dockerfile +FROM debian:bookworm-slim AS runtime +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates curl libssl3 && rm -rf /var/lib/apt/lists/* +``` + +## Image Tagging +- `sha-` — immutable, untuk rollback +- `latest` — mutable, untuk convenience +- Build cache: `sha--buildcache` +- Registry: `ghcr.io/asepharyana/asepharyana-hub/` + +## Manual Deploy Steps +```bash +# 1. Login GHCR +echo $GITHUB_TOKEN | docker login ghcr.io -u asepharyana --password-stdin + +# 2. Full stack +docker compose -f infra/compose/traefik.yml \ + -f infra/compose/shared.yml \ + -f infra/compose/nats.yml \ + -f infra/compose/dapr.yml \ + -f infra/compose/scraper.yml \ + --env-file .env up -d --remove-orphans + +# 3. Selective (hanya satu service) +docker compose -f infra/compose/scraper.yml --env-file .env up -d +``` + +## Troubleshooting + +### Container reach Tailscale +Pastikan route ke Tailscale di main table: +```bash +ip route add 100.64.0.0/10 dev tailscale0 table main +systemctl restart tailscale-routes +``` + +### Healthcheck gagal di scratch images +NATS dan Dapr placement pake scratch — tidak bisa healthcheck. Cukup `service_started` di depends_on. + +### Dapr sidecar crash +```bash +docker logs scraper-api-dapr | grep -iE "fatal|error" +``` +Penyebab umum: komponen config salah, NATS/Dapr placement belum siap. diff --git a/.claude/skills/event-driven.md b/.claude/skills/event-driven.md new file mode 100644 index 0000000..e170a60 --- /dev/null +++ b/.claude/skills/event-driven.md @@ -0,0 +1,133 @@ +--- +name: event-driven +description: Event-driven patterns dengan Dapr + NATS untuk Asepharyana Hub +--- + +# Event-Driven Architecture — Asepharyana Hub + +## Stack +- **Message Backbone**: NATS + JetStream (untuk streaming & job queue) +- **Pub/Sub Runtime**: Dapr sidecar per service (pubsub via Redis built-in) +- **State Store**: Dapr → Redis + +## Event Topics Convention + +``` +hub.. + +Contoh: +hub.image.cached → Image selesai di-cache ke CDN +hub.image.repaired → Image diperbaiki (CNAME change) +hub.scrape.anime.done → Scrape anime selesai +hub.system.alert → Error/alert dari service +``` + +## CloudEvents Format + +```json +{ + "specversion": "1.0", + "type": "hub.image.cached", + "source": "scraper-api", + "subject": "anime-poster", + "id": "uuid-v4", + "time": "2026-07-21T10:00:00Z", + "datacontenttype": "application/json", + "data": { ... } +} +``` + +## Publish Event (Rust via HTTP API) + +Gunakan `reqwest` langsung ke Dapr sidecar (SDK Rust masih experimental): + +```rust +let event = serde_json::json!({ + "specversion": "1.0", + "type": "hub.image.cached", + "source": "scraper-api", + "id": Uuid::new_v4().to_string(), + "time": chrono::Utc::now().to_rfc3339(), + "datacontenttype": "application/json", + "data": { "original_url": url, "cdn_url": cdn_url } +}); + +reqwest::Client::new() + .post("http://localhost:3500/v1.0/publish/pubsub/hub.image.cached") + .json(&event) + .send() + .await?; +``` + +## Service Invocation + +```bash +curl http://localhost:3500/v1.0/invoke//method/ +``` + +## State Store + +```bash +# Set +curl -X POST http://localhost:3500/v1.0/state/statestore \ + -H "Content-Type: application/json" \ + -d '[{"key": "mykey", "value": "myvalue"}]' + +# Get +curl http://localhost:3500/v1.0/state/statestore/mykey + +# Delete +curl -X DELETE http://localhost:3500/v1.0/state/statestore/mykey +``` + +## Scraper Event Integration + +File yang perlu dimodifikasi untuk event-driven: + +| File | Perubahan | +|------|-----------| +| `src/events/bus.rs` | Ganti backend dari tokio broadcast ke Dapr pub/sub | +| `src/bootstrap/mod.rs` | Init DaprClient, inject ke AppState | +| `src/presentation/state.rs` | Tambah `dapr_client` field | +| `src/proxy/use_cases.rs` | Publish `ImageRepaired` & `ImageCached` events | +| `src/infrastructure/services/images/cache.rs` | Emit event tiap cache selesai | +| `Cargo.toml` | Tambah `reqwest`, `uuid`, `chrono` (jika belum ada) | + +## Event Handlers (Subscribe) + +Buat `src/subscribers/` untuk handler: + +```rust +// src/subscribers/image_handler.rs +pub async fn handle_image_cached(event: CloudEvent) -> Result<()> { + // Log, notifikasi, update status +} +``` + +Daftarkan subscribers di `bootstrap/mod.rs` dengan spawn task: +```rust +tokio::spawn(async move { + let mut stream = dapr_client.subscribe("pubsub", "hub.image.cached"); + while let Some(event) = stream.next().await { + handle_image_cached(event).await; + } +}); +``` + +## Testing Event-Driven Code + +```rust +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_publish_event() { + let client = MockDaprClient::new(); + client.expect_publish() + .with(...) + .returning(|_| Ok(())); + // ... test + } +} +``` diff --git a/.claude/skills/hub-rules.md b/.claude/skills/hub-rules.md new file mode 100644 index 0000000..5abb551 --- /dev/null +++ b/.claude/skills/hub-rules.md @@ -0,0 +1,96 @@ +--- +name: hub-rules +description: Aturan repository, arsitektur hub, submodule, dan workflow Asepharyana Hub +--- + +# Asepharyana Hub — Repository Rules + +## Struktur Repository + +``` +asepharyana-hub/ +├── apps/ # Git submodules — source code aplikasi +├── docs/ # Dokumentasi, ADR, deployment guide +├── infra/ # Infrastructure as code +│ ├── compose/ # Satu compose file per service +│ ├── dapr/ # Dapr component configs +│ ├── docker/ # Dockerfiles per service +│ └── traefik/ # Static & dynamic Traefik config +├── scripts/ # Utility scripts (cleanup, update-deps) +└── .github/workflows/ # CI/CD pipelines +``` + +### Aturan Submodule +- Setiap aplikasi di `apps/` adalah **submodule** ke repo terpisah. +- Perubahan kode aplikasi dilakukan di **repo masing-masing**, bukan di sini. +- Submodule pointer diupdate oleh CI/CD (bukan manual). + +## Infrastructure Patterns + +### Networking +- Semua service join **`app-shared-net`** (external Docker bridge) +- Service discovery via Docker DNS (container alias) +- Traefik sebagai ingress untuk HTTP/S eksternal +- Tailscale untuk cross-VPS (PostgreSQL, Redis, browserless di `imrnes`) + +### Compose File Pattern +```yaml +services: + : + container_name: + image: ghcr.io/asepharyana/asepharyana-hub/:sha- + restart: always + networks: + app-shared-net: + aliases: + - + env_file: + - ../../.env + +networks: + app-shared-net: + name: app-shared-net + external: true +``` + +### Dapr Sidecar Pattern +```yaml + -dapr: + container_name: -dapr + image: daprio/daprd:latest + restart: always + depends_on: + nats: + condition: service_started + dapr-placement: + condition: service_started + networks: + - app-shared-net + command: + - './daprd' + - '--app-id=' + - '--app-port=' + - '--dapr-http-port=3500' + - '--dapr-grpc-port=50001' + - '--placement-host-address=dapr-placement:50005' + - '--resources-path=/components' + volumes: + - ../../infra/dapr/components:/components +``` + +### Traefik Routing +- Router + service definition di `infra/traefik/dynamic/apps.yaml` +- Subdomain pattern: `.asepharyana.my.id` + `.asepharyana.web.id` +- TLS cert dari volume mount (bukan auto-acme) + +### CI/CD +- `docker-build-push.yml` — build per service, push ke GHCR, update compose manifest +- `deploy-docker.yml` — SSH ke orangevps, pull images, restart +- Selective deploy: hanya compose file yg berubah + +## Deployment Order +1. `shared.yml` (Redis) +2. `nats.yml` (NATS message bus) +3. `dapr.yml` (Dapr placement) +4. `traefik.yml` (Reverse proxy) +5. Service compose files (apps + Dapr sidecar) diff --git a/.gitignore b/.gitignore index 9377110..7ed0623 100644 --- a/.gitignore +++ b/.gitignore @@ -38,12 +38,13 @@ testem.log # System Files .DS_Store Thumbs.db -.claude +.claude/* +!.claude/skills/ +!.claude/settings.json # Next.js .next out **/.codegraph/** -**/.claude/** test-output **/**.env **/**.env.**