Author SHA1 Message Date
monrepo-bot 324fe08913 chore: update llm-api to 81c51772495c 2026-08-02 09:18:56 +00:00
Asep Haryana e171201b1c chore: sync port references and docs to 4000s infra 2026-08-02 16:16:40 +07:00
Asep Haryana ec1394bdc5 chore: bump tools submodule (ports 4007/4008) 2026-08-02 14:31:47 +07:00
asepharyana f0787b5f23 fix(nix): restrict flake to x86_64-linux (nixpkgs 26.11 dropped darwin) 2026-08-01 18:03:44 +07:00
asepharyana 0df6c35d5a ci: publish flake to FlakeHub (rolling) 2026-08-01 17:58:22 +07:00
Asep Haryana 5d21848e87 perf(infra): traefik response speed - HTTP/3 + backend conn pooling
- entryPoints.websecure.http3: enable QUIC (UDP 443, alt-svc h3)
- serversTransport maxIdleConnsPerHost 2->100: reuse backend keep-alive
- forwardingTimeouts.dialTimeout 30s->3s: fail fast on dead backends
- disable version-check/anonymous-usage network chatter
2026-08-01 13:12:50 +07:00
Asep Haryana b96548bffa fix(infra): raise upload-buffer response cap to 2GB
Chunked file streaming (TeleUploader) serves multi-part bodies up to
120MB+; 10MB maxResponseBodyBytes made every big download 500.
2026-08-01 12:52:34 +07:00
Asep Haryana e19fa76dc3 fix(infra): add teleuploader traefik router for upload hostnames
Restore upload.asepharyana.my.id/.web.id routing lost during Nix migration
(docker label router removed with container). Upload chain uses 2GB body
buffer + dedicated 300/100 rate limit, service targets bun on host:3000.
2026-08-01 12:34:22 +07:00
Asep Haryana c535ee7858 ci(otel): add logs pipeline to otel-collector (OTLP /v1/logs 404 fix) 2026-07-31 14:13:56 +07:00
Asep Haryana 80b5161630 ci(infra): traefik dynamic config path -> /home/code (drop /root dependency) 2026-07-31 14:02:07 +07:00
Asep Haryana 1ccc0355ac ci(traefik): add hermes dashboard route via file provider 2026-07-31 13:50:46 +07:00
asepharyana 12aed642a9 ci(traefik): add zeavis routes via file provider (Nix migration) 2026-07-31 12:42:14 +07:00
asepharyana f8e3dccb44 ci(traefik): lidm routes -> host ports 3100/3101 (3000/3001 occupied by teleuploader+gmw) 2026-07-31 11:37:13 +07:00
asepharyana 1811c6adc2 ci(traefik): add lidm frontend+backend routes via file provider (Nix migration) 2026-07-31 11:29:47 +07:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> ceeac5c02f chore(deps): bump the github-actions group across 1 directory with 3 updates (#9)
Bumps the github-actions group with 3 updates in the / directory: [actions/checkout](https://github.com/actions/checkout), [DeterminateSystems/nix-installer-action](https://github.com/determinatesystems/nix-installer-action) and [DeterminateSystems/magic-nix-cache-action](https://github.com/determinatesystems/magic-nix-cache-action).


Updates `actions/checkout` from 4 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

Updates `DeterminateSystems/nix-installer-action` from 16 to 22
- [Release notes](https://github.com/determinatesystems/nix-installer-action/releases)
- [Commits](https://github.com/determinatesystems/nix-installer-action/compare/v16...v22)

Updates `DeterminateSystems/magic-nix-cache-action` from 8 to 14
- [Release notes](https://github.com/determinatesystems/magic-nix-cache-action/releases)
- [Commits](https://github.com/determinatesystems/magic-nix-cache-action/compare/v8...v14)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: DeterminateSystems/magic-nix-cache-action
  dependency-version: '14'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
- dependency-name: DeterminateSystems/nix-installer-action
  dependency-version: '22'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-30 22:34:36 +07:00
Asep Haryana d0c9f16d5d chore(infra): disable Docker compose for Nix-migrated services (hub, scraper, tools, llm-api) 2026-07-30 22:29:20 +07:00
Asep Haryana 6cbafbfe2e fix(flake): update scraper pinned rev for config fix 2026-07-30 22:28:24 +07:00
GitHub Action d6b9a0806b chore: update manifests and submodules [skip ci] 2026-07-30 15:23:34 +00:00
Asep Haryana 60a1af436f fix(infra): llm-api port 8080→8082 (conflict with gmw-proxy nginx) 2026-07-30 22:18:56 +07:00
Asep Haryana 2459541677 fix(scraper): update submodule - config list_separator fix 2026-07-30 22:18:03 +07:00
Asep Haryana 33b999d2b7 fix(ci): disable Determinate Nix / FlakeHub (no flakehub flakes used)
DeterminateSystems/nix-installer-action defaults to determinate:true
which tries to auth with FlakeHub via GitHub JWT. We don't use any
FlakeHub flakes, so disable it — skipping the auth entirely.
2026-07-30 22:05:41 +07:00
Asep Haryana 59f131f951 fix(ci): combine build+deploy per-service, pass exact store path
Separate deploy job failed because it used ls to find store paths
by name, finding OLD local paths instead of the freshly copied CI
paths. Now each service builds, copies, and updates its profile
in a single job using the exact store path from the build output.
No more guessing which path is the right one.
2026-07-30 21:42:39 +07:00
Asep Haryana cc8c5088a4 fix(ci): use SSH_PRIVATE_KEY secret, sanitize key format
- Ganti secret name: VPS_SSH_KEY → SSH_PRIVATE_KEY (nama yg ada)
- Fix nix copy URL: ***@ → $VPS_USER@
- Sanitize SSH key: strip \r\n, validasi dengan ssh-keygen
- Cegah libcrypto error dari key format broken
2026-07-30 21:32:40 +07:00
Asep Haryana d15e8621a9 fix(ci): proper SSH user in nix copy, deploy job structure
- Fix nix copy URL: ***@ → $VPS_USER@
- Store path from build output, passed across jobs
- Deploy job waits for all builds via needs: build
- SSH key setup in its own step, guarded by main branch
- Only deploy on main branch pushes
- Remote deploy script fetches from VPS nix store
2026-07-30 21:14:20 +07:00
Asep Haryana 33d5b12ec6 fix: tools-workers install path (no cd backend in installPhase) 2026-07-30 20:11:31 +07:00
Asep Haryana b5596e1398 feat(infra): full Nix migration — all 6 services + CI/CD
- flake.nix: 6 derivations (hub, scraper, tools-gateway, tools-workers, tools-frontend, llm-api)
- Fetch submodule source via builtins.fetchGit with pinned revs
- Fix cargo HOME/TMPDIR for Nix sandbox permission issues
- Fix llm-api: CMake/Clang deps for llama.cpp-sys2 bindgen
- Add LIBCLANG_PATH, LD_LIBRARY_PATH for Rust bindgen builds
- Systemd units: tools-gateway (3501), tools-frontend (3500), tools-workers, llm-api (8080)
- tools.target for grouped management
- Env configs: /etc/tools/env, /etc/llm-api/env
- GitHub Actions: nix-build.yml — matrix build + nix copy + deploy
- Update Traefik apps.yaml: tools/host.docker.internal:3500, llm-api/host.docker.internal:8080
- iptables: allow Docker→host on 3099, 4091, 3500, 3501, 8080
- Add scripts/nix-deploy.sh for CI/CD deploy step
2026-07-30 19:45:43 +07:00
Asep Haryana 46730ec07d feat(infra): Nix build for scraper, GitHub Actions workflow
- Build scraper (Rust) with Nix — cargo build --release
- Create scraper systemd unit (port 4091), env from Docker config
- Fix HOME/CARGO_HOME for Rust/cargo in Nix sandbox
- Update Traefik apps.yaml: scraper -> host.docker.internal:4091
- Add iptables rules for port 4091 (Docker->host)
- Add GitHub Actions workflow: nix-build.yml (determinate-nix + deploy)
- Save iptables rules persistently
2026-07-30 18:46:45 +07:00
Asep Haryana 1ae8a53cd4 chore: ignore Nix build result symlink 2026-07-30 18:33:52 +07:00
Asep Haryana ff52f8841c feat(infra): Nix build for hub app, systemd deployment
- Add flake.nix with derivations for hub (Next.js), scraper, tools, llm-api
- Create hub systemd unit (port 3099)
- Update Traefik dynamic config to point to host hub service
- Add iptables rule for Docker-to-host communication
- Use fetchGit for submodule source resolution
2026-07-30 18:33:43 +07:00
monrepo-bot b747ae5c3b chore: update hub to b007ced615dc 2026-07-29 00:12:03 +00:00
monrepo-bot 64ce812b4e chore: update hub to 5370fd47954d 2026-07-29 00:10:06 +00:00
monrepo-bot 9837d9dc50 chore: update hub to 407aa7b8993b 2026-07-28 00:11:32 +00:00
monrepo-bot 86dda74174 chore: update hub to a803849cba6c 2026-07-27 00:21:20 +00:00
monrepo-bot d045602f3e chore: update hub to 6042a6056a99 2026-07-27 00:17:20 +00:00
GitHub Action 32049c70ac chore: update manifests and submodules [skip ci] 2026-07-26 12:18:15 +00:00
asepharyana be7348a27d fix(infra): remove buffer/compress from llm-api for real SSE streaming
- llm-api uses llm-chain instead of common-chain
- llm-chain excludes buffer & compress middlewares
- Prevents Traefik from buffering streaming SSE responses
- Each token is now flushed immediately
2026-07-26 19:14:52 +07:00
asepharyana 43f0df4493 fix(infra): remove buffer/compress from llm-api middleware chain
- Buffer middleware causes SSE stream buffering
- Compress middleware breaks real-time streaming
- Added llm-chain with only secure-headers + retry + rate-limit
- llm-api router now uses llm-chain@file instead of common-chain@file
2026-07-26 19:13:08 +07:00
GitHub Action f0c7534152 chore: update manifests and submodules [skip ci] 2026-07-26 12:09:58 +00:00
asepharyana a4687c9d5b fix(llm-api): update submodule - reasoning/content stream separation 2026-07-26 19:04:36 +07:00
GitHub Action e42e967b7f chore: update manifests and submodules [skip ci] 2026-07-26 11:27:08 +00:00
asepharyana d99bc06df3 fix(llm-api): update submodule to 6ff31b5 — Jinja template via minijinja
- Replaced llama-cpp-2 apply_chat_template with minijinja rendering
- reasoning_content separate from content in API response
- Works with MiniCPM5 thinking model natively
2026-07-26 18:21:43 +07:00
GitHub Action 87df0d7ae2 chore: update manifests and submodules [skip ci] 2026-07-26 10:15:32 +00:00
monrepo-bot 518afa5645 chore: update llm-api to 9a63ff16018f 2026-07-26 10:11:01 +00:00
GitHub Action 2913f515f0 chore: update manifests and submodules [skip ci] 2026-07-26 09:14:25 +00:00
monrepo-bot 05e20df680 chore: update llm-api to 59c77108a55b 2026-07-26 09:09:23 +00:00
GitHub Action 59e2f9d173 chore: update manifests and submodules [skip ci] 2026-07-26 09:08:26 +00:00
monrepo-bot f78b826e59 chore: update llm-api to 495b9ed126e6 2026-07-26 09:03:36 +00:00
GitHub Action d75424634d chore: update manifests and submodules [skip ci] 2026-07-26 08:58:06 +00:00
GitHub Action cc8cefb33a chore: update manifests and submodules [skip ci] 2026-07-26 08:53:27 +00:00
monrepo-bot 757b48b446 chore: update llm-api to 254532458b16 2026-07-26 08:49:23 +00:00
monrepo-bot 20c81b4018 chore: update llm-api to 7c8f747faf43 2026-07-26 08:49:00 +00:00
GitHub Action ca81a8b7ee chore: update manifests and submodules [skip ci] 2026-07-26 08:44:28 +00:00
GitHub Action ac25a51113 chore: update manifests and submodules [skip ci] 2026-07-26 08:42:08 +00:00
asepharyana 10aeda4d4b fix: update llm-api MODEL_PATH for new MiniCPM5 GGUF
Updates MODEL_PATH to MiniCPM5-1B-Claude-Opus-Fable5-V2-Thinking-Q8_0.gguf
2026-07-26 15:40:14 +07:00
monrepo-bot 5a0737dc74 chore: update llm-api to d8425ea3b316 2026-07-26 08:38:25 +00:00
GitHub Action 1e4b17afb0 chore: update manifests and submodules [skip ci] 2026-07-25 12:06:23 +00:00
asepharyana 22e8a872cd fix(infra): replace short -S flags with long --system/--ingroup in hub Dockerfile
The oven/bun:1.2 base image was updated and -S flag on adduser is
now ambiguous between --shell and --system, causing build failure.
2026-07-25 19:02:00 +07:00
asepharyana 18cabefe9c fix(ci): remove duplicate submodule sync code in docker-build-push
The Sync submodule locally step had an identical duplicate block: after
cd-ing into SUBNODULE_PATH in the first block, the second block tried
cd SUBNODULE_PATH again, causing 'No such file or directory' since
the working directory was already inside the submodule path.

Also added cd $GITHUB_WORKSPACE at the end of the first block to
ensure the next step starts from the workspace root.
2026-07-25 18:58:59 +07:00
monrepo-bot f66f25f120 chore: update tools to 3956b90c3ce3 2026-07-25 11:13:58 +00:00
monrepo-bot 865fbf9547 chore: update llm-api to 14032db705fa 2026-07-25 11:13:53 +00:00
monrepo-bot 1d50980eed chore: update scraper-api to bc782ae4f825 2026-07-25 11:13:44 +00:00
monrepo-bot ef29e0e4ef chore: update hub to 51798f483f59 2026-07-25 11:13:39 +00:00
asepharyana 16c0231b9f fix(hub): update subproject commits for hub and hub-guide 2026-07-25 17:54:01 +07:00
asepharyanaandClaude Code 3be2744070 fix(hub): switch Dockerfile from npm to bun
hub project uses bun.lock, but Dockerfile was using npm install which
resolves dependencies differently. Switched to oven/bun image and
bun install --frozen-lockfile for deterministic builds.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-07-25 17:43:11 +07:00
asepharyana 3dfde5e872 fix(ci): add tools to allowed services in update-submodule workflow 2026-07-25 16:39:38 +07:00
asepharyana 632868356d fix(tools): update submodule with corrected service name 2026-07-25 15:28:51 +07:00
asepharyanaandClaude Code 2e3d5c3de6 chore: add notify-parent workflows to hub and tools submodules
Add .github/workflows/notify-parent.yml to both submodules for
repository_dispatch to parent hub repo on push to main.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-07-25 15:28:11 +07:00
asepharyanaandClaude Code 6fcdf967b6 chore(llm-api): add notify-parent workflow
Add .github/workflows/notify-parent.yml for repository_dispatch.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-07-25 15:13:58 +07:00
asepharyanaandClaude Code ac01422d21 chore(llm-api): update submodule to refactor/clean-architecture
Update submodule pointer to latest refactored architecture.

- Clean architecture (domain/application/infrastructure/presentation)
- Web chat UI at GET /
- Consolidated sampler builder

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-07-25 15:07:43 +07:00
asepharyana 95a9921b7a fix(deploy): use 'docker compose config --services' for reliable service name extraction
Replace fragile grep-based extraction that matched volume names
(nats_data, tools_data) and 'null' as pseudo-services.
2026-07-25 14:21:48 +07:00
GitHub Action fde53f22b6 chore: update manifests and submodules [skip ci] 2026-07-25 06:45:09 +00:00
asepharyana e953c3cb67 ci(llm-api): integrate into hub CI/CD pipeline
- Add llm-api to docker-build-push workflow (build, push, deploy)
- Add llm-api compose file to deploy-docker ALL_COMPOSE_FILES
- Add llm-api to update-submodule workflow
- Add llm-api CodeQL build step in security workflow
- Fix typo in Traefik host rule (asepharya -> asepharyana)
- Update CLAUDE.md submodule list
- Fix .gitmodules URL for llm-api submodule
- Clean up standalone infra files from llm-api submodule repo
2026-07-25 13:39:35 +07:00
asepharyana cbad784bca chore: update submodule commits for hub and hub-guide 2026-07-25 12:06:49 +07:00
asepharyana 189311015c chore(hub-guide): rename repo to github.com/asepharyana/asepharyana-hub-guide 2026-07-25 12:06:16 +07:00
asepharyana ab3c95dae2 chore(hub-guide): rename repo to github.com/asepharyana/hub-guide 2026-07-25 12:04:52 +07:00
asepharyana 05d6b4a055 chore: update llm-api submodule to e1f5195 (SSE streaming) 2026-07-25 11:34:30 +07:00
GitHub Action 2033d9b2bc chore: update manifests and submodules [skip ci] 2026-07-25 04:22:20 +00:00
asepharyana 8fc4a15e0c feat(llm-api): add MiniCPM-V LLM API service with Traefik routing 2026-07-25 11:20:15 +07:00
asepharyana 88f6968654 feat(infra): move hub-guide submodule to plugins/hub-guide 2026-07-25 10:56:24 +07:00
asepharyana 08883ba1a6 feat(infra): add hub-guide Claude Code plugin submodule 2026-07-25 10:54:46 +07:00
GitHub Action 4fbca91c8c chore: update manifests and submodules [skip ci] 2026-07-24 12:59:04 +00:00
asepharyanaandKilo 1cdd849d89 feat(infra): update tools submodule with PDF split/compress frontend
Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 19:54:37 +07:00
asepharyanaandKilo de161d95a8 feat(infra): update tools submodule with image+PDF tool implementations
Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 19:52:50 +07:00
GitHub Action c4791b3160 chore: update manifests and submodules [skip ci] 2026-07-24 12:08:26 +00:00
asepharyanaandKilo e9f3cd458c feat(infra): update tools submodule with interactive resize/convert options
Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 19:07:17 +07:00
asepharyanaandKilo 14c9668e85 fix(infra): update tools submodule with panic-safe contour detection
Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 19:01:40 +07:00
GitHub Action 6f1dfea710 chore: update manifests and submodules [skip ci] 2026-07-24 11:54:28 +00:00
asepharyanaandKilo 3c52036f7e fix(infra): replace curl healthcheck with wget, install wget in runtime
Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 18:52:40 +07:00
GitHub Action e10a2e498b chore: update manifests and submodules [skip ci] 2026-07-24 11:50:52 +00:00
GitHub Action 183659b56c chore: update manifests and submodules [skip ci] 2026-07-24 11:50:03 +00:00
asepharyanaandKilo 38873dd73e fix(infra): install leptonica/tesseract dev libs for tesseract feature build
Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 18:44:22 +07:00
asepharyanaandKilo be2117aa16 fix(infra): audit — NATS subject, worker spawn, tesseract feature, pipeline PDF
Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 18:24:35 +07:00
GitHub Action f92aa8ef30 chore: update manifests and submodules [skip ci] 2026-07-24 10:58:20 +00:00
GitHub Action 4be55b57bb chore: update manifests and submodules [skip ci] 2026-07-24 10:57:26 +00:00
asepharyanaandKilo 5c79a91a2e fix(infra): copy bun binary to runtime for Next.js frontend
Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 17:55:59 +07:00
GitHub Action 13e2c38eef chore: update manifests and submodules [skip ci] 2026-07-24 10:53:19 +00:00
asepharyanaandKilo 8f62f955d1 fix(infra): route tools Traefik to Next.js frontend (port 3000)
Frontend serves UI, proxies /api/* to Rust gateway on port 3001.
Rust gateway remains internal, not exposed via Traefik.

Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 17:52:23 +07:00
GitHub Action d2f592261e chore: update manifests and submodules [skip ci] 2026-07-24 10:48:34 +00:00
GitHub Action 70f54c8c7a chore: update manifests and submodules [skip ci] 2026-07-24 10:47:39 +00:00
asepharyanaandKilo a35e19b546 fix(infra): update tools submodule entrypoint, use port 3002
Entrypoint now auto-discovers binary locations in various paths.
Host port changed to 3002 due to stale iptables rule on port 3001.

Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 17:47:24 +07:00
asepharyanaandKilo c25850b727 fix(infra): use unique binary names in Dockerfile builder stage
The cp destination /app/gateway conflicts with the source code
directory apps/tools/backend/gateway/ which is copied to /app/gateway/.
Use /app/tools-gateway-bin to avoid name collision.

Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 17:42:50 +07:00
asepharyanaandKilo 97b48ddb21 fix(infra): remove depends_on redis/nats from tools.yml
Shared services are in separate compose files. Tools depends on them
at runtime but Docker compose doesn't need to manage their lifecycle.

Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 17:40:35 +07:00
GitHub Action ab994ffd54 chore: update manifests and submodules [skip ci] 2026-07-24 07:17:21 +00:00
GitHub Action 9d0b075b92 chore: update manifests and submodules [skip ci] 2026-07-24 07:16:37 +00:00
asepharyanaandKilo c5a7ae247a fix(infra): copy binaries from builder's filesystem, not cache mount
The builder stage uses --mount=type=cache for /app/target, so binaries
are in the cache which isn't available to the runtime stage COPY.
Fix by copying from /app/gateway and /app/workers which were cp'd
to the builder's regular filesystem.

Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 14:10:13 +07:00
asepharyanaandKilo cbda54fdee fix(infra): update tools submodule to rust 1.89 toolchain
Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 13:57:51 +07:00
asepharyanaandKilo 836ef6862e fix(infra): bump cargo-chef image to rust 1.89 for compatibility
cargo-chef with rust 1.85 has compatibility issues with some dependency crates.

Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 13:45:51 +07:00
asepharyanaandKilo bd751c0fca fix(infra): update tools submodule to fix AbortController type error
Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 13:43:32 +07:00
asepharyanaandKilo ad7a0de37b fix(infra): update tools submodule to fix lucide icon imports
Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 13:41:07 +07:00
asepharyana 05a9179356 chore: update tools submodule to latest (add bun.lock) 2026-07-24 13:32:44 +07:00
asepharyanaandKilo 584a7155e6 fix(infra): use pre-built cargo-chef image in Dockerfile
Replace cargo install cargo-chef with pre-built image
lukemathwalker/cargo-chef:latest-rust-1.85.0 and add build
cache mounts for faster rebuilds.

Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 13:32:31 +07:00
asepharyanaandKilo a41b2d7312 fix(infra): remove bun.lock from Dockerfile, fix frontend build
The bun.lock doesn't exist yet - use bun install without frozen-lockfile.
This will be optimized once a lockfile is generated.

Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 13:20:30 +07:00
asepharyanaandKilo c8fcaecbdb fix(ci): make tools submodule public for CI access
Tools repo is now public so GITHUB_TOKEN can clone it.
Revert the token injection workaround since it's no longer needed.

Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 13:16:21 +07:00
asepharyanaandKilo d8b1457a11 fix(infra): convert apps/tools to submodule, fix Dockerfile paths
Move tools code to its own repo (asepharyana/asepharyana-hub-tools)
and add as git submodule following the existing app pattern.
Fix Dockerfile paths to use apps/tools/ prefix since build context
is the repo root.

Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 13:11:54 +07:00
asepharyanaandKilo 67288c8723 feat(infra): add tools service with document scanner, image & PDF tools
Implement self-hosted document scanner and media processing tools as
an alternative to CamScanner/ilovepdf without third-party uploads.

Backend: Rust Axum gateway + worker pool with NATS JetStream queue
Frontend: Next.js 16 + shadcn/ui + Tailwind v4 + Framer Motion
Pipeline: Canny edge detection -> DLT homography warp -> Sauvola
binarization -> Hough deskew -> Tesseract OCR -> searchable PDF

Phase 1 (MVP) delivers:
- Document scanner with perspective correction and OCR
- Image compress/resize/convert tools
- PDF merge/split/compress tools
- Real-time WebSocket progress updates
- Rate limiting, auto-cleanup, Prometheus metrics
- Full CI/CD pipeline with Docker multi-stage build

Co-Authored-By: Kilo <kilo@kilo.ai>
2026-07-24 13:08:09 +07:00
asepharyana 9c12d135e4 Implement code changes to enhance functionality and improve performance 2026-07-24 10:19:03 +07:00
asepharyana 68dea35d73 feat: add infrastructure documentation and processing pipeline for document scanner
- Introduced a comprehensive Docker image architecture for the project, detailing multi-stage builds for Rust backend and Next.js frontend.
- Added Docker Compose configuration for the tools service, including environment variables and volume management.
- Documented CI/CD integration steps for Docker build and deployment workflows.
- Implemented a detailed processing pipeline for document scanning, covering stages from image preprocessing to PDF generation.
- Included edge case handling and performance budget for each stage of the pipeline.
- Enhanced security considerations and rollback strategies for the tools service.
2026-07-24 10:19:03 +07:00
GitHub Action 447b22d9da chore: update manifests and submodules [skip ci] 2026-07-24 02:11:27 +00:00
mytheclipsebotreview[bot] e38fe75d3c Auto-merge PR #6
chore(deps): bump actions/checkout from 6 to 7 in the github-actions group across 1 directory
2026-07-24 02:09:41 +00:00
asepharyana ddc4c58243 chore: update subproject commit for hub application 2026-07-24 08:26:03 +07:00
Asepharyana 1e10973c86 ci: trigger fresh run for updated security.yml 2026-07-24 01:39:57 +07:00
dependabot[bot] 0242fc4b68 chore(deps): bump actions/checkout
Bumps the github-actions group with 1 update in the / directory: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 6 to 7
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-23 18:39:01 +00:00
Asepharyana b7e4203a15 fix(security): checkout git submodules for CodeQL Rust analysis 2026-07-24 01:36:17 +07:00
asepharyanaandClaude Opus 4.8 7181710aa7 chore: remove lidm routes from file config — now via Docker labels
Routing is handled by container labels (traefik.enable + router labels),
so file-level routes for lidm are no longer needed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 19:19:30 +07:00
asepharyanaandClaude Opus 4.8 478a0f849f fix: use websecure + tls for lidm routes, same pattern as other services
Cloudflare Origin CA wildcard cert covers *.asepharyana.my.id,
so websecure entrypoint with tls works correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 19:07:22 +07:00
asepharyanaandClaude Opus 4.8 7fc863d611 feat: add lidm frontend and backend routes to traefik config
Add routers and services for lidm.asepharyana.my.id (→ lidm-frontend:3000)
and lidm-api.asepharyana.my.id (→ lidm-backend:3001) behind Traefik.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 19:02:13 +07:00
GitHub Action df7aaedfba chore: update manifests and submodules [skip ci] 2026-07-23 11:17:49 +00:00
asepharyanaandAsep Haryana Saputra 93d053b123 ci(deploy): remove :latest tag from Docker images to avoid cache bugs
Using only :sha-<short> tags ensures deterministic image references
and prevents stale :latest caches from serving wrong images.

Co-Authored-By: Asep Haryana Saputra <asepharyana@users.noreply.github.com>
2026-07-23 18:15:17 +07:00
asepharyana 401a26aea9 chore: update hub submodule to f3ddc62 2026-07-23 17:56:37 +07:00
asepharyana 0a4952ce7f chore: update hub submodule to 22cdc2e 2026-07-23 17:56:15 +07:00
asepharyana 23ba9c48fa chore: update hub submodule to e528b6a 2026-07-23 17:05:17 +07:00
asepharyana 7e92351f45 chore: update hub submodule to 4dbaba7 2026-07-23 15:54:01 +07:00
asepharyana 8e489b9f41 chore: update hub submodule to 6b725f3 2026-07-23 15:03:04 +07:00
asepharyana 0dbaf66743 chore: update hub submodule to e529e8c 2026-07-23 12:47:56 +07:00
asepharyana 6091d358e1 chore: update hub submodule to d94524b 2026-07-23 12:21:34 +07:00
asepharyanaandClaude Opus 4.8 c17f5f5b32 docs: update ARCHITECTURE.md and CLAUDE.md
- HTTP flow includes hub routes (/, /dashboard, /api/dashboard)
- Compose table adds hub.yml and observability.yml
- Prometheus auto-discovery section (Docker SD + labels)
- Traefik metrics query reference
- Docker socket access pattern (group_add: 988)
- Service registry table includes root domain, hub, jaeger
- CLAUDE.md monitoring section

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 11:11:30 +07:00
asepharyana 83b825b449 chore: bump hub submodule (latency metric fix) 2026-07-23 07:59:08 +07:00
asepharyana 48086de66b chore: bump hub submodule to f0bbd13 2026-07-23 07:52:41 +07:00
asepharyana 545397eb1f fix(infra): fix Prometheus Docker SD relabel + group_add 2026-07-23 07:50:20 +07:00
asepharyanaandClaude Opus 4.8 40e1a80f9f feat(infra): auto-discovery Prometheus + Traefik metrics
- Prometheus: add docker_sd_configs for auto-discovery via labels
- Prometheus: mount Docker socket for Docker SD
- Traefik: enable --metrics.prometheus with entrypoint/service labels
- Traefik: add prometheus.io/scrape=true label
- Observability: mount Docker socket for Prometheus container
- Bump hub submodule (Traefik metrics queries + sparkline charts)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 07:45:59 +07:00
asepharyana 688ac095f6 fix(infra): add group_add for docker socket access in hub 2026-07-23 07:35:58 +07:00
GitHub Action a4852f636a chore: update manifests and submodules [skip ci] 2026-07-23 00:33:56 +00:00
asepharyana 69cae1e73e fix(infra): chown .next dir in hub Dockerfile for runtime permissions 2026-07-23 07:32:59 +07:00
GitHub Action bd9cc1f4c7 chore: update manifests and submodules [skip ci] 2026-07-23 00:25:10 +00:00
asepharyanaandClaude Opus 4.8 aaaf5de70c feat(infra): remove Go dashboard, dashboard is now part of hub Next.js app
- Remove dashboard container from observability.yml
- Remove infra/docker/dashboard.Dockerfile
- Remove dashboard route from Traefik apps.yaml
- Mount Docker socket on hub container for /api/dashboard
- Remove dashboard build from CI/CD (docker-build-push.yml)
- Update submodule pointer to hub with Next.js dashboard rewrite

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 07:23:12 +07:00
asepharyana cea96fcfc2 fix(ci): update dashboard detection to apps/hub/dashboard/ 2026-07-23 07:19:55 +07:00
asepharyanaandClaude Opus 4.8 5ff8f9224f feat(infra): migrate dashboard into hub submodule
- Move infra/dashboard/ → apps/hub/dashboard/ (hub submodule repo)
- Update dashboard.Dockerfile to COPY from apps/hub/dashboard/
- Update go module path

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 07:19:55 +07:00
asepharyana caf3f1d79b fix(infra): remove hub subdomain from Traefik rule 2026-07-23 07:19:55 +07:00
asepharyana 4e4ed101c5 fix(infra): hub at root domain asepharyana.my.id 2026-07-23 07:19:55 +07:00
asepharyanaandClaude Opus 4.8 9db886e68b feat(infra): add hub service — Docker, Traefik, CI/CD
- Add infra/docker/hub.Dockerfile (Next.js multi-stage build)
- Add infra/compose/hub.yml with app-shared-net
- Add Traefik route for hub.asepharyana.{my,web}.id → hub:3000
- Update docker-build-push.yml: path detection, matrix build, dispatch, manifest
- Update deploy-docker.yml: include hub.yml in ALL_COMPOSE_FILES
- Update update-submodule.yml: allow hub service
- Update docs/ARCHITECTURE.md

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 07:19:55 +07:00
asepharyanaandClaude Opus 4.8 cdaa3c3d57 feat(root): add apps/hub as git submodule
- Created asepharyana/asepharyana-hub-hub repo
- Updated CLAUDE.md with both submodules

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 07:19:55 +07:00
GitHub Action f085375363 chore: update manifests and submodules [skip ci] 2026-07-22 23:03:10 +00:00
asepharyana 5999ac159a feat(infra): responsive UI with mobile-first design and blur header
- Mobile-first responsive: single column → 2 columns → auto-fill grid
- Sticky header with backdrop-filter blur
- Cards with hover glow and fade-in animation
- SVG charts scale with container (max-width constraint per breakpoint)
- Touch-friendly link targets on mobile
- Custom dark scrollbar
- 5 breakpoints: 480, 640, 768, 1024, 1280px
2026-07-23 06:02:10 +07:00
GitHub Action 28ba9dd78c chore: update manifests and submodules [skip ci] 2026-07-22 22:59:17 +00:00
asepharyana 4cc2ba1630 fix(infra): remove all fixed grid-column overrides, layout fully auto-flow
All cards now auto-arrange in the CSS grid — no hardcoded positions,
no fixed widths. Layout adapts naturally to content and screen size.
2026-07-23 05:58:17 +07:00
GitHub Action bf39884ea6 chore: update manifests and submodules [skip ci] 2026-07-22 22:39:21 +00:00
asepharyana 373cb4fc1a feat(infra): fully dynamic dashboard layout with auto-adaptive grid
- Replace fixed grid-column spans with auto-fill grid
- Charts auto-hide when no data available (no gaps)
- System Resources row spans full width only when data exists
- Responsive breakpoints for mobile, tablet, desktop
- SVG charts scale with container width
2026-07-23 05:38:27 +07:00
GitHub Action f96041a164 chore: update manifests and submodules [skip ci] 2026-07-22 22:32:10 +00:00
asepharyana 9a48a49f21 feat(infra): auto-discovered node metrics (CPU, RAM, Disk) and fully dynamic dashboard
- Add node-exporter container for host metrics (CPU, RAM, Disk)
- SVG gauge charts rendered server-side from Prometheus data
- Auto-detect compose project name from Docker labels
- Auto-detect web services via traefik.* labels for quick links
- Remove all hardcoded service name filters
- Dynamic system name based on compose project
2026-07-23 05:31:05 +07:00
GitHub Action 4f15cafc02 chore: update manifests and submodules [skip ci] 2026-07-22 22:21:46 +00:00
asepharyana 6c24508c77 fix(ci): add missing dashboard output in changes job so update-manifest runs 2026-07-23 05:20:59 +07:00
GitHub Action c909c4a9a1 chore: update manifests and submodules [skip ci] 2026-07-22 22:17:46 +00:00
asepharyana aa05820393 fix(ci): correct dashboard change detection regex to match files inside infra/dashboard/ 2026-07-23 05:16:47 +07:00
asepharyana ffe07ddef0 fix(infra): filter hub services by compose project label, fix TraceList field name collision 2026-07-22 18:46:16 +07:00
asepharyana 0bc61d8b1c fix(infra): downgrade go.mod to 1.24 for CI build compatibility 2026-07-22 18:37:13 +07:00
asepharyana afae724f6b refactor(infra): server-side SVG dashboard with Go
- Replace nginx+Chart.js client-side dashboard with Go server
- Server-side rendered SVG charts (donut, line charts)
- Direct Docker socket integration via Go stdlib
- Embedded HTML template via //go:embed
- Minimal scratch image (~9MB total)
- Remove nginx.conf and client-side Chart.js
2026-07-22 18:35:29 +07:00
asepharyana 9f54f90621 fix(infra): use anonymous Prometheus volume to avoid deploy script name collision 2026-07-22 18:28:16 +07:00
asepharyana ee4fd677ea feat(infra): add Prometheus and Chart.js dashboard with real-time charts
- Add Prometheus server scraping OTel collector
- Add Chart.js donut chart for service health distribution
- Add line charts for RPS, latency, error rate, trace volume
- Add Prometheus API proxy to nginx whitelist
- Restructure dashboard layout with 12-column responsive grid
2026-07-22 18:26:41 +07:00
asepharyana 1ee99ffc2c fix(infra): restrict Docker API proxy to whitelist only 2026-07-22 18:20:11 +07:00
asepharyana 46314e6d38 fix(infra): correct Traefik OTLP tracing flags 2026-07-22 18:16:18 +07:00
GitHub Action 362e9f4115 chore: update manifests and submodules [skip ci] 2026-07-22 11:13:25 +00:00
asepharyana da268f1671 feat: enhance observability with dynamic links, Docker API integration, and health checks 2026-07-22 18:12:44 +07:00
asepharyana 4ef4f615c1 feat: add dashboard service to Docker build and push workflow 2026-07-22 18:12:44 +07:00
GitHub Action 5d5a0f48b5 chore: update manifests and submodules [skip ci] 2026-07-22 09:44:30 +00:00
asepharyana dcb93035f3 feat(observability): add OpenTelemetry Collector, Jaeger, and custom dashboard for monitoring
fix(infra): update NATS configuration for OpenTelemetry tracing and JetStream support
refactor(dapr): modify Dapr sidecar configuration to use new resources path and config file
chore(docs): update deployment documentation to include observability services
docs(commit-convention): establish commit message guidelines for Asepharyana Hub
docs(deploy-workflow): outline CI/CD pipeline and manual deployment steps for Asepharyana Hub
feat(event-driven): implement event-driven architecture patterns with Dapr and NATS
docs(hub-rules): define repository structure, submodule strategy, and infrastructure patterns
2026-07-22 16:43:37 +07:00
asepharyana df71487563 Add comprehensive documentation for CI/CD pipeline, NATS, security practices, Tailscale networking, and troubleshooting guide
- Created CI/CD pipeline documentation detailing workflows, triggers, and actions for `asepharyana-hub`.
- Added NATS + JetStream guide covering architecture, configuration, CLI tools, and event topics.
- Introduced a security guide outlining best practices for secrets management, TLS, container security, and access control.
- Documented Tailscale networking setup and troubleshooting for connectivity between VPS and bare-metal nodes.
- Compiled a troubleshooting guide addressing common issues across deployment, Dapr, NATS, Traefik, Tailscale, Docker, database, and submodules.
2026-07-22 16:35:38 +07:00
asepharyana 5025d103e9 chore: update scraper submodule to 7640e66 and add deployment documentation 2026-07-22 15:16:45 +07:00
GitHub Action fd3c7f8c29 chore: update manifests and submodules [skip ci] 2026-07-22 08:08:45 +00:00
monrepo-bot 979a70b279 chore: update scraper-api to 7640e66c7673 2026-07-22 08:03:34 +00:00
GitHub Action a3c5d747ca chore: update manifests and submodules [skip ci] 2026-07-22 08:01:56 +00:00
GitHub Action ce7f630894 chore: update manifests and submodules [skip ci] 2026-07-22 07:56:42 +00:00
monrepo-bot ff047b44de chore: update scraper-api to 96ce29b0f349 2026-07-22 07:52:38 +00:00
monrepo-bot 8e128568e7 chore: update scraper-api to 5c51ec895f5f 2026-07-22 07:51:29 +00:00
GitHub Action 8f22d052f8 chore: update manifests and submodules [skip ci] 2026-07-22 07:30:23 +00:00
GitHub Action 005fe466ed chore: update manifests and submodules [skip ci] 2026-07-22 07:29:39 +00:00
asepharyana b7180ab30c chore: update .gitignore to include .playwright-mcp directory 2026-07-22 14:25:00 +07:00
monrepo-bot 299756d285 chore: update scraper-api to 891064354b49 2026-07-22 07:23:54 +00:00
GitHub Action ad67ed9711 chore: update manifests and submodules [skip ci] 2026-07-22 07:19:02 +00:00
asepharyana 2bb1f1e814 chore: update scraper submodule — remove 17 unused deps 2026-07-22 14:13:16 +07:00
GitHub Action aa83986b23 chore: update manifests and submodules [skip ci] 2026-07-22 07:10:32 +00:00
asepharyana 9b9ce9e59e chore: update scraper submodule — add episodes+downloads to detail 2026-07-22 14:04:58 +07:00
GitHub Action 73011d9d14 chore: update manifests and submodules [skip ci] 2026-07-22 07:02:50 +00:00
asepharyana a659f598ee chore: update scraper submodule — revert to alqanime.si 2026-07-22 13:56:38 +07:00
GitHub Action 1befc9285a chore: update manifests and submodules [skip ci] 2026-07-22 06:52:39 +00:00
GitHub Action 2f3bc950a6 chore: update manifests and submodules [skip ci] 2026-07-22 06:46:53 +00:00
asepharyana 7bf4d69f17 chore: update scraper submodule — CLAUDE.md cleanup 2026-07-22 13:43:20 +07:00
monrepo-bot e54c959765 chore: update scraper-api to c035601a3666 2026-07-22 06:41:21 +00:00
GitHub Action e825c553d6 chore: update manifests and submodules [skip ci] 2026-07-22 06:08:47 +00:00
monrepo-bot 61cd1a712a chore: update scraper-api to 562ada69ec9e 2026-07-22 06:03:20 +00:00
GitHub Action cc3e891960 chore: update manifests and submodules [skip ci] 2026-07-22 06:00:58 +00:00
asepharyana b960f9915c chore: remove EXTERNAL_BROWSERLESS env var from compose and docs 2026-07-22 12:55:50 +07:00
asepharyana 7e563b7a54 chore: update scraper submodule — remove browserless fallback
Dropped EXTERNAL_BROWSERLESS_WS feature; proxy-bun service now handles
all proxy needs.
2026-07-22 12:55:32 +07:00
asepharyana 64b8bb29a3 fix: include all compose files for dependency resolution on selective deploy
Scraper services depend on dapr-placement (dapr.yml) and nats (nats.yml).
When only scraper.yml changed, docker compose pull/up failed with
'undefined service' because dependent compose files were excluded.

Now always include ALL compose files for dependency resolution, but
selectively pull and up only the target services during partial updates.
2026-07-22 08:40:49 +07:00
GitHub Action 443ef9a8da chore: update manifests and submodules [skip ci] 2026-07-22 00:22:16 +00:00
GitHub Action 36de54dd85 chore: update manifests and submodules [skip ci] 2026-07-22 00:21:29 +00:00
asepharyana 79015974df fix: map scraper-api service name to correct submodule path
The update-submodule.yml workflow used apps/${SERVICE} directly,
but the submodule path is apps/scraper, not apps/scraper-api.
Added a case statement to map service names to correct paths.
2026-07-22 07:17:40 +07:00
asepharyana f7f8ee1beb chore: update scraper submodule to fix notify-parent target
Points to asepharyana/asepharyana-hub instead of the dead
MythEclipse/ultimate-asepharyana.tech.
2026-07-22 07:07:16 +07:00
asepharyana a6b822bbfe fix: add manual cargo build step for CodeQL Rust analysis
The security.yml CodeQL job fails because:
1. The Rust project is in apps/scraper/, not at the repo root
2. CodeQL v4's autobuilder can't find Cargo.toml at the root
3. CodeQL v4 requires the Rust code to be built for analysis

Also removes javascript-typescript language since this repo has no JS/TS code.
2026-07-22 07:02:25 +07:00
asepharyana 0ba5da78f9 fix: remove javascript-typescript from CodeQL languages in security.yml
The repo only has Rust source code (no JS/TS). CodeQL v4 fails when
a specified language has no code to analyze, which blocks PR #5
(dependabot bumping codeql-action from v3 to v4).
2026-07-22 06:59:17 +07:00
asepharyana 26d980fc00 refactor(infra): update Traefik configuration references and remove obsolete traefik.yaml 2026-07-21 19:18:42 +07:00
asepharyana 1098f97c05 fix: remove duplicate clean-code.md — only keep the symlink to kana-best-practice-engineering 2026-07-21 18:38:27 +07:00
asepharyana 3fdf1c670f refactor(clean-code): symlink to kana-best-practice-engineering for comprehensive clean code references (SOLID, TDD, architecture, paradigms, craft, oath) 2026-07-21 18:23:07 +07:00
asepharyana 3299759dae feat: add Claude Code project skills — clean-code, hub-rules, commit-convention, event-driven, deploy-workflow 2026-07-21 18:21:29 +07:00
asepharyana bc153b7491 fix(dapr): use pubsub.redis — Dapr 1.17 doesn't include built-in NATS pubsub. Redis already running. 2026-07-21 17:33:09 +07:00
asepharyana 05f3a65358 fix(dapr): use pubsub.nats instead of deprecated natsstreaming, use --resources-path 2026-07-21 17:31:57 +07:00
asepharyana 7b983daa4c fix(nats,dapr): remove healthchecks — scratch images have no shell/wget. Fix depends_on to use service_started 2026-07-21 17:30:21 +07:00
asepharyana 9a29d86fbd fix(nats): correct CLI flags — use -js/-sd instead of --store_dir/--max_payload 2026-07-21 17:27:48 +07:00
asepharyanaandClaude Opus 4.8 ea168b4b20 feat: add NATS + Dapr infrastructure for event-driven architecture
- infra/compose/nats.yml: NATS server with JetStream, persistence volume, healthcheck
- infra/compose/dapr.yml: Dapr placement service for sidecar coordination
- infra/dapr/: Dapr global config, pubsub component (NATS), statestore (Redis)
- infra/compose/scraper.yml: add Dapr sidecar container + depends_on nats/dapr
- docs/add-dapr-service.md: guide for integrating Dapr into new services
- docs/add-new-app.md: add Dapr sidecar step
- ARCHITECTURE.md: add NATS/Dapr to infra table + service mesh diagram
- infra/README.md: update layout and deployment order
- deploy-docker.yml: add nats.yml + dapr.yml to ALL_COMPOSE_FILES

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 17:26:02 +07:00
asepharyana d546a1acf9 fix(scraper): healthcheck tolerate 404 — server is up even if root returns 404 2026-07-21 15:52:51 +07:00
asepharyana 23269e1ab2 fix(scraper): fix healthcheck — use wget instead of ss (not available in slim image) 2026-07-21 15:36:42 +07:00
asepharyanaandClaude Opus 4.8 8f05d1df3d fix(scraper): add EXTERNAL_BROWSERLESS_WS back — service still running on imrnes
Also relax scraper healthcheck to use TCP fallback (ss) for when /health
endpoint doesn't respond in time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 15:32:25 +07:00
asepharyanaandClaude Opus 4.8 28934aaafc docs: update Tailscale IPs and node names to match live infrastructure
- ARCHITECTURE.md: orange → orangevps (100.79.111.61), update imrnes IP to 100.121.180.82, archlinux IP to 100.84.39.83, remove offline laptop node
- README.md: update VPS table, env examples, troubleshooting IPs
- CONTRIBUTING.md: fix VPS name and IP in deploy section

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 15:04:57 +07:00
GitHub Action 75da3af5b8 chore: update manifests and submodules [skip ci] 2026-07-21 06:34:29 +00:00
asepharyana c4ecf8f21b Merge commit 'f465ca2'
# Conflicts:
#	infra/compose/scraper.yml
2026-07-21 13:28:37 +07:00
asepharyanaandClaude Opus 4.8 f465ca261e refactor: clean up codebase — remove legacy references, optimize dockerfile, prune .env.example, simplify CI/CD
- Traefik: remove asephstech/asephscloud SSL certs, fix api.insecure, add ping healthcheck
- .env.example: prune from 145 to 30 vars (remove Firebase, Discord, Coolify, etc.)
- scraper.Dockerfile: remove Node.js & Chromium from runtime, add healthcheck
- CI/CD: remove orphan container ref, commented blocks, fix lint workflow
- infra: remove mysql config (no active service), clean up middleware config
- root: remove .nvmrc (dup), renovate.json (use dependabot), simplify Makefile
- scripts: fix legacy package refs, simplify update-deps to use bun
- docs: remove stale handoff-log, quality-gates, superpowers designs, archived files
- compose: add healthchecks to traefik, redis, and scraper-api services

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 13:27:45 +07:00
asepharyana 092c0b363d Refactor Dockerfile for scraper, update Traefik configuration, and clean up scripts
- Simplified Dockerfile for scraper by removing unnecessary Node.js installation and optimizing build stages.
- Updated Traefik environment configuration to use shorter variable names for certificate paths.
- Removed commented-out middleware configurations in dynamic middlewares.yaml.
- Cleaned up legacy SSL configurations in ssl.yaml.
- Disabled insecure API access in Traefik configuration.
- Deleted unused renovate.json file.
- Modified update environment script to only process apps directory.
- Updated GHCR cleanup script to target specific package.
- Streamlined dependency update script to focus on app submodules.
- Removed setup script for Prometheus as it is no longer needed.
2026-07-21 13:27:13 +07:00
GitHub Action 84d6056e5e chore: update manifests and submodules [skip ci] 2026-07-21 06:01:26 +00:00
asepharyana 80027ee5d3 chore: update subproject commit for scraper application 2026-07-21 12:54:57 +07:00
asepharyana 5d6bdf6ce3 refactor: update architecture and documentation for scraper service, remove deprecated services 2026-07-21 12:54:57 +07:00
asepharyana cdad9ae720 chore: remove unused configuration files and submodules 2026-07-21 12:54:57 +07:00
asepharyana 44a56fa0a9 feat: add comprehensive deployment guide for Docker and GitHub Actions 2026-07-21 12:54:57 +07:00
mytheclipsebotreview[bot] 8145aff114 Auto-merge PR #4
Test PR-Agent V3 - Model Fix
2026-07-17 02:02:32 +00:00
asepharyana 45ab3c8792 test: pr-agent v3 with model fix 2026-07-17 08:03:26 +07:00
112 changed files with 9142 additions and 4970 deletions
+29
View File
@@ -0,0 +1,29 @@
{
"skills": [
{
"name": "clean-code",
"filePattern": ".claude/skills/clean-code/SKILL.md",
"description": "Clean Code, Clean Architecture, SOLID, TDD — dari kana-best-practice-engineering"
},
{
"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"
}
]
}
+1
View File
@@ -0,0 +1 @@
/home/asephs/kana-best-practice-engineering/skills/clean-code
+65
View File
@@ -0,0 +1,65 @@
---
name: commit-convention
description: Enforce commit message convention untuk Asepharyana Hub
---
# Commit Convention — Asepharyana Hub
## Format
```
<type>(<scope>): <description>
[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
+103
View File
@@ -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-<short>` + `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-<short-sha>` — immutable, untuk rollback
- `latest` — mutable, untuk convenience
- Build cache: `sha-<short>-buildcache`
- Registry: `ghcr.io/asepharyana/asepharyana-hub/<service>`
## 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.
+133
View File
@@ -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.<domain>.<action>
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/<app-id>/method/<path>
```
## 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
}
}
```
+96
View File
@@ -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)
### Compose File Pattern
```yaml
services:
<service>:
container_name: <service>
image: ghcr.io/asepharyana/asepharyana-hub/<service>:sha-<sha>
restart: always
networks:
app-shared-net:
aliases:
- <service>
env_file:
- ../../.env
networks:
app-shared-net:
name: app-shared-net
external: true
```
### Dapr Sidecar Pattern
```yaml
<service>-dapr:
container_name: <service>-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=<service>'
- '--app-port=<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: `<service>.asepharyana.my.id` + `<service>.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)
-7
View File
@@ -1,7 +0,0 @@
{
"image": "mcr.microsoft.com/devcontainers/universal:2",
"features": {
"ghcr.io/devcontainers/features/rust:1": {},
"ghcr.io/devcontainers/features/node:2": {}
}
}
+23 -38
View File
@@ -32,7 +32,7 @@ jobs:
if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' || github.event.workflow_run.conclusion == 'success'
steps:
- name: Checkout repository
uses: actions/checkout@v6
uses: actions/checkout@v7
with:
fetch-depth: 1
submodules: false
@@ -89,7 +89,7 @@ jobs:
git fetch origin main --depth=1 || true
# Detect changed files before resetting
ALL_COMPOSE_FILES="infra/compose/traefik.yml infra/compose/shared.yml infra/compose/scraper.yml infra/compose/elysia.yml infra/compose/react.yml infra/compose/rust-auth.yml"
ALL_COMPOSE_FILES="infra/compose/traefik.yml infra/compose/shared.yml infra/compose/scraper.yml infra/compose/hub.yml infra/compose/tools.yml infra/compose/llm-api.yml infra/compose/nats.yml infra/compose/dapr.yml infra/compose/observability.yml"
TRAEFIK_DYNAMIC_DIR="infra/traefik/dynamic"
if git rev-parse HEAD >/dev/null 2>&1; then
@@ -142,8 +142,6 @@ jobs:
TARGET_COMPOSE=""
fi
# Submodule update removed for faster VPS deployments
if command -v "docker" >/dev/null 2>&1 && docker compose version >/dev/null 2>&1; then
COMPOSE_CMD="docker compose"
elif command -v docker-compose >/dev/null 2>&1; then
@@ -153,24 +151,28 @@ jobs:
exit 1
fi
# Construct compose arguments
docker rm -f imphenbot-app || true
if [ -n "$TARGET_COMPOSE" ]; then
# Always include ALL compose files for dependency resolution
COMPOSE_ARGS=""
for f in $TARGET_COMPOSE; do
for f in $ALL_COMPOSE_FILES; do
if [ -f "$f" ]; then
COMPOSE_ARGS="$COMPOSE_ARGS -f $f"
fi
done
UP_FLAGS="-d" # No --remove-orphans for selective updates to avoid killing other services
if [ -n "$TARGET_COMPOSE" ]; then
# Extract service names from target compose file(s) for selective up
TARGET_SERVICES=""
for f in $TARGET_COMPOSE; do
if [ -f "$f" ]; then
svcs=$($COMPOSE_CMD -f "$f" config --services 2>/dev/null | tr '\n' ' ' | xargs)
TARGET_SERVICES="$TARGET_SERVICES $svcs"
fi
done
TARGET_SERVICES=$(echo "$TARGET_SERVICES" | xargs) # trim whitespace
echo "🎯 Selective update for services: $TARGET_SERVICES"
else
echo "🚀 Performing full deployment of all services..."
COMPOSE_ARGS=""
for f in $ALL_COMPOSE_FILES; do
COMPOSE_ARGS="$COMPOSE_ARGS -f $f"
done
UP_FLAGS="-d --remove-orphans"
TARGET_SERVICES=""
fi
echo "📥 Pulling images for target services..."
@@ -179,7 +181,7 @@ jobs:
# Retry pull up to 3 times to handle transient Docker attestation lease errors
for attempt in 1 2 3; do
echo "Pull attempt $attempt/3..."
if $COMPOSE_CMD $COMPOSE_ARGS --env-file .env pull; then
if $COMPOSE_CMD $COMPOSE_ARGS --env-file .env pull $TARGET_SERVICES; then
echo "✅ Pull succeeded on attempt $attempt"
PULL_SUCCESS=true
break
@@ -196,10 +198,6 @@ jobs:
echo "🧹 Clearing Git locks..."
rm -f .git/shallow.lock || true
# Cooldown to allow daemon to settle (optional but kept for safety)
# echo "⏳ Waiting for Docker daemon to settle..."
# sleep 2
echo "🧹 Removing stale target containers by container_name..."
# Extract all explicitly defined container_names from compose files and remove them to prevent conflicts
if [ -n "$TARGET_COMPOSE" ]; then
@@ -223,7 +221,11 @@ jobs:
echo "🆙 Starting services..."
echo "🔍 Debug: Current docker containers:"
docker ps -a
$COMPOSE_CMD $COMPOSE_ARGS --env-file .env up $UP_FLAGS
if [ -n "$TARGET_SERVICES" ]; then
$COMPOSE_CMD $COMPOSE_ARGS --env-file .env up -d $TARGET_SERVICES
else
$COMPOSE_CMD $COMPOSE_ARGS --env-file .env up -d --remove-orphans
fi
# ── Traefik reload ──
if [ "${RELOAD_TRAEFIK:-false}" = "true" ]; then
@@ -235,20 +237,3 @@ jobs:
fi
EOF
# - name: Restart coolify-proxy (delay 1 min)
# env:
# SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
# VPS_HOST: ${{ secrets.VPS_HOST }}
# VPS_USER: ${{ secrets.VPS_USER }}
# run: |
# mkdir -p ~/.ssh
# echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
# chmod 600 ~/.ssh/id_rsa
# ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts
#
# echo "⏳ Waiting 60s for containers to settle before restarting proxy..."
# sleep 60
#
# echo "🔄 Restarting coolify-proxy..."
# ssh "${VPS_USER}@${VPS_HOST}" "docker restart coolify-proxy"
# echo "✅ coolify-proxy restarted."
+36 -32
View File
@@ -5,7 +5,10 @@ on:
branches:
- main
paths:
- 'apps/**'
- 'apps/scraper/**'
- 'apps/hub/**'
- 'apps/tools/**'
- 'apps/llm-api/**'
- '.github/workflows/docker-build-push.yml'
- 'infra/**'
- '!infra/compose/**'
@@ -34,11 +37,11 @@ jobs:
outputs:
matrix: ${{ steps.set-matrix.outputs.matrix }}
scraper-api: ${{ steps.filter.outputs['scraper-api'] == 'true' || steps.dispatch.outputs['scraper-api'] == 'true' || github.event_name == 'workflow_dispatch' }}
elysia-api: ${{ steps.filter.outputs['elysia-api'] == 'true' || steps.dispatch.outputs['elysia-api'] == 'true' || github.event_name == 'workflow_dispatch' }}
react-web: ${{ steps.filter.outputs['react-web'] == 'true' || steps.dispatch.outputs['react-web'] == 'true' || github.event_name == 'workflow_dispatch' }}
rust-auth: ${{ steps.filter.outputs['rust-auth'] == 'true' || steps.dispatch.outputs['rust-auth'] == 'true' || github.event_name == 'workflow_dispatch' }}
hub: ${{ steps.filter.outputs['hub'] == 'true' || steps.dispatch.outputs['hub'] == 'true' || github.event_name == 'workflow_dispatch' }}
tools: ${{ steps.filter.outputs['tools'] == 'true' || steps.dispatch.outputs['tools'] == 'true' || github.event_name == 'workflow_dispatch' }}
llm-api: ${{ steps.filter.outputs['llm-api'] == 'true' || steps.dispatch.outputs['llm-api'] == 'true' || github.event_name == 'workflow_dispatch' }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: false
fetch-depth: 2
@@ -63,9 +66,9 @@ jobs:
}
echo "scraper-api=$(changed '^(apps/scraper(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/scraper\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
echo "elysia-api=$(changed '^(apps/elysia(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/elysia\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
echo "react-web=$(changed '^(apps/react(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/react\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
echo "rust-auth=$(changed '^(apps/rust-auth(/|$)|infra/docker/rust\.Dockerfile$|\.github/workflows/docker-build-push\.yml$|\.gitmodules$)')" >> "$GITHUB_OUTPUT"
echo "hub=$(changed '^(apps/hub(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/hub\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
echo "tools=$(changed '^(apps/tools(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/tools\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
echo "llm-api=$(changed '^(apps/llm-api(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/llm-api\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
- name: Parse repository_dispatch payload
id: dispatch
@@ -87,15 +90,15 @@ jobs:
fi
case "$SERVICE" in
scraper-api|elysia-api|react-web|rust-auth) ;;
scraper-api|hub|tools|llm-api) ;;
*)
exit 1
;;
esac
if ! [[ "$SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then echo "::error::Invalid sha '$SHA'. Expected 40 hex characters"; exit 1; fi
if ! [[ "$SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then echo "::error::Invalid sha '$SHA'. Expected 40 hex characters"; fi
SERVICES=(scraper-api elysia-api react-web rust-auth)
SERVICES=(scraper-api hub tools llm-api)
for svc in "${SERVICES[@]}"; do
if [ "$SERVICE" = "$svc" ]; then
echo "${svc}=true" >> "$GITHUB_OUTPUT"
@@ -113,9 +116,9 @@ jobs:
}
if [ "${{ steps.filter.outputs['scraper-api'] == 'true' || steps.dispatch.outputs['scraper-api'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "scraper-api" "docker-scraper" "apps/scraper"; fi
if [ "${{ steps.filter.outputs['elysia-api'] == 'true' || steps.dispatch.outputs['elysia-api'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "elysia-api" "docker-elysia" "apps/elysia"; fi
if [ "${{ steps.filter.outputs['react-web'] == 'true' || steps.dispatch.outputs['react-web'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "react-web" "docker-react" "apps/react"; fi
if [ "${{ steps.filter.outputs['rust-auth'] == 'true' || steps.dispatch.outputs['rust-auth'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "rust-auth" "docker-rust-auth" "apps/rust-auth"; fi
if [ "${{ steps.filter.outputs['hub'] == 'true' || steps.dispatch.outputs['hub'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "hub" "docker-hub" "apps/hub"; fi
if [ "${{ steps.filter.outputs['tools'] == 'true' || steps.dispatch.outputs['tools'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "tools" "docker-tools" "apps/tools"; fi
if [ "${{ steps.filter.outputs['llm-api'] == 'true' || steps.dispatch.outputs['llm-api'] == 'true' || github.event_name == 'workflow_dispatch' }}" == "true" ]; then add_service "llm-api" "docker-llm-api" "apps/llm-api"; fi
JSON_ARRAY="[$(IFS=,; echo "${SERVICES[*]}")]"
echo "matrix=$JSON_ARRAY" >> $GITHUB_OUTPUT
@@ -135,9 +138,9 @@ jobs:
case "$SERVICE" in
"scraper-api") REPO="https://github.com/asepharyana/asepharyana-hub-scraper.git" ;;
"elysia-api") REPO="https://github.com/asepharyana/asepharyana-hub-elysia.git" ;;
"react-web") REPO="https://github.com/asepharyana/asepharyana-hub-react.git" ;;
"rust-auth") REPO="https://github.com/asepharyana/asepharyana-hub-rust-auth.git" ;;
"hub") REPO="https://github.com/asepharyana/asepharyana-hub-hub.git" ;;
"tools") echo "tools is built from monorepo, no submodule wait needed"; exit 0 ;;
"llm-api") REPO="https://github.com/asepharyana/asepharyana-hub-llm-api.git" ;;
*)
echo "::error::Unsupported service '$SERVICE'"
exit 1
@@ -182,7 +185,7 @@ jobs:
contents: read
packages: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: false
@@ -198,6 +201,7 @@ jobs:
cd "$SUBMODULE_PATH"
git fetch origin "$DISPATCH_SHA"
git checkout "$DISPATCH_SHA"
cd "${GITHUB_WORKSPACE}"
fi
- uses: docker/login-action@v4
@@ -215,13 +219,13 @@ jobs:
SVC_NAME="${{ matrix.id }}"
SHORT=${GITHUB_SHA:0:7}
echo "image=${REGISTRY}/${IMAGE_NAME_PREFIX}/${SVC_NAME}" >> $GITHUB_OUTPUT
echo "tags=${REGISTRY}/${IMAGE_NAME_PREFIX}/${SVC_NAME}:latest,${REGISTRY}/${IMAGE_NAME_PREFIX}/${SVC_NAME}:sha-${SHORT}" >> $GITHUB_OUTPUT
echo "tags=${REGISTRY}/${IMAGE_NAME_PREFIX}/${SVC_NAME}:sha-${SHORT}" >> $GITHUB_OUTPUT
echo "cache-registry=${REGISTRY}/${IMAGE_NAME_PREFIX}/${SVC_NAME}:buildcache" >> $GITHUB_OUTPUT
case "$SVC_NAME" in
"scraper-api") echo "dockerfile=infra/docker/scraper.Dockerfile" >> $GITHUB_OUTPUT ;;
"elysia-api") echo "dockerfile=infra/docker/elysia.Dockerfile" >> $GITHUB_OUTPUT ;;
"react-web") echo "dockerfile=infra/docker/react.Dockerfile" >> $GITHUB_OUTPUT ;;
"rust-auth") echo "dockerfile=infra/docker/rust.Dockerfile" >> $GITHUB_OUTPUT ;;
"hub") echo "dockerfile=infra/docker/hub.Dockerfile" >> $GITHUB_OUTPUT ;;
"tools") echo "dockerfile=infra/docker/tools.Dockerfile" >> $GITHUB_OUTPUT ;;
"llm-api") echo "dockerfile=infra/docker/llm-api.Dockerfile" >> $GITHUB_OUTPUT ;;
esac
- name: Build and Push Docker image
@@ -252,7 +256,7 @@ jobs:
permissions:
contents: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: false
token: ${{ secrets.GITHUB_TOKEN }}
@@ -266,15 +270,15 @@ jobs:
declare -A SERVICES
SERVICES["scraper-api"]="scraper.yml"
SERVICES["elysia-api"]="elysia.yml"
SERVICES["react-web"]="react.yml"
SERVICES["rust-auth"]="rust-auth.yml"
SERVICES["hub"]="hub.yml"
SERVICES["tools"]="tools.yml"
SERVICES["llm-api"]="llm-api.yml"
declare -A PATHS
PATHS["scraper-api"]="apps/scraper"
PATHS["elysia-api"]="apps/elysia"
PATHS["react-web"]="apps/react"
PATHS["rust-auth"]="apps/rust-auth"
PATHS["hub"]="apps/hub"
PATHS["tools"]="apps/tools"
PATHS["llm-api"]="apps/llm-api"
# Use git config for possible commits
git config --local user.email "action@github.com"
@@ -283,9 +287,9 @@ jobs:
for id in "${!SERVICES[@]}"; do
SHOULD_HAVE_RUN=false
if [ "${{ needs.changes.outputs['scraper-api'] }}" == "true" ] && [ "$id" == "scraper-api" ]; then SHOULD_HAVE_RUN=true; fi
if [ "${{ needs.changes.outputs['elysia-api'] }}" == "true" ] && [ "$id" == "elysia-api" ]; then SHOULD_HAVE_RUN=true; fi
if [ "${{ needs.changes.outputs['react-web'] }}" == "true" ] && [ "$id" == "react-web" ]; then SHOULD_HAVE_RUN=true; fi
if [ "${{ needs.changes.outputs['rust-auth'] }}" == "true" ] && [ "$id" == "rust-auth" ]; then SHOULD_HAVE_RUN=true; fi
if [ "${{ needs.changes.outputs['hub'] }}" == "true" ] && [ "$id" == "hub" ]; then SHOULD_HAVE_RUN=true; fi
if [ "${{ needs.changes.outputs['tools'] }}" == "true" ] && [ "$id" == "tools" ]; then SHOULD_HAVE_RUN=true; fi
if [ "${{ needs.changes.outputs['llm-api'] }}" == "true" ] && [ "$id" == "llm-api" ]; then SHOULD_HAVE_RUN=true; fi
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then SHOULD_HAVE_RUN=true; fi
@@ -0,0 +1,20 @@
name: Publish to FlakeHub
on:
push:
branches: [main, master]
workflow_dispatch:
jobs:
flakehub-publish:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v6
- uses: DeterminateSystems/determinate-nix-action@main
- uses: DeterminateSystems/flakehub-push@main
with:
visibility: public
rolling: true
+8 -10
View File
@@ -5,26 +5,24 @@ on:
pull_request:
branches: [main]
paths:
- 'apps/*/src/**/*.ts'
- 'apps/*/src/**/*.tsx'
- 'biome.json'
- '*.json'
- '*.js'
push:
branches: [main]
paths:
- 'apps/*/src/**/*.ts'
- 'apps/*/src/**/*.tsx'
- 'biome.json'
- '*.json'
- '*.js'
jobs:
biome:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
submodules: recursive
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm install -g @biomejs/biome
- run: biome ci . --no-errors-on-unmatched
- uses: oven-sh/setup-bun@v2
- run: bun install --frozen-lockfile
- run: bun run ci
+84
View File
@@ -0,0 +1,84 @@
name: Nix Build & Deploy — All Services
on:
push:
branches: [main]
paths:
- 'apps/**'
- 'infra/**'
- 'flake.nix'
- 'flake.lock'
- '.github/workflows/nix-build.yml'
workflow_dispatch:
concurrency:
group: nix-deploy
cancel-in-progress: false
permissions:
contents: read
env:
VPS_HOST: ${{ secrets.VPS_HOST }}
VPS_USER: ${{ secrets.VPS_USER }}
jobs:
build-and-deploy:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
service: [hub, scraper, tools-gateway, tools-workers, tools-frontend, llm-api]
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: recursive
fetch-depth: 0
- name: Install Nix
uses: DeterminateSystems/nix-installer-action@v22
with:
determinate: false
extra-conf: |
sandbox = false
accept-flake-config = true
- name: Cache Nix
uses: DeterminateSystems/magic-nix-cache-action@v14
- name: Build ${{ matrix.service }}
id: build
run: |
nix build .#${{ matrix.service }} --impure --option sandbox false --print-build-logs
STORE_PATH=$(readlink result)
echo "store-path=$STORE_PATH" >> "$GITHUB_OUTPUT"
echo "✅ ${{ matrix.service }}: $STORE_PATH"
- name: Setup SSH key
if: github.ref == 'refs/heads/main'
env:
SSH_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
sed -i 's/\r$//' ~/.ssh/id_ed25519
ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null 2>&1 || { echo "SSH key invalid"; exit 1; }
ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts 2>/dev/null
- name: Deploy ${{ matrix.service }} to VPS
if: github.ref == 'refs/heads/main'
run: |
STORE_PATH="${{ steps.build.outputs.store-path }}"
echo "=== Copying ${{ matrix.service }}: $STORE_PATH ==="
nix copy --to "ssh://$VPS_USER@$VPS_HOST" "$STORE_PATH"
echo "=== Updating profile ==="
ssh "$VPS_USER@$VPS_HOST" "sudo /nix/var/nix/profiles/default/bin/nix-env --profile /nix/var/nix/profiles/${{ matrix.service }} --set '$STORE_PATH'"
echo "=== Restarting service ==="
ssh "$VPS_USER@$VPS_HOST" "sudo systemctl restart ${{ matrix.service }}" || echo " ⚠️ restart failed (may not be enabled yet)"
echo "✅ ${{ matrix.service }} deployed"
+14 -4
View File
@@ -13,8 +13,18 @@ jobs:
permissions:
security-events: write
steps:
- uses: actions/checkout@v6
- uses: github/codeql-action/init@v3
- uses: actions/checkout@v7
with:
languages: javascript-typescript, rust
- uses: github/codeql-action/analyze@v3
fetch-depth: 2
submodules: recursive
- uses: github/codeql-action/init@v4
with:
languages: rust
- name: Build Rust projects for CodeQL analysis
run: |
cargo build --manifest-path apps/scraper/Cargo.toml
cargo build --manifest-path apps/llm-api/Cargo.toml
- uses: github/codeql-action/analyze@v4
-36
View File
@@ -1,36 +0,0 @@
name: TypeCheck
on:
pull_request:
branches: [main]
paths:
- 'apps/react/src/**/*.ts'
- 'apps/react/src/**/*.tsx'
- 'apps/elysia/src/**/*.ts'
- 'apps/elysia/tsconfig.json'
- 'tsconfig.base.json'
jobs:
typecheck:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
with:
submodules: recursive
- uses: oven/setup-bun@v2
with:
bun-version: latest
- name: TypeCheck apps/react
working-directory: apps/react
run: |
bun install
npx tsc --noEmit
- name: TypeCheck apps/elysia
working-directory: apps/elysia
run: |
bun install
bun run typecheck
+13 -7
View File
@@ -11,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- name: Validate payload
env:
@@ -36,7 +36,7 @@ jobs:
fi
case "$SERVICE" in
scraper-api|elysia-api|react-web|rust-auth) ;;
scraper-api|hub|llm-api|tools) ;;
*)
echo "::error::Unsupported service '$SERVICE'"
exit 1
@@ -52,14 +52,20 @@ jobs:
run: |
set -euo pipefail
echo "Updating ${SERVICE} to ${SHA}"
git submodule update --init "apps/${SERVICE}"
cd "apps/${SERVICE}"
# full fetch so we get tree objects for the target SHA
# Map service name to submodule path
case "$SERVICE" in
scraper-api) SUBMODULE_PATH="apps/scraper" ;;
llm-api) SUBMODULE_PATH="apps/llm-api" ;;
*) SUBMODULE_PATH="apps/${SERVICE}" ;;
esac
echo "Updating ${SUBMODULE_PATH} to ${SHA}"
git submodule update --init "${SUBMODULE_PATH}"
cd "${SUBMODULE_PATH}"
git fetch --depth=1 origin master 2>/dev/null || git fetch --depth=1 origin main
git checkout "${SHA}"
cd "${GITHUB_WORKSPACE}"
git add "apps/${SERVICE}"
git add "${SUBMODULE_PATH}"
git diff --cached --quiet && exit 0
git config user.name "monrepo-bot"
+5 -3
View File
@@ -1,4 +1,3 @@
apps/gmw/
# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# compiled output
@@ -35,16 +34,18 @@ npm-debug.log
yarn-error.log
testem.log
/typings
.playwright-mcp/
# System Files
.DS_Store
Thumbs.db
.claude
.claude/*
!.claude/skills/
!.claude/settings.json
# Next.js
.next
out
**/.codegraph/**
**/.claude/**
test-output
**/**.env
**/**.env.**
@@ -62,3 +63,4 @@ docs/todo.md
**/vendor/
.codegraph/
result
+12 -9
View File
@@ -1,12 +1,15 @@
[submodule "apps/elysia"]
path = apps/elysia
url = https://github.com/asepharyana/asepharyana-hub-elysia.git
[submodule "apps/react"]
path = apps/react
url = https://github.com/asepharyana/asepharyana-hub-react.git
[submodule "apps/scraper"]
path = apps/scraper
url = https://github.com/asepharyana/asepharyana-hub-scraper.git
[submodule "apps/rust-auth"]
path = apps/rust-auth
url = https://github.com/asepharyana/asepharyana-hub-rust-auth.git
[submodule "apps/hub"]
path = apps/hub
url = https://github.com/asepharyana/asepharyana-hub-hub.git
[submodule "apps/tools"]
path = apps/tools
url = https://github.com/asepharyana/asepharyana-hub-tools.git
[submodule "plugins/hub-guide"]
path = plugins/hub-guide
url = https://github.com/asepharyana/asepharyana-hub-guide.git
[submodule "apps/llm-api"]
path = apps/llm-api
url = https://github.com/asepharyana/asepharyana-hub-llm-api.git
+65
View File
@@ -0,0 +1,65 @@
---
name: commit-convention
description: Commit message convention — type(scope): description for Asepharyana Hub
---
# Commit Convention — Asepharyana Hub
## Format
```
<type>(<scope>): <description>
[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
+97
View File
@@ -0,0 +1,97 @@
---
name: deploy-workflow
description: CI/CD pipeline, Docker build patterns, manual deploy steps, and troubleshooting for 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-<short>` + `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/*
```
## 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.
+133
View File
@@ -0,0 +1,133 @@
---
name: event-driven
description: Event-driven architecture patterns with Dapr + NATS for Asepharyana Hub services
---
# 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.<domain>.<action>
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/<app-id>/method/<path>
```
## 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
}
}
```
+101
View File
@@ -0,0 +1,101 @@
---
name: hub-rules
description: Repository structure, submodule strategy, infrastructure patterns, and architecture of 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)
### Compose File Pattern
```yaml
services:
<service>:
container_name: <service>
image: ghcr.io/asepharyana/asepharyana-hub/<service>:sha-<sha>
restart: always
networks:
app-shared-net:
aliases:
- <service>
env_file:
- ../../.env
networks:
app-shared-net:
name: app-shared-net
external: true
```
### Dapr Sidecar Pattern
```yaml
<service>-dapr:
container_name: <service>-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=<service>'
- '--app-port=<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: `<service>.asepharyana.my.id` + `<service>.asepharya.web.id`
- TLS cert dari volume mount (bukan auto-acme)
### Image Tagging
- `sha-<short-sha>` — immutable, untuk rollback
- `latest` — mutable, untuk convenience
- Registry: `ghcr.io/asepharyana/asepharyana-hub/<service>`
### 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)
-8
View File
@@ -1,8 +0,0 @@
{
"mcpServers": {
"next-devtools": {
"command": "npx",
"args": ["-y", "next-devtools-mcp@latest"]
}
}
}
-81
View File
@@ -1,81 +0,0 @@
# Agent Protocol v10: Strategic Execution and Diagnostic Directives
1. **Mandatory Executability.**
- **Directive:** All generated outputs must be syntactically correct and directly executable by the target system's interpreter, compiler, or shell.
- **Constraint:** Placeholders and pseudo-code are forbidden. Every output must be a complete, functional artifact.
2. **Data and State Integrity.**
- **Directive:** All generated code must strictly adhere to declared data structures, schemas, and the target language's type system.
- **Constraint:** Any operation producing a type mismatch, schema violation, or logical inconsistency is an invalid operation and must be discarded.
3. **Atomic and Consistent State Modification.**
- **Directive:** Modification of a shared resource must be performed as an atomic operation or within an ACID-compliant transaction.
- **Constraint:** Operations that can lead to race conditions or inconsistent state are prohibited. Immutability is the required default.
4. **Zero-Trust Security (Inviolable Safety Constraint).**
- **Directive:** Secrets must not be stored as literal values in source code. They must be loaded at runtime from a secure external source.
- **Constraint:** Generated access policies must adhere to the Principle of Least Privilege.
5. **Supply Chain Security (Inviolable Safety Constraint).**
- **Directive:** All external dependencies must be sourced from trusted repositories and defined in a lockfile for deterministic resolution.
- **Constraint:** The dependency graph must be scanned for known CVEs. Dependencies with critical vulnerabilities are prohibited.
6. **Deterministic and Reproducible Builds.**
- **Directive:** From a given source commit, the build process must produce a byte-for-byte identical artifact in every execution.
- **Constraint:** All automated tests must be deterministic. A regression test codifying the fixed bug's failure condition must be included with the fix.
7. **Structured, Traceable Logging.**
- **Directive:** All processes must emit structured (JSON) logs for significant events. All log entries for a request must contain the same unique trace ID.
- **Constraint:** Error conditions must be explicitly logged with context and propagated. Errors must not be silently suppressed.
8. **Strict API Contract Enforcement.**
- **Directive:** All network communication must strictly conform to its published, versioned API contract.
- **Constraint:** Any network call violating the contract must be rejected. Breaking changes require a major version increment (SemVer).
9. **Distributed System Consensus.**
- **Directive:** Changes to shared state across a distributed system are committed only after a formal consensus algorithm confirms quorum.
- **Constraint:** Nodes in a minority partition must enter a read-only or unavailable state to prevent a split-brain scenario.
10. **Execution Planning and Pre-flight Validation (Think Before Acting).**
- **Directive:** For any multi-step task, a detailed execution plan (sequence of commands and file modifications) must be formulated before any state-modifying action is taken.
- **Constraint:** Before executing a command, the agent must first use a validation or dry-run flag (e.g., `--dry-run`, `--check`) if available. The operation may only proceed if the pre-flight check passes without error.
11. **Post-Failure Root Cause Analysis (Evaluate Mistakes from Logs).**
- **Directive:** Upon command execution failure (non-zero exit code), the current execution plan must be halted, and the agent must enter a diagnostic mode.
- **Constraint:** In diagnostic mode, the agent is required to: 1) Capture and parse the complete `stdout` and `stderr` logs. 2) Identify the specific error message or stack trace. 3) Correlate the error with the last command to form a root cause hypothesis. 4) Formulate a new, corrective execution plan based on the analysis.
12. **Context-Aware File System Operations.**
- **Directive:** Before modifying any file, its full content must be read to establish context. All edits must be based on an in-memory understanding of the file's current state.
- **Constraint:** Blind file operations, such as stream-based search-and-replace without structural validation, are strictly prohibited.
13. **Idempotent State Transitions.**
- **Directive:** Operations that modify state must be designed to be idempotent wherever the protocol allows.
- **Constraint:** Executing the same operation multiple times must result in the same final system state as executing it only once.
14. **Resource Lifecycle Management.**
- **Directive:** All finite system resources (e.g., file handles, network sockets) must be explicitly released after use.
- **Constraint:** The agent must generate code that prevents resource leaks, utilizing language-specific constructs like `try-with-resources` or `defer`.
15. **Configuration as Code (CaC).**
- **Directive:** All configuration must be defined and versioned in source-controlled files.
- **Constraint:** Manual, out-of-band configuration changes are prohibited. Versioned files are the single source of truth.
16. **Atomic and Semantic Version Control.**
- **Directive:** All code changes must be organized into logically atomic commits representing one complete unit of work.
- **Constraint:** Commit messages must adhere to a defined specification (e.g., Conventional Commits).
17. **User Authority and Command Primacy.**
- **Directive:** User-provided instructions and corrections are the definitive source of truth and have the highest operational priority.
- **Constraint:** The agent must immediately adapt its process to align with user directives. Rejected solutions must not be proposed again.
18. **Precedent-Based Improvement.**
- **Directive:** User-approved outputs and successful patterns must be recorded and prioritized as precedents for subsequent tasks.
- **Constraint:** Performance, security, and code quality must not degrade.
19. **Optimization by Explicit Consent.**
- **Directive:** The agent may identify and propose optimizations with a technical justification and supporting metrics.
- **Constraint:** The agent is prohibited from applying any self-initiated optimization without an explicit "approve" command from the user.
20. **System Hierarchy and Safety Overrides.**
- **Directive:** The operational control hierarchy is absolute: 1) **User Command**, 2) **Inviolable Safety Directives (#4, #5)**, 3) **Standard Operational Directives**.
- **Constraint:** If a command conflicts with an Inviolable Directive, the agent must halt, report the conflict and risk, and await a revised command.
-1
View File
@@ -1 +0,0 @@
22.11.0
-8
View File
@@ -1,8 +0,0 @@
{
"recommendations": [
"nrwl.angular-console",
"biomejs.biome",
"ms-playwright.playwright",
"firsttris.vscode-jest-runner"
]
}
-23
View File
@@ -1,23 +0,0 @@
{
"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[javascriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[jsonc]": {
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit"
}
}
+48 -66
View File
@@ -5,9 +5,6 @@
```
asepharyana-hub/
├── apps/ # Application services (Git submodules)
│ ├── elysia/ # Realtime API (Bun/Elysia/Drizzle/Redis)
│ ├── react/ # Frontend SPA (React/Vite/TanStack)
│ ├── rust-auth/ # IAM & auth service (Axum/SeaORM)
│ └── scraper/ # Web scraper service
├── docs/ # Documentation
│ ├── adr/ # Architecture Decision Records
@@ -31,20 +28,12 @@ asepharyana-hub/
## Technology Stack
### Backend Services
### Services
| Service | Language/Runtime | Framework | Database | Key Libraries |
| ------------- | ---------------- | ---------- | ------------------------ | ------------------------------------------------------------ |
| **rust-auth** | Rust | Axum 0.8 | PostgreSQL (sqlx) | sqlx, jsonwebtoken, argon2, redis, opentelemetry, prometheus |
| **elysia** | TypeScript/Bun | Elysia 1.4 | PostgreSQL (Drizzle ORM) | Drizzle ORM, Redis (ioredis), JWT (jose), OTel, Swagger |
| --------- | ---------------- | --------- | -------- | ------------- |
| **scraper** | _(submodule)_ | — | — | — |
### Frontend
| Service | Framework | Build Tool | Key Libraries |
| --------- | --------- | ---------- | --------------------------------------------------------------------------------------------------- |
| **react** | React 19 | Vite 7 | TanStack Router + Query, Three.js/React Three Fiber, Tailwind CSS 4, Zustand, Recharts, tsParticles |
### Infrastructure
| Component | Technology | Purpose |
@@ -53,7 +42,9 @@ asepharyana-hub/
| Container Runtime | Docker + Docker Compose | Service isolation and orchestration |
| Container Registry | GHCR (ghcr.io) | Docker image storage |
| Networking | Tailscale | Secure overlay network between VPS nodes |
| Cache | Redis (Alpine) | Session store, rate limit counters, caching |
| Message Bus | NATS + JetStream | Event-driven pub/sub, job queues, streaming |
| Runtime Sidecar | Dapr | Service invocation, pub/sub abstraction, state management |
| Cache & State | Redis (Alpine) | Session store, rate limit counters, caching, Dapr state store |
| CI/CD | GitHub Actions | Build, test, deploy automation |
## Infrastructure
@@ -62,7 +53,7 @@ asepharyana-hub/
Traefik runs as the entry point for all HTTP/S traffic. It is configured via:
- **Static config**: `infra/traefik/traefik.yaml` — entry points, providers, plugins
- **Static config**: CLI arguments in `infra/compose/traefik.yml` — entry points, providers, plugins
- **Dynamic config**: `infra/traefik/dynamic/` — routers, services, middlewares, TLS
- **Docker provider**: Auto-discovers containers with `traefik.enable=true` labels
- **File provider**: Loads `apps.yaml` (routers/services), `middlewares.yaml`, `ssl.yaml`
@@ -90,7 +81,7 @@ Shared services:
Service compose files are combined during deployment:
```bash
docker compose -f traefik.yml -f shared.yml -f elysia.yml -f react.yml ... up -d
docker compose -f traefik.yml -f shared.yml -f scraper.yml up -d
```
### Tailscale Networking
@@ -98,10 +89,9 @@ docker compose -f traefik.yml -f shared.yml -f elysia.yml -f react.yml ... up -d
```mermaid
graph TB
subgraph "Tailnet (100.64.0.0/10)"
IMRNES["imrnes (100.108.1.124)"]
ORANGE["orange (100.96.248.86)"]
ARCH["archlinux (100.114.19.66)"]
LAPTOP["laptop-2f6e1iph (100.86.195.29)"]
IMRNES["imrnes (100.121.180.82)"]
ORANGEVPS["orangevps (100.79.111.61)"]
ARCH["archlinux (100.84.39.83)"]
end
subgraph "imrnes Services"
@@ -109,28 +99,16 @@ graph TB
REDIS[Redis]
end
subgraph "orange Containers"
subgraph "orangevps Containers"
TRAEFIK[Traefik :443]
RUST_AUTH[rust-auth :3000]
ELYSIA[elysia-api :4092]
REACT[react-web :80]
SCRAPER[scraper-api :4091]
end
TRAEFIK --> RUST_AUTH
TRAEFIK --> ELYSIA
TRAEFIK --> REACT
TRAEFIK --> SCRAPER
RUST_AUTH -.->|Tailscale IP| PG
ELYSIA -.->|Tailscale IP| PG
RUST_AUTH -.->|Tailscale IP| REDIS
ELYSIA -.->|Tailscale IP| REDIS
style IMRNES fill:#3a7,color:#fff
style ORANGE fill:#37a,color:#fff
style ORANGEVPS fill:#37a,color:#fff
style ARCH fill:#773,color:#fff
style LAPTOP fill:#777,color:#fff
```
Container-to-Tailscale connectivity requires a systemd service that adds a route to the main routing table:
@@ -139,7 +117,7 @@ Container-to-Tailscale connectivity requires a systemd service that adds a route
ip route add 100.64.0.0/10 dev tailscale0 table main
```
This is managed by `/etc/systemd/system/tailscale-routes.service` on the `orange` VPS.
This is managed by `/etc/systemd/system/tailscale-routes.service` on the `orangevps` VPS.
## Data Flow
@@ -155,7 +133,7 @@ sequenceDiagram
participant Redis as Redis (imrnes via Tailscale)
User->>DNS: asepharyana.my.id
DNS->>User: A/AAAA record → orange VPS IP
DNS->>User: A/AAAA record → orangevps VPS IP
User->>Traefik: HTTPS request :443
Traefik->>Traefik: TLS termination
Traefik->>Traefik: Middleware chain (headers, rate-limit, buffer)
@@ -165,8 +143,8 @@ sequenceDiagram
App->>DB: sqlx/Drizzle query via Tailscale
DB-->>App: Result set
else Cache lookup
App->>Redis: GET/SET via Tailscale
Redis-->>App: Cached value
App->>Cache: GET/SET via Tailscale
Cache-->>App: Cached value
end
App-->>Traefik: HTTP response
@@ -215,7 +193,7 @@ flowchart LR
### VPS Deployment
The `orange` VPS (Tailscale `100.96.248.86`) hosts all application containers:
The `orangevps` VPS (Tailscale `100.79.111.61`) hosts all application containers:
1. GitHub Actions SSHes into the VPS
2. Production secrets are written as `.env`
@@ -228,7 +206,7 @@ The `orange` VPS (Tailscale `100.96.248.86`) hosts all application containers:
### Selective Deployment
The deploy workflow supports selective updates — if only `infra/compose/elysia.yml` changed, only `elysia-api` is pulled and recreated, avoiding disruption to other services.
The deploy workflow supports selective updates — if only one compose file changed, only the corresponding service is pulled and recreated, avoiding disruption to other services.
```mermaid
graph TB
@@ -265,7 +243,7 @@ Each application lives in its own Git repository and is imported as a submodule
### Submodule Lifecycle
1. Developer pushes to a submodule (e.g., `apps/elysia`)
1. Developer pushes to a submodule (e.g., `apps/scraper`)
2. Submodule's GitHub Action dispatches `repository_dispatch` to the super-repo with the service name and new SHA
3. Super-repo detects the dispatch, waits for the SHA to be fetchable, then builds only that service
4. The compose manifest is updated and committed with the new SHA tag
@@ -275,21 +253,24 @@ Each application lives in its own Git repository and is imported as a submodule
```bash
# Update a single submodule to latest
cd apps/elysia
cd apps/scraper
git checkout main
git pull
cd ../..
git add apps/elysia
git commit -m "chore(elysia): update submodule to latest"
# Update all submodules
git submodule update --remote --merge
git add apps/scraper
git commit -m "chore(scraper): update submodule to latest"
```
## Service Mesh & Inter-Service Communication
### HTTP (External + Internal via Traefik)
External traffic and internal HTTP calls route through Traefik. Services on `app-shared-net` can also communicate directly by container name.
### Event-Driven (NATS + Dapr)
NATS with JetStream provides a persistent message backbone. Each service has a Dapr sidecar that abstracts pub/sub, service invocation, and state management.
```mermaid
graph LR
graph TB
subgraph "External"
WWW[Internet]
end
@@ -298,35 +279,36 @@ graph LR
TRAEFIK[Traefik :443]
subgraph "app-shared-net"
REACT[react-web<br/>:80]
ELYSIA[elysia-api<br/>:4092]
RUST_AUTH[rust-auth<br/>:3000]
NATS[NATS + JetStream<br/>:4222]
DAPR_PLACEMENT[Dapr Placement<br/>:50005]
subgraph "Service: scraper-api"
SCRAPER[scraper-api<br/>:4091]
REDIS[redis<br/>:6379]
DAPR_SIDECAR[Dapr Sidecar<br/>:3500]
SCRAPER --- DAPR_SIDECAR
end
end
subgraph "Imrnes VPS"
PG[(PostgreSQL<br/>:5432)]
REDIS_IMR[Redis<br/>:6379]
DAPR_SIDECAR -.->|gRPC pub/sub| NATS
DAPR_SIDECAR -.->|placement| DAPR_PLACEMENT
end
WWW -->|HTTPS| TRAEFIK
TRAEFIK --> REACT
TRAEFIK --> ELYSIA
TRAEFIK --> RUST_AUTH
TRAEFIK --> SCRAPER
ELYSIA -->|Tailscale| PG
RUST_AUTH -->|Tailscale| PG
ELYSIA -->|internal| REDIS
RUST_AUTH -->|internal| REDIS
ELYSIA -->|Tailscale| REDIS_IMR
RUST_AUTH -->|Tailscale| REDIS_IMR
```
### Communication Patterns
| Pattern | Mechanism | Use Case |
|---------|-----------|----------|
| External HTTP | Traefik → Service | User requests, API calls |
| Internal HTTP | Service → Service (via Traefik or direct) | Synchronous queries |
| Pub/Sub Event | Dapr sidecar → NATS JetStream | Async notifications, image cache events |
| Service Invocation | Dapr sidecar gRPC | Cross-service RPC with retry & observability |
| State Store | Dapr → Redis | Shared state, job progress |
## Observability
- **Prometheus metrics**: Available on rust-auth via `axum-prometheus`
- **Traefik access logs**: JSON format, logged at INFO level
- **Traefik access logs**: JSON format, logged at INFO level
- **Dashboard**: Traefik dashboard at `traefik.asepharyana.my.id` (secured)
+14
View File
@@ -12,8 +12,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Restructured the repository into a lightweight hub repo with standalone app submodules.
- Simplified root tooling to plain `package.json` scripts and per-service commands.
- Kept infrastructure, deployment workflows, and documentation in the root hub repo.
- Cleaned up Traefik SSL config: removed legacy `asephstech`/`asephscloud` cert references, synced volume mounts, fixed `api.insecure`.
- Pruned `.env.example` from 145 legacy vars (Firebase, Discord, Coolify, Portainer, YouTube, etc.) to 30 focused vars.
- Optimized `scraper.Dockerfile`: removed Node.js and Chromium from runtime image.
- Simplified CI/CD workflows: removed orphan container reference, commented code blocks.
- Cleaned up scripts: removed stale MySQL config, fixed package references, simplified update-deps.
- Added NATS + JetStream message broker infrastructure (`infra/compose/nats.yml`).
- Added Dapr runtime infrastructure: placement service, sidecar pattern, pub/sub + state store components.
- Integrated Dapr sidecar into scraper service (`infra/compose/scraper.yml`).
- Updated deployment order: shared → NATS → Dapr → Traefik → apps.
- Added `docs/add-dapr-service.md` guide for adding Dapr to new services.
### Removed
- Removed deprecated services from apps, compose files, Dockerfiles, Traefik routes, and workflows.
- Removed stale monorepo orchestration configs and hook tooling from the root repo.
- Removed `.nvmrc` (duplicate of `.node-version`), `renovate.json` (using dependabot).
- Removed stale documentation: `dependency-map.md`, `observability.md`, `handoff-log.jsonl`, `quality-gates.json`, `workflow-state.json`.
- Removed deprecated `docs/superpowers/` design docs.
- Removed `infra/config/mysql/` (no active MySQL service) and `docs/config/squid.conf.archived`.
+131
View File
@@ -0,0 +1,131 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Repository Overview
Asepharyana Hub is a **hub monorepo** for Asep Haryana Saputra's portfolio ecosystem. Application services live in separate repos imported as Git submodules under `apps/`. Infrastructure (Docker Compose, Traefik, Dapr) lives in `infra/`.
```
asepharyana-hub/
├── apps/ # Git submodules — each app is its own repo
│ ├── hub/ # Personal portfolio SPA (asepharyana-hub-hub)
│ └── scraper/ # Rust scraper API (asepharyana-hub-scraper)
├── docs/ # ADRs, deployment guide, new-app guide
├── infra/
│ ├── compose/ # One Docker Compose file per service
│ ├── dapr/ # Dapr config + component definitions
│ ├── docker/ # Dockerfiles per service
│ └── traefik/ # Reverse proxy config (static + dynamic)
├── scripts/ # Utility scripts (cleanup, update-deps, git hooks)
└── .github/workflows/ # CI/CD pipelines
```
### Submodule Strategy
- Each app in `apps/` is a separate Git repo imported as a submodule. Code changes happen in the submodule repo, not here.
- Submodule pointers are updated by CI/CD (via `repository_dispatch` or manual commit).
- Current submodules:
- `apps/hub``asepharyana/asepharyana-hub-hub`
- `apps/scraper``asepharyana/asepharyana-hub-scraper`
- `apps/llm-api``asepharyana/asepharyana-hub-llm-api`
- `apps/tools``asepharyana/asepharyana-hub-tools`.
### Infrastructure Stack
- **Traefik v3.6** — reverse proxy, TLS termination, middleware chain, Prometheus metrics (`--metrics.prometheus=true`)
- **NATS + JetStream** — message broker with persistent streaming
- **Dapr** — sidecar runtime (pub/sub abstraction, state management, service invocation)
- **Redis (Alpine)** — cache, session store, Dapr state store & pub/sub backend
- **Prometheus** — metrics backend with Docker service discovery (`docker_sd_configs`). Auto-discovers containers with `prometheus.io/scrape=true` label.
- **Jaeger** — distributed tracing backend (all-in-one), OTLP receiver
- **Tailscale** — secure overlay network between VPS nodes (PostgreSQL on `imrnes`, containers on `orangevps`)
### Monitoring
- **Hub dashboard** at `/dashboard` (Next.js client page, auto-refresh 15s)
- **Dashboard API** at `/api/dashboard` — returns JSON with Docker containers, Jaeger traces, Prometheus metrics (RPS, latency, errors, node CPU/RAM/Disk)
- **Docker socket** mounted on `hub` container (`--group-add 988`) for container discovery
- **Prometheus** auto-scrapes Traefik for per-service request metrics
### Networking
- All containers join `app-shared-net` (external Docker bridge network). Service discovery via Docker DNS (container name aliases).
- Traefik handles all external HTTP/S traffic on port 443.
- Cross-VPS traffic (DB, Redis) goes through Tailscale (`100.64.0.0/10`). Container-to-Tailscale connectivity requires a route in the main routing table (managed by `tailscale-routes.service`).
## Commands
```bash
make init-submodules # Initialize submodules after clone
make dev # Start shared dev infrastructure (Redis)
make update-submodules # Update all submodules to latest
bun run check # Biome lint + format + write
bun run ci # Biome CI mode (no writes, exit code on issues)
bun run format # Format only
bun run lint # Lint only
docker build -f infra/docker/scraper.Dockerfile -t scraper-api:latest . # Build image
```
### Validate YAML
```bash
python -c "import pathlib, yaml; [yaml.safe_load(open(p)) for p in pathlib.Path('infra').rglob('*.yml')]"
for f in infra/compose/*.yml; do docker compose -f "$f" config >/dev/null && echo "OK $f"; done
```
## CI/CD Workflows
| Workflow | Trigger | Action |
|----------|---------|--------|
| `lint.yml` | PR/push to main touching `*.json`, `*.js`, `biome.json` | `bun run ci` (Biome lint) |
| `docker-build-push.yml` | Push to main touching `apps/**`/`infra/**`, or `repository_dispatch` | Build Docker images per changed service, push to GHCR, update compose manifests |
| `deploy-docker.yml` | After build completes, or push touching `infra/**` | SSH to VPS (orangevps), pull images, restart containers selectively |
| `security.yml` | PR to main + weekly Monday | CodeQL analysis (Rust) |
| `update-submodule.yml` | `repository_dispatch` | Update submodule pointer in hub repo |
### Deployment Order
1. `shared.yml` (Redis)
2. `nats.yml` (NATS + JetStream)
3. `dapr.yml` (Dapr placement)
4. `traefik.yml` (Reverse proxy)
5. Service compose files (app + Dapr sidecar)
### Secrets Required for Deploy
`SSH_PRIVATE_KEY`, `VPS_HOST`, `VPS_USER`, `VPS_TARGET_DIR`, `ENV_FILE_PRODUCTION`
## Infrastructure Patterns
### Compose File Pattern
Each service gets one compose file. Containers join `app-shared-net` with a `container_name` alias for DNS. The network is declared `external: true`.
### Dapr Sidecar Pattern
Each app gets a companion `daprd` sidecar container. Dapr components (pubsub, statestore) are mounted from `infra/dapr/components/`. The sidecar communicates with NATS for pub/sub and Dapr placement for actor coordination.
### Traefik Routing
- Routers + services defined in `infra/traefik/dynamic/apps.yaml`
- Subdomain pattern: `<service>.asepharyana.my.id` and `<service>.asepharya.web.id`
- TLS certs from volume mounts (not auto-ACME)
- Middleware chain: `secure-headers``compress``retry``rate-limit``buffer`
### Image Tagging
- `sha-<short-sha>` — immutable, for deterministic rollbacks
- `latest` — mutable, for convenience
- Registry: `ghcr.io/asepharyana/asepharyana-hub/<service>`
- Build cache: `sha-<short>-buildcache` (registry-based caching)
## Adding a New Service
1. Create a separate repo for the app code
2. Add as submodule: `git submodule add <url> apps/<name>`
3. Create Dockerfile in `infra/docker/`
4. Create compose file in `infra/compose/` (app + Dapr sidecar)
5. Add Traefik router in `infra/traefik/dynamic/apps.yaml`
6. Add build job in `.github/workflows/docker-build-push.yml`
7. See `docs/add-new-app.md` for full guide
## Commit Convention
Format: `<type>(<scope>): <description>`
Types: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `ci`, `perf`, `style`
Scopes: `scraper`, `infra`, `ci`, `dapr`, `nats`, `docs`, `deps`, `scripts`, `root`
Scope is required. Use imperative mood. No period at end of subject line. Co-Authored-By footer for AI-generated commits.
+15 -67
View File
@@ -14,9 +14,8 @@
## Prerequisites
- **Git** with LFS support
- **Node.js** >= 22.11.0 (via `.node-version` or `.nvmrc`)
- **Node.js** >= 22.11.0 (via `.node-version`)
- **Bun** >= 1.3.11 (package manager)
- **Rust** >= 1.89.0 (for Rust services)
- **Docker** and **Docker Compose** (for shared infrastructure)
## Local Setup
@@ -40,18 +39,14 @@ This checks out all submodules at the pinned commit (not `main`). The submodules
| Path | Remote |
| ---------------- | --------------------------------------- |
| `apps/elysia` | `asepharyana/asepharyana-hub-elysia` |
| `apps/scraper` | `asepharyana/asepharyana-hub-scraper` |
| `apps/react` | `asepharyana/asepharyana-hub-react` |
| `apps/rust-auth` | `asepharyana/asepharyana-hub-rust-auth` |
### 3. Install Dependencies per Service
Install dependencies for TypeScript/Bun services:
```bash
cd apps/elysia && bun install && cd ../..
cd apps/react && npm install && cd ../..
cd apps/scraper && bun install && cd ../..
```
### 4. Start Shared Infrastructure
@@ -75,76 +70,42 @@ Key variables to configure:
| Variable | Description |
| -------------- | ----------------------------------------------------- |
| `DATABASE_URL` | PostgreSQL connection (Tailscale IP to `imrnes` VPS) |
| `REDIS_URL` | Redis connection (`redis://localhost:6379` for local) |
| `JWT_SECRET` | JWT signing secret |
| `GITHUB_TOKEN` | GitHub personal access token |
## Development Workflow
### Running Services
**Rust API (rust-auth):**
```bash
cd apps/rust-auth
cargo run
```
**Elysia API (elysia):**
```bash
cd apps/elysia
bun run dev
```
**React Frontend (react):**
```bash
cd apps/react
npm run dev
```
Refer to each service's own documentation for setup and development instructions.
### API Documentation
- Rust OpenAPI: `http://localhost:4091/docs`
- Elysia Swagger: `http://localhost:4092/docs`
- Elysia AsyncAPI: `http://localhost:4092/docs-ws`
Refer to each service's own documentation for API docs and endpoints.
## Coding Standards
### Linting
- **ESLint** with `@antfu/eslint-config` for TypeScript/JavaScript
- **Cargo Clippy** for Rust
- **Biome** for TypeScript/JavaScript formatting and linting
Run linting:
```bash
# TypeScript/JavaScript
eslint . --no-error-on-unmatched-pattern
# Rust specific
cd apps/rust-auth && cargo clippy -- -D warnings
bun run check
```
### Formatting
- **Prettier** for TypeScript/JavaScript/Markdown (config in `.prettierrc`)
- Single quotes, 100 print width, 2-space indent, trailing commas
- **Cargo fmt** for Rust
- **Biome** for TypeScript/JavaScript
- **EditorConfig** for general formatting (`.editorconfig`)
```bash
# Prettier
prettier --write .
# Rust
cd apps/rust-auth && cargo fmt
# Format all
bun run format
```
### Rust Configuration
Rust services use edition `2024` with stable toolchain (nightly features may be used).
## Commit Message Format
@@ -177,18 +138,15 @@ This project enforces **Conventional Commits** for all commit messages.
### Examples
```
feat(rust-auth): add OAuth2 Google login flow
fix(elysia): handle null JWT payload in auth middleware
chore: update eslint config to v10
feat(scraper): add new data source integration
chore: update biome config to v10
docs: add API endpoint documentation for scraper
refactor(react): extract Header component from App
test(elysia): add unit tests for rate limiter
ci: migrate to CodeQL v3
```
### Scopes
Common scopes: `rust-auth`, `elysia`, `react`, `scraper`, `infra`, `ci`, `deps`
Common scopes: `scraper`, `infra`, `ci`, `deps`
## Pull Request Process
@@ -202,8 +160,7 @@ Common scopes: `rust-auth`, `elysia`, `react`, `scraper`, `infra`, `ci`, `deps`
3. **Run checks locally** before pushing:
```bash
cd apps/react && npx tsc --noEmit
eslint . --no-error-on-unmatched-pattern
bun run check
```
4. **Push and open a PR** against `main`. CI will automatically run:
@@ -218,7 +175,7 @@ Common scopes: `rust-auth`, `elysia`, `react`, `scraper`, `infra`, `ci`, `deps`
- Updates compose manifests to use the new SHA tags
6. **Deployment Pipeline** triggers after a successful Docker build:
- SSHes into the VPS (`orange`, Tailscale IP `100.96.248.86`)
- SSHes into the VPS (`orangevps`, Tailscale IP `100.79.111.61`)
- Pulls updated Docker images
- Recreates only the changed containers
- All services share the `app-shared-net` Docker network
@@ -227,13 +184,4 @@ Common scopes: `rust-auth`, `elysia`, `react`, `scraper`, `infra`, `ci`, `deps`
## Adding a New Service
See `docs/add-new-app.md` for the complete step-by-step guide. In summary:
1. Create the app in `apps/<name>`
2. Add it as a Git submodule in `.gitmodules`
3. Register it in `infra/compose/<name>.yml`
4. Add a Dockerfile at `infra/docker/<name>.Dockerfile`
5. Add Traefik routing config in `infra/traefik/dynamic/apps.yaml`
6. Add CI entries in `.github/workflows/docker-build-push.yml`
7. Add compose file to the deploy script in `deploy-docker.yml`
8. Add any required GitHub secrets for the service
See `docs/add-new-app.md` for the complete step-by-step guide.
+1 -11
View File
@@ -1,4 +1,4 @@
.PHONY: help dev lint format test clean update-submodules deploy init-submodules status
.PHONY: help dev update-submodules deploy init-submodules status
SHELL := /bin/bash
@@ -8,16 +8,6 @@ help: ## Show this help
dev: ## Start development infrastructure (Redis etc.)
docker compose -f infra/compose/shared.yml up -d
lint: ## Run Biome linter
biome lint .
format: ## Format code with Biome
biome format --write .
clean: ## Clean build artifacts
rm -rf apps/*/dist apps/*/.next apps/*/target 2>/dev/null || true
rm -rf node_modules 2>/dev/null || true
update-submodules: ## Update all git submodules to latest remote
git submodule update --remote --merge --recursive
+177 -150
View File
@@ -1,205 +1,232 @@
# Asepharyana Hub
# Architecture
Hub repo untuk ekosistem portfolio dan layanan pendukung milik Asep Haryana Saputra.
Aplikasi dipisah sebagai submodule agar frontend, API, dan service pendukung bisa dikembangkan serta di-deploy secara independen.
## Hub Repository Structure Overview
## Services
| Service | Path | Default Local Port | Notes |
| :---------- | :--------------- | :----------------- | :---------------------------------------------------------------------------- |
| Rust API | `apps/rust-auth` | `4091` | API utama (Axum + SeaORM), scraping, image proxy/cache, metrics, OpenAPI docs |
| Elysia API | `apps/elysia` | `4092` | API realtime/auth/chat/quiz (Elysia + Bun + Drizzle + Redis) |
| React Web | `apps/react` | `3000` | Frontend React/Vite |
| Scraper | `apps/scraper` | — | Web scraper service |
## Infrastructure
File compose berada di `infra/compose/`:
- `traefik.yml`: reverse proxy Traefik untuk semua layanan.
- `shared.yml`: Redis.
- `rust-auth.yml`, `elysia.yml`, `react.yml`, `scraper.yml`: manifest deploy per service (image GHCR bertag SHA).
Dockerfile per service berada di `infra/docker/`.
## Docker Image Builds
Build image via Dockerfile:
```bash
docker build -f infra/docker/rust.Dockerfile -t rust-auth:latest .
docker build -f infra/docker/elysia.Dockerfile -t elysia-api:latest .
docker build -f infra/docker/react.Dockerfile -t react-web:latest .
docker build -f infra/docker/scraper.Dockerfile -t scraper-api:latest .
```diff
asepharyana-hub/
├── apps/ # Application services (Git submodules)
│ └── scraper/ # Web scraper service
├── docs/ # Documentation
│ ├── adr/ # Architecture Decision Records
│ ├── add-new-app.md # Guide for adding new services
│ └── superpowers/ # Project capabilities tracking
├── infra/ # Infrastructure as code
│ ├── compose/ # Docker Compose files per service
│ ├── config/ # Infrastructure configuration
│ ├── docker/ # Dockerfiles per service
│ └── traefik/ # Traefik reverse proxy config
│ └── dynamic/ # Dynamic routing rules (YAML)
├── scripts/ # Utility scripts
│ ├── git-hooks/ # Git hook scripts
│ ├── cleanup-ghcr.sh # GHCR image cleanup
│ └── update-deps.sh # Dependency update helper
├── .github/workflows/ # CI/CD pipelines
├── eslint.config.mjs # Root ESLint config
├── package.json # Root formatting/lint helper scripts
└── .prettierrc # Prettier formatting rules
```
Tag and push:
## Technology Stack
### Services
|| Service | Path | Language/Runtime | Framework | Database | Key Libraries |
||---------|----------------|------------------|-----------|----------|---------------|
|| **scraper** | `apps/scraper` | — | — | — | — |
### Infrastructure
|| Component | Technology | Purpose |
||---------------------|-------------------------|------------------------------------------------------------------|
|| Reverse Proxy | Traefik v3.6 | TLS termination, routing, middleware (rate-limit, headers, auth) |
|| Container Runtime | Docker + Docker Compose | Service isolation and orchestration |
|| Container Registry | GHCR (ghcr.io) | Docker image storage |
|| Networking | Tailscale | Secure overlay network between VPS nodes |
|| Message Bus | NATS + JetStream | Event-driven pub/sub, job queues, streaming |
|| Runtime Sidecar | Dapr | Service invocation, pub/sub abstraction, state management |
|| Cache & State | Redis (Alpine) | Session store, rate limit counters, caching, Dapr state store |
|| CI/CD | GitHub Actions | Build, test, deploy automation |
### Infrastructure
### Traefik Reverse Proxy
Traefik runs as the entry point for all HTTP/S traffic. It is configured via:
- **Static config**: CLI arguments in `infra/compose/traefik.yml` — entry points, providers, plugins
- **Dynamic config**: `infra/traefik/dynamic/` — routers, services, middlewares, TLS
- **Docker provider**: Auto-discovers containers with `traefik.enable=true` labels
- **File provider**: Loads `apps.yaml` (routers/services), `middlewares.yaml`, `ssl.yaml`
Key middleware chains (`infra/traefik/dynamic/middlewares.yaml`):
- `secure-headers` — SSL redirect, HSTS, XSS protection, CSP
- `compress` — Gzip compression for responses over 256 bytes
- `rate-limit` — 100 avg / 50 burst requests
- `buffer` — 10MB request/response body limit
- `block-sensitive-paths` — blocks `.env`, `.git`, `/wp-admin` etc.
- `common-chain` — composes secure-headers + compress + retry + rate-limit + buffer
All services route through Traefik on port 443 (TLS), with automatic HTTP-to-HTTPS redirect.
### Docker Compose
Each service has its own Compose file under `infra/compose/`. All services join the `app-shared-net` external Docker network, enabling inter-service communication by container name.
Shared services:
- `infra/compose/shared.yml` — Redis (alias: `redis`)
- `infra/compose/traefik.yml` — Traefik reverse proxy
Service compose files are combined during deployment:
```bash
SHORT_SHA=$(git rev-parse --short HEAD)
docker tag rust-auth:latest ghcr.io/asepharyana/asepharyana-hub/rust-auth:sha-$SHORT_SHA
docker push ghcr.io/asepharyana/asepharyana-hub/rust-auth:sha-$SHORT_SHA
# repeat for elysia-api, react-web, scraper-api
docker compose -f traefik.yml -f shared.yml -f scraper.yml up -d
```
## Local Development
### Tailscale Networking
### 1) Jalankan dependency bersama
### Arsitektur
Semua VPS terhubung via **Tailscale**. Setiap VPS punya IP Tailscale dan service berkomunikasi antar VPS melalui Tailscale network (`100.64.0.0/10`). Container-to-Tailscale connectivity requires a systemd service that adds a route to the main routing table:
```bash
docker compose -f infra/compose/shared.yml up -d
ip route add 100.64.0.0/10 dev tailscale0 table main
```
### 2) Jalankan service yang dibutuhkan
This is managed by `/etc/systemd/system/tailscale-routes.service` on the `orangevps` VPS.
```bash
# Rust API
cd apps/rust-auth
cargo run
### Data Flow
# Elysia API
cd apps/elysia
bun install
bun run dev
### Request Flow (Production)
# React web
cd apps/react
npm install
npm run dev
```mermaid
sequenceDiagram
participant User as Browser/Client
participant DNS as Cloudflare DNS
participant Traefik as Traefik Proxy
participant App as Application Container
participant DB as PostgreSQL (imrnes via Tailscale)
participant Redis as Redis (imrnes via Tailscale)
User->>DNS: asepharyana.my.id
DNS->>User: A/AAAA record → orangevps VPS IP
User->>Traefik: HTTPS request :443
Traefik->>Traefik: TLS termination
Traefik->>Traefik: Middleware chain (headers, rate-limit, buffer)
Traefik->>App: HTTP reverse-proxy (internal network)
alt Database query
App->>DB: sqlx/Drizzle query via Tailscale
DB-->>App: Result set
else Cache lookup
App->>Cache: GET/SET via Tailscale
Cache-->>App: Cached value
end
App-->>Traefik: HTTP response
Traefik-->>User: HTTPS response
```
## API Docs and Monitoring
### CI/CD Pipeline
- Rust OpenAPI: `/docs`
- Elysia Swagger: `/docs`
- Elysia AsyncAPI viewer: `/docs-ws`
```mermaid
flowchart LR
A[Push to main] --> B{Changed paths?}
B -->|apps/** or infra/docker/**| C[Build Docker Images]
B -->|infra/compose/**| D[Deploy to VPS]
B -->|apps/*/src/**/*.ts| E[Lint + TypeCheck]
C --> F[Push to GHCR]
F --> G[Update Compose tags]
G --> D
D --> H[SSH into VPS]
H --> I[Pull images]
I --> J[docker compose up -d]
subgraph "Build Phase"
C
F
G
end
subgraph "Deploy Phase"
D
H
I
J
end
```
### Deployment Architecture
### Image Tags
- `latest` — mutable, for convenience
- `sha-<short-sha>` — immutable, for deterministic rollbacks
- Build cache: `sha-<short>-buildcache`
Registry: `ghcr.io/asepharyana/asepharyana-hub/<service>`
## Deployment Notes
- Pipeline memakai image tag berbasis commit SHA (`sha-<short-sha>`), bukan `latest`.
- Deploy Compose sekarang mencakup `infra/compose/*.yml` dan `deploy-docker.yml` akan berjalan langsung ketika `infra/compose/**` berubah.
- Selective deployment: hanya compose file yg berubah yang di-redeploy.
## Networking & Tailscale
### Arsitektur
Semua VPS terhubung via **Tailscale**. Setiap VPS punya IP Tailscale dan service berkomunikasi antar VPS melalui Tailscale network (`100.64.0.0/10`).
| VPS | Tailscale IP | Service |
| :---------------- | :-------------- | :------------------------------------- |
| `imrnes` | `100.108.1.124` | PostgreSQL (`hub`), Redis |
| `orange` | `100.96.248.86` | App containers (Traefik, 9Router, dll) |
| `archlinux` | `100.114.19.66` | _(development machine)_ |
| `laptop-2f6e1iph` | `100.86.195.29` | _(offline)_ |
### Container → Tailscale Connectivity
Docker containers di bridge network (`app-shared-net`) **tidak otomatis bisa access Tailscale IPs** karena Tailscale menggunakan **custom policy routing** (routes di `table 52`, bukan `main` table).
#### Fix: Tailscale Route di Main Table
Agar container bisa reach Tailscale IPs (untuk DB, Redis, dll), tambahkan route ke `main` routing table:
Semua VPS terhubung via **Tailscale**. Setiap VPS punya IP Tailscale dan service berkomunikasi antar VPS melalui Tailscale network (`100.64.0.0/10`). Container-to-Tailscale connectivity requires a systemd service that adds a route to the main routing table:
```bash
# Manual (hilang setelah reboot)
ip route add 100.64.0.0/10 dev tailscale0 table main
# Persistent (systemd service)
# Sudah dikonfigurasi sebagai /etc/systemd/system/tailscale-routes.service
# Service ini berjalan otomatis setelah tailscaled start
systemctl enable tailscale-routes.service
systemctl start tailscale-routes.service
```
#### Environment Variables
This is managed by `/etc/systemd/system/tailscale-routes.service` on the `orangevps` VPS.
### Environment Variables
Service yang connect ke Tailscale IP:
```env
# PostgreSQL di imrnes
DATABASE_URL=postgres://user:pass@100.108.1.124:5432/dbname
DATABASE_URL=postgres://user:***@100.121.180.82:6432/dbname
# Redis di imrnes
REDIS_URL=redis://100.108.1.124:6379
REDIS_URL=redis://100.121.180.82:6379
```
#### Persistent Systemd Service
## Submodule Strategy
File: `/etc/systemd/system/tailscale-routes.service`
Each application lives in its own Git repository and is imported as a submodule into `apps/`. This approach:
```ini
[Unit]
Description=Add Tailscale routes to main routing table
After=tailscaled.service
Requires=tailscaled.service
- **Enables independent development** — each service can be developed, tested, and versioned separately
- **Pins exact commits** — the super-repository tracks exact submodule SHAs, enabling reproducible deployments
- **Supports `repository_dispatch`** — when a submodule receives a push, it can trigger the super-repository to build and deploy only that service
[Service]
Type=oneshot
ExecStart=/bin/bash -c '/usr/sbin/ip route add 100.64.0.0/10 dev tailscale0 table main 2>/dev/null || /usr/sbin/ip route replace 100.64.0.0/10 dev tailscale0 table main'
RemainAfterExit=yes
### Submodule Lifecycle
[Install]
WantedBy=multi-user.target
```
1. Developer pushes to a submodule (e.g., `apps/scraper`)
2. Submodule's GitHub Action dispatches `repository_dispatch` to the super-repo with the service name and new SHA
3. Super-repo detects the dispatch, waits for the SHA to be fetchable, then builds only that service
4. The compose manifest is updated and committed with the new SHA tag
5. The deploy workflow runs and updates only the changed containers
Install & enable:
### Updating Submodules
```bash
sudo tee /etc/systemd/system/tailscale-routes.service > /dev/null << 'EOF'
[Unit]
Description=Add Tailscale routes to main routing table
After=tailscaled.service
Requires=tailscaled.service
[Service]
Type=oneshot
ExecStart=/bin/bash -c '/usr/sbin/ip route add 100.64.0.0/10 dev tailscale0 table main 2>/dev/null || /usr/sbin/ip route replace 100.64.0.0/10 dev tailscale0 table main'
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable tailscale-routes.service
sudo systemctl start tailscale-routes.service
# Update a single submodule to latest
cd apps/scraper
git checkout main
git pull
cd ../..
git add apps/scraper
git commit -m "chore(scraper): update submodule to latest"
```
#### Troubleshooting
```bash
# Cek Tailscale peers
tailscale status
# Cek route table 52 (Tailscale internal)
ip route show table 52
# Cek route table main (yang dipakai container)
ip route show table main | grep 100.
# Test connectivity dari dalam container
docker exec <container> node -e "
const net = require('net');
const c = new net.Socket();
c.setTimeout(5000);
c.connect(5432, '100.108.1.124', () => { console.log('OK'); c.end(); });
c.on('error', e => { console.log('FAIL:', e.code); });
c.on('timeout', () => { console.log('TIMEOUT'); c.destroy(); });
"
# Cek service tailscale-routes
systemctl status tailscale-routes.service
```
## Menambahkan Aplikasi Baru
Panduan langkah demi langkah untuk menambahkan aplikasi baru ada di `docs/add-new-app.md`.
## License
MIT
## Test Section
This is a test for PR-Agent auto-review.
Submodule apps/elysia deleted from b8f4b5806e
Submodule
+1
Submodule apps/hub added at b007ced615
Submodule
+1
Submodule apps/llm-api added at 81c5177249
Submodule apps/react deleted from 124d2f88bd
Submodule apps/rust-auth deleted from 6e695f07e8
Submodule
+1
Submodule apps/tools added at 036f67d05a
+238
View File
@@ -0,0 +1,238 @@
# Arsitektur asepharyana-hub
## Topologi Fisik
Dua node terhubung via **Tailscale** overlay network:
```
┌──────────────────────────────┐ ┌──────────────────────────────┐
│ orangevps (VPS) │ │ imrnes (Bare-metal) │
│ IP: 45.127.35.244 │ │ Tailscale: 100.121.180.82 │
│ Tailscale: 100.x.x.x │◄──────┤ │
│ │ │ Layanan: │
│ Layanan: │ │ ├─ PostgreSQL (port 6432) │
│ ├─ Traefik (port 80/443) │ │ └─ Redis (port 6379) │
│ ├─ NATS + JetStream │ │ │
│ ├─ Dapr Placement │ └──────────────────────────────┘
│ ├─ Redis (cache, Dapr) │
│ ├─ Scraper API + Dapr │
│ └─ Hub (Next.js SPA) │
└──────────────────────────────┘
```
### Konektivitas Container ke Tailscale
Container di `orangevps` tidak bisa langsung mencapai IP Tailscale (`100.x.x.x`). Route Tailscale harus ditambahkan ke tabel routing utama (`main`) via `tailscale-routes.service` agar traffic dari container bisa melewati host ke Tailscale.
## Alur Request HTTP (External)
```
Internet
▼ Port 443
Traefik (v3.6)
├─ TLS termination (sertifikat dari volume mount)
├─ Middleware chain: secure-headers → compress → retry → rate-limit → buffer
├─ Plugin: real-ip (Cloudflare), block-sensitive-paths
▼ Router matching
Host(`asepharyana.my.id`) || Host(`www.asepharyana.my.id`) → hub
host(`hub.asepharyana.my.id`) → hub (SPA + dashboard)
Host(`scraper.asepharyana.my.id`) || Host(`api.asepharyana.my.id`) → scraper-api
├─ hub (Next.js, port 3000)
│ ├─ / — Portfolio SPA
│ ├─ /dashboard — Ops dashboard (client-side, auto-refresh 15s)
│ ├─ /api/dashboard — JSON: Docker containers, Jaeger traces, Prometheus metrics
│ └─ Docker socket mounted (:ro) for container discovery
▼ Service load balancer
http://scraper-api:4091
Scraper API (Rust / Axum)
├─ Health check: GET /, respon 200
├─ REST endpoints
├─ Database via `DATABASE_URL` (Tailscale → PostgreSQL di imrnes)
├─ Cache via `REDIS_URL` (Redis lokal di container)
└─ Pub/sub via Dapr sidecar (localhost:3500)
```
## Infrastruktur Internal
### Docker Compose Project
Semua service berjalan dalam satu Docker Compose project bernama `compose` dan bergabung di network `app-shared-net`:
| File | Service | Peran |
|------|---------|-------|
| `traefik.yml` | `traefik` | Reverse proxy + TLS + metrics Prometheus |
| `shared.yml` | `redis` | Cache, session store, backend Dapr pub/sub & state |
| `nats.yml` | `nats` | Message broker + JetStream persistent streaming |
| `dapr.yml` | `dapr-placement` | Koordinasi actor placement untuk sidecar Dapr |
| `scraper.yml` | `scraper-api` + `scraper-api-dapr` | Aplikasi Rust + sidecar Dapr |
| `hub.yml` | `hub` | Next.js SPA portfolio + dashboard + Docker socket |
| `observability.yml` | `otel-collector`, `jaeger`, `prometheus`, `node-exporter` | Tracing, metrics, observability |
### Dapr Sidecar Pattern
Setiap aplikasi yang menggunakan Dapr mendapat sidecar container `daprd`:
```
┌─────────────────────┐
│ scraper-api │
│ (app port 4091) │
└────────┬────────────┘
│ localhost:3500 (HTTP)
│ localhost:50001 (gRPC)
┌────────▼────────────┐
│ scraper-api-dapr │
│ (daprd sidecar) │
│ │
│ Dapr components: │
│ ├─ pubsub.redis │
│ └─ state.redis │
└─────────────────────┘
```
Komponen Dapr:
| Komponen | Tipe | Backend |
|----------|------|---------|
| `pubsub` | `pubsub.redis` | `redis:6379` |
| `statestore` | `state.redis` | `redis:6379` (prefix `dapr`) |
### Monitoring & Auto-Discovery
#### Prometheus Docker Auto-Discovery
Prometheus menggunakan `docker_sd_configs` untuk auto-detect container yang perlu di-scrape. Cukup tambah label pada container:
```yaml
labels:
- 'prometheus.io/scrape=true'
- 'prometheus.io/port=8080' # port metrics endpoint
- 'prometheus.io/path=/metrics' # optional, default /metrics
```
Prometheus akan auto-detect dan mulai scrape container dalam 15 detik.
#### Traefik Metrics
Traefik mengekspos metrics Prometheus di port 8080 (`--metrics.prometheus=true`). Metrics yang tersedia:
| Metric | Query untuk dashboard |
|--------|----------------------|
| Request rate | `sum(rate(traefik_service_requests_total[1m]))` |
| Latency | `avg(traefik_service_request_duration_seconds_sum / traefik_service_request_duration_seconds_count) * 1000` |
| Error rate | `sum(rate(traefik_service_requests_total{code=~"5.."}[1m]))` |
Dashboard di `/api/dashboard` returns node metrics + Traefik range data untuk 4 sparkline charts (RPS, latency, errors, trace volume).
#### Docker Socket Access
Container yang perlu akses Docker socket (`/var/run/docker.sock`) harus punya group docker (GID 988):
```yaml
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
group_add:
- '988'
```di compose atau `--group-add 988` via CLI. Berlaku untuk `hub` (container list) dan `prometheus` (Docker SD).
### NATS + JetStream
NATS berjalan dengan flag `-js` untuk mengaktifkan JetStream. Persistent stream disimpan di volume `nats_data`. Dapr pub/sub routing:
```
Service → Dapr sidecar (pubsub.redis) → Redis streams
```
> **Catatan:** Saat ini Dapr pub/sub menggunakan Redis, bukan NATS. Jika ingin migrasi ke NATS untuk pub/sub, komponen Dapr perlu diganti dengan `pubsub.nats`.
## Arsitektur CI/CD
```
Push ke main (apps/**, infra/**)
docker-build-push.yml
├─ Phase 1: Detect changed services
├─ Phase 2: Build & Push image ke GHCR
└─ Phase 3: Update compose manifest + submodule pointer
▼ (workflow_run trigger)
deploy-docker.yml
├─ SSH ke orangevps
├─ Git sync, pull images
├─ Remove stale containers
└─ Selective restart service
```
Submodule update dari remote repo via `repository_dispatch`:
```
Push ke asepharyana-hub-scraper
▼ (repository_dispatch)
update-submodule.yml
├─ Update submodule pointer
└─ Commit & push ke hub repo
▼ (repository_dispatch trigger)
docker-build-push.yml
└─ Build, push, deploy
```
## Image Tagging Strategy
| Tag | Contoh | Penggunaan |
|-----|--------|------------|
| `sha-<short>` | `sha-a3c5d74` | Immutable, deterministic rollback |
| `latest` | `latest` | Mutable, convenience |
| `buildcache` | `sha-a3c5d74-buildcache` | Registry-based build cache (internal) |
## Networking
### Port Map
| Port | Service | Deskripsi |
|------|---------|-----------|
| 443 | Traefik | HTTPS eksternal |
| 80 | Traefik | Redirect ke HTTPS |
| 4222 | NATS | Client connections |
| 8222 | NATS | HTTP monitor / health |
| 6379 | Redis | Internal container network |
| 3500 | Dapr sidecar | Dapr HTTP API (per service) |
| 50001 | Dapr sidecar | Dapr gRPC API (per service) |
| 50005 | Dapr placement | Actor placement |
| 4091 | Scraper API | Aplikasi HTTP |
## Event Topics Convention
Semua event menggunakan prefix `hub.`:
| Topic | Payload | Deskripsi |
|-------|---------|-----------|
| `hub.image.cached` | `{original_url, cdn_url, source}` | Image selesai di-cache |
| `hub.image.repaired` | `{old_url, new_url}` | CNAME image diperbaiki |
| `hub.scrape.anime.done` | `{source, slug, duration}` | Scrape anime selesai |
| `hub.system.alert` | `{service, level, message}` | Error/alert dari service |
## Service Registry (Traefik)
Domain routing:
| Subdomain | Service | URL Backend |
|-----------|---------|-------------|
| `asepharyana.my.id` (root) | Hub SPA + dashboard | `http://hub:3000` |
| `www.*` | Hub (alias) | `http://hub:3000` |
| `hub.*` | Hub (alias) | `http://hub:3000` |
| `scraper.*` | Scraper API | `http://scraper-api:4091` |
| `api.*` | Scraper API (alias) | `http://scraper-api:4091` |
| `traefik.*` | Traefik Dashboard | `api@internal` |
| `jaeger.*` | Jaeger UI | `http://jaeger:16686` |
Semua domain tersedia di:
- `<service>.asepharyana.my.id`
- `<service>.asepharyana.web.id`
+681
View File
@@ -0,0 +1,681 @@
# Deployment Guide
Panduan deploy aplikasi apapun menggunakan **Docker + Docker Compose + GitHub Actions + VPS**.
## Arsitektur
```
GitHub Repo ──► GitHub Actions ──► Registry (GHCR / Docker Hub / ECR / dll.)
VPS (<VPS_HOST>)
docker compose pull + up
```
## Prerequisites
- Docker Engine >= 24.x
- Docker Compose v2 (plugin)
- Git
- Akun GitHub dengan akses repo
- SSH key di `~/.ssh/<KEY_NAME>` (default: `id_ed25519`)
## Konfigurasi VPS Target
Buat berkas `~/orangevps` (atau sesuaikan dengan env Anda):
```text
ssh <USER>@<VPS_HOST>
```
Contoh isi `~/orangevps`:
```text
ssh root@45.127.35.244
```
| Parameter | Nilai | Contoh |
|-----------|-------|--------|
| User | `<USER>` | `root` |
| Host | `<VPS_HOST>` | `45.127.35.244` |
| SSH Key | `~/.ssh/<KEY_NAME>` | `~/.ssh/id_ed25519` |
| Target Dir di VPS | `<VPS_TARGET_DIR>` | `/opt/app` atau `/root/app` |
> Tip: Jika SSH key menggunakan nama selain default, sesuaikan path dan `ssh -i` sesuai.
## Registry
Pilih registry untuk menyimpan image Docker. Sesuaikan dengan proyek:
| Registry | URL | Auth |
|----------|-----|------|
| GitHub Container Registry | `ghcr.io` | `GITHUB_TOKEN` |
| Docker Hub | `docker.io` | username / PAT |
| AWS ECR | `<account>.dkr.ecr.<region>.amazonaws.com` | `aws ecr get-login-password` |
| Google GCR | `gcr.io` | `gcloud auth print-access-token` |
| Azure ACR | `<registry>.azurecr.io` | `az acr login` |
Contoh namespace untuk GHCR:
```text
Registry : ghcr.io
Namespace: <GITHUB_USERNAME_OR_ORG>
Repo : <REPO_NAME>
```
Pastikan package/visibility di registry mengizinkan akses pull dari VPS.
---
## Deploy Otomatis (Recommended)
Gunakan GitHub Actions untuk otomatisasi build, push, dan deploy.
### Workflow 1: Build dan Push Image
File: `.github/workflows/docker-build-push.yml`
```yaml
name: Build and Push Docker Images
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/setup-buildx-action@v4
- uses: docker/build-push-action@v7
with:
context: .
file: Dockerfile
push: true
tags: |
ghcr.io/${{ github.repository }}/<SERVICE_NAME>:latest
ghcr.io/${{ github.repository }}/<SERVICE_NAME>:sha-${{ github.sha }}
cache-from: type=registry,ref=ghcr.io/${{ github.repository }}/<SERVICE_NAME>:buildcache
cache-to: type=registry,ref=ghcr.io/${{ github.repository }}/<SERVICE_NAME>:buildcache,mode=max
```
Ubah `<SERVICE_NAME>` sesuai service (misal: `app`, `web`, `api`). Jika monorepo, gunakan matrix strategy untuk build beberapa service sekaligus.
### Workflow 2: Deploy ke VPS
File: `.github/workflows/deploy-docker.yml`
```yaml
name: Deploy Docker to VPS
on:
workflow_run:
workflows: ['Build and Push Docker Images']
types: [completed]
push:
branches: [main]
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Deploy to VPS
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
VPS_HOST: ${{ secrets.VPS_HOST }}
VPS_USER: ${{ secrets.VPS_USER }}
VPS_TARGET_DIR: ${{ secrets.VPS_TARGET_DIR }}
ENV_FILE_PRODUCTION: ${{ secrets.ENV_FILE_PRODUCTION }}
run: |
set -euo pipefail
mkdir -p ~/.ssh
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh-keyscan -H -t ed25519,rsa "$VPS_HOST" >> ~/.ssh/known_hosts
SSH_OPTS=(-o ControlMaster=auto -o ControlPath=/tmp/ssh-%r@%h:%p -o ControlPersist=600 -o StrictHostKeyChecking=yes)
ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" "mkdir -p $VPS_TARGET_DIR && mkdir -p $VPS_TARGET_DIR/infra/compose"
echo "$ENV_FILE_PRODUCTION" > .env.prod
scp "${SSH_OPTS[@]}" .env.prod "$VPS_USER@$VPS_HOST:$VPS_TARGET_DIR/.env"
ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" bash -s <<'EOF'
set -euo pipefail
cd "$VPS_TARGET_DIR"
docker network inspect app-shared-net >/dev/null 2>&1 || docker network create app-shared-net
if [ ! -d ".git" ]; then
git init
git remote add origin https://github.com/<GITHUB_USER>/<REPO_NAME>.git
fi
git fetch origin main --depth=1 || true
git reset --hard FETCH_HEAD
docker compose --env-file .env pull
docker compose --env-file .env up -d --remove-orphans
EOF
```
### Secrets GitHub yang Diperlukan
Buka **Settings > Secrets and variables > Actions**:
| Secret | Deskripsi |
|--------|-----------|
| `SSH_PRIVATE_KEY` | Isi dengan `cat ~/.ssh/<KEY_NAME>` |
| `VPS_HOST` | IP atau domain VPS |
| `VPS_USER` | User SSH (misal: `root`, `ubuntu`, `deploy`) |
| `VPS_TARGET_DIR` | Direktori aplikasi di VPS |
| `ENV_FILE_PRODUCTION` | Isi dengan environment production |
### Trigger Manual
```bash
gh workflow run deploy-docker.yml
```
---
## Dockerfile Patterns
Pilih pattern sesuai jenis aplikasi.
### Pattern 1: Multi-stage Build (SPA / static assets)
```dockerfile
FROM oven/bun:1 AS builder
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
RUN bun run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
### Pattern 2: Single-stage (runtime image)
```dockerfile
FROM oven/bun:1
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install --frozen-lockfile
COPY . .
EXPOSE 3000
CMD ["bun", "run", "start"]
```
### Pattern 3: Compiled binary (Rust / Go / Zig)
```dockerfile
FROM rust:1 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release
FROM debian:bookworm-slim
COPY --from=builder /app/target/release/app /usr/local/bin/app
EXPOSE 8080
CMD ["app"]
```
---
## Docker Compose Patterns
### Single service
```yaml
services:
app:
container_name: app
image: registry.example.com/org/app:latest
restart: always
ports:
- "3000:3000"
environment:
- NODE_ENV=production
```
### Multi-service dengan shared network
```yaml
services:
app:
container_name: app
image: registry.example.com/org/app:latest
restart: always
networks: [app-shared-net]
redis:
container_name: redis
image: redis:7-alpine
restart: always
networks: [app-shared-net]
networks:
app-shared-net:
name: app-shared-net
external: true
```
### Dengan reverse proxy (Traefik / Caddy / Nginx)
```yaml
services:
app:
container_name: app
image: registry.example.com/org/app:latest
restart: always
networks: [app-shared-net]
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.app.rule=Host(`app.example.com`)'
- 'traefik.http.routers.app.entrypoints=websecure'
- 'traefik.http.routers.app.tls=true'
- 'traefik.http.services.app.loadbalancer.server.port=3000'
networks:
app-shared-net:
name: app-shared-net
external: true
```
---
## Deploy Manual (Lokal)
### 1. Build dan Push ke Registry
Login ke registry:
```bash
echo $GITHUB_TOKEN | docker login ghcr.io -u <GITHUB_USERNAME> --password-stdin
```
Build dan push:
```bash
docker build -t ghcr.io/<GITHUB_USERNAME>/<REPO_NAME>/<SERVICE_NAME>:latest -f Dockerfile .
docker push ghcr.io/<GITHUB_USERNAME>/<REPO_NAME>/<SERVICE_NAME>:latest
```
Tag tambahan dengan SHA commit:
```bash
SHORT_SHA=$(git rev-parse --short HEAD)
docker tag ghcr.io/<GITHUB_USERNAME>/<REPO_NAME>/<SERVICE_NAME>:latest \
ghcr.io/<GITHUB_USERNAME>/<REPO_NAME>/<SERVICE_NAME>:sha-${SHORT_SHA}
docker push ghcr.io/<GITHUB_USERNAME>/<REPO_NAME>/<SERVICE_NAME>:sha-${SHORT_SHA}
```
### 2. Pull dan Deploy di VPS
SSH ke VPS:
```bash
ssh -i ~/.ssh/<KEY_NAME> <USER>@<VPS_HOST>
```
Clone repo (jika belum):
```bash
git clone https://github.com/<GITHUB_USER>/<REPO_NAME>.git <VPS_TARGET_DIR>
cd <VPS_TARGET_DIR>
```
Buat shared network (hanya sekali):
```bash
docker network create app-shared-net
```
Siapkan environment:
```bash
cp .env.example .env
# Edit .env sesuai nilai production
nano .env
```
Login ke registry di VPS:
```bash
echo $GITHUB_TOKEN | docker login ghcr.io -u <GITHUB_USERNAME> --password-stdin
```
Pull gambar terbaru:
```bash
cd <VPS_TARGET_DIR>
docker compose -f docker-compose.yml --env-file .env pull
```
Deploy (up):
```bash
docker compose -f docker-compose.yml --env-file .env up -d --remove-orphans
```
Verifikasi:
```bash
docker compose -f docker-compose.yml ps
docker compose -f docker-compose.yml logs -f <SERVICE_NAME>
```
---
## Deployment Order (Manual)
Jika deploy bertahap, gunakan urutan ini:
```bash
# 1. Shared services (Redis, database, dll.)
docker compose -f infra/compose/shared.yml up -d
# 2. Reverse proxy
docker compose -f infra/compose/traefik.yml up -d
# 3. Aplikasi
docker compose \
-f infra/compose/app1.yml \
-f infra/compose/app2.yml \
up -d
```
---
## Perintah Berguna di VPS
```bash
# Lihat semua container
docker ps -a
# Log service
docker logs -f <container_name>
# Restart satu service
docker compose -f <compose_file> up -d --force-recreate
# Hapus network lama (hati-hati)
docker network rm app-shared-net
docker network create app-shared-net
# Bersihkan image unused
docker image prune -a -f
docker system prune -a -f
```
---
## Troubleshooting
### Image tidak bisa di-pull
Pastikan sudah login ke registry di VPS:
```bash
docker logout ghcr.io
echo $GITHUB_TOKEN | docker login ghcr.io -u <GITHUB_USERNAME> --password-stdin
```
Periksa visibility package di registry (harus `Public` atau akses diberikan).
### Port sudah dipakai
```bash
docker ps | grep :80
docker ps | grep :443
```
### Reverse proxy tidak routing
Periksa label di compose file dan pastikan shared network ada:
```bash
docker network inspect app-shared-net
docker logs traefik
```
---
## Environment Variable Management
### Pola 1: `.env` di VPS (recommended untuk production)
```bash
# Di VPS
cd <VPS_TARGET_DIR>
cp .env.example .env
# Edit sesuai production
nano .env
```
CI/CD upload `.env` via secret, tidak simpan di repo.
### Pola 2: Docker secrets (Swarm mode)
```yaml
services:
app:
image: app:latest
secrets:
- db_password
secrets:
db_password:
file: ./secrets/db_password.txt
```
### Pola 3: External secret manager
- **HashiCorp Vault**: inject via env atau file
- **AWS Secrets Manager**: `aws secretsmanager get-secret-value`
- **Doppler / Infisical**: unified secret management
---
## Tagging dan Versioning
### Strategy yang umum
| Strategy | Contoh tag | Kegunaan |
|----------|-----------|----------|
| Latest + SHA | `latest`, `sha-abc1234` | CI/CD cepat, traceable |
| SemVer | `1.2.3`, `1.2`, `1` | Release publik |
| Git tag mirror | `v1.2.3` | Sync dengan git tag |
| Branch mirror | `main`, `develop` | Preview / staging |
### Contoh git tag driven deploy
```bash
git tag v1.2.3
git push origin v1.2.3
```
CI/CD membaca tag, build image dengan tag yang sama, dan deploy.
---
## Rollback
### Rollback via registry
```bash
# Lihat tag yang tersedia
docker manifest inspect ghcr.io/org/app:latest
# atau lihat UI registry
# Di VPS, edit compose file ke tag sebelumnya
# lalu:
docker compose --env-file .env pull
docker compose --env-file .env up -d --remove-orphans
```
### Rollback via git
```bash
git revert HEAD
git push origin main
# CI/CD otomatis build dan deploy versi sebelumnya
```
---
## Health Checks
### Di Dockerfile
```dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
```
### Di Docker Compose
```yaml
services:
app:
image: app:latest
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
```
---
## Monitoring & Observability
```bash
# Log aggregated
docker compose logs -f --tail=100
# Resource usage
docker stats
# Disk usage
docker system df
# Cleanup
docker system prune -a -f
```
---
## Catatan Keamanan
- Jangan commit `.env` atau SSH private key ke repo.
- Gunakan GitHub Secrets (atau secret manager) untuk credential di CI/CD.
- Rotate token dan key secara berkala.
- Batasi akses SSH ke VPS (ubah port default, gunakan fail2ban).
- Set `StrictHostKeyChecking=yes` pada SSH opsional deployment.
---
---
## Proyek Ini: asepharyana-hub
> Dokumentasi spesifik untuk repo ini. Lihat juga [ADR-0002](adr/0002-env-file-via-github-secret.md).
### Topologi
| Host | IP | Peran |
|------|----|-------|
| `orangevps` (VPS) | `45.127.35.244` | Docker host: Traefik, scraper-api, Redis, NATS, Dapr |
| `imrnes` (bare-metal) | `100.121.180.82` (Tailscale) | PostgreSQL (port 6432), Redis (port 6379) |
### Environment Variables
**Production `.env` tidak pernah di-commit.** File ini disimpan sebagai GitHub secret `ENV_FILE_PRODUCTION` dan di-SCP ke VPS saat deploy via `deploy-docker.yml`.
Cara update:
```bash
# Baca current .env dari VPS
ssh root@45.127.35.244 "cat /root/asepharyana-hub/.env"
# Update GitHub secret (dari output di atas)
cat > /tmp/env-updated << 'EOF'
<paste content, edit, lalu>
EOF
cat /tmp/env-updated | gh secret set ENV_FILE_PRODUCTION --repo asepharyana/asepharyana-hub
```
**Jangan manual edit `.env` di VPS tanpa update GitHub secret juga** — nanti ke- overwrite pas deploy berikutnya.
### Database
| Variable | Value |
|----------|-------|
| `DATABASE_URL` | `postgres://asephs:hunterz@100.121.180.82:6432/hub` |
| `REDIS_URL` | `redis://redis:6379` (Docker network) |
### Kompose
Proyek compose bernama `compose`, terdiri dari 5 file yang selalu di-include bersamaan:
```bash
/root/asepharyana-hub/infra/compose/
├── traefik.yml # Reverse proxy
├── shared.yml # Redis
├── nats.yml # NATS
├── dapr.yml # Dapr placement
├── scraper.yml # Scraper API
└── observability.yml # OTel Collector, Jaeger, Dashboard
```bash
cd /root/asepharyana-hub
docker compose \
-p compose \
--env-file .env \
-f infra/compose/traefik.yml \
-f infra/compose/shared.yml \
-f infra/compose/scraper.yml \
-f infra/compose/nats.yml \
-f infra/compose/dapr.yml \
-f infra/compose/observability.yml \
up -d --remove-orphans
```
---
## Checklist Deploy Proyek Baru
1. [ ] Dockerfile ditest lokal (`docker build`, `docker run`)
2. [ ] Docker Compose file valid (`docker compose config`)
3. [ ] `.dockerignore` sesuai (node_modules, .git, .env)
4. [ ] Registry dibuat (GHCR package / Docker Hub repo / ECR / dll.)
5. [ ] GitHub Actions workflow dibuat dengan permission `packages: write`
6. [ ] VPS siap: Docker, Docker Compose, SSH key
7. [ ] Shared network dibuat (`docker network create`)
8. [ ] `.env` production di-VPS atau via secret manager
9. [ ] Reverse proxy (Traefik / Caddy / Nginx) routing ke container
10. [ ] Health check endpoint aktif
+206
View File
@@ -0,0 +1,206 @@
# Development Guide
Panduan setup lingkungan development lokal untuk kontributor `asepharyana-hub`.
## Prasyarat
| Tool | Versi Minimal | Catatan |
|------|---------------|---------|
| Git | 2.40+ | Submodule support |
| Docker | 24+ | Dengan Docker Compose v2 plugin |
| Rust | 1.85+ | Hanya untuk `apps/scraper` |
| Bun | 1.x | Root tooling (Biome) |
| Dapr CLI | 1.14+ | Opsional, untuk development dengan Dapr |
## Setup Awal
```bash
# 1. Clone repo
git clone https://github.com/asepharyana/asepharyana-hub.git
cd asepharyana-hub
# 2. Init submodules
make init-submodules
# 3. Setup environment
cp .env.example .env
# Edit .env sesuai kebutuhan lokal
# 4. Install root dependencies
bun install
```
## Menjalankan Infrastruktur Lokal
Beberapa service membutuhkan Redis. Jalankan dengan:
```bash
make dev
# atau equivalen:
docker compose -f infra/compose/shared.yml up -d
```
Ini akan menjalankan Redis Alpine di `localhost:6379`.
### (Opsional) NATS Lokal
Jika service membutuhkan pub/sub:
```bash
docker compose -f infra/compose/nats.yml up -d
# NATS client: localhost:4222
# NATS monitor: localhost:8222
```
### (Opsional) Dapr Placement Lokal
Jika service membutuhkan sidecar Dapr:
```bash
docker compose -f infra/compose/dapr.yml up -d
# Dapr placement: localhost:50005
```
## Menjalankan Service Lokal
### Scraper API (Rust)
```bash
# Pastikan Redis sudah running (make dev)
cd apps/scraper
# Cargo run
cargo run
# Dengan Dapr sidecar (jika placement running)
dapr run \
--app-id scraper-api \
--app-port 4091 \
--dapr-http-port 3500 \
--resources-path ../../infra/dapr/components \
-- cargo run
```
### Dengan Docker Compose (Full Stack)
Untuk menjalankan semua service sekaligus:
```bash
docker compose \
-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
```
Untuk service baru, tambahkan compose file-nya ke daftar.
## Update Submodules
### Pull latest dari semua submodule
```bash
make update-submodules
# atau:
git submodule update --remote --merge --recursive
```
### Check status submodule
```bash
make status
# atau:
git submodule status
```
### Sync .env ke submodule
```bash
bash scripts/2updateenv.sh
# Copy .env root ke apps/*/
```
## Linting & Formatting
Root repo menggunakan **Biome** untuk linting dan formatting:
```bash
bun run check # Lint + format + write
bun run ci # CI mode (no write, exit code on issues)
bun run lint # Lint only
bun run format # Format only
```
## Build Docker Image Lokal
```bash
# Scraper API
docker build -f infra/docker/scraper.Dockerfile -t scraper-api:local .
# Service baru: tambahkan Dockerfile di infra/docker/
```
## Testing
Saat ini belum ada test runner di root level. Masing-masing submodule mengelola testing sendiri:
```bash
# Scraper API (Rust)
cd apps/scraper && cargo test
```
## Validasi YAML
Sebelum commit perubahan infra, validasi semua file YAML:
```bash
python -c "
import pathlib, yaml
for p in pathlib.Path('infra').rglob('*.yml'):
with open(p) as f: yaml.safe_load(f)
print(f'OK {p}')
for p in pathlib.Path('infra').rglob('*.yaml'):
with open(p) as f: yaml.safe_load(f)
print(f'OK {p}')
"
for f in infra/compose/*.yml; do
docker compose -f "$f" config >/dev/null && echo "OK $f"
done
```
## Git Workflow
### Commit Convention
```
<type>(<scope>): <description>
```
Type: `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `ci`, `perf`, `style`
Scope: `scraper`, `infra`, `ci`, `dapr`, `nats`, `docs`, `deps`, `scripts`, `root`
Contoh:
```
feat(scraper): add image cache endpoint
fix(infra): correct Traefik rate-limit config
chore(deps): bump biome to 2.5.0
```
### Branch Strategy
- `main` — production branch, push triggers CI/CD
- Fitur baru: branch dari `main`, PR ke `main`
- Submodule development: dilakukan di repo masing-masing, hub hanya update pointer
## Deployment ke VPS
Push ke `main` otomatis trigger CI/CD. Untuk trigger manual:
```bash
gh workflow run deploy-docker.yml
```
Lihat `docs/DEPLOYMENT.md` untuk detail.
+162
View File
@@ -0,0 +1,162 @@
# Menambahkan Dapr ke Service Baru
Panduan integrasi Dapr runtime sidecar untuk service di `asepharyana-hub`.
## Prasyarat
- NATS server berjalan (`infra/compose/nats.yml`)
- Dapr placement service berjalan (`infra/compose/dapr.yml`)
## 1. Compose File
Setiap service butuh sidecar container Dapr. Contoh:
```yaml
services:
app:
container_name: app
image: ghcr.io/asepharyana/asepharyana-hub/app:latest
restart: always
depends_on:
dapr-placement:
condition: service_healthy
nats:
condition: service_healthy
networks:
app-shared-net:
aliases:
- app
env_file:
- ../../.env
app-dapr:
container_name: app-dapr
image: daprio/daprd:latest
restart: always
depends_on:
dapr-placement:
condition: service_healthy
nats:
condition: service_healthy
networks:
- app-shared-net
depends_on:
dapr-placement:
condition: service_healthy
nats:
condition: service_healthy
otel-collector:
condition: service_started
networks:
- app-shared-net
command:
- './daprd'
- '--app-id=app'
- '--app-port=3000'
- '--dapr-http-port=3500'
- '--dapr-grpc-port=50001'
- '--placement-host-address=dapr-placement:50005'
- '--config=/dapr/config.yaml'
- '--resources-path=/dapr/components'
volumes:
- ../../infra/dapr:/dapr:ro
networks:
app-shared-net:
name: app-shared-net
external: true
```
## 2. Mengakses Dapr dari Service
### Via HTTP API (semua bahasa)
Sidecar listen di `localhost:3500`:
```bash
# Publish event
curl -X POST http://localhost:3500/v1.0/publish/pubsub/hub.event.type \
-H "Content-Type: application/json" \
-d '{"key": "value"}'
# Service invocation
curl http://localhost:3500/v1.0/invoke/app/method/endpoint
# State store
curl -X POST http://localhost:3500/v1.0/state/statestore \
-H "Content-Type: application/json" \
-d '[{"key": "mykey", "value": "myvalue"}]'
```
### Via Dapr SDK (Rust)
Tambah ke `Cargo.toml`:
```toml
dapr-sdk = { version = "0.15", features = ["pubsub", "http"] }
tokio-stream = "0.1"
```
Contoh publish event:
```rust
use dapr_sdk::client::{Client, Event};
use dapr_sdk::DaprClient;
let client = DaprClient::new("127.0.0.1", 3500).await?;
client.publish_event("pubsub", "hub.image.cached", serde_json::json!({
"original_url": url,
"cdn_url": cdn_url,
})).await?;
```
Contoh subscribe event:
```rust
let mut stream = client.subscribe_events("pubsub", "hub.image.cached").await?;
while let Some(event) = stream.next().await {
let data: MyEvent = serde_json::from_slice(&event.data)?;
// handle event
}
```
## 3. Event Topics Convention
Gunakan prefix `hub.` untuk semua event:
| Topic | Payload | Description |
|-------|---------|-------------|
| `hub.image.cached` | `{original_url, cdn_url, source}` | Image selesai di-cache |
| `hub.image.repaired` | `{old_url, new_url}` | CNAME image diperbaiki |
| `hub.scrape.anime.done` | `{source, slug, duration}` | Scrape anime selesai |
| `hub.system.alert` | `{service, level, message}` | Error/alert dari service |
## 4. Local Development
Untuk development tanpa Docker:
```bash
# 1. Install Dapr CLI
# 2. Init Dapr local
dapr init
# 3. Run service dengan sidecar
dapr run --app-id app --app-port 3000 --dapr-http-port 3500 \
--resources-path ./infra/dapr/components \
-- cargo run
```
## 5. Verifikasi
```bash
# Sidecar health
curl http://localhost:3500/v1.0/healthz
# Publish test event
curl -X POST http://localhost:3500/v1.0/publish/pubsub/hub.test \
-H "Content-Type: application/json" \
-d '{"test": true}'
# NATS stream stats
curl http://localhost:8222/jszetstream
```
+43
View File
@@ -52,6 +52,49 @@ networks:
Gunakan `app-shared-net` agar service dapat diakses oleh Traefik dan service lain.
## 3.5. Tambahkan Dapr sidecar (wajib untuk pub/sub)
Setiap service yang ingin menggunakan Dapr pub/sub atau service invocation harus punya sidecar.
Tambah di `infra/compose/<nama-app>.yml`:
```yaml
<nama-app>-dapr:
container_name: <nama-app>-dapr
image: daprio/daprd:latest
restart: always
depends_on:
dapr-placement:
condition: service_healthy
nats:
condition: service_healthy
otel-collector:
condition: service_started
networks:
- app-shared-net
command:
- './daprd'
- '--app-id=<nama-app>'
- '--app-port=<port>'
- '--dapr-http-port=3500'
- '--dapr-grpc-port=50001'
- '--placement-host-address=dapr-placement:50005'
- '--config=/dapr/config.yaml'
- '--resources-path=/dapr/components'
volumes:
- ../../infra/dapr:/dapr:ro
```
Pastikan juga app container punya `depends_on` ke dapr-placement, nats, dan otel-collector:
```yaml
depends_on:
dapr-placement:
condition: service_healthy
nats:
condition: service_healthy
otel-collector:
condition: service_started
```
## 4. Tambahkan route Traefik
Update `infra/traefik/dynamic/apps.yaml`:
@@ -23,10 +23,7 @@ Use `asepharyana-hub` as the root hub repository.
Current app submodules:
| Service | Path | Remote |
| -------------- | ---------------- | --------------------------------------- |
| Elysia API | `apps/elysia` | `asepharyana/asepharyana-hub-elysia` |
| React frontend | `apps/react` | `asepharyana/asepharyana-hub-react` |
| Rust auth API | `apps/rust-auth` | `asepharyana/asepharyana-hub-rust-auth` |
| ----------- | -------------- | --------------------------------------- |
| Scraper API | `apps/scraper` | `asepharyana/asepharyana-hub-scraper` |
## Consequences
+124
View File
@@ -0,0 +1,124 @@
# ADR 0002: Production `.env` via GitHub Encrypted Secret
## Status
Accepted
## Context
The project runs on a remote VPS (`orangevps`, IP `45.127.35.244`) that hosts multiple services via Docker Compose. These services require environment variables (database credentials, API keys, tokens) that must not be committed to the repository.
The production `.env` file on the VPS is **not** a copy of the committed `.env` in the repo root — it contains additional secrets (Portainer tokens, Discord bot tokens, etc.) that only exist in production.
Previously, the `.env` file on the VPS was edited manually via SSH, which led to drift between the local `.env` and the production `.env`. When the database server IP or port changed in the local `.env`, the production `.env` was not updated, causing service outages.
## Decision
The production `.env` file is stored as a **GitHub Actions encrypted secret** named `ENV_FILE_PRODUCTION`. During deployment, the `.github/workflows/deploy-docker.yml` workflow writes this secret to a file and SCPs it to the VPS.
### Flow
```
GitHub Secret (ENV_FILE_PRODUCTION)
▼ (deploy-docker.yml)
echo "$ENV_FILE_PRODUCTION" > .env.prod
scp .env.prod → VPS:$VPS_TARGET_DIR/.env
▼ (docker compose --env-file .env up)
Container reads $DATABASE_URL, $JWT_SECRET, etc.
```
### How to update
```bash
# 1. Read current content from the VPS
ssh root@45.127.35.244 "cat /root/asepharyana-hub/.env"
# 2. Pipe updated content to the GitHub secret
# (requires gh CLI with repo access)
cat /path/to/updated-env | gh secret set ENV_FILE_PRODUCTION --repo asepharyana/asepharyana-hub
# 3. Trigger a redeploy to push it to the VPS
gh workflow run deploy-docker.yml
# OR apply immediately on the VPS (for hotfix):
ssh root@45.127.35.244 "sed -i 's|OLD_VALUE|NEW_VALUE|' /root/asepharyana-hub/.env"
# Then restart affected containers
```
## Server Topology
| Host | IP | Role |
|------|----|------|
| `orangevps` (VPS) | `45.127.35.244` | Docker host: Traefik, scraper-api, Redis, NATS, Dapr |
| `imrnes` (bare-metal) | `100.121.180.82` (Tailscale) | PostgreSQL (port 6432), Redis (port 6379), Browserless |
## Database
| Variable | Value |
|----------|-------|
| `DATABASE_URL` | `postgres://asephs:hunterz@100.121.180.82:6432/hub` |
| `REDIS_URL` | `redis://redis:6379` (Docker network, overridden per-service) |
| `EXTERNAL_BROWSERLESS_WS` | `ws://43.134.105.109:3001/?token=...` (external proxy) |
> **Important:** The Docker Compose `environment:` section uses variable interpolation (`${DATABASE_URL}`), which is resolved from the `--env-file .env` at compose time — NOT from the service's `env_file`. Both must be kept in sync.
## Docker Compose Project Structure
The VPS runs a single Docker Compose project named `compose` composed of multiple files:
```bash
/root/asepharyana-hub/infra/compose/
├── traefik.yml # Reverse proxy (TLS termination, routing)
├── shared.yml # Redis
├── nats.yml # NATS message broker + JetStream
├── dapr.yml # Dapr placement service
├── scraper.yml # Scraper API + Dapr sidecar
└── observability.yml # OTel Collector + Jaeger + Dashboard
```
All files are always included together for dependency resolution:
```bash
docker compose \
--env-file .env \
-f infra/compose/traefik.yml \
-f infra/compose/shared.yml \
-f infra/compose/scraper.yml \
-f infra/compose/nats.yml \
-f infra/compose/dapr.yml \
-f infra/compose/observability.yml \
up -d
```
## GitHub Secrets Required
| Secret | Description |
|--------|-------------|
| `SSH_PRIVATE_KEY` | SSH key for VPS access |
| `VPS_HOST` | `45.127.35.244` |
| `VPS_USER` | `root` |
| `VPS_TARGET_DIR` | `/root/asepharyana-hub` |
| `ENV_FILE_PRODUCTION` | Full `.env` content for production |
## Consequences
### Positive
- Environment is version-controlled via GitHub Secrets audit log.
- No risk of committing secrets to the repo.
- Deployment is fully automated — `.env` is pushed on every deploy.
- Easy to rotate secrets: update `ENV_FILE_PRODUCTION` and redeploy.
### Negative
- The secret is opaque — you cannot diff it or review changes via PR.
- If the secret falls out of sync with the local `.env`, services silently break on next deploy.
- Requires `gh` CLI or GitHub UI to update — not a simple file edit.
### Mitigations
- Keep the **committed `.env`** in the repo root as the source of truth for non-secret values (database URL, ports, API endpoints).
- Document any manual SSH hotfix at the same time as updating the GitHub secret.
- Run `gh secret set ENV_FILE_PRODUCTION` with the latest server `.env` content after any hotfix.
+234
View File
@@ -0,0 +1,234 @@
# Backup & Disaster Recovery
## Aset yang Perlu di-Backup
| Aset | Lokasi | Frekuensi | Metode |
|------|--------|-----------|--------|
| Database PostgreSQL | `imrnes` (100.121.180.82:6432) | Harian | `pg_dump` |
| Volume Redis | `orangevps` (Docker volume) | Opsional | Redis RDB / AOF |
| Volume NATS JetStream | `orangevps` (Docker volume) | Opsional | File copy |
| Docker Compose manifests | GitHub (hub repo) | Real-time | Git |
| Environment variables | GitHub secret `ENV_FILE_PRODUCTION` | Manual | `gh secret set` |
| TLS certificates | `orangevps` (`/root/*.pem`, `*.key`) | Saat renew | SCP |
| Tailscale auth | Tailscale admin console | - | Cloud-managed |
| GitHub Actions secrets | GitHub UI | Manual | Backup list |
## Database PostgreSQL (Prioritas Tertinggi)
### Backup Manual
```bash
# Dari orangevps (via Tailscale)
pg_dump -h 100.121.180.82 -p 6432 -U asephs -d hub \
--no-owner --no-acl \
-F c -f /root/db-backups/hub-$(date +%Y%m%d-%H%M%S).dump
# Atau dari imrnes langsung
pg_dump -U asephs -d hub \
-F c -f /backup/hub/hub-$(date +%Y%m%d-%H%M%S).dump
```
### Restore
```bash
# Drop dan recreate database
dropdb -h 100.121.180.82 -p 6432 -U asephs hub
createdb -h 100.121.180.82 -p 6432 -U asephs hub
# Restore dari dump
pg_restore -h 100.121.180.82 -p 6432 -U asephs -d hub \
--no-owner --no-acl \
/path/to/backup/hub-20260101-120000.dump
```
### Backup Otomatis (via Cron di imrnes)
```bash
# /etc/cron.d/hub-db-backup
0 2 * * * root pg_dump -U asephs -d hub -F c -f /backup/hub/hub-$(date +\%Y\%m\%d).dump && find /backup/hub -name "hub-*.dump" -mtime +30 -delete
```
## Volume Docker
### Redis
Redis data bisa di-recover dari NATS events (event sourcing). Jika tidak ada persistence requirement, cukup restart:
```bash
docker volume rm redis_data
docker compose -f infra/compose/shared.yml up -d
```
Jika perlu backup:
```bash
# Save RDB snapshot
docker exec redis redis-cli SAVE
# Copy dari volume
docker run --rm -v redis_data:/data -v /backup:/backup alpine cp /data/dump.rdb /backup/redis-$(date +%Y%m%d).rdb
```
### NATS JetStream
```bash
# Backup volume
docker run --rm -v nats_data:/data -v /backup:/backup alpine \
tar czf /backup/nats-$(date +%Y%m%d).tar.gz -C /data .
```
## Environment Variables
### Backup `.env` dari VPS
```bash
# Simpan current .env dari VPS
ssh root@45.127.35.244 "cat /root/asepharyana-hub/.env" > .env.backup.$(date +%Y%m%d)
# Update GitHub secret
cat .env.backup.$(date +%Y%m%d) | gh secret set ENV_FILE_PRODUCTION --repo asepharyana/asepharyana-hub
```
### Restore `.env` jika hilang
```bash
# Buat .env baru dari template
cp .env.example .env
# Edit secrets (manual dari password manager atau GitHub secret)
# Atau download dari GitHub secret
gh secret list --repo asepharyana/asepharyana-hub
```
## TLS Certificates
### Backup
```bash
# Di orangevps
tar czf /root/cert-backup-$(date +%Y%m%d).tar.gz \
/root/asepharyana.my.id.pem \
/root/asepharyana.my.id.key \
/root/asepharyana.web.id.pem \
/root/asepharyana.web.id.key \
/root/asepharyana-hub/infra/traefik/dynamic/ssl.yaml
# SCP ke local
scp root@45.127.35.244:/root/cert-backup-*.tar.gz .
```
### Restore
```bash
# SCP ke VPS
scp cert-backup-20260101.tar.gz root@45.127.35.244:/root/
# Extract
ssh root@45.127.35.244 "tar xzf /root/cert-backup-20260101.tar.gz -C / && docker restart traefik"
```
## Disaster Recovery Scenarios
### Skenario 1: VPS (orangevps) mati total
**Dampak:** Semua service down.
**Recovery:**
```bash
# 1. Provision VPS baru (atau restore dari snapshot)
# 2. Install Docker + Tailscale
# 3. Clone repo
git clone https://github.com/asepharyana/asepharyana-hub.git /root/asepharyana-hub
# 4. Setup Tailscale, route service
# 5. Restore .env
echo "<ENV_FILE_PRODUCTION>" > /root/asepharyana-hub/.env
# 6. Restore TLS certs
# 7. Create network
docker network create app-shared-net
# 8. Start services sesuai urutan
cd /root/asepharyana-hub
for f in shared.yml nats.yml dapr.yml traefik.yml scraper.yml; do
docker compose -f infra/compose/$f --env-file .env up -d
done
# 9. Update DNS jika IP baru
```
### Skenario 2: Database (imrnes) mati total
**Dampak:** Semua service yang butuh database error.
**Recovery:**
```bash
# 1. Fix imrnes atau provision server baru
# 2. Setup PostgreSQL
# 3. Restore dari backup terakhir
# 4. Update Tailscale IP jika perlu
# 5. Update .env dan GitHub secret
# 6. Redeploy
```
### Skenario 3: GitHub repository hilang
**Dampak:** Kehilangan CI/CD, tapi Docker images masih ada di GHCR.
**Recovery:**
```bash
# 1. Create repo baru di GitHub
# 2. Push dari local clone
git remote add origin-new https://github.com/asepharyana/asepharyana-hub-new.git
git push origin-new main
# 3. Re-create GitHub secrets
# 4. Re-create workflows
# 5. Update VPS remote
ssh root@45.127.35.244 "cd /root/asepharyana-hub && git remote set-url origin https://github.com/asepharyana/asepharyana-hub-new.git"
```
### Skenario 4: GHCR registry tidak bisa diakses
**Dampak:** Tidak bisa pull image.
**Recovery:**
```bash
# 1. Build image langsung di VPS
docker build -f infra/docker/scraper.Dockerfile -t ghcr.io/asepharyana/asepharyana-hub/scraper-api:local .
# 2. Update compose file untuk sementara
sed -i 's|image: ghcr.io/.*|image: ghcr.io/asepharyana/asepharyana-hub/scraper-api:local|' infra/compose/scraper.yml
# 3. Start
docker compose -f infra/compose/scraper.yml up -d
```
### Skenario 5: Semua server mati (total loss)
**Recovery:**
```bash
# 1. Provision VPS baru
# 2. Provision server database baru
# 3. Setup Tailscale
# 4. Clone repo, restore .env, certs
# 5. Restore database dari backup (jika ada)
# 6. Jika tidak ada backup database:
# - Build image dari GHCR
# - Start service dengan database kosong
# - Data akan terisi ulang dari scraping
```
## Checklist Pencegahan
- [ ] Cron job backup database berjalan
- [ ] Backup `.env` disimpan di luar VPS (password manager)
- [ ] TLS certificates backup disimpan di luar VPS
- [ ] GitHub secrets terdaftar (tidak hanya diingat)
- [ ] Docker images bisa di-rebuild dari CI (GHCR sebagai source of truth)
- [ ] Tailscale admin access via multiple accounts
+248
View File
@@ -0,0 +1,248 @@
# CI/CD Pipeline
Dokumentasi pipeline CI/CD untuk `asepharyana-hub`. Terdiri dari 5 GitHub Actions workflow yang saling terhubung.
## Workflow Overview
```
┌─────────────┐
│ Lint │ (PR/push → Biome)
└──────┬──────┘
Push ke main ─────┼────── repository_dispatch
┌──────▼──────────────────┐
│ docker-build-push.yml │
│ │
│ Phase 1: Detect │
│ Phase 2: Build & Push │
│ Phase 3: Update │
│ manifests │
└──────┬──────────────────┘
│ workflow_run
┌──────▼──────────────┐
│ deploy-docker.yml │
│ SSH → VPS │
│ Pull → Restart │
└─────────────────────┘
repository_dispatch ──► update-submodule.yml
(dari submodule) (update pointer → commit)
docker-build-push.yml
(triggered by push)
```
## Workflow Detail
### 1. Lint (`lint.yml`)
**Trigger:** PR/push ke `main` yang mengubah `*.json`, `*.js`, `biome.json`
**Aksi:**
- Checkout repo dengan submodules
- Setup Bun
- `bun install --frozen-lockfile`
- `bun run ci` (Biome CI mode)
**Permissions:** read-only
### 2. Build and Push Docker Images (`docker-build-push.yml`)
**Trigger:**
- Push ke `main` yang mengubah `apps/**`, `infra/**`, atau file workflow
- `repository_dispatch` tipe `submodule-updated`
- `workflow_dispatch` (manual)
**Concurrency:** Satu workflow per branch (cancel-in-progress=false)
#### Phase 1: Detect Changes
Job `changes` mendeteksi service mana yang perlu di-build:
- **Push event:** `git diff --name-only` antara `before` dan `after` SHA
- **repository_dispatch:** Parse payload `{service, sha}` dan validasi
- **workflow_dispatch:** Build semua service
Output format matrix:
```json
[{"id":"scraper-api","target":"docker-scraper","path":"apps/scraper"}]
```
#### Phase 2: Build & Push (Matrix)
Job `build` berjalan paralel per service (matrix strategy):
1. Checkout repo + sync submodule
2. Jika `repository_dispatch`, checkout submodule ke SHA tertentu
3. Login ke GHCR
4. Setup Docker Buildx
5. Build & push dengan tag:
- `ghcr.io/asepharyana/asepharyana-hub/<service>:latest`
- `ghcr.io/asepharyana/asepharyana-hub/<service>:sha-<shortsha>`
6. Build cache: registry-based (`:<service>:buildcache`)
#### Phase 3: Update Manifests
Job `update-manifest`:
1. Update image tag di compose file (`infra/compose/<service>.yml`)
2. Jika `repository_dispatch`, update submodule pointer
3. Commit dengan message `chore: update manifests and submodules [skip ci]`
4. Push dengan retry (3 attempts, rebase jika conflict)
### 3. Deploy Docker to VPS (`deploy-docker.yml`)
**Trigger:**
- `workflow_run` setelah `docker-build-push.yml` selesai
- Push ke `main` yang mengubah `infra/**`
- `workflow_dispatch` (manual)
**Concurrency:** Satu deployment dalam satu waktu (`group: deploy-vps`)
**Aksi di VPS (via SSH):**
```
1. Setup SSH multiplexing
2. SCP .env dari GitHub secret ke VPS
3. Docker login ke GHCR
4. Git sync (fetch + reset --hard)
5. Detect changed files:
├─ Compose stack changes → selective container update
├─ Traefik dynamic config → SIGHUP
└─ Other infra → full deploy
6. Pull images (retry 3x)
7. Remove stale containers
8. Up services
9. SIGHUP Traefik jika perlu
```
### 4. Security Scan (`security.yml`)
**Trigger:**
- PR ke `main`
- Jadwal: Setiap Senin (`0 6 * * 1`)
**Aksi:**
- Checkout dengan fetch-depth 2
- CodeQL init untuk Rust
- `cargo build` di `apps/scraper`
- CodeQL analyze
### 5. Update Submodule Pointer (`update-submodule.yml`)
**Trigger:** `repository_dispatch` tipe `submodule-updated`
**Aksi:**
1. Validasi payload (`service`, `sha`)
2. Map service ke submodule path (e.g., `scraper-api``apps/scraper`)
3. Update submodule ke SHA yang diberikan
4. Commit sebagai `monrepo-bot` dengan message:
`chore: update <service> to <shortsha>`
5. Push dengan retry (3 attempts)
## Flow Submodule Update
Flow lengkap ketika code berubah di submodule repo:
```
1. Developer push ke asepharyana-hub-scraper
2. GitHub Action di scraper repo kirim repository_dispatch
ke asepharyana-hub
3. update-submodule.yml terima dispatch, update pointer
4. Commit masuk ke hub repo main
5. Commit ini trigger docker-build-push.yml
(push ke main dengan path apps/scraper/**)
6. Build image baru, update compose file
7. Deploy ke VPS
```
## Secrets yang Diperlukan
| Secret | Workflow | Deskripsi |
|--------|----------|-----------|
| `SSH_PRIVATE_KEY` | deploy-docker | SSH key untuk akses VPS |
| `VPS_HOST` | deploy-docker | IP VPS (`45.127.35.244`) |
| `VPS_USER` | deploy-docker | User SSH (`root`) |
| `VPS_TARGET_DIR` | deploy-docker | Dir di VPS (`/root/asepharyana-hub`) |
| `ENV_FILE_PRODUCTION` | deploy-docker | Full `.env` production |
## Menambahkan Service Baru ke Pipeline
Untuk menambahkan service baru, update:
### `docker-build-push.yml`
1. **Phase 1 — `changes` job:** Tambah detection logic untuk service baru:
```yaml
echo "new-service=$(changed '^(apps/new-service(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/new-service\.Dockerfile$)')" >> "$GITHUB_OUTPUT"
```
2. **Phase 1 — `repository_dispatch`:** Tambah case:
```yaml
case "$SERVICE" in
scraper-api|new-service) ;;
```
3. **Phase 1 — `set-matrix`:** Tambah service:
```bash
if [ "${{ ...['new-service'] == 'true' ... }}" == "true" ]; then add_service "new-service" "docker-new-service" "apps/new-service"; fi
```
4. **Phase 2 — `meta` step:** Tambah mapping Dockerfile:
```bash
"new-service") echo "dockerfile=infra/docker/new-service.Dockerfile" >> $GITHUB_OUTPUT ;;
```
5. **Phase 3 — `update-manifest`:** Tambah mapping:
```bash
SERVICES["new-service"]="new-service.yml"
PATHS["new-service"]="apps/new-service"
```
### `deploy-docker.yml`
Tambah compose file ke `ALL_COMPOSE_FILES`:
```bash
ALL_COMPOSE_FILES="infra/compose/traefik.yml infra/compose/shared.yml infra/compose/scraper.yml infra/compose/nats.yml infra/compose/dapr.yml infra/compose/new-service.yml"
```
## Rollback
### Rollback Image
```bash
# Cari SHA tag sebelumnya di GHCR packages
# Update compose file ke tag tersebut
sed -i 's|sha-badcommit|sha-goodcommit|g' infra/compose/scraper.yml
git commit -am "fix: rollback scraper-api to sha-goodcommit"
git push
```
### Rollback via Git Revert
```bash
git revert HEAD
git push origin main
# Pipeline otomatis build dan deploy
```
## Monitoring Pipeline
```bash
# Cek status workflow terbaru
gh run list --limit 5
# Lihat log workflow tertentu
gh run view <run-id> --log
# Trigger workflow manual
gh workflow run deploy-docker.yml
```
-39
View File
@@ -1,39 +0,0 @@
# Squid Proxy Configuration (Archived)
**Status**: Archived; not referenced by active infrastructure.
**Date archived**: 2026-06-04
**Original location**: `infra/squid.conf`
**Reason**: No active Docker Compose service mounts or starts Squid. The config is preserved here for reference if Squid proxying is restored later.
## Original configuration
```squid
# Allow localhost access only
acl localhost src 127.0.0.1/32 ::1/128
http_access allow localhost
# Allow Docker bridge subnets (Adjusted for our expected subnets)
acl docker_bridge src 172.16.0.0/12
http_access allow docker_bridge
# Deny all other access
http_access deny all
# Standard port - Explicitly bind to all interfaces
http_port 0.0.0.0:3128
# Hide source info
forwarded_for off
via off
```
## Restore checklist
1. Copy this config back to `infra/squid.conf`.
2. Add a Squid service to an active compose file.
3. Mount the config into the Squid container.
4. Document which services should use the proxy.
5. Validate network exposure and authentication before production use.
+286
View File
@@ -0,0 +1,286 @@
# NATS + JetStream Guide
Dokumentasi konfigurasi, penggunaan, dan troubleshooting NATS di infrastruktur `asepharyana-hub`.
## Arsitektur
NATS berjalan di container `nats` dengan JetStream diaktifkan (`-js`). Data persistent disimpan di volume Docker `nats_data`.
```
Service ──► NATS (port 4222) ──► JetStream (disk)
├─ Monitoring HTTP: port 8222
└─ Client connections: port 4222
```
### Hubungan dengan Dapr
Saat ini Dapr pub/sub menggunakan **Redis** (`pubsub.redis`), bukan NATS. NATS berfungsi sebagai message broker independen untuk:
- Event streaming antar service
- Persistent job queues
- Pub/sub untuk service yang tidak menggunakan Dapr
Jika ingin Dapr menggunakan NATS sebagai backend pub/sub, ganti komponen `pubsub.yaml`:
```yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.nats
version: v1
metadata:
- name: natsURL
value: nats://nats:4222
```
## Konfigurasi Compose
File: `infra/compose/nats.yml`
```yaml
services:
nats:
container_name: nats
image: nats:latest
restart: always
networks:
app-shared-net:
aliases:
- nats
ports:
- '4222:4222' # client connections
- '8222:8222' # HTTP monitor
command:
- '-js' # enable JetStream
- '-sd'
- '/data' # storage directory
volumes:
- nats_data:/data
```
## CLI Tools
### Install NATS CLI
```bash
# Linux
curl -sf https://bin.nats.dev/nats | sh
sudo mv nats /usr/local/bin/
# Atau via package manager
# brew install nats-io/nats-tools/nats (macOS)
```
### Koneksi ke NATS
```bash
# Dari host (port 4222 ter-expose)
nats context save hub --server nats://localhost:4222 --description "Hub Production"
nats context select hub
# Test koneksi
nats server check
nats server info
```
### Manage Streams (JetStream)
```bash
# List semua stream
nats stream list
# Lihat detail stream
nats stream info <stream-name>
# Buat stream
nats stream add <stream-name> \
--subjects "hub.>" \
--storage file \
--max-msgs 1000000 \
--max-bytes 1G \
--retention limits
# Hapus stream
nats stream rm <stream-name>
# Purge (hapus semua message, retain stream)
nats stream purge <stream-name>
```
### Pub/Sub
```bash
# Subscribe ke subject
nats sub "hub.>"
nats sub "hub.image.cached"
# Publish message
nats pub "hub.test" '{"message": "hello"}'
nats pub "hub.image.cached" '{"original_url": "https://example.com/img.jpg", "cdn_url": "https://cdn.example.com/img.jpg"}'
# Request-reply
nats request "hub.service.do" '{"task": "process"}'
```
### Monitoring via HTTP API
```bash
# Server info
curl http://localhost:8222/
# JetStream info
curl http://localhost:8222/jszetstream
# Stream detail
curl http://localhost:8222/jszetstream?stream=<stream-name>
# Consumer info
curl http://localhost:8222/jszetstream?stream=<stream-name>&consumer=<consumer-name>
# Server stats
curl http://localhost:8222/varz
# Connections
curl http://localhost:8222/connz
```
## Event Topics Convention
Semua topik menggunakan prefix `hub.`:
| Subject | Payload | Deskripsi |
|---------|---------|-----------|
| `hub.image.cached` | `{original_url, cdn_url, source}` | Image selesai di-cache |
| `hub.image.repaired` | `{old_url, new_url}` | CNAME image diperbaiki |
| `hub.scrape.anime.done` | `{source, slug, duration}` | Scrape anime selesai |
| `hub.system.alert` | `{service, level, message}` | Error/alert dari service |
| `hub.test` | Any | Testing |
### Wildcard Subjects
NATS mendukung wildcard:
- `hub.>` — semua event hub (multi-level)
- `hub.image.*` — semua event image (single-level)
- `hub.*.done` — semua event yang selesai (single-level)
## JetStream Configuration
### Storage
Data JetStream disimpan di volume Docker `nats_data`.
Lokasi di VPS:
```bash
docker volume inspect nats_data
# atau
ls -la /var/lib/docker/volumes/nats_data/_data/
```
### Memory & Limits
NATS tidak memiliki konfigurasi limit memori default. Untuk production, pertimbangkan:
```yaml
command:
- '-js'
- '-sd'
- '/data'
- '--max_pending_size=64MB'
- '--max_payload=1MB'
```
Atau gunakan NATS configuration file:
```yaml
# nats-server.conf
jetstream:
max_memory_store: 256MB
max_file_store: 10GB
```
## Troubleshooting
### Stream data tidak muncul
```bash
# 1. Cek koneksi NATS
nats server check
# 2. Cek apakah JetStream aktif
curl http://localhost:8222/jszetstream
# 3. Cek stream dan message count
nats stream list
# 4. Subscribe langsung untuk test
nats sub ">"
```
### NATS tidak bisa start
```bash
# Cek log
docker logs nats
# Cek apakah port 4222 sudah dipakai
ss -tlnp | grep 4222
# Cek volume data korup
docker run --rm -v nats_data:/data alpine ls -la /data
# Restart
docker compose -f infra/compose/nats.yml up -d --force-recreate
```
### Disk JetStream penuh
```bash
# Cek ukuran volume
docker system df -v | grep nats_data
# Purge stream jika perlu
nats stream purge <stream-name>
# Atau hapus volume (data hilang!)
docker compose -f infra/compose/nats.yml down
docker volume rm nats_data
docker compose -f infra/compose/nats.yml up -d
```
### Slow consumer
```bash
# Cek consumer lag
nats stream info <stream-name>
# Lihat fields: "Pending" dan "Acknowledgment"
# Lihat stats server
curl http://localhost:8222/varz | jq '.slow_consumers'
```
## Migration: Redis Pub/Sub ke NATS
Jika ingin migrasi dari Dapr pub/sub Redis ke NATS:
1. Buat stream NATS untuk topik `hub.>`
2. Update `infra/dapr/components/pubsub.yaml` dari `pubsub.redis` ke `pubsub.nats`
3. Deploy ulang semua service (Dapr sidecar akan reconnect)
4. Verifikasi event flow
```yaml
# infra/dapr/components/pubsub.yaml (setelah migrasi)
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.nats
version: v1
metadata:
- name: natsURL
value: nats://nats:4222
```

Some files were not shown because too many files have changed in this diff Show More