From b31fe9d1883ffe28138ef342d0dd6fee5dd99e85 Mon Sep 17 00:00:00 2001 From: asepharyana Date: Thu, 9 Jul 2026 22:08:26 +0700 Subject: [PATCH] chore: initial hub repo structure --- .bun-version | 1 + .devcontainer/devcontainer.json | 7 + .dockerignore | 32 + .editorconfig | 13 + .github/CODEOWNERS | 11 + .github/ISSUE_TEMPLATE/bug_report.md | 37 + .github/ISSUE_TEMPLATE/feature_request.md | 23 + .github/SECURITY.md | 30 + .github/dependabot.yml | 21 + .github/pull_request_template.md | 49 + .github/workflows/deploy-docker.yml | 253 ++++ .github/workflows/docker-build-push.yml | 331 ++++ .github/workflows/lint.yml | 29 + .github/workflows/security.yml | 19 + .github/workflows/typecheck.yml | 22 + .github/workflows/update-submodule.yml | 32 + .gitignore | 64 + .gitmodules | 12 + .kilocode/mcp.json | 8 + .kilocode/rules/rules.md | 81 + .node-version | 1 + .npmrc | 2 + .nvmrc | 1 + .prettierignore | 6 + .prettierrc | 8 + .vscode/extensions.json | 9 + .vscode/settings.json | 1 + ARCHITECTURE.md | 332 ++++ AUTHORS.md | 3 + CHANGELOG.md | 19 + CONTRIBUTING.md | 239 +++ LICENSE | 21 + Makefile | 32 + README.md | 201 +++ apps/elysia/.gitignore | 75 + apps/react/.gitignore | 9 + apps/rust-auth/.gitignore | 7 + apps/scraper/.gitignore | 67 + bun.lock | 488 ++++++ docs/add-new-app.md | 124 ++ docs/adr/0001-use-hub-repo-with-submodules.md | 52 + docs/adr/README.md | 23 + docs/config/squid.conf.archived | 39 + .../plans/2026-05-24-moonrepo-migration.md | 686 +++++++++ .../plans/2026-05-25-postgresql-migration.md | 712 +++++++++ .../2026-05-27-production-github-actions.md | 533 +++++++ ...6-05-27-react-direct-runtime-and-images.md | 29 + .../2026-05-24-moonrepo-migration-design.md | 130 ++ .../2026-05-25-postgresql-migration-design.md | 197 +++ ...026-05-26-remove-nix-docker-only-design.md | 21 + ...-05-27-production-github-actions-design.md | 100 ++ .../2026-07-09-monorepo-restructure-design.md | 135 ++ eslint.config.mjs | 10 + infra/README.md | 98 ++ infra/compose/elysia.yml | 24 + infra/compose/react.yml | 13 + infra/compose/rust-auth.yml | 20 + infra/compose/scraper.yml | 21 + infra/compose/shared.yml | 21 + infra/compose/traefik.yml | 74 + infra/config/mysql/init.sql/00_init_user.sql | 10 + infra/config/mysql/init.sql/tracer_study.sql | 1329 +++++++++++++++++ infra/docker/elysia.Dockerfile | 26 + infra/docker/react-server.js | 75 + infra/docker/react.Dockerfile | 16 + infra/docker/rust.Dockerfile | 42 + infra/docker/scraper.Dockerfile | 50 + infra/traefik/TRAEFIK_ENV_CONFIG.md | 61 + infra/traefik/dynamic/apps.yaml | 57 + infra/traefik/dynamic/middlewares.yaml | 74 + infra/traefik/dynamic/ssl.yaml | 14 + infra/traefik/traefik.yaml | 29 + package.json | 13 + renovate.json | 7 + result | 1 + scripts/2updateenv.sh | 23 + scripts/clean-node-modules.ps1 | 169 +++ scripts/clean-node-modules.sh | 82 + scripts/cleanup-ghcr.sh | 54 + scripts/git-hooks/setup-hooks.sh | 14 + scripts/sync-submodules.sh | 13 + scripts/update-deps.sh | 65 + setup_prometheus.sh | 87 ++ 83 files changed, 7969 insertions(+) create mode 100644 .bun-version create mode 100644 .devcontainer/devcontainer.json create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/SECURITY.md create mode 100644 .github/dependabot.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/deploy-docker.yml create mode 100644 .github/workflows/docker-build-push.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/security.yml create mode 100644 .github/workflows/typecheck.yml create mode 100644 .github/workflows/update-submodule.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 .kilocode/mcp.json create mode 100644 .kilocode/rules/rules.md create mode 100644 .node-version create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 .prettierignore create mode 100644 .prettierrc create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json create mode 100644 ARCHITECTURE.md create mode 100644 AUTHORS.md create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 README.md create mode 100644 apps/elysia/.gitignore create mode 100644 apps/react/.gitignore create mode 100644 apps/rust-auth/.gitignore create mode 100644 apps/scraper/.gitignore create mode 100644 bun.lock create mode 100644 docs/add-new-app.md create mode 100644 docs/adr/0001-use-hub-repo-with-submodules.md create mode 100644 docs/adr/README.md create mode 100644 docs/config/squid.conf.archived create mode 100644 docs/superpowers/plans/2026-05-24-moonrepo-migration.md create mode 100644 docs/superpowers/plans/2026-05-25-postgresql-migration.md create mode 100644 docs/superpowers/plans/2026-05-27-production-github-actions.md create mode 100644 docs/superpowers/plans/2026-05-27-react-direct-runtime-and-images.md create mode 100644 docs/superpowers/specs/2026-05-24-moonrepo-migration-design.md create mode 100644 docs/superpowers/specs/2026-05-25-postgresql-migration-design.md create mode 100644 docs/superpowers/specs/2026-05-26-remove-nix-docker-only-design.md create mode 100644 docs/superpowers/specs/2026-05-27-production-github-actions-design.md create mode 100644 docs/superpowers/specs/2026-07-09-monorepo-restructure-design.md create mode 100644 eslint.config.mjs create mode 100644 infra/README.md create mode 100644 infra/compose/elysia.yml create mode 100644 infra/compose/react.yml create mode 100644 infra/compose/rust-auth.yml create mode 100644 infra/compose/scraper.yml create mode 100644 infra/compose/shared.yml create mode 100644 infra/compose/traefik.yml create mode 100644 infra/config/mysql/init.sql/00_init_user.sql create mode 100644 infra/config/mysql/init.sql/tracer_study.sql create mode 100644 infra/docker/elysia.Dockerfile create mode 100644 infra/docker/react-server.js create mode 100644 infra/docker/react.Dockerfile create mode 100644 infra/docker/rust.Dockerfile create mode 100644 infra/docker/scraper.Dockerfile create mode 100644 infra/traefik/TRAEFIK_ENV_CONFIG.md create mode 100644 infra/traefik/dynamic/apps.yaml create mode 100644 infra/traefik/dynamic/middlewares.yaml create mode 100644 infra/traefik/dynamic/ssl.yaml create mode 100644 infra/traefik/traefik.yaml create mode 100644 package.json create mode 100644 renovate.json create mode 120000 result create mode 100644 scripts/2updateenv.sh create mode 100644 scripts/clean-node-modules.ps1 create mode 100644 scripts/clean-node-modules.sh create mode 100644 scripts/cleanup-ghcr.sh create mode 100755 scripts/git-hooks/setup-hooks.sh create mode 100755 scripts/sync-submodules.sh create mode 100644 scripts/update-deps.sh create mode 100644 setup_prometheus.sh diff --git a/.bun-version b/.bun-version new file mode 100644 index 0000000..17e63e7 --- /dev/null +++ b/.bun-version @@ -0,0 +1 @@ +1.3.11 diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..b117216 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,7 @@ +{ + "image": "mcr.microsoft.com/devcontainers/universal:2", + "features": { + "ghcr.io/devcontainers/features/rust:1": {}, + "ghcr.io/devcontainers/features/node:2": {} + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bb13b17 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,32 @@ +**/.git +**/.gitmodules +**/node_modules +**/dist +**/.output +**/target +**/.svelte-kit +**/.next +**/.DS_Store +**/build +!apps/*/scripts/build/ +!apps/*/src/**/build/ +**/*.log +**/*.pem +.env +.env.* +!.env.example + +# IDE and temporary files +**/.vscode +**/.idea +**/tmp +**/temp +**/.cache +**/coverage +**/.npm +**/.bun +**/.pnpm-store +**/.yarn +**/.cargo-ok +**/*.swp +**/*~ diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..6e87a00 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,13 @@ +# Editor configuration, see http://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..8e97a09 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,11 @@ +# Default owners +* @MythEclipse + +# Infrastructure +/infra/ @MythEclipse + +# GitHub config +/.github/ @MythEclipse + +# Docs +/docs/ @MythEclipse diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..924c5cc --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: Bug Report +about: Create a report to help us improve +title: '[Bug] ' +labels: bug +assignees: asepharyana +--- + +## Description + +A clear and concise description of what the bug is. + +## Steps to Reproduce + +1. Go to '...' +2. Click on '...' +3. Scroll down to '...' +4. See error + +## Expected Behavior + +A clear and concise description of what you expected to happen. + +## Actual Behavior + +A clear and concise description of what actually happened, including any error messages, logs, or screenshots. + +## Environment + +- OS: [e.g. Ubuntu 22.04, macOS 14.0] +- Browser (if applicable): [e.g. Chrome 120, Firefox 121] +- Version / Commit: [e.g. v1.0.0, commit 3a2b1c4] +- Runtime: [e.g. Node 20, Docker 24] + +## Additional Context + +Add any other context about the problem here, such as relevant log output, stack traces, or screenshots. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..5f797a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,23 @@ +--- +name: Feature Request +about: Suggest an idea for this project +title: '[Feature] ' +labels: enhancement +assignees: asepharyana +--- + +## Problem Description + +A clear and concise description of the problem or need this feature would address. Ex. I'm always frustrated when [...] + +## Proposed Solution + +A clear and concise description of what you would like to happen, including any specific functionality, API changes, or user interface considerations. + +## Alternatives Considered + +A clear and concise description of any alternative solutions or features you have considered and why they were not sufficient. + +## Additional Context + +Add any other context, mockups, or examples about the feature request here. diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..c5513b7 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,30 @@ +# Security Policy + +## Supported Versions + +Only the latest stable release of this project receives security updates. Older versions are not supported. + +| Version | Supported | +| -------- | ------------------ | +| latest | :white_check_mark: | +| < latest | :x: | + +## Reporting a Vulnerability + +If you discover a security vulnerability, please report it via [GitHub Security Advisories](https://github.com/asepharyana/asepharyana-hub/security/advisories). + +Do **not** report security vulnerabilities via public GitHub issues. + +Please include the following information in your report: + +- A description of the vulnerability and the potential impact +- Steps to reproduce the issue +- The version(s) affected +- Any possible mitigations you have identified + +## Disclosure Policy + +- We will acknowledge receipt of your report within **48 hours**. +- We will investigate the issue and provide an estimated timeline for a fix. +- Once a fix is released, we will publish a security advisory and credit the reporter (if desired). +- We ask that you allow us **90 days** from the date of the report to release a fix before any public disclosure. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..41a7550 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,21 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for more information: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates +# https://containers.dev/guide/dependabot + +version: 2 +updates: + - package-ecosystem: 'devcontainers' + directory: '/' + schedule: + interval: weekly + + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: weekly + groups: + github-actions: + patterns: + - '*' diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..b1bb7c2 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,49 @@ +--- +name: Pull Request +about: Submit a pull request to improve the project +title: '' +labels: '' +assignees: '' +--- + +## Description + +A clear and concise description of the changes being introduced and why they are needed. Include any relevant motivation or context. + +## Type of Change + +Please delete options that are not relevant. + +- [ ] **feat** -- A new feature +- [ ] **fix** -- A bug fix +- [ ] **chore** -- Maintenance, dependency updates, build configuration +- [ ] **docs** -- Documentation only changes +- [ ] **refactor** -- A code change that neither fixes a bug nor adds a feature + +## How Has This Been Tested? + +Please describe the tests that you ran to verify your changes. + +- [ ] Unit tests pass +- [ ] Integration / E2E tests pass +- [ ] Manual testing performed (describe below) + +## Checklist + +- [ ] My code follows the project's coding style and conventions +- [ ] I have performed a self-review of my own changes +- [ ] I have added or updated tests to cover my changes (if applicable) +- [ ] All new and existing tests pass locally +- [ ] I have updated the documentation (if applicable) +- [ ] My changes generate no new warnings, linter errors, or type errors +- [ ] Any dependent changes have been merged and published + +## Related Issues + +Fixes # (issue) + +Closes # (issue) + +## Screenshots (if applicable) + +Add screenshots to help explain your changes. diff --git a/.github/workflows/deploy-docker.yml b/.github/workflows/deploy-docker.yml new file mode 100644 index 0000000..7ec1ae8 --- /dev/null +++ b/.github/workflows/deploy-docker.yml @@ -0,0 +1,253 @@ +name: Deploy Docker to VPS + +on: + workflow_run: + workflows: ['Build and Push Docker Images'] + types: + - completed + branches: + - main + push: + branches: + - main + paths: + - 'infra/**' + - '.github/workflows/deploy-docker.yml' + - '.github/workflows/docker-build-push.yml' + workflow_dispatch: + +# Prevent multiple deployments from running simultaneously +concurrency: + group: deploy-vps + cancel-in-progress: false + +permissions: + contents: read + packages: read + +jobs: + deploy: + runs-on: ubuntu-latest + if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' || github.event.workflow_run.conclusion == 'success' + steps: + - name: Checkout repository + uses: actions/checkout@v6 + with: + fetch-depth: 1 + submodules: false + + - 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 }} + GHCR_USERNAME: ${{ github.actor }} + GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + + echo "Deploy event: ${{ github.event_name }}" + echo "Deploy ref: ${{ github.ref }}" + echo "Deploy sha: ${{ github.sha }}" + + if [ -z "${SSH_PRIVATE_KEY:-}" ] || [ -z "${VPS_HOST:-}" ] || [ -z "${VPS_USER:-}" ] || [ -z "${VPS_TARGET_DIR:-}" ]; then + echo "❌ Deployment secrets are not fully configured. Please set SSH_PRIVATE_KEY, VPS_HOST, VPS_USER, and VPS_TARGET_DIR." + exit 1 + fi + + 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 + + # Use SSH multiplexing for faster subsequent commands + 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" + + echo "πŸ” Logging in to GitHub Container Registry..." + printf '%s' "$GHCR_TOKEN" | ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" "docker login ghcr.io -u '$GHCR_USERNAME' --password-stdin" + + ssh "${SSH_OPTS[@]}" "$VPS_USER@$VPS_HOST" "export VPS_TARGET_DIR=$VPS_TARGET_DIR; bash -s" <<'EOF' + set -euo pipefail + cd "$VPS_TARGET_DIR" + + # Ensure shared network exists + docker network inspect app-shared-net >/dev/null 2>&1 || docker network create app-shared-net + + echo "πŸ”„ Synchronizing repository..." + if [ ! -d ".git" ]; then + echo "Initializing git repository..." + git init + git remote add origin https://github.com/asepharyana/asepharyana-hub.git + fi + 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" + TRAEFIK_DYNAMIC_DIR="infra/traefik/dynamic" + + if git rev-parse HEAD >/dev/null 2>&1; then + BEFORE_REV=$(git rev-parse HEAD) + git reset --hard FETCH_HEAD + AFTER_REV=$(git rev-parse HEAD) + + if [ "$BEFORE_REV" = "$AFTER_REV" ]; then + echo "ℹ️ No new commits detected. Using full file list for safety." + TARGET_COMPOSE="" + else + CHANGED=$(git diff --name-only "$BEFORE_REV" "$AFTER_REV" || true) + echo "πŸ“„ Changed files:" + echo "$CHANGED" + + # Detect compose stack changes + CHANGED_COMPOSE=$(echo "$CHANGED" | grep '^infra/compose/.*\.yml$' || true) + TARGET_COMPOSE="" + for f in $CHANGED_COMPOSE; do + case " $ALL_COMPOSE_FILES " in + *" $f "*) TARGET_COMPOSE="$TARGET_COMPOSE $f" ;; + esac + done + TARGET_COMPOSE=$(printf '%s' "$TARGET_COMPOSE" | xargs || true) + if [ -n "$TARGET_COMPOSE" ]; then + echo "🎯 Detected compose stack changes in: $TARGET_COMPOSE" + else + echo "ℹ️ No stack compose files changed." + fi + + # Detect Traefik dynamic config changes + CHANGED_TRAEFIK=$(echo "$CHANGED" | grep "^$TRAEFIK_DYNAMIC_DIR/" || true) + if [ -n "$CHANGED_TRAEFIK" ]; then + echo "🎯 Detected Traefik dynamic config changes:" + echo "$CHANGED_TRAEFIK" + RELOAD_TRAEFIK="true" + else + echo "ℹ️ No Traefik dynamic config changes." + fi + + # Detect infra file changes (Dockerfiles, config, traefik static) + CHANGED_INFRA=$(echo "$CHANGED" | grep '^infra/' | grep -v '^infra/compose/' || true) + if [ -n "$CHANGED_INFRA" ]; then + echo "πŸ“¦ Detected other infra file changes:" + echo "$CHANGED_INFRA" + fi + fi + else + git reset --hard FETCH_HEAD + 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 + COMPOSE_CMD="docker-compose" + else + echo "❌ docker compose is not installed on the remote host." + exit 1 + fi + + # Construct compose arguments + docker rm -f imphenbot-app || true + + if [ -n "$TARGET_COMPOSE" ]; then + COMPOSE_ARGS="" + for f in $TARGET_COMPOSE; 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 + 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" + fi + + echo "πŸ“₯ Pulling images for target services..." + export DOCKER_CLI_EXPERIMENTAL=enabled + PULL_SUCCESS=false + # 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 + echo "βœ… Pull succeeded on attempt $attempt" + PULL_SUCCESS=true + break + else + echo "⚠️ Pull attempt $attempt failed. Retrying in 5s..." + sleep 5 + fi + done + if [ "$PULL_SUCCESS" != "true" ]; then + echo "❌ Failed to pull images after 3 attempts." + exit 1 + fi + + 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 + for f in $TARGET_COMPOSE; do + if [ -f "$f" ]; then + grep "container_name:" "$f" | awk '{print $2}' | while read -r cname; do + docker rm -f "$cname" >/dev/null 2>&1 || true + done + fi + done + else + for f in $ALL_COMPOSE_FILES; do + if [ -f "$f" ]; then + grep "container_name:" "$f" | awk '{print $2}' | while read -r cname; do + docker rm -f "$cname" >/dev/null 2>&1 || true + done + fi + done + fi + + echo "πŸ†™ Starting services..." + echo "πŸ” Debug: Current docker containers:" + docker ps -a + $COMPOSE_CMD $COMPOSE_ARGS --env-file .env up $UP_FLAGS + + # ── Traefik reload ── + if [ "${RELOAD_TRAEFIK:-false}" = "true" ]; then + echo "πŸ”„ Traefik dynamic config changed β€” reloading Traefik..." + # Traefik watches the dynamic config dir (providers.file.watch=true), + # but send SIGHUP as insurance + docker kill --signal HUP traefik 2>/dev/null || docker exec traefik kill -HUP 1 2>/dev/null || true + echo "βœ… Traefik reload signal sent" + 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." diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml new file mode 100644 index 0000000..5b785aa --- /dev/null +++ b/.github/workflows/docker-build-push.yml @@ -0,0 +1,331 @@ +name: Build and Push Docker Images + +on: + push: + branches: + - main + paths: + - 'apps/**' + - 'packages/**' + - '.github/workflows/docker-build-push.yml' + - 'infra/**' + - '!infra/compose/**' + repository_dispatch: + types: [submodule-updated] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + +env: + REGISTRY: ghcr.io + IMAGE_NAME_PREFIX: asepharyana/asepharyana-hub + +jobs: + # ────────────────────────────────────────────── + # Phase 1: Detect which services have changed + # ────────────────────────────────────────────── + changes: + runs-on: ubuntu-latest + 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' }} + steps: + - uses: actions/checkout@v6 + with: + submodules: false + fetch-depth: 2 + + - name: Detect changed services + id: filter + if: github.event_name == 'push' + env: + BEFORE: ${{ github.event.before }} + AFTER: ${{ github.sha }} + run: | + set -euo pipefail + if [ -z "${BEFORE:-}" ] || [[ "$BEFORE" =~ ^0+$ ]]; then + CHANGED_FILES=$(git ls-files) + else + git fetch --no-tags --depth=2 origin "$BEFORE" || true + CHANGED_FILES=$(git diff --name-only "$BEFORE" "$AFTER") + fi + + changed() { + printf '%s\n' "$CHANGED_FILES" | grep -Eq "$1" && echo true || echo false + } + + echo "scraper-api=$(changed '^(apps/scraper(/|$)|\.github/workflows/docker-build-push\.yml$|infra/docker/scraper\.Dockerfile$)')" >> "$GITHUB_OUTPUT" + echo "elysia-api=$(changed '^(apps/elysia(/|$)|packages(/|$)|\.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" + + - name: Parse repository_dispatch payload + id: dispatch + if: github.event_name == 'repository_dispatch' + env: + SERVICE: ${{ github.event.client_payload.service }} + SHA: ${{ github.event.client_payload.sha }} + run: | + set -euo pipefail + + if [ -z "${SERVICE:-}" ]; then + echo "::error::repository_dispatch payload missing service" + exit 1 + fi + + if [ -z "${SHA:-}" ]; then + echo "::error::repository_dispatch payload missing sha" + exit 1 + fi + + case "$SERVICE" in + scraper-api|elysia-api|react-web|rust-auth) ;; + *) + exit 1 + ;; + esac + + if ! [[ "$SHA" =~ ^[0-9a-fA-F]{40}$ ]]; then echo "::error::Invalid sha '$SHA'. Expected 40 hex characters"; exit 1; fi + + SERVICES=(scraper-api elysia-api react-web rust-auth) + for svc in "${SERVICES[@]}"; do + if [ "$SERVICE" = "$svc" ]; then + echo "${svc}=true" >> "$GITHUB_OUTPUT" + else + echo "${svc}=false" >> "$GITHUB_OUTPUT" + fi + done + + - name: Set matrix + id: set-matrix + run: | + SERVICES=() + add_service() { + SERVICES+=("{\"id\":\"$1\",\"target\":\"$2\",\"path\":\"$3\"}") + } + + 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 + + JSON_ARRAY="[$(IFS=,; echo "${SERVICES[*]}")]" + echo "matrix=$JSON_ARRAY" >> $GITHUB_OUTPUT + + wait-submodule-ref: + needs: [changes] + if: github.event_name == 'repository_dispatch' + runs-on: ubuntu-latest + steps: + - name: Wait for submodule ref + env: + SERVICE: ${{ github.event.client_payload.service }} + SHA: ${{ github.event.client_payload.sha }} + run: | + set -euo pipefail + + 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" ;; + *) + echo "::error::Unsupported service '$SERVICE'" + exit 1 + ;; + esac + + echo "Waiting for $SERVICE commit $SHA in $REPO" + TMPDIR=$(mktemp -d) + git init "$TMPDIR/probe" >/dev/null + git -C "$TMPDIR/probe" remote add origin "$REPO" + for attempt in {1..30}; do + if git -C "$TMPDIR/probe" fetch --depth=1 origin "$SHA" >/dev/null 2>&1; then + echo "Submodule commit $SHA is fetchable for $SERVICE" + rm -rf "$TMPDIR" + exit 0 + fi + echo "Attempt $attempt/30: $SHA not fetchable yet; waiting 10s" + sleep 10 + done + rm -rf "$TMPDIR" + + echo "::error::Submodule commit $SHA for $SERVICE was not fetchable after 300s" + exit 1 + + # ───────────────────────────────────────────────── + # Phase 2: Build and Push Images (Matrix) + # ───────────────────────────────────────────────── + build: + needs: [changes, wait-submodule-ref] + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.changes.outputs.matrix) }} + if: | + always() && + needs.changes.result == 'success' && + (needs.wait-submodule-ref.result == 'success' || needs.wait-submodule-ref.result == 'skipped') && + needs.changes.outputs.matrix != '[]' + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v6 + with: + submodules: false + + - name: Sync submodule locally + env: + EVENT_NAME: ${{ github.event_name }} + DISPATCH_SHA: ${{ github.event.client_payload.sha }} + SUBMODULE_PATH: ${{ matrix.path }} + run: | + set -euo pipefail + git submodule update --init --recursive "$SUBMODULE_PATH" + if [ "$EVENT_NAME" = "repository_dispatch" ] && [ -n "${DISPATCH_SHA:-}" ]; then + cd "$SUBMODULE_PATH" + git fetch origin "$DISPATCH_SHA" + git checkout "$DISPATCH_SHA" + fi + + - uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Docker metadata + id: meta + run: | + 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 "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 ;; + esac + + - name: Build and Push Docker image + uses: docker/build-push-action@v7 + with: + context: . + file: ${{ steps.meta.outputs.dockerfile }} + push: true + tags: ${{ steps.meta.outputs.tags }} + build-args: | + COMMIT_COUNT=${{ env.NR_COMMIT_COUNT || github.run_number }} + COMMIT_SHA=${{ env.NR_COMMIT_SHA || github.sha }} + cache-from: type=registry,ref=${{ steps.meta.outputs['cache-registry'] }} + cache-to: type=registry,ref=${{ steps.meta.outputs['cache-registry'] }},mode=max + + # ────────────────────────────────────────────── + # Phase 3: Update Manifests and Submodule Refs + # ────────────────────────────────────────────── + update-manifest: + needs: [changes, wait-submodule-ref, build] + if: | + always() && + needs.changes.result == 'success' && + (needs.wait-submodule-ref.result == 'success' || needs.wait-submodule-ref.result == 'skipped') && + (needs.build.result == 'success' || needs.build.result == 'skipped') + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + with: + submodules: false + token: ${{ secrets.GITHUB_TOKEN }} + ref: main + + - name: Update tags and submodules + run: | + SHORT_SHA=${GITHUB_SHA:0:7} + TAG="sha-$SHORT_SHA" + CHANGED=false + + declare -A SERVICES + SERVICES["scraper-api"]="scraper.yml" + SERVICES["elysia-api"]="elysia.yml" + SERVICES["react-web"]="react.yml" + SERVICES["rust-auth"]="rust-auth.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" + + # Use git config for possible commits + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + 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 [ "${{ github.event_name }}" == "workflow_dispatch" ]; then SHOULD_HAVE_RUN=true; fi + + if [ "$SHOULD_HAVE_RUN" == "true" ]; then + COMPOSE_FILE="infra/compose/${SERVICES[$id]}" + if [ -f "$COMPOSE_FILE" ]; then + echo "Updating $COMPOSE_FILE to $TAG" + sed -i "s|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_PREFIX }}/$id:.*|image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME_PREFIX }}/$id:$TAG|g" "$COMPOSE_FILE" + git add "$COMPOSE_FILE" + CHANGED=true + fi + + # If it's a repository_dispatch for this specific service, update its submodule pointer + if [ "${{ github.event_name }}" == "repository_dispatch" ] && [ "${{ github.event.client_payload.service }}" == "$id" ]; then + SHA_DISPATCH="${{ github.event.client_payload.sha }}" + SUB_PATH="${PATHS[$id]}" + if [ -n "$SHA_DISPATCH" ]; then + echo "Updating submodule $SUB_PATH to $SHA_DISPATCH" + git submodule update --init "$SUB_PATH" + git -C "$SUB_PATH" fetch origin "$SHA_DISPATCH" + git -C "$SUB_PATH" checkout "$SHA_DISPATCH" + git add "$SUB_PATH" + CHANGED=true + fi + fi + fi + done + + if [ "$CHANGED" == "true" ]; then + git commit -m "chore: update manifests and submodules [skip ci]" + + for attempt in {1..3}; do + if git pull --rebase origin main && git push origin main; then + exit 0 + fi + echo "Manifest push attempt $attempt/3 failed; retrying" + git rebase --abort || true + git pull --rebase origin main || true + sleep 5 + done + + echo "::error::Failed to push manifest update after 3 attempts" + exit 1 + else + echo "No changes detected." + fi diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..a3cbf63 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,29 @@ +name: Lint + +on: + pull_request: + branches: [main] + paths: + - 'apps/*/src/**/*.ts' + - 'apps/*/src/**/*.tsx' + - 'apps/*/eslint.config.mjs' + - 'eslint.config.mjs' + push: + branches: [main] + paths: + - 'apps/*/src/**/*.ts' + - 'apps/*/src/**/*.tsx' + - 'eslint.config.mjs' + +jobs: + eslint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: npm install -g eslint @antfu/eslint-config + - run: eslint . --no-error-on-unmatched-pattern || echo "Lint check completed (best-effort)" diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..5c8ef13 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,19 @@ +name: Security + +on: + pull_request: + branches: [main] + schedule: + - cron: '0 6 * * 1' # Every Monday + +jobs: + codeql: + runs-on: ubuntu-latest + permissions: + security-events: write + steps: + - uses: actions/checkout@v6 + - uses: github/codeql-action/init@v3 + with: + languages: javascript-typescript + - uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/typecheck.yml b/.github/workflows/typecheck.yml new file mode 100644 index 0000000..f9276cd --- /dev/null +++ b/.github/workflows/typecheck.yml @@ -0,0 +1,22 @@ +name: TypeCheck + +on: + pull_request: + branches: [main] + paths: + - 'apps/**/*.ts' + - 'apps/**/*.tsx' + - 'tsconfig.base.json' + +jobs: + typecheck: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: | + cd apps/react && npm install && npx tsc --noEmit || echo "TypeScript check completed" diff --git a/.github/workflows/update-submodule.yml b/.github/workflows/update-submodule.yml new file mode 100644 index 0000000..9f5203d --- /dev/null +++ b/.github/workflows/update-submodule.yml @@ -0,0 +1,32 @@ +name: Update Submodule Pointer +on: + repository_dispatch: + types: [submodule-updated] + +permissions: + contents: write + +jobs: + update: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Update submodule pointer + env: + SERVICE: ${{ github.event.client_payload.service }} + SHA: ${{ github.event.client_payload.sha }} + run: | + echo "Updating ${SERVICE} to ${SHA}" + git submodule update --init "apps/${SERVICE}" + cd "apps/${SERVICE}" + # unshallow β†’ full fetch so we get tree objects for the target SHA + git fetch --depth=1000 origin master + git checkout "${SHA}" + cd "${GITHUB_WORKSPACE}" + git add "apps/${SERVICE}" + git diff --cached --quiet && exit 0 + git config user.name "monrepo-bot" + git config user.email "monrepo-bot@users.noreply.github.com" + git commit -m "chore: update ${SERVICE} to ${SHA:0:12}" + git push diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5478ff4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,64 @@ +apps/gmw/ +# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# compiled output +dist +tmp +out-tsc +error.log +# dependencies +node_modules +.bun/** +.turbo/ +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace +**/target/** +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db +.claude +# Next.js +.next +out +**/.codegraph/** +**/.claude/** +test-output +**/**.env +**/**.env.** +vite.config.*.timestamp* +vitest.config.*.timestamp* + +storybook-static +~/.bun/** +docs/dependency-map.md +docs/handoff-log.jsonl +docs/observability.md +docs/quality-gates.json +docs/workflow-state.json +docs/todo.md +**/vendor/ + +.codegraph/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..833eaa0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,12 @@ +[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 diff --git a/.kilocode/mcp.json b/.kilocode/mcp.json new file mode 100644 index 0000000..ee5d245 --- /dev/null +++ b/.kilocode/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "next-devtools": { + "command": "npx", + "args": ["-y", "next-devtools-mcp@latest"] + } + } +} diff --git a/.kilocode/rules/rules.md b/.kilocode/rules/rules.md new file mode 100644 index 0000000..9b3d1c5 --- /dev/null +++ b/.kilocode/rules/rules.md @@ -0,0 +1,81 @@ +# 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. diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..7af24b7 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +22.11.0 diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..d9ca886 --- /dev/null +++ b/.npmrc @@ -0,0 +1,2 @@ +engine-strict=true +save-exact=true diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..7af24b7 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.11.0 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..167a59b --- /dev/null +++ b/.prettierignore @@ -0,0 +1,6 @@ +# Add files here to ignore them from prettier formatting +/dist +/coverage +*.env +**/target/ +**/node_modules/ diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..b9551d9 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "singleQuote": true, + "printWidth": 100, + "tabWidth": 2, + "trailingComma": "all", + "semi": true, + "arrowParens": "always" +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c647399 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,9 @@ +{ + "recommendations": [ + "nrwl.angular-console", + "esbenp.prettier-vscode", + "dbaeumer.vscode-eslint", + "ms-playwright.playwright", + "firsttris.vscode-jest-runner" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..fb9521a --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,332 @@ +# Architecture + +## Hub Repository Structure Overview + +``` +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 +β”‚ β”œβ”€β”€ 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 +``` + +## Technology Stack + +### Backend 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 | +| ------------------ | ----------------------- | ---------------------------------------------------------------- | +| 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 | +| Cache | Redis (Alpine) | Session store, rate limit counters, caching | +| 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**: `infra/traefik/traefik.yaml` β€” 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 +docker compose -f traefik.yml -f shared.yml -f elysia.yml -f react.yml ... up -d +``` + +### Tailscale Networking + +```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)"] + end + + subgraph "imrnes Services" + PG[(PostgreSQL)] + REDIS[Redis] + end + + subgraph "orange 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 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: + +``` +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. + +## Data Flow + +### Request Flow (Production) + +```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 β†’ orange 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->>Redis: GET/SET via Tailscale + Redis-->>App: Cached value + end + + App-->>Traefik: HTTP response + Traefik-->>User: HTTPS response +``` + +### CI/CD Pipeline + +```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 + +- Every push to `main` triggers Docker builds for changed services +- Images are tagged with both `latest` and `sha-` (e.g., `sha-b0ef947`) +- Compose files are auto-updated to pin the new SHA tag +- This enables deterministic rollbacks by reverting the compose file change + +### VPS Deployment + +The `orange` VPS (Tailscale `100.96.248.86`) hosts all application containers: + +1. GitHub Actions SSHes into the VPS +2. Production secrets are written as `.env` +3. The repo is synchronized via `git pull` +4. Changed compose files are detected by `git diff` +5. Docker images are pulled (with retry logic for transient failures) +6. Old containers are removed by `container_name` +7. `docker compose up -d` brings up the new containers +8. Traefik automatically detects the new containers via Docker provider + +### 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. + +```mermaid +graph TB + subgraph "Orange VPS" + DIR[/root/asepharyana-hub/] + ENV[.env] + COMPOSE[infra/compose/*.yml] + NET[app-shared-net] + + DIR -->|git pull| COMPOSE + ENV -->|docker compose --env-file| COMPOSE + COMPOSE -->|docker compose pull| IMAGES[(GHCR Images)] + COMPOSE -->|docker compose up -d| CONT[Containers] + CONT --> NET + end + + subgraph "GitHub Actions" + BUILD[Build & Push] + DEPLOY[Deploy Workflow] + BUILD -->|trigger| DEPLOY + DEPLOY -->|SSH| DIR + end + + IMAGES -->|registry| GHCR[ghcr.io/asepharyana] +``` + +## Submodule Strategy + +Each application lives in its own Git repository and is imported as a submodule into `apps/`. This approach: + +- **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 + +### Submodule Lifecycle + +1. Developer pushes to a submodule (e.g., `apps/elysia`) +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 + +### Updating Submodules + +```bash +# Update a single submodule to latest +cd apps/elysia +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 +``` + +## Service Mesh & Inter-Service Communication + +```mermaid +graph LR + subgraph "External" + WWW[Internet] + end + + subgraph "Orange VPS" + TRAEFIK[Traefik :443] + + subgraph "app-shared-net" + REACT[react-web
:80] + ELYSIA[elysia-api
:4092] + RUST_AUTH[rust-auth
:3000] + SCRAPER[scraper-api
:4091] + REDIS[redis
:6379] + end + end + + subgraph "Imrnes VPS" + PG[(PostgreSQL
:5432)] + REDIS_IMR[Redis
:6379] + 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 +``` + +## Observability + +- **Prometheus metrics**: Available on rust-auth via `axum-prometheus` +- **Traefik access logs**: JSON format, logged at INFO level +- **Dashboard**: Traefik dashboard at `traefik.asepharyana.my.id` (secured) diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 0000000..7c96c42 --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,3 @@ +# Authors + +- Asep Haryana Saputra (@asepharyana) - Project maintainer diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5587792 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Changed + +- 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. + +### 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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..2b7c9ab --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,239 @@ +# Contributing to Asepharyana Hub + +## Table of Contents + +- [Prerequisites](#prerequisites) +- [Local Setup](#local-setup) +- [Development Workflow](#development-workflow) +- [Project Structure](#project-structure) +- [Coding Standards](#coding-standards) +- [Commit Message Format](#commit-message-format) +- [Pull Request Process](#pull-request-process) +- [Adding a New Service](#adding-a-new-service) + +## Prerequisites + +- **Git** with LFS support +- **Node.js** >= 22.11.0 (via `.node-version` or `.nvmrc`) +- **Bun** >= 1.3.11 (package manager) +- **Rust** >= 1.89.0 (for Rust services) +- **Docker** and **Docker Compose** (for shared infrastructure) + +## Local Setup + +### 1. Clone the Repository + +```bash +git clone https://github.com/asepharyana/asepharyana-hub.git +cd asepharyana-hub +``` + +### 2. Initialize Submodules + +This hub repo uses Git submodules for all application services: + +```bash +git submodule update --init --recursive +``` + +This checks out all submodules at the pinned commit (not `main`). The submodules and their remotes are: + +| 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 ../.. +``` + +### 4. Start Shared Infrastructure + +Start shared services (Redis) via Docker Compose: + +```bash +docker compose -f infra/compose/shared.yml up -d +``` + +### 5. Configure Environment + +Copy the example environment file and adjust as needed: + +```bash +cp .env.example .env +``` + +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 +``` + +### API Documentation + +- Rust OpenAPI: `http://localhost:4091/docs` +- Elysia Swagger: `http://localhost:4092/docs` +- Elysia AsyncAPI: `http://localhost:4092/docs-ws` + +## Coding Standards + +### Linting + +- **ESLint** with `@antfu/eslint-config` for TypeScript/JavaScript +- **Cargo Clippy** for Rust + +Run linting: + +```bash +# TypeScript/JavaScript +eslint . --no-error-on-unmatched-pattern + +# Rust specific +cd apps/rust-auth && cargo clippy -- -D warnings +``` + +### Formatting + +- **Prettier** for TypeScript/JavaScript/Markdown (config in `.prettierrc`) + - Single quotes, 100 print width, 2-space indent, trailing commas +- **Cargo fmt** for Rust +- **EditorConfig** for general formatting (`.editorconfig`) + +```bash +# Prettier +prettier --write . + +# Rust +cd apps/rust-auth && cargo fmt +``` + +### Rust Configuration + +Rust services use edition `2024` with stable toolchain (nightly features may be used). + +## Commit Message Format + +This project enforces **Conventional Commits** for all commit messages. + +### Format + +``` +(): + +[optional body] + +[optional footer] +``` + +### Types + +| Type | Usage | +| ---------- | ----------------------------------------------------------- | +| `feat` | A new feature | +| `fix` | A bug fix | +| `chore` | Maintenance, config, tooling changes | +| `docs` | Documentation only changes | +| `refactor` | Code change that neither fixes a bug nor adds a feature | +| `test` | Adding or updating tests | +| `ci` | CI/CD configuration and scripts | +| `style` | Formatting, missing semicolons, etc. (no production change) | +| `perf` | Performance improvement | + +### Examples + +``` +feat(rust-auth): add OAuth2 Google login flow +fix(elysia): handle null JWT payload in auth middleware +chore: update eslint 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` + +## Pull Request Process + +1. **Create a branch** from `main` with a descriptive name: + - `feat/my-feature` + - `fix/issue-description` + - `chore/update-config` + +2. **Make your changes** following the coding standards above. + +3. **Run checks locally** before pushing: + + ```bash + cd apps/react && npx tsc --noEmit + eslint . --no-error-on-unmatched-pattern + ``` + +4. **Push and open a PR** against `main`. CI will automatically run: + - **Lint** β€” ESLint across changed TypeScript files + - **TypeCheck** β€” TypeScript compilation check + - **Security** β€” CodeQL analysis (weekly schedule + PRs) + +5. **Docker Build Pipeline** triggers on pushes to `main` when `apps/**` changes: + - Detects which services changed + - Builds Docker images for only those services + - Pushes to GHCR with `latest` and `sha-` tags + - 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`) + - Pulls updated Docker images + - Recreates only the changed containers + - All services share the `app-shared-net` Docker network + +7. **Merge** after CI passes and you have at least one approval (if applicable). Use squash merge to keep history clean. + +## 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/` +2. Add it as a Git submodule in `.gitmodules` +3. Register it in `infra/compose/.yml` +4. Add a Dockerfile at `infra/docker/.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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..77fd3f7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Asep Haryana Saputra + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6796ffe --- /dev/null +++ b/Makefile @@ -0,0 +1,32 @@ +.PHONY: help dev lint format test clean update-submodules deploy init-submodules status + +SHELL := /bin/bash + +help: ## Show this help + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' + +dev: ## Start development infrastructure (Redis etc.) + docker compose -f infra/compose/shared.yml up -d + +lint: ## Run ESLint across the repo + eslint . --no-error-on-unmatched-pattern + +format: ## Format code with Prettier + prettier --write "**/*.{ts,tsx,js,jsx,json,md,yaml,yml}" + +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 + +deploy: ## Deploy to VPS (triggers GitHub Actions) + @echo "Push to main to trigger deployment, or run:" + @echo " gh workflow run deploy-docker.yml" + +init-submodules: ## Initialize all submodules + git submodule update --init --recursive + +status: ## Show submodule status + git submodule status diff --git a/README.md b/README.md new file mode 100644 index 0000000..c653559 --- /dev/null +++ b/README.md @@ -0,0 +1,201 @@ +# Asepharyana Hub + +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. + +## 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 . +``` + +Tag and push: + +```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 +``` + +## Local Development + +### 1) Jalankan dependency bersama + +```bash +docker compose -f infra/compose/shared.yml up -d +``` + +### 2) Jalankan service yang dibutuhkan + +```bash +# Rust API +cd apps/rust-auth +cargo run + +# Elysia API +cd apps/elysia +bun install +bun run dev + +# React web +cd apps/react +npm install +npm run dev +``` + +## API Docs and Monitoring + +- Rust OpenAPI: `/docs` +- Elysia Swagger: `/docs` +- Elysia AsyncAPI viewer: `/docs-ws` + +## Deployment Notes + +- Pipeline memakai image tag berbasis commit SHA (`sha-`), bukan `latest`. +- Deploy Compose sekarang mencakup `infra/compose/*.yml` dan `deploy-docker.yml` akan berjalan langsung ketika `infra/compose/**` berubah. + +## 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: + +```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 + +Service yang connect ke Tailscale IP: + +```env +# PostgreSQL di imrnes +DATABASE_URL=postgres://user:pass@100.108.1.124:5432/dbname + +# Redis di imrnes +REDIS_URL=redis://100.108.1.124:6379 +``` + +#### Persistent Systemd Service + +File: `/etc/systemd/system/tailscale-routes.service` + +```ini +[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 +``` + +Install & enable: + +```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 +``` + +#### 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 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 diff --git a/apps/elysia/.gitignore b/apps/elysia/.gitignore new file mode 100644 index 0000000..4ca0fe3 --- /dev/null +++ b/apps/elysia/.gitignore @@ -0,0 +1,75 @@ +node_modules +dist +.env +.env.local +*.log +.DS_Store + +/generated/prisma +apps/gmw/ +# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# compiled output +dist +tmp +out-tsc +error.log +# dependencies +node_modules +.bun/** +.turbo/ +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace +**/target/** +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db +.claude +# Next.js +.next +out +**/.codegraph/** +**/.claude/** +test-output +**/**.env +**/**.env.** +vite.config.*.timestamp* +vitest.config.*.timestamp* + + +storybook-static +~/.bun/** +docs/dependency-map.md +docs/handoff-log.jsonl +docs/observability.md +docs/quality-gates.json +docs/workflow-state.json +docs/todo.md +**/vendor/ + +# moonrepo +.moon/cache +.~moon** \ No newline at end of file diff --git a/apps/react/.gitignore b/apps/react/.gitignore new file mode 100644 index 0000000..9edfe15 --- /dev/null +++ b/apps/react/.gitignore @@ -0,0 +1,9 @@ +node_modules +dist +*.tsbuildinfo +.env* +.claude/ +.codegraph/ +.code-review-graph/ +.superpowers/ +.remember/ diff --git a/apps/rust-auth/.gitignore b/apps/rust-auth/.gitignore new file mode 100644 index 0000000..e40812c --- /dev/null +++ b/apps/rust-auth/.gitignore @@ -0,0 +1,7 @@ +target/ +.env +*.swp +*.swo +.DS_Store +**/.codegraph/** +**/.claude/** \ No newline at end of file diff --git a/apps/scraper/.gitignore b/apps/scraper/.gitignore new file mode 100644 index 0000000..c9eb931 --- /dev/null +++ b/apps/scraper/.gitignore @@ -0,0 +1,67 @@ +apps/gmw/ +# See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files. + +# compiled output +dist +tmp +out-tsc +error.log +# dependencies +node_modules +.bun/** +.turbo/ +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace +**/target/** +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# misc +/.sass-cache +/connect.lock +/coverage +/libpeerconnection.log +npm-debug.log +yarn-error.log +testem.log +/typings + +# System Files +.DS_Store +Thumbs.db +.claude +# Next.js +.next +out +**/.codegraph/** +**/.claude/** +test-output +**/**.env +**/**.env.** +vite.config.*.timestamp* +vitest.config.*.timestamp* + + +storybook-static +~/.bun/** +docs/dependency-map.md +docs/handoff-log.jsonl +docs/observability.md +docs/quality-gates.json +docs/workflow-state.json +docs/todo.md +**/vendor/ + +# moonrepo +.moon/cache +.~moon** \ No newline at end of file diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..bc05b50 --- /dev/null +++ b/bun.lock @@ -0,0 +1,488 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "ultimate-asepharyana.tech", + "devDependencies": { + "@commitlint/cli": "^19.7.1", + "@commitlint/config-conventional": "^19.7.1", + "eslint": "^9.18.0", + "husky": "^9.1.7", + "lint-staged": "^15.4.3", + "prettier": "^3.4.2", + }, + }, + }, + "packages": { + "@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], + + "@commitlint/cli": ["@commitlint/cli@19.8.1", "", { "dependencies": { "@commitlint/format": "^19.8.1", "@commitlint/lint": "^19.8.1", "@commitlint/load": "^19.8.1", "@commitlint/read": "^19.8.1", "@commitlint/types": "^19.8.1", "tinyexec": "^1.0.0", "yargs": "^17.0.0" }, "bin": { "commitlint": "./cli.js" } }, "sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA=="], + + "@commitlint/config-conventional": ["@commitlint/config-conventional@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "conventional-changelog-conventionalcommits": "^7.0.2" } }, "sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ=="], + + "@commitlint/config-validator": ["@commitlint/config-validator@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "ajv": "^8.11.0" } }, "sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ=="], + + "@commitlint/ensure": ["@commitlint/ensure@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "lodash.camelcase": "^4.3.0", "lodash.kebabcase": "^4.1.1", "lodash.snakecase": "^4.1.1", "lodash.startcase": "^4.4.0", "lodash.upperfirst": "^4.3.1" } }, "sha512-mXDnlJdvDzSObafjYrOSvZBwkD01cqB4gbnnFuVyNpGUM5ijwU/r/6uqUmBXAAOKRfyEjpkGVZxaDsCVnHAgyw=="], + + "@commitlint/execute-rule": ["@commitlint/execute-rule@19.8.1", "", {}, "sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA=="], + + "@commitlint/format": ["@commitlint/format@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "chalk": "^5.3.0" } }, "sha512-kSJj34Rp10ItP+Eh9oCItiuN/HwGQMXBnIRk69jdOwEW9llW9FlyqcWYbHPSGofmjsqeoxa38UaEA5tsbm2JWw=="], + + "@commitlint/is-ignored": ["@commitlint/is-ignored@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "semver": "^7.6.0" } }, "sha512-AceOhEhekBUQ5dzrVhDDsbMaY5LqtN8s1mqSnT2Kz1ERvVZkNihrs3Sfk1Je/rxRNbXYFzKZSHaPsEJJDJV8dg=="], + + "@commitlint/lint": ["@commitlint/lint@19.8.1", "", { "dependencies": { "@commitlint/is-ignored": "^19.8.1", "@commitlint/parse": "^19.8.1", "@commitlint/rules": "^19.8.1", "@commitlint/types": "^19.8.1" } }, "sha512-52PFbsl+1EvMuokZXLRlOsdcLHf10isTPlWwoY1FQIidTsTvjKXVXYb7AvtpWkDzRO2ZsqIgPK7bI98x8LRUEw=="], + + "@commitlint/load": ["@commitlint/load@19.8.1", "", { "dependencies": { "@commitlint/config-validator": "^19.8.1", "@commitlint/execute-rule": "^19.8.1", "@commitlint/resolve-extends": "^19.8.1", "@commitlint/types": "^19.8.1", "chalk": "^5.3.0", "cosmiconfig": "^9.0.0", "cosmiconfig-typescript-loader": "^6.1.0", "lodash.isplainobject": "^4.0.6", "lodash.merge": "^4.6.2", "lodash.uniq": "^4.5.0" } }, "sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A=="], + + "@commitlint/message": ["@commitlint/message@19.8.1", "", {}, "sha512-+PMLQvjRXiU+Ae0Wc+p99EoGEutzSXFVwQfa3jRNUZLNW5odZAyseb92OSBTKCu+9gGZiJASt76Cj3dLTtcTdg=="], + + "@commitlint/parse": ["@commitlint/parse@19.8.1", "", { "dependencies": { "@commitlint/types": "^19.8.1", "conventional-changelog-angular": "^7.0.0", "conventional-commits-parser": "^5.0.0" } }, "sha512-mmAHYcMBmAgJDKWdkjIGq50X4yB0pSGpxyOODwYmoexxxiUCy5JJT99t1+PEMK7KtsCtzuWYIAXYAiKR+k+/Jw=="], + + "@commitlint/read": ["@commitlint/read@19.8.1", "", { "dependencies": { "@commitlint/top-level": "^19.8.1", "@commitlint/types": "^19.8.1", "git-raw-commits": "^4.0.0", "minimist": "^1.2.8", "tinyexec": "^1.0.0" } }, "sha512-03Jbjb1MqluaVXKHKRuGhcKWtSgh3Jizqy2lJCRbRrnWpcM06MYm8th59Xcns8EqBYvo0Xqb+2DoZFlga97uXQ=="], + + "@commitlint/resolve-extends": ["@commitlint/resolve-extends@19.8.1", "", { "dependencies": { "@commitlint/config-validator": "^19.8.1", "@commitlint/types": "^19.8.1", "global-directory": "^4.0.1", "import-meta-resolve": "^4.0.0", "lodash.mergewith": "^4.6.2", "resolve-from": "^5.0.0" } }, "sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg=="], + + "@commitlint/rules": ["@commitlint/rules@19.8.1", "", { "dependencies": { "@commitlint/ensure": "^19.8.1", "@commitlint/message": "^19.8.1", "@commitlint/to-lines": "^19.8.1", "@commitlint/types": "^19.8.1" } }, "sha512-Hnlhd9DyvGiGwjfjfToMi1dsnw1EXKGJNLTcsuGORHz6SS9swRgkBsou33MQ2n51/boIDrbsg4tIBbRpEWK2kw=="], + + "@commitlint/to-lines": ["@commitlint/to-lines@19.8.1", "", {}, "sha512-98Mm5inzbWTKuZQr2aW4SReY6WUukdWXuZhrqf1QdKPZBCCsXuG87c+iP0bwtD6DBnmVVQjgp4whoHRVixyPBg=="], + + "@commitlint/top-level": ["@commitlint/top-level@19.8.1", "", { "dependencies": { "find-up": "^7.0.0" } }, "sha512-Ph8IN1IOHPSDhURCSXBz44+CIu+60duFwRsg6HqaISFHQHbmBtxVw4ZrFNIYUzEP7WwrNPxa2/5qJ//NK1FGcw=="], + + "@commitlint/types": ["@commitlint/types@19.8.1", "", { "dependencies": { "@types/conventional-commits-parser": "^5.0.0", "chalk": "^5.3.0" } }, "sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], + + "@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw=="], + + "@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="], + + "@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg=="], + + "@eslint/js": ["@eslint/js@9.39.4", "", {}, "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], + + "@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="], + + "@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="], + + "@humanfs/types": ["@humanfs/types@0.15.0", "", {}, "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + + "@types/conventional-commits-parser": ["@types/conventional-commits-parser@5.0.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@25.9.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw=="], + + "JSONStream": ["JSONStream@1.3.5", "", { "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" }, "bin": { "JSONStream": "./bin.js" } }, "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ=="], + + "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], + + "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "array-ify": ["array-ify@1.0.0", "", {}, "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "brace-expansion": ["brace-expansion@1.1.15", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], + + "cli-truncate": ["cli-truncate@4.0.0", "", { "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" } }, "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + + "compare-func": ["compare-func@2.0.0", "", { "dependencies": { "array-ify": "^1.0.0", "dot-prop": "^5.1.0" } }, "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "conventional-changelog-angular": ["conventional-changelog-angular@7.0.0", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ=="], + + "conventional-changelog-conventionalcommits": ["conventional-changelog-conventionalcommits@7.0.2", "", { "dependencies": { "compare-func": "^2.0.0" } }, "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w=="], + + "conventional-commits-parser": ["conventional-commits-parser@5.0.0", "", { "dependencies": { "JSONStream": "^1.3.5", "is-text-path": "^2.0.0", "meow": "^12.0.1", "split2": "^4.0.0" }, "bin": { "conventional-commits-parser": "cli.mjs" } }, "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA=="], + + "cosmiconfig": ["cosmiconfig@9.0.1", "", { "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ=="], + + "cosmiconfig-typescript-loader": ["cosmiconfig-typescript-loader@6.3.0", "", { "dependencies": { "jiti": "2.6.1" }, "peerDependencies": { "@types/node": "*", "cosmiconfig": ">=9", "typescript": ">=5" } }, "sha512-Akr82WH1Wfqatyiqpj8HDkO2o2KmJRu1FhKfSNJP3K4IdXwHfEyL7MOb62i1AGQVLtIQM+iCE9CGOtrfhR+mmA=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "dargs": ["dargs@8.1.0", "", {}, "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "dot-prop": ["dot-prop@5.3.0", "", { "dependencies": { "is-obj": "^2.0.0" } }, "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + + "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + + "error-ex": ["error-ex@1.3.4", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + + "eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="], + + "espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="], + + "esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], + + "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.4.2", "", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="], + + "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], + + "git-raw-commits": ["git-raw-commits@4.0.0", "", { "dependencies": { "dargs": "^8.0.0", "meow": "^12.0.1", "split2": "^4.0.0" }, "bin": { "git-raw-commits": "cli.mjs" } }, "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "global-directory": ["global-directory@4.0.1", "", { "dependencies": { "ini": "4.1.1" } }, "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q=="], + + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], + + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "ini": ["ini@4.1.1", "", {}, "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g=="], + + "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "is-obj": ["is-obj@2.0.0", "", {}, "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w=="], + + "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], + + "is-text-path": ["is-text-path@2.0.0", "", { "dependencies": { "text-extensions": "^2.0.0" } }, "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], + + "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], + + "lint-staged": ["lint-staged@15.5.2", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^13.1.0", "debug": "^4.4.0", "execa": "^8.0.1", "lilconfig": "^3.1.3", "listr2": "^8.2.5", "micromatch": "^4.0.8", "pidtree": "^0.6.0", "string-argv": "^0.3.2", "yaml": "^2.7.0" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-YUSOLq9VeRNAo/CTaVmhGDKG+LBtA8KF1X4K5+ykMSwWST1vDxJRB2kv2COgLb1fvpCo+A/y9A0G0znNVmdx4w=="], + + "listr2": ["listr2@8.3.3", "", { "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", "eventemitter3": "^5.0.1", "log-update": "^6.1.0", "rfdc": "^1.4.1", "wrap-ansi": "^9.0.0" } }, "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.kebabcase": ["lodash.kebabcase@4.1.1", "", {}, "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "lodash.mergewith": ["lodash.mergewith@4.6.2", "", {}, "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ=="], + + "lodash.snakecase": ["lodash.snakecase@4.1.1", "", {}, "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="], + + "lodash.startcase": ["lodash.startcase@4.4.0", "", {}, "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg=="], + + "lodash.uniq": ["lodash.uniq@4.5.0", "", {}, "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ=="], + + "lodash.upperfirst": ["lodash.upperfirst@4.3.1", "", {}, "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg=="], + + "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], + + "meow": ["meow@12.1.1", "", {}, "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw=="], + + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], + + "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], + + "minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], + + "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "parse-json": ["parse-json@5.2.0", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + + "pidtree": ["pidtree@0.6.0", "", { "bin": { "pidtree": "bin/pidtree.js" } }, "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], + + "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], + + "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], + + "semver": ["semver@7.8.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-c8jsqUZm3omBOI66G90z1Dyw5z622G8oLG+omfsHBJf3CWQTlOcwOjvOG6wtiNfW6anKm/eA39LMwMtMez2TiQ=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "text-extensions": ["text-extensions@2.4.0", "", {}, "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g=="], + + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], + + "tinyexec": ["tinyexec@1.2.4", "", {}, "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], + + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], + + "unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "@commitlint/config-validator/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "@commitlint/format/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "@commitlint/load/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "@commitlint/top-level/find-up": ["find-up@7.0.0", "", { "dependencies": { "locate-path": "^7.2.0", "path-exists": "^5.0.0", "unicorn-magic": "^0.1.0" } }, "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g=="], + + "@commitlint/types/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "cli-truncate/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "import-fresh/resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "lint-staged/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "log-update/slice-ansi": ["slice-ansi@7.1.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-iOBWFgUX7caIZiuutICxVgX1SdxwAVFFKwt1EvMYYec/NWO5meOJ6K5uQxhrYBdQJne4KxiqZc+KptFOWFSI9w=="], + + "log-update/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + + "restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], + + "slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="], + + "wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + + "wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "@commitlint/config-validator/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "@commitlint/top-level/find-up/locate-path": ["locate-path@7.2.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA=="], + + "@commitlint/top-level/find-up/path-exists": ["path-exists@5.0.0", "", {}, "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ=="], + + "cli-truncate/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "cli-truncate/string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "log-update/slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@5.1.0", "", { "dependencies": { "get-east-asian-width": "^1.3.1" } }, "sha512-5XHYaSyiqADb4RnZ1Bdad6cPp8Toise4TzEjcOYDHZkTCbKgiUl7WTUCpNWHuxmDt91wnsZBc9xinNzopv3JMQ=="], + + "log-update/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "wrap-ansi/string-width/emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + + "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "@commitlint/top-level/find-up/locate-path/p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], + + "cli-truncate/string-width/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "@commitlint/top-level/find-up/locate-path/p-locate/p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], + + "@commitlint/top-level/find-up/locate-path/p-locate/p-limit/yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + } +} diff --git a/docs/add-new-app.md b/docs/add-new-app.md new file mode 100644 index 0000000..5f8d9a0 --- /dev/null +++ b/docs/add-new-app.md @@ -0,0 +1,124 @@ +# Menambahkan Aplikasi Baru ke Deployment + +Dokumen ini menjelaskan langkah menambahkan service baru ke `asepharyana-hub`. Root repo berfungsi sebagai hub: source aplikasi berada di `apps/` sebagai submodule, sedangkan Docker Compose, Traefik, dan workflow deploy tetap berada di root repo. + +## 1. Buat repo aplikasi + +Buat repo baru di GitHub dengan pola nama: + +```text +https://github.com/asepharyana/asepharyana-hub-.git +``` + +Lalu tambahkan ke root hub sebagai submodule: + +```bash +git submodule add https://github.com/asepharyana/asepharyana-hub-.git apps/ +git submodule update --init --recursive +``` + +## 2. Tambahkan Dockerfile + +Tambahkan Dockerfile runtime di `infra/docker/.Dockerfile`. + +Gunakan root repo sebagai build context agar Dockerfile bisa mengakses submodule path: + +```bash +docker build -f infra/docker/.Dockerfile -t :local . +``` + +## 3. Tambahkan Compose file + +Buat `infra/compose/.yml`: + +```yaml +services: + : + container_name: + image: ghcr.io/asepharyana/asepharyana-hub/:sha- + restart: always + networks: + app-shared-net: + aliases: + - + env_file: + - ../../.env + +networks: + app-shared-net: + name: app-shared-net + external: true +``` + +Gunakan `app-shared-net` agar service dapat diakses oleh Traefik dan service lain. + +## 4. Tambahkan route Traefik + +Update `infra/traefik/dynamic/apps.yaml`: + +```yaml +http: + routers: + : + rule: 'Host(`.asepharyana.my.id`) || Host(`.asepharyana.web.id`)' + entryPoints: + - websecure + tls: {} + middlewares: + - common-chain@file + service: -service + + services: + -service: + loadBalancer: + servers: + - url: 'http://:' +``` + +## 5. Update workflow build + +Update `.github/workflows/docker-build-push.yml`: + +1. Tambahkan path detection untuk `apps/` dan `infra/docker/.Dockerfile`. +2. Tambahkan service ke matrix build. +3. Tambahkan mapping Dockerfile di step `Docker metadata`. +4. Tambahkan mapping compose file dan submodule path di step `Update tags and submodules`. + +## 6. Update workflow deploy + +Tambahkan compose file baru ke `ALL_COMPOSE_FILES` di `.github/workflows/deploy-docker.yml`: + +```bash +infra/compose/.yml +``` + +## 7. Update dokumentasi + +Update file berikut bila service baru mengubah arsitektur publik: + +- `README.md` +- `ARCHITECTURE.md` +- `infra/README.md` +- `.gitmodules` + +## 8. Validasi + +Jalankan validasi YAML dan compose rendering: + +```bash +python - <<'PY' +import pathlib, yaml +for path in pathlib.Path('infra').rglob('*.yml'): + with path.open() as fh: + yaml.safe_load(fh) + print(f'OK {path}') +for path in pathlib.Path('infra').rglob('*.yaml'): + with path.open() as fh: + yaml.safe_load(fh) + print(f'OK {path}') +PY + +for f in infra/compose/*.yml; do + docker compose -f "$f" config >/dev/null && echo "OK $f" +done +``` diff --git a/docs/adr/0001-use-hub-repo-with-submodules.md b/docs/adr/0001-use-hub-repo-with-submodules.md new file mode 100644 index 0000000..b78bb2a --- /dev/null +++ b/docs/adr/0001-use-hub-repo-with-submodules.md @@ -0,0 +1,52 @@ +# ADR 0001: Use a Hub Repository with App Submodules + +## Status + +Accepted + +## Context + +The project contains multiple independent application services that share one deployment surface: Docker Compose, Traefik routing, GitHub Actions workflows, and operational documentation. + +The services should be developed and versioned independently, while deployment infrastructure should remain centralized so production routing and compose manifests stay consistent. + +## Decision + +Use `asepharyana-hub` as the root hub repository. + +- Application code lives under `apps/` as Git submodules. +- Infrastructure lives in the root repo under `infra/`. +- Documentation lives in the root repo under `docs/`. +- CI/CD workflows live in the root repo under `.github/workflows/`. +- Root tooling stays minimal: `package.json`, Prettier, ESLint, Makefile helpers, and deployment scripts. + +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 + +### Positive + +- Each app can evolve in its own repository. +- The hub pins exact submodule revisions for reproducible deployments. +- Deployment infrastructure remains centralized and easier to audit. +- Root tooling stays lightweight and does not impose one build system on every service. + +### Negative + +- Developers must understand Git submodule workflows. +- Updating a service requires updating the submodule pointer in the hub repo. +- Cross-service changes require coordinating commits across multiple repositories. + +### Mitigations + +- Keep `.gitmodules` accurate and minimal. +- Use `scripts/sync-submodules.sh` for local checkout consistency. +- Document service-addition steps in `docs/add-new-app.md`. +- Keep GitHub Actions responsible for Docker image builds, compose tag updates, and deployments. diff --git a/docs/adr/README.md b/docs/adr/README.md new file mode 100644 index 0000000..49859d3 --- /dev/null +++ b/docs/adr/README.md @@ -0,0 +1,23 @@ +# Architecture Decision Records + +This directory contains Architecture Decision Records (ADRs). + +Each ADR documents an architectural decision, its context, and its consequences. + +## How to use + +```bash +coder-workflow adr new "" +coder-workflow adr list +coder-workflow adr status --status accepted +coder-workflow adr graph +``` + +## Status Definitions + +| Status | Meaning | +| ---------- | --------------------------- | +| Proposed | Under discussion | +| Accepted | Agreed upon and implemented | +| Deprecated | No longer recommended | +| Superseded | Replaced by a newer ADR | diff --git a/docs/config/squid.conf.archived b/docs/config/squid.conf.archived new file mode 100644 index 0000000..42e9cc5 --- /dev/null +++ b/docs/config/squid.conf.archived @@ -0,0 +1,39 @@ +# 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. diff --git a/docs/superpowers/plans/2026-05-24-moonrepo-migration.md b/docs/superpowers/plans/2026-05-24-moonrepo-migration.md new file mode 100644 index 0000000..72a935c --- /dev/null +++ b/docs/superpowers/plans/2026-05-24-moonrepo-migration.md @@ -0,0 +1,686 @@ +# Moonrepo Migration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate the monorepo to moonrepo for centralized task orchestration, caching, and consistent dependency management across all 7 apps. + +**Architecture:** moonrepo sits as a task orchestration layer above the existing Nix build system. `.moon/tasks/` holds shared task definitions (TypeScript and Rust). Each app gets a `moon.yml` with tags and inter-project dependencies. Nix, Docker Compose, git submodules, and infra remain untouched. + +**Tech Stack:** moonrepo CLI, Node 22, Bun, TypeScript 5+, Cargo (Rust) + +--- + +### Task 1: Install moonrepo CLI and scaffold .moon/ directory + +**Files:** +- Create: `.moon/workspace.yml` +- Create: `.moon/toolchain.yml` +- Create: `.moon/tasks/` (directory) + +- [ ] **Step 1: Install moonrepo CLI via curl** + +```bash +curl -fsSL https://moonrepo.dev/install/moon.sh | bash +``` + +- [ ] **Step 2: Verify installation** + +Run: `moon --version` +Expected: prints version number (e.g., `moon 1.x.x`) + +- [ ] **Step 3: Create .moon/ scaffold directories** + +```bash +mkdir -p .moon/tasks +``` + +- [ ] **Step 4: Commit** + +```bash +git add .moon/ +git commit -m "chore: scaffold .moon/ directory for moonrepo" +``` + +--- + +### Task 2: Configure workspace.yml + +**Files:** +- Create: `.moon/workspace.yml` + +- [ ] **Step 1: Write .moon/workspace.yml** + +```yaml +# https://moonrepo.dev/docs/config/workspace +$schema: "https://moonrepo.dev/schemas/workspace.json" + +projects: + - "apps/*" + +vcs: + manager: "git" + defaultBranch: "main" + +runner: + implicitDeps: + # TypeScript apps: lint depends on build (for typecheck path) + - "typescript-build.build" + + cacheTtl: 604800 +``` + +- [ ] **Step 2: Validate config structure** + +Run: `moon check` +Expected: no errors (will warn about missing project configs β€” expected) + +- [ ] **Step 3: Commit** + +```bash +git add .moon/workspace.yml +git commit -m "chore: configure moonrepo workspace with project glob" +``` + +--- + +### Task 3: Configure toolchain.yml + +**Files:** +- Create: `.moon/toolchain.yml` + +- [ ] **Step 1: Write .moon/toolchain.yml** + +```yaml +# https://moonrepo.dev/docs/config/toolchain +$schema: "https://moonrepo.dev/schemas/toolchain.json" + +node: + version: "22.11.0" + packageManager: "bun" + bun: + version: "1.3.11" + +typescript: + syncProjectReferences: true + createMissingConfig: false + routeOutDirToCache: false +``` + +- [ ] **Step 2: Commit** + +```bash +git add .moon/toolchain.yml +git commit -m "chore: configure moonrepo toolchain (Node 22, Bun 1.3)" +``` + +--- + +### Task 4: Create shared TypeScript task definitions + +**Files:** +- Create: `.moon/tasks/typescript-build.yml` +- Create: `.moon/tasks/typescript-lint.yml` +- Create: `.moon/tasks/typescript-test.yml` + +- [ ] **Step 1: Write .moon/tasks/typescript-build.yml** + +```yaml +# https://moonrepo.dev/docs/config/tasks +$schema: "https://moonrepo.dev/schemas/tasks.json" + +tasks: + build: + command: "bun run build" + inputs: + - "src/**/*" + - "tsconfig.json" + - "package.json" + outputs: + - ".next" + - "dist" + - ".output" + options: + cache: true + dev: + command: "bun run dev" + local: true + options: + persistent: true +``` + +- [ ] **Step 2: Write .moon/tasks/typescript-lint.yml** + +```yaml +# https://moonrepo.dev/docs/config/tasks +$schema: "https://moonrepo.dev/schemas/tasks.json" + +tasks: + lint: + command: "bun run lint" + inputs: + - "src/**/*" + - "eslint.config.mjs" + - "tsconfig.json" + options: + cache: false + typecheck: + command: "bun run check-types" + inputs: + - "src/**/*" + - "tsconfig.json" + options: + cache: false +``` + +- [ ] **Step 3: Write .moon/tasks/typescript-test.yml** + +```yaml +# https://moonrepo.dev/docs/config/tasks +$schema: "https://moonrepo.dev/schemas/tasks.json" + +tasks: + test: + command: "bun test" + inputs: + - "src/**/*" + - "test/**/*" + - "tests/**/*" + - "vitest.config.ts" + options: + cache: false + e2e: + command: "noop" + local: true + options: + cache: false +``` + +- [ ] **Step 4: Commit** + +```bash +git add .moon/tasks/typescript-build.yml .moon/tasks/typescript-lint.yml .moon/tasks/typescript-test.yml +git commit -m "chore: add shared TypeScript task definitions" +``` + +--- + +### Task 5: Create shared Rust task definitions + +**Files:** +- Create: `.moon/tasks/rust-build.yml` +- Create: `.moon/tasks/rust-test.yml` +- Create: `.moon/tasks/rust-lint.yml` + +- [ ] **Step 1: Write .moon/tasks/rust-build.yml** + +```yaml +# https://moonrepo.dev/docs/config/tasks +$schema: "https://moonrepo.dev/schemas/tasks.json" + +tasks: + build: + command: "cargo build --release" + platform: system + inputs: + - "src/**/*" + - "Cargo.toml" + - "Cargo.lock" + - "build.rs" + outputs: + - "target/release/*" + options: + cache: true + envFile: false + dev: + command: "cargo run" + platform: system + local: true + options: + persistent: true + envFile: false +``` + +- [ ] **Step 2: Write .moon/tasks/rust-test.yml** + +```yaml +# https://moonrepo.dev/docs/config/tasks +$schema: "https://moonrepo.dev/schemas/tasks.json" + +tasks: + test: + command: "cargo test" + platform: system + inputs: + - "src/**/*" + - "Cargo.toml" + - "Cargo.lock" + - "tests/**/*" + options: + cache: false + envFile: false +``` + +- [ ] **Step 3: Write .moon/tasks/rust-lint.yml** + +```yaml +# https://moonrepo.dev/docs/config/tasks +$schema: "https://moonrepo.dev/schemas/tasks.json" + +tasks: + lint: + command: "cargo clippy -- -D warnings" + platform: system + inputs: + - "src/**/*" + - "Cargo.toml" + options: + cache: false + envFile: false + fmt-check: + command: "cargo fmt --check" + platform: system + inputs: + - "src/**/*" + options: + cache: false + envFile: false +``` + +- [ ] **Step 4: Commit** + +```bash +git add .moon/tasks/rust-build.yml .moon/tasks/rust-test.yml .moon/tasks/rust-lint.yml +git commit -m "chore: add shared Rust task definitions" +``` + +--- + +### Task 6: Create per-app moon.yml for TypeScript apps + +**Files:** +- Create: `apps/nextjs/moon.yml` +- Create: `apps/elysia/moon.yml` +- Create: `apps/solidjs/moon.yml` + +- [ ] **Step 1: Write apps/nextjs/moon.yml** + +```yaml +# https://moonrepo.dev/docs/config/project +$schema: "https://moonrepo.dev/schemas/project.json" + +type: "application" +language: "typescript" +platform: "node" +tags: + - "lang:typescript" + - "type:frontend" + +dependsOn: + - id: "rust-auth" + - id: "elysia" +``` + +- [ ] **Step 2: Write apps/elysia/moon.yml** + +```yaml +# https://moonrepo.dev/docs/config/project +$schema: "https://moonrepo.dev/schemas/project.json" + +type: "application" +language: "typescript" +platform: "bun" +tags: + - "lang:typescript" + - "type:backend" +``` + +- [ ] **Step 3: Write apps/solidjs/moon.yml** + +```yaml +# https://moonrepo.dev/docs/config/project +$schema: "https://moonrepo.dev/schemas/project.json" + +type: "application" +language: "typescript" +platform: "bun" +tags: + - "lang:typescript" + - "type:frontend" + +dependsOn: + - id: "elysia" + - id: "rust-auth" +``` + +- [ ] **Step 4: Commit** + +```bash +git add apps/nextjs/moon.yml apps/elysia/moon.yml apps/solidjs/moon.yml +git commit -m "chore: add moon.yml for TypeScript apps (nextjs, elysia, solidjs)" +``` + +--- + +### Task 7: Create per-app moon.yml for Rust apps + +**Files:** +- Create: `apps/rust/moon.yml` +- Create: `apps/rust-auth/moon.yml` +- Create: `apps/leptos/moon.yml` + +- [ ] **Step 1: Write apps/rust/moon.yml** + +```yaml +# https://moonrepo.dev/docs/config/project +$schema: "https://moonrepo.dev/schemas/project.json" + +type: "application" +language: "rust" +platform: "system" +tags: + - "lang:rust" + - "type:backend" + +fileGroups: + sources: + - "src/**/*.rs" + - "Cargo.toml" + - "Cargo.lock" + - "build.rs" + - "rustfmt.toml" +``` + +- [ ] **Step 2: Write apps/rust-auth/moon.yml** + +```yaml +# https://moonrepo.dev/docs/config/project +$schema: "https://moonrepo.dev/schemas/project.json" + +type: "application" +language: "rust" +platform: "system" +tags: + - "lang:rust" + - "type:backend" + +fileGroups: + sources: + - "src/**/*.rs" + - "Cargo.toml" + - "Cargo.lock" +``` + +- [ ] **Step 3: Write apps/leptos/moon.yml** + +```yaml +# https://moonrepo.dev/docs/config/project +$schema: "https://moonrepo.dev/schemas/project.json" + +type: "application" +language: "rust" +platform: "system" +tags: + - "lang:rust" + - "type:frontend" + +dependsOn: + - id: "rust" + +fileGroups: + sources: + - "src/**/*.rs" + - "Cargo.toml" + - "Cargo.lock" + - "Trunk.toml" + - "rust-toolchain.toml" +``` + +- [ ] **Step 4: Commit** + +```bash +git add apps/rust/moon.yml apps/rust-auth/moon.yml apps/leptos/moon.yml +git commit -m "chore: add moon.yml for Rust apps (rust, rust-auth, leptos)" +``` + +--- + +### Task 8: Create moon.yml for 9router + +**Files:** +- Create: `apps/9router/moon.yml` + +- [ ] **Step 1: Write apps/9router/moon.yml** + +```yaml +# https://moonrepo.dev/docs/config/project +$schema: "https://moonrepo.dev/schemas/project.json" + +type: "application" +language: "unknown" +platform: "system" +tags: + - "type:router" + +fileGroups: + sources: + - "src/**/*" + - "next.config.mjs" + - "package.json" +``` + +- [ ] **Step 2: Commit** + +```bash +git add apps/9router/moon.yml +git commit -m "chore: add moon.yml for 9router" +``` + +--- + +### Task 9: Update flake.nix β€” add moon CLI to devShell + +**Files:** +- Modify: `flake.nix` + +- [ ] **Step 1: Add moon to nativeBuildInputs in flake.nix** + +Find the `devShells.default` block in `flake.nix`. Add `moon` to `nativeBuildInputs`: + +``` +devShells.default = pkgs.mkShell { + name = "ultimate-asepharyana-dev"; + nativeBuildInputs = with pkgs; [ + rustToolchain + bun + nodejs_22 + pkg-config + openssl + trunk + wasm-bindgen-cli + binaryen + process-compose + mysql84 + redis + minio-client + gh + git + moon # <-- add this line + ]; + # ... shellHook unchanged +}; +``` + +- [ ] **Step 2: Verify Nix can still evaluate the flake** + +Run: `nix flake check --no-build 2>&1 | head -20` +Expected: no evaluation errors + +- [ ] **Step 3: Commit** + +```bash +git add flake.nix +git commit -m "chore: add moon CLI to Nix devShell" +``` + +--- + +### Task 10: Update .gitignore + +**Files:** +- Modify: `.gitignore` + +- [ ] **Step 1: Add moonrepo cache entries to .gitignore** + +Append to `.gitignore`: + +```gitignore +# moonrepo +.moon/cache +.~moon +``` + +- [ ] **Step 2: Commit** + +```bash +git add .gitignore +git commit -m "chore: add moonrepo cache entries to .gitignore" +``` + +--- + +### Task 11: Validate installation with moon check + +**Files:** (none β€” validation only) + +- [ ] **Step 1: Run moon check** + +```bash +moon check +``` + +Expected: `OK` or zero errors. If warnings about unresolved project IDs appear, verify that `apps/*` glob in `workspace.yml` matches all project directories. + +- [ ] **Step 2: Run moon query projects** + +```bash +moon query projects +``` + +Expected: lists all 7 projects with their tags: nextjs, elysia, solidjs, rust, rust-auth, leptos, 9router + +- [ ] **Step 3: Verify tag queries work** + +```bash +moon query projects --tag lang:typescript +``` + +Expected: nextjs, elysia, solidjs + +```bash +moon query projects --tag lang:rust +``` + +Expected: rust, rust-auth, leptos + +- [ ] **Step 4: Commit (if any fixes were needed)** + +No commit needed if check passes clean. + +--- + +### Task 12: Test β€” moon run build on TypeScript apps + +**Files:** (none β€” test only) + +- [ ] **Step 1: Build nextjs** + +```bash +moon run nextjs:build +``` + +Expected: `next build` runs successfully, outputs to `.next/` + +- [ ] **Step 2: Build elysia** + +```bash +moon run elysia:build +``` + +Expected: `bun build` runs successfully, outputs to `dist/` + +- [ ] **Step 3: Build solidjs** + +```bash +moon run solidjs:build +``` + +Expected: `vinxi build` runs successfully, outputs to `.output/` + +- [ ] **Step 4: Verify caching on second build (nextjs)** + +```bash +moon run nextjs:build +``` + +Expected: `Cached` β€” no rebuild, uses moonrepo cache + +--- + +### Task 13: Test β€” moon run lint and test + +**Files:** (none β€” test only) + +- [ ] **Step 1: Run lint across TypeScript apps** + +```bash +moon run :lint +``` + +Expected: all apps with a `lint` task run it. Note failures as they exist pre-migration (not caused by moonrepo). + +- [ ] **Step 2: Run tests across TypeScript apps** + +```bash +moon run :test +``` + +Expected: all apps with a `test` task run it. + +- [ ] **Step 3: Run tag-scoped commands** + +```bash +moon run --tag lang:typescript :build +``` + +Expected: builds nextjs, elysia, solidjs only (not Rust apps). + +--- + +### Task 14: Final commit and documentation + +**Files:** +- Modify: `.gitignore` (if any final updates) + +- [ ] **Step 1: Final moon check** + +```bash +moon check +``` + +Expected: clean, no errors. + +- [ ] **Step 2: Commit any remaining changes** + +```bash +git status +``` + +If nothing outstanding, move on. + +- [ ] **Step 3: Verify the full workspace is clean** + +```bash +git status +``` + +Expected: working tree clean. diff --git a/docs/superpowers/plans/2026-05-25-postgresql-migration.md b/docs/superpowers/plans/2026-05-25-postgresql-migration.md new file mode 100644 index 0000000..efd20bb --- /dev/null +++ b/docs/superpowers/plans/2026-05-25-postgresql-migration.md @@ -0,0 +1,712 @@ +# PostgreSQL Migration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate elysia (Drizzle ORM + MySQL) and rust (SeaORM + MySQL) apps to PostgreSQL via direct cutover. + +**Architecture:** Export MySQL data, convert schema to PostgreSQL format, import into target PostgreSQL instance, update app drivers and connection strings, deploy and validate. + +**Tech Stack:** MySQL (source), PostgreSQL (target), Drizzle ORM (elysia), SeaORM (rust), pgloader or manual conversion for schema migration. + +--- + +## File Structure + +### Elysia App Changes +- `apps/elysia/src/db/lib/database.ts` β€” Replace mysql2 driver with postgres driver +- `apps/elysia/src/db/lib/schema.ts` β€” Replace mysqlTable with pgTable, update column types +- `apps/elysia/package.json` β€” Replace mysql2 with pg dependency +- `.env` or config file β€” Update DATABASE_URL to PostgreSQL connection string + +### Rust App Changes +- `apps/rust/Cargo.toml` β€” Replace sqlx-mysql feature with sqlx-postgres +- `apps/rust/src/infra/db_setup.rs` β€” Update DbBackend::MySql to DbBackend::Postgres, adjust SQL syntax +- Config/environment β€” Update DATABASE_URL to PostgreSQL connection string + +### Migration Artifacts +- `mysql_backup.sql` β€” MySQL dump (created during migration, not committed) +- `converted.sql` β€” PostgreSQL-compatible dump (created during migration, not committed) + +--- + +## Task Breakdown + +### Task 1: Backup MySQL and Export Schema + +**Files:** +- Create: `mysql_backup.sql` (temporary, not committed) + +- [ ] **Step 1: Export MySQL database** + +```bash +cd /mnt/code/bp3/ultimate-asepharyana.tech +mysqldump -u -p -h > mysql_backup.sql +``` + +Expected: File created with full schema + data. Verify file size > 1MB (contains data). + +-[]**Step 2: Verify backup integrity** + +```bash +# Check row counts in backup +grep "INSERT INTO" mysql_backup.sql | wc -l +``` + +Expected: Multiple INSERT statements present. Note row counts for later validation. + +- [ ] **Step 3: Document backup location** + +Store `mysql_backup.sql` in safe location (not in git). This is rollback insurance. + +--- + +### Task 2: Convert MySQL Schema to PostgreSQL + +**Files:** +- Create: `converted.sql` (temporary, not committed) + +- [ ] **Step 1: Install pgloader (if not present)** + +```bash +# macOS +brew install pgloader + +# Linux (Ubuntu/Debian) +sudo apt-get install pgloader + +# Or use Docker +docker run --rm -v $(pwd):/data pgloader/pgloader pgloader /data/mysql_backup.sql postgresql://asephs:hunterz@100.108.1.124:5432/hub +``` + +- [ ] **Step 2: Convert MySQL dump to PostgreSQL** + +```bash +pgloader mysql_backup.sql postgresql://asephs:hunterz@100.108.1.124:5432/hub +``` + +Or manually convert if pgloader unavailable: +- Replace `AUTO_INCREMENT` with `SERIAL` or `BIGSERIAL` +- Replace `DATETIME` with `TIMESTAMP` +- Replace backticks with double quotes +- Update index syntax for PostgreSQL + +Expected: Conversion completes without errors. Check for warnings about type conversions. + +- [ ] **Step 3: Verify conversion output** + +```bash +# If using pgloader, it creates converted.sql automatically +# If manual, save converted schema to file +cat converted.sql | head -50 +``` + +Expected: PostgreSQL-compatible SQL syntax (no backticks, SERIAL types, TIMESTAMP). + +--- + +### Task 3: Test PostgreSQL Import + +**Files:** +- Target: PostgreSQL instance at `postgresql://asephs:hunterz@100.108.1.124:5432/hub` + +- [ ] **Step 1: Connect to target PostgreSQL** + +```bash +psql postgresql://asephs:hunterz@100.108.1.124:5432/hub +``` + +Expected: Connected to PostgreSQL. Prompt shows `hub=#`. + +- [ ] **Step 2: Import converted schema** + +```bash +psql postgresql://asephs:hunterz@100.108.1.124:5432/hub < converted.sql +``` + +Expected: Import completes. Check for errors (should be none). + +- [ ] **Step 3: Validate table creation** + +```bash +psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "\dt" +``` + +Expected: All tables listed (User, Account, Session, Role, Permission, UserRole, ImageCache, etc.). + +- []**Step 4: Validate row counts** + +```bash +psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "SELECT COUNT(*) FROM \"User\";" +psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "SELECT COUNT(*) FROM \"Account\";" +``` + +Expected: Row counts match MySQL backup (from Task 1, Step 2). + +- [ ] **Step 5: Validate indexes** + +```bash +psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "\di" +``` + +Expected: All indexes present (email_idx, username_idx, userId_idx, sessionToken_idx, etc.). + +- [ ] **Step 6: Validate foreign keys** + +```bash +psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "SELECT constraint_name, table_name FROM information_schema.table_constraints WHERE constraint_type = 'FOREIGN KEY';" +``` + +Expected: Foreign key constraints listed (Userβ†’Account, Userβ†’Session, etc.). + +--- + +### Task 4: Update Elysia Database Driver + +**Files:** +- Modify: `apps/elysia/src/db/lib/database.ts` +- Modify: `apps/elysia/src/db/lib/schema.ts` +- Modify: `apps/elysia/package.json` + +- [ ] **Step 1: Update package.json dependencies** + +Replace mysql2 with pg: + +```json +{ + "dependencies": { + "drizzle-orm": "^0.45.2", + "pg": "^8.11.0", + "elysia": "^1.4.28" + }, + "devDependencies": { + "drizzle-kit": "^0.31.10" + } +} +``` + +Run: `cd apps/elysia && bun install` + +Expected: pg installed, mysql2 removed from node_modules. + +- [ ] **Step 2: Update database.ts driver** + +Replace entire file: + +```typescript +import type { PostgresJsDatabase } from 'drizzle-orm/postgres-js' +import { drizzle } from 'drizzle-orm/postgres-js' +import postgres from 'postgres' +import * as schema from './schema' + +export type Database = PostgresJsDatabase + +let dbInstance: Database | null = null +let sqlInstance: ReturnType | null = null + +export function initializeDb(databaseUrl: string): Database { + if (dbInstance) { + return dbInstance + } + + sqlInstance = postgres(databaseUrl) + dbInstance = drizzle(sqlInstance, { schema, mode: 'default' }) + + return dbInstance +} + +export function getDb(): Database { + if (!dbInstance) { + throw new Error('Database not initialized. Call initializeDb first.') + } + return dbInstance +} + +export async function closeDb() { + if (sqlInstance) { + await sqlInstance.end() + sqlInstance = null + dbInstance = null + } +} +``` + +Expected: File updated. No syntax errors. + +- [ ] **Step 3: Update schema.ts imports** + +Replace: +```typescript +import { + index, + int, + mysqlTable, + primaryKey, + text, + timestamp, + varchar, +} from 'drizzle-orm/mysql-core' +``` + +With: +```typescript +import { + index, + integer, + pgTable, + primaryKey, + text, + timestamp, + varchar, +} from 'drizzle-orm/postgres-core' +``` + +- [ ] **Step 4: Update schema.ts table definitions** + +Replace all `mysqlTable` with `pgTable` and `int` with `integer`: + +```typescript +// Before +export const users = mysqlTable( + 'User', + { + id: varchar('id', { length: 255 }).primaryKey(), + name: varchar('name', { length: 255 }), + // ... + }, + // ... +) + +// After +export const users = pgTable( + 'User', + { + id: varchar('id', { length: 255 }).primaryKey(), + name: varchar('name', { length: 255 }), + // ... + }, + // ... +) +``` + +Do this for all tables: users, accounts, sessions, roles, permissions, userRoles, and any others. + +Expected: All `mysqlTable` β†’ `pgTable`, all `int` β†’ `integer`. + +- [ ] **Step 5: Update environment variable** + +Set DATABASE_URL in `.env` or deployment config: + +```bash +DATABASE_URL=postgresql://asephs:hunterz@100.108.1.124:5432/hub +``` + +Expected: Environment variable set and accessible to elysia app. + +- [ ] **Step 6: Test elysia connection** + +```bash +cd apps/elysia +bun run src/index.ts +``` + +Expected: App starts without connection errors. Check logs for "Database initialized" or similar. + +- [ ] **Step 7: Commit elysia changes** + +```bash +cd /mnt/code/bp3/ultimate-asepharyana.tech +git add apps/elysia/src/db/lib/database.ts apps/elysia/src/db/lib/schema.ts apps/elysia/package.json +git commit -m "feat(elysia): migrate database driver from MySQL to PostgreSQL" +``` + +Expected: Commit created with message. + +--- + +### Task 5: Update Rust Database Driver + +**Files:** +- Modify: `apps/rust/Cargo.toml` +- Modify: `apps/rust/src/infra/db_setup.rs` + +- [ ] **Step 1: Update Cargo.toml features** + +Replace: +```toml +sea-orm = { version = "1.1.19", features = ["sqlx-mysql", "runtime-tokio-rustls", "macros", "with-chrono", "with-uuid"] } +``` + +With: +```toml +sea-orm = { version = "1.1.19", features = ["sqlx-postgres", "runtime-tokio-rustls", "macros", "with-chrono", "with-uuid"] } +``` + +Expected: Cargo.toml updated. Feature changed from sqlx-mysql to sqlx-postgres. + +- [ ] **Step 2: Update db_setup.rs backend check** + +Replace: +```rust +match backend { + DbBackend::MySql => { + // MySQL-specific logic + } + _ => { + info!("ℹ️ Skipping schema init for non-MySQL backend"); + } +} +``` + +With: +```rust +match backend { + DbBackend::Postgres => { + // PostgreSQL-specific logic + let tables = vec![( + "ImageCache", + schema + .create_table_from_entity(image_cache::Entity) + .if_not_exists() + .to_owned(), + )]; + + for (name, stmt) in tables { + match db.execute(backend.build(&stmt)).await { + Ok(_) => info!(" βœ“ Table '{}' checked/created", name), + Err(e) => { + error!(" [!] Failed to create table '{}': {}", name, e); + return Err(e); + } + } + } + + // PostgreSQL index creation (different syntax) + let index_sql = "CREATE INDEX IF NOT EXISTS idx_image_cache_cdn_url ON \"ImageCache\" (cdn_url)"; + match db.execute(Statement::from_string(backend, index_sql)).await { + Ok(_) => info!(" βœ“ Index 'idx_image_cache_cdn_url' ensured"), + Err(e) => { + let err_str = e.to_string(); + // PostgreSQL duplicate index error + if err_str.contains("already exists") { + info!(" βœ“ Index 'idx_image_cache_cdn_url' already exists"); + } else { + error!(" [!] Failed to create index on ImageCache: {}", e); + } + } + } + + info!("βœ… Database schema initialization complete."); + } + _ => { + info!("ℹ️ Skipping schema init for non-PostgreSQL backend"); + } +} +``` + +Expected: db_setup.rs updated with PostgreSQL backend handling. + +- [ ] **Step 3: Update environment variable** + +Set DATABASE_URL in `.env` or deployment config: + +```bash +DATABASE_URL=postgresql://asephs:hunterz@100.108.1.124:5432/hub +``` + +Expected: Environment variable set and accessible to rust app. + +- [ ] **Step 4: Rebuild rust app** + +```bash +cd apps/rust +cargo build --release +``` + +Expected: Build completes without errors. Compilation uses sqlx-postgres feature. + +- [ ] **Step 5: Test rust connection** + +```bash +cd apps/rust +cargo run +``` + +Expected: App starts without connection errors. Check logs for "Database schema initialization complete" or similar. + +- [ ] **Step 6: Commit rust changes** + +```bash +cd /mnt/code/bp3/ultimate-asepharyana.tech +git add apps/rust/Cargo.toml apps/rust/src/infra/db_setup.rs +git commit -m "feat(rust): migrate database driver from MySQL to PostgreSQL" +``` + +Expected: Commit created with message. + +--- + +### Task 6: Smoke Tests - Elysia App + +**Files:** +- Test: Manual testing via HTTP requests or app UI + +- [ ] **Step 1: Start elysia app** + +```bash +cd apps/elysia +bun run src/index.ts +``` + +Expected: App running on configured port (check logs for port). + +- [ ] **Step 2: Test user login** + +```bash +curl -X POST http://localhost:3000/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com","password":"password"}' +``` + +Expected: Response 200 or 401 (auth error is OK, connection error is not). + +-[]**Step 3: Test user creation (if endpoint exists)** + +```bash +curl -X POST http://localhost:3000/users \ + -H "Content-Type: application/json" \ + -d '{"name":"Test User","email":"newuser@example.com"}' +``` + +Expected: Response 200/201 or 400 (validation error is OK). + +- [ ] **Step 4: Test session retrieval** + +```bash +curl -X GET http://localhost:3000/sessions \ + -H "Authorization: Bearer " +``` + +Expected: Response 200 with session data or 401 (auth error is OK). + +- [ ] **Step 5: Check database logs** + +```bash +# In elysia app logs, verify queries are executing against PostgreSQL +# Look for connection strings or query logs showing PostgreSQL +``` + +Expected: Logs show PostgreSQL queries (not MySQL). + +--- + +### Task 7: Smoke Tests - Rust App + +**Files:** +- Test: Manual testing via HTTP requests or app UI + +- [ ] **Step 1: Start rust app** + +```bash +cd apps/rust +cargo run --release +``` + +Expected: App running on configured port (check logs for port). + +- [ ] **Step 2: Test image cache endpoint (if exists)** + +```bash +curl -X GET http://localhost:8000/api/cache/status +``` + +Expected: Response 200 with cache status or 404 (endpoint may not exist). + +- [ ] **Step 3: Test scraping/CDN endpoint** + +```bash +curl -X GET http://localhost:8000/api/health +``` + +Expected: Response 200 with health status. + +- [ ] **Step 4: Check database logs** + +```bash +# In rust app logs, verify queries are executing against PostgreSQL +# Look for connection strings or query logs showing PostgreSQL +``` + +Expected: Logs show PostgreSQL queries (not MySQL). + +--- + +### Task 8: Data Integrity Validation + +**Files:** +- Test: PostgreSQL queries + +- [] **Step 1: Verify user count** + +```bash +psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "SELECT COUNT(*) as user_count FROM \"User\";" +``` + +Expected: Count matches MySQL backup count (from Task 1, Step 2). + +- [ ] **Step 2: Verify account count** + +```bash +psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "SELECT COUNT(*) as account_count FROM \"Account\";" +``` + +Expected: Count matches MySQL backup. + +- [ ] **Step 3: Verify session count** + +```bash +psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "SELECT COUNT(*) as session_count FROM \"Session\";" +``` + +Expected: Count matches MySQL backup. + +- [] **Step 4: Verify no orphaned foreign keys** + +```bash +psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c " +SELECT a.id FROM \"Account\" a +LEFT JOIN \"User\" u ON a.user_id = u.id +WHERE u.id IS NULL; +" +``` + +Expected: No rows returned (no orphaned accounts). + +- [ ] **Step 5: Verify role/permission relationships** + +```bash +psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "SELECT COUNT(*) as role_count FROM \"Role\";" +psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "SELECT COUNT(*) as permission_count FROM \"Permission\";" +``` + +Expected: Counts match MySQL backup. + +--- + +### Task 9: Performance Baseline + +**Files:** +- Test: Query performance comparison + +- [ ] **Step 1: Benchmark user query on PostgreSQL** + +```bash +psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "EXPLAIN ANALYZE SELECT * FROM \"User\" WHERE email = 'test@example.com';" +``` + +Expected: Query plan shows index usage (Seq Scan or Index Scan). Note execution time. + +- [ ] **Step 2: Benchmark account query on PostgreSQL** + +```bash +psql postgresql://asephs:hunterz@100.108.1.124:5432/hub -c "EXPLAIN ANALYZE SELECT * FROM \"Account\" WHERE user_id = 'user-123';" +``` + +Expected: Query plan shows index usage. Note execution time. + +- [ ] **Step 3: Compare with MySQL baseline (if available)** + +If MySQL is still running, run same queries and compare execution times. + +Expected: PostgreSQL performance similar or better than MySQL. + +--- + +### Task 10: Cleanup and Documentation + +**Files:** +- Create: `MIGRATION_LOG.md` (optional, for documentation) + +- [ ] **Step 1: Remove temporary files** + +```bash +rm mysql_backup.sql converted.sql +``` + +Expected: Temporary migration files deleted. + +- [ ] **Step 2: Document migration completion** + +Create `MIGRATION_LOG.md`: + +```markdown +# PostgreSQL Migration Log + +**Date:** 2026-05-25 +**Status:** βœ… Complete + +## Summary +- Migrated elysia app from MySQL to PostgreSQL +- Migrated rust app from MySQL to PostgreSQL +- All data validated and integrity confirmed +- Apps tested and operational + +## Changes +- elysia: Updated database driver (mysql2 β†’ postgres), schema (mysqlTable β†’ pgTable) +- rust: Updated Cargo.toml feature (sqlx-mysql β†’ sqlx-postgres), db_setup.rs backend handling + +## Validation +- Row counts match pre-migration +- Foreign keys intact +- Indexes present and performant +- Auth flow functional +- Session management working + +## Rollback +MySQL backup available at: [location if kept] +To rollback: Restore MySQL from backup, revert connection strings, redeploy apps. +``` + +- [ ] **Step 3: Final commit** + +```bash +git add MIGRATION_LOG.md +git commit -m "docs: add PostgreSQL migration completion log" +``` + +Expected: Commit created. + +- [ ] **Step 4: Verify all apps running** + +```bash +# Check elysia +curl http://localhost:3000/health + +# Check rust +curl http://localhost:8000/health +``` + +Expected: Both apps respond with 200 status. + +--- + +## Self-Review + +**Spec Coverage:** +- βœ… Pre-migration (backup, convert, test) β€” Tasks 1-3 +- βœ… Elysia code updates (driver, schema, env) β€” Task 4 +- βœ… Rust code updates (Cargo.toml, db_setup.rs, env) β€” Task 5 +- βœ… Smoke tests (auth, endpoints, logs) β€” Tasks 6-7 +- βœ… Data validation (row counts, foreign keys) β€” Task 8 +- βœ… Performance baseline β€” Task 9 +- βœ… Cleanup and documentation β€” Task 10 + +**Placeholder Scan:** +- βœ… No TBD/TODO +- βœ… All code blocks complete +- βœ… All commands exact with expected output +- βœ… All file paths exact + +**Type Consistency:** +- βœ… Database type: `PostgresJsDatabase` (elysia), `DbBackend::Postgres` (rust) +- βœ… Connection string format consistent: `postgresql://asephs:hunterz@100.108.1.124:5432/hub` +- βœ… Table names consistent: "User", "Account", "Session", etc. diff --git a/docs/superpowers/plans/2026-05-27-production-github-actions.md b/docs/superpowers/plans/2026-05-27-production-github-actions.md new file mode 100644 index 0000000..15e5e01 --- /dev/null +++ b/docs/superpowers/plans/2026-05-27-production-github-actions.md @@ -0,0 +1,533 @@ +# Production GitHub Actions Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rework GitHub Actions build/deploy workflows into a production baseline with reliable React submodule updates, least-privilege permissions, clear deploy behavior, and current official action versions. + +**Architecture:** Keep two workflows: `docker-build-push.yml` for detect/build/manifest updates, and `deploy-docker.yml` for VPS deployment. Add dispatch submodule SHA readiness checks before parent pointer updates, and remove recursive submodule checkout from deploy runner. + +**Tech Stack:** GitHub Actions YAML, GitHub-hosted Ubuntu runners, Docker Buildx, GHCR, git submodules, Docker Compose over SSH. + +--- + +## File Structure + +- Modify `.github/workflows/docker-build-push.yml`: add default permissions, validate repository dispatch payloads, wait for submodule SHAs, keep selective matrix builds, harden manifest update. +- Modify `.github/workflows/deploy-docker.yml`: add default permissions, remove recursive checkout, keep auto deploy from successful build, make deploy logs clearer. +- Modify `.github/dependabot.yml`: add GitHub Actions update config so official actions stay current. + +--- + +### Task 1: Harden build workflow permissions and dispatch validation + +**Files:** +- Modify: `.github/workflows/docker-build-push.yml` + +- [ ] **Step 1: Add workflow-level read permissions** + +At top level, after `concurrency`, add: + +```yaml +permissions: + contents: read +``` + +Expected shape: + +```yaml +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + +env: + REGISTRY: ghcr.io +``` + +- [ ] **Step 2: Replace dispatch parser with payload validation** + +In `.github/workflows/docker-build-push.yml`, replace `Parse repository_dispatch payload` step body with: + +```yaml + - name: Parse repository_dispatch payload + id: dispatch + if: github.event_name == 'repository_dispatch' + env: + SERVICE: ${{ github.event.client_payload.service }} + SHA: ${{ github.event.client_payload.sha }} + run: | + set -euo pipefail + + if [ -z "${SERVICE:-}" ]; then + echo "::error::repository_dispatch payload missing service" + exit 1 + fi + + if [ -z "${SHA:-}" ]; then + echo "::error::repository_dispatch payload missing sha" + exit 1 + fi + + case "$SERVICE" in + rust-api|elysia-api|react-web|9router) ;; + *) + echo "::error::Unsupported service '$SERVICE'. Expected one of: rust-api, elysia-api, react-web, 9router" + exit 1 + ;; + esac + + case "$SHA" in + *[!0-9a-fA-F]*|???????????????????????????????????????|?????????????????????????????????????????*) + echo "::error::Invalid sha '$SHA'. Expected 40 hex characters" + exit 1 + ;; + esac + + declare -a SERVICES=("rust-api" "elysia-api" "react-web" "9router") + for svc in "${SERVICES[@]}"; do + if [ "$SERVICE" = "$svc" ]; then + echo "${svc}=true" >> "$GITHUB_OUTPUT" + else + echo "${svc}=false" >> "$GITHUB_OUTPUT" + fi + done +``` + +- [ ] **Step 3: Run YAML syntax check** + +Run: + +```bash +python - <<'PY' +from pathlib import Path +import yaml +for path in Path('.github/workflows').glob('*.yml'): + yaml.safe_load(path.read_text()) + print(f'OK {path}') +PY +``` + +Expected: + +```text +OK .github/workflows/deploy-docker.yml +OK .github/workflows/docker-build-push.yml +``` + +- [ ] **Step 4: Commit** + +```bash +git add .github/workflows/docker-build-push.yml +git commit -m "ci: validate dispatch payloads" +``` + +--- + +### Task 2: Add submodule SHA readiness wait before build/update + +**Files:** +- Modify: `.github/workflows/docker-build-push.yml` + +- [ ] **Step 1: Add readiness job after changes job** + +Insert this job between `changes` and `build`: + +```yaml + wait-submodule-ref: + needs: [changes] + if: github.event_name == 'repository_dispatch' + runs-on: ubuntu-latest + steps: + - name: Wait for submodule ref + env: + SERVICE: ${{ github.event.client_payload.service }} + SHA: ${{ github.event.client_payload.sha }} + run: | + set -euo pipefail + + case "$SERVICE" in + "rust-api") REPO="https://github.com/MythEclipse/ultimate-asepharyana-tech-rust.git" ;; + "elysia-api") REPO="https://github.com/MythEclipse/ultimate-asepharyana-tech-elysia.git" ;; + "react-web") REPO="https://github.com/MythEclipse/ultimate-asepharyana-tech-react.git" ;; + "9router") REPO="https://github.com/MythEclipse/9router.git" ;; + *) + echo "::error::Unsupported service '$SERVICE'" + exit 1 + ;; + esac + + echo "Waiting for $SERVICE ref $SHA in $REPO" + for attempt in {1..30}; do + if git ls-remote --exit-code "$REPO" "$SHA" >/dev/null 2>&1; then + echo "Submodule ref $SHA is fetchable for $SERVICE" + exit 0 + fi + echo "Attempt $attempt/30: $SHA not visible yet; waiting 10s" + sleep 10 + done + + echo "::error::Submodule ref $SHA for $SERVICE was not fetchable after 300s" + exit 1 +``` + +- [ ] **Step 2: Make build wait for readiness job without blocking push/manual events** + +Change build job header from: + +```yaml + build: + needs: [changes] +``` + +to: + +```yaml + build: + needs: [changes, wait-submodule-ref] + if: | + always() && + needs.changes.result == 'success' && + (needs.wait-submodule-ref.result == 'success' || needs.wait-submodule-ref.result == 'skipped') && + needs.changes.outputs.matrix != '[]' +``` + +Remove existing build-level line: + +```yaml + if: needs.changes.outputs.matrix != '[]' +``` + +- [ ] **Step 3: Make update-manifest wait for readiness job** + +Change update-manifest header from: + +```yaml + update-manifest: + needs: [changes, build] + if: | + always() && + (needs.build.result == 'success' || needs.build.result == 'skipped') +``` + +to: + +```yaml + update-manifest: + needs: [changes, wait-submodule-ref, build] + if: | + always() && + needs.changes.result == 'success' && + (needs.wait-submodule-ref.result == 'success' || needs.wait-submodule-ref.result == 'skipped') && + (needs.build.result == 'success' || needs.build.result == 'skipped') +``` + +- [ ] **Step 4: Run YAML syntax check** + +Run same command from Task 1 Step 3. + +Expected both workflow files print `OK`. + +- [ ] **Step 5: Commit** + +```bash +git add .github/workflows/docker-build-push.yml +git commit -m "ci: wait for submodule refs before builds" +``` + +--- + +### Task 3: Harden manifest update and submodule checkout + +**Files:** +- Modify: `.github/workflows/docker-build-push.yml` + +- [ ] **Step 1: Add job permissions to build and manifest jobs** + +Ensure build job contains: + +```yaml + permissions: + contents: read + packages: write +``` + +Ensure update-manifest job contains: + +```yaml + permissions: + contents: write +``` + +- [ ] **Step 2: Replace dispatch submodule checkout block** + +Inside `Update tags and submodules`, replace the repository_dispatch submodule update block with: + +```bash + if [ "${{ github.event_name }}" == "repository_dispatch" ] && [ "${{ github.event.client_payload.service }}" == "$id" ]; then + SHA_DISPATCH="${{ github.event.client_payload.sha }}" + SUB_PATH="${PATHS[$id]}" + if [ -n "$SHA_DISPATCH" ]; then + echo "Updating submodule $SUB_PATH to $SHA_DISPATCH" + git submodule update --init "$SUB_PATH" + git -C "$SUB_PATH" fetch origin "$SHA_DISPATCH" + git -C "$SUB_PATH" checkout "$SHA_DISPATCH" + git add "$SUB_PATH" + CHANGED=true + fi + fi +``` + +- [ ] **Step 3: Add pull/rebase retry before push** + +Replace: + +```bash + git commit -m "chore: update manifests and submodules [skip ci]" + git pull --rebase origin main + git push origin main +``` + +with: + +```bash + git commit -m "chore: update manifests and submodules [skip ci]" + + for attempt in {1..3}; do + if git pull --rebase origin main && git push origin main; then + exit 0 + fi + echo "Manifest push attempt $attempt/3 failed; retrying" + git rebase --abort || true + git pull --rebase origin main || true + sleep 5 + done + + echo "::error::Failed to push manifest update after 3 attempts" + exit 1 +``` + +- [ ] **Step 4: Run YAML syntax check** + +Run same command from Task 1 Step 3. + +Expected both workflow files print `OK`. + +- [ ] **Step 5: Commit** + +```bash +git add .github/workflows/docker-build-push.yml +git commit -m "ci: harden manifest updates" +``` + +--- + +### Task 4: Make deploy checkout submodule-free and least privilege + +**Files:** +- Modify: `.github/workflows/deploy-docker.yml` + +- [ ] **Step 1: Add workflow-level read permissions** + +After `concurrency`, add: + +```yaml +permissions: + contents: read +``` + +Expected shape: + +```yaml +concurrency: + group: deploy-vps + cancel-in-progress: false + +permissions: + contents: read +``` + +If current `cancel-in-progress` is `true`, change it to `false`. + +- [ ] **Step 2: Make checkout non-recursive** + +Replace checkout step: + +```yaml + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive +``` + +with: + +```yaml + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + submodules: false +``` + +- [ ] **Step 3: Add deploy context log** + +At start of `Deploy with Docker Compose on VPS` run script, after `set -euo pipefail`, add: + +```bash + echo "Deploy event: ${{ github.event_name }}" + echo "Deploy ref: ${{ github.ref }}" + echo "Deploy sha: ${{ github.sha }}" +``` + +- [ ] **Step 4: Run YAML syntax check** + +Run same command from Task 1 Step 3. + +Expected both workflow files print `OK`. + +- [ ] **Step 5: Commit** + +```bash +git add .github/workflows/deploy-docker.yml +git commit -m "ci: avoid submodule checkout during deploy" +``` + +--- + +### Task 5: Add GitHub Actions Dependabot updates + +**Files:** +- Modify: `.github/dependabot.yml` + +- [ ] **Step 1: Add github-actions ecosystem** + +Append this update entry under `updates:`: + +```yaml + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: weekly + groups: + github-actions: + patterns: + - '*' +``` + +Expected file shape: + +```yaml +version: 2 +updates: + - package-ecosystem: 'devcontainers' + directory: '/' + schedule: + interval: weekly + + - package-ecosystem: 'github-actions' + directory: '/' + schedule: + interval: weekly + groups: + github-actions: + patterns: + - '*' +``` + +- [ ] **Step 2: Run YAML syntax check** + +Run: + +```bash +python - <<'PY' +from pathlib import Path +import yaml +paths = [Path('.github/dependabot.yml'), *Path('.github/workflows').glob('*.yml')] +for path in paths: + yaml.safe_load(path.read_text()) + print(f'OK {path}') +PY +``` + +Expected: + +```text +OK .github/dependabot.yml +OK .github/workflows/deploy-docker.yml +OK .github/workflows/docker-build-push.yml +``` + +- [ ] **Step 3: Commit** + +```bash +git add .github/dependabot.yml +git commit -m "ci: enable github actions dependency updates" +``` + +--- + +### Task 6: Final validation + +**Files:** +- Validate: `.github/workflows/docker-build-push.yml` +- Validate: `.github/workflows/deploy-docker.yml` +- Validate: `.github/dependabot.yml` + +- [ ] **Step 1: Run YAML syntax check** + +Run: + +```bash +python - <<'PY' +from pathlib import Path +import yaml +paths = [Path('.github/dependabot.yml'), *Path('.github/workflows').glob('*.yml')] +for path in paths: + yaml.safe_load(path.read_text()) + print(f'OK {path}') +PY +``` + +Expected all files print `OK`. + +- [ ] **Step 2: Check workflows recognized by GitHub CLI** + +Run: + +```bash +gh workflow list +``` + +Expected output includes: + +```text +Build and Push Docker Images +Deploy Docker to VPS +``` + +- [ ] **Step 3: Inspect final diff** + +Run: + +```bash +git diff -- .github/workflows .github/dependabot.yml +``` + +Expected: + +- `docker-build-push.yml` has dispatch validation, `wait-submodule-ref`, job permissions, and manifest push retry. +- `deploy-docker.yml` has non-recursive checkout and read-only permissions. +- `dependabot.yml` has `github-actions` updates. + +- [ ] **Step 4: Commit any final validation fixes** + +If Step 1 or Step 2 required fixes, commit them: + +```bash +git add .github/workflows .github/dependabot.yml +git commit -m "ci: finalize production workflow hardening" +``` + +If no fixes were needed, do not create an empty commit. diff --git a/docs/superpowers/plans/2026-05-27-react-direct-runtime-and-images.md b/docs/superpowers/plans/2026-05-27-react-direct-runtime-and-images.md new file mode 100644 index 0000000..a57b024 --- /dev/null +++ b/docs/superpowers/plans/2026-05-27-react-direct-runtime-and-images.md @@ -0,0 +1,29 @@ +# React Direct Runtime and Images Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Serve the React SPA without nginx and remove frontend image-cache proxy calls so browser uses direct image/API URLs. + +**Architecture:** The React Docker image still builds with Bun/Vite, but runtime uses `vite preview` from Bun instead of nginx. `CachedImage` normalizes image URLs and renders them directly, keeping fallback/retry UI but removing `/proxy/image-cache` auditing. Traefik keeps routing `react-web:80`, so compose and dynamic routing stay stable. + +**Tech Stack:** Bun, Vite, React, TypeScript, Docker, Docker Compose, Traefik. + +--- + +## Tasks + +### Task 1: Switch React Runtime From nginx to Bun/Vite Preview + +Modify `infra/docker/react.Dockerfile` so runtime stage uses `oven/bun:1-alpine`, installs production deps, copies `/app/dist`, exposes 80, and runs `bunx vite preview --host 0.0.0.0 --port 80`. Verify nginx runtime/copy lines are gone and Vite preview command exists. + +### Task 2: Remove Image Cache Proxy From CachedImage + +Modify `apps/react/src/components/ui/cached-image.tsx` to remove `API_BASE_URL` import, `/proxy/image-cache` POST, audit state/function, and auditing overlay. Keep direct normalized image URLs, retry behavior, and fallback image. + +### Task 3: Verify Build and Config + +Run static checks for both tasks, `bun --cwd apps/react run build`, `docker build -f infra/docker/react.Dockerfile -t react-web:test .`, start test container on `18080:80`, curl root, clean container, and run `git diff --check`. + +### Task 4: Commit and Push + +Commit changed files: `infra/docker/react.Dockerfile`, `apps/react/src/components/ui/cached-image.tsx`, `docs/superpowers/plans/2026-05-27-react-direct-runtime-and-images.md`. Push branch only after verification if user asks; do not push from worktree unless explicitly instructed. diff --git a/docs/superpowers/specs/2026-05-24-moonrepo-migration-design.md b/docs/superpowers/specs/2026-05-24-moonrepo-migration-design.md new file mode 100644 index 0000000..464e2f1 --- /dev/null +++ b/docs/superpowers/specs/2026-05-24-moonrepo-migration-design.md @@ -0,0 +1,130 @@ +# Moonrepo Migration Design + +**Date:** 2026-05-24 +**Status:** approved + +## Context + +Ultimate Asepharyana Tech is a polyglot monorepo with 7 applications managed as git submodules: nextjs (React/Next.js), elysia (Bun/Elysia), solidjs (SolidStart), rust (Axum), rust-auth (Axum auth service), leptos (Leptos WASM), and 9router. Nix flakes handle system dependencies and Docker image builds. Docker Compose manages deployment. + +## Problem + +- No centralized task orchestration β€” running build/lint/test across all apps is manual and error-prone +- No dependency caching β€” builds are not incremental across apps +- Inconsistent developer experience β€” each app has its own conventions, onboarding is slow + +## Goal + +Adopt moonrepo for task orchestration, caching, and dependency graph management across all 7 apps while preserving the existing Nix build system, Docker Compose deployment, and git submodule structure. + +## Architecture + +### Directory Structure + +``` +ultimate-asepharyana.tech/ +β”œβ”€β”€ .moon/ +β”‚ β”œβ”€β”€ workspace.yml +β”‚ β”œβ”€β”€ toolchain.yml +β”‚ └── tasks/ +β”‚ β”œβ”€β”€ typescript-build.yml +β”‚ β”œβ”€β”€ typescript-lint.yml +β”‚ β”œβ”€β”€ typescript-test.yml +β”‚ β”œβ”€β”€ rust-build.yml +β”‚ β”œβ”€β”€ rust-test.yml +β”‚ └── rust-lint.yml +β”œβ”€β”€ apps/ +β”‚ β”œβ”€β”€ nextjs/moon.yml +β”‚ β”œβ”€β”€ elysia/moon.yml +β”‚ β”œβ”€β”€ solidjs/moon.yml +β”‚ β”œβ”€β”€ rust/moon.yml +β”‚ β”œβ”€β”€ rust-auth/moon.yml +β”‚ β”œβ”€β”€ leptos/moon.yml +β”‚ └── 9router/moon.yml +β”œβ”€β”€ .moon/workspace.yml # unchanged β€” Nix build + deployment +β”œβ”€β”€ flake.nix # unchanged +β”œβ”€β”€ infra/ # unchanged β€” Docker Compose, Traefik, nginx +└── scripts/ # unchanged +``` + +### Tag Taxonomy + +| Tag | Projects | +|-----|----------| +| `lang:typescript` | nextjs, elysia, solidjs | +| `lang:rust` | rust, rust-auth, leptos | +| `type:frontend` | nextjs, solidjs, leptos | +| `type:backend` | rust, elysia, rust-auth | +| `type:router` | 9router | + +### Toolchain + +- moonrepo manages Node 22, Bun, and TypeScript versions via `.moon/toolchain.yml` for consistent access across all TypeScript apps +- Rust toolchain remains managed by Nix/devShell β€” moonrepo does not touch Rust installation +- Nix devShell gains the moon CLI binary + +### Project Dependencies + +- **nextjs** depends on `rust-auth` and `elysia` (frontend consumes both APIs) +- **solidjs** depends on `elysia` and `rust-auth` +- **leptos** depends on `rust` (CSR frontend consumes rust backend API) +- **elysia**, **rust**, **rust-auth**, **9router** β€” no project dependencies + +## Task Definitions + +### Shared Tasks: TypeScript + +All TypeScript apps inherit from `.moon/tasks/typescript-*.yml`: + +| Task | Command | Inputs | Outputs | +|------|---------|--------|---------| +| `build` | `bun run build` | `src/**/*`, `tsconfig.json`, `package.json` | `.next`, `dist`, `.output` | +| `lint` | `bun run lint` | `src/**/*`, `eslint.config.mjs`, `tsconfig.json` | β€” | +| `typecheck` | `bun run check-types` | `src/**/*`, `tsconfig.json` | β€” | +| `test` | `bun test` | `src/**/*`, `test/**/*` | β€” | +| `e2e` | overridden per app | β€” | β€” | + +### Shared Tasks: Rust + +All Rust apps inherit from `.moon/tasks/rust-*.yml`, using system tasks: + +| Task | Command | Inputs | Outputs | +|------|---------|--------|---------| +| `build` | `cargo build --release` | `src/**/*`, `Cargo.toml`, `Cargo.lock`, `build.rs` | `target/release/*` | +| `test` | `cargo test` | `src/**/*`, `Cargo.toml`, `Cargo.lock`, `tests/**/*` | β€” | +| `lint` | `cargo clippy -- -D warnings` | `src/**/*`, `Cargo.toml` | β€” | +| `fmt-check` | `cargo fmt --check` | `src/**/*` | β€” | + +### Implicit Dependencies + +Configured in `workspace.yml`: `lint` implicitly depends on `build` for TypeScript apps (typecheck path), and `build` propagates to dependents when source inputs change. + +## Key Commands + +```bash +moon run :build # build all changed apps + dependents +moon run :lint # lint all +moon run :test # test all +moon run --tag lang:rust :test # test Rust apps only +moon run --affected :build # build only what changed +moon query projects --tag lang:typescript # list TypeScript projects +``` + +## What Does NOT Change + +- **Nix flakes** β€” system dependencies, Docker image builds, devShell remain as-is +- **Docker Compose** β€” deployment continues through existing compose files in `infra/compose/` +- **Git submodules** β€” all 7 app repos stay as submodules under `apps/` +- **infra/** β€” Traefik and Docker Compose deployment remain under `infra/`; legacy Grafana/Prometheus/Alertmanager configs have been removed +- **scripts/** β€” existing utility scripts unchanged + +## Migration Steps (High-Level) + +1. Install moonrepo CLI, create `.moon/` scaffolding +2. Create shared task definitions in `.moon/tasks/` +3. Configure `workspace.yml` with project list, tags, and dependency constraints +4. Configure `toolchain.yml` for Node 22 + Bun +5. Add `moon.yml` to each of the 7 app directories +6. Validate: `moon check`, `moon run :build`, `moon run :lint`, `moon run :test` +7. Add moon CLI to Nix devShell +8. Commit and document diff --git a/docs/superpowers/specs/2026-05-25-postgresql-migration-design.md b/docs/superpowers/specs/2026-05-25-postgresql-migration-design.md new file mode 100644 index 0000000..f27f821 --- /dev/null +++ b/docs/superpowers/specs/2026-05-25-postgresql-migration-design.md @@ -0,0 +1,197 @@ +# PostgreSQL Migration Design +**Date:** 2026-05-25 +**Scope:** Migrate elysia (Drizzle ORM + MySQL) and rust (SeaORM + MySQL) apps to PostgreSQL +**Approach:** Direct cutover (Approach B) +**Connection String:** `postgresql://asephs:hunterz@100.108.1.124:5432/hub` + +--- + +## Current State + +### Elysia App +- **ORM:** Drizzle ORM v0.45.2 +- **Driver:** mysql2 v3.20.0 +- **Schema Location:** `apps/elysia/src/db/lib/schema.ts` +- **Tables:** users, accounts, sessions, roles, permissions, userRoles, and related junction tables +- **Database File:** `apps/elysia/src/db/lib/database.ts` + +### Rust App +- **ORM:** SeaORM v1.1.19 +- **Feature:** sqlx-mysql +- **DB Setup:** `apps/rust/src/infra/db_setup.rs` +- **Tables:** ImageCache (primary table managed by app) +- **Config:** Environment-based connection string + +### Apps NOT Migrating +- 9router (uses SQLite runtime, excluded per requirements) +- nextjs (frontend, minimal DB usage) +- leptos, solidjs, rust-auth (frontends, no DB) + +--- + +## Migration Steps + +### Phase 1: Pre-Migration (Preparation) +1. **Backup MySQL** + ```bash + mysqldump -u -p > mysql_backup.sql + ``` +2. **Convert MySQL dump to PostgreSQL** + - Use `pgloader` or manual conversion for schema compatibility + - Handle type conversions: + - `INT` β†’ `INTEGER` + - `VARCHAR(n)` β†’ `VARCHAR(n)` (PostgreSQL compatible) + - `DATETIME` β†’ `TIMESTAMP` + - `AUTO_INCREMENT` β†’ `SERIAL` or `BIGSERIAL` + - Verify indexes and foreign keys convert correctly +3. **Test import into target PostgreSQL** + ```bash + psql postgresql://asephs:hunterz@100.108.1.124:5432/hub < converted.sql + ``` +4. **Validate data integrity** + - Row counts match MySQL + - Indexes exist + - Foreign key constraints enforced + +### Phase 2: Code Updates + +#### Elysia App +1. **Update `apps/elysia/src/db/lib/database.ts`** + - Replace `mysql2` import with `postgres` driver + - Change Drizzle dialect from `drizzle-orm/mysql2` to `drizzle-orm/postgres` + - Update connection string format + +2. **Update `apps/elysia/src/db/lib/schema.ts`** + - Replace `drizzle-orm/mysql-core` imports with `drizzle-orm/postgres-core` + - Change `mysqlTable` to `pgTable` + - Adjust column types if needed (e.g., `int` β†’ `integer`) + +3. **Update `apps/elysia/package.json`** + - Replace `mysql2` with `pg` (PostgreSQL driver) + - Keep `drizzle-orm` and `drizzle-kit` versions + +4. **Update environment/config** + - Change `DATABASE_URL` to PostgreSQL connection string + +#### Rust App +1. **Update `apps/rust/Cargo.toml`** + - Replace `sqlx-mysql` feature with `sqlx-postgres` in sea-orm dependency + - Add `postgres` feature if needed + +2. **Update `apps/rust/src/infra/db_setup.rs`** + - Change `DbBackend::MySql` check to `DbBackend::Postgres` + - Adjust SQL syntax for PostgreSQL (e.g., index creation) + - Update error code handling (PostgreSQL uses different error codes) + +3. **Update config/environment** + - Change `DATABASE_URL` to PostgreSQL connection string + +### Phase 3: Cutover (Execution) +1. **Stop all apps** + ```bash + # Stop elysia + # Stop rust app + ``` +2. **Export MySQL data** + ```bash + mysqldump -u -p > final_backup.sql + ``` +3. **Convert and import to PostgreSQL** + ```bash + # Convert dump + # Import to PostgreSQL + psql postgresql://asephs:hunterz@100.108.1.124:5432/hub < converted.sql + ``` +4. **Deploy updated apps** + - Deploy elysia with PostgreSQL driver + - Deploy rust with PostgreSQL feature +5. **Verify connectivity** + - Test database queries from both apps + - Check auth flow (users table) + - Verify session management + +### Phase 4: Validation +1. **Smoke tests** + - User login/logout + - Session creation and retrieval + - Role/permission queries + - ImageCache operations (rust app) +2. **Data integrity checks** + - Row counts match pre-migration + - No orphaned foreign keys + - Indexes performing as expected +3. **Performance baseline** + - Compare query times MySQL vs PostgreSQL + - Monitor connection pool usage + +### Phase 5: Rollback Plan (if needed) +1. **Stop apps** +2. **Restore MySQL from backup** + ```bash + mysql -u -p < mysql_backup.sql + ``` +3. **Revert connection strings in apps** +4. **Redeploy with MySQL drivers** +5. **Restart apps** + +--- + +## Technical Details + +### Elysia Driver Change +**Before:** +```typescript +import { drizzle } from 'drizzle-orm/mysql2' +import { createPool } from 'mysql2/promise' +``` + +**After:** +```typescript +import { drizzle } from 'drizzle-orm/postgres-js' +import postgres from 'postgres' +``` + +### Rust Feature Change +**Before:** +```toml +sea-orm = { version = "1.1.19", features = ["sqlx-mysql", ...] } +``` + +**After:** +```toml +sea-orm = { version = "1.1.19", features = ["sqlx-postgres", ...] } +``` + +--- + +## Risk Assessment + +| Risk | Mitigation | +|------|-----------| +| Data loss during migration | Full MySQL backup before cutover; test import first | +| App downtime | Cutover during low-traffic window; rollback plan ready | +| Connection string misconfiguration | Test connection before deploying apps | +| Schema incompatibilities | Pre-test conversion; validate indexes/constraints | +| Performance regression | Baseline MySQL performance; monitor PostgreSQL after cutover | + +--- + +## Success Criteria + +- βœ… All data migrated to PostgreSQL (row counts match) +- βœ… Elysia app connects and queries work +- βœ… Rust app connects and queries work +- βœ… Auth flow functional (login/logout) +- βœ… No orphaned foreign keys +- βœ… Indexes present and performant +- βœ… Rollback plan tested and documented + +--- + +## Timeline + +- **Pre-migration:** 30 min (backup, convert, test) +- **Code updates:** 1-2 hours (driver changes, testing) +- **Cutover:** 15-30 min (stop apps, migrate data, restart) +- **Validation:** 30 min (smoke tests, data checks) +- **Total:** ~3-4 hours (including buffer) diff --git a/docs/superpowers/specs/2026-05-26-remove-nix-docker-only-design.md b/docs/superpowers/specs/2026-05-26-remove-nix-docker-only-design.md new file mode 100644 index 0000000..a4f7601 --- /dev/null +++ b/docs/superpowers/specs/2026-05-26-remove-nix-docker-only-design.md @@ -0,0 +1,21 @@ +# Hapus Nix, Docker-Only + +**Goal:** Remove all Nix configuration and adapt CI/dev to Docker-only. + +**Scope:** +- Delete: `flake.nix`, `flake.lock`, `nix/`, `.envrc`, `.direnv/` +- Modify: `.github/workflows/docker-build-push.yml` β€” remove Nix steps, move `rust-api` to Dockerfile build +- Modify: `.vscode/settings.json` β€” remove `nixEnvSelector` +- Modify: `README.md` β€” replace `nix build` with `docker build` + +**CI Changes:** +- `rust-api` uses `infra/docker/rust.Dockerfile` (already exists) +- Remove `cachix/install-nix-action`, `cachix/cachix-action`, `nix build` step +- Remove `flake.nix`, `flake.lock`, `nix/**` from path triggers +- Update path filters: `nix/apps/*.nix` β†’ `infra/docker/*.Dockerfile` +- Remove `repository_dispatch` Nix `--override-input` logic +- Add `rust-api` to Dockerfile case + push branch + +**Dev Workflow:** +- Rust toolchain via `rustup`/`rust-toolchain.toml` in `apps/rust/` +- Dev via `docker compose` instead of `process-compose` diff --git a/docs/superpowers/specs/2026-05-27-production-github-actions-design.md b/docs/superpowers/specs/2026-05-27-production-github-actions-design.md new file mode 100644 index 0000000..de73aa4 --- /dev/null +++ b/docs/superpowers/specs/2026-05-27-production-github-actions-design.md @@ -0,0 +1,100 @@ +# Production GitHub Actions redesign + +## Goal + +Rework `.github/workflows` into a production baseline that balances reliability, security, speed, and clear failures while keeping automatic deploys from `main`. + +## Current problems + +- Deploy runner checks out submodules recursively even though deploy work happens on the VPS. Fresh submodule SHAs can fail with `not our ref` before deploy starts. +- Repository dispatch can update a parent submodule pointer before the submodule SHA is fetchable by GitHub Actions. +- Build and deploy permissions are broader than needed at workflow level. +- Failure logs do not clearly separate build, manifest update, submodule readiness, and deployment phases. + +## Chosen approach + +Use a split pipeline: + +1. `docker-build-push.yml` remains the build and manifest update pipeline. +2. `deploy-docker.yml` remains deploy-only. +3. Repository dispatch waits for the requested submodule SHA to be fetchable before building and updating the parent pointer. +4. Deploy checkout no longer uses recursive submodules; the VPS updates submodules after resetting to `origin/main`. + +## Build workflow design + +Triggers: + +- `push` to `main` for app/package/docker/workflow changes. +- `repository_dispatch` with `service` and `sha` payload. +- `workflow_dispatch` for manual full builds. + +Jobs: + +- `changes`: detects the service matrix and validates dispatch payloads. +- `build`: builds and pushes only selected service images using Docker Buildx registry cache. +- `update-manifest`: updates compose image tags and, for dispatch events, updates the matching submodule pointer. + +Repository dispatch handling: + +- For dispatch events, wait until `git ls-remote` or equivalent fetch confirms the payload SHA exists in the service submodule remote. +- Retry for a bounded timeout and fail with an explicit message if the SHA never becomes visible. +- Only after readiness is confirmed, checkout the SHA in the submodule and commit the parent pointer update. + +## Deploy workflow design + +Triggers: + +- `workflow_run` from successful `Build and Push Docker Images` on `main`. +- `push` to `main` for `infra/compose/**` and deploy workflow changes. +- `workflow_dispatch`. + +Behavior: + +- Keep automatic deploy from `main`. +- Keep a single deploy concurrency group. +- Use non-recursive checkout on the runner. +- On the VPS, fetch/reset `origin/main`, update submodules, compute changed compose stacks, pull images, and run Docker Compose. + +## Permissions and action trust + +Defaults: + +```yaml +permissions: + contents: read +``` + +Job-specific permissions: + +- Build job: `contents: read`, `packages: write`. +- Manifest update job: `contents: write`. +- Deploy job: `contents: read`. + +Action pinning: + +- GitHub-owned and Docker official actions may use major versions such as `actions/checkout@v4` and `docker/build-push-action@v6`. +- Any future third-party action should be pinned to a full commit SHA. + +## Reliability details + +- Keep `set -euo pipefail` in shell steps. +- Add bounded retry around submodule SHA readiness. +- Add clear logs for selected services, image tags, compose files changed, and dispatch payload values. +- Avoid recursive submodule checkout in deploy to eliminate fresh-SHA checkout race. +- Manifest commits continue using `[skip ci]` to prevent build loops. + +## Speed details + +- Keep selective matrix builds. +- Keep Docker Buildx registry cache. +- Keep deploy checkout shallow and submodule-free. +- Avoid rebuilding from compose-only manifest commits. + +## Validation + +Before marking implementation complete: + +- Validate workflow YAML syntax. +- Run `gh workflow list` or equivalent sanity checks. +- Verify build workflow still detects React updates. +- Verify deploy workflow no longer fails during runner checkout for fresh submodule SHAs. diff --git a/docs/superpowers/specs/2026-07-09-monorepo-restructure-design.md b/docs/superpowers/specs/2026-07-09-monorepo-restructure-design.md new file mode 100644 index 0000000..ba345ba --- /dev/null +++ b/docs/superpowers/specs/2026-07-09-monorepo-restructure-design.md @@ -0,0 +1,135 @@ +# Monorepo Restructure: ultimate-asepharyana.tech β†’ asepharyana-hub + +**Date:** 2026-07-09 +**Status:** Approved +**Owner:** @asepharyana + +## Background + +Proyek ini sebelumnya bernama `ultimate-asepharyana.tech` β€” sebuah monorepo yang berisi beberapa service aplikasi, infrastruktur, dokumentasi, dan utility scripts. Karena GitHub org `MythEclipse` kena banned, semua remote repos tidak bisa diakses. Perlu dilakukan restruktur dan rename project secara menyeluruh. + +## Goals + +1. Rename project dari `ultimate-asepharyana.tech` ke `asepharyana-hub` +2. Hapus service docker-manager dan teleuploader dari proyek +3. Hapus semua pointer `.git` submodule (clean slate) +4. Update `.gitmodules` dengan remote baru ke `github.com/asepharyana/*` +5. Update semua referensi: workflows, konfigurasi, dokumentasi, image names +6. Siapkan struktur lokal β€” inisialisasi git dan push dilakukan terpisah + +## Service yang tetap dipertahankan + +| Service | Path | Git remote baru | +|---------|------|----------------| +| Elysia API | `apps/elysia` | `asepharyana/asepharyana-hub-elysia` | +| React Frontend | `apps/react` | `asepharyana/asepharyana-hub-react` | +| Scraper | `apps/scraper` | `asepharyana/asepharyana-hub-scraper` | +| Rust Auth | `apps/rust-auth` | `asepharyana/asepharyana-hub-rust-auth` | + +## Service yang dihapus + +| Service | Path | +|---------|------| +| Docker Manager | `apps/docker-manager/` | +| TeleUploader | `apps/teleuploader/` | + +## File yang akan dihapus + +- `apps/docker-manager/` (seluruh direktori) +- `apps/teleuploader/` (seluruh direktori) +- `infra/compose/docker-manager.yml` +- `infra/compose/teleuploader.yml` +- `infra/docker/docker-manager.Dockerfile` +- `infra/docker/teleuploader.Dockerfile` + +## File pointer `.git` submodule yang dihapus + +- `apps/elysia/.git` +- `apps/react/.git` +- `apps/scraper/.git` +- `apps/rust-auth/.git` + +## Rename mapping + +| Lokasi | Dari | Ke | +|--------|------|----| +| `package.json` `name` | `ultimate-asepharyana.tech` | `asepharyana-hub` | +| `README.md` | judul & path references | `asepharyana-hub` | +| `ARCHITECTURE.md` | directory tree, paths | `asepharyana-hub` | +| `CONTRIBUTING.md` | repo names & paths | `asepharyana-hub-*` | +| `infra/README.md` | deskripsi | `asepharyana-hub` | +| `.env.example` | `TRAEFIK_CONFIG_PATH` | `asepharyana-hub` | +| Perlengkapan infra Traefik | path config references | `asepharyana-hub` | +| `scripts/cleanup-ghcr.sh` | `MythEclipse` | `asepharyana` | +| `docs/add-new-app.md` | path references | `asepharyana-hub` | +| `docs/adr/*.md` | path references | `asepharyana-hub` | + +## Image name migration (GHCR) + +| Lama | Baru | +|------|------| +| `ghcr.io/mytheclipse/ultimate-asepharyana.tech/elysia-api:*` | `ghcr.io/asepharyana/asepharyana-hub/elysia-api:*` | +| `ghcr.io/mytheclipse/ultimate-asepharyana.tech/react-web:*` | `ghcr.io/asepharyana/asepharyana-hub/react-web:*` | +| `ghcr.io/mytheclipse/ultimate-asepharyana.tech/scraper-api:*` | `ghcr.io/asepharyana/asepharyana-hub/scraper-api:*` | +| `ghcr.io/mytheclipse/ultimate-asepharyana.tech/rust-auth:*` | `ghcr.io/asepharyana/asepharyana-hub/rust-auth:*` | +| `ghcr.io/mytheclipse/ultimate-asepharyana.tech/docker-manager:*` | β€” (dihapus) | +| `ghcr.io/mytheclipse/ultimate-asepharyana.tech/teleuploader:*` | β€” (dihapus) | + +## Workflow changes + +### `docker-build-push.yml` +- `IMAGE_NAME_PREFIX`: `mytheclipse/ultimate-asepharyana.tech` β†’ `asepharyana/asepharyana-hub` +- Hapus service entries: `docker-manager`, `teleuploader` +- Update repo URLs di `wait-submodule-ref` dari `MythEclipse/*` ke `asepharyana/*` +- Update `update-manifest` phase β€” hapus service docker-manager & teleuploader + +### `deploy-docker.yml` +- Update `git remote add origin` +- Hapus docker-manager & teleuploader dari compose list dan checkout + +### `update-submodule.yml` +- Hapus service docker-manager & teleuploader +- Update nama workflow + +## `.gitmodules` + +Hanya berisi 4 apps dengan remote baru: + +```ini +[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 +``` + +## Execution plan + +1. Hapus docker-manager & teleuploader direktori + file infra +2. Hapus `.git` pointer di submodule +3. Update `.gitmodules` +4. Update `package.json` +5. Update `README.md`, `ARCHITECTURE.md`, `CONTRIBUTING.md` +6. Update `infra/compose/*.yml` image tags +7. Update `.env.example`, `infra/README.md`, docs traefik +8. Update `docker-build-push.yml` +9. Update `deploy-docker.yml` +10. Update `update-submodule.yml` +11. Update `scripts/cleanup-ghcr.sh` +12. Update `docs/add-new-app.md` +13. Update `docs/adr/*.md` path references (historical) + +## Post-execution state + +- Root direktori `asepharyana-hub/` dengan source code apps utuh (tanpa git) +- 4 app submodule terdaftar di `.gitmodules` dengan remote baru +- Infra/docs/scripts tetap menyatu di root +- 0 references ke `MythEclipse/ultimate-asepharyana.tech` di file konfigurasi +- Siap untuk `git init && git add && git commit` kapan saja diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..c2fd5c7 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,10 @@ +import antfu from '@antfu/eslint-config'; + +export default antfu({ + formatters: true, + typescript: true, + rules: { + 'no-console': 'off', + 'eslint-comments/no-unlimited-disable': 'off', + }, +}); diff --git a/infra/README.md b/infra/README.md new file mode 100644 index 0000000..2af9394 --- /dev/null +++ b/infra/README.md @@ -0,0 +1,98 @@ +# Infrastructure + +Docker Compose and Traefik configuration for `asepharyana-hub`. + +## Layout + +```text +infra/ +β”œβ”€β”€ compose/ # One compose file per stack/service +β”‚ β”œβ”€β”€ traefik.yml # Public reverse proxy +β”‚ β”œβ”€β”€ shared.yml # Shared Redis +β”‚ β”œβ”€β”€ react.yml # React SPA +β”‚ β”œβ”€β”€ scraper.yml # Scraper API +β”‚ β”œβ”€β”€ elysia.yml # Elysia API +β”‚ └── rust-auth.yml # Rust auth API +β”œβ”€β”€ docker/ # Dockerfiles and image runtime helpers +β”œβ”€β”€ traefik/ # Static and dynamic Traefik configuration +β”‚ β”œβ”€β”€ dynamic/ # Routers, services, middlewares, TLS certs +β”‚ └── TRAEFIK_ENV_CONFIG.md +└── config/ # Service bootstrap configuration +``` + +Archived configs that are not deployed live under `docs/config/`. + +## First-time setup + +Create the shared Docker network before starting any service: + +```bash +docker network create app-shared-net +``` + +Create `.env` from `.env.example` and fill production values. Do not commit `.env`. + +## Deployment order + +The GitHub deploy workflow combines the active compose files automatically. For manual deployment, use this order: + +```bash +docker compose -f infra/compose/shared.yml up -d +docker compose -f infra/compose/traefik.yml up -d + +docker compose \ + -f infra/compose/react.yml \ + -f infra/compose/scraper.yml \ + -f infra/compose/elysia.yml \ + -f infra/compose/rust-auth.yml \ + up -d +``` + +## Environment variables + +Common variables used by infra compose files: + +```env +DATABASE_URL= +GITHUB_TOKEN= +JWT_SECRET= +SHARED_REDIS_EXPOSE=127.0.0.1:6379:6379 +``` + +Traefik certificate path variables are optional because `infra/compose/traefik.yml` provides production-compatible defaults. See `infra/traefik/TRAEFIK_ENV_CONFIG.md` for the full list. + +## Traefik + +Traefik reads dynamic config from `infra/traefik/dynamic/`: + +- `apps.yaml` β€” routers and upstream services +- `middlewares.yaml` β€” shared middleware chains +- `ssl.yaml` β€” TLS certificates + +The primary certificate intentionally pairs `asephstech.pem` with `asephscloud.key` to preserve the current production layout. + +## Validation + +Run syntax checks after editing infra YAML: + +```bash +python - <<'PY' +import pathlib, yaml +for path in pathlib.Path('infra').rglob('*.yml'): + with path.open() as fh: + yaml.safe_load(fh) + print(f'OK {path}') +for path in pathlib.Path('infra').rglob('*.yaml'): + with path.open() as fh: + yaml.safe_load(fh) + print(f'OK {path}') +PY +``` + +Check compose rendering when Docker is available: + +```bash +for f in infra/compose/*.yml; do + docker compose -f "$f" config >/dev/null && echo "OK $f" +done +``` diff --git a/infra/compose/elysia.yml b/infra/compose/elysia.yml new file mode 100644 index 0000000..455d555 --- /dev/null +++ b/infra/compose/elysia.yml @@ -0,0 +1,24 @@ +services: + elysia-api: + container_name: elysia-api + image: ghcr.io/asepharyana/asepharyana-hub/elysia-api:sha-0899edc + restart: always + networks: + app-shared-net: + aliases: + - elysia-api + env_file: + - ../../.env + environment: + - REDIS_URL=redis://redis:6379 + - JWT_SECRET=${JWT_SECRET:?JWT_SECRET is required} + - DATABASE_URL=${DATABASE_URL} + - GITHUB_TOKEN=${GITHUB_TOKEN} + - PORT=4092 + - OTEL_EXPORTER_OTLP_ENDPOINT=http://otlp-metrics-backend:4317 + - OTEL_SERVICE_NAME=elysia-api + +networks: + app-shared-net: + name: app-shared-net + external: true diff --git a/infra/compose/react.yml b/infra/compose/react.yml new file mode 100644 index 0000000..fac299f --- /dev/null +++ b/infra/compose/react.yml @@ -0,0 +1,13 @@ +services: + react-web: + container_name: react-web + image: ghcr.io/asepharyana/asepharyana-hub/react-web:sha-0899edc + restart: always + networks: [app-shared-net] + environment: + - VITE_API_URL=https://scraper.asepharyana.my.id/api + - VITE_ELYSIA_URL=https://elysia.asepharyana.my.id +networks: + app-shared-net: + name: app-shared-net + external: true diff --git a/infra/compose/rust-auth.yml b/infra/compose/rust-auth.yml new file mode 100644 index 0000000..f5d952e --- /dev/null +++ b/infra/compose/rust-auth.yml @@ -0,0 +1,20 @@ +services: + rust-auth: + container_name: rust-auth-api + image: ghcr.io/asepharyana/asepharyana-hub/rust-auth:sha-0899edc + restart: always + networks: + app-shared-net: + aliases: + - rust-auth + env_file: + - ../../.env + environment: + - PORT=3000 + - OTEL_EXPORTER_OTLP_ENDPOINT=http://otlp-metrics-backend:4317 + - OTEL_SERVICE_NAME=rust-auth + +networks: + app-shared-net: + name: app-shared-net + external: true diff --git a/infra/compose/scraper.yml b/infra/compose/scraper.yml new file mode 100644 index 0000000..b91db1c --- /dev/null +++ b/infra/compose/scraper.yml @@ -0,0 +1,21 @@ +services: + scraper-api: + container_name: scraper-api + image: ghcr.io/asepharyana/asepharyana-hub/scraper-api:sha-0899edc + restart: always + networks: + app-shared-net: + aliases: + - scraper-api + env_file: + - ../../.env + environment: + - REDIS_URL=redis://redis:6379 + - JWT_SECRET=${JWT_SECRET:?JWT_SECRET is required} + - DATABASE_URL=${DATABASE_URL} + - OTEL_EXPORTER_OTLP_ENDPOINT=http://otlp-metrics-backend:4317 + - OTEL_SERVICE_NAME=scraper-api +networks: + app-shared-net: + name: app-shared-net + external: true diff --git a/infra/compose/shared.yml b/infra/compose/shared.yml new file mode 100644 index 0000000..9345cde --- /dev/null +++ b/infra/compose/shared.yml @@ -0,0 +1,21 @@ +services: + redis: + container_name: redis + image: 'redis:alpine' + restart: always + networks: + app-shared-net: + aliases: + - redis + ports: + - '${SHARED_REDIS_EXPOSE:-127.0.0.1:6379:6379}' + volumes: + - 'redis_data:/data' + +networks: + app-shared-net: + name: app-shared-net + external: true + +volumes: + redis_data: null diff --git a/infra/compose/traefik.yml b/infra/compose/traefik.yml new file mode 100644 index 0000000..d838861 --- /dev/null +++ b/infra/compose/traefik.yml @@ -0,0 +1,74 @@ +services: + traefik: + container_name: traefik + image: traefik:v3.6 + restart: always + sysctls: + - net.core.somaxconn=65535 + - net.ipv4.ip_local_port_range=1024 65535 + ulimits: + nofile: + soft: 1048576 + hard: 1048576 + ports: + - '80:80' + - '443:443' + networks: + - app-shared-net + extra_hosts: + - 'host.docker.internal:10.0.1.1' + command: + - '--api.dashboard=true' + - '--api.insecure=false' + - '--providers.docker=true' + - '--providers.docker.endpoint=unix:///var/run/docker.sock' + - '--providers.docker.exposedByDefault=false' + - '--providers.docker.network=app-shared-net' + - '--providers.docker.watch=true' + - '--providers.file.directory=/etc/traefik/dynamic' + - '--providers.file.watch=true' + - '--entryPoints.web.address=:80' + - '--entryPoints.web.http.redirections.entryPoint.to=websecure' + - '--entryPoints.web.http.redirections.entryPoint.scheme=https' + - '--accesslog=true' + - '--accesslog.bufferingsize=100' + - '--log.level=INFO' + - '--log.format=json' + - '--entryPoints.web.transport.respondingTimeouts.readTimeout=0' + - '--entryPoints.web.transport.respondingTimeouts.writeTimeout=0' + - '--entryPoints.web.transport.respondingTimeouts.idleTimeout=0' + - '--entryPoints.web.transport.lifeCycle.requestAcceptGraceTimeout=15s' + - '--entryPoints.web.transport.lifeCycle.graceTimeOut=10s' + - '--entryPoints.websecure.transport.respondingTimeouts.readTimeout=0' + - '--entryPoints.websecure.transport.respondingTimeouts.writeTimeout=0' + - '--entryPoints.websecure.transport.respondingTimeouts.idleTimeout=0' + - '--entryPoints.websecure.transport.lifeCycle.requestAcceptGraceTimeout=15s' + - '--entryPoints.websecure.transport.lifeCycle.graceTimeOut=10s' + - '--entryPoints.websecure.address=:443' + - '--experimental.plugins.real-ip.moduleName=github.com/soulbalz/traefik-real-ip' + - '--experimental.plugins.real-ip.version=v1.0.3' + - '--experimental.plugins.blockpath.moduleName=github.com/traefik/plugin-blockpath' + - '--experimental.plugins.blockpath.version=v0.2.1' + environment: + - DOCKER_API_VERSION=1.41 + - GOMEMLIMIT=4096MiB + - GOGC=200 + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ${TRAEFIK_CONFIG_PATH:-/root/asepharyana-hub/infra/traefik/dynamic}:/etc/traefik/dynamic:ro + - ${TRAEFIK_CERT_ASEPHARYANA_MY_ID_PEM:-/root/asepharyana.my.id.pem}:/etc/traefik/certs/asepharyana.my.id.pem:ro + - ${TRAEFIK_CERT_ASEPHARYANA_MY_ID_KEY:-/root/asepharyana.my.id.key}:/etc/traefik/certs/asepharyana.my.id.key:ro + - ${TRAEFIK_CERT_ASEPHARYANA_WEB_ID_PEM:-/root/asepharyana.web.id.pem}:/etc/traefik/certs/asepharyana.web.id.pem:ro + - ${TRAEFIK_CERT_ASEPHARYANA_WEB_ID_KEY:-/root/asepharyana.web.id.key}:/etc/traefik/certs/asepharyana.web.id.key:ro + labels: + - 'traefik.enable=true' + - 'traefik.http.routers.traefik.rule=Host(`traefik.asepharyana.my.id`) || Host(`traefik.asepharyana.web.id`)' + - 'traefik.http.routers.traefik.service=api@internal' + - 'traefik.http.routers.traefik.entrypoints=websecure' + - 'traefik.http.routers.traefik.tls=true' + - 'traefik.http.routers.traefik.middlewares=admin-chain@file' + +networks: + app-shared-net: + name: app-shared-net + external: true diff --git a/infra/config/mysql/init.sql/00_init_user.sql b/infra/config/mysql/init.sql/00_init_user.sql new file mode 100644 index 0000000..2784633 --- /dev/null +++ b/infra/config/mysql/init.sql/00_init_user.sql @@ -0,0 +1,10 @@ +-- Create database if not exists +CREATE DATABASE IF NOT EXISTS `tracer_study`; + +-- Create dedicated user for tracer_study +CREATE USER IF NOT EXISTS 'tracerstudy'@'%' IDENTIFIED BY 'tracerstudy_secret'; +GRANT ALL PRIVILEGES ON `tracer_study`.* TO 'tracerstudy'@'%'; + + +-- Flush privileges +FLUSH PRIVILEGES; diff --git a/infra/config/mysql/init.sql/tracer_study.sql b/infra/config/mysql/init.sql/tracer_study.sql new file mode 100644 index 0000000..88d51ed --- /dev/null +++ b/infra/config/mysql/init.sql/tracer_study.sql @@ -0,0 +1,1329 @@ +-- phpMyAdmin SQL Dump +-- version 5.2.0 +-- https://www.phpmyadmin.net/ +-- +-- Host: localhost:3306 +-- Generation Time: Mar 07, 2026 at 10:39 AM +-- Server version: 8.0.30 +-- PHP Version: 8.3.19 + +SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO"; +START TRANSACTION; +SET time_zone = "+00:00"; + + +/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; +/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; +/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; +/*!40101 SET NAMES utf8mb4 */; + +-- +-- Database: `tracer_study` +-- + +-- -------------------------------------------------------- + +-- +-- Table structure for table `alumnis` +-- + +CREATE TABLE `alumnis` ( + `id` bigint UNSIGNED NOT NULL, + `user_id` bigint UNSIGNED NOT NULL, + `nim` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `username` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `birth_place` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `birth_date` date NOT NULL, + `gender` enum('male','female') COLLATE utf8mb4_unicode_ci NOT NULL, + `address` text COLLATE utf8mb4_unicode_ci NOT NULL, + `phone_number` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `gpa` decimal(3,2) NOT NULL, + `faculty` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `major` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `entry_year` year NOT NULL, + `graduation_year` year NOT NULL, + `status` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `category_field` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `waiting_period` int DEFAULT NULL, + `job_position` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `company_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `foto` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `alumnis` +-- + +INSERT INTO `alumnis` (`id`, `user_id`, `nim`, `username`, `birth_place`, `birth_date`, `gender`, `address`, `phone_number`, `email`, `gpa`, `faculty`, `major`, `entry_year`, `graduation_year`, `status`, `category_field`, `waiting_period`, `job_position`, `company_name`, `foto`, `created_at`, `updated_at`) VALUES +(11, 12, '701220220', 'Jaka', 'Jambi', '2003-12-18', 'male', 'muaro,jambi', '081520222070', 'jaka@gmail.com', '4.00', 'Sains dan Teknologi', 'Sistem Informasi', 2023, 2027, 'bekerja', 'product & analysis', 2, 'Business Analyst', 'PT Teknologi Maju', NULL, '2025-12-28 13:22:37', '2026-01-21 00:23:19'), +(12, 13, '701220127', 'Nadia Oktarina', 'Palembang', '2003-10-01', 'female', 'muaro,jambi', '081520222026', 'nadiaokta847@gmail.com', '4.00', 'Sains dan Teknologi', 'Sistem Informasi', 2022, 2026, 'bekerja', 'design & ux', 1, 'UI/UX Designer', 'PT PERTAMINA PERSERO', NULL, '2025-12-28 13:53:07', '2026-03-03 03:24:50'), +(13, 14, '701220001', 'Ahmad Fauzi', 'Bandung', '2000-01-15', 'male', 'Jl. Merdeka No 1', '081234567801', 'ahmad.fauzi@gmail.com', '3.45', 'Sains dan Teknologi', 'Sistem Informasi', 2018, 2022, 'bekerja', 'it & development', 7, 'Programmer', 'PT Teknologi Maju', NULL, '2025-12-28 14:25:04', '2026-01-21 00:34:57'), +(14, 15, '701220002', 'Siti Aisyah', 'Jakarta', '1999-05-20', 'female', 'Jl. Sudirman No 10', '081234567802', 'siti.aisyah@gmail.com', '3.80', 'Sains dan Teknologi', 'Sistem Informasi', 2017, 2021, 'bekerja', 'product & analysis', 9, 'System Analyst', 'PT Digital Solusi', NULL, '2025-12-28 14:25:04', '2026-01-21 00:34:32'), +(15, 16, '701220003', 'Budi Santoso', 'Surabaya', '2000-03-10', 'male', 'Jl. Pahlawan No 5', '081234567803', 'budi.santoso@gmail.com', '3.20', 'Sains dan Teknologi', 'Sistem Informasi', 2018, 2022, 'belum bekerja', NULL, NULL, NULL, NULL, NULL, '2025-12-28 14:25:05', '2026-01-21 00:33:56'), +(16, 17, '701220004', 'Rina Marlina', 'Bogor', '2001-07-08', 'female', 'Jl. Raya Bogor Km 30', '081234567804', 'rina.marlina@gmail.com', '3.60', 'Sains dan Teknologi', 'Sistem Informasi', 2019, 2023, 'bekerja', 'design & ux', 19, 'UI/UX Designer', 'PT Kreatif Digital', NULL, '2025-12-28 14:25:05', '2026-01-21 00:33:26'), +(17, 18, '701220005', 'Dedi Pratama', 'Medan', '1999-11-25', 'male', 'Jl. Gatot Subroto No 12', '081234567805', 'dedi.pratama@gmail.com', '3.10', 'Sains dan Teknologi', 'Sistem Informasi', 2017, 2021, 'belum bekerja', NULL, NULL, NULL, NULL, NULL, '2025-12-28 14:25:05', '2026-01-21 00:32:45'), +(18, 19, '701220006', 'Lina Putri', 'Padang', '2000-09-14', 'female', 'Jl. Veteran No 7', '081234567806', 'lina.putri@gmail.com', '3.75', 'Sains dan Teknologi', 'Sistem Informasi', 2018, 2022, 'bekerja', 'product & analysis', 24, 'Data Analyst', 'PT Data Cerdas', NULL, '2025-12-28 14:25:06', '2026-01-21 00:32:19'), +(19, 20, '701220007', 'Rizky Maulana', 'Depok', '2001-02-02', 'male', 'Jl. Margonda Raya', '081234567807', 'rizky.maulana@gmail.com', '3.50', 'Sains dan Teknologi', 'Sistem Informasi', 2019, 2023, 'bekerja', 'it & development', 30, 'Backend Developer', 'PT Software Nusantara', NULL, '2025-12-28 14:25:06', '2026-01-21 00:31:07'), +(20, 21, '701220008', 'Putri Ananda', 'Palembang', '2000-06-18', 'female', 'Jl. Sudirman Ilir', '081234567808', 'putri.ananda@gmail.com', '3.90', 'Sains dan Teknologi', 'Sistem Informasi', 2018, 2022, 'bekerja', 'it & development', 22, 'Mobile Developer', 'PT Aplikasi Hebat', NULL, '2025-12-28 14:25:07', '2026-01-21 00:30:35'), +(21, 22, '701220009', 'Andi Wijaya', 'Makassar', '1999-12-30', 'male', 'Jl. Pettarani No 9', '081234567809', 'andi.wijaya@gmail.com', '3.00', 'Sains dan Teknologi', 'Sistem Informasi', 2017, 2021, 'belum bekerja', NULL, NULL, NULL, NULL, NULL, '2025-12-28 14:25:07', '2026-01-21 00:29:08'), +(22, 23, '701220010', 'Nurul Hidayah', 'Yogyakarta', '2001-04-05', 'female', 'Jl. Kaliurang Km 5', '081234567810', 'nurul.hidayah@gmail.com', '3.85', 'Sains dan Teknologi', 'Sistem Informasi', 2019, 2023, 'bekerja', 'product & analysis', 19, 'Risk Analyst', 'PT BTN tbk', NULL, '2025-12-28 14:25:07', '2026-01-21 00:29:43'), +(80, 50, '701220015', 'Dedi Pratama', 'Medan', '1996-03-23', 'male', 'Jl. Gatot Subroto', '081355566678', 'dedi.pratama20@gmail.com', '3.10', 'Sains dan Teknologi', 'Sistem Informasi', 2016, 2020, 'wirausaha', NULL, NULL, NULL, 'Usaha Mandiri', NULL, '2026-01-13 02:44:37', '2026-01-21 00:28:44'), +(98, 68, '701220011', 'Ahmad Fauzi', 'Bandung', '1999-05-12', 'male', 'Jl. Merdeka No. 10', '081234567890', 'ahmad.fauzi02@gmail.com', '3.45', 'Sains dan Teknologi', 'Sistem Informasi', 2018, 2022, 'bekerja', 'it & development', 2, 'Software Engineer', 'PT Teknologi Nusantara', NULL, '2026-01-13 03:06:32', '2026-01-21 00:28:04'), +(99, 69, '701220012', 'Siti Aminah', 'Jakarta', '2000-02-20', 'female', 'Jl. Sudirman No. 5', '081298765432', 'siti.aminah@gmail.com', '3.67', 'Sains dan Teknologi', 'Sistem Informasi', 2019, 2023, 'bekerja', 'product & analysis', 3, 'Product Analyst', 'PT Digital Solusi', NULL, '2026-01-13 03:06:32', '2026-01-21 00:27:30'), +(100, 70, '701220013', 'Budi Santoso', 'Surabaya', '1998-11-03', 'male', 'Jl. Ahmad Yani No. 21', '081377788899', 'budi.santoso22@gmail.com', '3.20', 'Sains dan Teknologi', 'Sistem Informasi', 2017, 2021, 'belum bekerja', NULL, NULL, NULL, NULL, NULL, '2026-01-13 03:06:33', '2026-01-21 00:26:52'), +(101, 84, '701220014', 'Rina badria', 'Yogyakarta', '1999-07-15', 'female', 'Jl. Kaliurang Km 7', '081366679088', 'rinabadr@gmail.com', '3.80', 'Sains dan Teknologi', 'Sistem Informasi', 2019, 2023, 'bekerja', 'business & management', 1, 'Business Analyst', 'PT Konsultan Maju', NULL, '2026-01-13 03:06:34', '2026-02-27 21:36:08'), +(102, 72, '701220133', 'M Syarifuddin', 'Palembang', '2003-01-16', 'male', 'jambi, batang hari', '081520222234', 'udin@gmail.com', '3.29', 'Sains dan Teknologi', 'Sistem Informasi', 2020, 2024, 'wirausaha', NULL, NULL, NULL, 'Bisnis Service Leptop', NULL, '2026-01-14 08:09:07', '2026-01-21 00:25:24'), +(104, 78, '701220251', 'Ahmad Fairuz Akbar', 'Surakarta', '2000-02-27', 'male', 'Jl. Merdeka No. 10', '81234567890', 'ahmadfairuz1@gmail.com', '3.45', 'Fakultas Ilmu Komputer', 'Sistem Informasi', 2018, 2022, 'bekerja', 'it & development', 2, 'Software Engineer', 'PT Teknologi Nusantara', NULL, '2026-01-26 01:00:24', '2026-02-28 00:50:13'), +(106, 80, '701220335', 'Mustofa', 'Palembang', '2002-10-12', 'male', 'Jln Sunan Ampel No 10, Kota Palembang', '081532236770', 'mustop4@gmail.com', '3.98', 'Sains dan Teknologi', 'Sistem Informasi', 2021, 2025, 'belum bekerja', NULL, NULL, NULL, NULL, NULL, '2026-02-27 20:13:10', '2026-02-27 20:13:10'), +(107, 81, '701220515', 'Ikbal', 'Bandung', '1999-08-12', 'male', 'Jl. Merdeka No. 10', '081234556890', 'ikbal2@gmail.com', '3.65', 'Sains dan Teknologi', 'Sistem Informasi', 2019, 2023, 'bekerja', 'it & development', 6, 'Software Engineer', 'PT Teknologi Nusantara', NULL, '2026-02-27 21:22:01', '2026-02-27 21:22:01'), +(108, 82, '701220516', 'Siti Rohaya', 'Brebes', '2001-02-20', 'female', 'Jl. Sudirman No. 5', '081599765432', 'sitiryh@gmail.com', '3.69', 'Sains dan Teknologi', 'Sistem Informasi', 2019, 2023, 'bekerja', 'product & analysis', 10, 'Product Analyst', 'PT Digital Solusi', NULL, '2026-02-27 21:22:01', '2026-02-27 21:22:01'), +(109, 83, '701220518', 'Budiman', 'Jakarta', '1999-12-03', 'male', 'Jl. Ahmad Yani No. 21', '081377788899', 'budiman02@gmail.com', '3.70', 'Fakultas Ilmu Komputer', 'Sistem Informasi', 2019, 2023, 'belum bekerja', NULL, NULL, NULL, NULL, NULL, '2026-02-27 21:22:02', '2026-02-27 21:36:07'), +(123, 85, '701220615', 'Rendi Pratama', 'Medan', '1999-03-08', 'male', 'Jl. Gatot Subroto', '081353566677', 'rendipratama10@gmail.com', '3.70', 'Sains dan Teknologi', 'Sistem Informasi', 2019, 2023, 'bekerja', 'operation & support', 4, 'IT Support', 'Usaha Mandiri', NULL, '2026-02-27 21:36:09', '2026-02-27 21:36:09'); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `cache` +-- + +CREATE TABLE `cache` ( + `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `value` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL, + `expiration` int NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `cache` +-- + +INSERT INTO `cache` (`key`, `value`, `expiration`) VALUES +('tracer-study-cache-livewire-rate-limiter:62dcdddd2131784090e8a5916c260a90678e71b3', 'i:1;', 1772534892), +('tracer-study-cache-livewire-rate-limiter:62dcdddd2131784090e8a5916c260a90678e71b3:timer', 'i:1772534892;', 1772534892), +('tracer-study-cache-livewire-rate-limiter:a17961fa74e9275d529f489537f179c05d50c2f3', 'i:1;', 1772537303), +('tracer-study-cache-livewire-rate-limiter:a17961fa74e9275d529f489537f179c05d50c2f3:timer', 'i:1772537303;', 1772537303), +('tracer-study-cache-livewire-rate-limiter:c9648cc0cb3d6646e566f360255b7283d9012d02', 'i:2;', 1772535874), +('tracer-study-cache-livewire-rate-limiter:c9648cc0cb3d6646e566f360255b7283d9012d02:timer', 'i:1772535874;', 1772535874), +('tracer-study-cache-spatie.permission.cache', 'a:3:{s:5:\"alias\";a:4:{s:1:\"a\";s:2:\"id\";s:1:\"b\";s:4:\"name\";s:1:\"c\";s:10:\"guard_name\";s:1:\"r\";s:5:\"roles\";}s:11:\"permissions\";a:33:{i:0;a:4:{s:1:\"a\";i:1;s:1:\"b\";s:11:\"view_alumni\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:1:{i:0;i:1;}}i:1;a:4:{s:1:\"a\";i:2;s:1:\"b\";s:13:\"create_alumni\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:1:{i:0;i:1;}}i:2;a:4:{s:1:\"a\";i:3;s:1:\"b\";s:11:\"edit_alumni\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:1:{i:0;i:1;}}i:3;a:4:{s:1:\"a\";i:4;s:1:\"b\";s:13:\"delete_alumni\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:1:{i:0;i:1;}}i:4;a:4:{s:1:\"a\";i:5;s:1:\"b\";s:13:\"import_alumni\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:1:{i:0;i:1;}}i:5;a:4:{s:1:\"a\";i:6;s:1:\"b\";s:13:\"export_alumni\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:1:{i:0;i:1;}}i:6;a:4:{s:1:\"a\";i:7;s:1:\"b\";s:18:\"view_questionnaire\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:1:{i:0;i:1;}}i:7;a:4:{s:1:\"a\";i:8;s:1:\"b\";s:20:\"create_questionnaire\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:1:{i:0;i:1;}}i:8;a:4:{s:1:\"a\";i:9;s:1:\"b\";s:18:\"edit_questionnaire\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:1:{i:0;i:1;}}i:9;a:4:{s:1:\"a\";i:10;s:1:\"b\";s:20:\"delete_questionnaire\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:1:{i:0;i:1;}}i:10;a:4:{s:1:\"a\";i:11;s:1:\"b\";s:14:\"view_responses\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:1:{i:0;i:1;}}i:11;a:4:{s:1:\"a\";i:12;s:1:\"b\";s:16:\"export_responses\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:1:{i:0;i:1;}}i:12;a:4:{s:1:\"a\";i:13;s:1:\"b\";s:18:\"fill_questionnaire\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:1:{i:0;i:2;}}i:13;a:4:{s:1:\"a\";i:14;s:1:\"b\";s:8:\"view_job\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:2:{i:0;i:1;i:1;i:2;}}i:14;a:4:{s:1:\"a\";i:15;s:1:\"b\";s:10:\"create_job\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:2:{i:0;i:1;i:1;i:2;}}i:15;a:4:{s:1:\"a\";i:16;s:1:\"b\";s:12:\"edit_own_job\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:2:{i:0;i:1;i:1;i:2;}}i:16;a:4:{s:1:\"a\";i:17;s:1:\"b\";s:14:\"delete_own_job\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:2:{i:0;i:1;i:1;i:2;}}i:17;a:4:{s:1:\"a\";i:18;s:1:\"b\";s:16:\"view_scholarship\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:2:{i:0;i:1;i:1;i:2;}}i:18;a:4:{s:1:\"a\";i:19;s:1:\"b\";s:18:\"create_scholarship\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:2:{i:0;i:1;i:1;i:2;}}i:19;a:4:{s:1:\"a\";i:20;s:1:\"b\";s:20:\"edit_own_scholarship\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:2:{i:0;i:1;i:1;i:2;}}i:20;a:4:{s:1:\"a\";i:21;s:1:\"b\";s:22:\"delete_own_scholarship\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:2:{i:0;i:1;i:1;i:2;}}i:21;a:4:{s:1:\"a\";i:22;s:1:\"b\";s:15:\"view_internship\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:2:{i:0;i:1;i:1;i:2;}}i:22;a:4:{s:1:\"a\";i:23;s:1:\"b\";s:17:\"create_internship\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:2:{i:0;i:1;i:1;i:2;}}i:23;a:4:{s:1:\"a\";i:24;s:1:\"b\";s:19:\"edit_own_internship\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:2:{i:0;i:1;i:1;i:2;}}i:24;a:4:{s:1:\"a\";i:25;s:1:\"b\";s:21:\"delete_own_internship\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:2:{i:0;i:1;i:1;i:2;}}i:25;a:4:{s:1:\"a\";i:26;s:1:\"b\";s:20:\"view_admin_dashboard\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:1:{i:0;i:1;}}i:26;a:4:{s:1:\"a\";i:27;s:1:\"b\";s:21:\"view_alumni_dashboard\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:1:{i:0;i:2;}}i:27;a:4:{s:1:\"a\";i:28;s:1:\"b\";s:10:\"view_forum\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:2:{i:0;i:1;i:1;i:2;}}i:28;a:4:{s:1:\"a\";i:29;s:1:\"b\";s:18:\"create_forum_topic\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:2:{i:0;i:1;i:1;i:2;}}i:29;a:4:{s:1:\"a\";i:30;s:1:\"b\";s:20:\"edit_own_forum_topic\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:2:{i:0;i:1;i:1;i:2;}}i:30;a:4:{s:1:\"a\";i:31;s:1:\"b\";s:22:\"delete_own_forum_topic\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:2:{i:0;i:1;i:1;i:2;}}i:31;a:4:{s:1:\"a\";i:32;s:1:\"b\";s:17:\"reply_forum_topic\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:2:{i:0;i:1;i:1;i:2;}}i:32;a:4:{s:1:\"a\";i:33;s:1:\"b\";s:14:\"moderate_forum\";s:1:\"c\";s:3:\"web\";s:1:\"r\";a:1:{i:0;i:1;}}}s:5:\"roles\";a:2:{i:0;a:3:{s:1:\"a\";i:1;s:1:\"b\";s:5:\"admin\";s:1:\"c\";s:3:\"web\";}i:1;a:3:{s:1:\"a\";i:2;s:1:\"b\";s:6:\"alumni\";s:1:\"c\";s:3:\"web\";}}}', 1772622265); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `cache_locks` +-- + +CREATE TABLE `cache_locks` ( + `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `owner` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `expiration` int NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `cache_locks` +-- + +INSERT INTO `cache_locks` (`key`, `owner`, `expiration`) VALUES +('tracer-study-cache-laravel-queue-overlap:Filament\\Actions\\Imports\\Jobs\\ImportCsv:import3', 'rPXYSGIT8gg4QdZR', 1768297580), +('tracer-study-cache-laravel-queue-overlap:Filament\\Actions\\Imports\\Jobs\\ImportCsv:import4', 'tDnwFiMR7ZsnggVu', 1768298088), +('tracer-study-cache-laravel-queue-overlap:Filament\\Actions\\Imports\\Jobs\\ImportCsv:import6', 'a1iKRUkkRI4MSMv8', 1772253156); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `exports` +-- + +CREATE TABLE `exports` ( + `id` bigint UNSIGNED NOT NULL, + `completed_at` timestamp NULL DEFAULT NULL, + `file_disk` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `file_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `exporter` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `processed_rows` int UNSIGNED NOT NULL DEFAULT '0', + `total_rows` int UNSIGNED NOT NULL, + `successful_rows` int UNSIGNED NOT NULL DEFAULT '0', + `user_id` bigint UNSIGNED NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `exports` +-- + +INSERT INTO `exports` (`id`, `completed_at`, `file_disk`, `file_name`, `exporter`, `processed_rows`, `total_rows`, `successful_rows`, `user_id`, `created_at`, `updated_at`) VALUES +(1, '2025-12-28 12:51:02', 'local', 'export-1-alumnis', 'App\\Filament\\Exports\\AlumniExporter', 10, 10, 10, 1, '2025-12-28 12:51:01', '2025-12-28 12:51:02'), +(2, '2025-12-29 21:16:11', 'local', 'export-2-questionnaire-responses', 'App\\Filament\\Exports\\QuestionnaireResponseExporter', 2, 2, 2, 1, '2025-12-29 21:16:05', '2025-12-29 21:16:11'), +(3, '2026-01-13 02:32:40', 'local', 'export-3-alumnis', 'App\\Filament\\Exports\\AlumniExporter', 13, 13, 13, 1, '2026-01-13 02:32:33', '2026-01-13 02:32:40'), +(4, '2026-01-27 02:07:27', 'local', 'export-4-questionnaire-responses', 'App\\Filament\\Exports\\QuestionnaireResponseExporter', 10, 10, 10, 1, '2026-01-27 02:07:22', '2026-01-27 02:07:27'), +(5, '2026-01-29 03:11:14', 'local', 'export-5-questionnaire-responses', 'App\\Filament\\Exports\\QuestionnaireResponseExporter', 7, 7, 7, 1, '2026-01-29 03:11:05', '2026-01-29 03:11:14'), +(6, '2026-02-01 05:22:46', 'local', 'ekspor-6-questionnaire-responses', 'App\\Filament\\Exports\\QuestionnaireResponseExporter', 1, 1, 1, 1, '2026-02-01 05:22:39', '2026-02-01 05:22:46'); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `failed_import_rows` +-- + +CREATE TABLE `failed_import_rows` ( + `id` bigint UNSIGNED NOT NULL, + `data` json NOT NULL, + `import_id` bigint UNSIGNED NOT NULL, + `validation_error` text COLLATE utf8mb4_unicode_ci, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `failed_jobs` +-- + +CREATE TABLE `failed_jobs` ( + `id` bigint UNSIGNED NOT NULL, + `uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `connection` text COLLATE utf8mb4_unicode_ci NOT NULL, + `queue` text COLLATE utf8mb4_unicode_ci NOT NULL, + `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `forum_replies` +-- + +CREATE TABLE `forum_replies` ( + `id` bigint UNSIGNED NOT NULL, + `topic_id` bigint UNSIGNED NOT NULL, + `user_id` bigint UNSIGNED NOT NULL, + `content` text COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `forum_replies` +-- + +INSERT INTO `forum_replies` (`id`, `topic_id`, `user_id`, `content`, `created_at`, `updated_at`) VALUES +(1, 1, 78, 'info dong', '2026-01-31 05:54:19', '2026-01-31 05:54:19'), +(11, 3, 78, 'baik pak', '2026-02-01 03:27:51', '2026-02-01 03:27:51'); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `forum_topics` +-- + +CREATE TABLE `forum_topics` ( + `id` bigint UNSIGNED NOT NULL, + `user_id` bigint UNSIGNED NOT NULL, + `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `content` text COLLATE utf8mb4_unicode_ci NOT NULL, + `is_pinned` tinyint(1) NOT NULL DEFAULT '0', + `is_locked` tinyint(1) NOT NULL DEFAULT '0', + `views_count` int NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `forum_topics` +-- + +INSERT INTO `forum_topics` (`id`, `user_id`, `title`, `content`, `is_pinned`, `is_locked`, `views_count`, `created_at`, `updated_at`) VALUES +(1, 78, 'Info Magang', '

teman-teman adakah info magang sebagai admin disekitaran mendalo

', 0, 1, 66, '2026-01-31 05:46:15', '2026-02-28 01:12:31'), +(2, 1, 'Isi kuesioner ', '

Silahkan isi kuesioner tahunan kalian

', 0, 0, 42, '2026-01-31 06:11:51', '2026-02-28 01:12:58'), +(3, 1, 'Info Pengambilan Ijazah', '

Yang posisi lagi di jambi, kalo mau ambil ijazah tunggu masuk kuliah tanggal 22

note: bawa kelengkapan berkas

', 1, 0, 10, '2026-02-01 03:22:35', '2026-02-28 01:12:13'); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `imports` +-- + +CREATE TABLE `imports` ( + `id` bigint UNSIGNED NOT NULL, + `completed_at` timestamp NULL DEFAULT NULL, + `file_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `file_path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `importer` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `processed_rows` int UNSIGNED NOT NULL DEFAULT '0', + `total_rows` int UNSIGNED NOT NULL, + `successful_rows` int UNSIGNED NOT NULL DEFAULT '0', + `user_id` bigint UNSIGNED NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `imports` +-- + +INSERT INTO `imports` (`id`, `completed_at`, `file_name`, `file_path`, `importer`, `processed_rows`, `total_rows`, `successful_rows`, `user_id`, `created_at`, `updated_at`) VALUES +(1, '2025-12-28 14:25:08', 'alumni_import.csv', 'D:\\laragon\\www\\sisfor_alumni2\\filament-starter\\storage\\app/private\\livewire-tmp/cWfV2xhlYy2Rxr9PHGGeXoHjYZ0VLH-metaYWx1bW5pX2ltcG9ydC5jc3Y=-.csv', 'App\\Filament\\Imports\\AlumniImporter', 10, 10, 10, 1, '2025-12-28 14:25:02', '2025-12-28 14:25:08'), +(2, '2026-01-02 22:24:17', 'alumni_import.csv', 'D:\\laragon\\www\\sisfor_alumni2\\filament-starter\\storage\\app/private\\livewire-tmp/Qw3Se9MhdU3K7qQjEknAkuRtbZPM75-metaYWx1bW5pX2ltcG9ydC5jc3Y=-.csv', 'App\\Filament\\Imports\\AlumniImporter', 10, 10, 10, 1, '2026-01-02 22:24:08', '2026-01-02 22:24:17'), +(5, '2026-01-13 03:06:34', 'template_import_alumni.csv', 'D:\\laragon\\www\\sisfor_alumni2\\filament-starter\\storage\\app/private\\livewire-tmp/q03r9qdRsjBSWJSeLmTidKfSoxeXSc-metadGVtcGxhdGVfaW1wb3J0X2FsdW1uaS5jc3Y=-.csv', 'App\\Filament\\Imports\\AlumniImporter', 5, 5, 5, 1, '2026-01-13 03:06:29', '2026-01-13 03:06:34'), +(7, '2026-02-27 21:36:09', 'test1.csv', 'D:\\laragon\\www\\sisfor_alumni2\\filament-starter\\storage\\app/private\\livewire-tmp/a6mIDStEO7yGVnbdW4Nq5PGkARAMZL-metadGVzdDEuY3N2-.csv', 'App\\Filament\\Imports\\AlumniImporter', 5, 5, 5, 1, '2026-02-27 21:36:02', '2026-02-27 21:36:09'); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `internships` +-- + +CREATE TABLE `internships` ( + `id` bigint UNSIGNED NOT NULL, + `user_id` bigint UNSIGNED NOT NULL, + `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `company` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `location` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `description` text COLLATE utf8mb4_unicode_ci NOT NULL, + `requirements` text COLLATE utf8mb4_unicode_ci, + `duration` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `deadline` date DEFAULT NULL, + `contact_info` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `internships` +-- + +INSERT INTO `internships` (`id`, `user_id`, `title`, `company`, `location`, `description`, `requirements`, `duration`, `deadline`, `contact_info`, `created_at`, `updated_at`) VALUES +(1, 18, 'Magang Web Developer', 'PT Teknologi Maju Bersama', 'Yogyakarta, DI Yogyakarta', '

 PT Teknologi Maju Bersama membuka program magang Web Developer bagi fresh graduate yang ingin mengembangkan kemampuan dalam pengembangan aplikasi berbasis web. Peserta magang akan terlibat langsung dalam proyek pengembangan internal perusahaan. 

', '
  • Mahasiswa aktif atau fresh graduate jurusan Sistem Informasi / Teknik Informatika
  • Menguasai dasar HTML, CSS, dan JavaScript
  • Memahami framework Laravel menjadi nilai tambah
  • Bersedia bekerja secara tim dan mengikuti jadwal magang
', '6 bulan', '2026-02-05', 'internship@teknologimaju.co.id', '2026-01-02 23:13:14', '2026-01-02 23:13:14'); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `jobs` +-- + +CREATE TABLE `jobs` ( + `id` bigint UNSIGNED NOT NULL, + `queue` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `attempts` tinyint UNSIGNED NOT NULL, + `reserved_at` int UNSIGNED DEFAULT NULL, + `available_at` int UNSIGNED NOT NULL, + `created_at` int UNSIGNED NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `job_batches` +-- + +CREATE TABLE `job_batches` ( + `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `total_jobs` int NOT NULL, + `pending_jobs` int NOT NULL, + `failed_jobs` int NOT NULL, + `failed_job_ids` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `options` mediumtext COLLATE utf8mb4_unicode_ci, + `cancelled_at` int DEFAULT NULL, + `created_at` int NOT NULL, + `finished_at` int DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `job_batches` +-- + +INSERT INTO `job_batches` (`id`, `name`, `total_jobs`, `pending_jobs`, `failed_jobs`, `failed_job_ids`, `options`, `cancelled_at`, `created_at`, `finished_at`) VALUES +('a0b40b37-4a90-4670-a51d-f6fd8076b373', '', 1, 0, 0, '[]', 'a:2:{s:13:\"allowFailures\";b:1;s:7:\"finally\";a:1:{i:0;O:47:\"Laravel\\SerializableClosure\\SerializableClosure\":1:{s:12:\"serializable\";O:46:\"Laravel\\SerializableClosure\\Serializers\\Signed\":2:{s:12:\"serializable\";s:3952:\"O:46:\"Laravel\\SerializableClosure\\Serializers\\Native\":5:{s:3:\"use\";a:4:{s:9:\"columnMap\";a:17:{s:3:\"nim\";s:3:\"nim\";s:8:\"username\";s:8:\"username\";s:11:\"birth_place\";s:11:\"birth_place\";s:10:\"birth_date\";s:10:\"birth_date\";s:6:\"gender\";s:6:\"gender\";s:7:\"address\";s:7:\"address\";s:12:\"phone_number\";s:12:\"phone_number\";s:5:\"email\";s:5:\"email\";s:3:\"gpa\";s:3:\"gpa\";s:7:\"faculty\";s:7:\"faculty\";s:5:\"major\";s:5:\"major\";s:10:\"entry_year\";s:10:\"entry_year\";s:15:\"graduation_year\";s:15:\"graduation_year\";s:6:\"status\";s:6:\"status\";s:12:\"job_position\";s:12:\"job_position\";s:12:\"company_name\";s:12:\"company_name\";s:4:\"foto\";s:4:\"foto\";}s:6:\"import\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":5:{s:5:\"class\";s:38:\"Filament\\Actions\\Imports\\Models\\Import\";s:2:\"id\";i:1;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";s:15:\"collectionClass\";N;}s:13:\"jobConnection\";N;s:7:\"options\";a:0:{}}s:8:\"function\";s:2925:\"function () use ($columnMap, $import, $jobConnection, $options) {\n $import->touch(\'completed_at\');\n\n event(new \\Filament\\Actions\\Imports\\Events\\ImportCompleted($import, $columnMap, $options));\n\n if (! $import->user instanceof \\Illuminate\\Contracts\\Auth\\Authenticatable) {\n return;\n }\n\n $failedRowsCount = $import->getFailedRowsCount();\n\n \\Filament\\Notifications\\Notification::make()\n ->title($import->importer::getCompletedNotificationTitle($import))\n ->body($import->importer::getCompletedNotificationBody($import))\n ->when(\n ! $failedRowsCount,\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->success(),\n )\n ->when(\n $failedRowsCount && ($failedRowsCount < $import->total_rows),\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->warning(),\n )\n ->when(\n $failedRowsCount === $import->total_rows,\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->danger(),\n )\n ->when(\n $failedRowsCount,\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->actions([\n \\Filament\\Notifications\\Actions\\Action::make(\'downloadFailedRowsCsv\')\n ->label(trans_choice(\'filament-actions::import.notifications.completed.actions.download_failed_rows_csv.label\', $failedRowsCount, [\n \'count\' => \\Illuminate\\Support\\Number::format($failedRowsCount),\n ]))\n ->color(\'danger\')\n ->url(route(\'filament.imports.failed-rows.download\', [\'import\' => $import], absolute: false), shouldOpenInNewTab: true)\n ->markAsRead(),\n ]),\n )\n ->when(\n ($jobConnection === \'sync\') ||\n (blank($jobConnection) && (config(\'queue.default\') === \'sync\')),\n fn (\\Filament\\Notifications\\Notification $notification) => $notification\n ->persistent()\n ->send(),\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->sendToDatabase($import->user, isEventDispatched: true),\n );\n }\";s:5:\"scope\";s:36:\"Filament\\Tables\\Actions\\ImportAction\";s:4:\"this\";N;s:4:\"self\";s:32:\"0000000000000e0e0000000000000000\";}\";s:4:\"hash\";s:44:\"Il55kOM50sNFDkx0J1btTaCHBmZXRBPm1BERwLmAvMg=\";}}}}', NULL, 1766951427, 1766951432), +('a0b40b6c-e64b-4a5c-bed9-73d2ff339213', '', 2, 0, 0, '[]', 'a:2:{s:13:\"allowFailures\";b:1;s:7:\"finally\";a:1:{i:0;O:47:\"Laravel\\SerializableClosure\\SerializableClosure\":1:{s:12:\"serializable\";O:46:\"Laravel\\SerializableClosure\\Serializers\\Signed\":2:{s:12:\"serializable\";s:7132:\"O:46:\"Laravel\\SerializableClosure\\Serializers\\Native\":5:{s:3:\"use\";a:1:{s:4:\"next\";O:46:\"Filament\\Actions\\Exports\\Jobs\\ExportCompletion\":7:{s:11:\"\0*\0exporter\";O:35:\"App\\Filament\\Exports\\AlumniExporter\":3:{s:9:\"\0*\0export\";O:38:\"Filament\\Actions\\Exports\\Models\\Export\":33:{s:13:\"\0*\0connection\";s:5:\"mysql\";s:8:\"\0*\0table\";N;s:13:\"\0*\0primaryKey\";s:2:\"id\";s:10:\"\0*\0keyType\";s:3:\"int\";s:12:\"incrementing\";b:1;s:7:\"\0*\0with\";a:0:{}s:12:\"\0*\0withCount\";a:0:{}s:19:\"preventsLazyLoading\";b:0;s:10:\"\0*\0perPage\";i:15;s:6:\"exists\";b:1;s:18:\"wasRecentlyCreated\";b:1;s:28:\"\0*\0escapeWhenCastingToString\";b:0;s:13:\"\0*\0attributes\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:35:\"App\\Filament\\Exports\\AlumniExporter\";s:10:\"total_rows\";i:10;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2025-12-28 19:51:01\";s:10:\"created_at\";s:19:\"2025-12-28 19:51:01\";s:2:\"id\";i:1;s:9:\"file_name\";s:16:\"export-1-alumnis\";}s:11:\"\0*\0original\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:35:\"App\\Filament\\Exports\\AlumniExporter\";s:10:\"total_rows\";i:10;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2025-12-28 19:51:01\";s:10:\"created_at\";s:19:\"2025-12-28 19:51:01\";s:2:\"id\";i:1;s:9:\"file_name\";s:16:\"export-1-alumnis\";}s:10:\"\0*\0changes\";a:1:{s:9:\"file_name\";s:16:\"export-1-alumnis\";}s:11:\"\0*\0previous\";a:0:{}s:8:\"\0*\0casts\";a:4:{s:12:\"completed_at\";s:9:\"timestamp\";s:14:\"processed_rows\";s:7:\"integer\";s:10:\"total_rows\";s:7:\"integer\";s:15:\"successful_rows\";s:7:\"integer\";}s:17:\"\0*\0classCastCache\";a:0:{}s:21:\"\0*\0attributeCastCache\";a:0:{}s:13:\"\0*\0dateFormat\";N;s:10:\"\0*\0appends\";a:0:{}s:19:\"\0*\0dispatchesEvents\";a:0:{}s:14:\"\0*\0observables\";a:0:{}s:12:\"\0*\0relations\";a:0:{}s:10:\"\0*\0touches\";a:0:{}s:27:\"\0*\0relationAutoloadCallback\";N;s:26:\"\0*\0relationAutoloadContext\";N;s:10:\"timestamps\";b:1;s:13:\"usesUniqueIds\";b:0;s:9:\"\0*\0hidden\";a:0:{}s:10:\"\0*\0visible\";a:0:{}s:11:\"\0*\0fillable\";a:0:{}s:10:\"\0*\0guarded\";a:0:{}}s:12:\"\0*\0columnMap\";a:18:{s:7:\"user_id\";s:7:\"User id\";s:3:\"nim\";s:3:\"Nim\";s:8:\"username\";s:8:\"Username\";s:11:\"birth_place\";s:11:\"Birth place\";s:10:\"birth_date\";s:10:\"Birth date\";s:6:\"gender\";s:6:\"Gender\";s:7:\"address\";s:7:\"Address\";s:12:\"phone_number\";s:12:\"Phone number\";s:5:\"email\";s:5:\"Email\";s:3:\"gpa\";s:3:\"Gpa\";s:7:\"faculty\";s:7:\"Faculty\";s:5:\"major\";s:5:\"Major\";s:10:\"entry_year\";s:10:\"Entry year\";s:15:\"graduation_year\";s:15:\"Graduation year\";s:6:\"status\";s:6:\"Status\";s:12:\"job_position\";s:12:\"Job position\";s:12:\"company_name\";s:12:\"Company name\";s:4:\"foto\";s:4:\"Foto\";}s:10:\"\0*\0options\";a:0:{}}s:9:\"\0*\0export\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":5:{s:5:\"class\";s:38:\"Filament\\Actions\\Exports\\Models\\Export\";s:2:\"id\";i:1;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";s:15:\"collectionClass\";N;}s:12:\"\0*\0columnMap\";a:18:{s:7:\"user_id\";s:7:\"User id\";s:3:\"nim\";s:3:\"Nim\";s:8:\"username\";s:8:\"Username\";s:11:\"birth_place\";s:11:\"Birth place\";s:10:\"birth_date\";s:10:\"Birth date\";s:6:\"gender\";s:6:\"Gender\";s:7:\"address\";s:7:\"Address\";s:12:\"phone_number\";s:12:\"Phone number\";s:5:\"email\";s:5:\"Email\";s:3:\"gpa\";s:3:\"Gpa\";s:7:\"faculty\";s:7:\"Faculty\";s:5:\"major\";s:5:\"Major\";s:10:\"entry_year\";s:10:\"Entry year\";s:15:\"graduation_year\";s:15:\"Graduation year\";s:6:\"status\";s:6:\"Status\";s:12:\"job_position\";s:12:\"Job position\";s:12:\"company_name\";s:12:\"Company name\";s:4:\"foto\";s:4:\"Foto\";}s:10:\"\0*\0formats\";a:2:{i:0;E:47:\"Filament\\Actions\\Exports\\Enums\\ExportFormat:Csv\";i:1;E:48:\"Filament\\Actions\\Exports\\Enums\\ExportFormat:Xlsx\";}s:10:\"\0*\0options\";a:0:{}s:7:\"chained\";a:1:{i:0;s:3217:\"O:44:\"Filament\\Actions\\Exports\\Jobs\\CreateXlsxFile\":4:{s:11:\"\0*\0exporter\";O:35:\"App\\Filament\\Exports\\AlumniExporter\":3:{s:9:\"\0*\0export\";O:38:\"Filament\\Actions\\Exports\\Models\\Export\":33:{s:13:\"\0*\0connection\";s:5:\"mysql\";s:8:\"\0*\0table\";N;s:13:\"\0*\0primaryKey\";s:2:\"id\";s:10:\"\0*\0keyType\";s:3:\"int\";s:12:\"incrementing\";b:1;s:7:\"\0*\0with\";a:0:{}s:12:\"\0*\0withCount\";a:0:{}s:19:\"preventsLazyLoading\";b:0;s:10:\"\0*\0perPage\";i:15;s:6:\"exists\";b:1;s:18:\"wasRecentlyCreated\";b:1;s:28:\"\0*\0escapeWhenCastingToString\";b:0;s:13:\"\0*\0attributes\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:35:\"App\\Filament\\Exports\\AlumniExporter\";s:10:\"total_rows\";i:10;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2025-12-28 19:51:01\";s:10:\"created_at\";s:19:\"2025-12-28 19:51:01\";s:2:\"id\";i:1;s:9:\"file_name\";s:16:\"export-1-alumnis\";}s:11:\"\0*\0original\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:35:\"App\\Filament\\Exports\\AlumniExporter\";s:10:\"total_rows\";i:10;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2025-12-28 19:51:01\";s:10:\"created_at\";s:19:\"2025-12-28 19:51:01\";s:2:\"id\";i:1;s:9:\"file_name\";s:16:\"export-1-alumnis\";}s:10:\"\0*\0changes\";a:1:{s:9:\"file_name\";s:16:\"export-1-alumnis\";}s:11:\"\0*\0previous\";a:0:{}s:8:\"\0*\0casts\";a:4:{s:12:\"completed_at\";s:9:\"timestamp\";s:14:\"processed_rows\";s:7:\"integer\";s:10:\"total_rows\";s:7:\"integer\";s:15:\"successful_rows\";s:7:\"integer\";}s:17:\"\0*\0classCastCache\";a:0:{}s:21:\"\0*\0attributeCastCache\";a:0:{}s:13:\"\0*\0dateFormat\";N;s:10:\"\0*\0appends\";a:0:{}s:19:\"\0*\0dispatchesEvents\";a:0:{}s:14:\"\0*\0observables\";a:0:{}s:12:\"\0*\0relations\";a:0:{}s:10:\"\0*\0touches\";a:0:{}s:27:\"\0*\0relationAutoloadCallback\";N;s:26:\"\0*\0relationAutoloadContext\";N;s:10:\"timestamps\";b:1;s:13:\"usesUniqueIds\";b:0;s:9:\"\0*\0hidden\";a:0:{}s:10:\"\0*\0visible\";a:0:{}s:11:\"\0*\0fillable\";a:0:{}s:10:\"\0*\0guarded\";a:0:{}}s:12:\"\0*\0columnMap\";a:18:{s:7:\"user_id\";s:7:\"User id\";s:3:\"nim\";s:3:\"Nim\";s:8:\"username\";s:8:\"Username\";s:11:\"birth_place\";s:11:\"Birth place\";s:10:\"birth_date\";s:10:\"Birth date\";s:6:\"gender\";s:6:\"Gender\";s:7:\"address\";s:7:\"Address\";s:12:\"phone_number\";s:12:\"Phone number\";s:5:\"email\";s:5:\"Email\";s:3:\"gpa\";s:3:\"Gpa\";s:7:\"faculty\";s:7:\"Faculty\";s:5:\"major\";s:5:\"Major\";s:10:\"entry_year\";s:10:\"Entry year\";s:15:\"graduation_year\";s:15:\"Graduation year\";s:6:\"status\";s:6:\"Status\";s:12:\"job_position\";s:12:\"Job position\";s:12:\"company_name\";s:12:\"Company name\";s:4:\"foto\";s:4:\"Foto\";}s:10:\"\0*\0options\";a:0:{}}s:9:\"\0*\0export\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":5:{s:5:\"class\";s:38:\"Filament\\Actions\\Exports\\Models\\Export\";s:2:\"id\";i:1;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";s:15:\"collectionClass\";N;}s:12:\"\0*\0columnMap\";a:18:{s:7:\"user_id\";s:7:\"User id\";s:3:\"nim\";s:3:\"Nim\";s:8:\"username\";s:8:\"Username\";s:11:\"birth_place\";s:11:\"Birth place\";s:10:\"birth_date\";s:10:\"Birth date\";s:6:\"gender\";s:6:\"Gender\";s:7:\"address\";s:7:\"Address\";s:12:\"phone_number\";s:12:\"Phone number\";s:5:\"email\";s:5:\"Email\";s:3:\"gpa\";s:3:\"Gpa\";s:7:\"faculty\";s:7:\"Faculty\";s:5:\"major\";s:5:\"Major\";s:10:\"entry_year\";s:10:\"Entry year\";s:15:\"graduation_year\";s:15:\"Graduation year\";s:6:\"status\";s:6:\"Status\";s:12:\"job_position\";s:12:\"Job position\";s:12:\"company_name\";s:12:\"Company name\";s:4:\"foto\";s:4:\"Foto\";}s:10:\"\0*\0options\";a:0:{}}\";}s:19:\"chainCatchCallbacks\";a:0:{}}}s:8:\"function\";s:266:\"function (\\Illuminate\\Bus\\Batch $batch) use ($next) {\n if (! $batch->cancelled()) {\n \\Illuminate\\Container\\Container::getInstance()->make(\\Illuminate\\Contracts\\Bus\\Dispatcher::class)->dispatch($next);\n }\n }\";s:5:\"scope\";s:27:\"Illuminate\\Bus\\ChainedBatch\";s:4:\"this\";N;s:4:\"self\";s:32:\"00000000000009740000000000000000\";}\";s:4:\"hash\";s:44:\"pqKSC78MefRaewEgLAWwiqS7IqmCa0ZSeZu8cYc6PjU=\";}}}}', NULL, 1766951462, 1766951462), +('a0b42d0a-679b-4f32-a8b2-105969130d4c', '', 1, 0, 0, '[]', 'a:2:{s:13:\"allowFailures\";b:1;s:7:\"finally\";a:1:{i:0;O:47:\"Laravel\\SerializableClosure\\SerializableClosure\":1:{s:12:\"serializable\";O:46:\"Laravel\\SerializableClosure\\Serializers\\Signed\":2:{s:12:\"serializable\";s:3952:\"O:46:\"Laravel\\SerializableClosure\\Serializers\\Native\":5:{s:3:\"use\";a:4:{s:9:\"columnMap\";a:17:{s:3:\"nim\";s:3:\"nim\";s:8:\"username\";s:8:\"username\";s:11:\"birth_place\";s:11:\"birth_place\";s:10:\"birth_date\";s:10:\"birth_date\";s:6:\"gender\";s:6:\"gender\";s:7:\"address\";s:7:\"address\";s:12:\"phone_number\";s:12:\"phone_number\";s:5:\"email\";s:5:\"email\";s:3:\"gpa\";s:3:\"gpa\";s:7:\"faculty\";s:7:\"faculty\";s:5:\"major\";s:5:\"major\";s:10:\"entry_year\";s:10:\"entry_year\";s:15:\"graduation_year\";s:15:\"graduation_year\";s:6:\"status\";s:6:\"status\";s:12:\"job_position\";s:12:\"job_position\";s:12:\"company_name\";s:12:\"company_name\";s:4:\"foto\";s:4:\"foto\";}s:6:\"import\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":5:{s:5:\"class\";s:38:\"Filament\\Actions\\Imports\\Models\\Import\";s:2:\"id\";i:1;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";s:15:\"collectionClass\";N;}s:13:\"jobConnection\";N;s:7:\"options\";a:0:{}}s:8:\"function\";s:2925:\"function () use ($columnMap, $import, $jobConnection, $options) {\n $import->touch(\'completed_at\');\n\n event(new \\Filament\\Actions\\Imports\\Events\\ImportCompleted($import, $columnMap, $options));\n\n if (! $import->user instanceof \\Illuminate\\Contracts\\Auth\\Authenticatable) {\n return;\n }\n\n $failedRowsCount = $import->getFailedRowsCount();\n\n \\Filament\\Notifications\\Notification::make()\n ->title($import->importer::getCompletedNotificationTitle($import))\n ->body($import->importer::getCompletedNotificationBody($import))\n ->when(\n ! $failedRowsCount,\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->success(),\n )\n ->when(\n $failedRowsCount && ($failedRowsCount < $import->total_rows),\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->warning(),\n )\n ->when(\n $failedRowsCount === $import->total_rows,\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->danger(),\n )\n ->when(\n $failedRowsCount,\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->actions([\n \\Filament\\Notifications\\Actions\\Action::make(\'downloadFailedRowsCsv\')\n ->label(trans_choice(\'filament-actions::import.notifications.completed.actions.download_failed_rows_csv.label\', $failedRowsCount, [\n \'count\' => \\Illuminate\\Support\\Number::format($failedRowsCount),\n ]))\n ->color(\'danger\')\n ->url(route(\'filament.imports.failed-rows.download\', [\'import\' => $import], absolute: false), shouldOpenInNewTab: true)\n ->markAsRead(),\n ]),\n )\n ->when(\n ($jobConnection === \'sync\') ||\n (blank($jobConnection) && (config(\'queue.default\') === \'sync\')),\n fn (\\Filament\\Notifications\\Notification $notification) => $notification\n ->persistent()\n ->send(),\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->sendToDatabase($import->user, isEventDispatched: true),\n );\n }\";s:5:\"scope\";s:36:\"Filament\\Tables\\Actions\\ImportAction\";s:4:\"this\";N;s:4:\"self\";s:32:\"0000000000000e0e0000000000000000\";}\";s:4:\"hash\";s:44:\"Il55kOM50sNFDkx0J1btTaCHBmZXRBPm1BERwLmAvMg=\";}}}}', NULL, 1766957102, 1766957107), +('a0b6c30f-c73e-47c8-98e4-1902f8eafaa8', '', 2, 0, 0, '[]', 'a:2:{s:13:\"allowFailures\";b:1;s:7:\"finally\";a:1:{i:0;O:47:\"Laravel\\SerializableClosure\\SerializableClosure\":1:{s:12:\"serializable\";O:46:\"Laravel\\SerializableClosure\\Serializers\\Signed\":2:{s:12:\"serializable\";s:6182:\"O:46:\"Laravel\\SerializableClosure\\Serializers\\Native\":5:{s:3:\"use\";a:1:{s:4:\"next\";O:46:\"Filament\\Actions\\Exports\\Jobs\\ExportCompletion\":7:{s:11:\"\0*\0exporter\";O:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\":3:{s:9:\"\0*\0export\";O:38:\"Filament\\Actions\\Exports\\Models\\Export\":33:{s:13:\"\0*\0connection\";s:5:\"mysql\";s:8:\"\0*\0table\";N;s:13:\"\0*\0primaryKey\";s:2:\"id\";s:10:\"\0*\0keyType\";s:3:\"int\";s:12:\"incrementing\";b:1;s:7:\"\0*\0with\";a:0:{}s:12:\"\0*\0withCount\";a:0:{}s:19:\"preventsLazyLoading\";b:0;s:10:\"\0*\0perPage\";i:15;s:6:\"exists\";b:1;s:18:\"wasRecentlyCreated\";b:1;s:28:\"\0*\0escapeWhenCastingToString\";b:0;s:13:\"\0*\0attributes\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\";s:10:\"total_rows\";i:2;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2025-12-30 04:16:06\";s:10:\"created_at\";s:19:\"2025-12-30 04:16:05\";s:2:\"id\";i:2;s:9:\"file_name\";s:32:\"export-2-questionnaire-responses\";}s:11:\"\0*\0original\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\";s:10:\"total_rows\";i:2;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2025-12-30 04:16:06\";s:10:\"created_at\";s:19:\"2025-12-30 04:16:05\";s:2:\"id\";i:2;s:9:\"file_name\";s:32:\"export-2-questionnaire-responses\";}s:10:\"\0*\0changes\";a:2:{s:10:\"updated_at\";s:19:\"2025-12-30 04:16:06\";s:9:\"file_name\";s:32:\"export-2-questionnaire-responses\";}s:11:\"\0*\0previous\";a:1:{s:10:\"updated_at\";s:19:\"2025-12-30 04:16:05\";}s:8:\"\0*\0casts\";a:4:{s:12:\"completed_at\";s:9:\"timestamp\";s:14:\"processed_rows\";s:7:\"integer\";s:10:\"total_rows\";s:7:\"integer\";s:15:\"successful_rows\";s:7:\"integer\";}s:17:\"\0*\0classCastCache\";a:0:{}s:21:\"\0*\0attributeCastCache\";a:0:{}s:13:\"\0*\0dateFormat\";N;s:10:\"\0*\0appends\";a:0:{}s:19:\"\0*\0dispatchesEvents\";a:0:{}s:14:\"\0*\0observables\";a:0:{}s:12:\"\0*\0relations\";a:0:{}s:10:\"\0*\0touches\";a:0:{}s:27:\"\0*\0relationAutoloadCallback\";N;s:26:\"\0*\0relationAutoloadContext\";N;s:10:\"timestamps\";b:1;s:13:\"usesUniqueIds\";b:0;s:9:\"\0*\0hidden\";a:0:{}s:10:\"\0*\0visible\";a:0:{}s:11:\"\0*\0fillable\";a:0:{}s:10:\"\0*\0guarded\";a:0:{}}s:12:\"\0*\0columnMap\";a:6:{s:19:\"questionnaire.title\";s:9:\"Kuesioner\";s:9:\"user.name\";s:6:\"Alumni\";s:15:\"user.alumni.nim\";s:3:\"NIM\";s:17:\"user.alumni.major\";s:7:\"Jurusan\";s:7:\"answers\";s:17:\"Ringkasan Jawaban\";s:12:\"submitted_at\";s:14:\"Tanggal Submit\";}s:10:\"\0*\0options\";a:0:{}}s:9:\"\0*\0export\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":5:{s:5:\"class\";s:38:\"Filament\\Actions\\Exports\\Models\\Export\";s:2:\"id\";i:2;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";s:15:\"collectionClass\";N;}s:12:\"\0*\0columnMap\";a:6:{s:19:\"questionnaire.title\";s:9:\"Kuesioner\";s:9:\"user.name\";s:6:\"Alumni\";s:15:\"user.alumni.nim\";s:3:\"NIM\";s:17:\"user.alumni.major\";s:7:\"Jurusan\";s:7:\"answers\";s:17:\"Ringkasan Jawaban\";s:12:\"submitted_at\";s:14:\"Tanggal Submit\";}s:10:\"\0*\0formats\";a:2:{i:0;E:47:\"Filament\\Actions\\Exports\\Enums\\ExportFormat:Csv\";i:1;E:48:\"Filament\\Actions\\Exports\\Enums\\ExportFormat:Xlsx\";}s:10:\"\0*\0options\";a:0:{}s:7:\"chained\";a:1:{i:0;s:2742:\"O:44:\"Filament\\Actions\\Exports\\Jobs\\CreateXlsxFile\":4:{s:11:\"\0*\0exporter\";O:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\":3:{s:9:\"\0*\0export\";O:38:\"Filament\\Actions\\Exports\\Models\\Export\":33:{s:13:\"\0*\0connection\";s:5:\"mysql\";s:8:\"\0*\0table\";N;s:13:\"\0*\0primaryKey\";s:2:\"id\";s:10:\"\0*\0keyType\";s:3:\"int\";s:12:\"incrementing\";b:1;s:7:\"\0*\0with\";a:0:{}s:12:\"\0*\0withCount\";a:0:{}s:19:\"preventsLazyLoading\";b:0;s:10:\"\0*\0perPage\";i:15;s:6:\"exists\";b:1;s:18:\"wasRecentlyCreated\";b:1;s:28:\"\0*\0escapeWhenCastingToString\";b:0;s:13:\"\0*\0attributes\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\";s:10:\"total_rows\";i:2;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2025-12-30 04:16:06\";s:10:\"created_at\";s:19:\"2025-12-30 04:16:05\";s:2:\"id\";i:2;s:9:\"file_name\";s:32:\"export-2-questionnaire-responses\";}s:11:\"\0*\0original\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\";s:10:\"total_rows\";i:2;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2025-12-30 04:16:06\";s:10:\"created_at\";s:19:\"2025-12-30 04:16:05\";s:2:\"id\";i:2;s:9:\"file_name\";s:32:\"export-2-questionnaire-responses\";}s:10:\"\0*\0changes\";a:2:{s:10:\"updated_at\";s:19:\"2025-12-30 04:16:06\";s:9:\"file_name\";s:32:\"export-2-questionnaire-responses\";}s:11:\"\0*\0previous\";a:1:{s:10:\"updated_at\";s:19:\"2025-12-30 04:16:05\";}s:8:\"\0*\0casts\";a:4:{s:12:\"completed_at\";s:9:\"timestamp\";s:14:\"processed_rows\";s:7:\"integer\";s:10:\"total_rows\";s:7:\"integer\";s:15:\"successful_rows\";s:7:\"integer\";}s:17:\"\0*\0classCastCache\";a:0:{}s:21:\"\0*\0attributeCastCache\";a:0:{}s:13:\"\0*\0dateFormat\";N;s:10:\"\0*\0appends\";a:0:{}s:19:\"\0*\0dispatchesEvents\";a:0:{}s:14:\"\0*\0observables\";a:0:{}s:12:\"\0*\0relations\";a:0:{}s:10:\"\0*\0touches\";a:0:{}s:27:\"\0*\0relationAutoloadCallback\";N;s:26:\"\0*\0relationAutoloadContext\";N;s:10:\"timestamps\";b:1;s:13:\"usesUniqueIds\";b:0;s:9:\"\0*\0hidden\";a:0:{}s:10:\"\0*\0visible\";a:0:{}s:11:\"\0*\0fillable\";a:0:{}s:10:\"\0*\0guarded\";a:0:{}}s:12:\"\0*\0columnMap\";a:6:{s:19:\"questionnaire.title\";s:9:\"Kuesioner\";s:9:\"user.name\";s:6:\"Alumni\";s:15:\"user.alumni.nim\";s:3:\"NIM\";s:17:\"user.alumni.major\";s:7:\"Jurusan\";s:7:\"answers\";s:17:\"Ringkasan Jawaban\";s:12:\"submitted_at\";s:14:\"Tanggal Submit\";}s:10:\"\0*\0options\";a:0:{}}s:9:\"\0*\0export\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":5:{s:5:\"class\";s:38:\"Filament\\Actions\\Exports\\Models\\Export\";s:2:\"id\";i:2;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";s:15:\"collectionClass\";N;}s:12:\"\0*\0columnMap\";a:6:{s:19:\"questionnaire.title\";s:9:\"Kuesioner\";s:9:\"user.name\";s:6:\"Alumni\";s:15:\"user.alumni.nim\";s:3:\"NIM\";s:17:\"user.alumni.major\";s:7:\"Jurusan\";s:7:\"answers\";s:17:\"Ringkasan Jawaban\";s:12:\"submitted_at\";s:14:\"Tanggal Submit\";}s:10:\"\0*\0options\";a:0:{}}\";}s:19:\"chainCatchCallbacks\";a:0:{}}}s:8:\"function\";s:266:\"function (\\Illuminate\\Bus\\Batch $batch) use ($next) {\n if (! $batch->cancelled()) {\n \\Illuminate\\Container\\Container::getInstance()->make(\\Illuminate\\Contracts\\Bus\\Dispatcher::class)->dispatch($next);\n }\n }\";s:5:\"scope\";s:27:\"Illuminate\\Bus\\ChainedBatch\";s:4:\"this\";N;s:4:\"self\";s:32:\"00000000000008fd0000000000000000\";}\";s:4:\"hash\";s:44:\"LYnbg9f8eVk3HQiI2EPE64SkSGp8Ss/aI1Ho8geoFDI=\";}}}}', NULL, 1767068170, 1767068171), +('a0bee752-6f7c-4ad8-a672-301fa493da46', '', 1, 0, 0, '[]', 'a:2:{s:13:\"allowFailures\";b:1;s:7:\"finally\";a:1:{i:0;O:47:\"Laravel\\SerializableClosure\\SerializableClosure\":1:{s:12:\"serializable\";O:46:\"Laravel\\SerializableClosure\\Serializers\\Signed\":2:{s:12:\"serializable\";s:3952:\"O:46:\"Laravel\\SerializableClosure\\Serializers\\Native\":5:{s:3:\"use\";a:4:{s:9:\"columnMap\";a:17:{s:3:\"nim\";s:3:\"nim\";s:8:\"username\";s:8:\"username\";s:11:\"birth_place\";s:11:\"birth_place\";s:10:\"birth_date\";s:10:\"birth_date\";s:6:\"gender\";s:6:\"gender\";s:7:\"address\";s:7:\"address\";s:12:\"phone_number\";s:12:\"phone_number\";s:5:\"email\";s:5:\"email\";s:3:\"gpa\";s:3:\"gpa\";s:7:\"faculty\";s:7:\"faculty\";s:5:\"major\";s:5:\"major\";s:10:\"entry_year\";s:10:\"entry_year\";s:15:\"graduation_year\";s:15:\"graduation_year\";s:6:\"status\";s:6:\"status\";s:12:\"job_position\";s:12:\"job_position\";s:12:\"company_name\";s:12:\"company_name\";s:4:\"foto\";s:4:\"foto\";}s:6:\"import\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":5:{s:5:\"class\";s:38:\"Filament\\Actions\\Imports\\Models\\Import\";s:2:\"id\";i:2;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";s:15:\"collectionClass\";N;}s:13:\"jobConnection\";N;s:7:\"options\";a:0:{}}s:8:\"function\";s:2925:\"function () use ($columnMap, $import, $jobConnection, $options) {\n $import->touch(\'completed_at\');\n\n event(new \\Filament\\Actions\\Imports\\Events\\ImportCompleted($import, $columnMap, $options));\n\n if (! $import->user instanceof \\Illuminate\\Contracts\\Auth\\Authenticatable) {\n return;\n }\n\n $failedRowsCount = $import->getFailedRowsCount();\n\n \\Filament\\Notifications\\Notification::make()\n ->title($import->importer::getCompletedNotificationTitle($import))\n ->body($import->importer::getCompletedNotificationBody($import))\n ->when(\n ! $failedRowsCount,\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->success(),\n )\n ->when(\n $failedRowsCount && ($failedRowsCount < $import->total_rows),\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->warning(),\n )\n ->when(\n $failedRowsCount === $import->total_rows,\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->danger(),\n )\n ->when(\n $failedRowsCount,\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->actions([\n \\Filament\\Notifications\\Actions\\Action::make(\'downloadFailedRowsCsv\')\n ->label(trans_choice(\'filament-actions::import.notifications.completed.actions.download_failed_rows_csv.label\', $failedRowsCount, [\n \'count\' => \\Illuminate\\Support\\Number::format($failedRowsCount),\n ]))\n ->color(\'danger\')\n ->url(route(\'filament.imports.failed-rows.download\', [\'import\' => $import], absolute: false), shouldOpenInNewTab: true)\n ->markAsRead(),\n ]),\n )\n ->when(\n ($jobConnection === \'sync\') ||\n (blank($jobConnection) && (config(\'queue.default\') === \'sync\')),\n fn (\\Filament\\Notifications\\Notification $notification) => $notification\n ->persistent()\n ->send(),\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->sendToDatabase($import->user, isEventDispatched: true),\n );\n }\";s:5:\"scope\";s:36:\"Filament\\Tables\\Actions\\ImportAction\";s:4:\"this\";N;s:4:\"self\";s:32:\"0000000000000e0e0000000000000000\";}\";s:4:\"hash\";s:44:\"GCZAyQdkaHJYMqjhOd28P1Xgsw5xrUOo6jYQ3ta+7s8=\";}}}}', NULL, 1767417852, 1767417857), +('a0d35e04-f74d-41a6-aef0-e8e37eafc79e', '', 2, 0, 0, '[]', 'a:2:{s:13:\"allowFailures\";b:1;s:7:\"finally\";a:1:{i:0;O:47:\"Laravel\\SerializableClosure\\SerializableClosure\":1:{s:12:\"serializable\";O:46:\"Laravel\\SerializableClosure\\Serializers\\Signed\":2:{s:12:\"serializable\";s:7984:\"O:46:\"Laravel\\SerializableClosure\\Serializers\\Native\":5:{s:3:\"use\";a:1:{s:4:\"next\";O:46:\"Filament\\Actions\\Exports\\Jobs\\ExportCompletion\":7:{s:11:\"\0*\0exporter\";O:35:\"App\\Filament\\Exports\\AlumniExporter\":3:{s:9:\"\0*\0export\";O:38:\"Filament\\Actions\\Exports\\Models\\Export\":33:{s:13:\"\0*\0connection\";s:5:\"mysql\";s:8:\"\0*\0table\";N;s:13:\"\0*\0primaryKey\";s:2:\"id\";s:10:\"\0*\0keyType\";s:3:\"int\";s:12:\"incrementing\";b:1;s:7:\"\0*\0with\";a:0:{}s:12:\"\0*\0withCount\";a:0:{}s:19:\"preventsLazyLoading\";b:0;s:10:\"\0*\0perPage\";i:15;s:6:\"exists\";b:1;s:18:\"wasRecentlyCreated\";b:1;s:28:\"\0*\0escapeWhenCastingToString\";b:0;s:13:\"\0*\0attributes\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:35:\"App\\Filament\\Exports\\AlumniExporter\";s:10:\"total_rows\";i:13;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2026-01-13 09:32:35\";s:10:\"created_at\";s:19:\"2026-01-13 09:32:33\";s:2:\"id\";i:3;s:9:\"file_name\";s:16:\"export-3-alumnis\";}s:11:\"\0*\0original\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:35:\"App\\Filament\\Exports\\AlumniExporter\";s:10:\"total_rows\";i:13;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2026-01-13 09:32:35\";s:10:\"created_at\";s:19:\"2026-01-13 09:32:33\";s:2:\"id\";i:3;s:9:\"file_name\";s:16:\"export-3-alumnis\";}s:10:\"\0*\0changes\";a:2:{s:10:\"updated_at\";s:19:\"2026-01-13 09:32:35\";s:9:\"file_name\";s:16:\"export-3-alumnis\";}s:11:\"\0*\0previous\";a:1:{s:10:\"updated_at\";s:19:\"2026-01-13 09:32:33\";}s:8:\"\0*\0casts\";a:4:{s:12:\"completed_at\";s:9:\"timestamp\";s:14:\"processed_rows\";s:7:\"integer\";s:10:\"total_rows\";s:7:\"integer\";s:15:\"successful_rows\";s:7:\"integer\";}s:17:\"\0*\0classCastCache\";a:0:{}s:21:\"\0*\0attributeCastCache\";a:0:{}s:13:\"\0*\0dateFormat\";N;s:10:\"\0*\0appends\";a:0:{}s:19:\"\0*\0dispatchesEvents\";a:0:{}s:14:\"\0*\0observables\";a:0:{}s:12:\"\0*\0relations\";a:0:{}s:10:\"\0*\0touches\";a:0:{}s:27:\"\0*\0relationAutoloadCallback\";N;s:26:\"\0*\0relationAutoloadContext\";N;s:10:\"timestamps\";b:1;s:13:\"usesUniqueIds\";b:0;s:9:\"\0*\0hidden\";a:0:{}s:10:\"\0*\0visible\";a:0:{}s:11:\"\0*\0fillable\";a:0:{}s:10:\"\0*\0guarded\";a:0:{}}s:12:\"\0*\0columnMap\";a:21:{s:2:\"id\";s:2:\"ID\";s:3:\"nim\";s:3:\"NIM\";s:8:\"username\";s:11:\"Nama Alumni\";s:11:\"birth_place\";s:12:\"Tempat Lahir\";s:10:\"birth_date\";s:13:\"Tanggal Lahir\";s:6:\"gender\";s:13:\"Jenis Kelamin\";s:7:\"address\";s:6:\"Alamat\";s:12:\"phone_number\";s:5:\"No HP\";s:5:\"email\";s:5:\"Email\";s:3:\"gpa\";s:3:\"IPK\";s:7:\"faculty\";s:8:\"Fakultas\";s:5:\"major\";s:13:\"Program Studi\";s:10:\"entry_year\";s:11:\"Tahun Masuk\";s:15:\"graduation_year\";s:11:\"Tahun Lulus\";s:6:\"status\";s:6:\"Status\";s:14:\"category_field\";s:15:\"Kategori Bidang\";s:14:\"waiting_period\";s:11:\"Masa Tunggu\";s:12:\"job_position\";s:16:\"Posisi Pekerjaan\";s:12:\"company_name\";s:24:\"Nama Instansi/Perusahaan\";s:10:\"created_at\";s:14:\"Tanggal dibuat\";s:10:\"updated_at\";s:18:\"Tanggal diperbarui\";}s:10:\"\0*\0options\";a:0:{}}s:9:\"\0*\0export\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":5:{s:5:\"class\";s:38:\"Filament\\Actions\\Exports\\Models\\Export\";s:2:\"id\";i:3;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";s:15:\"collectionClass\";N;}s:12:\"\0*\0columnMap\";a:21:{s:2:\"id\";s:2:\"ID\";s:3:\"nim\";s:3:\"NIM\";s:8:\"username\";s:11:\"Nama Alumni\";s:11:\"birth_place\";s:12:\"Tempat Lahir\";s:10:\"birth_date\";s:13:\"Tanggal Lahir\";s:6:\"gender\";s:13:\"Jenis Kelamin\";s:7:\"address\";s:6:\"Alamat\";s:12:\"phone_number\";s:5:\"No HP\";s:5:\"email\";s:5:\"Email\";s:3:\"gpa\";s:3:\"IPK\";s:7:\"faculty\";s:8:\"Fakultas\";s:5:\"major\";s:13:\"Program Studi\";s:10:\"entry_year\";s:11:\"Tahun Masuk\";s:15:\"graduation_year\";s:11:\"Tahun Lulus\";s:6:\"status\";s:6:\"Status\";s:14:\"category_field\";s:15:\"Kategori Bidang\";s:14:\"waiting_period\";s:11:\"Masa Tunggu\";s:12:\"job_position\";s:16:\"Posisi Pekerjaan\";s:12:\"company_name\";s:24:\"Nama Instansi/Perusahaan\";s:10:\"created_at\";s:14:\"Tanggal dibuat\";s:10:\"updated_at\";s:18:\"Tanggal diperbarui\";}s:10:\"\0*\0formats\";a:2:{i:0;E:47:\"Filament\\Actions\\Exports\\Enums\\ExportFormat:Csv\";i:1;E:48:\"Filament\\Actions\\Exports\\Enums\\ExportFormat:Xlsx\";}s:10:\"\0*\0options\";a:0:{}s:7:\"chained\";a:1:{i:0;s:3643:\"O:44:\"Filament\\Actions\\Exports\\Jobs\\CreateXlsxFile\":4:{s:11:\"\0*\0exporter\";O:35:\"App\\Filament\\Exports\\AlumniExporter\":3:{s:9:\"\0*\0export\";O:38:\"Filament\\Actions\\Exports\\Models\\Export\":33:{s:13:\"\0*\0connection\";s:5:\"mysql\";s:8:\"\0*\0table\";N;s:13:\"\0*\0primaryKey\";s:2:\"id\";s:10:\"\0*\0keyType\";s:3:\"int\";s:12:\"incrementing\";b:1;s:7:\"\0*\0with\";a:0:{}s:12:\"\0*\0withCount\";a:0:{}s:19:\"preventsLazyLoading\";b:0;s:10:\"\0*\0perPage\";i:15;s:6:\"exists\";b:1;s:18:\"wasRecentlyCreated\";b:1;s:28:\"\0*\0escapeWhenCastingToString\";b:0;s:13:\"\0*\0attributes\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:35:\"App\\Filament\\Exports\\AlumniExporter\";s:10:\"total_rows\";i:13;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2026-01-13 09:32:35\";s:10:\"created_at\";s:19:\"2026-01-13 09:32:33\";s:2:\"id\";i:3;s:9:\"file_name\";s:16:\"export-3-alumnis\";}s:11:\"\0*\0original\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:35:\"App\\Filament\\Exports\\AlumniExporter\";s:10:\"total_rows\";i:13;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2026-01-13 09:32:35\";s:10:\"created_at\";s:19:\"2026-01-13 09:32:33\";s:2:\"id\";i:3;s:9:\"file_name\";s:16:\"export-3-alumnis\";}s:10:\"\0*\0changes\";a:2:{s:10:\"updated_at\";s:19:\"2026-01-13 09:32:35\";s:9:\"file_name\";s:16:\"export-3-alumnis\";}s:11:\"\0*\0previous\";a:1:{s:10:\"updated_at\";s:19:\"2026-01-13 09:32:33\";}s:8:\"\0*\0casts\";a:4:{s:12:\"completed_at\";s:9:\"timestamp\";s:14:\"processed_rows\";s:7:\"integer\";s:10:\"total_rows\";s:7:\"integer\";s:15:\"successful_rows\";s:7:\"integer\";}s:17:\"\0*\0classCastCache\";a:0:{}s:21:\"\0*\0attributeCastCache\";a:0:{}s:13:\"\0*\0dateFormat\";N;s:10:\"\0*\0appends\";a:0:{}s:19:\"\0*\0dispatchesEvents\";a:0:{}s:14:\"\0*\0observables\";a:0:{}s:12:\"\0*\0relations\";a:0:{}s:10:\"\0*\0touches\";a:0:{}s:27:\"\0*\0relationAutoloadCallback\";N;s:26:\"\0*\0relationAutoloadContext\";N;s:10:\"timestamps\";b:1;s:13:\"usesUniqueIds\";b:0;s:9:\"\0*\0hidden\";a:0:{}s:10:\"\0*\0visible\";a:0:{}s:11:\"\0*\0fillable\";a:0:{}s:10:\"\0*\0guarded\";a:0:{}}s:12:\"\0*\0columnMap\";a:21:{s:2:\"id\";s:2:\"ID\";s:3:\"nim\";s:3:\"NIM\";s:8:\"username\";s:11:\"Nama Alumni\";s:11:\"birth_place\";s:12:\"Tempat Lahir\";s:10:\"birth_date\";s:13:\"Tanggal Lahir\";s:6:\"gender\";s:13:\"Jenis Kelamin\";s:7:\"address\";s:6:\"Alamat\";s:12:\"phone_number\";s:5:\"No HP\";s:5:\"email\";s:5:\"Email\";s:3:\"gpa\";s:3:\"IPK\";s:7:\"faculty\";s:8:\"Fakultas\";s:5:\"major\";s:13:\"Program Studi\";s:10:\"entry_year\";s:11:\"Tahun Masuk\";s:15:\"graduation_year\";s:11:\"Tahun Lulus\";s:6:\"status\";s:6:\"Status\";s:14:\"category_field\";s:15:\"Kategori Bidang\";s:14:\"waiting_period\";s:11:\"Masa Tunggu\";s:12:\"job_position\";s:16:\"Posisi Pekerjaan\";s:12:\"company_name\";s:24:\"Nama Instansi/Perusahaan\";s:10:\"created_at\";s:14:\"Tanggal dibuat\";s:10:\"updated_at\";s:18:\"Tanggal diperbarui\";}s:10:\"\0*\0options\";a:0:{}}s:9:\"\0*\0export\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":5:{s:5:\"class\";s:38:\"Filament\\Actions\\Exports\\Models\\Export\";s:2:\"id\";i:3;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";s:15:\"collectionClass\";N;}s:12:\"\0*\0columnMap\";a:21:{s:2:\"id\";s:2:\"ID\";s:3:\"nim\";s:3:\"NIM\";s:8:\"username\";s:11:\"Nama Alumni\";s:11:\"birth_place\";s:12:\"Tempat Lahir\";s:10:\"birth_date\";s:13:\"Tanggal Lahir\";s:6:\"gender\";s:13:\"Jenis Kelamin\";s:7:\"address\";s:6:\"Alamat\";s:12:\"phone_number\";s:5:\"No HP\";s:5:\"email\";s:5:\"Email\";s:3:\"gpa\";s:3:\"IPK\";s:7:\"faculty\";s:8:\"Fakultas\";s:5:\"major\";s:13:\"Program Studi\";s:10:\"entry_year\";s:11:\"Tahun Masuk\";s:15:\"graduation_year\";s:11:\"Tahun Lulus\";s:6:\"status\";s:6:\"Status\";s:14:\"category_field\";s:15:\"Kategori Bidang\";s:14:\"waiting_period\";s:11:\"Masa Tunggu\";s:12:\"job_position\";s:16:\"Posisi Pekerjaan\";s:12:\"company_name\";s:24:\"Nama Instansi/Perusahaan\";s:10:\"created_at\";s:14:\"Tanggal dibuat\";s:10:\"updated_at\";s:18:\"Tanggal diperbarui\";}s:10:\"\0*\0options\";a:0:{}}\";}s:19:\"chainCatchCallbacks\";a:0:{}}}s:8:\"function\";s:266:\"function (\\Illuminate\\Bus\\Batch $batch) use ($next) {\n if (! $batch->cancelled()) {\n \\Illuminate\\Container\\Container::getInstance()->make(\\Illuminate\\Contracts\\Bus\\Dispatcher::class)->dispatch($next);\n }\n }\";s:5:\"scope\";s:27:\"Illuminate\\Bus\\ChainedBatch\";s:4:\"this\";N;s:4:\"self\";s:32:\"00000000000008fd0000000000000000\";}\";s:4:\"hash\";s:44:\"eMaUhkuyXG6iehMNkJX6EIaLtEgZoE5Cwa2qF4TWSfM=\";}}}}', NULL, 1768296759, 1768296760), +('a0d36a1f-1bef-439d-b045-3647f3b37b25', '', 1, 0, 0, '[]', 'a:2:{s:13:\"allowFailures\";b:1;s:7:\"finally\";a:1:{i:0;O:47:\"Laravel\\SerializableClosure\\SerializableClosure\":1:{s:12:\"serializable\";O:46:\"Laravel\\SerializableClosure\\Serializers\\Signed\":2:{s:12:\"serializable\";s:4040:\"O:46:\"Laravel\\SerializableClosure\\Serializers\\Native\":5:{s:3:\"use\";a:4:{s:9:\"columnMap\";a:19:{s:3:\"nim\";s:3:\"nim\";s:8:\"username\";s:8:\"username\";s:11:\"birth_place\";s:11:\"birth_place\";s:10:\"birth_date\";s:10:\"birth_date\";s:6:\"gender\";s:6:\"gender\";s:7:\"address\";s:7:\"address\";s:12:\"phone_number\";s:12:\"phone_number\";s:5:\"email\";s:5:\"email\";s:3:\"gpa\";s:3:\"gpa\";s:7:\"faculty\";s:7:\"faculty\";s:5:\"major\";s:5:\"major\";s:10:\"entry_year\";s:10:\"entry_year\";s:15:\"graduation_year\";s:15:\"graduation_year\";s:6:\"status\";s:6:\"status\";s:14:\"category_field\";s:14:\"category_field\";s:14:\"waiting_period\";s:14:\"waiting_period\";s:12:\"job_position\";s:12:\"job_position\";s:12:\"company_name\";s:12:\"company_name\";s:4:\"foto\";s:4:\"foto\";}s:6:\"import\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":5:{s:5:\"class\";s:38:\"Filament\\Actions\\Imports\\Models\\Import\";s:2:\"id\";i:5;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";s:15:\"collectionClass\";N;}s:13:\"jobConnection\";N;s:7:\"options\";a:0:{}}s:8:\"function\";s:2925:\"function () use ($columnMap, $import, $jobConnection, $options) {\n $import->touch(\'completed_at\');\n\n event(new \\Filament\\Actions\\Imports\\Events\\ImportCompleted($import, $columnMap, $options));\n\n if (! $import->user instanceof \\Illuminate\\Contracts\\Auth\\Authenticatable) {\n return;\n }\n\n $failedRowsCount = $import->getFailedRowsCount();\n\n \\Filament\\Notifications\\Notification::make()\n ->title($import->importer::getCompletedNotificationTitle($import))\n ->body($import->importer::getCompletedNotificationBody($import))\n ->when(\n ! $failedRowsCount,\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->success(),\n )\n ->when(\n $failedRowsCount && ($failedRowsCount < $import->total_rows),\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->warning(),\n )\n ->when(\n $failedRowsCount === $import->total_rows,\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->danger(),\n )\n ->when(\n $failedRowsCount,\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->actions([\n \\Filament\\Notifications\\Actions\\Action::make(\'downloadFailedRowsCsv\')\n ->label(trans_choice(\'filament-actions::import.notifications.completed.actions.download_failed_rows_csv.label\', $failedRowsCount, [\n \'count\' => \\Illuminate\\Support\\Number::format($failedRowsCount),\n ]))\n ->color(\'danger\')\n ->url(route(\'filament.imports.failed-rows.download\', [\'import\' => $import], absolute: false), shouldOpenInNewTab: true)\n ->markAsRead(),\n ]),\n )\n ->when(\n ($jobConnection === \'sync\') ||\n (blank($jobConnection) && (config(\'queue.default\') === \'sync\')),\n fn (\\Filament\\Notifications\\Notification $notification) => $notification\n ->persistent()\n ->send(),\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->sendToDatabase($import->user, isEventDispatched: true),\n );\n }\";s:5:\"scope\";s:36:\"Filament\\Tables\\Actions\\ImportAction\";s:4:\"this\";N;s:4:\"self\";s:32:\"0000000000000ea30000000000000000\";}\";s:4:\"hash\";s:44:\"GW5zTMvFxTdGGeVixyQBmL8UQ2Z5B8pP4RkvrmLwlwg=\";}}}}', NULL, 1768298789, 1768298794); +INSERT INTO `job_batches` (`id`, `name`, `total_jobs`, `pending_jobs`, `failed_jobs`, `failed_job_ids`, `options`, `cancelled_at`, `created_at`, `finished_at`) VALUES +('a0ef7ec8-8c61-4a5d-b019-79294f90db93', '', 2, 0, 0, '[]', 'a:2:{s:13:\"allowFailures\";b:1;s:7:\"finally\";a:1:{i:0;O:47:\"Laravel\\SerializableClosure\\SerializableClosure\":1:{s:12:\"serializable\";O:46:\"Laravel\\SerializableClosure\\Serializers\\Signed\":2:{s:12:\"serializable\";s:6006:\"O:46:\"Laravel\\SerializableClosure\\Serializers\\Native\":5:{s:3:\"use\";a:1:{s:4:\"next\";O:46:\"Filament\\Actions\\Exports\\Jobs\\ExportCompletion\":7:{s:11:\"\0*\0exporter\";O:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\":3:{s:9:\"\0*\0export\";O:38:\"Filament\\Actions\\Exports\\Models\\Export\":33:{s:13:\"\0*\0connection\";s:5:\"mysql\";s:8:\"\0*\0table\";N;s:13:\"\0*\0primaryKey\";s:2:\"id\";s:10:\"\0*\0keyType\";s:3:\"int\";s:12:\"incrementing\";b:1;s:7:\"\0*\0with\";a:0:{}s:12:\"\0*\0withCount\";a:0:{}s:19:\"preventsLazyLoading\";b:0;s:10:\"\0*\0perPage\";i:15;s:6:\"exists\";b:1;s:18:\"wasRecentlyCreated\";b:1;s:28:\"\0*\0escapeWhenCastingToString\";b:0;s:13:\"\0*\0attributes\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\";s:10:\"total_rows\";i:10;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2026-01-27 09:07:22\";s:10:\"created_at\";s:19:\"2026-01-27 09:07:22\";s:2:\"id\";i:4;s:9:\"file_name\";s:32:\"export-4-questionnaire-responses\";}s:11:\"\0*\0original\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\";s:10:\"total_rows\";i:10;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2026-01-27 09:07:22\";s:10:\"created_at\";s:19:\"2026-01-27 09:07:22\";s:2:\"id\";i:4;s:9:\"file_name\";s:32:\"export-4-questionnaire-responses\";}s:10:\"\0*\0changes\";a:1:{s:9:\"file_name\";s:32:\"export-4-questionnaire-responses\";}s:11:\"\0*\0previous\";a:0:{}s:8:\"\0*\0casts\";a:4:{s:12:\"completed_at\";s:9:\"timestamp\";s:14:\"processed_rows\";s:7:\"integer\";s:10:\"total_rows\";s:7:\"integer\";s:15:\"successful_rows\";s:7:\"integer\";}s:17:\"\0*\0classCastCache\";a:0:{}s:21:\"\0*\0attributeCastCache\";a:0:{}s:13:\"\0*\0dateFormat\";N;s:10:\"\0*\0appends\";a:0:{}s:19:\"\0*\0dispatchesEvents\";a:0:{}s:14:\"\0*\0observables\";a:0:{}s:12:\"\0*\0relations\";a:0:{}s:10:\"\0*\0touches\";a:0:{}s:27:\"\0*\0relationAutoloadCallback\";N;s:26:\"\0*\0relationAutoloadContext\";N;s:10:\"timestamps\";b:1;s:13:\"usesUniqueIds\";b:0;s:9:\"\0*\0hidden\";a:0:{}s:10:\"\0*\0visible\";a:0:{}s:11:\"\0*\0fillable\";a:0:{}s:10:\"\0*\0guarded\";a:0:{}}s:12:\"\0*\0columnMap\";a:6:{s:19:\"questionnaire.title\";s:9:\"Kuesioner\";s:9:\"user.name\";s:6:\"Alumni\";s:15:\"user.alumni.nim\";s:3:\"NIM\";s:17:\"user.alumni.major\";s:7:\"Jurusan\";s:7:\"answers\";s:17:\"Ringkasan Jawaban\";s:12:\"submitted_at\";s:14:\"Tanggal Submit\";}s:10:\"\0*\0options\";a:0:{}}s:9:\"\0*\0export\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":5:{s:5:\"class\";s:38:\"Filament\\Actions\\Exports\\Models\\Export\";s:2:\"id\";i:4;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";s:15:\"collectionClass\";N;}s:12:\"\0*\0columnMap\";a:6:{s:19:\"questionnaire.title\";s:9:\"Kuesioner\";s:9:\"user.name\";s:6:\"Alumni\";s:15:\"user.alumni.nim\";s:3:\"NIM\";s:17:\"user.alumni.major\";s:7:\"Jurusan\";s:7:\"answers\";s:17:\"Ringkasan Jawaban\";s:12:\"submitted_at\";s:14:\"Tanggal Submit\";}s:10:\"\0*\0formats\";a:2:{i:0;E:47:\"Filament\\Actions\\Exports\\Enums\\ExportFormat:Csv\";i:1;E:48:\"Filament\\Actions\\Exports\\Enums\\ExportFormat:Xlsx\";}s:10:\"\0*\0options\";a:0:{}s:7:\"chained\";a:1:{i:0;s:2654:\"O:44:\"Filament\\Actions\\Exports\\Jobs\\CreateXlsxFile\":4:{s:11:\"\0*\0exporter\";O:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\":3:{s:9:\"\0*\0export\";O:38:\"Filament\\Actions\\Exports\\Models\\Export\":33:{s:13:\"\0*\0connection\";s:5:\"mysql\";s:8:\"\0*\0table\";N;s:13:\"\0*\0primaryKey\";s:2:\"id\";s:10:\"\0*\0keyType\";s:3:\"int\";s:12:\"incrementing\";b:1;s:7:\"\0*\0with\";a:0:{}s:12:\"\0*\0withCount\";a:0:{}s:19:\"preventsLazyLoading\";b:0;s:10:\"\0*\0perPage\";i:15;s:6:\"exists\";b:1;s:18:\"wasRecentlyCreated\";b:1;s:28:\"\0*\0escapeWhenCastingToString\";b:0;s:13:\"\0*\0attributes\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\";s:10:\"total_rows\";i:10;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2026-01-27 09:07:22\";s:10:\"created_at\";s:19:\"2026-01-27 09:07:22\";s:2:\"id\";i:4;s:9:\"file_name\";s:32:\"export-4-questionnaire-responses\";}s:11:\"\0*\0original\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\";s:10:\"total_rows\";i:10;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2026-01-27 09:07:22\";s:10:\"created_at\";s:19:\"2026-01-27 09:07:22\";s:2:\"id\";i:4;s:9:\"file_name\";s:32:\"export-4-questionnaire-responses\";}s:10:\"\0*\0changes\";a:1:{s:9:\"file_name\";s:32:\"export-4-questionnaire-responses\";}s:11:\"\0*\0previous\";a:0:{}s:8:\"\0*\0casts\";a:4:{s:12:\"completed_at\";s:9:\"timestamp\";s:14:\"processed_rows\";s:7:\"integer\";s:10:\"total_rows\";s:7:\"integer\";s:15:\"successful_rows\";s:7:\"integer\";}s:17:\"\0*\0classCastCache\";a:0:{}s:21:\"\0*\0attributeCastCache\";a:0:{}s:13:\"\0*\0dateFormat\";N;s:10:\"\0*\0appends\";a:0:{}s:19:\"\0*\0dispatchesEvents\";a:0:{}s:14:\"\0*\0observables\";a:0:{}s:12:\"\0*\0relations\";a:0:{}s:10:\"\0*\0touches\";a:0:{}s:27:\"\0*\0relationAutoloadCallback\";N;s:26:\"\0*\0relationAutoloadContext\";N;s:10:\"timestamps\";b:1;s:13:\"usesUniqueIds\";b:0;s:9:\"\0*\0hidden\";a:0:{}s:10:\"\0*\0visible\";a:0:{}s:11:\"\0*\0fillable\";a:0:{}s:10:\"\0*\0guarded\";a:0:{}}s:12:\"\0*\0columnMap\";a:6:{s:19:\"questionnaire.title\";s:9:\"Kuesioner\";s:9:\"user.name\";s:6:\"Alumni\";s:15:\"user.alumni.nim\";s:3:\"NIM\";s:17:\"user.alumni.major\";s:7:\"Jurusan\";s:7:\"answers\";s:17:\"Ringkasan Jawaban\";s:12:\"submitted_at\";s:14:\"Tanggal Submit\";}s:10:\"\0*\0options\";a:0:{}}s:9:\"\0*\0export\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":5:{s:5:\"class\";s:38:\"Filament\\Actions\\Exports\\Models\\Export\";s:2:\"id\";i:4;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";s:15:\"collectionClass\";N;}s:12:\"\0*\0columnMap\";a:6:{s:19:\"questionnaire.title\";s:9:\"Kuesioner\";s:9:\"user.name\";s:6:\"Alumni\";s:15:\"user.alumni.nim\";s:3:\"NIM\";s:17:\"user.alumni.major\";s:7:\"Jurusan\";s:7:\"answers\";s:17:\"Ringkasan Jawaban\";s:12:\"submitted_at\";s:14:\"Tanggal Submit\";}s:10:\"\0*\0options\";a:0:{}}\";}s:19:\"chainCatchCallbacks\";a:0:{}}}s:8:\"function\";s:266:\"function (\\Illuminate\\Bus\\Batch $batch) use ($next) {\n if (! $batch->cancelled()) {\n \\Illuminate\\Container\\Container::getInstance()->make(\\Illuminate\\Contracts\\Bus\\Dispatcher::class)->dispatch($next);\n }\n }\";s:5:\"scope\";s:27:\"Illuminate\\Bus\\ChainedBatch\";s:4:\"this\";N;s:4:\"self\";s:32:\"00000000000009260000000000000000\";}\";s:4:\"hash\";s:44:\"71kZRCoXQupQTIZKNNfO0c89dX8YTVq8Sxk7a46ap2A=\";}}}}', NULL, 1769504847, 1769504847), +('a0f39b8e-ec88-46f2-84f1-7fee5144d044', '', 2, 0, 0, '[]', 'a:2:{s:13:\"allowFailures\";b:1;s:7:\"finally\";a:1:{i:0;O:47:\"Laravel\\SerializableClosure\\SerializableClosure\":1:{s:12:\"serializable\";O:46:\"Laravel\\SerializableClosure\\Serializers\\Signed\":2:{s:12:\"serializable\";s:6182:\"O:46:\"Laravel\\SerializableClosure\\Serializers\\Native\":5:{s:3:\"use\";a:1:{s:4:\"next\";O:46:\"Filament\\Actions\\Exports\\Jobs\\ExportCompletion\":7:{s:11:\"\0*\0exporter\";O:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\":3:{s:9:\"\0*\0export\";O:38:\"Filament\\Actions\\Exports\\Models\\Export\":33:{s:13:\"\0*\0connection\";s:5:\"mysql\";s:8:\"\0*\0table\";N;s:13:\"\0*\0primaryKey\";s:2:\"id\";s:10:\"\0*\0keyType\";s:3:\"int\";s:12:\"incrementing\";b:1;s:7:\"\0*\0with\";a:0:{}s:12:\"\0*\0withCount\";a:0:{}s:19:\"preventsLazyLoading\";b:0;s:10:\"\0*\0perPage\";i:15;s:6:\"exists\";b:1;s:18:\"wasRecentlyCreated\";b:1;s:28:\"\0*\0escapeWhenCastingToString\";b:0;s:13:\"\0*\0attributes\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\";s:10:\"total_rows\";i:7;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2026-01-29 10:11:06\";s:10:\"created_at\";s:19:\"2026-01-29 10:11:05\";s:2:\"id\";i:5;s:9:\"file_name\";s:32:\"export-5-questionnaire-responses\";}s:11:\"\0*\0original\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\";s:10:\"total_rows\";i:7;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2026-01-29 10:11:06\";s:10:\"created_at\";s:19:\"2026-01-29 10:11:05\";s:2:\"id\";i:5;s:9:\"file_name\";s:32:\"export-5-questionnaire-responses\";}s:10:\"\0*\0changes\";a:2:{s:10:\"updated_at\";s:19:\"2026-01-29 10:11:06\";s:9:\"file_name\";s:32:\"export-5-questionnaire-responses\";}s:11:\"\0*\0previous\";a:1:{s:10:\"updated_at\";s:19:\"2026-01-29 10:11:05\";}s:8:\"\0*\0casts\";a:4:{s:12:\"completed_at\";s:9:\"timestamp\";s:14:\"processed_rows\";s:7:\"integer\";s:10:\"total_rows\";s:7:\"integer\";s:15:\"successful_rows\";s:7:\"integer\";}s:17:\"\0*\0classCastCache\";a:0:{}s:21:\"\0*\0attributeCastCache\";a:0:{}s:13:\"\0*\0dateFormat\";N;s:10:\"\0*\0appends\";a:0:{}s:19:\"\0*\0dispatchesEvents\";a:0:{}s:14:\"\0*\0observables\";a:0:{}s:12:\"\0*\0relations\";a:0:{}s:10:\"\0*\0touches\";a:0:{}s:27:\"\0*\0relationAutoloadCallback\";N;s:26:\"\0*\0relationAutoloadContext\";N;s:10:\"timestamps\";b:1;s:13:\"usesUniqueIds\";b:0;s:9:\"\0*\0hidden\";a:0:{}s:10:\"\0*\0visible\";a:0:{}s:11:\"\0*\0fillable\";a:0:{}s:10:\"\0*\0guarded\";a:0:{}}s:12:\"\0*\0columnMap\";a:6:{s:19:\"questionnaire.title\";s:9:\"Kuesioner\";s:9:\"user.name\";s:6:\"Alumni\";s:15:\"user.alumni.nim\";s:3:\"NIM\";s:17:\"user.alumni.major\";s:7:\"Jurusan\";s:7:\"answers\";s:17:\"Ringkasan Jawaban\";s:12:\"submitted_at\";s:14:\"Tanggal Submit\";}s:10:\"\0*\0options\";a:0:{}}s:9:\"\0*\0export\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":5:{s:5:\"class\";s:38:\"Filament\\Actions\\Exports\\Models\\Export\";s:2:\"id\";i:5;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";s:15:\"collectionClass\";N;}s:12:\"\0*\0columnMap\";a:6:{s:19:\"questionnaire.title\";s:9:\"Kuesioner\";s:9:\"user.name\";s:6:\"Alumni\";s:15:\"user.alumni.nim\";s:3:\"NIM\";s:17:\"user.alumni.major\";s:7:\"Jurusan\";s:7:\"answers\";s:17:\"Ringkasan Jawaban\";s:12:\"submitted_at\";s:14:\"Tanggal Submit\";}s:10:\"\0*\0formats\";a:2:{i:0;E:47:\"Filament\\Actions\\Exports\\Enums\\ExportFormat:Csv\";i:1;E:48:\"Filament\\Actions\\Exports\\Enums\\ExportFormat:Xlsx\";}s:10:\"\0*\0options\";a:0:{}s:7:\"chained\";a:1:{i:0;s:2742:\"O:44:\"Filament\\Actions\\Exports\\Jobs\\CreateXlsxFile\":4:{s:11:\"\0*\0exporter\";O:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\":3:{s:9:\"\0*\0export\";O:38:\"Filament\\Actions\\Exports\\Models\\Export\":33:{s:13:\"\0*\0connection\";s:5:\"mysql\";s:8:\"\0*\0table\";N;s:13:\"\0*\0primaryKey\";s:2:\"id\";s:10:\"\0*\0keyType\";s:3:\"int\";s:12:\"incrementing\";b:1;s:7:\"\0*\0with\";a:0:{}s:12:\"\0*\0withCount\";a:0:{}s:19:\"preventsLazyLoading\";b:0;s:10:\"\0*\0perPage\";i:15;s:6:\"exists\";b:1;s:18:\"wasRecentlyCreated\";b:1;s:28:\"\0*\0escapeWhenCastingToString\";b:0;s:13:\"\0*\0attributes\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\";s:10:\"total_rows\";i:7;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2026-01-29 10:11:06\";s:10:\"created_at\";s:19:\"2026-01-29 10:11:05\";s:2:\"id\";i:5;s:9:\"file_name\";s:32:\"export-5-questionnaire-responses\";}s:11:\"\0*\0original\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\";s:10:\"total_rows\";i:7;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2026-01-29 10:11:06\";s:10:\"created_at\";s:19:\"2026-01-29 10:11:05\";s:2:\"id\";i:5;s:9:\"file_name\";s:32:\"export-5-questionnaire-responses\";}s:10:\"\0*\0changes\";a:2:{s:10:\"updated_at\";s:19:\"2026-01-29 10:11:06\";s:9:\"file_name\";s:32:\"export-5-questionnaire-responses\";}s:11:\"\0*\0previous\";a:1:{s:10:\"updated_at\";s:19:\"2026-01-29 10:11:05\";}s:8:\"\0*\0casts\";a:4:{s:12:\"completed_at\";s:9:\"timestamp\";s:14:\"processed_rows\";s:7:\"integer\";s:10:\"total_rows\";s:7:\"integer\";s:15:\"successful_rows\";s:7:\"integer\";}s:17:\"\0*\0classCastCache\";a:0:{}s:21:\"\0*\0attributeCastCache\";a:0:{}s:13:\"\0*\0dateFormat\";N;s:10:\"\0*\0appends\";a:0:{}s:19:\"\0*\0dispatchesEvents\";a:0:{}s:14:\"\0*\0observables\";a:0:{}s:12:\"\0*\0relations\";a:0:{}s:10:\"\0*\0touches\";a:0:{}s:27:\"\0*\0relationAutoloadCallback\";N;s:26:\"\0*\0relationAutoloadContext\";N;s:10:\"timestamps\";b:1;s:13:\"usesUniqueIds\";b:0;s:9:\"\0*\0hidden\";a:0:{}s:10:\"\0*\0visible\";a:0:{}s:11:\"\0*\0fillable\";a:0:{}s:10:\"\0*\0guarded\";a:0:{}}s:12:\"\0*\0columnMap\";a:6:{s:19:\"questionnaire.title\";s:9:\"Kuesioner\";s:9:\"user.name\";s:6:\"Alumni\";s:15:\"user.alumni.nim\";s:3:\"NIM\";s:17:\"user.alumni.major\";s:7:\"Jurusan\";s:7:\"answers\";s:17:\"Ringkasan Jawaban\";s:12:\"submitted_at\";s:14:\"Tanggal Submit\";}s:10:\"\0*\0options\";a:0:{}}s:9:\"\0*\0export\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":5:{s:5:\"class\";s:38:\"Filament\\Actions\\Exports\\Models\\Export\";s:2:\"id\";i:5;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";s:15:\"collectionClass\";N;}s:12:\"\0*\0columnMap\";a:6:{s:19:\"questionnaire.title\";s:9:\"Kuesioner\";s:9:\"user.name\";s:6:\"Alumni\";s:15:\"user.alumni.nim\";s:3:\"NIM\";s:17:\"user.alumni.major\";s:7:\"Jurusan\";s:7:\"answers\";s:17:\"Ringkasan Jawaban\";s:12:\"submitted_at\";s:14:\"Tanggal Submit\";}s:10:\"\0*\0options\";a:0:{}}\";}s:19:\"chainCatchCallbacks\";a:0:{}}}s:8:\"function\";s:266:\"function (\\Illuminate\\Bus\\Batch $batch) use ($next) {\n if (! $batch->cancelled()) {\n \\Illuminate\\Container\\Container::getInstance()->make(\\Illuminate\\Contracts\\Bus\\Dispatcher::class)->dispatch($next);\n }\n }\";s:5:\"scope\";s:27:\"Illuminate\\Bus\\ChainedBatch\";s:4:\"this\";N;s:4:\"self\";s:32:\"00000000000009260000000000000000\";}\";s:4:\"hash\";s:44:\"I6qGVrctjDxayuzSurnIBrRG8ApwB6fGyFaEQnmp9Fg=\";}}}}', NULL, 1769681473, 1769681474), +('a0f9d38d-45c0-47a3-b613-419b978c68fe', '', 2, 0, 0, '[]', 'a:2:{s:13:\"allowFailures\";b:1;s:7:\"finally\";a:1:{i:0;O:47:\"Laravel\\SerializableClosure\\SerializableClosure\":1:{s:12:\"serializable\";O:46:\"Laravel\\SerializableClosure\\Serializers\\Signed\":2:{s:12:\"serializable\";s:6646:\"O:46:\"Laravel\\SerializableClosure\\Serializers\\Native\":5:{s:3:\"use\";a:1:{s:4:\"next\";O:46:\"Filament\\Actions\\Exports\\Jobs\\ExportCompletion\":7:{s:11:\"\0*\0exporter\";O:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\":3:{s:9:\"\0*\0export\";O:38:\"Filament\\Actions\\Exports\\Models\\Export\":33:{s:13:\"\0*\0connection\";s:5:\"mysql\";s:8:\"\0*\0table\";N;s:13:\"\0*\0primaryKey\";s:2:\"id\";s:10:\"\0*\0keyType\";s:3:\"int\";s:12:\"incrementing\";b:1;s:7:\"\0*\0with\";a:0:{}s:12:\"\0*\0withCount\";a:0:{}s:19:\"preventsLazyLoading\";b:0;s:10:\"\0*\0perPage\";i:15;s:6:\"exists\";b:1;s:18:\"wasRecentlyCreated\";b:1;s:28:\"\0*\0escapeWhenCastingToString\";b:0;s:13:\"\0*\0attributes\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\";s:10:\"total_rows\";i:1;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2026-02-01 12:22:40\";s:10:\"created_at\";s:19:\"2026-02-01 12:22:39\";s:2:\"id\";i:6;s:9:\"file_name\";s:32:\"ekspor-6-questionnaire-responses\";}s:11:\"\0*\0original\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\";s:10:\"total_rows\";i:1;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2026-02-01 12:22:40\";s:10:\"created_at\";s:19:\"2026-02-01 12:22:39\";s:2:\"id\";i:6;s:9:\"file_name\";s:32:\"ekspor-6-questionnaire-responses\";}s:10:\"\0*\0changes\";a:2:{s:10:\"updated_at\";s:19:\"2026-02-01 12:22:40\";s:9:\"file_name\";s:32:\"ekspor-6-questionnaire-responses\";}s:11:\"\0*\0previous\";a:1:{s:10:\"updated_at\";s:19:\"2026-02-01 12:22:39\";}s:8:\"\0*\0casts\";a:4:{s:12:\"completed_at\";s:9:\"timestamp\";s:14:\"processed_rows\";s:7:\"integer\";s:10:\"total_rows\";s:7:\"integer\";s:15:\"successful_rows\";s:7:\"integer\";}s:17:\"\0*\0classCastCache\";a:0:{}s:21:\"\0*\0attributeCastCache\";a:0:{}s:13:\"\0*\0dateFormat\";N;s:10:\"\0*\0appends\";a:0:{}s:19:\"\0*\0dispatchesEvents\";a:0:{}s:14:\"\0*\0observables\";a:0:{}s:12:\"\0*\0relations\";a:0:{}s:10:\"\0*\0touches\";a:0:{}s:27:\"\0*\0relationAutoloadCallback\";N;s:26:\"\0*\0relationAutoloadContext\";N;s:10:\"timestamps\";b:1;s:13:\"usesUniqueIds\";b:0;s:9:\"\0*\0hidden\";a:0:{}s:10:\"\0*\0visible\";a:0:{}s:11:\"\0*\0fillable\";a:0:{}s:10:\"\0*\0guarded\";a:0:{}}s:12:\"\0*\0columnMap\";a:9:{s:19:\"questionnaire.title\";s:9:\"Kuesioner\";s:9:\"user.name\";s:6:\"Alumni\";s:15:\"user.alumni.nim\";s:3:\"NIM\";s:17:\"user.alumni.major\";s:7:\"Jurusan\";s:7:\"answers\";s:17:\"Ringkasan Jawaban\";s:11:\"total_point\";s:10:\"Total Poin\";s:16:\"point_percentage\";s:10:\"Persentase\";s:14:\"point_category\";s:8:\"Kategori\";s:12:\"submitted_at\";s:14:\"Tanggal Submit\";}s:10:\"\0*\0options\";a:0:{}}s:9:\"\0*\0export\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":5:{s:5:\"class\";s:38:\"Filament\\Actions\\Exports\\Models\\Export\";s:2:\"id\";i:6;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";s:15:\"collectionClass\";N;}s:12:\"\0*\0columnMap\";a:9:{s:19:\"questionnaire.title\";s:9:\"Kuesioner\";s:9:\"user.name\";s:6:\"Alumni\";s:15:\"user.alumni.nim\";s:3:\"NIM\";s:17:\"user.alumni.major\";s:7:\"Jurusan\";s:7:\"answers\";s:17:\"Ringkasan Jawaban\";s:11:\"total_point\";s:10:\"Total Poin\";s:16:\"point_percentage\";s:10:\"Persentase\";s:14:\"point_category\";s:8:\"Kategori\";s:12:\"submitted_at\";s:14:\"Tanggal Submit\";}s:10:\"\0*\0formats\";a:2:{i:0;E:47:\"Filament\\Actions\\Exports\\Enums\\ExportFormat:Csv\";i:1;E:48:\"Filament\\Actions\\Exports\\Enums\\ExportFormat:Xlsx\";}s:10:\"\0*\0options\";a:0:{}s:7:\"chained\";a:1:{i:0;s:2974:\"O:44:\"Filament\\Actions\\Exports\\Jobs\\CreateXlsxFile\":4:{s:11:\"\0*\0exporter\";O:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\":3:{s:9:\"\0*\0export\";O:38:\"Filament\\Actions\\Exports\\Models\\Export\":33:{s:13:\"\0*\0connection\";s:5:\"mysql\";s:8:\"\0*\0table\";N;s:13:\"\0*\0primaryKey\";s:2:\"id\";s:10:\"\0*\0keyType\";s:3:\"int\";s:12:\"incrementing\";b:1;s:7:\"\0*\0with\";a:0:{}s:12:\"\0*\0withCount\";a:0:{}s:19:\"preventsLazyLoading\";b:0;s:10:\"\0*\0perPage\";i:15;s:6:\"exists\";b:1;s:18:\"wasRecentlyCreated\";b:1;s:28:\"\0*\0escapeWhenCastingToString\";b:0;s:13:\"\0*\0attributes\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\";s:10:\"total_rows\";i:1;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2026-02-01 12:22:40\";s:10:\"created_at\";s:19:\"2026-02-01 12:22:39\";s:2:\"id\";i:6;s:9:\"file_name\";s:32:\"ekspor-6-questionnaire-responses\";}s:11:\"\0*\0original\";a:8:{s:7:\"user_id\";i:1;s:8:\"exporter\";s:50:\"App\\Filament\\Exports\\QuestionnaireResponseExporter\";s:10:\"total_rows\";i:1;s:9:\"file_disk\";s:5:\"local\";s:10:\"updated_at\";s:19:\"2026-02-01 12:22:40\";s:10:\"created_at\";s:19:\"2026-02-01 12:22:39\";s:2:\"id\";i:6;s:9:\"file_name\";s:32:\"ekspor-6-questionnaire-responses\";}s:10:\"\0*\0changes\";a:2:{s:10:\"updated_at\";s:19:\"2026-02-01 12:22:40\";s:9:\"file_name\";s:32:\"ekspor-6-questionnaire-responses\";}s:11:\"\0*\0previous\";a:1:{s:10:\"updated_at\";s:19:\"2026-02-01 12:22:39\";}s:8:\"\0*\0casts\";a:4:{s:12:\"completed_at\";s:9:\"timestamp\";s:14:\"processed_rows\";s:7:\"integer\";s:10:\"total_rows\";s:7:\"integer\";s:15:\"successful_rows\";s:7:\"integer\";}s:17:\"\0*\0classCastCache\";a:0:{}s:21:\"\0*\0attributeCastCache\";a:0:{}s:13:\"\0*\0dateFormat\";N;s:10:\"\0*\0appends\";a:0:{}s:19:\"\0*\0dispatchesEvents\";a:0:{}s:14:\"\0*\0observables\";a:0:{}s:12:\"\0*\0relations\";a:0:{}s:10:\"\0*\0touches\";a:0:{}s:27:\"\0*\0relationAutoloadCallback\";N;s:26:\"\0*\0relationAutoloadContext\";N;s:10:\"timestamps\";b:1;s:13:\"usesUniqueIds\";b:0;s:9:\"\0*\0hidden\";a:0:{}s:10:\"\0*\0visible\";a:0:{}s:11:\"\0*\0fillable\";a:0:{}s:10:\"\0*\0guarded\";a:0:{}}s:12:\"\0*\0columnMap\";a:9:{s:19:\"questionnaire.title\";s:9:\"Kuesioner\";s:9:\"user.name\";s:6:\"Alumni\";s:15:\"user.alumni.nim\";s:3:\"NIM\";s:17:\"user.alumni.major\";s:7:\"Jurusan\";s:7:\"answers\";s:17:\"Ringkasan Jawaban\";s:11:\"total_point\";s:10:\"Total Poin\";s:16:\"point_percentage\";s:10:\"Persentase\";s:14:\"point_category\";s:8:\"Kategori\";s:12:\"submitted_at\";s:14:\"Tanggal Submit\";}s:10:\"\0*\0options\";a:0:{}}s:9:\"\0*\0export\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":5:{s:5:\"class\";s:38:\"Filament\\Actions\\Exports\\Models\\Export\";s:2:\"id\";i:6;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";s:15:\"collectionClass\";N;}s:12:\"\0*\0columnMap\";a:9:{s:19:\"questionnaire.title\";s:9:\"Kuesioner\";s:9:\"user.name\";s:6:\"Alumni\";s:15:\"user.alumni.nim\";s:3:\"NIM\";s:17:\"user.alumni.major\";s:7:\"Jurusan\";s:7:\"answers\";s:17:\"Ringkasan Jawaban\";s:11:\"total_point\";s:10:\"Total Poin\";s:16:\"point_percentage\";s:10:\"Persentase\";s:14:\"point_category\";s:8:\"Kategori\";s:12:\"submitted_at\";s:14:\"Tanggal Submit\";}s:10:\"\0*\0options\";a:0:{}}\";}s:19:\"chainCatchCallbacks\";a:0:{}}}s:8:\"function\";s:266:\"function (\\Illuminate\\Bus\\Batch $batch) use ($next) {\n if (! $batch->cancelled()) {\n \\Illuminate\\Container\\Container::getInstance()->make(\\Illuminate\\Contracts\\Bus\\Dispatcher::class)->dispatch($next);\n }\n }\";s:5:\"scope\";s:27:\"Illuminate\\Bus\\ChainedBatch\";s:4:\"this\";N;s:4:\"self\";s:32:\"00000000000009360000000000000000\";}\";s:4:\"hash\";s:44:\"JkwpW1GemcJFNo8+DB1runfLgpqhc0OmIaN+AA/i1cI=\";}}}}', NULL, 1769948565, 1769948566), +('a12f7d37-1b1d-48be-afba-eeb5c33f232f', '', 1, 0, 0, '[]', 'a:2:{s:13:\"allowFailures\";b:1;s:7:\"finally\";a:1:{i:0;O:47:\"Laravel\\SerializableClosure\\SerializableClosure\":1:{s:12:\"serializable\";O:46:\"Laravel\\SerializableClosure\\Serializers\\Signed\":2:{s:12:\"serializable\";s:4040:\"O:46:\"Laravel\\SerializableClosure\\Serializers\\Native\":5:{s:3:\"use\";a:4:{s:9:\"columnMap\";a:19:{s:3:\"nim\";s:3:\"nim\";s:8:\"username\";s:8:\"username\";s:11:\"birth_place\";s:11:\"birth_place\";s:10:\"birth_date\";s:10:\"birth_date\";s:6:\"gender\";s:6:\"gender\";s:7:\"address\";s:7:\"address\";s:12:\"phone_number\";s:12:\"phone_number\";s:5:\"email\";s:5:\"email\";s:3:\"gpa\";s:3:\"gpa\";s:7:\"faculty\";s:7:\"faculty\";s:5:\"major\";s:5:\"major\";s:10:\"entry_year\";s:10:\"entry_year\";s:15:\"graduation_year\";s:15:\"graduation_year\";s:6:\"status\";s:6:\"status\";s:14:\"category_field\";s:14:\"category_field\";s:14:\"waiting_period\";s:14:\"waiting_period\";s:12:\"job_position\";s:12:\"job_position\";s:12:\"company_name\";s:12:\"company_name\";s:4:\"foto\";s:4:\"foto\";}s:6:\"import\";O:45:\"Illuminate\\Contracts\\Database\\ModelIdentifier\":5:{s:5:\"class\";s:38:\"Filament\\Actions\\Imports\\Models\\Import\";s:2:\"id\";i:7;s:9:\"relations\";a:0:{}s:10:\"connection\";s:5:\"mysql\";s:15:\"collectionClass\";N;}s:13:\"jobConnection\";N;s:7:\"options\";a:0:{}}s:8:\"function\";s:2925:\"function () use ($columnMap, $import, $jobConnection, $options) {\n $import->touch(\'completed_at\');\n\n event(new \\Filament\\Actions\\Imports\\Events\\ImportCompleted($import, $columnMap, $options));\n\n if (! $import->user instanceof \\Illuminate\\Contracts\\Auth\\Authenticatable) {\n return;\n }\n\n $failedRowsCount = $import->getFailedRowsCount();\n\n \\Filament\\Notifications\\Notification::make()\n ->title($import->importer::getCompletedNotificationTitle($import))\n ->body($import->importer::getCompletedNotificationBody($import))\n ->when(\n ! $failedRowsCount,\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->success(),\n )\n ->when(\n $failedRowsCount && ($failedRowsCount < $import->total_rows),\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->warning(),\n )\n ->when(\n $failedRowsCount === $import->total_rows,\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->danger(),\n )\n ->when(\n $failedRowsCount,\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->actions([\n \\Filament\\Notifications\\Actions\\Action::make(\'downloadFailedRowsCsv\')\n ->label(trans_choice(\'filament-actions::import.notifications.completed.actions.download_failed_rows_csv.label\', $failedRowsCount, [\n \'count\' => \\Illuminate\\Support\\Number::format($failedRowsCount),\n ]))\n ->color(\'danger\')\n ->url(route(\'filament.imports.failed-rows.download\', [\'import\' => $import], absolute: false), shouldOpenInNewTab: true)\n ->markAsRead(),\n ]),\n )\n ->when(\n ($jobConnection === \'sync\') ||\n (blank($jobConnection) && (config(\'queue.default\') === \'sync\')),\n fn (\\Filament\\Notifications\\Notification $notification) => $notification\n ->persistent()\n ->send(),\n fn (\\Filament\\Notifications\\Notification $notification) => $notification->sendToDatabase($import->user, isEventDispatched: true),\n );\n }\";s:5:\"scope\";s:36:\"Filament\\Tables\\Actions\\ImportAction\";s:4:\"this\";N;s:4:\"self\";s:32:\"0000000000000ee00000000000000000\";}\";s:4:\"hash\";s:44:\"kimKbbWMUdhF8ZpjeuAEs3Uv23Hmwap8FBf6DnJnGtU=\";}}}}', NULL, 1772253363, 1772253369); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `job_opportunities` +-- + +CREATE TABLE `job_opportunities` ( + `id` bigint UNSIGNED NOT NULL, + `user_id` bigint UNSIGNED NOT NULL, + `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `company` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `location` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `description` text COLLATE utf8mb4_unicode_ci NOT NULL, + `requirements` text COLLATE utf8mb4_unicode_ci, + `salary_range` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `deadline` date DEFAULT NULL, + `contact_info` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `job_opportunities` +-- + +INSERT INTO `job_opportunities` (`id`, `user_id`, `title`, `company`, `location`, `description`, `requirements`, `salary_range`, `deadline`, `contact_info`, `created_at`, `updated_at`) VALUES +(1, 1, 'Junior Web Developer', 'PT Digital Solusi Nusantara', 'Bandung, Jawa Barat', '

 PT Digital Solusi Nusantara membuka kesempatan bagi lulusan baru untuk bergabung sebagai Junior Web Developer. Posisi ini bertanggung jawab dalam pengembangan dan pemeliharaan aplikasi berbasis web menggunakan teknologi modern. 

', '
  • Lulusan D3/S1 Teknik Informatika atau Sistem Informasi
  • Menguasai dasar HTML, CSS, dan JavaScript
  • Familiar dengan framework Laravel atau React
  • Mampu bekerja dalam tim dan memiliki kemauan belajar tinggi
', 'Rp 5.000.000 – Rp 7.000.000 per bulan', '2026-01-10', 'hrd@digitalsolusi.co.id', '2025-12-28 13:12:36', '2025-12-28 13:12:36'), +(3, 1, 'System Analyst Junior', 'PT Solusi Informatika Global', 'Jakarta Barat', '
 Perusahaan membuka posisi System Analyst Junior untuk membantu analisis kebutuhan sistem, pembuatan dokumentasi, dan koordinasi dengan tim developer dalam pengembangan aplikasi perusahaan. 
', '
  • Lulusan S1 Sistem Informasi
  • Memahami SDLC dan UML
  • Mampu membuat dokumen kebutuhan sistem
  • Memiliki kemampuan komunikasi yang baik
', 'Rp 5.000.000 – Rp 7.000.000 per bulan', '2026-03-19', 'recruitment@siginfo.co.id', '2026-01-18 03:39:09', '2026-01-18 03:39:09'), +(4, 1, 'Junior Web Developer', 'PT Digital Solusi Nusantara', 'Bandung, Jawa Barat', '
 PT Digital Solusi Nusantara membuka kesempatan bagi lulusan baru untuk bergabung sebagai Junior Web Developer. Posisi ini bertanggung jawab dalam pengembangan dan pemeliharaan aplikasi berbasis web menggunakan teknologi modern. 
', '
  • Lulusan S1 Sistem Informasi
  • Menguasai dasar HTML, CSS, dan JavaScript
  • Familiar dengan framework Laravel atau React
  • Mampu bekerja dalam tim dan memiliki kemauan belajar tinggi
', 'Rp 8.000.000 – Rp 10.000.000 per bulan', '2026-04-03', '0812-3456-7890', '2026-01-18 03:42:37', '2026-01-18 03:42:37'); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `migrations` +-- + +CREATE TABLE `migrations` ( + `id` int UNSIGNED NOT NULL, + `migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `batch` int NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `migrations` +-- + +INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES +(33, '0001_01_01_000000_create_users_table', 1), +(34, '0001_01_01_000001_create_cache_table', 1), +(35, '0001_01_01_000002_create_jobs_table', 1), +(36, '2025_12_12_042503_create_alumnis_table', 1), +(37, '2025_12_23_034532_create_permission_tables', 1), +(38, '2025_12_23_051722_create_questionnaires_table', 1), +(39, '2025_12_23_074959_create_questionnaire_questions_table', 1), +(40, '2025_12_23_075229_create_questionnaire_responses_table', 1), +(41, '2025_12_23_075712_create_response_answers_table', 1), +(42, '2025_12_23_151007_create_notifications_table', 1), +(43, '2025_12_23_151222_create_imports_table', 1), +(44, '2025_12_23_151223_create_exports_table', 1), +(45, '2025_12_23_151224_create_failed_import_rows_table', 1), +(46, '2025_12_24_081956_create_job_opportunities_table', 1), +(47, '2025_12_24_082547_create_scholarships_table', 1), +(48, '2025_12_24_083203_create_internships_table', 1), +(50, '2026_01_11_084609_add_category_field_and_waiting_period_to_alumnis_table', 2), +(51, '2026_01_31_111857_create_forum_topics_table', 3), +(52, '2026_01_31_112014_create_forum_replies_table', 3), +(53, '2026_02_01_111402_add_max_point_to_questionnaire_questions_table', 4), +(54, '2026_02_01_111552_add_point_to_response_answers_table', 4); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `model_has_permissions` +-- + +CREATE TABLE `model_has_permissions` ( + `permission_id` bigint UNSIGNED NOT NULL, + `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `model_id` bigint UNSIGNED NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `model_has_roles` +-- + +CREATE TABLE `model_has_roles` ( + `role_id` bigint UNSIGNED NOT NULL, + `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `model_id` bigint UNSIGNED NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `model_has_roles` +-- + +INSERT INTO `model_has_roles` (`role_id`, `model_type`, `model_id`) VALUES +(1, 'App\\Models\\User', 1), +(2, 'App\\Models\\User', 12), +(2, 'App\\Models\\User', 13), +(2, 'App\\Models\\User', 14), +(2, 'App\\Models\\User', 15), +(2, 'App\\Models\\User', 16), +(2, 'App\\Models\\User', 17), +(2, 'App\\Models\\User', 18), +(2, 'App\\Models\\User', 19), +(2, 'App\\Models\\User', 20), +(2, 'App\\Models\\User', 21), +(2, 'App\\Models\\User', 22), +(2, 'App\\Models\\User', 23), +(2, 'App\\Models\\User', 24), +(2, 'App\\Models\\User', 50), +(2, 'App\\Models\\User', 68), +(2, 'App\\Models\\User', 69), +(2, 'App\\Models\\User', 70), +(2, 'App\\Models\\User', 71), +(2, 'App\\Models\\User', 72), +(2, 'App\\Models\\User', 77), +(2, 'App\\Models\\User', 78), +(2, 'App\\Models\\User', 79), +(2, 'App\\Models\\User', 80), +(2, 'App\\Models\\User', 81), +(2, 'App\\Models\\User', 82), +(2, 'App\\Models\\User', 83), +(2, 'App\\Models\\User', 84), +(2, 'App\\Models\\User', 85); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `notifications` +-- + +CREATE TABLE `notifications` ( + `id` char(36) COLLATE utf8mb4_unicode_ci NOT NULL, + `type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `notifiable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `notifiable_id` bigint UNSIGNED NOT NULL, + `data` text COLLATE utf8mb4_unicode_ci NOT NULL, + `read_at` timestamp NULL DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `notifications` +-- + +INSERT INTO `notifications` (`id`, `type`, `notifiable_type`, `notifiable_id`, `data`, `read_at`, `created_at`, `updated_at`) VALUES +('00fa1033-a30d-43ab-ae0a-3e1ddac62529', 'Filament\\Notifications\\DatabaseNotification', 'App\\Models\\User', 1, '{\"actions\":[{\"name\":\"download_csv\",\"color\":null,\"event\":null,\"eventData\":[],\"dispatchDirection\":false,\"dispatchToComponent\":null,\"extraAttributes\":[],\"icon\":null,\"iconPosition\":\"before\",\"iconSize\":null,\"isOutlined\":false,\"isDisabled\":false,\"label\":\"Unduh .csv\",\"shouldClose\":false,\"shouldMarkAsRead\":true,\"shouldMarkAsUnread\":false,\"shouldOpenUrlInNewTab\":true,\"size\":\"sm\",\"tooltip\":null,\"url\":\"\\/filament\\/exports\\/6\\/download?format=csv\",\"view\":\"filament-actions::link-action\"},{\"name\":\"download_xlsx\",\"color\":null,\"event\":null,\"eventData\":[],\"dispatchDirection\":false,\"dispatchToComponent\":null,\"extraAttributes\":[],\"icon\":null,\"iconPosition\":\"before\",\"iconSize\":null,\"isOutlined\":false,\"isDisabled\":false,\"label\":\"Unduh .xlsx\",\"shouldClose\":false,\"shouldMarkAsRead\":true,\"shouldMarkAsUnread\":false,\"shouldOpenUrlInNewTab\":true,\"size\":\"sm\",\"tooltip\":null,\"url\":\"\\/filament\\/exports\\/6\\/download?format=xlsx\",\"view\":\"filament-actions::link-action\"}],\"body\":\"Your questionnaire response export has completed and 1 row exported.\",\"color\":null,\"duration\":\"persistent\",\"icon\":\"heroicon-o-check-circle\",\"iconColor\":\"success\",\"status\":\"success\",\"title\":\"Ekspor selesai\",\"view\":\"filament-notifications::notification\",\"viewData\":[],\"format\":\"filament\"}', '2026-02-01 05:23:14', '2026-02-01 05:22:47', '2026-02-01 05:23:14'), +('0b9c790f-376c-4b81-b5f7-1165999518b2', 'Filament\\Notifications\\DatabaseNotification', 'App\\Models\\User', 1, '{\"actions\":[],\"body\":\"Import alumni selesai dan 5 baris berhasil diimpor.\",\"color\":null,\"duration\":\"persistent\",\"icon\":\"heroicon-o-check-circle\",\"iconColor\":\"success\",\"status\":\"success\",\"title\":\"Import completed\",\"view\":\"filament-notifications::notification\",\"viewData\":[],\"format\":\"filament\"}', NULL, '2026-01-13 03:06:34', '2026-01-13 03:06:34'), +('15278e54-0040-48c1-aec9-0edce500b5cb', 'Filament\\Notifications\\DatabaseNotification', 'App\\Models\\User', 1, '{\"actions\":[],\"body\":\"Import alumni selesai dan 10 baris berhasil diimpor.\",\"color\":null,\"duration\":\"persistent\",\"icon\":\"heroicon-o-check-circle\",\"iconColor\":\"success\",\"status\":\"success\",\"title\":\"Import completed\",\"view\":\"filament-notifications::notification\",\"viewData\":[],\"format\":\"filament\"}', NULL, '2025-12-28 12:50:32', '2025-12-28 12:50:32'), +('16637fea-3ab9-4642-bd33-41aff0a227b6', 'Filament\\Notifications\\DatabaseNotification', 'App\\Models\\User', 1, '{\"actions\":[],\"body\":\"Import alumni selesai dan 5 baris berhasil diimpor.\",\"color\":null,\"duration\":\"persistent\",\"icon\":\"heroicon-o-check-circle\",\"iconColor\":\"success\",\"status\":\"success\",\"title\":\"Impor selesai\",\"view\":\"filament-notifications::notification\",\"viewData\":[],\"format\":\"filament\"}', NULL, '2026-02-27 21:36:10', '2026-02-27 21:36:10'), +('1cb7a1d9-e051-43e6-9090-d96f485543f3', 'Filament\\Notifications\\DatabaseNotification', 'App\\Models\\User', 1, '{\"actions\":[{\"name\":\"download_csv\",\"color\":null,\"event\":null,\"eventData\":[],\"dispatchDirection\":false,\"dispatchToComponent\":null,\"extraAttributes\":[],\"icon\":null,\"iconPosition\":\"before\",\"iconSize\":null,\"isOutlined\":false,\"isDisabled\":false,\"label\":\"Download .csv\",\"shouldClose\":false,\"shouldMarkAsRead\":true,\"shouldMarkAsUnread\":false,\"shouldOpenUrlInNewTab\":true,\"size\":\"sm\",\"tooltip\":null,\"url\":\"\\/filament\\/exports\\/1\\/download?format=csv\",\"view\":\"filament-actions::link-action\"},{\"name\":\"download_xlsx\",\"color\":null,\"event\":null,\"eventData\":[],\"dispatchDirection\":false,\"dispatchToComponent\":null,\"extraAttributes\":[],\"icon\":null,\"iconPosition\":\"before\",\"iconSize\":null,\"isOutlined\":false,\"isDisabled\":false,\"label\":\"Download .xlsx\",\"shouldClose\":false,\"shouldMarkAsRead\":true,\"shouldMarkAsUnread\":false,\"shouldOpenUrlInNewTab\":true,\"size\":\"sm\",\"tooltip\":null,\"url\":\"\\/filament\\/exports\\/1\\/download?format=xlsx\",\"view\":\"filament-actions::link-action\"}],\"body\":\"Your alumni export has completed and 10 rows exported.\",\"color\":null,\"duration\":\"persistent\",\"icon\":\"heroicon-o-check-circle\",\"iconColor\":\"success\",\"status\":\"success\",\"title\":\"Export completed\",\"view\":\"filament-notifications::notification\",\"viewData\":[],\"format\":\"filament\"}', '2025-12-28 12:51:13', '2025-12-28 12:51:02', '2025-12-28 12:51:13'), +('2dd902d2-9c84-48a6-88d7-c02f43655e38', 'Filament\\Notifications\\DatabaseNotification', 'App\\Models\\User', 1, '{\"actions\":[{\"name\":\"download_csv\",\"color\":null,\"event\":null,\"eventData\":[],\"dispatchDirection\":false,\"dispatchToComponent\":null,\"extraAttributes\":[],\"icon\":null,\"iconPosition\":\"before\",\"iconSize\":null,\"isOutlined\":false,\"isDisabled\":false,\"label\":\"Download .csv\",\"shouldClose\":false,\"shouldMarkAsRead\":true,\"shouldMarkAsUnread\":false,\"shouldOpenUrlInNewTab\":true,\"size\":\"sm\",\"tooltip\":null,\"url\":\"\\/filament\\/exports\\/5\\/download?format=csv\",\"view\":\"filament-actions::link-action\"},{\"name\":\"download_xlsx\",\"color\":null,\"event\":null,\"eventData\":[],\"dispatchDirection\":false,\"dispatchToComponent\":null,\"extraAttributes\":[],\"icon\":null,\"iconPosition\":\"before\",\"iconSize\":null,\"isOutlined\":false,\"isDisabled\":false,\"label\":\"Download .xlsx\",\"shouldClose\":false,\"shouldMarkAsRead\":true,\"shouldMarkAsUnread\":false,\"shouldOpenUrlInNewTab\":true,\"size\":\"sm\",\"tooltip\":null,\"url\":\"\\/filament\\/exports\\/5\\/download?format=xlsx\",\"view\":\"filament-actions::link-action\"}],\"body\":\"Your questionnaire response export has completed and 7 rows exported.\",\"color\":null,\"duration\":\"persistent\",\"icon\":\"heroicon-o-check-circle\",\"iconColor\":\"success\",\"status\":\"success\",\"title\":\"Export completed\",\"view\":\"filament-notifications::notification\",\"viewData\":[],\"format\":\"filament\"}', '2026-01-29 03:11:32', '2026-01-29 03:11:15', '2026-01-29 03:11:32'), +('4d6bef7e-bfef-445f-b0b6-b6977daf00bd', 'Filament\\Notifications\\DatabaseNotification', 'App\\Models\\User', 1, '{\"actions\":[{\"name\":\"download_csv\",\"color\":null,\"event\":null,\"eventData\":[],\"dispatchDirection\":false,\"dispatchToComponent\":null,\"extraAttributes\":[],\"icon\":null,\"iconPosition\":\"before\",\"iconSize\":null,\"isOutlined\":false,\"isDisabled\":false,\"label\":\"Download .csv\",\"shouldClose\":false,\"shouldMarkAsRead\":true,\"shouldMarkAsUnread\":false,\"shouldOpenUrlInNewTab\":true,\"size\":\"sm\",\"tooltip\":null,\"url\":\"\\/filament\\/exports\\/2\\/download?format=csv\",\"view\":\"filament-actions::link-action\"},{\"name\":\"download_xlsx\",\"color\":null,\"event\":null,\"eventData\":[],\"dispatchDirection\":false,\"dispatchToComponent\":null,\"extraAttributes\":[],\"icon\":null,\"iconPosition\":\"before\",\"iconSize\":null,\"isOutlined\":false,\"isDisabled\":false,\"label\":\"Download .xlsx\",\"shouldClose\":false,\"shouldMarkAsRead\":true,\"shouldMarkAsUnread\":false,\"shouldOpenUrlInNewTab\":true,\"size\":\"sm\",\"tooltip\":null,\"url\":\"\\/filament\\/exports\\/2\\/download?format=xlsx\",\"view\":\"filament-actions::link-action\"}],\"body\":\"Your questionnaire response export has completed and 2 rows exported.\",\"color\":null,\"duration\":\"persistent\",\"icon\":\"heroicon-o-check-circle\",\"iconColor\":\"success\",\"status\":\"success\",\"title\":\"Export completed\",\"view\":\"filament-notifications::notification\",\"viewData\":[],\"format\":\"filament\"}', '2025-12-29 21:16:42', '2025-12-29 21:16:12', '2025-12-29 21:16:42'), +('a5fe8313-831f-4962-be14-3104c1bb7a1e', 'Filament\\Notifications\\DatabaseNotification', 'App\\Models\\User', 1, '{\"actions\":[{\"name\":\"download_csv\",\"color\":null,\"event\":null,\"eventData\":[],\"dispatchDirection\":false,\"dispatchToComponent\":null,\"extraAttributes\":[],\"icon\":null,\"iconPosition\":\"before\",\"iconSize\":null,\"isOutlined\":false,\"isDisabled\":false,\"label\":\"Download .csv\",\"shouldClose\":false,\"shouldMarkAsRead\":true,\"shouldMarkAsUnread\":false,\"shouldOpenUrlInNewTab\":true,\"size\":\"sm\",\"tooltip\":null,\"url\":\"\\/filament\\/exports\\/4\\/download?format=csv\",\"view\":\"filament-actions::link-action\"},{\"name\":\"download_xlsx\",\"color\":null,\"event\":null,\"eventData\":[],\"dispatchDirection\":false,\"dispatchToComponent\":null,\"extraAttributes\":[],\"icon\":null,\"iconPosition\":\"before\",\"iconSize\":null,\"isOutlined\":false,\"isDisabled\":false,\"label\":\"Download .xlsx\",\"shouldClose\":false,\"shouldMarkAsRead\":true,\"shouldMarkAsUnread\":false,\"shouldOpenUrlInNewTab\":true,\"size\":\"sm\",\"tooltip\":null,\"url\":\"\\/filament\\/exports\\/4\\/download?format=xlsx\",\"view\":\"filament-actions::link-action\"}],\"body\":\"Your questionnaire response export has completed and 10 rows exported.\",\"color\":null,\"duration\":\"persistent\",\"icon\":\"heroicon-o-check-circle\",\"iconColor\":\"success\",\"status\":\"success\",\"title\":\"Export completed\",\"view\":\"filament-notifications::notification\",\"viewData\":[],\"format\":\"filament\"}', NULL, '2026-01-27 02:07:28', '2026-01-27 02:07:28'), +('a9827db2-a578-4d6f-98d9-59d12da1eb41', 'Filament\\Notifications\\DatabaseNotification', 'App\\Models\\User', 1, '{\"actions\":[],\"body\":\"Import alumni selesai dan 10 baris berhasil diimpor.\",\"color\":null,\"duration\":\"persistent\",\"icon\":\"heroicon-o-check-circle\",\"iconColor\":\"success\",\"status\":\"success\",\"title\":\"Import completed\",\"view\":\"filament-notifications::notification\",\"viewData\":[],\"format\":\"filament\"}', NULL, '2026-01-02 22:24:18', '2026-01-02 22:24:18'), +('d212c38d-6d78-4336-aa89-b374455a302e', 'Filament\\Notifications\\DatabaseNotification', 'App\\Models\\User', 1, '{\"actions\":[{\"name\":\"download_csv\",\"color\":null,\"event\":null,\"eventData\":[],\"dispatchDirection\":false,\"dispatchToComponent\":null,\"extraAttributes\":[],\"icon\":null,\"iconPosition\":\"before\",\"iconSize\":null,\"isOutlined\":false,\"isDisabled\":false,\"label\":\"Download .csv\",\"shouldClose\":false,\"shouldMarkAsRead\":true,\"shouldMarkAsUnread\":false,\"shouldOpenUrlInNewTab\":true,\"size\":\"sm\",\"tooltip\":null,\"url\":\"\\/filament\\/exports\\/3\\/download?format=csv\",\"view\":\"filament-actions::link-action\"},{\"name\":\"download_xlsx\",\"color\":null,\"event\":null,\"eventData\":[],\"dispatchDirection\":false,\"dispatchToComponent\":null,\"extraAttributes\":[],\"icon\":null,\"iconPosition\":\"before\",\"iconSize\":null,\"isOutlined\":false,\"isDisabled\":false,\"label\":\"Download .xlsx\",\"shouldClose\":false,\"shouldMarkAsRead\":true,\"shouldMarkAsUnread\":false,\"shouldOpenUrlInNewTab\":true,\"size\":\"sm\",\"tooltip\":null,\"url\":\"\\/filament\\/exports\\/3\\/download?format=xlsx\",\"view\":\"filament-actions::link-action\"}],\"body\":\"Your alumni export has completed and 13 rows exported.\",\"color\":null,\"duration\":\"persistent\",\"icon\":\"heroicon-o-check-circle\",\"iconColor\":\"success\",\"status\":\"success\",\"title\":\"Export completed\",\"view\":\"filament-notifications::notification\",\"viewData\":[],\"format\":\"filament\"}', '2026-01-13 02:33:00', '2026-01-13 02:32:41', '2026-01-13 02:33:00'), +('f8296e68-1d49-4391-b406-bf0a840b2eb6', 'Filament\\Notifications\\DatabaseNotification', 'App\\Models\\User', 1, '{\"actions\":[],\"body\":\"Import alumni selesai dan 10 baris berhasil diimpor.\",\"color\":null,\"duration\":\"persistent\",\"icon\":\"heroicon-o-check-circle\",\"iconColor\":\"success\",\"status\":\"success\",\"title\":\"Import completed\",\"view\":\"filament-notifications::notification\",\"viewData\":[],\"format\":\"filament\"}', NULL, '2025-12-28 14:25:08', '2025-12-28 14:25:08'); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `password_reset_tokens` +-- + +CREATE TABLE `password_reset_tokens` ( + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- -------------------------------------------------------- + +-- +-- Table structure for table `permissions` +-- + +CREATE TABLE `permissions` ( + `id` bigint UNSIGNED NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `permissions` +-- + +INSERT INTO `permissions` (`id`, `name`, `guard_name`, `created_at`, `updated_at`) VALUES +(1, 'view_alumni', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(2, 'create_alumni', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(3, 'edit_alumni', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(4, 'delete_alumni', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(5, 'import_alumni', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(6, 'export_alumni', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(7, 'view_questionnaire', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(8, 'create_questionnaire', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(9, 'edit_questionnaire', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(10, 'delete_questionnaire', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(11, 'view_responses', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(12, 'export_responses', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(13, 'fill_questionnaire', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(14, 'view_job', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(15, 'create_job', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(16, 'edit_own_job', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(17, 'delete_own_job', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(18, 'view_scholarship', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(19, 'create_scholarship', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(20, 'edit_own_scholarship', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(21, 'delete_own_scholarship', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(22, 'view_internship', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(23, 'create_internship', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(24, 'edit_own_internship', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(25, 'delete_own_internship', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(26, 'view_admin_dashboard', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(27, 'view_alumni_dashboard', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(28, 'view_forum', 'web', '2026-01-31 05:39:48', '2026-01-31 05:39:48'), +(29, 'create_forum_topic', 'web', '2026-01-31 05:39:49', '2026-01-31 05:39:49'), +(30, 'edit_own_forum_topic', 'web', '2026-01-31 05:39:49', '2026-01-31 05:39:49'), +(31, 'delete_own_forum_topic', 'web', '2026-01-31 05:39:49', '2026-01-31 05:39:49'), +(32, 'reply_forum_topic', 'web', '2026-01-31 05:39:49', '2026-01-31 05:39:49'), +(33, 'moderate_forum', 'web', '2026-01-31 05:39:49', '2026-01-31 05:39:49'); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `questionnaires` +-- + +CREATE TABLE `questionnaires` ( + `id` bigint UNSIGNED NOT NULL, + `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `description` text COLLATE utf8mb4_unicode_ci, + `tahun_periode` year NOT NULL, + `is_active` tinyint(1) NOT NULL DEFAULT '1', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `questionnaires` +-- + +INSERT INTO `questionnaires` (`id`, `title`, `description`, `tahun_periode`, `is_active`, `created_at`, `updated_at`) VALUES +(8, 'KUESIONER TRACER STUDY ALUMNI', 'Kuesioner ini digunakan untuk memperoleh umpan balik alumni sebagai bahan evaluasi dan pengembangan kurikulum serta peningkatan mutu program studi.\nSetiap pernyataan dijawab dengan satu pilihan berikut:\n1 = Sangat Tidak Setuju\n2 = Tidak Setuju\n3 = Netral\n4 = Setuju\n5 = Sangat Setuju', 2026, 1, '2026-02-01 04:46:19', '2026-02-01 04:46:19'), +(9, 'Pendidikan dan Karir', 'Bagian ini bertujuan untuk mengetahui perkembangan pendidikan dan karier alumni setelah lulus.', 2025, 1, '2026-02-27 20:25:23', '2026-02-27 21:59:00'), +(10, 'Penilaian Program Studi', 'Bagian ini bertujuan untuk mengevaluasi kualitas pendidikan dan fasilitas program studi.', 2026, 1, '2026-02-27 20:47:39', '2026-02-27 20:57:47'); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `questionnaire_questions` +-- + +CREATE TABLE `questionnaire_questions` ( + `id` bigint UNSIGNED NOT NULL, + `questionnaire_id` bigint UNSIGNED NOT NULL, + `question_text` text COLLATE utf8mb4_unicode_ci NOT NULL, + `question_type` enum('text','textarea','radio','checkbox','select') COLLATE utf8mb4_unicode_ci NOT NULL, + `options` json DEFAULT NULL, + `is_required` tinyint(1) NOT NULL DEFAULT '0', + `max_point` int NOT NULL DEFAULT '5' COMMENT 'Maksimal poin yang bisa didapat dari pertanyaan ini', + `order` int NOT NULL DEFAULT '0', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `questionnaire_questions` +-- + +INSERT INTO `questionnaire_questions` (`id`, `questionnaire_id`, `question_text`, `question_type`, `options`, `is_required`, `max_point`, `order`, `created_at`, `updated_at`) VALUES +(20, 8, 'Kompetensi yang saya peroleh selama kuliah sesuai dengan kebutuhan pekerjaan saya saat ini.', 'radio', '[\"Sangat Tidak Setuju\", \"Tidak Setuju\", \"Netral\", \"Setuju\", \"Sangat Setuju\"]', 1, 5, 1, '2026-02-01 04:50:26', '2026-02-01 04:50:26'), +(21, 8, 'Kurikulum program studi relevan dengan perkembangan dunia kerja.', 'radio', '[\"Sangat Tidak Setuju\", \"Tidak Setuju\", \"Netral\", \"Setuju\", \"Sangat Setuju\"]', 1, 5, 2, '2026-02-01 04:52:18', '2026-02-01 04:52:18'), +(22, 8, 'Mata kuliah yang saya peroleh mendukung pelaksanaan tugas di tempat kerja.', 'radio', '[\"Sangat Tidak Setuju\", \"Tidak Setuju\", \"Netral\", \"Setuju\", \"Sangat Setuju\"]', 1, 5, 3, '2026-02-01 04:54:57', '2026-02-01 04:54:57'), +(23, 8, 'Program studi membekali saya dengan kemampuan berpikir kritis.', 'radio', '[\"Sangat Tidak Setuju\", \"Tidak Setuju\", \"Netral\", \"Setuju\", \"Sangat Setuju\"]', 1, 5, 4, '2026-02-01 04:56:52', '2026-02-01 04:56:52'), +(24, 8, 'Program studi membekali saya dengan kemampuan pemecahan masalah.', 'radio', '[\"Sangat Tidak Setuju\", \"Tidak Setuju\", \"Netral\", \"Setuju\", \"Sangat Setuju\"]', 1, 5, 5, '2026-02-01 05:00:25', '2026-02-01 05:00:25'), +(25, 9, 'Apakah Anda melanjutkan studi setelah lulus?', 'radio', '[\"Ya\", \"Tidak\"]', 1, 5, 1, '2026-02-27 20:27:19', '2026-02-27 20:27:19'), +(26, 9, 'Jika ya, program studi yang diambil', 'text', NULL, 1, 5, 2, '2026-02-27 20:27:49', '2026-02-27 20:27:49'), +(27, 9, 'Apakah Anda saat ini bekerja?', 'radio', '[\"Ya, sesuai bidang studi\", \"Ya, tidak sesuai bidang studi\", \"Tidak bekerja\"]', 1, 5, 3, '2026-02-27 20:29:14', '2026-02-27 20:29:14'), +(28, 9, 'Posisi/jabatan terakhir Anda', 'text', NULL, 1, 5, 4, '2026-02-27 20:29:34', '2026-02-27 20:29:34'), +(29, 9, 'Nama perusahaan/instansi tempat bekerja', 'text', NULL, 1, 5, 5, '2026-02-27 20:29:55', '2026-02-27 20:31:23'), +(30, 9, 'Seberapa sesuai pekerjaan Anda dengan bidang studi?', 'radio', '[\"Sangat sesuai\", \"Sesuai\", \"Kurang sesuai\", \"Tidak sesuai\"]', 1, 5, 6, '2026-02-27 20:31:06', '2026-02-27 20:31:06'), +(31, 10, 'Seberapa puas Anda terhadap kualitas pendidikan di program studi?', 'radio', '[\"Sangat puas\", \"Puas\", \"Cukup\", \"Kurang\"]', 1, 5, 1, '2026-02-27 20:51:42', '2026-02-27 20:51:42'), +(32, 10, 'Bagaimana kualitas fasilitas pendukung pembelajaran (laboratorium, perpustakaan, dll)?', 'radio', '[\"Sangat baik\", \"Baik\", \"Cukup\", \"Kurang\"]', 1, 5, 2, '2026-02-27 20:52:38', '2026-02-27 20:52:38'), +(33, 10, 'Apakah kurikulum program studi relevan dengan kebutuhan industri saat ini?', 'radio', '[\"Sangat relevan\", \"Relevan\", \"Kurang Relevan\", \"Tidak Relevan\"]', 1, 5, 3, '2026-02-27 20:53:58', '2026-02-27 20:53:58'), +(34, 10, 'Seberapa besar program studi membantu Anda dalam mengembangkan soft skills (komunikasi, kepemimpinan, kerja tim, dll)?', 'radio', '[\"Sangat Besar\", \"Besar\", \"Cukup\", \"Kurang\"]', 1, 5, 4, '2026-02-27 20:54:50', '2026-02-27 20:54:50'); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `questionnaire_responses` +-- + +CREATE TABLE `questionnaire_responses` ( + `id` bigint UNSIGNED NOT NULL, + `questionnaire_id` bigint UNSIGNED NOT NULL, + `user_id` bigint UNSIGNED NOT NULL, + `submitted_at` timestamp NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `questionnaire_responses` +-- + +INSERT INTO `questionnaire_responses` (`id`, `questionnaire_id`, `user_id`, `submitted_at`, `created_at`, `updated_at`) VALUES +(12, 8, 78, '2026-02-01 05:15:38', '2026-02-01 05:15:38', '2026-02-01 05:15:38'), +(14, 8, 12, '2026-02-27 20:39:42', '2026-02-27 20:39:42', '2026-02-27 20:39:42'), +(16, 10, 12, '2026-02-27 20:58:54', '2026-02-27 20:58:54', '2026-02-27 20:58:54'), +(17, 8, 14, '2026-02-27 21:00:14', '2026-02-27 21:00:14', '2026-02-27 21:00:14'), +(19, 10, 14, '2026-02-27 21:01:53', '2026-02-27 21:01:53', '2026-02-27 21:01:53'); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `response_answers` +-- + +CREATE TABLE `response_answers` ( + `id` bigint UNSIGNED NOT NULL, + `response_id` bigint UNSIGNED NOT NULL, + `question_id` bigint UNSIGNED NOT NULL, + `answer_text` text COLLATE utf8mb4_unicode_ci, + `point` int NOT NULL DEFAULT '0' COMMENT 'Poin yang didapat dari jawaban ini', + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `response_answers` +-- + +INSERT INTO `response_answers` (`id`, `response_id`, `question_id`, `answer_text`, `point`, `created_at`, `updated_at`) VALUES +(44, 12, 20, 'Setuju', 4, '2026-02-01 05:15:38', '2026-02-01 05:15:38'), +(45, 12, 21, 'Netral', 3, '2026-02-01 05:15:38', '2026-02-01 05:15:38'), +(46, 12, 22, 'Netral', 3, '2026-02-01 05:15:38', '2026-02-01 05:15:38'), +(47, 12, 23, 'Setuju', 4, '2026-02-01 05:15:38', '2026-02-01 05:15:38'), +(48, 12, 24, 'Setuju', 4, '2026-02-01 05:15:38', '2026-02-01 05:15:38'), +(54, 14, 20, 'Setuju', 4, '2026-02-27 20:39:42', '2026-02-27 20:39:42'), +(55, 14, 21, 'Setuju', 4, '2026-02-27 20:39:42', '2026-02-27 20:39:42'), +(56, 14, 22, 'Setuju', 4, '2026-02-27 20:39:42', '2026-02-27 20:39:42'), +(57, 14, 23, 'Setuju', 4, '2026-02-27 20:39:42', '2026-02-27 20:39:42'), +(58, 14, 24, 'Setuju', 4, '2026-02-27 20:39:42', '2026-02-27 20:39:42'), +(65, 16, 31, 'Puas', 4, '2026-02-27 20:58:54', '2026-02-27 20:58:54'), +(66, 16, 32, 'Cukup', 3, '2026-02-27 20:58:54', '2026-02-27 20:58:54'), +(67, 16, 33, 'Relevan', 1, '2026-02-27 20:58:54', '2026-02-27 20:58:54'), +(68, 16, 34, 'Cukup', 3, '2026-02-27 20:58:54', '2026-02-27 20:58:54'), +(69, 17, 20, 'Setuju', 4, '2026-02-27 21:00:14', '2026-02-27 21:00:14'), +(70, 17, 21, 'Setuju', 4, '2026-02-27 21:00:14', '2026-02-27 21:00:14'), +(71, 17, 22, 'Setuju', 4, '2026-02-27 21:00:14', '2026-02-27 21:00:14'), +(72, 17, 23, 'Setuju', 4, '2026-02-27 21:00:14', '2026-02-27 21:00:14'), +(73, 17, 24, 'Setuju', 4, '2026-02-27 21:00:14', '2026-02-27 21:00:14'), +(80, 19, 31, 'Puas', 4, '2026-02-27 21:01:53', '2026-02-27 21:01:53'), +(81, 19, 32, 'Baik', 4, '2026-02-27 21:01:53', '2026-02-27 21:01:53'), +(82, 19, 33, 'Relevan', 1, '2026-02-27 21:01:53', '2026-02-27 21:01:53'), +(83, 19, 34, 'Besar', 1, '2026-02-27 21:01:53', '2026-02-27 21:01:53'); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `roles` +-- + +CREATE TABLE `roles` ( + `id` bigint UNSIGNED NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `roles` +-- + +INSERT INTO `roles` (`id`, `name`, `guard_name`, `created_at`, `updated_at`) VALUES +(1, 'admin', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'), +(2, 'alumni', 'web', '2025-12-28 12:49:06', '2025-12-28 12:49:06'); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `role_has_permissions` +-- + +CREATE TABLE `role_has_permissions` ( + `permission_id` bigint UNSIGNED NOT NULL, + `role_id` bigint UNSIGNED NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `role_has_permissions` +-- + +INSERT INTO `role_has_permissions` (`permission_id`, `role_id`) VALUES +(1, 1), +(2, 1), +(3, 1), +(4, 1), +(5, 1), +(6, 1), +(7, 1), +(8, 1), +(9, 1), +(10, 1), +(11, 1), +(12, 1), +(14, 1), +(15, 1), +(16, 1), +(17, 1), +(18, 1), +(19, 1), +(20, 1), +(21, 1), +(22, 1), +(23, 1), +(24, 1), +(25, 1), +(26, 1), +(28, 1), +(29, 1), +(30, 1), +(31, 1), +(32, 1), +(33, 1), +(13, 2), +(14, 2), +(15, 2), +(16, 2), +(17, 2), +(18, 2), +(19, 2), +(20, 2), +(21, 2), +(22, 2), +(23, 2), +(24, 2), +(25, 2), +(27, 2), +(28, 2), +(29, 2), +(30, 2), +(31, 2), +(32, 2); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `scholarships` +-- + +CREATE TABLE `scholarships` ( + `id` bigint UNSIGNED NOT NULL, + `user_id` bigint UNSIGNED NOT NULL, + `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `provider` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `description` text COLLATE utf8mb4_unicode_ci NOT NULL, + `requirements` text COLLATE utf8mb4_unicode_ci, + `amount` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `deadline` date DEFAULT NULL, + `link` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `scholarships` +-- + +INSERT INTO `scholarships` (`id`, `user_id`, `title`, `provider`, `description`, `requirements`, `amount`, `deadline`, `link`, `created_at`, `updated_at`) VALUES +(1, 1, 'Beasiswa Prestasi Mahasiswa TI', 'Kementerian Pendidikan, Kebudayaan, Riset, dan Teknologi', '

Beasiswa ini ditujukan bagi mahasiswa lulusan S1 Program Studi Sistem Informasi dan Teknik Informatika yang memiliki prestasi akademik maupun non-akademik. 

Beasiswa diberikan untuk membantu biaya pendidikan dan mendukung pengembangan potensi mahasiswa selama masa studi. 

', '
  • Mahasiswa aktif Program Studi Sistem Informasi
  • Freshgraduate
  • IPK minimal 3.50
  • Memiliki surat rekomendasi dari desa
  • Tidak sedang menerima beasiswa lain
', 'Rp 6.000.000 / semester', '2026-01-09', 'https://beasiswa.kemdikbud.go.id/prestasi-mahasiswa', '2025-12-28 13:16:32', '2025-12-28 13:16:32'), +(3, 1, 'Beasiswa Alumni Berprestasi', 'Yayasan Pendidikan Nusantara', '
 Beasiswa ini ditujukan bagi alumni berprestasi yang ingin melanjutkan studi atau mengembangkan kompetensi profesional. Beasiswa diberikan sebagai bentuk dukungan pengembangan sumber daya manusia. 
', '
  • Alumni perguruan tinggi maksimal lulus 3 tahun terakhir
  • IPK minimal 3.25
  • Surat rekomendasi fakultas
  • Proposal rencana pengembangan diri
', 'Rp 10.000.000 / semester', '2026-03-05', 'https://beasiswa.ypnusantara.or.id', '2026-01-18 03:47:05', '2026-01-18 03:49:26'), +(4, 1, 'Beasiswa Digital Talent', 'Kementerian Komunikasi dan Informatika', '
Beasiswa pelatihan dan pengembangan kompetensi digital. 
', '
  • Mahasiswa aktif atau alumni SI/TI
  • IPK minimal 3.00
  • Memiliki minat di bidang teknologi digital
  • Bersedia mengikuti seluruh rangkaian program
', 'Rp 8.000.000', '2026-03-19', 'https://lpdp.kemenkeu.go.id', '2026-01-18 03:53:17', '2026-01-18 03:53:17'); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `sessions` +-- + +CREATE TABLE `sessions` ( + `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `user_id` bigint UNSIGNED DEFAULT NULL, + `ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `user_agent` text COLLATE utf8mb4_unicode_ci, + `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL, + `last_activity` int NOT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `sessions` +-- + +INSERT INTO `sessions` (`id`, `user_id`, `ip_address`, `user_agent`, `payload`, `last_activity`) VALUES +('l6xEI7AnFnhA9cPYzXc6ur5hOWAkGSRYmJrczzfq', NULL, '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0', 'YTo0OntzOjY6Il90b2tlbiI7czo0MDoiMnVsYkFLOGNMMjl4U2pxb0VVUFBZRTFJNWxuaG9pTmpPSjVjcXJCbCI7czo2OiJfZmxhc2giO2E6Mjp7czozOiJvbGQiO2E6MDp7fXM6MzoibmV3IjthOjA6e319czo5OiJfcHJldmlvdXMiO2E6MTp7czozOiJ1cmwiO3M6NDA6Imh0dHA6Ly8xMjcuMC4wLjE6ODAwMC90cmFjZXItc3R1ZHkvbG9naW4iO31zOjM6InVybCI7YToxOntzOjg6ImludGVuZGVkIjtzOjM0OiJodHRwOi8vMTI3LjAuMC4xOjgwMDAvdHJhY2VyLXN0dWR5Ijt9fQ==', 1772537243), +('sRftpMnDPbOd7QC2WzITLjYHns5jH9h9C8G3iZiH', NULL, '127.0.0.1', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0', 'YToyOntzOjY6Il90b2tlbiI7czo0MDoiR0txMmN5bFprc0haMm1qRTJ1Y0J1WXd5SXNyWFEzOWw4Z3NERjJWeiI7czo2OiJfZmxhc2giO2E6Mjp7czozOiJvbGQiO2E6MDp7fXM6MzoibmV3IjthOjA6e319fQ==', 1772533583); + +-- -------------------------------------------------------- + +-- +-- Table structure for table `users` +-- + +CREATE TABLE `users` ( + `id` bigint UNSIGNED NOT NULL, + `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `email_verified_at` timestamp NULL DEFAULT NULL, + `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, + `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL, + `created_at` timestamp NULL DEFAULT NULL, + `updated_at` timestamp NULL DEFAULT NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- +-- Dumping data for table `users` +-- + +INSERT INTO `users` (`id`, `name`, `email`, `email_verified_at`, `password`, `remember_token`, `created_at`, `updated_at`) VALUES +(1, 'Administrator', 'admin@gmail.com', NULL, '$2y$12$xSO6sqCK2WczsTWxAsvofedbRlJSw0/cRpX/tSQy69WT7d5.Fazby', 'iPMNtNqnOXUQNSDL6g4kLyuFTfddMt2P1aUl0QcDobEyObgYELQbAx8bl57g', '2025-12-28 12:49:06', '2025-12-28 13:50:02'), +(12, 'Jaka', 'jaka@gmail.com', NULL, '$2y$12$e6rMhr19xrvrwPwHOvawFOSvsHdvqT2RBDfKjb9O1p5kGl9SJej9W', NULL, '2025-12-28 13:22:37', '2025-12-28 13:47:27'), +(13, 'Nadia Oktarina', 'nadiaokta847@gmail.com', NULL, '$2y$12$YsgIY.7MePyZsDKHej9UnuTJAmFJDU6oNAFr/h0lYTqW5epU1t4wK', '2raRpkCwAJsGJxqIQTYGcexmsWMpMWugLmpbh0IOnRn3mnHuZAYDiz5QfFUx', '2025-12-28 13:53:07', '2026-03-03 04:03:52'), +(14, 'Ahmad Fauzi', 'ahmad.fauzi@gmail.com', NULL, '$2y$12$FBofMveiP3.a90xnhy1skeo5aeerTRS6jWHRl2V2/hSKYeyy.jDMi', NULL, '2025-12-28 14:25:04', '2025-12-28 14:25:04'), +(15, 'Siti Aisyah', 'siti.aisyah@gmail.com', NULL, '$2y$12$dVIxRwsOxBLIXswlezm1mu7BOzea.AmHza00AuRadHm2piZ.Su.Yq', NULL, '2025-12-28 14:25:04', '2025-12-28 14:25:04'), +(16, 'Budi Santoso', 'budi.santoso@gmail.com', NULL, '$2y$12$ANofpD0fi63QiYcytgbbRuHd6ndlUR3e9vgrSNIZQ83lte.FCHYl2', NULL, '2025-12-28 14:25:05', '2025-12-28 14:25:05'), +(17, 'Rina Marlina', 'rina.marlina@gmail.com', NULL, '$2y$12$odUNr3GrWPqWg8OYA8R/0ut1ykWD0fo04SO4s0d/sd/A25ZnTTnnO', NULL, '2025-12-28 14:25:05', '2025-12-28 14:25:05'), +(18, 'Dedi Pratama', 'dedi.pratama@gmail.com', NULL, '$2y$12$iM3r22WisCptI1iywMiucuaDg7uHcOgG5DRtrv1kxULxRJofGorjy', NULL, '2025-12-28 14:25:05', '2025-12-28 14:25:05'), +(19, 'Lina Putri', 'lina.putri@gmail.com', NULL, '$2y$12$Q5ib5xtBQuT27dO3diHR2OMbvuEudjqOfQS.TykbIuoiVPMvg/AD6', NULL, '2025-12-28 14:25:06', '2025-12-28 14:25:06'), +(20, 'Rizky Maulana', 'rizky.maulana@gmail.com', NULL, '$2y$12$727wKDdO61RcfK5i5AdKv.HqYqmpnd/byVOOIa6L./eGMgFkBcCha', NULL, '2025-12-28 14:25:06', '2025-12-28 14:25:06'), +(21, 'Putri Ananda', 'putri.ananda@gmail.com', NULL, '$2y$12$8P2oUga1mv3Pau2FdjROmexrir9PpZio/9e13rwx1bKCmZXbaCSNy', NULL, '2025-12-28 14:25:07', '2025-12-28 14:25:07'), +(22, 'Andi Wijaya', 'andi.wijaya@gmail.com', NULL, '$2y$12$xiiauIYOnswDUkugw655tOCVXfcaRppT8lOHJTOCiE7MQ14vTaDUa', NULL, '2025-12-28 14:25:07', '2025-12-28 14:25:07'), +(23, 'Nurul Hidayah', 'nurul.hidayah@gmail.com', NULL, '$2y$12$sdEC9rUCkiQmUkq28kOcf.u7wmevQOTYq75FXWXJ.ERYjYxN.qR.C', NULL, '2025-12-28 14:25:07', '2025-12-28 14:25:07'), +(50, 'Dedi Pratama', 'dedi.pratama20@gmail.com', NULL, '$2y$12$HuaXSxIQcoAxOJR8tqgbveMVNl9EkxKed6G6QatIr36CwhYo4ML8e', NULL, '2026-01-13 02:44:37', '2026-01-13 04:23:40'), +(68, 'Ahmad Fauzi', 'ahmad.fauzi02@gmail.com', NULL, '$2y$12$L6h8w6jY/0lq7Rg9pVNCieoAdWeDnTXArpJyWVmF0U5SePkEmTshm', NULL, '2026-01-13 03:06:32', '2026-01-13 03:06:32'), +(69, 'Siti Aminah', 'siti.aminah@gmail.com', NULL, '$2y$12$opetj9YrhvwZrKYlkNgpkuVKvvkwuapvISFtaS3dAO1gcInlst53.', NULL, '2026-01-13 03:06:32', '2026-01-13 03:06:32'), +(70, 'Budi Santoso', 'budi.santoso22@gmail.com', NULL, '$2y$12$btj7XzZQL/KunfJfTPAk1uvRBN3uUbz5bBgMQrCllBm1wvk/.gWcu', NULL, '2026-01-13 03:06:33', '2026-01-13 03:06:33'), +(71, 'Rina Putri', 'rina.putri@gmail.com', NULL, '$2y$12$wzenw3s2S9B9MzI.nNGsk.j3I.TypVBnnJYAJdKmLC3LO2lDYm5oa', NULL, '2026-01-13 03:06:34', '2026-01-13 03:06:34'), +(72, 'M syarifuddin', 'udin@gmail.com', NULL, '$2y$12$ctoSJBmL7qa8yOJ.o4ECJu7kdCgZa39d/uTfcHNbvK7WX4N5/wZbW', NULL, '2026-01-14 08:09:07', '2026-01-14 08:09:07'), +(78, 'Ahmad Fairuz', 'ahmadfairuz1@gmail.com', NULL, '$2y$12$xOxJB2QNwYwIaqIuJyyeyOh42YslIFFuAm7lQFgzZf6vA07JiXQYy', NULL, '2026-01-26 01:00:24', '2026-01-27 02:29:18'), +(80, 'Mustofa', 'mustop4@gmail.com', NULL, '$2y$12$yDNh98AGPvSRBBjkAf8Hg.SXVYzSL7z8xB7Zh1GMwAMuFiDCYyYkG', NULL, '2026-02-27 20:13:10', '2026-02-27 20:13:10'), +(81, 'Ikbal', 'ikbal2@gmail.com', NULL, '$2y$12$es8n5pV/7HQdhl8OUnaI.OAXN7aOo1/Hg/UARfwnQQa8MbeXwzIty', NULL, '2026-02-27 21:22:01', '2026-02-27 21:22:01'), +(82, 'Siti Rohaya', 'sitiryh@gmail.com', NULL, '$2y$12$.rH1CXHxJcwBi55V1VFCfe8f2EMHZOx2OYbUGeanKd9dxhq5LIkwS', NULL, '2026-02-27 21:22:01', '2026-02-27 21:22:01'), +(83, 'Budiman', 'budiman02@gmail.com', NULL, '$2y$12$XkVNmCJrWcLb16GJ0NwAIeq1tLb/ru3tP9wfN24T4VBH0J/Vt2SlG', NULL, '2026-02-27 21:22:02', '2026-02-27 21:22:02'), +(84, 'Rina badria', 'rinabadr@gmail.com', NULL, '$2y$12$rDCeV1V1gLfteNXbEePleu6QaU1Sl3viI52l.Xc1ZaZ0OH5QT17l.', NULL, '2026-02-27 21:22:02', '2026-02-27 21:22:02'), +(85, 'Rendi Pratama', 'rendipratama10@gmail.com', NULL, '$2y$12$LDJEnhHBMELnMw6PuZH59unl5nUxL36SLkI1nMmTmlUpNrV2AyrJO', NULL, '2026-02-27 21:36:09', '2026-02-27 21:36:09'); + +-- +-- Indexes for dumped tables +-- + +-- +-- Indexes for table `alumnis` +-- +ALTER TABLE `alumnis` + ADD PRIMARY KEY (`id`), + ADD UNIQUE KEY `alumnis_nim_unique` (`nim`), + ADD UNIQUE KEY `alumnis_email_unique` (`email`), + ADD KEY `alumnis_user_id_foreign` (`user_id`); + +-- +-- Indexes for table `cache` +-- +ALTER TABLE `cache` + ADD PRIMARY KEY (`key`); + +-- +-- Indexes for table `cache_locks` +-- +ALTER TABLE `cache_locks` + ADD PRIMARY KEY (`key`); + +-- +-- Indexes for table `exports` +-- +ALTER TABLE `exports` + ADD PRIMARY KEY (`id`), + ADD KEY `exports_user_id_foreign` (`user_id`); + +-- +-- Indexes for table `failed_import_rows` +-- +ALTER TABLE `failed_import_rows` + ADD PRIMARY KEY (`id`), + ADD KEY `failed_import_rows_import_id_foreign` (`import_id`); + +-- +-- Indexes for table `failed_jobs` +-- +ALTER TABLE `failed_jobs` + ADD PRIMARY KEY (`id`), + ADD UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`); + +-- +-- Indexes for table `forum_replies` +-- +ALTER TABLE `forum_replies` + ADD PRIMARY KEY (`id`), + ADD KEY `forum_replies_user_id_foreign` (`user_id`), + ADD KEY `forum_replies_topic_id_index` (`topic_id`); + +-- +-- Indexes for table `forum_topics` +-- +ALTER TABLE `forum_topics` + ADD PRIMARY KEY (`id`), + ADD KEY `forum_topics_user_id_foreign` (`user_id`), + ADD KEY `forum_topics_created_at_is_pinned_index` (`created_at`,`is_pinned`); + +-- +-- Indexes for table `imports` +-- +ALTER TABLE `imports` + ADD PRIMARY KEY (`id`), + ADD KEY `imports_user_id_foreign` (`user_id`); + +-- +-- Indexes for table `internships` +-- +ALTER TABLE `internships` + ADD PRIMARY KEY (`id`), + ADD KEY `internships_user_id_foreign` (`user_id`); + +-- +-- Indexes for table `jobs` +-- +ALTER TABLE `jobs` + ADD PRIMARY KEY (`id`), + ADD KEY `jobs_queue_index` (`queue`); + +-- +-- Indexes for table `job_batches` +-- +ALTER TABLE `job_batches` + ADD PRIMARY KEY (`id`); + +-- +-- Indexes for table `job_opportunities` +-- +ALTER TABLE `job_opportunities` + ADD PRIMARY KEY (`id`), + ADD KEY `job_opportunities_user_id_foreign` (`user_id`); + +-- +-- Indexes for table `migrations` +-- +ALTER TABLE `migrations` + ADD PRIMARY KEY (`id`); + +-- +-- Indexes for table `model_has_permissions` +-- +ALTER TABLE `model_has_permissions` + ADD PRIMARY KEY (`permission_id`,`model_id`,`model_type`), + ADD KEY `model_has_permissions_model_id_model_type_index` (`model_id`,`model_type`); + +-- +-- Indexes for table `model_has_roles` +-- +ALTER TABLE `model_has_roles` + ADD PRIMARY KEY (`role_id`,`model_id`,`model_type`), + ADD KEY `model_has_roles_model_id_model_type_index` (`model_id`,`model_type`); + +-- +-- Indexes for table `notifications` +-- +ALTER TABLE `notifications` + ADD PRIMARY KEY (`id`), + ADD KEY `notifications_notifiable_type_notifiable_id_index` (`notifiable_type`,`notifiable_id`); + +-- +-- Indexes for table `password_reset_tokens` +-- +ALTER TABLE `password_reset_tokens` + ADD PRIMARY KEY (`email`); + +-- +-- Indexes for table `permissions` +-- +ALTER TABLE `permissions` + ADD PRIMARY KEY (`id`), + ADD UNIQUE KEY `permissions_name_guard_name_unique` (`name`,`guard_name`); + +-- +-- Indexes for table `questionnaires` +-- +ALTER TABLE `questionnaires` + ADD PRIMARY KEY (`id`); + +-- +-- Indexes for table `questionnaire_questions` +-- +ALTER TABLE `questionnaire_questions` + ADD PRIMARY KEY (`id`), + ADD KEY `questionnaire_questions_questionnaire_id_foreign` (`questionnaire_id`); + +-- +-- Indexes for table `questionnaire_responses` +-- +ALTER TABLE `questionnaire_responses` + ADD PRIMARY KEY (`id`), + ADD UNIQUE KEY `questionnaire_responses_questionnaire_id_user_id_unique` (`questionnaire_id`,`user_id`), + ADD KEY `questionnaire_responses_user_id_foreign` (`user_id`); + +-- +-- Indexes for table `response_answers` +-- +ALTER TABLE `response_answers` + ADD PRIMARY KEY (`id`), + ADD KEY `response_answers_response_id_foreign` (`response_id`), + ADD KEY `response_answers_question_id_foreign` (`question_id`); + +-- +-- Indexes for table `roles` +-- +ALTER TABLE `roles` + ADD PRIMARY KEY (`id`), + ADD UNIQUE KEY `roles_name_guard_name_unique` (`name`,`guard_name`); + +-- +-- Indexes for table `role_has_permissions` +-- +ALTER TABLE `role_has_permissions` + ADD PRIMARY KEY (`permission_id`,`role_id`), + ADD KEY `role_has_permissions_role_id_foreign` (`role_id`); + +-- +-- Indexes for table `scholarships` +-- +ALTER TABLE `scholarships` + ADD PRIMARY KEY (`id`), + ADD KEY `scholarships_user_id_foreign` (`user_id`); + +-- +-- Indexes for table `sessions` +-- +ALTER TABLE `sessions` + ADD PRIMARY KEY (`id`), + ADD KEY `sessions_user_id_index` (`user_id`), + ADD KEY `sessions_last_activity_index` (`last_activity`); + +-- +-- Indexes for table `users` +-- +ALTER TABLE `users` + ADD PRIMARY KEY (`id`), + ADD UNIQUE KEY `users_email_unique` (`email`); + +-- +-- AUTO_INCREMENT for dumped tables +-- + +-- +-- AUTO_INCREMENT for table `alumnis` +-- +ALTER TABLE `alumnis` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=124; + +-- +-- AUTO_INCREMENT for table `exports` +-- +ALTER TABLE `exports` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7; + +-- +-- AUTO_INCREMENT for table `failed_import_rows` +-- +ALTER TABLE `failed_import_rows` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `failed_jobs` +-- +ALTER TABLE `failed_jobs` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT; + +-- +-- AUTO_INCREMENT for table `forum_replies` +-- +ALTER TABLE `forum_replies` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=15; + +-- +-- AUTO_INCREMENT for table `forum_topics` +-- +ALTER TABLE `forum_topics` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7; + +-- +-- AUTO_INCREMENT for table `imports` +-- +ALTER TABLE `imports` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=8; + +-- +-- AUTO_INCREMENT for table `internships` +-- +ALTER TABLE `internships` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=6; + +-- +-- AUTO_INCREMENT for table `jobs` +-- +ALTER TABLE `jobs` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=7; + +-- +-- AUTO_INCREMENT for table `job_opportunities` +-- +ALTER TABLE `job_opportunities` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=8; + +-- +-- AUTO_INCREMENT for table `migrations` +-- +ALTER TABLE `migrations` + MODIFY `id` int UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=55; + +-- +-- AUTO_INCREMENT for table `permissions` +-- +ALTER TABLE `permissions` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=34; + +-- +-- AUTO_INCREMENT for table `questionnaires` +-- +ALTER TABLE `questionnaires` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=14; + +-- +-- AUTO_INCREMENT for table `questionnaire_questions` +-- +ALTER TABLE `questionnaire_questions` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=36; + +-- +-- AUTO_INCREMENT for table `questionnaire_responses` +-- +ALTER TABLE `questionnaire_responses` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=20; + +-- +-- AUTO_INCREMENT for table `response_answers` +-- +ALTER TABLE `response_answers` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=84; + +-- +-- AUTO_INCREMENT for table `roles` +-- +ALTER TABLE `roles` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=3; + +-- +-- AUTO_INCREMENT for table `scholarships` +-- +ALTER TABLE `scholarships` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=8; + +-- +-- AUTO_INCREMENT for table `users` +-- +ALTER TABLE `users` + MODIFY `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, AUTO_INCREMENT=86; + +-- +-- Constraints for dumped tables +-- + +-- +-- Constraints for table `alumnis` +-- +ALTER TABLE `alumnis` + ADD CONSTRAINT `alumnis_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE; + +-- +-- Constraints for table `exports` +-- +ALTER TABLE `exports` + ADD CONSTRAINT `exports_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE; + +-- +-- Constraints for table `failed_import_rows` +-- +ALTER TABLE `failed_import_rows` + ADD CONSTRAINT `failed_import_rows_import_id_foreign` FOREIGN KEY (`import_id`) REFERENCES `imports` (`id`) ON DELETE CASCADE; + +-- +-- Constraints for table `forum_replies` +-- +ALTER TABLE `forum_replies` + ADD CONSTRAINT `forum_replies_topic_id_foreign` FOREIGN KEY (`topic_id`) REFERENCES `forum_topics` (`id`) ON DELETE CASCADE, + ADD CONSTRAINT `forum_replies_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE; + +-- +-- Constraints for table `forum_topics` +-- +ALTER TABLE `forum_topics` + ADD CONSTRAINT `forum_topics_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE; + +-- +-- Constraints for table `imports` +-- +ALTER TABLE `imports` + ADD CONSTRAINT `imports_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE; + +-- +-- Constraints for table `internships` +-- +ALTER TABLE `internships` + ADD CONSTRAINT `internships_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE; + +-- +-- Constraints for table `job_opportunities` +-- +ALTER TABLE `job_opportunities` + ADD CONSTRAINT `job_opportunities_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE; + +-- +-- Constraints for table `model_has_permissions` +-- +ALTER TABLE `model_has_permissions` + ADD CONSTRAINT `model_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE; + +-- +-- Constraints for table `model_has_roles` +-- +ALTER TABLE `model_has_roles` + ADD CONSTRAINT `model_has_roles_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE; + +-- +-- Constraints for table `questionnaire_questions` +-- +ALTER TABLE `questionnaire_questions` + ADD CONSTRAINT `questionnaire_questions_questionnaire_id_foreign` FOREIGN KEY (`questionnaire_id`) REFERENCES `questionnaires` (`id`) ON DELETE CASCADE; + +-- +-- Constraints for table `questionnaire_responses` +-- +ALTER TABLE `questionnaire_responses` + ADD CONSTRAINT `questionnaire_responses_questionnaire_id_foreign` FOREIGN KEY (`questionnaire_id`) REFERENCES `questionnaires` (`id`) ON DELETE CASCADE, + ADD CONSTRAINT `questionnaire_responses_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE; + +-- +-- Constraints for table `response_answers` +-- +ALTER TABLE `response_answers` + ADD CONSTRAINT `response_answers_question_id_foreign` FOREIGN KEY (`question_id`) REFERENCES `questionnaire_questions` (`id`) ON DELETE CASCADE, + ADD CONSTRAINT `response_answers_response_id_foreign` FOREIGN KEY (`response_id`) REFERENCES `questionnaire_responses` (`id`) ON DELETE CASCADE; + +-- +-- Constraints for table `role_has_permissions` +-- +ALTER TABLE `role_has_permissions` + ADD CONSTRAINT `role_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE, + ADD CONSTRAINT `role_has_permissions_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE; + +-- +-- Constraints for table `scholarships` +-- +ALTER TABLE `scholarships` + ADD CONSTRAINT `scholarships_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE; +COMMIT; + +/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; +/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */; +/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; diff --git a/infra/docker/elysia.Dockerfile b/infra/docker/elysia.Dockerfile new file mode 100644 index 0000000..a7dae84 --- /dev/null +++ b/infra/docker/elysia.Dockerfile @@ -0,0 +1,26 @@ +# build stage +FROM oven/bun:1-alpine AS builder +WORKDIR /app + +# install dependencies with cache mounts +COPY apps/elysia/package.json apps/elysia/bun.lock ./ +RUN --mount=type=cache,target=/root/.bun/install/cache \ + bun install --frozen-lockfile + +# build the application +COPY apps/elysia ./ +RUN bun run build + +# runtime stage +FROM oven/bun:1-distroless +WORKDIR /app + +# copy build artifacts +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/package.json ./ + +# distroless uses nonroot user (UID 65532) by default, or we can use it +USER nonroot + +EXPOSE 4092 +CMD ["run", "dist/index.js"] diff --git a/infra/docker/react-server.js b/infra/docker/react-server.js new file mode 100644 index 0000000..674b7ca --- /dev/null +++ b/infra/docker/react-server.js @@ -0,0 +1,75 @@ +import { existsSync, readFileSync } from "node:fs" +import { resolve, sep } from "node:path" + +const distDir = resolve("./dist") +const indexPath = resolve(distDir, "index.html") + +const contentTypes = { + html: "text/html; charset=utf-8", + css: "text/css; charset=utf-8", + js: "application/javascript; charset=utf-8", + json: "application/json; charset=utf-8", + svg: "image/svg+xml", + png: "image/png", + jpg: "image/jpeg", + jpeg: "image/jpeg", + gif: "image/gif", + webp: "image/webp", + ico: "image/x-icon", + woff: "font/woff", + woff2: "font/woff2", + ttf: "font/ttf", +} + +function responseFromFile(filePath, headers) { + try { + return new Response(readFileSync(filePath), { headers }) + } catch (error) { + if (error?.code === "ENOENT") { + return new Response("Not Found", { status: 404 }) + } + + console.error("File read error:", error) + return new Response("Internal Server Error", { status: 500 }) + } +} + +function serveIndex() { + return responseFromFile(indexPath, { + "Content-Type": contentTypes.html, + "Cache-Control": "no-cache", + }) +} + +function serveFile(filePath, pathname) { + const ext = filePath.split(".").pop() || "" + return responseFromFile(filePath, { + "Content-Type": contentTypes[ext] || "application/octet-stream", + "Cache-Control": pathname.startsWith("/assets/") ? "public, max-age=31536000, immutable" : "no-cache", + }) +} + +Bun.serve({ + port: 80, + hostname: "0.0.0.0", + fetch(req) { + const url = new URL(req.url) + const pathname = url.pathname + const filePath = resolve(distDir, pathname.slice(1)) + const insideDist = filePath === distDir || filePath.startsWith(`${distDir}${sep}`) + + if (!insideDist) { + return new Response("Forbidden", { status: 403 }) + } + + if (pathname !== "/" && existsSync(filePath)) { + return serveFile(filePath, pathname) + } + + if (!pathname.includes(".") && existsSync(indexPath)) { + return serveIndex() + } + + return new Response("Not Found", { status: 404 }) + }, +}) diff --git a/infra/docker/react.Dockerfile b/infra/docker/react.Dockerfile new file mode 100644 index 0000000..c9761cc --- /dev/null +++ b/infra/docker/react.Dockerfile @@ -0,0 +1,16 @@ +# ─── Stage 1: Build ───────────────────────────────────────────────────────── +FROM oven/bun:1 AS builder +WORKDIR /app +COPY apps/react/package.json apps/react/bun.lock ./ +RUN bun install --frozen-lockfile +COPY apps/react . +RUN bun run build + +# ─── Stage 2: Runtime (Bun static server) ────────────────────────────────── +FROM oven/bun:1-alpine +WORKDIR /app +COPY infra/docker/react-server.js ./server.js +COPY --from=builder /app/dist ./dist + +EXPOSE 80 +CMD ["bun", "server.js"] \ No newline at end of file diff --git a/infra/docker/rust.Dockerfile b/infra/docker/rust.Dockerfile new file mode 100644 index 0000000..a087171 --- /dev/null +++ b/infra/docker/rust.Dockerfile @@ -0,0 +1,42 @@ +# Use cargo-chef for dependency caching +FROM lukemathwalker/cargo-chef:latest-rust-1.89.0 AS chef +WORKDIR /app + +FROM chef AS planner +COPY apps/rust-auth . +RUN cargo chef prepare --recipe-path recipe.json + +FROM chef AS builder +COPY --from=planner /app/recipe.json recipe.json +# Utilize buildkit cache mounts for cargo +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/app/target \ + cargo chef cook --release --recipe-path recipe.json + +# Build application +COPY apps/rust-auth . +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/app/target \ + cargo build --release && \ + cp target/release/rust-auth /app/rust-auth + +# Final runtime image +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/* + +# Add non-root user +RUN groupadd -g 1001 appgroup && \ + useradd -u 1001 -g appgroup -s /bin/sh appuser + +WORKDIR /app +COPY --from=builder /app/rust-auth /app/rust-auth + +# Run as non-root +USER appuser + +EXPOSE 3000 +CMD ["./rust-auth"] diff --git a/infra/docker/scraper.Dockerfile b/infra/docker/scraper.Dockerfile new file mode 100644 index 0000000..6ba0634 --- /dev/null +++ b/infra/docker/scraper.Dockerfile @@ -0,0 +1,50 @@ +# Use cargo-chef for dependency caching +FROM lukemathwalker/cargo-chef:latest-rust-1.89.0 AS chef +WORKDIR /app + +# Install Node.js if needed for build scripts +RUN apt-get update && apt-get install -y --no-install-recommends \ + nodejs \ + && rm -rf /var/lib/apt/lists/* + +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 +# Utilize buildkit cache mounts for cargo +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/app/target \ + cargo chef cook --release --recipe-path recipe.json + +# Build application +COPY apps/scraper . +RUN --mount=type=cache,target=/usr/local/cargo/registry \ + --mount=type=cache,target=/app/target \ + cargo build --release && \ + cp target/release/scraper /app/scraper + +# Final runtime image +FROM debian:bookworm-slim AS runtime +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + libssl3 \ + chromium \ + fonts-liberation \ + fonts-noto-color-emoji \ + && rm -rf /var/lib/apt/lists/* + +# Add non-root user +RUN groupadd -g 1001 appgroup && \ + useradd -u 1001 -g appgroup -s /bin/sh appuser + +WORKDIR /app +COPY --from=builder /app/scraper /app/scraper + +# Run as non-root +USER appuser + +EXPOSE 4091 +CMD ["./scraper"] diff --git a/infra/traefik/TRAEFIK_ENV_CONFIG.md b/infra/traefik/TRAEFIK_ENV_CONFIG.md new file mode 100644 index 0000000..9d1b952 --- /dev/null +++ b/infra/traefik/TRAEFIK_ENV_CONFIG.md @@ -0,0 +1,61 @@ +# Traefik Environment Configuration + +This document describes environment variables used to configure Traefik certificate and config paths in production deployments. + +## Certificate Path Environment Variables + +All certificate paths support environment variable substitution with safe fallback defaults. This allows flexible certificate management across different deployment environments without modifying compose files. + +### Configuration Variables + +| Variable | Description | Default Path | Purpose | +| ------------------------------------- | --------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------- | +| `TRAEFIK_CONFIG_PATH` | Directory containing dynamic Traefik configuration files (YAML) | `/root/asepharyana-hub/infra/traefik/dynamic` | Location of middleware, router, and service definitions | +| `TRAEFIK_CERT_ASEPHARYANA_MY_ID_PEM` | Certificate file for asepharyana.my.id | `/root/asepharyana.my.id.pem` | SSL/TLS certificate for asepharyana.my.id domain | +| `TRAEFIK_CERT_ASEPHARYANA_MY_ID_KEY` | Key file for asepharyana.my.id | `/root/asepharyana.my.id.key` | SSL/TLS private key for asepharyana.my.id domain | +| `TRAEFIK_CERT_ASEPHARYANA_WEB_ID_PEM` | Certificate file for asepharyana.web.id | `/root/asepharyana.web.id.pem` | SSL/TLS certificate for asepharyana.web.id domain | +| `TRAEFIK_CERT_ASEPHARYANA_WEB_ID_KEY` | Key file for asepharyana.web.id | `/root/asepharyana.web.id.key` | SSL/TLS private key for asepharyana.web.id domain | + +## Usage + +### Default Behavior (Production) + +If no environment variables are set, Traefik will use the default paths shown above. This is suitable for production deployments where certificates are installed at these standard locations. + +```bash +docker compose -f infra/compose/traefik.yml up -d +``` + +### Custom Paths (Custom Deployments) + +To override paths for a custom deployment, set environment variables before starting services: + +```bash +export TRAEFIK_CONFIG_PATH=/etc/traefik/custom-dynamic + +docker compose -f infra/compose/traefik.yml up -d +``` + +### Via .env File + +Create or update your `.env` file in the deployment directory: + +```env +TRAEFIK_CONFIG_PATH=/root/asepharyana-hub/infra/traefik/dynamic +TRAEFIK_CERT_ASEPHARYANA_MY_ID_PEM=/root/asepharyana.my.id.pem +TRAEFIK_CERT_ASEPHARYANA_MY_ID_KEY=/root/asepharyana.my.id.key +TRAEFIK_CERT_ASEPHARYANA_WEB_ID_PEM=/root/asepharyana.web.id.pem +TRAEFIK_CERT_ASEPHARYANA_WEB_ID_KEY=/root/asepharyana.web.id.key +``` + +Then deploy: + +```bash +docker compose --env-file .env -f infra/compose/traefik.yml up -d +``` + +## Notes + +- All certificate paths use read-only mounts (`:ro`) for security +- If a certificate file is missing at the specified path, Docker volume mounting will failβ€”ensure certificates exist before starting Traefik +- The dynamic configuration directory must contain valid YAML files for Traefik to load properly diff --git a/infra/traefik/dynamic/apps.yaml b/infra/traefik/dynamic/apps.yaml new file mode 100644 index 0000000..2720dff --- /dev/null +++ b/infra/traefik/dynamic/apps.yaml @@ -0,0 +1,57 @@ +http: + routers: + # ── React SPA (domain root) ── + react: + rule: 'Host(`asepharyana.my.id`) || Host(`asepharyana.web.id`)' + entryPoints: + - websecure + tls: {} + service: react-service + + # ── Scraper API ── + scraper: + rule: 'Host(`scraper.asepharyana.my.id`) || Host(`api.asepharyana.my.id`) || Host(`scraper.asepharyana.web.id`) || Host(`api.asepharyana.web.id`)' + entryPoints: + - websecure + tls: {} + middlewares: + - common-chain@file + service: scraper-service + + # ── Elysia API ── + elysia: + rule: 'Host(`elysia.asepharyana.my.id`) || Host(`elysia.asepharyana.web.id`)' + entryPoints: + - websecure + tls: {} + middlewares: + - common-chain@file + service: elysia-service + + # ── Rust Auth API ── + rust-auth: + rule: 'Host(`auth.asepharyana.my.id`) || Host(`auth.asepharyana.web.id`)' + entryPoints: + - websecure + tls: {} + middlewares: + - common-chain@file + service: rust-auth-service + + services: + react-service: + loadBalancer: + servers: + - url: 'http://react-web:80' + scraper-service: + loadBalancer: + servers: + - url: 'http://scraper-api:4091' + elysia-service: + loadBalancer: + servers: + - url: 'http://elysia-api:4092' + rust-auth-service: + loadBalancer: + servers: + - url: 'http://rust-auth:3000' diff --git a/infra/traefik/dynamic/middlewares.yaml b/infra/traefik/dynamic/middlewares.yaml new file mode 100644 index 0000000..a8b6a87 --- /dev/null +++ b/infra/traefik/dynamic/middlewares.yaml @@ -0,0 +1,74 @@ +http: + middlewares: + secure-headers: + headers: + sslRedirect: true + forceSTSHeader: true + stsSeconds: 31536000 + stsIncludeSubdomains: true + stsPreload: true + frameDeny: true + contentTypeNosniff: true + browserXSSFilter: true + referrerPolicy: 'same-origin' + customResponseHeaders: + X-Content-Type-Options: 'nosniff' + X-Frame-Options: 'DENY' + X-XSS-Protection: '1; mode=block' + Referrer-Policy: 'same-origin' + Permissions-Policy: 'geolocation=(), microphone=(), camera=()' + compress: + compress: + minResponseBodyBytes: 256 + excludedContentTypes: + - 'image/*' + - 'application/octet-stream' + retry: + retry: + attempts: 3 + rate-limit: + rateLimit: + average: 100 + burst: 50 + buffer: + buffering: + maxRequestBodyBytes: 10485760 + maxResponseBodyBytes: 10485760 + memRequestBodyBytes: 1048576 + memResponseBodyBytes: 1048576 + admin-chain: + chain: + middlewares: + - secure-headers + - compress + - retry + + # ── Useful Plugins ── + real-ip: + plugin: + real-ip: + excludednetworks: + - '127.0.0.1/32' + realipheader: 'CF-Connecting-IP' + + block-sensitive-paths: + plugin: + blockpath: + regex: + - "^/\\.env" + - "^/\\.git" + - '^/wp-admin' + - "^/wp-login\\.php" + - "^/config\\.php" + + # ── Common Chain ── + common-chain: + chain: + middlewares: + # - real-ip + # - block-sensitive-paths + - secure-headers + - compress + - retry + - rate-limit + - buffer diff --git a/infra/traefik/dynamic/ssl.yaml b/infra/traefik/dynamic/ssl.yaml new file mode 100644 index 0000000..f861fef --- /dev/null +++ b/infra/traefik/dynamic/ssl.yaml @@ -0,0 +1,14 @@ +tls: + certificates: + # Legacy production layout: asephstech.pem is paired with asephscloud.key. + - certFile: /etc/traefik/certs/asephstech.pem + keyFile: /etc/traefik/certs/asephscloud.key + - certFile: /etc/traefik/certs/asepharyana.my.id.pem + keyFile: /etc/traefik/certs/asepharyana.my.id.key + - certFile: /etc/traefik/certs/asepharyana.web.id.pem + keyFile: /etc/traefik/certs/asepharyana.web.id.key + stores: + default: + defaultCertificate: + certFile: /etc/traefik/certs/asephstech.pem + keyFile: /etc/traefik/certs/asephscloud.key diff --git a/infra/traefik/traefik.yaml b/infra/traefik/traefik.yaml new file mode 100644 index 0000000..4f0c45b --- /dev/null +++ b/infra/traefik/traefik.yaml @@ -0,0 +1,29 @@ +api: + dashboard: true + insecure: true + +log: + level: INFO + format: json + +accessLog: {} + +entryPoints: + web: + address: ':80' + http: + redirections: + entryPoint: + to: websecure + scheme: https + websecure: + address: ':443' + +providers: + docker: + endpoint: 'unix:///var/run/docker.sock' + exposedByDefault: false + network: app-shared-net + file: + directory: /etc/traefik/dynamic + watch: true diff --git a/package.json b/package.json new file mode 100644 index 0000000..f88625b --- /dev/null +++ b/package.json @@ -0,0 +1,13 @@ +{ + "name": "asepharyana-hub", + "private": true, + "scripts": { + "lint": "eslint .", + "format": "prettier --write .", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "devDependencies": { + "prettier": "^3.4.2", + "eslint": "^9.18.0" + } +} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..84ada57 --- /dev/null +++ b/renovate.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["config:recommended"], + "git-submodules": { + "enabled": true + } +} diff --git a/result b/result new file mode 120000 index 0000000..a7a5306 --- /dev/null +++ b/result @@ -0,0 +1 @@ +/nix/store/a1vm413z2gayp4dfj6fvcnsp880bx08g-apps-leptos-0.1.0 \ No newline at end of file diff --git a/scripts/2updateenv.sh b/scripts/2updateenv.sh new file mode 100644 index 0000000..6d08cfb --- /dev/null +++ b/scripts/2updateenv.sh @@ -0,0 +1,23 @@ +#!/bin/bash +# Copy .env from root to all direct subprojects/packages (not nested, not build, not .git, not .github, not node_modules, not .turbo, not .next, not .vscode, not dist, not public, not src, not coverage, not logs, not .devcontainer, not .yarn, not target) +# Do NOT copy to /apps or /packages root, only to their subfolders. + +ROOT_ENV="./.env" + +if [ ! -f "$ROOT_ENV" ]; then + echo "Root .env file not found at $ROOT_ENV" + exit 1 +fi + +for parent in apps packages; do + for dir in ./$parent/*/; do + # Remove trailing slash + dir="${dir%/}" + base=$(basename "$dir") + if [[ "$base" =~ ^(\.git|\.github|node_modules|\.turbo|\.next|\.vscode|dist|public|src|coverage|logs|\.devcontainer|\.yarn|target)$ ]]; then + continue + fi + cp "$ROOT_ENV" "$dir/.env" + echo "Copied .env to $dir/.env" + done +done diff --git a/scripts/clean-node-modules.ps1 b/scripts/clean-node-modules.ps1 new file mode 100644 index 0000000..1489473 --- /dev/null +++ b/scripts/clean-node-modules.ps1 @@ -0,0 +1,169 @@ +<# +.SYNOPSIS + Clean all node_modules directories in the repository with optional cache and lockfile cleanup. + +.DESCRIPTION + This script recursively finds and removes all 'node_modules' directories starting at the repo root. + Optionally, it can also remove build caches and lockfiles, and run 'pnpm store prune'. + +.PARAMETER IncludeCache + Also remove common cache/build output directories (e.g., .next, .turbo, .vite, node_modules/.cache, dist, build, coverage, out, storybook-static). + +.PARAMETER IncludeLock + Also remove lock files (pnpm-lock.yaml, package-lock.json, yarn.lock) in the repo. + +.PARAMETER PruneStore + After deletion, try to run 'pnpm store prune' if pnpm is installed. + +.PARAMETER Yes + Proceed without interactive confirmation (non-interactive mode). + +.PARAMETER DryRun + Show what would be removed without deleting anything. + +.EXAMPLE + # Preview what will be removed + ./scripts/clean-node-modules.ps1 -DryRun + +.EXAMPLE + # Clean node_modules only, no prompt + ./scripts/clean-node-modules.ps1 -Yes + +.EXAMPLE + # Deep clean including caches and lockfiles, and prune pnpm store + ./scripts/clean-node-modules.ps1 -IncludeCache -IncludeLock -PruneStore -Yes +#> + +[CmdletBinding(SupportsShouldProcess=$true)] +param( + [switch] $IncludeCache, + [switch] $IncludeLock, + [switch] $PruneStore, + [switch] $Yes, + [switch] $DryRun +) + +$ErrorActionPreference = 'Stop' + +function Write-Section($text) { + Write-Host "`n==== $text ====\n" -ForegroundColor Cyan +} + +function Safe-RemoveDirectory { + param( + [Parameter(Mandatory=$true)][string] $Path, + [switch] $Preview + ) + if (-not (Test-Path -LiteralPath $Path)) { return } + if ($Preview) { Write-Host "[dir] $Path"; return } + try { + # Use cmd rmdir for better handling of read-only/long paths on Windows PowerShell 5.1 + & cmd.exe /c "rmdir /s /q \"$Path\"" | Out-Null + } catch { + try { Remove-Item -LiteralPath $Path -Recurse -Force -ErrorAction Stop } catch { + Write-Warning "Failed to remove directory: $Path -> $($_.Exception.Message)" + } + } +} + +function Safe-RemoveFile { + param( + [Parameter(Mandatory=$true)][string] $Path, + [switch] $Preview + ) + if (-not (Test-Path -LiteralPath $Path)) { return } + if ($Preview) { Write-Host "[file] $Path"; return } + try { Remove-Item -LiteralPath $Path -Force -ErrorAction Stop } catch { + Write-Warning "Failed to remove file: $Path -> $($_.Exception.Message)" + } +} + +# Resolve repo root (this script lives in ./scripts) +$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path +Set-Location -LiteralPath $RepoRoot + +Write-Host "Repo root: $RepoRoot" -ForegroundColor DarkGray + +# Accumulators +$DirsToDelete = New-Object System.Collections.Generic.List[string] +$FilesToDelete = New-Object System.Collections.Generic.List[string] + +# 1) node_modules everywhere (including root) +Write-Section 'Scanning node_modules directories' +$nodeModulesDirs = Get-ChildItem -LiteralPath $RepoRoot -Directory -Recurse -Force -ErrorAction SilentlyContinue | + Where-Object { $_.Name -eq 'node_modules' } + +# Ensure root node_modules is included if present +$rootNM = Join-Path $RepoRoot 'node_modules' +if (Test-Path -LiteralPath $rootNM) { + $DirsToDelete.Add($rootNM) +} +foreach ($d in $nodeModulesDirs) { + if (-not $DirsToDelete.Contains($d.FullName)) { $DirsToDelete.Add($d.FullName) } +} +Write-Host ("Found {0} node_modules directory(ies)" -f $DirsToDelete.Count) + +# 2) Optional caches/output folders +if ($IncludeCache) { + Write-Section 'Scanning cache/output directories' + $cacheNames = @( + '.next', '.turbo', '.vite', '.parcel-cache', '.cache', + 'dist', 'build', 'coverage', 'out', 'storybook-static', + '.wrangler' + ) + # node_modules/.cache is common; include it via name match too + $allDirs = Get-ChildItem -LiteralPath $RepoRoot -Directory -Recurse -Force -ErrorAction SilentlyContinue + foreach ($dir in $allDirs) { + if ($cacheNames -contains $dir.Name) { + if (-not $DirsToDelete.Contains($dir.FullName)) { $DirsToDelete.Add($dir.FullName) } + } + } + Write-Host ("Found {0} cache/output directory(ies)" -f ($DirsToDelete | Where-Object { Test-Path $_ }).Count) +} + +# 3) Optional lockfiles +if ($IncludeLock) { + Write-Section 'Scanning lock files' + $lockGlobs = @('pnpm-lock.yaml', 'package-lock.json', 'yarn.lock') + foreach ($glob in $lockGlobs) { + $files = Get-ChildItem -LiteralPath $RepoRoot -Recurse -Force -File -Filter $glob -ErrorAction SilentlyContinue + foreach ($f in $files) { if (-not $FilesToDelete.Contains($f.FullName)) { $FilesToDelete.Add($f.FullName) } } + } + Write-Host ("Found {0} lock file(s)" -f $FilesToDelete.Count) +} + +# 4) Summary +Write-Section 'Summary' +Write-Host ("Directories to delete: {0}" -f $DirsToDelete.Count) +Write-Host ("Files to delete: {0}" -f $FilesToDelete.Count) + +$preview = $DryRun -or (-not $Yes) +if ($preview) { + Write-Host "Preview mode (no deletions). Use -Yes to confirm, or pass -DryRun:$false to hide this list." -ForegroundColor Yellow + foreach ($dir in $DirsToDelete) { Safe-RemoveDirectory -Path $dir -Preview } + foreach ($fil in $FilesToDelete) { Safe-RemoveFile -Path $fil -Preview } + if (-not $Yes) { Write-Host "\nRun again with -Yes to confirm deletion." -ForegroundColor Yellow } + exit 0 +} + +# 5) Deletion +Write-Section 'Deleting directories' +foreach ($dir in $DirsToDelete) { Safe-RemoveDirectory -Path $dir } + +if ($FilesToDelete.Count -gt 0) { + Write-Section 'Deleting files' + foreach ($fil in $FilesToDelete) { Safe-RemoveFile -Path $fil } +} + +# 6) Optional pnpm store prune +if ($PruneStore) { + Write-Section 'Pruning pnpm store' + $pnpm = Get-Command pnpm -ErrorAction SilentlyContinue + if ($null -ne $pnpm) { + try { & pnpm store prune } catch { Write-Warning "pnpm store prune failed: $($_.Exception.Message)" } + } else { + Write-Warning "pnpm not found on PATH; skipping 'pnpm store prune'." + } +} + +Write-Host "\nDone." -ForegroundColor Green diff --git a/scripts/clean-node-modules.sh b/scripts/clean-node-modules.sh new file mode 100644 index 0000000..129825a --- /dev/null +++ b/scripts/clean-node-modules.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Clean all node_modules directories with optional cache & lockfile cleanup. +# Usage: +# ./scripts/clean-node-modules.sh [--include-cache] [--include-lock] [--prune-store] [--yes] [--dry-run] + +INCLUDE_CACHE=false +INCLUDE_LOCK=false +PRUNE_STORE=false +YES=false +DRY_RUN=false + +for arg in "$@"; do + case "$arg" in + --include-cache) INCLUDE_CACHE=true ;; + --include-lock) INCLUDE_LOCK=true ;; + --prune-store) PRUNE_STORE=true ;; + --yes) YES=true ;; + --dry-run) DRY_RUN=true ;; + *) echo "Unknown option: $arg"; exit 2 ;; + esac +done + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +preview() { + if [[ "$DRY_RUN" == true || "$YES" == false ]]; then echo 1; else echo 0; fi +} + +section() { echo -e "\n==== $* ====\n"; } + +DIRS_TO_DELETE=() +FILES_TO_DELETE=() + +section "Scanning node_modules directories" +while IFS= read -r -d '' d; do DIRS_TO_DELETE+=("$d"); done < <(find "$REPO_ROOT" -type d -name node_modules -print0) +if [[ -d "$REPO_ROOT/node_modules" ]]; then DIRS_TO_DELETE+=("$REPO_ROOT/node_modules"); fi + +section "Summary so far" +echo "Found ${#DIRS_TO_DELETE[@]} node_modules directories" + +if [[ "$INCLUDE_CACHE" == true ]]; then + section "Scanning cache/output directories" + while IFS= read -r -d '' d; do DIRS_TO_DELETE+=("$d"); done < <(find "$REPO_ROOT" -type d \( \ + -name .next -o -name .turbo -o -name .vite -o -name .parcel-cache -o -name .cache -o \ + -name dist -o -name build -o -name coverage -o -name out -o -name storybook-static -o -name .wrangler \ + \) -print0) +fi + +if [[ "$INCLUDE_LOCK" == true ]]; then + section "Scanning lock files" + while IFS= read -r -d '' f; do FILES_TO_DELETE+=("$f"); done < <(\ + find "$REPO_ROOT" -type f \( -name pnpm-lock.yaml -o -name package-lock.json -o -name yarn.lock \) -print0) +fi + +section "Summary" +echo "Directories to delete: ${#DIRS_TO_DELETE[@]}" +echo "Files to delete: ${#FILES_TO_DELETE[@]}" + +if [[ $(preview) -eq 1 ]]; then + echo "Preview mode (no deletions). Re-run with --yes to confirm." + for d in "${DIRS_TO_DELETE[@]}"; do echo "[dir] $d"; done + for f in "${FILES_TO_DELETE[@]}"; do echo "[file] $f"; done + exit 0 +fi + +section "Deleting directories" +for d in "${DIRS_TO_DELETE[@]}"; do rm -rf -- "$d" || true; done + +if [[ ${#FILES_TO_DELETE[@]} -gt 0 ]]; then + section "Deleting files" + for f in "${FILES_TO_DELETE[@]}"; do rm -f -- "$f" || true; done +fi + +if [[ "$PRUNE_STORE" == true ]]; then + section "Pruning pnpm store" + if command -v pnpm >/dev/null 2>&1; then pnpm store prune || true; else echo "pnpm not found; skipping"; fi +fi + +echo "Done." diff --git a/scripts/cleanup-ghcr.sh b/scripts/cleanup-ghcr.sh new file mode 100644 index 0000000..0041247 --- /dev/null +++ b/scripts/cleanup-ghcr.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +# Cleanup script for old GitHub Container Registry (GHCR) images +# This script uses the 'gh' CLI to delete old package versions. +# Requires 'gh' CLI to be installed and authenticated with 'delete:packages' scope. + +set -e + +# Configuration +ORG="asepharyana" +PACKAGE_NAMES=("rust-api" "elysia-api" "nextjs-web") + +echo "πŸš€ Starting GHCR cleanup for $ORG..." + +for PACKAGE in "${PACKAGE_NAMES[@]}"; do + echo "------------------------------------------------" + echo "πŸ“¦ Checking package: $PACKAGE" + + # List versions that are NOT 'latest' and DON'T match the current SHAs + # This is a safe approach: list all versions and let the user decide or + # filter by date/tag patterns. + + # For simplicity and safety, this script will list versions and + # provide the command to delete them. + # To AUTOMATICALLY delete, uncomment the 'gh api' call below. + + echo "πŸ” Fetching versions..." + VERSIONS=$(gh api "/orgs/$ORG/packages/container/$PACKAGE/versions" --paginate -q '.[] | "\(.id) \(.metadata.container.tags[0] // "no-tag") \(.updated_at)"') + + if [ -z "$VERSIONS" ]; then + echo "βœ… No versions found for $PACKAGE" + continue + fi + + echo "$VERSIONS" | while read -r ID TAG DATE; do + if [[ "$TAG" == "latest" ]]; then + echo "✨ Skipping latest: $ID ($DATE)" + continue + fi + + # Example: only delete if the tag doesn't start with 'sha-' (adjust as needed) + # Or delete very old ones. + + echo "πŸ—‘οΈ Found old version: $ID | Tag: $TAG | Date: $DATE" + + # UNCOMMENT THE LINE BELOW TO ENABLE AUTOMATIC DELETION + # gh api -X DELETE "/orgs/$ORG/packages/container/$PACKAGE/versions/$ID" + # echo "βœ… Deleted $ID" + done +done + +echo "------------------------------------------------" +echo "βœ… Cleanup script finished." +echo "πŸ’‘ Note: Deletion is commented out by default for safety. Edit the script to enable it." diff --git a/scripts/git-hooks/setup-hooks.sh b/scripts/git-hooks/setup-hooks.sh new file mode 100755 index 0000000..6351754 --- /dev/null +++ b/scripts/git-hooks/setup-hooks.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "=== Setting up Git hooks ===" +REPO_ROOT=$(git rev-parse --show-toplevel) + +cat <&2 + exit 1 + } + + # Heuristic check: Ensure the manifest actually declares dependencies before thrashing the disk with ncu. + if ! grep -Eq '"(dependencies|devDependencies|peerDependencies)"[[:space:]]*:' package.json; then + echo "Notice: No valid dependency blocks detected in $dir/package.json. Bypassing node traversal." + exit 0 + fi + + # Delegate command resolution to bunx. This obliterates the 'command not found' failure mode + # by dynamically sourcing the npm-check-updates binary, completely ignoring global namespace pollution. + echo "Executing constraint-free updates via bunx..." + bunx --bun npm-check-updates -u + + echo "Commencing rigorous package installation phase..." + bun install + ) || { + echo "WARNING: Subshell failure encountered in $dir. Subsystem continues." >&2 + } +done + +echo "--------------------------------------------------------------------------------" +echo "SYSTEM STATE: All traversable dependencies aggressively updated." diff --git a/setup_prometheus.sh b/setup_prometheus.sh new file mode 100644 index 0000000..3fde927 --- /dev/null +++ b/setup_prometheus.sh @@ -0,0 +1,87 @@ +#!/bin/bash +set -euo pipefail + +PROMETHEUS_VERSION="2.53.0" +TAILSCALE_IP="100.108.1.124" +PROMETHEUS_DIR="/etc/prometheus" +PROMETHEUS_DATA_DIR="/var/lib/prometheus" + +echo "Creating prometheus user and group..." +if ! getent group prometheus >/dev/null; then + groupadd --system prometheus +fi + +if ! getent passwd prometheus >/dev/null; then + useradd --system -g prometheus --no-create-home --shell /usr/sbin/nologin prometheus +fi + +echo "Creating directories..." +mkdir -p "$PROMETHEUS_DIR" "$PROMETHEUS_DATA_DIR" +chown prometheus:prometheus "$PROMETHEUS_DIR" "$PROMETHEUS_DATA_DIR" + +echo "Downloading Prometheus v${PROMETHEUS_VERSION}..." +cd /tmp +wget -qO prometheus.tar.gz "https://github.com/prometheus/prometheus/releases/download/v${PROMETHEUS_VERSION}/prometheus-${PROMETHEUS_VERSION}.linux-amd64.tar.gz" + +echo "Extracting Prometheus..." +tar -xf prometheus.tar.gz +cd "prometheus-${PROMETHEUS_VERSION}.linux-amd64" + +echo "Installing binaries..." +install -m 0755 -o prometheus -g prometheus prometheus promtool /usr/local/bin/ + +echo "Installing consoles and libraries..." +cp -a consoles console_libraries "$PROMETHEUS_DIR/" +chown -R prometheus:prometheus "$PROMETHEUS_DIR/consoles" "$PROMETHEUS_DIR/console_libraries" + +echo "Creating configuration file..." +cat < "${PROMETHEUS_DIR}/prometheus.yml" +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: "prometheus" + static_configs: + - targets: ["${TAILSCALE_IP}:9090"] +EOF +chown prometheus:prometheus "${PROMETHEUS_DIR}/prometheus.yml" +chmod 0644 "${PROMETHEUS_DIR}/prometheus.yml" + +echo "Creating systemd unit file..." +cat < /etc/systemd/system/prometheus.service +[Unit] +Description=Prometheus Time Series Collection and Processing Server +Wants=network-online.target +After=network-online.target + +[Service] +User=prometheus +Group=prometheus +Type=simple +ExecStart=/usr/local/bin/prometheus \\ + --config.file=${PROMETHEUS_DIR}/prometheus.yml \\ + --storage.tsdb.path=${PROMETHEUS_DATA_DIR} \\ + --web.console.templates=${PROMETHEUS_DIR}/consoles \\ + --web.console.libraries=${PROMETHEUS_DIR}/console_libraries \\ + --web.listen-address=${TAILSCALE_IP}:9090 \\ + --web.external-url=http://${TAILSCALE_IP}:9090/ +ExecReload=/bin/kill -HUP \$MAINPID +Restart=on-failure +RestartSec=5s +LimitNOFILE=65536 + +[Install] +WantedBy=multi-user.target +EOF +chmod 0644 /etc/systemd/system/prometheus.service + +echo "Reloading systemd, enabling and starting prometheus..." +systemctl daemon-reload +systemctl enable prometheus +systemctl restart prometheus + +echo "Checking Prometheus status..." +systemctl status prometheus --no-pager || true + +echo "Prometheus setup completed successfully!"