diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fad6efe --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.ai/ +.git/ +.github/ +.idea/ +coverage/ +dist/ +node_modules/ +npm-debug.log* +result.json diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f15441a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..ae1dcea --- /dev/null +++ b/.env.example @@ -0,0 +1,18 @@ +# Benchfinity local stack environment. +# Copy this file to .env and adjust the values before running: +# +# cp .env.example .env +# docker compose --profile full up +# +# .env is gitignored; never commit real credentials. + +# Postgres (required by the future backend) +POSTGRES_USER=benchfinity +POSTGRES_PASSWORD=change-me +POSTGRES_DB=benchfinity +DATABASE_URL=postgresql://benchfinity:change-me@postgres:5432/benchfinity + +# MinIO (optional object storage) +MINIO_ROOT_USER=benchfinity +MINIO_ROOT_PASSWORD=change-me-too +MINIO_ENDPOINT=http://minio:9000 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..092d3d3 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @BenchFinity/maintainers diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..0377373 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,45 @@ +name: Bug report +description: Report a reproducible problem in Benchfinity. +title: "Bug: " +labels: + - bug +body: + - type: textarea + id: summary + attributes: + label: Summary + description: What happened? + validations: + required: true + - type: textarea + id: steps + attributes: + label: Steps to reproduce + description: List the exact steps needed to reproduce the problem. + placeholder: | + 1. Open... + 2. Set... + 3. Export... + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + validations: + required: true + - type: input + id: browser + attributes: + label: Browser and OS + placeholder: "Chrome on macOS" + - type: textarea + id: notes + attributes: + label: Additional context diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..cbfacf5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: Security vulnerability + url: https://github.com/BenchFinity/workbench/security/advisories/new + about: Report security issues privately when private vulnerability reporting is available. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..ecbcc6f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,29 @@ +name: Feature request +description: Suggest an improvement or new Benchfinity capability. +title: "Feature: " +labels: + - enhancement +body: + - type: textarea + id: problem + attributes: + label: Problem or workflow + description: What user workflow should this improve? + validations: + required: true + - type: textarea + id: proposal + attributes: + label: Proposed solution + description: Describe the behavior you want. + validations: + required: true + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + - type: textarea + id: acceptance + attributes: + label: Acceptance criteria + description: What should be true when this is complete? diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..fc13ce3 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,26 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + groups: + npm-development: + dependency-type: development + npm-production: + dependency-type: production + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + - package-ecosystem: docker + directory: / + schedule: + interval: weekly + open-pull-requests-limit: 5 + groups: + docker-base-images: + patterns: + - "*" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..6d815cf --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,13 @@ +## Summary + +- + +## Validation + +- [ ] `npm run test` +- [ ] `npm run build` +- [ ] `npm audit` + +## Notes + +- diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..8affdde --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,38 @@ +# GitHub release note generation. +changelog: + exclude: + labels: + - ignore-for-release + - duplicate + - invalid + authors: + - dependabot + - dependabot[bot] + categories: + - title: Breaking Changes + labels: + - breaking-change + - breaking + - title: Features + labels: + - enhancement + - feature + - title: Fixes + labels: + - bug + - fix + - title: CI and Release + labels: + - ci + - release + - docker + - title: Documentation + labels: + - documentation + - docs + - title: Dependencies + labels: + - dependencies + - title: Other Changes + labels: + - "*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9adc48b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,354 @@ +name: CI + +on: + pull_request: + branches: + - develop + - main + - "release/**" + - "rc/**" + push: + branches: + - develop + - main + - "feature/**" + - "release/**" + - "rc/**" + workflow_dispatch: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + validate: + name: validate + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Format check + run: npm run format:check + + - name: Typecheck + run: npm run typecheck + + - name: Test + run: npm run test + + - name: Build + run: npm run build + + - name: Audit dependencies (block on high and above) + run: npm audit --audit-level=high + + dependency-review: + name: dependency-review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Dependency review (block on high and above) + uses: actions/dependency-review-action@v5 + with: + fail-on-severity: high + + image-scan: + name: image-scan + runs-on: ubuntu-latest + needs: validate + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build image for scanning + uses: docker/build-push-action@v7 + with: + context: . + load: true + tags: benchfinity:scan + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Scan image (block on fixable high and critical) + uses: aquasecurity/trivy-action@v0.36.0 + with: + image-ref: benchfinity:scan + severity: HIGH,CRITICAL + ignore-unfixed: true + vuln-type: os,library + exit-code: "1" + format: table + + docker: + name: docker + runs-on: ubuntu-latest + needs: [validate, image-scan] + if: github.event_name == 'push' + permissions: + contents: read + packages: write + outputs: + image: ${{ steps.meta.outputs.image }} + tags: ${{ steps.meta.outputs.tags }} + version: ${{ steps.meta.outputs.version }} + release_tag: ${{ steps.meta.outputs.release_tag }} + release_name: ${{ steps.meta.outputs.release_name }} + deploy_tag: ${{ steps.meta.outputs.deploy_tag }} + prerelease: ${{ steps.meta.outputs.prerelease }} + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Compute image tags + id: meta + shell: bash + run: | + version="$(node -p "require('./package.json').version")" + short_sha="${GITHUB_SHA::7}" + image="ghcr.io/${GITHUB_REPOSITORY,,}" + branch="${GITHUB_REF_NAME}" + release_tag="" + release_name="" + deploy_tag="" + prerelease="false" + + if [[ "${branch}" == "main" ]]; then + # Publish an immutable :- that production pins to, + # so every release changes CD desired state and ArgoCD rolls it out. + tags="${image}:${version}"$'\n'"${image}:${version}-${short_sha}" + deploy_tag="${version}-${short_sha}" + release_tag="v${version}" + release_name="Benchfinity v${version}" + elif [[ "${branch}" == "develop" ]]; then + tags="${image}:${version}-SNAPSHOT.${short_sha}"$'\n'"${image}:develop" + elif [[ "${branch}" == release/* || "${branch}" == rc/* ]]; then + tags="${image}:${version}-rc.${short_sha}" + release_tag="v${version}-rc.${short_sha}" + release_name="Benchfinity v${version} RC ${short_sha}" + prerelease="true" + elif [[ "${branch}" == feature/* ]]; then + safe_branch="$(printf '%s' "${branch#feature/}" | tr '[:upper:]' '[:lower:]' | sed -E 's#[^a-z0-9_.-]+#-#g; s#-+#-#g; s#(^-|-$)##g')" + tags="${image}:${version}-${safe_branch}-${short_sha}-SNAPSHOT" + else + safe_branch="$(printf '%s' "${branch}" | tr '[:upper:]' '[:lower:]' | sed -E 's#[^a-z0-9_.-]+#-#g; s#-+#-#g; s#(^-|-$)##g')" + tags="${image}:${version}-${safe_branch}-${short_sha}-SNAPSHOT" + fi + + { + echo "image=${image}" + echo "version=${version}" + echo "release_tag=${release_tag}" + echo "release_name=${release_name}" + echo "deploy_tag=${deploy_tag}" + echo "prerelease=${prerelease}" + echo "tags<> "${GITHUB_OUTPUT}" + + - name: Set up QEMU + uses: docker/setup-qemu-action@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Login to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push image + uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: | + org.opencontainers.image.title=Benchfinity + org.opencontainers.image.description=Gridfinity-compatible baseplate generator + org.opencontainers.image.source=https://github.com/${{ github.repository }} + org.opencontainers.image.revision=${{ github.sha }} + org.opencontainers.image.version=${{ steps.meta.outputs.version }} + cache-from: type=gha + cache-to: type=gha,mode=max + + release: + name: release + runs-on: ubuntu-latest + needs: docker + if: github.event_name == 'push' && needs.docker.outputs.release_tag != '' + permissions: + contents: write + steps: + - name: Publish GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ needs.docker.outputs.release_tag }} + RELEASE_NAME: ${{ needs.docker.outputs.release_name }} + PRERELEASE: ${{ needs.docker.outputs.prerelease }} + IMAGE: ${{ needs.docker.outputs.image }} + IMAGE_TAGS: ${{ needs.docker.outputs.tags }} + shell: bash + run: | + if gh release view "${TAG}" -R "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then + echo "Release ${TAG} already exists" + exit 0 + fi + + body="$(printf 'Container image:\n\n%s\n\nCommit: %s\n' "${IMAGE_TAGS}" "${GITHUB_SHA}")" + + gh api -X POST "repos/${GITHUB_REPOSITORY}/releases" \ + -f tag_name="${TAG}" \ + -f target_commitish="${GITHUB_SHA}" \ + -f name="${RELEASE_NAME}" \ + -f body="${body}" \ + -F prerelease="${PRERELEASE}" \ + -F generate_release_notes=true + + helm: + name: helm + runs-on: ubuntu-latest + needs: validate + if: github.event_name == 'push' + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Helm + uses: azure/setup-helm@v5 + + - name: Lint chart + run: helm lint deploy/helm/benchfinity + + - name: Template chart + run: helm template benchfinity deploy/helm/benchfinity > /dev/null + + - name: Compute chart version + id: chart + shell: bash + run: | + base="$(awk '/^version:/ { print $2; exit }' deploy/helm/benchfinity/Chart.yaml)" + short_sha="${GITHUB_SHA::7}" + branch="${GITHUB_REF_NAME}" + publish="false" + if [[ "${branch}" == "main" || "${GITHUB_REF}" == refs/tags/* ]]; then + version="${base}" + publish="true" + elif [[ "${branch}" == "develop" ]]; then + version="${base}-SNAPSHOT.${short_sha}" + publish="true" + else + version="${base}-${short_sha}" + fi + { + echo "version=${version}" + echo "publish=${publish}" + } >> "${GITHUB_OUTPUT}" + + - name: Package and push chart to GHCR + if: steps.chart.outputs.publish == 'true' + shell: bash + env: + CHART_VERSION: ${{ steps.chart.outputs.version }} + REGISTRY_USER: ${{ github.actor }} + REGISTRY_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + helm package deploy/helm/benchfinity \ + --version "${CHART_VERSION}" \ + --app-version "${CHART_VERSION}" + helm registry login ghcr.io -u "${REGISTRY_USER}" -p "${REGISTRY_TOKEN}" + helm push "benchfinity-${CHART_VERSION}.tgz" oci://ghcr.io/benchfinity/charts + + deploy: + name: deploy + runs-on: ubuntu-latest + needs: docker + # Only production (main) auto-deploys, once the immutable image is published. + # Bumps the pinned tag in Workbench-CD; ArgoCD (benchfinity-prod) reconciles + # it onto k8s-prod. Uses the SSH deploy key, not GITHUB_TOKEN. + if: github.event_name == 'push' && needs.docker.outputs.deploy_tag != '' + permissions: + contents: read + steps: + - name: Roll production by bumping Workbench-CD image tag + env: + DEPLOY_TAG: ${{ needs.docker.outputs.deploy_tag }} + DEPLOY_KEY: ${{ secrets.WORKBENCH_CD_DEPLOY_KEY }} + SOURCE_SHA: ${{ github.sha }} + CD_REPO: BenchFinity/Workbench-CD + CD_BRANCH: main + OVERLAY: overlays/production/kustomization.yaml + IMAGE_NAME: ghcr.io/benchfinity/workbench + shell: bash + run: | + set -euo pipefail + + if [[ -z "${DEPLOY_KEY}" ]]; then + echo "::error::WORKBENCH_CD_DEPLOY_KEY secret is not set; cannot push to ${CD_REPO}." \ + "Add a write deploy key to ${CD_REPO} and store its private key as that secret." + exit 1 + fi + + mkdir -p ~/.ssh + printf '%s\n' "${DEPLOY_KEY}" > ~/.ssh/cd_deploy_key + chmod 600 ~/.ssh/cd_deploy_key + ssh-keyscan -t ed25519,rsa github.com >> ~/.ssh/known_hosts 2>/dev/null + export GIT_SSH_COMMAND="ssh -i ~/.ssh/cd_deploy_key -o IdentitiesOnly=yes" + + git clone --depth 1 --branch "${CD_BRANCH}" "git@github.com:${CD_REPO}.git" cd-repo + cd cd-repo + + # DEPLOY_TAG and IMAGE_NAME are CI-derived (package.json version + commit + # SHA), not user-controlled input, so they are safe to pass to yq via env. + DEPLOY_TAG="${DEPLOY_TAG}" IMAGE_NAME="${IMAGE_NAME}" yq -i \ + '(.images[] | select(.name == strenv(IMAGE_NAME)) | .newTag) = strenv(DEPLOY_TAG)' \ + "${OVERLAY}" + + if git diff --quiet -- "${OVERLAY}"; then + echo "Production already pinned to ${DEPLOY_TAG}; nothing to deploy." + exit 0 + fi + + git config user.name "benchfinity-workbench-ci[bot]" + git config user.email "ci@benchfinity.com" + git add "${OVERLAY}" + git commit -m "deploy(workbench): roll production to ${DEPLOY_TAG}" \ + -m "Auto-bumped by Workbench CI from BenchFinity/Workbench@${SOURCE_SHA}. ArgoCD reconciles." + git push origin "HEAD:${CD_BRANCH}" + echo "Pushed ${DEPLOY_TAG} to ${CD_REPO}@${CD_BRANCH}; ArgoCD will roll it out." diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..6604fb1 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,45 @@ +name: CodeQL + +on: + push: + branches: + - develop + - main + pull_request: + branches: + - develop + - main + schedule: + - cron: "27 4 * * 1" + +concurrency: + group: codeql-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: + - javascript-typescript + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v4 + with: + languages: ${{ matrix.language }} + build-mode: none + + - name: Analyze + uses: github/codeql-action/analyze@v4 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/company-os-sync.yml b/.github/workflows/company-os-sync.yml new file mode 100644 index 0000000..44c3f80 --- /dev/null +++ b/.github/workflows/company-os-sync.yml @@ -0,0 +1,89 @@ +name: company-os-sync + +# Regenerates the company OS (BenchFinity/company) product/ + software/ review showcases +# from the source of truth here in Workbench, and opens a PR into the company repo. +# +# Requires a repo secret COMPANY_OS_TOKEN: a fine-grained PAT (or GitHub App installation +# token) with Contents: read & write + Pull requests: read & write on BenchFinity/company. +# Until that secret is set, the job guards itself off and is a clean no-op. + +on: + push: + branches: [develop] + paths: + - "scripts/company-os/**" + - "docs/PRODUCT-VISION.md" + - "docs/ROADMAP.md" + - "docs/DEPLOY.md" + - "AGENTS.md" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: company-os-sync + cancel-in-progress: true + +jobs: + sync: + runs-on: ubuntu-latest + steps: + - name: Check sync token + id: guard + env: + TOKEN: ${{ secrets.COMPANY_OS_TOKEN }} + run: | + if [ -z "$TOKEN" ]; then + echo "COMPANY_OS_TOKEN not set - skipping company-OS sync (no-op)." + echo "enabled=false" >> "$GITHUB_OUTPUT" + else + echo "enabled=true" >> "$GITHUB_OUTPUT" + fi + + - name: Checkout Workbench (generator source) + if: steps.guard.outputs.enabled == 'true' + uses: actions/checkout@v6 + + - name: Setup Node + if: steps.guard.outputs.enabled == 'true' + uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Checkout company OS + if: steps.guard.outputs.enabled == 'true' + uses: actions/checkout@v6 + with: + repository: BenchFinity/company + ref: develop + token: ${{ secrets.COMPANY_OS_TOKEN }} + path: company-os + + - name: Generate division showcases + if: steps.guard.outputs.enabled == 'true' + env: + COMPANY_OS_DIR: ${{ github.workspace }}/company-os + run: node scripts/company-os/generate.mjs + + - name: Open PR into company OS (only if changed) + if: steps.guard.outputs.enabled == 'true' + working-directory: company-os + env: + GH_TOKEN: ${{ secrets.COMPANY_OS_TOKEN }} + run: | + if git diff --quiet -- product/review.html software/review.html; then + echo "No showcase changes; nothing to sync." + exit 0 + fi + git config user.name "benchfinity-bot" + git config user.email "bot@users.noreply.github.com" + BRANCH="bot/company-os-sync-${GITHUB_SHA::7}" + git checkout -b "$BRANCH" + git add product/review.html software/review.html + git commit -m "chore(product,software): sync division showcases from Workbench" + git push -u origin "$BRANCH" + gh pr create --repo BenchFinity/company --base develop --head "$BRANCH" \ + --title "chore: sync product + software showcases from Workbench" \ + --body "Automated sync of product/review.html + software/review.html from BenchFinity/Workbench (scripts/company-os/generate.mjs). Source of truth is Workbench; these pages are generated - do not edit by hand." \ + || echo "PR may already exist for ${BRANCH}" diff --git a/.gitignore b/.gitignore index 45337be..0cfa5dd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,11 @@ node_modules/ dist/ .DS_Store result.json +.ai/ +.idea/ +coverage/ +.vite/ +npm-debug.log* +*.tsbuildinfo +.env +.env.* diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..1705290 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +dist +coverage +node_modules +package-lock.json +*.tsbuildinfo + +# Helm templates use Go template syntax, which is not valid YAML. +deploy/helm/*/templates/ + +# nginx configuration is not Prettier-managed. +docker/nginx.conf +docker/security-headers.inc diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..963354f --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,3 @@ +{ + "printWidth": 120 +} diff --git a/AGENTS.md b/AGENTS.md index 5ffe9fc..a869051 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,9 +2,9 @@ ## Project Context -Benchfinity is a personal KofTwentyTwo project for generating Gridfinity-compatible baseplates, split print bundles, and Bambu Studio-style 3MF files. V1 is a local browser app. The next phase is the Workbench version with accounts, projects, saved designs, export history, and a QQQ/Postgres backend. +Benchfinity is a BenchFinity project for generating Gridfinity-compatible baseplates, split print bundles, and Bambu Studio-style 3MF files. V1 is a local browser app. The next phase is the Workbench version with accounts, projects, saved designs, export history, and a QQQ/Postgres backend. -No external issue ticket is required for this personal project unless James asks for one. +Use GitHub Issues for new project work unless James explicitly asks to skip issue tracking. ## Required Startup Reads diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8a8de32 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,22 @@ +# CLAUDE.md + +Benchfinity (org `BenchFinity`, repo `workbench`) is a client-only Vite + React + TypeScript + Three.js app that generates Gridfinity-compatible baseplates and exports STL, split ZIP, and Bambu Studio-style 3MF files. This file is the policy layer for agents and contributors; `AGENTS.md` is the operational guide and the source of truth for required startup reads, commands, validation rules, and architecture boundaries. Read `AGENTS.md` first. + +## Naming + +- The product and repo brand is `Benchfinity`. The org is `BenchFinity`. `KofTwentyTwo` is a personal handle, not the brand. +- `Gridfinity` and `Tracefinity` are compatibility references, never the product name. Describe output as "Gridfinity-compatible", verified against the external Tracefinity standard. + +## Workflow + +- Default branch is `develop` (gitflow). Do not commit to `main` or `develop`; work on a `feature/` branch and open PRs into `develop`. +- Roadmap work is tracked in GitHub Issues under the `Workbench VNext` and `Repository Foundation` milestones (#1-#9). A ticket is optional for small personal work; when one applies, reference it in the PR and commit body (`Refs #6`, `Closes #9`). +- Conventional commits, GPG-signed. No AI attribution in commit messages or content. No emojis. + +## Hard constraint (issue #5) + +The V1 generator core (`src/geometry/*`, `src/validation.ts`, `src/export/*`) is pure and free of React, DOM, and storage. Preserve it as the first Workbench item type during VNext: wrap it behind the persistence layer rather than rewriting the geometry/export math. The serializable design unit is `BaseplateDesign` (`src/design.ts`, carries `schemaVersion`); persist `PlateInput` and the derived `PlateLayout`, never the Three.js meshes (recompute those on load). + +## Gates + +`npm run lint`, `npm run format:check`, `npm run typecheck`, `npm run test`, and `npm run build` must all pass; CI enforces them on every PR. The current test baseline is 34. diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..a23e8ad --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,31 @@ +# Code of Conduct + +## Our Pledge + +Project participants are expected to make participation in this community respectful and harassment-free for everyone, regardless of background, identity, experience level, or viewpoint. + +## Our Standards + +Expected behavior includes: + +- Being respectful and constructive. +- Giving and accepting feedback in good faith. +- Focusing discussion on the project and the work. +- Taking responsibility when a mistake affects others. + +Unacceptable behavior includes: + +- Harassment, intimidation, or discriminatory language. +- Personal attacks or sustained disruption. +- Publishing private information without permission. +- Conduct that would reasonably be considered inappropriate in a professional setting. + +## Enforcement + +Instances of unacceptable behavior may be reported through the repository owner profile or other maintainer contact path. Reports will be reviewed as promptly and fairly as practical. + +Maintainers may remove comments, close issues, reject contributions, or restrict participation when needed to protect the project community. + +## Attribution + +This code is adapted from the Contributor Covenant, version 2.0. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4c00c8b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,43 @@ +# Contributing to Benchfinity + +Benchfinity is a TypeScript, React, and Vite app for generating Gridfinity-compatible workbench baseplates. Contributions should keep the existing V1 generator stable while moving the Workbench roadmap forward through GitHub Issues. + +## Development Setup + +Prerequisites: + +- Node.js 22 or newer +- npm +- Git + +Commands: + +```bash +npm install +npm run test +npm run build +npm audit +``` + +Use `npm run dev` for local browser verification. + +## Workflow + +- Work from an issue when possible. +- Branch from `develop` using `feature/{issue-number}-{short-description}`. +- Keep pull requests focused and small enough to review. +- Use conventional commit subjects. +- Do not use the Bambu Studio CLI for automated validation on this project. Validate 3MF behavior through tests, package inspection, and manual GUI import when needed. + +## Quality Bar + +- Keep `npm run test`, `npm run build`, and `npm audit` green. +- Preserve existing STL, ZIP, 3MF, geometry, and preview behavior unless the issue explicitly changes it. +- Add focused tests for geometry, export, validation, and persistence behavior when touched. +- Keep `src/App.tsx` centered on top-level state and orchestration. Use `src/components`, `src/geometry`, and `src/export` for focused implementation. + +## Legal + +Benchfinity is licensed under AGPL-3.0-only; contributions are accepted under the same license. + +This project uses the [Developer Certificate of Origin](https://developercertificate.org/): sign off each commit with `git commit -s` to certify it is your own work and may be distributed under the project license (and any commercial license the maintainers may also offer). diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..da7772c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,38 @@ +# glibc Node toolchain (Chainguard node:latest-dev: npm + shell, glibc, nonroot +# uid 65532). Matches ADR 0001; replaces node:22-alpine (musl). Provides Node >=22 +# (currently 26.x) per package.json engines. Digest-pinned; refresh via Dependabot. +FROM cgr.dev/chainguard/node:latest-dev@sha256:5f539ca9ce7ed8b858059b3316640232bcb1ae7d3513ae67bb95527533bf1fba AS deps +# /app is the image's default WORKDIR and is owned/writable by the nonroot user. +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +FROM cgr.dev/chainguard/node:latest-dev@sha256:5f539ca9ce7ed8b858059b3316640232bcb1ae7d3513ae67bb95527533bf1fba AS builder +WORKDIR /app + +# --chown so the nonroot build user can write into node_modules (tsc emits +# .tmp/*.tsbuildinfo there); cross-stage COPY otherwise lands read-only for it. +COPY --chown=node:node --from=deps /app/node_modules ./node_modules +COPY --chown=node:node . . + +RUN npm run build + +# Distroless Chainguard nginx: nonroot (uid 65532), no shell, no package +# manager, daily-rebuilt with near-zero CVEs. See docs/adr/0001-distroless-base-images.md. +# Digest-pinned for reproducibility; refresh via Dependabot/Renovate or manually +# (docker buildx imagetools inspect cgr.dev/chainguard/nginx:latest). +FROM cgr.dev/chainguard/nginx:latest@sha256:71093c1127c31422838904b00b32287bd2bf58cd06e0abc3c85d96597d46a448 + +# Chainguard nginx mirrors the stock layout: nginx.conf includes +# /etc/nginx/conf.d/*.conf, listens on 8080, and serves /usr/share/nginx/html. +# Overwrite the base's default site (nginx.default.conf, also :8080) so only our +# SPA server block is active and there is no duplicate default_server. +COPY docker/nginx.conf /etc/nginx/conf.d/nginx.default.conf +COPY docker/security-headers.inc /etc/nginx/conf.d/security-headers.inc +COPY --from=builder /app/dist /usr/share/nginx/html + +EXPOSE 8080 + +# No HEALTHCHECK: distroless has no shell/wget. Container health is handled by +# orchestrator probes (Helm uses httpGet; compose has no healthcheck). diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fe6b903 --- /dev/null +++ b/LICENSE @@ -0,0 +1,662 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. + diff --git a/README.md b/README.md index 4308528..d7332e8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,11 @@ # Benchfinity -Browser app for generating Gridfinity-compatible workbench baseplates as STL, ZIP, and 3MF files. +[![CI](https://github.com/BenchFinity/workbench/actions/workflows/ci.yml/badge.svg?branch=develop)](https://github.com/BenchFinity/workbench/actions/workflows/ci.yml) +[![License](https://img.shields.io/github/license/BenchFinity/workbench)](LICENSE) + +Benchfinity is a browser app for generating Gridfinity-compatible workbench baseplates as STL, split ZIP bundles, and Bambu Studio-style 3MF files. + +The V1 app runs locally in the browser. The next Workbench phase adds accounts, projects, saved designs, export history, and a QQQ/Postgres backend. ## Commands @@ -9,8 +14,20 @@ npm install npm run dev npm run test npm run build +npm audit +``` + +## Docker + +Benchfinity publishes container images to GitHub Container Registry. + +```bash +docker build -t benchfinity-workbench . +docker run --rm -p 8080:8080 benchfinity-workbench ``` +The production image serves the built Vite app on port `8080`. Published images use `ghcr.io/benchfinity/workbench`. + ## Architecture - `src/App.tsx` owns top-level state, defaults, validation, and export orchestration. @@ -31,7 +48,7 @@ npm run build - Exact finished envelope with centered Gridfinity cells and solid perimeter padding. - Project name input used in exported filenames, manifest, and README. -- KofTwentyTwo project footer. +- Benchfinity footer linking to benchfinity.com. - Settings dialog for saved defaults in local storage. - Pre-export validation for project name, dimensions, margin, printable fit, and generated tiles. - Standard `42mm` Gridfinity pitch by default. @@ -42,3 +59,17 @@ npm run build - Printer bed presets grouped by brand, with Bambu Lab H2C left-nozzle selected by default and a `Custom` option for manual dimensions. - Edge-open underside connector notches with a separate connector key STL. - Bambu Studio-style 3MF export with one printable plate per generated tile, plus STL export for single plates and ZIP export for split plates. + +## Branch And Release Flow + +- `develop` is the default branch for integration work. +- `feature/*` branches publish snapshot container images tagged with the sanitized branch name and short commit SHA. +- `develop` publishes a snapshot image and the `develop` image tag. +- `release/*` and `rc/*` branches publish RC images and prerelease GitHub Releases. +- `main` publishes the clean package version image tag and a stable GitHub Release. + +## License + +Benchfinity is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0-only) — see [LICENSE](LICENSE). Running a modified version as a network service obligates you to offer its source to users (AGPL section 13). For commercial licensing without AGPL terms, contact the maintainers. + +Contributions are accepted under AGPL-3.0 via the Developer Certificate of Origin; sign off your commits with `git commit -s`. See [CONTRIBUTING](CONTRIBUTING.md). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..15efb85 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,26 @@ +# Security Policy + +## Supported Versions + +Benchfinity is pre-1.0 software. Security fixes are applied to the active development line on `develop` and to the latest published release when one exists. + +## Reporting a Vulnerability + +Do not report security vulnerabilities in public issues. + +Use GitHub private vulnerability reporting if it is available for this repository, or contact the maintainer directly through the repository owner profile. + +Please include: + +- A description of the vulnerability. +- Steps to reproduce it. +- The affected version, branch, or commit. +- Any known impact or workaround. + +The maintainer will acknowledge valid reports as quickly as practical and coordinate a fix before public disclosure. + +## Security Expectations + +- Do not commit secrets, tokens, credentials, or private project files. +- Do not include sensitive printer, account, or artifact data in logs. +- Keep dependency audit findings at zero before release handoff. diff --git a/compose.yaml b/compose.yaml new file mode 100644 index 0000000..a7f45d5 --- /dev/null +++ b/compose.yaml @@ -0,0 +1,77 @@ +# Local development stack for Benchfinity. +# +# docker compose up -> frontend only (default) +# docker compose --profile full up -> frontend + backend + postgres + minio +# +# Copy .env.example to .env first (the [full] profile reads it for credentials). +# The GHCR image is private; run `docker login ghcr.io` before pulling by image. + +services: + frontend: + # No profile: always part of the default `docker compose up`. + build: + context: . + dockerfile: Dockerfile + image: ghcr.io/benchfinity/workbench:develop + ports: + - "8080:8080" + restart: unless-stopped + # No healthcheck: the distroless Chainguard nginx base has no shell/wget. + # Health is handled by orchestrator HTTP probes (the Helm chart uses httpGet). + + backend: + # Placeholder for the QQQ/Java REST API. Image is TBD (#1); enable with + # the `full` profile once the backend repo/image exists. + profiles: ["full"] + image: ghcr.io/benchfinity/workbench-api:latest + restart: unless-stopped + depends_on: + - postgres + - minio + environment: + DATABASE_URL: ${DATABASE_URL:-postgresql://benchfinity:change-me@postgres:5432/benchfinity} + MINIO_ENDPOINT: ${MINIO_ENDPOINT:-http://minio:9000} + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-benchfinity} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-change-me-too} + ports: + - "8081:8080" + + postgres: + profiles: ["full"] + image: postgres:16-alpine + restart: unless-stopped + environment: + POSTGRES_USER: ${POSTGRES_USER:-benchfinity} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-change-me} + POSTGRES_DB: ${POSTGRES_DB:-benchfinity} + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-benchfinity} -d ${POSTGRES_DB:-benchfinity}"] + interval: 10s + timeout: 5s + retries: 5 + + minio: + profiles: ["full"] + image: minio/minio:latest + restart: unless-stopped + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${MINIO_ROOT_USER:-benchfinity} + MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-change-me-too} + ports: + - "9000:9000" + - "9001:9001" + volumes: + - minio-data:/data + healthcheck: + test: ["CMD-SHELL", "mc ready local || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + +volumes: + postgres-data: {} + minio-data: {} diff --git a/deploy/argocd/application.yaml b/deploy/argocd/application.yaml new file mode 100644 index 0000000..0e9a206 --- /dev/null +++ b/deploy/argocd/application.yaml @@ -0,0 +1,37 @@ +# EXAMPLE Argo CD Application for Benchfinity. +# +# This is a starting point, not a turnkey manifest. Before applying: +# - The GHCR image (and chart) are private. Give Argo CD / the target cluster +# pull access by configuring an imagePullSecret (set chart value +# imagePullSecrets) and, if syncing the chart from OCI, repo credentials. +# - Adjust repoURL, targetRevision, and destination to your environment. +# +# Argo CD Image Updater can track the rolling `:develop` snapshot tag. Add the +# annotations below (and install the Image Updater) to auto-bump the frontend +# image as new develop builds are pushed: +# +# annotations: +# argocd-image-updater.argoproj.io/image-list: frontend=ghcr.io/benchfinity/workbench +# argocd-image-updater.argoproj.io/frontend.update-strategy: digest +# argocd-image-updater.argoproj.io/frontend.allow-tags: regexp:^develop$ +# argocd-image-updater.argoproj.io/frontend.pull-secret: pullsecret:argocd/ghcr-pull +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: benchfinity + namespace: argocd +spec: + project: default + source: + repoURL: https://github.com/BenchFinity/workbench.git + path: deploy/helm/benchfinity + targetRevision: develop + destination: + server: https://kubernetes.default.svc + namespace: benchfinity + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true diff --git a/deploy/helm/benchfinity/Chart.yaml b/deploy/helm/benchfinity/Chart.yaml new file mode 100644 index 0000000..9cf1053 --- /dev/null +++ b/deploy/helm/benchfinity/Chart.yaml @@ -0,0 +1,11 @@ +apiVersion: v2 +name: benchfinity +description: Benchfinity Workbench - Gridfinity-compatible baseplate generator +type: application +# Chart version. Bumped independently of the application. +version: 0.1.0 +# Version of the Benchfinity application this chart deploys by default. +appVersion: "0.1.0" +# No dependencies: Postgres and MinIO are expected to be provided by the +# cluster (operators, managed services, or separate releases). The chart only +# configures connections to them via backend.database / backend.objectStore. diff --git a/deploy/helm/benchfinity/templates/NOTES.txt b/deploy/helm/benchfinity/templates/NOTES.txt new file mode 100644 index 0000000..3199c7c --- /dev/null +++ b/deploy/helm/benchfinity/templates/NOTES.txt @@ -0,0 +1,36 @@ +Benchfinity Workbench has been deployed. + +{{- if .Values.ingress.enabled }} + +The app is reachable via the configured Ingress host(s): +{{- range .Values.ingress.hosts }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ .host }} +{{- end }} +{{- else if .Values.frontend.enabled }} + +The frontend Service is ClusterIP-only. To reach it locally, port-forward: + + kubectl --namespace {{ .Release.Namespace }} port-forward \ + svc/{{ include "benchfinity.componentName" (dict "context" . "component" "frontend") }} \ + 8080:{{ .Values.frontend.service.port }} + +Then open http://127.0.0.1:8080 +{{- end }} + +{{- if .Values.backend.enabled }} + +Backend (QQQ API) is enabled and expects a reachable Postgres at +{{ .Values.backend.database.host }}:{{ .Values.backend.database.port }}. +{{- if not .Values.backend.database.existingSecret }} + + WARNING: backend.database.existingSecret is not set, so no DATABASE_PASSWORD + was injected. Set it to a Secret holding the DB password. +{{- end }} +{{- end }} + +{{- if .Values.imagePullSecrets }} +{{- else }} + +NOTE: The GHCR images are private. If pods fail with ImagePullBackOff, set +imagePullSecrets in values to a docker-registry Secret with GHCR access. +{{- end }} diff --git a/deploy/helm/benchfinity/templates/_helpers.tpl b/deploy/helm/benchfinity/templates/_helpers.tpl new file mode 100644 index 0000000..7885cf9 --- /dev/null +++ b/deploy/helm/benchfinity/templates/_helpers.tpl @@ -0,0 +1,80 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "benchfinity.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "benchfinity.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Chart name and version, used as a label. +*/}} +{{- define "benchfinity.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels. +*/}} +{{- define "benchfinity.labels" -}} +helm.sh/chart: {{ include "benchfinity.chart" . }} +{{ include "benchfinity.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels. +*/}} +{{- define "benchfinity.selectorLabels" -}} +app.kubernetes.io/name: {{ include "benchfinity.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Component-scoped selector labels. Pass a dict with "context" and "component". +*/}} +{{- define "benchfinity.componentSelectorLabels" -}} +{{ include "benchfinity.selectorLabels" .context }} +app.kubernetes.io/component: {{ .component }} +{{- end }} + +{{/* +Component-scoped common labels. Pass a dict with "context" and "component". +*/}} +{{- define "benchfinity.componentLabels" -}} +{{ include "benchfinity.labels" .context }} +app.kubernetes.io/component: {{ .component }} +{{- end }} + +{{/* +Per-component resource name, e.g. release-benchfinity-frontend. +Pass a dict with "context" and "component". +*/}} +{{- define "benchfinity.componentName" -}} +{{- printf "%s-%s" (include "benchfinity.fullname" .context) .component | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Service account name. +*/}} +{{- define "benchfinity.serviceAccountName" -}} +{{- default (include "benchfinity.fullname" .) .Values.serviceAccountName }} +{{- end }} diff --git a/deploy/helm/benchfinity/templates/backend-deployment.yaml b/deploy/helm/benchfinity/templates/backend-deployment.yaml new file mode 100644 index 0000000..f6c5bc0 --- /dev/null +++ b/deploy/helm/benchfinity/templates/backend-deployment.yaml @@ -0,0 +1,72 @@ +{{- if .Values.backend.enabled }} +{{- $component := "backend" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "benchfinity.componentName" (dict "context" . "component" $component) }} + labels: + {{- include "benchfinity.componentLabels" (dict "context" . "component" $component) | nindent 4 }} +spec: + replicas: {{ .Values.backend.replicas }} + selector: + matchLabels: + {{- include "benchfinity.componentSelectorLabels" (dict "context" . "component" $component) | nindent 6 }} + template: + metadata: + labels: + {{- include "benchfinity.componentLabels" (dict "context" . "component" $component) | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "benchfinity.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: backend + image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag }}" + imagePullPolicy: {{ .Values.backend.image.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + ports: + - name: http + containerPort: {{ .Values.backend.containerPort }} + protocol: TCP + env: + - name: DATABASE_HOST + value: {{ .Values.backend.database.host | quote }} + - name: DATABASE_PORT + value: {{ .Values.backend.database.port | quote }} + - name: DATABASE_NAME + value: {{ .Values.backend.database.name | quote }} + - name: DATABASE_USER + value: {{ .Values.backend.database.user | quote }} + {{- if .Values.backend.database.existingSecret }} + - name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.backend.database.existingSecret }} + key: {{ .Values.backend.database.existingSecretPasswordKey }} + {{- end }} + {{- if .Values.backend.objectStore.enabled }} + - name: OBJECT_STORE_ENDPOINT + value: {{ .Values.backend.objectStore.endpoint | quote }} + - name: OBJECT_STORE_BUCKET + value: {{ .Values.backend.objectStore.bucket | quote }} + {{- if .Values.backend.objectStore.existingSecret }} + - name: OBJECT_STORE_ACCESS_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.backend.objectStore.existingSecret }} + key: access-key + - name: OBJECT_STORE_SECRET_KEY + valueFrom: + secretKeyRef: + name: {{ .Values.backend.objectStore.existingSecret }} + key: secret-key + {{- end }} + {{- end }} + resources: + {{- toYaml .Values.backend.resources | nindent 12 }} +{{- end }} diff --git a/deploy/helm/benchfinity/templates/backend-service.yaml b/deploy/helm/benchfinity/templates/backend-service.yaml new file mode 100644 index 0000000..7b51f56 --- /dev/null +++ b/deploy/helm/benchfinity/templates/backend-service.yaml @@ -0,0 +1,18 @@ +{{- if .Values.backend.enabled }} +{{- $component := "backend" }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "benchfinity.componentName" (dict "context" . "component" $component) }} + labels: + {{- include "benchfinity.componentLabels" (dict "context" . "component" $component) | nindent 4 }} +spec: + type: {{ .Values.backend.service.type }} + ports: + - name: http + port: {{ .Values.backend.service.port }} + targetPort: {{ .Values.backend.containerPort }} + protocol: TCP + selector: + {{- include "benchfinity.componentSelectorLabels" (dict "context" . "component" $component) | nindent 4 }} +{{- end }} diff --git a/deploy/helm/benchfinity/templates/frontend-deployment.yaml b/deploy/helm/benchfinity/templates/frontend-deployment.yaml new file mode 100644 index 0000000..feabfc6 --- /dev/null +++ b/deploy/helm/benchfinity/templates/frontend-deployment.yaml @@ -0,0 +1,52 @@ +{{- if .Values.frontend.enabled }} +{{- $component := "frontend" }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "benchfinity.componentName" (dict "context" . "component" $component) }} + labels: + {{- include "benchfinity.componentLabels" (dict "context" . "component" $component) | nindent 4 }} +spec: + {{- if not .Values.frontend.autoscaling.enabled }} + replicas: {{ .Values.frontend.replicas }} + {{- end }} + selector: + matchLabels: + {{- include "benchfinity.componentSelectorLabels" (dict "context" . "component" $component) | nindent 6 }} + template: + metadata: + labels: + {{- include "benchfinity.componentLabels" (dict "context" . "component" $component) | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "benchfinity.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: frontend + image: "{{ .Values.frontend.image.repository }}:{{ .Values.frontend.image.tag }}" + imagePullPolicy: {{ .Values.frontend.image.pullPolicy }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + ports: + - name: http + containerPort: {{ .Values.frontend.containerPort }} + protocol: TCP + readinessProbe: + httpGet: + path: / + port: {{ .Values.frontend.containerPort }} + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + httpGet: + path: / + port: {{ .Values.frontend.containerPort }} + initialDelaySeconds: 10 + periodSeconds: 20 + resources: + {{- toYaml .Values.frontend.resources | nindent 12 }} +{{- end }} diff --git a/deploy/helm/benchfinity/templates/frontend-hpa.yaml b/deploy/helm/benchfinity/templates/frontend-hpa.yaml new file mode 100644 index 0000000..8a62283 --- /dev/null +++ b/deploy/helm/benchfinity/templates/frontend-hpa.yaml @@ -0,0 +1,23 @@ +{{- if and .Values.frontend.enabled .Values.frontend.autoscaling.enabled }} +{{- $component := "frontend" }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "benchfinity.componentName" (dict "context" . "component" $component) }} + labels: + {{- include "benchfinity.componentLabels" (dict "context" . "component" $component) | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "benchfinity.componentName" (dict "context" . "component" $component) }} + minReplicas: {{ .Values.frontend.autoscaling.minReplicas }} + maxReplicas: {{ .Values.frontend.autoscaling.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.frontend.autoscaling.targetCPUUtilizationPercentage }} +{{- end }} diff --git a/deploy/helm/benchfinity/templates/frontend-service.yaml b/deploy/helm/benchfinity/templates/frontend-service.yaml new file mode 100644 index 0000000..82cc18e --- /dev/null +++ b/deploy/helm/benchfinity/templates/frontend-service.yaml @@ -0,0 +1,18 @@ +{{- if .Values.frontend.enabled }} +{{- $component := "frontend" }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "benchfinity.componentName" (dict "context" . "component" $component) }} + labels: + {{- include "benchfinity.componentLabels" (dict "context" . "component" $component) | nindent 4 }} +spec: + type: {{ .Values.frontend.service.type }} + ports: + - name: http + port: {{ .Values.frontend.service.port }} + targetPort: {{ .Values.frontend.containerPort }} + protocol: TCP + selector: + {{- include "benchfinity.componentSelectorLabels" (dict "context" . "component" $component) | nindent 4 }} +{{- end }} diff --git a/deploy/helm/benchfinity/templates/ingress.yaml b/deploy/helm/benchfinity/templates/ingress.yaml new file mode 100644 index 0000000..4bfb675 --- /dev/null +++ b/deploy/helm/benchfinity/templates/ingress.yaml @@ -0,0 +1,35 @@ +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "benchfinity.fullname" . }} + labels: + {{- include "benchfinity.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- with .Values.ingress.tls }} + tls: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + pathType: {{ .pathType }} + backend: + service: + name: {{ include "benchfinity.componentName" (dict "context" $ "component" (.service | default "frontend")) }} + port: + number: {{ if eq (.service | default "frontend") "backend" }}{{ $.Values.backend.service.port }}{{ else }}{{ $.Values.frontend.service.port }}{{ end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/helm/benchfinity/templates/serviceaccount.yaml b/deploy/helm/benchfinity/templates/serviceaccount.yaml new file mode 100644 index 0000000..035d537 --- /dev/null +++ b/deploy/helm/benchfinity/templates/serviceaccount.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "benchfinity.serviceAccountName" . }} + labels: + {{- include "benchfinity.labels" . | nindent 4 }} +{{- with .Values.imagePullSecrets }} +imagePullSecrets: + {{- toYaml . | nindent 2 }} +{{- end }} diff --git a/deploy/helm/benchfinity/values.yaml b/deploy/helm/benchfinity/values.yaml new file mode 100644 index 0000000..8d58171 --- /dev/null +++ b/deploy/helm/benchfinity/values.yaml @@ -0,0 +1,100 @@ +# Default values for the Benchfinity Workbench chart. + +# Override the generated resource name (chart name) or the full release name. +nameOverride: "" +fullnameOverride: "" + +# Pull secrets for the private GHCR images, e.g. [{ name: ghcr-pull }]. +# Create the secret with: +# kubectl create secret docker-registry ghcr-pull \ +# --docker-server=ghcr.io --docker-username= --docker-password= +imagePullSecrets: [] + +frontend: + enabled: true + image: + repository: ghcr.io/benchfinity/workbench + tag: develop + pullPolicy: IfNotPresent + replicas: 1 + containerPort: 8080 + resources: + requests: + cpu: 25m + memory: 32Mi + limits: + cpu: 100m + memory: 64Mi + service: + type: ClusterIP + port: 80 + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 3 + targetCPUUtilizationPercentage: 80 + +backend: + # Disabled placeholder. The QQQ/Java REST API repo and image are TBD (#1). + enabled: false + image: + repository: ghcr.io/benchfinity/workbench-api + tag: latest + pullPolicy: IfNotPresent + replicas: 1 + containerPort: 8080 + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + service: + type: ClusterIP + port: 80 + # Connection to a cluster-provided Postgres (not bundled by this chart). + database: + host: "" + port: 5432 + name: benchfinity + user: benchfinity + # Secret holding the DB password. When set, the password is injected via + # secretKeyRef instead of a plaintext env value. + existingSecret: "" + existingSecretPasswordKey: password + # Optional MinIO/S3 object storage (not bundled by this chart). + objectStore: + enabled: false + endpoint: "" + bucket: benchfinity + existingSecret: "" + +ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: benchfinity.local + paths: + - path: / + pathType: Prefix + # Route to the "frontend" or "backend" service. + service: frontend + tls: [] + +# Applied at the pod level. +podSecurityContext: + runAsNonRoot: true + seccompProfile: + type: RuntimeDefault + +# Applied at the container level. The frontend uses the distroless Chainguard +# nginx base (nonroot uid 65532), which writes to its temp dirs, so +# readOnlyRootFilesystem is left off by default. +securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + capabilities: + drop: + - ALL diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 0000000..4c73ebb --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,20 @@ +server { + listen 8080; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + include /etc/nginx/conf.d/security-headers.inc; + + location / { + include /etc/nginx/conf.d/security-headers.inc; + try_files $uri $uri/ /index.html; + } + + location /assets/ { + include /etc/nginx/conf.d/security-headers.inc; + try_files $uri =404; + add_header Cache-Control "public, max-age=31536000, immutable"; + } +} diff --git a/docker/security-headers.inc b/docker/security-headers.inc new file mode 100644 index 0000000..0ba035f --- /dev/null +++ b/docker/security-headers.inc @@ -0,0 +1,13 @@ +# Security response headers, shared via include so they apply in every context. +# +# nginx add_header is NOT inherited into a location block once that block sets +# any add_header of its own (see the /assets/ Cache-Control header). Including +# this snippet in both the server block and that location keeps the headers on +# every response. +# +# The CSP is intentionally conservative but allows what the Three.js SPA needs: +# blob: workers/images and inline styles. +add_header X-Content-Type-Options "nosniff" always; +add_header X-Frame-Options "SAMEORIGIN" always; +add_header Referrer-Policy "strict-origin-when-cross-origin" always; +add_header Content-Security-Policy "default-src 'self'; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self'; worker-src 'self' blob:; object-src 'none'; base-uri 'self'" always; diff --git a/docs/AGENT-HANDOFF.md b/docs/AGENT-HANDOFF.md index 31f92f8..701d648 100644 --- a/docs/AGENT-HANDOFF.md +++ b/docs/AGENT-HANDOFF.md @@ -4,6 +4,8 @@ Benchfinity V1 is complete, audited, tested, and pushed as the foundation for the larger Workbench phase. The local app generates Gridfinity-compatible baseplates, preview meshes, STL exports, split ZIP exports, and Bambu Studio-style 3MF packages with one plate per tile and connector-key objects when split. +The repository now uses `develop` as the default branch. Workbench VNext planning has moved into GitHub Issues #1 through #8, grouped by the `Workbench VNext` milestone and the `Benchfinity Roadmap` project. Repository setup work is tracked in GitHub Issue #9. The repository home is `BenchFinity/workbench`. + ## Most Important Ground Truth - Use `Benchfinity` as the product and repo name. @@ -31,10 +33,11 @@ Expected results: ## Next Best Work 1. Start the Workbench phase from `docs/WORKBENCH-VNEXT.md`. -2. Decide backend repo/app shape for QQQ/Postgres integration. -3. Add account, project, workbench item, baseplate design, printer profile, and export artifact persistence. -4. Keep the existing V1 generator as the first workbench item type rather than rewriting the geometry/export core. -5. Add an artifact-storage decision before server-side generation. +2. Finish and merge GitHub Issue #9 on `feature/9-public-repo-ci`. +3. Return to GitHub Issue #1 for the backend repo/app shape decision. +4. Add account, project, workbench item, baseplate design, printer profile, and export artifact persistence through the roadmap issues. +5. Keep the existing V1 generator as the first workbench item type rather than rewriting the geometry/export core. +6. Add an artifact-storage decision before server-side generation. ## Open Cautions diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md new file mode 100644 index 0000000..3b2d632 --- /dev/null +++ b/docs/DEPLOY.md @@ -0,0 +1,86 @@ +# Deploying Benchfinity + +The frontend ships now. The backend (QQQ/Java API), Postgres, and MinIO are +future additions and are disabled placeholders everywhere below. + +## 1. Pull the image from GHCR + +The repo and its image are private, so authenticate first: + +```bash +echo "$GHCR_TOKEN" | docker login ghcr.io -u --password-stdin +docker pull ghcr.io/benchfinity/workbench:develop +``` + +Tags: `:develop` (rolling develop build), `:` (release on main), +plus per-commit `-SNAPSHOT` tags. + +The frontend image is built on the distroless Chainguard nginx base +(`cgr.dev/chainguard/nginx`, nonroot, no shell/package manager) per +[ADR 0001](adr/0001-distroless-base-images.md). It has no container +`HEALTHCHECK`; health is delegated to orchestrator HTTP probes (the Helm chart +uses `httpGet`). + +## 2. Run locally with docker compose + +```bash +cp .env.example .env # only needed for the full stack +docker compose up # frontend only -> http://localhost:8080 +docker compose --profile full up # + backend + postgres + minio (future) +``` + +The default profile builds/pulls only the frontend. `--profile full` adds the +placeholder backend (port 8081), Postgres, and MinIO (9000 API / 9001 console). + +## 3. Install the Helm chart + +```bash +helm install benchfinity deploy/helm/benchfinity --namespace benchfinity --create-namespace +``` + +Key values: + +| Value | Purpose | +| ----------------------------------- | ----------------------------------------- | +| `frontend.image.tag` | Image tag to deploy (default `develop`) | +| `frontend.autoscaling.enabled` | Enable the HPA | +| `ingress.enabled` / `ingress.hosts` | Expose via Ingress | +| `backend.enabled` | Turn on the future backend (off) | +| `backend.database.*` | Connection to a cluster-provided Postgres | +| `backend.objectStore.*` | Optional MinIO/S3 | +| `imagePullSecrets` | Required for the private GHCR image | + +Postgres and MinIO are not bundled; point `backend.database` / `objectStore` at +cluster-provided services. + +The image is private. Create a pull secret and reference it: + +```bash +kubectl -n benchfinity create secret docker-registry ghcr-pull \ + --docker-server=ghcr.io --docker-username= --docker-password= +helm install benchfinity deploy/helm/benchfinity -n benchfinity \ + --set imagePullSecrets[0].name=ghcr-pull +``` + +## 4. Argo CD + +Apply the example Application (edit repo/destination first): + +```bash +kubectl apply -f deploy/argocd/application.yaml +``` + +It syncs `deploy/helm/benchfinity` at `develop` into the `benchfinity` +namespace with automated prune + self-heal. See the file header for the Image +Updater annotation that tracks the `:develop` tag, and the private-registry +note. + +## 5. Published chart + +CI packages and pushes the chart to GHCR as an OCI artifact on develop/main: + +```bash +helm pull oci://ghcr.io/benchfinity/charts/benchfinity +# or install directly: +helm install benchfinity oci://ghcr.io/benchfinity/charts/benchfinity --version +``` diff --git a/docs/PLAN-benchfinity-cd.md b/docs/PLAN-benchfinity-cd.md new file mode 100644 index 0000000..5279eb3 --- /dev/null +++ b/docs/PLAN-benchfinity-cd.md @@ -0,0 +1,95 @@ +# PLAN: benchfinity-cd (production deployment) + +Take `benchfinity.com` live on `k8s-prod` via a private raw-Kustomize CD repo +modeled on `voyage-cd`, onboarded into `KofTwentyTwo/k8s-app-of-apps`. Web tier +live, full data plane live, QQQ backend parked. Decision: [ADR 0002](adr/0002-production-deployment-architecture.md). +Refs #29 (create repo), #30 (deploy). + +## Values + +| Thing | Value | +| ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Cluster / Argo dest | `https://k8s-prod-vip.galaxy.lan:6443` (Argo CD on `k8s-infra`) | +| Namespace | `benchfinity-prod` | +| Web image | `ghcr.io/benchfinity/workbench:0.1.0` (public, no pull secret) | +| Hosts | `benchfinity.com`, `www.benchfinity.com` | +| TLS | cert-manager `Certificate`, ClusterIssuer `letsencrypt-production` | +| Postgres | Zalando CR `benchfinity-postgres`, v17, 3 instances + pooler; db `benchfinity`; users `benchfinity-dba` (super), `benchfinity` (login); `local-path` 10Gi | +| Object store | MinIO StatefulSet, `local-path`; secret `benchfinity-minio-secrets` | +| Backups | B2 bucket `k8s-prod-backups`, prefix **`benchfinity-prod/`** (postgres + minio) | +| NAS (proxy) | `10.120.149.4` | +| Proxy SNIs | `dsm,calendar,chat,contacts,drive,file,mail.benchfinity.com` | +| API (parked) | `benchfinity-api`, `replicas: 0`, image TBD (#1), port 8000, `/qqq-api/health` | + +## benchfinity-cd repo layout + +``` +base/ + benchfinity-web-{deployment,service,ingress,certificate}.yaml # LIVE + benchfinity-api-{deployment,service,hpa,pdb,configmap,serviceaccount}.yaml # PARKED replicas:0 + benchfinity-postgres.yaml # Zalando postgresql CR + qrun-redis-sentinel-{configmap,statefulset,service}.yaml # copied from voyage + benchfinity-minio-{statefulset,service,ingress}.yaml + postgres-b2-backup-cronjob.yaml # -> k8s-prod-backups/benchfinity-prod/postgres/ + minio-b2-backup-cronjob.yaml # -> k8s-prod-backups/benchfinity-prod/minio// + kustomization.yaml +overlays/production/kustomization.yaml # ns benchfinity-prod, images newTag 0.1.0, host/CORS +proxy/ nas-{endpoints(10.120.149.4),service,ingressroutetcp,ingress-http}.yaml +.github/workflows/validate.yml # GH Actions CI +README.md CLAUDE.md docs/ +``` + +## app-of-apps additions (KofTwentyTwo/k8s-app-of-apps) + +``` +apps/benchfinity/base/{application.yaml,infra-application.yaml,kustomization.yaml} # OVERRIDE templates +apps/benchfinity/overlays/production/{kustomization.yaml,proxy-application.yaml} # benchfinity-prod + benchfinity-proxy +shared/benchfinity-project.yaml # AppProject: srcRepos benchfinity-cd + app-of-apps; dests benchfinity-prod, benchfinity-proxy, in-cluster; CRD whitelist Namespace + ClusterIssuer +shared/repo-creds-benchfinity-cd.yaml # sealed SSH deploy key (argocd ns on k8s-infra) +infra/benchfinity/overlays/production/{benchfinity-minio-secrets-sealed.yaml,b2-backup-credentials-sealed.yaml,kustomization.yaml} +envs/production/kustomization.yaml # += apps/benchfinity/overlays/production/ +``` + +The `application.yaml` sources `git@github.com:BenchFinity/benchfinity-cd.git` +`path: overlays/production`; `infra-application.yaml` (sync-wave `-1`) sources +the app-of-apps `infra/benchfinity/overlays/production`. + +## Secrets (all sealed in-cluster via kubeseal) + +| Secret (ns) | Keys | Source | +| -------------------------------------------------- | -------------------------- | ---------------------------------------------------------------------------------- | +| `benchfinity-minio-secrets` (`benchfinity-prod`) | `MINIO_ROOT_USER/PASSWORD` | generated, sealed | +| `b2-backup-credentials` (`benchfinity-prod`) | `B2_KEY_ID/B2_APP_KEY` | reuse fleet key, sealed; writes prefix `benchfinity-prod/` | +| `repo-creds-benchfinity-cd` (`argocd`@`k8s-infra`) | `sshPrivateKey,url,type` | generated ed25519; public half = repo deploy key | +| postgres user creds | — | operator-generated (`*.benchfinity-postgres.credentials.postgresql.acid.zalan.do`) | + +1Password vault `benchfinity-prod` is the canonical plaintext record. + +## CI (GitHub Actions, `.github/workflows/validate.yml`) + +On PR and push to `main`: `kustomize build overlays/production` and +`kustomize build proxy`; `kubeconform -strict -ignore-missing-schemas`; +`gitleaks`; `kube-linter` + `yamllint` (advisory). Mirrors the fleet's Munitor +checks. The app-of-apps changes are validated by that repo's existing +CircleCI/Munitor. + +## Sequence + +1. **Prereq (workbench):** PR `develop` to `main` to cut `v0.1.0` and publish + `ghcr.io/benchfinity/workbench:0.1.0`. +2. Create private `BenchFinity/benchfinity-cd`; scaffold `base/`+`overlays/production`+`proxy/`+CI; pin `:0.1.0`. +3. Seal `benchfinity-minio-secrets` (generate), `repo-creds-benchfinity-cd` + (generate keypair + add deploy key), `b2-backup-credentials` (fleet key). +4. PR the app-of-apps additions (apps/shared/infra/envs). +5. Merge both; Argo CD materializes `benchfinity-prod` + `benchfinity-proxy`. + Verify HTTPS load + STL/3MF export at `benchfinity.com`; verify the Synology + subdomains route through the proxy. +6. Correct stale workbench docs (`DEPLOY.md`: image is public; supersede the + example `deploy/argocd/application.yaml`). + +## Open / deferred + +- `benchfinity-api` image + ConfigMap finalize — Phase A, #1. +- dev/staging environments — add when the backend needs them. +- Export-artifact storage (#3): MinIO is up; confirm the backend targets it + versus an external S3 before relying on it. diff --git a/docs/PLAN-cd-deployment.md b/docs/PLAN-cd-deployment.md new file mode 100644 index 0000000..0acfbdf --- /dev/null +++ b/docs/PLAN-cd-deployment.md @@ -0,0 +1,51 @@ +# PLAN: CD / Deployment Artifacts + +## Goal + +Give Benchfinity reproducible deployment paths: a local docker-compose stack, a +Helm chart for Kubernetes, an Argo CD Application example, and a CI job that +publishes the chart. Ship the frontend now; leave the future backend, Postgres, +and MinIO as disabled placeholders. + +## Approach + +- The frontend image is already built and pushed by `.github/workflows/ci.yml` + to `ghcr.io/benchfinity/workbench`. These artifacts consume that image; no + application source under `src/` changes. +- Local dev uses Compose profiles: default `docker compose up` runs only the + frontend; `--profile full` adds backend + Postgres + MinIO. +- Kubernetes uses the Helm chart. The chart configures connections to + cluster-provided Postgres/MinIO rather than bundling them as subcharts. +- Argo CD syncs the chart from the repo at `develop` (example manifest). +- CI gains a `helm` job that lints/templates on every push and publishes the + chart to GHCR OCI on develop/main/tags. +- The repo (and GHCR image/chart) are private, so `imagePullSecrets` is + supported in the chart and `docker login ghcr.io` is documented for compose. + +## Files Added + +- `compose.yaml`, `.env.example` - local stack and env template. +- `docker/security-headers.inc` - shared nginx security headers; `docker/nginx.conf` + includes it in the server block and locations. +- `deploy/helm/benchfinity/` - Chart.yaml, values.yaml, and templates + (serviceaccount, frontend deployment/service/HPA, backend deployment/service, + ingress, NOTES, helpers). +- `deploy/argocd/application.yaml` - example Argo CD Application. +- `docs/DEPLOY.md` - deployment guide. +- `.github/workflows/ci.yml` - appended `helm` lint/template/publish job. +- `.prettierignore` - excludes Helm templates (Go template syntax is not YAML). + +## Frontend-now / Backend-future Model + +- Frontend: enabled by default, static SPA served by nginx on 8080. +- Backend: QQQ/Java REST API, `backend.enabled: false`, image TBD (#1). +- Postgres: required by the backend, external to the chart (cluster-provided). +- MinIO: optional object storage, external to the chart, `objectStore.enabled: false`. + +## Open Questions + +- Backend image and repository: confirmed by #1. +- Postgres delivery: vendor as a subchart/operator vs. continue assuming an + external, cluster-provided instance. +- Public release: when the repo goes public, decide whether to make the GHCR + image and chart public and drop the imagePullSecret requirement. diff --git a/docs/PLAN-gridfinity-baseplate-generator.md b/docs/PLAN-gridfinity-baseplate-generator.md index 5181d80..748f3af 100644 --- a/docs/PLAN-gridfinity-baseplate-generator.md +++ b/docs/PLAN-gridfinity-baseplate-generator.md @@ -1,12 +1,15 @@ # PLAN: Gridfinity Baseplate Generator ## Goal + Build a browser-based app that accepts a desired finished footprint, printer bed limits, and Gridfinity cell size, then previews and exports one or more printable baseplate files. ## Approach + Use a client-only TypeScript web app with a shared geometry pipeline for preview and export. The app computes the largest standard Gridfinity grid that fits inside the requested finished size, centers it inside that envelope, fills the remaining perimeter with padding, splits it into bed-safe tiles when needed, renders the assembled result in 3D, and exports STL, ZIP, and 3MF assets. ## Product Scope + - Inputs: - Project name for exported filenames and bundle metadata. - Desired finished width and depth, with inch and mm support. @@ -25,6 +28,7 @@ Use a client-only TypeScript web app with a shared geometry pipeline for preview - Derived dimensions and tile count summary. ## V1 Decisions + - Canonical compatibility target: Tracefinity-compatible standard Gridfinity bins, using Tracefinity release `0.4.0` from 2026-05-26 as the current public reference point. - Baseplate profile: standard Gridfinity baseplate socket profile on a `42mm x 42mm` pitch, with `6mm x 2mm` magnet pockets enabled by default in the Tracefinity preset and configurable off for faster drawer baseplates. - Finished size behavior: exact requested envelope with the largest whole-cell Gridfinity grid centered inside it and solid perimeter padding around the edges. @@ -36,6 +40,7 @@ Use a client-only TypeScript web app with a shared geometry pipeline for preview - User defaults: settings dialog saves startup and reset defaults to browser local storage. ## Tracefinity Compatibility Notes + Tracefinity currently documents Gridfinity units as `42mm x 42mm`; bin height uses `7mm` units plus a `5mm` base; standard magnets are `6mm x 2mm`; and generated bins conform to the Gridfinity spec. Its public repository lists release `0.4.0` as latest on 2026-05-26 and describes generated bins as Gridfinity-compatible with proper base profile, magnet holes, and stacking lip. Practical v1 rule: generated baseplates must accept Tracefinity bins without requiring special Tracefinity-specific bin settings. Magnet pockets are configurable, but the Tracefinity preset defaults them on and the baseplate socket geometry and pitch should remain standard. @@ -47,6 +52,7 @@ References: - `https://github.com/gridfinity-unofficial/specification` ## Sizing Rules + All internal geometry should use millimeters. For a requested size: @@ -85,6 +91,7 @@ Example, `22in x 10.5in` with `42mm` cells: - Padding: `6.4mm` left/right and `7.35mm` front/back. ## Splitting Strategy + Split preferably on Gridfinity cell boundaries so each tile remains standard and predictable. When perimeter padding is present, include the padding in the outermost tiles only. Derived values: @@ -108,6 +115,7 @@ Rules: - If the bed cannot fit at least one grid cell plus required edge geometry, show a blocking validation error. ## Assembly Strategy + Start with connector choices that do not change the top Gridfinity interface. Recommended MVP connector: @@ -131,6 +139,7 @@ Later connector options: - No connector geometry for users mounting to a board. ## Geometry Pipeline + Create a small geometry core that produces indexed triangle meshes from typed inputs. Suggested core types: @@ -182,6 +191,7 @@ Implementation options: Recommendation: start with direct mesh generation if the chosen baseplate profile can be expressed without heavy boolean operations. Move to ManifoldJS if connector sockets, holes, and chamfers become fragile. ## Preview + Use Three.js for the viewport. Preview features: @@ -197,6 +207,7 @@ Preview features: The preview should consume the same generated tile mesh data used by exporters, not a simplified duplicate model. ## Export + STL: - Export one STL for unsplit plates. @@ -218,6 +229,7 @@ ZIP bundle: - `connector-*.stl` when applicable. ## Proposed Stack + - Vite, React, TypeScript. - Three.js for preview. - Optional `@react-three/fiber` if component-based scene composition is useful. @@ -230,6 +242,7 @@ ZIP bundle: - Playwright for viewport smoke tests and export flow checks. ## Files Affected + Initial implementation will likely add: - `package.json` and lockfile. @@ -240,6 +253,7 @@ Initial implementation will likely add: - `docs/` for planning and geometry notes. ## Steps + 1. [x] Confirm exact Gridfinity baseplate profile and first connector strategy. 2. [x] Scaffold TypeScript web app. 3. [x] Implement unit conversion, centered padded envelope sizing, and derived dimension summary. @@ -253,4 +267,5 @@ Initial implementation will likely add: 11. [x] Add browser smoke tests for preview and export. ## Open Questions + - None for v1 planning. Remaining choices are implementation details unless requirements change. diff --git a/docs/PRODUCT-VISION.md b/docs/PRODUCT-VISION.md new file mode 100644 index 0000000..118eec3 --- /dev/null +++ b/docs/PRODUCT-VISION.md @@ -0,0 +1,128 @@ +# Benchfinity — Product Vision + +> Source of truth for **what** Benchfinity is and does. Product vision and roadmap +> live in this repo and are owned here; `../brand/` owns everything else +> company-related (marketing, brand, community, content, legal, operations, +> website) and defers to this repo on product. +> +> This document supersedes the scope sketch in `docs/WORKBENCH-VNEXT.md`, which +> was an earlier draft. It was derived directly from a product discovery +> conversation with the founder plus a review of the existing V1 POC — not from +> external research (research informs positioning only, which is a `../brand/` +> concern). + +## One line + +An open-source online platform to design and print your **entire workshop +organization system in one place** — measure your real drawers and spaces, +generate grids that fit them, fill those grids with an ever-growing family of +specialized bins, compose it all into reusable systems, and export print-ready +files. + +## The problem + +The founder couldn't find a tool that made toolbox-drawer grids the way they +wanted, and refuses to bounce between four different tools to make grids, then +boxes, then tool outlines. Benchfinity is the one place that does it all — +grounded in real, measured furniture rather than abstract dimensions. + +## What Benchfinity generates + +The generation model is layered. Everything starts with a grid. + +1. **Grids (baseplates)** — the foundation. You enter a _measured_ drawer/space; + Benchfinity fits whole Gridfinity-compatible cells into it, centers the grid + with even perimeter padding, and auto-splits the result to fit your printer + bed. _This exists today in the V1 POC_ (`src/geometry`, `src/export`). +2. **Bins** — sit on grids. This is an **open-ended, ever-growing family of + typed generators**, not a fixed feature set: + - open bins, storage boxes, + - **tool-traced** bins (a tool outline becomes a custom cutout), + - **specialty** bins (e.g. HO-train storage), + - **specialized systems** (sockets, wrenches, and more added continuously). + + New bin types are expected to be added all the time. **The growing catalog is + the product's core value** — so a bin type is effectively a plugin: a + self-contained generator with its own parameters, geometry, and preview, all + emitting a Gridfinity-compatible footprint so it drops onto any grid. + +3. **Stand-alone boxes** — Modibox-style modular boxes that **interoperate** with + grids and bins but are not grid-bound. A parallel part track under the same + compatibility contract. + +## How work is organized + +### Access line (anonymous vs. account) + +- **No account:** generate any single part (grid or bin), see it in real-time 3D, + and export STL/3MF. You **cannot** save, reuse, or assemble parts into a system. +- **Logged in:** save work, reuse parts and grids, compose full systems, keep an + export history, and (as a richer end-state) visually lay out and place bins onto + grids and have it all remembered. + +### Composition hierarchy (logged-in) + +``` +Account + └── System (real furniture: "Tool Box 1", the HO-train table) + └── Container Type (a measured drawer/shelf + its reusable grid) + └── Container Instance (drawer 1…12 — same grid reused, unique contents) + └── Inserts (bins/holders drawn from the Component Library) +``` + +### Three independent reuse axes + +- **Part reuse** — a saved part design (e.g. a 2×1 scoop bin) dropped across any + number of containers, via a **Component Library**. +- **Container/grid reuse** — define a drawer's dimensions + grid once and stamp it + across N identical drawers (the "12 identical drawers, different contents" + case). The grid is shared; the contents are per-instance. +- **Layout reuse** _(rich end-state)_ — saved spatial arrangements of bins on a + grid, remembered per container instance. + +## Experience and outputs + +- **Real-time 3D rendering** of every part and layout (the POC-quality + React + Three.js frontend is the bar). +- Export **STL** and **3MF** (Bambu-style), including split bundles when a part + exceeds the printer bed. +- Built-in **catalog of known printers and bed sizes** (exists in the POC). + +## Architecture and foundations + +- **Frontend:** the existing React + Three.js app, held to POC quality or better. +- **Backend:** **QQQ + Postgres** for accounts, persistence, CRUD/REST, admin, and + server-side processes — also a deliberate **showcase of QQQ + the agentic + development process**. +- **Auth / accounts:** required for all stateful features. Provider is **open** + (Authentik, already run elsewhere in the founder's stack, vs. a simpler embedded + option) — to be decided at the platform layer. +- **Pure core preserved (issue #5):** the V1 geometry/validation/export core stays + free of React, DOM, and storage. Each generator (grid, each bin type, stand-alone + box) is a pure module wrapped behind persistence — we _wrap_, never _rewrite_ the + math. This is also what makes the bin-plugin model and future community + contribution possible. +- **License:** AGPL-3.0 everywhere. +- **Monetization:** fully free + GitHub Sponsors / sponsorware only. No paid tier. + The QQQ backend is justified by the product (accounts, reuse, systems) and the + showcase — not by revenue. +- **Delivery & ops:** built, hosted, and run solo on the existing Talos k8s cluster + via the ArgoCD `-cd`-repo GitOps pattern on a dedicated static IP. **Continuous + delivery — small increments shipped to production; launch then build.** +- **Definition of success:** community-canonical positioning (the metric and the + go-to-market work live in `../brand/`). + +## Scope boundaries + +- **In scope (this repo):** the application — generators, persistence, accounts, + composition/reuse, exports, the frontend, the QQQ backend, deployment. +- **Out of scope (in `../brand/`):** marketing, adoption channels, positioning, + community, brand, content, legal, sustainability strategy. + +## Open decisions (resolved later, not invented now) + +- Auth/account provider (Authentik vs. simpler embedded option). +- The concrete set and ordering of bin types (the family grows continuously; the + point is the plugin model, not a fixed list). +- When visual layout/placement of bins onto grids ships (a richer end-state, not + the first slice). diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md new file mode 100644 index 0000000..c3cec73 --- /dev/null +++ b/docs/ROADMAP.md @@ -0,0 +1,410 @@ +# Benchfinity Roadmap + +> **North Star:** Design and print an entire workshop organization system — grids, bins, and stand-alone boxes — in one open-source place. + +This is the authoritative phase plan for Benchfinity. It supersedes the older single-bucket `Workbench VNext` plan in scope sequencing; `docs/PRODUCT-VISION.md` remains the authoritative product vision and `AGENTS.md` the operational guide. + +## Operating principles + +- **Continuous delivery / ship small to prod.** Every increment is independently shippable. Small changes land in production continuously rather than in big-bang releases. +- **Launch then build.** Increment 0 puts the existing V1 grid generator live first; product depth is added against a running, public app. +- **Pure core preserved (issue #5).** The V1 generator core (`src/geometry/*`, `src/validation.ts`, `src/export/*`) plus `src/design.ts`/`src/geometry/types.ts` stay pure (no React/DOM/storage) and are **wrapped, never rewritten**. Persistence stores `PlateInput` + `selectedPrinterId` + `schemaVersion` (`BaseplateDesign`) and a JSON-safe projection of the derived `PlateLayout`; Three.js meshes are never persisted — they are recomputed on load. +- **Two parallel tracks.** A _backbone_ track (logged-in, **A → B**: backend + Postgres + auth/RBAC + app shell, then the persisted/reusable grid) runs in parallel with a _generators_ track (anonymous, client-side, **C**: the bin plugin family and stand-alone boxes shipping straight to the live anonymous app). The two tracks are decoupled by the auth gate, so anonymous generate → preview → export keeps working unchanged throughout. + +## Phase flow (two-track parallelism) + +```mermaid +flowchart TB + Inc0["Increment 0 — Launch
V1 grid live in prod"] + + subgraph Backbone["Backbone track (logged-in)"] + direction TB + A["Phase A — QQQ Spine
backend + Postgres + auth/RBAC + app shell"] + B["Phase B — Grid Persisted & Reusable
wrap pure core, container types/instances, export history, tests"] + A --> B + end + + subgraph Generators["Generators track (anonymous, client-side)"] + direction TB + C["Phase C — Bin Plugin Engine
bin types + stand-alone boxes"] + end + + D["Phase D — Composition & Visual Layout
rich logged-in end-state"] + + Inc0 --> A + Inc0 --> C + B --> D + C --> D + + classDef anon fill:#e6f7ff,stroke:#1d76db; + classDef back fill:#f3e6ff,stroke:#5319e7; + class Inc0,C anon; + class A,B,D back; +``` + +Increment 0 unblocks both tracks. The backbone (A → B) and the generators track (C) then proceed in parallel; both feed the Phase D rich end-state. + +--- + +## Increment 0 — Launch: V1 grid live + +**Goal:** Get the existing anonymous client-side V1 grid generator live in production at `benchfinity.com` on Talos `k8s-prod` via a new `benchfinity-cd` GitOps repo. Deployment only — no app features, no backend, no auth. This is the launch-then-build beachhead the whole continuous-delivery model depends on. + +> **Lineage:** builds on the closed Repository Foundation work (#9), which already produces the multi-arch GHCR image `ghcr.io/benchfinity/workbench` (rolling `:develop` + immutable `:-SNAPSHOT.`) and the OCI Helm chart `oci://ghcr.io/benchfinity/charts/benchfinity`. + +### 0.1 — Scaffold the Benchfinity-CD GitOps repo from the Website-CD pattern + +- **Ships to prod:** Repo scaffold only; nothing live yet. Foundation for 0.2–0.4. +- **Summary:** Create a new public repo `BenchFinity/benchfinity-cd` (clone at `/Users/james.maes/Git.Local/benchfinity/benchfinity-cd`) modeled on `/Users/james.maes/Git.Local/Kof22/Website-CD`. Because Benchfinity ships a **Helm chart** while Website-CD is raw Kustomize, the CD repo must **wrap the published OCI chart** rather than re-vendor manifests — recommended: an ArgoCD Application with inline per-env Helm values (`base/` + `overlays/production/`), honoring the issue #5 "wrap, never rewrite" principle at the deploy layer too. The workbench repo only builds the image and publishes the chart; the CD repo is the single source ArgoCD syncs. +- **Key tasks:** + - `gh repo create BenchFinity/benchfinity-cd --public`; clone locally. + - Directory layout mirroring Website-CD: `base/`, `overlays/production/`, `argocd/`. + - `base/values.yaml` for cross-env-stable chart defaults (`frontend.enabled=true`, replicas, resources from `deploy/helm/benchfinity/values.yaml`). + - `overlays/production/values.yaml` for prod overrides (ingress, host `benchfinity.com`, className `traefik`, cert-manager + Traefik annotations, TLS, frontend HA). + - `AI_STARTHERE.md` + `README.md` documenting the repo role, `main` branch, and ArgoCD sync target. + - Document that the image is **PUBLIC** (verified anon pull of `ghcr.io/benchfinity/workbench:develop` returns 200) — **no imagePullSecret**; do not copy kof22's private-registry assumptions. + - Keep all OCI references lowercase (`ghcr.io/benchfinity/...`) despite the `BenchFinity/Workbench` GitHub casing. +- **Dependencies:** none. +- **Maps to issues:** Increment 0 (Launch) — new issue _"Increment 0: Create Benchfinity-CD GitOps repo (ArgoCD + Kustomize)"_. + +### 0.2 — Wire the ArgoCD Application to sync the published chart into the cluster + +- **Ships to prod:** Frontend pods running in-cluster (ClusterIP), reachable via port-forward; not yet publicly exposed. +- **Summary:** Create the ArgoCD Application (modeled on `deploy/argocd/application.yaml`, repointed at the CD repo) deploying into a dedicated `benchfinity` namespace with automated prune + self-heal and `CreateNamespace=true`. For a controlled first launch, pin an immutable `:-SNAPSHOT.` tag; document the upgrade path to ArgoCD Image Updater digest-tracking on `:develop` for true CD. +- **Key tasks:** + - `benchfinity-cd/argocd/application-production.yaml`: `repoURL` → CD repo, `targetRevision=main`, `source=overlays/production`. + - `destination.namespace=benchfinity`, `syncPolicy.automated{prune:true,selfHeal:true}`, `syncOptions[CreateNamespace=true]`. + - Helm source mode: chart `benchfinity` from `oci://ghcr.io/benchfinity/charts`, with overlay values (or multi-source: git values + OCI chart). + - Override `frontend.image.tag` in the overlay (chart appVersion is decoupled from the image tag). + - Apply and confirm Healthy/Synced; `kubectl get pods -n benchfinity` shows the frontend Running (no pull secret). +- **Dependencies:** 0.1. +- **Maps to issues:** Increment 0 (Launch) — new issue _"Increment 0: Deploy V1 grid generator to production"_. + +### 0.3 — Configure Traefik ingress + TLS + static-IP exposure for benchfinity.com + +- **Ships to prod:** `benchfinity.com` served over HTTPS through Traefik on the static IP once DNS (0.4) resolves; HTTP redirects to HTTPS. +- **Summary:** Expose the frontend publicly over HTTPS via the cluster Traefik ingress on the dedicated static IP, with cert-manager issuing a Let's Encrypt cert. Replicate the Website-CD two-Ingress pattern: a `websecure` Ingress carrying the `cert-manager.io/cluster-issuer` annotation + a `tls` block, plus a separate `web`-entrypoint HTTP→HTTPS redirect. The Benchfinity chart renders only **one** Ingress with no redirect/middleware support, so the redirect is added out-of-band in the overlay. +- **Key tasks:** + - Overlay `ingress.enabled=true`, `className=traefik`, host `benchfinity.com` (path `/` → frontend). + - `cert-manager.io/cluster-issuer: letsencrypt-prod` (confirmed present and `Ready=True` on `k8s-prod`; Website-CD happens to use `zerossl-production`), `traefik.ingress.kubernetes.io/router.entrypoints: websecure`. + - `ingress.tls: [{hosts:[benchfinity.com], secretName: benchfinity-tls}]`. + - HTTP→HTTPS redirect via a Traefik redirect middleware annotation or an extra overlay manifest modeled on `kof22-website-ingress-http-redirect.yaml`. + - Confirm static-IP exposure is via the shared cluster Traefik LoadBalancer (apps are ClusterIP behind it; no per-app `loadBalancerIP`); verify Traefik's external IP. + - Decide www handling (apex + optional `www` SAN). +- **Dependencies:** 0.2. +- **Maps to issues:** Increment 0 (Launch). + +### 0.4 — Point DNS (Route 53) at the static IP and verify public anonymous HTTPS load + +- **Ships to prod:** **`benchfinity.com` is LIVE** — public, anonymous V1 grid generator with 3D preview and STL/3MF/ZIP export over valid HTTPS. Increment 0 complete. +- **Summary:** Create the Route 53 A record for `benchfinity.com` → dedicated static IP, wait for the cert to issue, then verify end-to-end: HTTPS resolves with a valid cert, the app loads anonymously, and the no-account generate → 3D preview → STL/3MF export flow works. +- **Key tasks:** + - Route 53 apex A record → static IP (+ `www` if in scope); confirm `dig`. + - Confirm `kubectl get certificate -n benchfinity` shows `Ready=True`. + - `curl -I https://benchfinity.com` → 200 valid LE cert; `http://` → 301/308. + - Verify anonymous app end-to-end (generate grid, render 3D, export STL + 3MF). + - Fix stale `docs/DEPLOY.md` sections 1 & 4 that claim the image is private / a pull secret is required (verified false); add a pointer to `benchfinity-cd` as the prod GitOps source of truth. +- **Dependencies:** 0.3. +- **Maps to issues:** Increment 0 (Launch). + +> **Sequencing note:** Let's Encrypt HTTP-01 cannot issue until DNS resolves to the static IP and :80 is reachable — a chicken-and-egg with 0.3. Mitigate by using a DNS-01 (Route 53) solver, or accept a short Pending window until DNS propagates. + +--- + +## Phase A — QQQ Platform Spine + +**Goal:** Stand up a QQQ + Postgres backend, ground the persistence schema in the confirmed `Account → System → ContainerType → ContainerInstance → Inserts` hierarchy, add pluggable auth + account-scoped RBAC, and ship a signed-in React app shell that calls QQQ's auto-generated REST API via `qqq-frontend-core`. The pure geometry/export core and the Three.js generator UI are **not touched** here — Phase A only builds the spine that Phase B wraps them behind. Runs in parallel with the anonymous generators track (Phase C). + +> **QQQ-cheap vs custom split (core Phase A principle):** QQQ makes the persistence/operational tier nearly free — metadata-defined Postgres CRUD, auto-generated REST + OpenAPI 3, a Material-UI admin dashboard for **internal ops only** (never embedded in the product), QProcesses, RBAC (`qbit-user-role-permissions` + `QPermissionRules`), pluggable auth/sessions, Quartz scheduling. The differentiators get zero help and stay custom: the React + Three.js generator UI and the anonymous shareable-link UX. Adopting QQQ is a deliberate shift from client-only (~$0/mo) to client + Java 21/Javalin/Quartz + Postgres, justified by the accounts/persistence goal **and** the explicit QQQ + agentic-process showcase — not by revenue (free + sponsorware, AGPL-3.0). +> +> **Pin everything, budget for thin docs:** org renamed Kingsrook → QRun-IO but Maven coords remain `com.kingsrook.qqq`; README-stable (v0.35.0) lags the latest tag (v0.40.0). Pin a specific QQQ + `qbit-bom` version and verify coordinates; authoritative knowledge is the source on `develop` and `qqq-sample-project`. + +### A1 — QQQ + Postgres backend scaffold (the spine, free CRUD/REST/admin) + +- **Summary:** Stand up the QQQ Java backend as a sibling app inside this repo's existing backend slot (the Helm chart, compose `full` profile, and CI already reserve a backend image + Postgres + MinIO). Bootstrap from `qqq-app-starter`/`new-qqq-application-template` (`qqq-backend-core` + `qqq-backend-module-postgres`/`-rdbms` + `qqq-middleware-javalin` + `qqq-openapi`), pinned to a concrete release. Resolves the #1 repo/app-shape question: backend lives **in-repo as a separate Maven module** (e.g. `/server`) producing `ghcr.io/benchfinity/workbench-api`, matching the disabled `backend.image` already in `values.yaml`/`compose.yaml`. Deliverable: a running QQQ instance with a trivial smoke table proving auto-generated REST + OpenAPI + the admin dashboard come for free. No product schema yet (that is A2). Pure core and Three.js UI untouched. +- **Key tasks:** + - Resolve #1: document the in-repo sibling-module decision in a new ADR and update `AGENTS.md` architecture boundaries. + - Pin `com.kingsrook.qqq` artifacts in `pom.xml`; configure `PostgreSQLBackendMetaData` from env (`DATABASE_*`, password via `secretKeyRef` — names already defined by the Helm `backend-deployment` template; Javalin on `backend.containerPort` 8080). + - Define one trivial `QTableMetaData` to verify auto-REST + OpenAPI 3 + Material-UI admin dashboard against local Postgres. + - Wire the QQQ backend into the compose `full` profile (already has backend + `postgres:16` + minio) so `docker compose --profile full up` runs frontend + QQQ + Postgres locally. + - Add a Java 21 Dockerfile producing `workbench-api` (separate from the frontend distroless nginx Dockerfile). + - Add a Maven build + minimal backend CI job so the HIGH+ security gates and PR checks extend to the Java module. +- **Dependencies:** none (within Phase A). +- **Maps to issues:** #1 (rewritten → _"Phase A: Confirm QQQ backend repo, app shape, and local dev run"_). + +### A2 — Postgres persistence schema grounded in the confirmed hierarchy + +- **Summary:** Model the confirmed hierarchy as QQQ tables over Postgres: `account → system → container_type → container_instance → insert`, plus `user`, `account_membership`, `export_artifact`, `printer_profile`, and a discriminated `Part/WorkbenchItem` table carrying `partType` + `schemaVersion`. Resolves #2. The serializable unit already exists client-side: `BaseplateDesign` (`src/design.ts`: `schemaVersion` + `PlateInput` + `selectedPrinterId`) and the derived `PlateLayout` (`src/geometry/types.ts`). Persist `PlateInput` + derived `PlateLayout` as JSONB (never Three.js meshes — recompute on load), promoting key filterable fields (finished width/depth, cell size, tile count, `partType`) to structured columns. Each part type is a discriminated row (`gridfinityBaseplate` first; bins/boxes added later by Phase C as new `partType` values + schemaVersion bumps) so the open-ended generator family persists without schema forks. The pure core is wrapped, not rewritten (#5). +- **Key tasks:** + - Define `QTableMetaData` for `account`, `user`, `account_membership`, `system`, `container_type`, `container_instance`, `insert`, `component_library_part`, `printer_profile`, `export_artifact`. + - Map the client model into columns: store `PlateInput` + `selectedPrinterId` + derived `PlateLayout` as JSONB; promote finishedWidth/Depth/unit, cell size, tile count, `partType` to structured columns. + - Conventions: UUID PKs, slugs on account/system/container, integer `revision` or `updated_at` for optimistic concurrency, `createdAt`/`updatedAt`, JSONB for versioned payloads. + - Carry `schemaVersion` (`DESIGN_SCHEMA_VERSION`, currently 1) on every persisted part. + - Indexes for account-scoped listing: `(account_id)`, `(system_id)`, `(container_type_id)`, `(account_id, part_type)`. + - `ExportArtifact` metadata in Postgres (exportType, version, filename, contentType, sizeBytes, storageKey, input hash) with binaries in MinIO/S3 (objectStore env already in the chart) — anchors the #3 storage decision. + - Backend tests for CRUD and the JSONB round-trip of `PlateInput`/`PlateLayout`. +- **Dependencies:** A1. +- **Maps to issues:** #2 (rewritten → expanded to the confirmed `System/ContainerType/ContainerInstance/Inserts` hierarchy; the older `account/project/workbench_item` draft in `WORKBENCH-VNEXT.md` is superseded). + +### A3 — Auth + account-scoped RBAC (accounts, membership, access rules) + +- **Summary:** Add authentication and account-scoped authorization so all stateful features sit behind an account boundary, while the anonymous generate → preview → export path stays fully public. Auth provider choice is **OPEN and resolved here** (ADR): Authentik (already in the founder's stack, via QQQ's OAuth2 module) vs a simpler QQQ table-based / Auth0 option. RBAC uses `QPermissionRules` + the `qbit-user-role-permissions` QBit to enforce owner/admin/member/viewer roles scoped by `account_membership`: every System/ContainerType/Insert/ExportArtifact query is filtered by `account_id`. Crucially, the anonymous public surface is **not** QQQ "record sharing" (that is authenticated scope-based sharing) — it is the public read path on QQQ's `FullyAnonymousAuthenticationModule`, which the Phase C generators track and any future share-link feature lean on. +- **Key tasks:** + - ADR the auth provider decision; configure the chosen QQQ auth module + session handling. + - Add `qbit-user-role-permissions`; map owner/admin/member/viewer onto `account_membership`. + - Account-scoped security filters via `QPermissionRules` on all reads/writes. + - Role policy: owners/admins manage account + members; members create/edit systems & parts; viewers read + download exports. + - Stand up the anonymous public path on `FullyAnonymousAuthenticationModule` (read-only / no persistence), distinct from authenticated sharing. + - Tests proving account scoping (user in account A cannot read/write account B's data). +- **Dependencies:** A1, A2. +- **Maps to issues:** #2 — new issue _"Phase A: Decide auth/account provider (Authentik vs embedded)"_. + +### A4 — Signed-in React app shell calling the QQQ REST API + +- **Summary:** Build the authenticated app shell as a **separate custom frontend surface** calling QQQ's auto-generated REST API via the `qqq-frontend-core` TypeScript client — explicitly **not** the QQQ Material-UI admin dashboard (internal ops only, never embedded). Resolves #4. The shell adds an account switcher + System/Container navigation, a top bar (item name, save status, export action, user menu), and the route structure (account / system / container / item / settings) **around** — not replacing — the existing generator. The current generator already stores its state as the exact `BaseplateDesign` payload in localStorage via `settings.ts`; A4 introduces sign-in and navigation/persistence affordances while preserving the local generate flow. This is the seam Phase B fills in. Three.js preview and pure geometry/export core untouched (#5). +- **Key tasks:** + - Account switcher, System/Container navigation, top bar, route structure for account/system/container/item/settings. + - Integrate `qqq-frontend-core` against the QQQ OpenAPI for typed REST access; do not fork/embed `qqq-frontend-material-dashboard`. + - Wire sign-in/sign-out; gate stateful routes behind auth while leaving the anonymous flow fully usable signed-out. + - Mount the existing generator inside the item route reading/writing the same `BaseplateDesign` shape; keep localStorage as the anonymous draft store; add "save to account" as the authed action. + - Keep `App.tsx` top-level orchestration only and generator components presentational (AGENTS.md boundaries). + - Frontend tests for auth-gated routing and the API client contract (mock the QQQ REST layer). +- **Dependencies:** A1, A2, A3. +- **Maps to issues:** #4 (kept; minor vocabulary alignment to System/Container hierarchy). + +--- + +## Phase B — V1 Grid Persisted & Reusable + +**Goal:** Wrap the pure baseplate generator as the first saved part type behind the Phase A QQQ/Postgres/auth spine; add ContainerType/ContainerInstance reuse, export history + artifact storage, and tests. **Hard constraint #5:** `src/geometry/*`, `src/validation.ts`, `src/export/*` stay pure and are **wrapped, never rewritten** — persist `PlateInput` + derived `PlateLayout` only; recompute Three.js meshes on load. Depends on Phase A delivering the QQQ backend, Postgres, auth provider, Account/User/Membership + RBAC, and the signed-in shell. Every increment is independently shippable; the anonymous app keeps working unchanged because all persistence is auth-gated. + +> **Wrap seam is real and already designed for:** `BaseplateDesign` carries `DESIGN_SCHEMA_VERSION=1` and is documented as "the persistence payload the Workbench (VNext) will store." #5 is honored by treating geometry/validation/export as read-only: `deriveLayout(input)` and `createPlateModels(layout,input)` are **called** on load, never reimplemented or serialized. `PlateLayout.tiles` carry numeric specs (safe) but `TileModel`/`GeometryPart` carry `BufferGeometry` (not safe, not persisted). +> +> **Suggested ship order:** B0 (pure, zero-risk, freezes the seam) → B1 (schema, needs the Phase A backend live) → B2 (save/load UI) → B3 (export history + storage) → B4 (tests, gates each). + +### B0 — Serialization contract & version boundary for the pure core (the wrap seam) + +- **Ships to prod:** **Yes** — pure frontend refactor, no backend dependency. Ships to the live anonymous app with zero user-visible change; de-risks every later increment by freezing the wrap seam first. +- **Summary:** Establish the explicit, tested serialization boundary **before** any persistence is wired. Promote `BaseplateDesign` to the canonical save payload, add a serialize/deserialize pair plus a forward-compatible migration shim, and define what derived layout metadata is persisted vs recomputed. This is the single seam honoring #5. +- **Key tasks:** + - New pure `src/persistence/baseplateRecord.ts`: `SavedBaseplate = { schemaVersion, design: BaseplateDesign, derived: PersistedLayoutMeta }` where `PersistedLayoutMeta` is a JSON-safe projection of `PlateLayout` (cols, rows, grid/padding/printable mm, tile count + summaries, errors, warnings) — **never** `BufferGeometry`/`TileModel`/meshes. + - `serializeBaseplate(...)` calls the **existing** `deriveLayout(input)` (do not reimplement); `deserializeBaseplate(record)` returns the inputs `App` already feeds into `deriveLayout`/`createPlateModels`. + - `migrateBaseplateRecord(raw)` keyed on `schemaVersion` (identity for v1). + - New pure `src/persistence/inputHash.ts`: `stableStringify` + a small synchronous hash (e.g. FNV-1a) over the canonical `BaseplateDesign`, so "does the latest export match the current design" (#7) is decidable identically on client and server. + - Refactor `App.tsx` **only** at the orchestration layer to round-trip through serialize/deserialize (no behavior change for anonymous flow). + - Tests: round-trip stability, projection excludes meshes, v1 migration identity, hash stability + sensitivity. +- **Dependencies:** none. +- **Maps to issues:** #5, #6. + +### B1 — Persistence model & account-scoped schema (QQQ tables + Postgres migrations) + +- **Ships to prod:** **Yes** — additive schema + migrations behind auth; no anonymous-flow change. Ships once Phase A's backend/Postgres/auth are live. +- **Summary:** Implement the durable schema for the saved-grid slice on top of Phase A's Account/User/Membership, reconciling the two documented vocabularies: `WORKBENCH-VNEXT.md`'s Project/WorkbenchItem (older draft) and `PRODUCT-VISION.md`'s authoritative `System → ContainerType → ContainerInstance → Inserts`. Phase B implements `System` (= Project), `ContainerType` (the measured drawer + its reusable grid, storing the `BaseplateDesign` JSONB + derived `PlateLayout` projection + structured filter columns), `ContainerInstance` (drawer 1..N reusing the same grid, unique contents), plus `printer_profile` and `export_artifact`. UUID-keyed, account-scoped, with revision-based optimistic concurrency. +- **Key tasks:** + - QQQ tables scoped by `account_id`: `system`, `container_type` (FK `system_id`; `BaseplateDesign` JSONB + derived `PlateLayout` JSONB from B0 + structured cols: cols, rows, finished_w/d_mm, tile_count, validation_ok), `container_instance` (FK `container_type_id`; ordinal 1..N; unique per-instance contents/notes), `printer_profile`, `export_artifact` (defined in B3). + - Conventions: UUID PKs (`gen_random_uuid`), slugs, integer `revision` (bump on update; reject stale writes with 409), audit columns, JSONB for versioned payload, structured columns only for listing/filtering fields. + - Non-bypassable account-scoping security filter on every table (mirrors Phase A RBAC roles). + - Indexes: `(account_id, system_id)`, `(account_id, container_type_id)`, unique `(account_id, system_slug)`, `(account_id, updated_at desc)`. + - Forward/backward idempotent migrations + a dev seed (one account/user/System with one ContainerType + 12 ContainerInstances to exercise the identical-drawers case). + - Persist `schemaVersion` alongside the JSONB so `migrateBaseplateRecord` can run on load. +- **Dependencies:** B0. +- **Maps to issues:** #2, #6. Record the vocabulary mapping (PRODUCT-VISION canonical) in an ADR. + +### B2 — Save / load the baseplate behind the app shell (wrap the generator as the first saved part) + +- **Ships to prod:** **Yes** — anonymous flow ships unchanged immediately; logged-in save/load lights up the moment B1's backend is reachable. The two paths are decoupled by the auth gate. +- **Summary:** Inside the Phase A signed-in shell, a saved `ContainerType` **is** the wrapped V1 generator. Existing `PlateControls`/`WorkspacePanel`/preview/validation/STL/ZIP/3MF behavior is preserved verbatim; App-level orchestration gains save/load that round-trips through the B0 seam to the B1 backend. Anonymous users keep the exact current single-part experience (no save). Logged-in users persist a ContainerType, reload it with identical preview-relevant state, and stamp N ContainerInstances that reuse the one grid (the headline "12 identical drawers" case). +- **Key tasks:** + - New `src/persistence/api.ts` wrapping QQQ/REST CRUD for system + container_type + container_instance; account-scoped server-side. + - Save/load orchestration in `App.tsx` **without touching geometry/validation/export**: on Save, `serializeBaseplate(...)` → POST/PUT carrying `revision`; on Load, fetch → `migrateBaseplateRecord` → `deserializeBaseplate` → feed existing `deriveLayout`/`createPlateModels` so **meshes are recomputed, never transported**. + - Gate persistence on auth; keep `PlateControls`/`WorkspacePanel` presentational. + - ContainerType + ContainerInstance UI: create a ContainerType inside a System; "stamp" it into instances 1..N. + - Surface 409 stale-revision conflicts as a non-destructive reload-or-overwrite prompt. + - Restore-fidelity check: a loaded design re-derives a `PlateLayout` equal to what was saved (warnings/validation included). +- **Dependencies:** B0, B1. +- **Maps to issues:** #5, #6. + +### B3 — Export artifact storage decision + export history with re-download + +- **Ships to prod:** **Yes** — object store + Helm `objectStore` values are already scaffolded (`enabled:false` today); flip on when the backend is live. Anonymous direct-download path ships unchanged throughout. +- **Summary:** Implement the #3 storage decision and the #7 history UI. Per the `-cd` pattern and the already-provisioned MinIO/B2 object store, the MVP decision is: generate artifacts **client-side** from the saved design (reusing `src/export` verbatim), upload the blob to object storage via the backend, persist metadata in Postgres, and serve re-downloads through short-lived presigned URLs. Each export records the B0 `inputHash` so the UI shows whether the latest export matches the current design. +- **Key tasks:** + - ADR the #3 decision: MVP = client-side generation (no #5 rewrite) + object storage for binaries + Postgres metadata + presigned short-lived URLs; reject Postgres-bytes and browser-only for the saved flow. + - `export_artifact` metadata: id, `container_type_id`, `account_id`, `export_type` (stl|zip|3mf), monotonic `version`, filename (reuse `buildExportFilename`), content_type, size_bytes, storage_key, `input_hash`, generation_summary, audit columns. + - Upload path: `App` produces the blob via the **existing** `createExportBlob` (untouched), PUTs to a backend process `generate_export_artifact` that streams to object storage and writes metadata; anonymous users keep direct browser download with no upload. + - Export-history inspector panel (presentational): list type/version/filename/size/created, re-download via presigned URL, and a "matches current design" badge comparing stored `input_hash` to the live design's hash. + - `delete_export_artifact` process (RBAC: owner/admin). +- **Dependencies:** B0, B1, B2. +- **Maps to issues:** #3, #7. + +### B4 — Phase B test coverage — raise the 34-test baseline + +- **Ships to prod:** **Yes** — tests gate every prior increment's PR; CI enforces lint/format/typecheck/test/build on each. No increment merges to `develop` without green gates above the 34 baseline. +- **Summary:** Add focused coverage for every Phase B flow while keeping the existing 34 geometry/export regression tests green (current baseline: 8 layout + 4 model + 8 export + 4 filenames + 4 validation + 3 printers + 3 settings = 34). Cover account scoping/membership, System/ContainerType/ContainerInstance CRUD, save/load round-trip fidelity, the 12-identical-drawers reuse case, optimistic-concurrency conflicts, and export-artifact metadata + history + freshness. +- **Key tasks:** + - Account scoping & RBAC matrix (owner/admin/member/viewer) enforced; cross-account denial. + - CRUD: System/ContainerType create/rename/duplicate/archive; ContainerInstance stamp 1..N + per-instance independence. + - Save/load fidelity (#5/#6): serialize → persist → load → migrate → deserialize → re-derive `PlateLayout` equals the saved projection (incl. warnings/validation); assert no meshes persisted. + - Optimistic concurrency: stale-revision update returns 409, client surfaces conflict without data loss. + - Export artifacts (#7): metadata correctness, version increment, presigned-URL re-download, freshness badge flips on a changed `PlateInput` field. + - Run the full gate (lint, format:check, typecheck, test, build); new total must exceed 34. Backend tests use the compose `full` profile / ephemeral Postgres; frontend persistence tests run offline against a stubbed transport. +- **Dependencies:** B0, B1, B2, B3. +- **Maps to issues:** #8, #5, #6, #7. + +--- + +## Phase C — Bin Plugin Engine (anonymous, client-side, parallel to A → B) + +**Goal:** Build the pluggable typed bin generator family and ship it straight to the live **anonymous** client app, in parallel with the Phase A/B backbone. Bin plugins mirror the existing `src/geometry` pure-core pattern (free of React/DOM/storage, #5) and reuse `GeometryPart[]` so the R3F preview and the export core (`serializeBinaryStl`, `createThreeMfPackageFromParts`) consume them **unchanged** — wrap-not-rewrite by construction. The catalog is the moat, so the registry must absorb new types cheaply (one module + one registry line + one test). + +> **Footprint contract is the single load-bearing compatibility guarantee** and must be **derived from `GRIDFINITY_PROFILE`**, not re-declared: a bin base footprint is the inverse of a baseplate socket — per-cell square of `socketOpeningMm` (37.8) with corner radius `socketCornerRadiusMm` (4) on a `defaultCellSizeMm` (42) pitch, minus a clearance constant. Tests assert footprint corner radius/opening equal the baseplate's socket constants so "drops onto any grid" is provable, not aspirational. +> +> **Scope discipline:** the photo-capture/AI tracing pipeline is **out of Phase C** — tool-traced bins (C3) consume an already-given contour polyline param only. + +### C1 — Plugin framework + Gridfinity footprint contract (the registry spine) + +- **Ships to prod:** Framework + constants + reference stub only; no user-facing bin generator (dead code path until C2). Safe to ship to the live anon app. +- **Summary:** Establish the pure plugin substrate every bin type plugs into. Define the footprint contract, the `BinGeneratorPlugin` interface, and a `BinRegistry`. Ships with **zero** concrete bin types beyond a trivial reference stub used only by tests, so the framework lands and is exercised before any product geometry. Add `GRIDFINITY_BIN_PROFILE` named constants (base profile 0.8/1.8/2.15 = 4.95mm; 7mm height unit; stacking lip 0.7/1.8/1.9 + 1.2 support + 0.6 fillet; top radius 3.75 / bottom 1.6; magnet 6×2mm dia/thick = r3.0/depth2.0 standard, r2.93/depth1.9 refined; M3 screw r1.5 + 2.75 counterbore; 0.5mm body clearance), cross-referencing `GRIDFINITY_PROFILE` as the source of truth. +- **Key tasks:** + - `src/geometry/bins/binProfile.ts` exporting `GRIDFINITY_BIN_PROFILE` as `const`, importing `GRIDFINITY_PROFILE` so the base footprint derives from the same source, not a magic literal. + - `BinFootprint` type + pure `computeBinFootprint(unitsX, unitsY, cellSizeMm)` returning the exact per-cell base profile a baseplate socket accepts. + - `BinGeneratorPlugin` interface: `{ id, label, description, schemaVersion, defaultParams, paramSchema: ParamField[], validate, deriveBinLayout, buildBinModel }` where `BinModel` reuses `GeometryPart[]`. + - `src/geometry/bins/registry.ts`: pure `BinRegistry` (register/get/list/has); registration via an explicit `registerBuiltInBins()` composition point, **never** import side effects (preserves tree-shaking + test isolation). + - `BinModel`/`BinLayout` types mirroring `TileModel`/`PlateLayout` so preview + export work unchanged. + - Reference test-only stub plugin (flat NxM pad) to prove registry + footprint + export + 3mf end-to-end. + - Tests: footprint corner radius == `socketCornerRadiusMm`, base profile heights sum to 4.95mm, magnet constants, registry round-trip, stub feeds `serializeBinaryStl` + `createThreeMfPackageFromParts` with non-empty triangles. Raises the baseline. +- **Dependencies:** none. +- **Maps to issues:** #5 (preserve/wrap pure core); Workbench VNext milestone → new issue _"Phase C: Bin plugin engine (typed generator framework)"_. + +### C2 — First bin types: open bin + storage box (validate the contract with real geometry + UI) + +- **Ships to prod:** **Yes** — first bins live in the anonymous client app: select type, set params, see 3D preview, export STL/ZIP/3MF. No account required. Ships in parallel with Phase A/B. +- **Summary:** Implement the two highest-demand bin types as self-contained pure plugins, proving the interface against real, distinct geometry. Open bin = walled NxMxU bin (optional dividers, scoop fillet, label tab, stacking lip, magnet/screw base holes). Storage box = closed/boxed variant (lid-ready full-height walls, optional Lite/Eco mode: no magnets, thin walls, gridded floor). Add a minimal anonymous-app UI: a bin-type selector + auto-rendered param form driven by `paramSchema`, reusing the R3F preview and STL/ZIP/3MF export path with no export-core changes. Generalize `BaseplateDesign` into a discriminated `WorkbenchItem` union (`itemType: 'baseplate' | 'bin'`) carrying `schemaVersion` + plugin id + serialized params (bins persist pluginId + params + derived footprint, never meshes — recomputed on load), honoring #5. +- **Key tasks:** + - `src/geometry/bins/types/openBin.ts`: pure plugin (outer walls + floor from `computeBinFootprint`; params unitsX/Y, heightU, divX/divY, scoopFilletMm, labelTab, stackingLip, baseHoleStyle). Reuse `roundedRectanglePath`/`circlePath` from `shapes.ts` and the `extrude`/`filterTriangles` manifold approach from `model.ts`. + - `src/geometry/bins/types/storageBox.ts`: closed box + Lite/Eco mode (wall thickness toggle, no magnet pockets, gridded floor). + - Register both via `registerBuiltInBins()`; confirm stable `registry.list()` order. + - Generalize `design.ts` `BaseplateDesign` → `WorkbenchItem` union; baseplate stays as-is. + - `src/components/BinControls.tsx` + a generic `SchemaForm` rendering `paramSchema` fields — presentational only, mirroring `PlateControls`; `App.tsx` gains an item-type switch routing preview/export through registry-produced `GeometryPart[]`. + - Wire bin models into the existing `PlatePreview`-style rendering and `createExportBlob` (STL single, ZIP split if over bed, 3MF via `createThreeMfPackageFromParts`). + - Tests: compartment count == divX\*divY, scoop fillet present, lip dims match `GRIDFINITY_BIN_PROFILE`, Lite mode has zero magnet holes + correct wall thickness, both footprints socket-compatible, both export non-empty STL + valid 3MF, `WorkbenchItem` round-trips. +- **Dependencies:** C1. +- **Maps to issues:** #5 (wrap not rewrite; generalize `design.ts`); Workbench VNext milestone → new issue _"Phase C: First bin types (open bin, storage box, tool-traced)"_. + +### C3 — Ongoing bin-type stream: tool-traced / photo-trace, specialty (HO-train), socket/wrench systems + +- **Ships to prod:** **Yes** — each type ships independently to the live anon app as it lands; the catalog grows continuously. A contract-conformance meta-test means a new type cannot ship broken. Photo-trace/AI pipeline NOT included (deferred). +- **Summary:** Demonstrate the registry scales: add bin types as small, isolated plugin contributions with no framework changes (the catalog-as-moat thesis). Tool-traced bins take a polyline/contour param (the outline a future photo-trace step would produce) and subtract it as a cutout pocket with Tracefinity-compatible clearance + chamfer. Specialty (HO-train) and socket/wrench systems are parameterized cutout-array bins. Establishes the contribution pattern (one file + one test + one registry line) that also unlocks future community contribution. +- **Key tasks:** + - `src/geometry/bins/types/toolTraced.ts`: bin floor + walls (reusing the open-bin base) minus a polygon cutout from a `contour: Vec2Mm[]` param, with `cutoutClearanceMm`/`cutoutChamferMm` named constants. Geometry only — no image processing. + - `src/geometry/bins/types/socketRack.ts` (drive-size array of circular/hex pockets sized by a drive-size table) and `specialtyTray.ts` (parameterized cutout grid for HO-train/hobby parts). + - Each plugin: defaultParams, paramSchema, validate, deriveBinLayout, buildBinModel + a single registry registration line; assert no edits to framework files (contribution isolation is the deliverable). + - Stable drive-size reference table (socket mm/inch → pocket diameter) as named constants, asserted in tests (treat as data, cite the source). + - Per-type tests + a **registry contract-conformance meta-test** over `registry.list()` (every plugin has id/schema/validate/build, defaultParams pass validate, buildBinModel yields ≥1 `GeometryPart` with triangles). + - Document the contribution recipe in a short bins CONTRIBUTING note. +- **Dependencies:** C1, C2. +- **Maps to issues:** Workbench VNext milestone (ongoing bin catalog). + +### C4 — Stand-alone Modibox-style boxes (interoperate but not grid-bound) + +- **Ships to prod:** **Yes** — stand-alone boxes ship to the live anon app as a third part family, completing the grid → bin → box generation model on the client side. +- **Summary:** Add the third generation family: stand-alone modular boxes that interoperate under the same compatibility contract but are **not** grid-bound. Model them as plugins in the same registry with a separate footprint posture: instead of `computeBinFootprint` constrained to the 42mm cell, a Modibox plugin emits its own modular-coupling profile while exposing **optional** Gridfinity-compatible feet so it can sit on a grid if desired. Proves the registry handles non-grid-bound families without a second framework, completing the "design an entire workshop system in one place" model. +- **Key tasks:** + - Define a `ModiboxFootprint`/coupling-profile alongside `BinFootprint` (separate posture: modular interlock dimensions as named constants, plus an optional Gridfinity-foot adapter reusing `computeBinFootprint` when the user opts in). + - `src/geometry/bins/types/modibox.ts`: parametric modular box (W/D/H in modular units, wall thickness, optional lid interface, optional grid-foot adapter), emitting `GeometryPart[]`. + - Extend the `WorkbenchItem` union to carry `itemType: 'standalone-box'` (or a registry category flag) so persistence + the UI item-type switch handle non-grid-bound parts cleanly. + - Surface standalone boxes in the same item-type selector; reuse `SchemaForm` + preview + export. + - Tests: coupling-profile constants asserted, optional grid-foot adapter produces a socket-compatible footprint when enabled (interoperability proof), box exports non-empty STL/3MF, contract-conformance meta-test still passes. +- **Dependencies:** C1, C2. +- **Maps to issues:** Workbench VNext milestone → new issue _"Phase C: Stand-alone boxes (Modibox-style, non-grid-bound)"_. + +--- + +## Phase D — Composition & Visual Layout (rich logged-in end-state) + +**Goal:** The rich logged-in end-state, building on the Phase A spine + Phase B persisted grid + the Phase C bin catalog. Realizes the full hierarchy and the three reuse axes (part, container/grid, saved layouts). + +### D1 — Composition: systems, container types, and instances + +- **Ships to prod:** Yes — logged-in composition; the anonymous client flow is unaffected. +- **Summary:** Implement the logged-in composition hierarchy from `PRODUCT-VISION.md`: `Account → System (real furniture) → ContainerType (measured drawer + reusable grid) → ContainerInstance (drawer 1..12, same grid reused, unique contents) → Inserts` from a Component Library. Realizes the three reuse axes: part reuse via the Component Library, container/grid reuse (define a drawer+grid once, stamp across N identical drawers), and saved layouts. +- **Key tasks:** + - Build the Component Library (parts drawn from the Phase C catalog) and Insert placement onto ContainerInstances. + - Wire System → ContainerType → ContainerInstance composition end-to-end in the signed-in shell over the Phase B schema. + - Reuse a saved part across containers; surface the reuse axes in the UI. +- **Dependencies:** Phase A spine + Phase B persisted grid (and Phase C catalog for parts). +- **Maps to issues:** new issue _"Phase D: Composition — systems, container types, and instances"_. + +### D2 — Visual layout and placement of bins on grids + +- **Ships to prod:** Yes — the rich end-state for logged-in users. +- **Summary:** A visual editor to spatially lay out and place bins onto a grid, persisted per ContainerInstance (the layout reuse axis). When this ships is an open decision per `PRODUCT-VISION.md` — explicitly not the first slice. Builds on the D1 composition hierarchy and the Phase C catalog. +- **Key tasks:** + - Drag/place bins on a grid in 3D; persist the arrangement per ContainerInstance and restore it on load. + - Persist layout as data (never meshes), consistent with #5. +- **Dependencies:** D1 (and the Phase C bin catalog). +- **Maps to issues:** new issue _"Phase D: Visual layout and placement of bins on grids"_. + +--- + +## GitHub mapping + +This roadmap restructures the existing issues, milestones, and board to match the five phases. The summary below is the actionable migration. + +### Issues — keep / rewrite / close / create + +**Keep (re-milestone only):** + +| Issue | Title | New phase | Rationale | +| ----- | -------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| #5 | Preserve V1 generator as first Workbench item type | Phase B | The #5 hard constraint; literally the definition of Phase B. Anchors B. | +| #4 | Build signed-in Workbench app shell | Phase A | App shell is explicitly part of the Phase A QQQ spine. Minor vocabulary alignment to System/Container only. | +| #8 | Add Workbench backend and frontend test coverage | Phase B | Tests are an explicit Phase B deliverable; covers scoping, CRUD, save/load, export history, and the preserved geometry/export regression suite (34 baseline). | + +**Rewrite (re-scope + re-milestone + relabel):** + +| Issue | Change | +| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| #1 | Retitle to _"Phase A: Confirm QQQ backend repo, app shape, and local dev run."_ Scope to backend-in-repo vs sibling repo, the local dev run shape (frontend + QQQ + Postgres), and the first implementation slice. **Remove** deployment/CD concerns (now the two new Increment 0 issues). Auth-provider choice split out to its own new Phase A issue. Milestone Workbench VNext → Phase A; `phase:1` → `phase:A`. | +| #2 | Expand _"Define Workbench persistence model"_ beyond the old account/project/item sketch to the confirmed `Account → System → ContainerType → ContainerInstance → Inserts` hierarchy + membership/RBAC scoping. Stays Phase A (the spine's schema); bin-type/layout tables detailed in C/D. Persist `PlateInput` + derived `PlateLayout` (#5), never meshes. `phase:1` → `phase:A`. | +| #3 | Move _"Decide export artifact storage"_ from `phase:1` → **Phase B** (export history is a Phase B deliverable). Keep the decision scope + metadata fields; frame as a prerequisite for #7. Milestone → Phase B; `phase:1` → `phase:B`. | +| #6 | Re-scope _"Persist baseplate design inputs and derived metadata"_ to Phase B and align vocabulary: a saved grid design is the reusable grid attached to a `ContainerType`. Persist `PlateInput` + derived `PlateLayout`; never persist meshes. Keep optimistic concurrency. `phase:2` → `phase:B`; milestone → Phase B. | +| #7 | Move _"Add export history and download actions"_ from `phase:3` → **Phase B**. Keep scope (record type/version/filename/content-type/size/storage-key/input-hash/summary; show whether the latest export matches current input; download actions). Depends on the #3 storage decision. `phase:3` → `phase:B`; milestone → Phase B. | + +**Close (no action beyond confirming it stays closed):** + +| Issue | Why | +| ----- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| #9 | Already closed and shipped (Repository Foundation: public repo metadata, Docker packaging, CI, GHCR images, releases). Its GHCR/QRun-style tagging output is the input to the two new Increment 0 deploy issues — it is the lineage root of Increment 0 but requires no reopen. | + +**Create (8 new issues):** + +| Title | Phase | Labels | +| ------------------------------------------------------------------- | ----------- | --------------------------------------- | +| Increment 0: Deploy V1 grid generator to production | Increment 0 | roadmap, ci, docker, deployment | +| Increment 0: Create Benchfinity-CD GitOps repo (ArgoCD + Kustomize) | Increment 0 | roadmap, deployment, decision | +| Phase A: Decide auth/account provider (Authentik vs embedded) | Phase A | roadmap, decision, backend | +| Phase C: Bin plugin engine (typed generator framework) | Phase C | roadmap, frontend, enhancement, plugin | +| Phase C: First bin types (open bin, storage box, tool-traced) | Phase C | roadmap, frontend, enhancement, plugin | +| Phase C: Stand-alone boxes (Modibox-style, non-grid-bound) | Phase C | roadmap, frontend, enhancement, plugin | +| Phase D: Composition — systems, container types, and instances | Phase D | roadmap, backend, frontend, enhancement | +| Phase D: Visual layout and placement of bins on grids | Phase D | roadmap, frontend, enhancement | + +### Milestone restructure + +| Milestone | Action | +| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Workbench VNext | **Retire.** It conflated `phase:1/2/3` persistence into one bucket; the work redistributes across Increment 0 / Phase A / Phase B (and the new Phase C/D issues). Close after re-milestoning #1–#8. | +| Increment 0: Launch (V1 grid live) | **Create.** Goal: anonymous V1 grid deployed to prod via the new `benchfinity-cd` GitOps repo. Holds the two new Increment 0 issues. Lineage: builds on closed #9. | +| Phase A: QQQ spine | **Create.** Goal: backend + Postgres + auth/RBAC + signed-in app shell. Holds rewritten #1, rewritten #2, #4, and the new auth-provider decision issue. | +| Phase B: Grid persisted & reusable | **Create.** Goal: wrap the pure core (#5) behind persistence, save/restore grid designs, export history, tests. Holds #5, rewritten #6, rewritten #3, rewritten #7, #8. | +| Phase C: Bin plugin engine + bin types + standalone boxes | **Create.** Goal: the generator family shipping to the live anonymous app in parallel with A → B. Holds the three new Phase C issues. | +| Phase D: Composition & visual layout | **Create.** Goal: the rich logged-in end-state. Holds the two new Phase D issues. | +| Repository Foundation | **Keep closed/complete.** Satisfied by #9. No changes. | + +### Project board restructure + +- **Phase field options** → `Increment 0: Launch`, `Phase A: QQQ Spine`, `Phase B: Grid Persisted`, `Phase C: Bin Plugins`, `Phase D: Composition`. Map existing items: #1,#2,#4 → Phase A; #3,#5,#6,#7,#8 → Phase B; #9 stays Repository Foundation (Done). +- **Phase labels** → rename/recolor `phase:1`→`phase:A`, `phase:2`→`phase:B`, fold `phase:3` into `phase:B`, repurpose `phase:4`/`phase:5` into `phase:C`/`phase:D` (or delete and recreate), add a `phase:0` label for Increment 0. +- **Add the 8 new issues** to the board; set their Phase field + status to Todo. Set the two Increment 0 issues to a **Now** priority (launch-then-build makes them the immediate beachhead). +- **New labels:** `deployment` (Talos/ArgoCD/Kustomize GitOps) and `plugin` (bin generator framework + types). Distinguish the two parallel tracks — backbone (A → B, logged-in) vs generators (C, anonymous) — via a `Track` single-select field (Backbone / Generators) or the new labels. diff --git a/docs/SESSION-STATE.md b/docs/SESSION-STATE.md index ae18a51..8edecbd 100644 --- a/docs/SESSION-STATE.md +++ b/docs/SESSION-STATE.md @@ -1,45 +1,88 @@ # Session State -## Current Branch -`feature/no-ticket-gridfinity-baseplate-generator` - -## Status -V1 implementation is complete and the audit follow-up is closed. The codebase is structured as a stable foundation for the larger Workbench/Benchfinity phase. - -## Implemented -- Benchfinity Vite, React, TypeScript app scaffold. -- Project name input used in filenames and export metadata. -- Footer identifies the app as a KofTwentyTwo project. -- Settings dialog persists startup and reset defaults in browser local storage. -- Pre-export validation blocks invalid project names, dimensions, margins, oversized grids, layouts without printable tiles, and connector-key plates that do not fit the selected bed. -- Centered padded envelope sizing. -- Bed-fit validation, brand-grouped printer presets, and balanced split planning. -- Bambu Lab H2C presets distinguish left nozzle, right nozzle, dual-nozzle safe, and total two-nozzle envelope. -- Tracefinity-compatible Gridfinity profile constants. -- Generated 3D mesh preview with tile labels and bed outline. -- Standard socket layer, magnet through-pockets, edge-open underside connector notches, and connector key mesh. -- Open-bottom lightweight mode for grid-only prints with no broad bottom floors, while keeping split connector notches backed by bottom-layer edge-cell pads. -- Bambu Studio-style 3MF export with millimeter units, one printable plate per generated tile, explicit bed placement transforms, and connector key object when split. -- 3MF exports include one connector key object per generated seam notch, grouped on a connector keys plate. -- 3MF build items now carry explicit per-plate global translations, so Bambu Studio fallback imports do not stack the split pieces even if plate metadata is flattened. -- 3MF package content types explicitly declare the Bambu metadata config and JSON parts. -- STL export for single plates, including rotated single-tile layouts. -- ZIP export for split plates with rotated tile STLs, connector key STL, connector quantity in `manifest.json` and `README.txt`, and project metadata. -- Persistent visible download link after export, so in-app browser users can see and retry the generated file. -- App shell, plate controls, workspace preview, form controls, and settings dialog are split into focused components so `App.tsx` stays centered on state and orchestration. -- Export orchestration is centralized in `createExportBlob.ts`, with connector-key 3MF object creation isolated in `connectorKeyObjects.ts`. -- 3MF export internals are split into focused `threeMf/` modules for package assembly, mesh conversion, placement, core XML, Bambu metadata, shared formatting, constants, and types. -- README architecture notes document the main module boundaries. -- Root `AGENTS.md` and `docs/AGENT-HANDOFF.md` document agent startup context, validation rules, architecture boundaries, and next-phase handoff notes. -- Unit/export tests, including Bambu-style 3MF package structure and a manifold edge regression for generated tile meshes. -- Regression coverage confirms open-bottom split tiles keep connector pads aligned with the underside notches. -- `docs/WORKBENCH-VNEXT.md` captures the QQQ/Postgres-backed account, project, and workbench direction for the next version. - -## Verification -- `npm run test` passes, 34 tests. -- `npm run build` passes with a Vite chunk-size warning caused by Three.js dependencies. -- `npm audit` reports 0 vulnerabilities after updating Vitest to 4.1.7. -- Browser smoke test passed with settings dialog open/close, 3MF export link creation, and no console errors. -- Do not use the installed Bambu Studio CLI for automated validation. Its `--info` mode can trigger macOS crash reports even when the log says the 3MF loaded. Use package-structure tests and manual Bambu Studio GUI import checks instead. -- Regression checks confirm split 3MF exports have one plate record per tile plus connector plate, and that build transforms spread the objects into non-overlapping plate lanes. -- Regression checks cover rotated single STL exports, rotated split ZIP STL exports, connector-key quantities, 3MF metadata content types, connector-key 3MF object placement, and connector-key plate validation. +**Last Updated:** 2026-05-30 + +## Current Status + +Product vision + roadmap rebuilt and merged; the company-OS sync is operational; **DNS + TLS +for `benchfinity.com` are now live** (registrar delegation fixed this session). **Next session: +Increment 0 — take the V1 grid generator live at `benchfinity.com`** (plan below; now needs only +a go-ahead — the static IP and DNS are already resolved). Workbench `develop` is clean and CI-green. + +## What Was Done This Session + +- **CI/CD:** merged the Node-24 action bumps (#24 setup-helm, #25 login-action, #26 + codeql-action, #27 checkout); `develop` CI green; GHCR packages (image + chart) made public. +- **Product discovery** (with James) → `docs/PRODUCT-VISION.md`: an open-source platform to + design+print a whole workshop organization system in one place — grids (fit to measured + drawers) → a pluggable family of bins → stand-alone boxes → composed into reusable systems. + Anonymous = generate/preview/export single parts; account = save/reuse/systems. Supersedes + the old `WORKBENCH-VNEXT.md` sketch. +- **Roadmap rebuilt** (`docs/ROADMAP.md`): platform-first, continuous-delivery, two-track. + Inc 0 launch → Phase A QQQ spine → B grid persisted/reusable → C bin plugin engine → D + composition/layout. Preserves the pure core (#5). PR #37 merged. +- **GitHub restructured** to Increment 0 / Phase A–D: remapped #1–#8, created #29–#36, retired + the `Workbench VNext` milestone, created phase milestones, remapped the project board `Phase` + field, swapped phase labels. +- **Company-OS sync (Mechanism A):** `scripts/company-os/generate.mjs` renders brand-voiced, + honest-state `product/review.html` + `software/review.html` into the company OS (`../brand` = + `BenchFinity/company`). PR #38 merged. CI `.github/workflows/company-os-sync.yml` (token-guarded) + auto-PRs the company repo on change. Company#6 merged → pages live on `company:develop`. + Verified end-to-end (secret `COMPANY_OS_TOKEN` set; sync run authenticated, clean no-op). +- **DNS + TLS:** `benchfinity.com` registrar NS were wedged on Route 53 (the vanity + `ns*-benchfinity.mmltholdings.com` host objects are stuck "linked" at the registry). Fixed by + delegating to in-bailiwick **`ns1`/`ns2.benchfinity.com` + glue → `50.122.5.149`** (no MMLT hop); + DNS is now served by the **Synology** zone (apex + `www → k8s → 50.122.5.149` already set), not + Route 53. `dsm.benchfinity.com` re-issued with a trusted Let's Encrypt cert (covers + calendar/chat/contacts/drive/mail too). AWS account `744245396565`, profile `kingsrook_root_admin`. + +## Active Branches + +| Branch | Status | +| ------------------- | ------------------------------------------------- | +| workbench `develop` | default; CI green; #37 + #38 merged; clean | +| workbench `main` | protected; behind develop | +| company `develop` | pages live (`product/` + `software/` review.html) | + +## Pending Work + +- [ ] **NEXT — Increment 0 go-live** (#29 create `Benchfinity-CD`, #30 deploy). Plan below. +- [ ] #16 HELD: npm-dev group (TS 5→6, Vite 6→8, plugin-react 4→6) fails `validate` (TS2882); + needs a dedicated migration, not an auto-merge. +- [ ] Phase A backlog when the QQQ spine starts: #1, #2, #4, #31. +- [ ] (Optional) verify the rotated `COMPANY_OS_TOKEN` if it was re-set after the wrap. + +## Increment 0 — next-session plan + +**Static IP is known: `50.122.5.149`** (benchfinity.com apex + `www` already resolve there via the +Synology DNS). **Only input needed: a go-ahead** to create public repo `BenchFinity/benchfinity-cd` + +- wire ArgoCD on `k8s-prod`. + +1. Create `BenchFinity/benchfinity-cd` (Kustomize `base/` + `overlays/production/`), templated on + `/Users/james.maes/Git.Local/Kof22/Website-CD`; **wrap the OCI chart** + `oci://ghcr.io/benchfinity/charts/benchfinity` via an ArgoCD Application with inline Helm + values. Image is **public** → no `imagePullSecret`. +2. ArgoCD Application → namespace `benchfinity`, automated prune+self-heal, `CreateNamespace`; + pin an immutable `:-SNAPSHOT.` tag for the first launch. +3. Traefik ingress for `benchfinity.com` + `cert-manager.io/cluster-issuer: letsencrypt-prod` + TLS + - HTTP→HTTPS redirect, on front-door IP **`50.122.5.149`**. NOTE: that IP currently fronts the + shared cluster (kof22 etc.) — confirm the benchfinity host routes through Traefik there. +4. **DNS is already done** (`benchfinity.com` → `50.122.5.149` via Synology). Just verify the + anonymous HTTPS load + STL/3MF export once the ingress + cert are up. Fix stale `docs/DEPLOY.md` + (image is public; no pull secret). + +## Key Reference + +- **Product** = here (`docs/PRODUCT-VISION.md`, `docs/ROADMAP.md`); **company/marketing** = + `../brand` (`BenchFinity/company`), which defers to us on product. +- **Deploy:** Talos `k8s-prod`, Traefik, cert-manager `letsencrypt-prod` (Ready), ArgoCD, + postgres-operator. Image `ghcr.io/benchfinity/workbench` + chart `…/charts/benchfinity` (both public). +- **DNS:** `benchfinity.com` is delegated to `ns1`/`ns2.benchfinity.com` (glue → `50.122.5.149`) and + served by the **Synology** zone — NOT Route 53; edit records in Synology DNS. Registrar/account is + `744245396565` via `aws --profile kingsrook_root_admin` (`aws sso login --profile kingsrook_root_admin` + first). Optional cleanup: zone NS still says `ns.benchfinity.com` (lame-delegation; harmless). +- **Company-OS sync:** edit `scripts/company-os/generate.mjs` → push `develop` → workflow PRs + `BenchFinity/company`. Secret `COMPANY_OS_TOKEN` (fine-grained PAT: company Contents + PRs R/W). +- **Gates:** `lint && format:check (runs on .md too) && typecheck && test (34 baseline) && build`. + Solo merges: `gh pr merge --squash --admin`. Don't use the Bambu Studio CLI. diff --git a/docs/TODO.md b/docs/TODO.md index 0c3046b..37d602c 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,31 +1,44 @@ # TODO -## Gridfinity Baseplate Generator - -- [x] Choose canonical v1 Gridfinity baseplate geometry: Tracefinity-compatible standard Gridfinity. -- [x] Decide default sizing behavior: exact requested envelope with centered grid and equal perimeter padding. -- [x] Decide v1 split connector: edge-open underside connector notches with separate printed spline keys. -- [x] Decide Tracefinity preset magnet behavior: on by default, configurable off. -- [x] Scaffold the TypeScript web app. -- [x] Implement centered padded envelope sizing and unit conversion. -- [x] Implement bed-fit validation and split layout. -- [x] Render a 3D preview from generated tile mesh data. -- [x] Export single-plate STL. -- [x] Export split-plate ZIP with manifest. -- [x] Add printer bed presets with Custom manual entry. -- [x] Add project name input for export filenames and bundle metadata. -- [x] Add settings dialog for saved defaults in local storage. -- [x] Add open-bottom lightweight print mode. -- [x] Export 3MF for direct Bambu Studio import. -- [x] Add tests for sizing, splitting, manifests, and preview/export smoke paths. - -## Benchfinity Workbench Next Phase - -- [ ] Confirm backend repo/app shape for QQQ/Postgres. -- [ ] Define account, user, membership, project, workbench item, baseplate design, printer profile, and export artifact tables. -- [ ] Decide artifact storage for generated STL, ZIP, and 3MF files. -- [ ] Build the signed-in app shell with account/project navigation. -- [ ] Preserve the V1 generator as the first workbench item type. -- [ ] Persist baseplate design inputs and derived layout metadata. -- [ ] Add export history with download actions. -- [ ] Add backend and frontend tests for account scoping, project CRUD, design save/load, and export metadata. +Product vision: `docs/PRODUCT-VISION.md`. Authoritative roadmap: `docs/ROADMAP.md`. +Living tracker: GitHub Issues + the **Benchfinity Roadmap** project (milestones +`Increment 0` / `Phase A` / `B` / `C` / `D`). This file is a convenience mirror. + +## Done + +- [x] V1 Gridfinity **grid** generator: centered/padded envelope sizing, bed-fit + split, 3D preview, STL / split-ZIP / 3MF export, printer presets, settings, tests. +- [x] Repository Foundation (#9): public repo, container CI, distroless Chainguard images, HIGH+ security gates, AGPL-3.0 + DCO, branch protection, Dependabot, Helm/compose/ArgoCD + chart publishing. +- [x] CI hardening: Node-24 action bumps (#24–#27); GHCR packages public; `develop` CI green. +- [x] Product vision + roadmap rebuilt; GitHub restructured to Increment 0 / Phase A–D. +- [x] Company-OS sync: `scripts/company-os/` generator + token-guarded CI workflow; `product/` + `software/` showcases live on `BenchFinity/company`. + +## Increment 0 — Launch (ship the V1 grid generator live) — NEXT + +- [ ] #29 Create `Benchfinity-CD` GitOps repo (ArgoCD + Kustomize, wraps the OCI chart). +- [ ] #30 Deploy V1 grid generator to production (`benchfinity.com`, dedicated static IP, Traefik + cert-manager `letsencrypt-prod`). + +## Phase A — QQQ Spine (the platform-first showcase) + +- [ ] #1 Confirm QQQ backend repo, app shape, and local dev run. +- [ ] #2 Define persistence model: System → ContainerType → ContainerInstance → Inserts. +- [ ] #31 Decide auth/account provider (Authentik vs embedded). +- [ ] #4 Build signed-in Workbench app shell. + +## Phase B — Grid Persisted & Reusable + +- [ ] #5 Preserve V1 generator as first Workbench item type (wrap, never rewrite — issue #5 constraint). +- [ ] #6 Persist baseplate design inputs and derived metadata. +- [ ] #3 Decide export artifact storage. +- [ ] #7 Add export history and download actions. +- [ ] #8 Add backend + frontend test coverage (raise the 34-test baseline). + +## Phase C — Bin Plugin Engine (anonymous, client-side, runs parallel to A→B) + +- [ ] #32 Bin plugin engine (typed generator framework + Gridfinity footprint contract). +- [ ] #33 First bin types: open bin, storage box, tool-traced. +- [ ] #34 Stand-alone boxes (Modibox-style, non-grid-bound). + +## Phase D — Composition & Visual Layout (rich logged-in end-state) + +- [ ] #35 Composition: systems, container types, and instances. +- [ ] #36 Visual layout and placement of bins on grids. diff --git a/docs/WORKBENCH-VNEXT.md b/docs/WORKBENCH-VNEXT.md index 533508b..e5f94e0 100644 --- a/docs/WORKBENCH-VNEXT.md +++ b/docs/WORKBENCH-VNEXT.md @@ -1,9 +1,11 @@ # Workbench VNext Requirements ## Goal + Turn the v1 single-purpose browser generator into a QQQ-backed Workbench where signed-in users can manage accounts, projects, generated baseplates, export history, and reusable printer/settings profiles. ## Product Shape + VNext should preserve the fast v1 generation flow, but add persistence and navigation around it. - Users belong to an account. @@ -16,6 +18,7 @@ VNext should preserve the fast v1 generation flow, but add persistence and navig ## Account And Project Model ### Account + Represents the billing/security boundary, even if billing is deferred. - `id` @@ -26,6 +29,7 @@ Represents the billing/security boundary, even if billing is deferred. - default units and printer profile ### User + Represents a person who can sign in. - `id` @@ -36,6 +40,7 @@ Represents a person who can sign in. - last active account ### Account Membership + Joins users to accounts. - `accountId` @@ -45,6 +50,7 @@ Joins users to accounts. - invite state if invitation flow is included in MVP ### Project + Organizes related generated designs. - `id` @@ -58,6 +64,7 @@ Organizes related generated designs. - archived flag ### Workbench Item + Generic project-scoped thing. VNext starts with baseplates, but the model should not block future generators. - `id` @@ -72,6 +79,7 @@ Generic project-scoped thing. VNext starts with baseplates, but the model should - `updatedAt` ### Baseplate Design + Typed payload for the existing generator state. - `workbenchItemId` @@ -85,6 +93,7 @@ Typed payload for the existing generator state. - preview camera/view options if worth preserving ### Export Artifact + Versioned generated output. - `id` @@ -105,6 +114,7 @@ Versioned generated output. Use QQQ for CRUD metadata, process actions, permissions, and admin-style screens. Use Postgres as the durable source of truth. ### QQQ Tables + - `account` - `user` - `account_membership` @@ -115,6 +125,7 @@ Use QQQ for CRUD metadata, process actions, permissions, and admin-style screens - `export_artifact` ### QQQ Processes + - `create_project` - `duplicate_project` - `archive_project` @@ -125,6 +136,7 @@ Use QQQ for CRUD metadata, process actions, permissions, and admin-style screens - `set_default_printer_profile` ### Postgres Notes + - Use UUID primary keys. - Scope all project, item, design, profile, and artifact queries by `account_id`. - Store generator inputs as structured columns for important filters plus a JSONB payload for versioned design settings. @@ -133,6 +145,7 @@ Use QQQ for CRUD metadata, process actions, permissions, and admin-style screens - Keep artifact binary storage outside Postgres unless files are small enough for early MVP convenience. Persist metadata in Postgres either way. ### Security Rules + - Account membership controls all access. - Owners/admins can manage account settings and members. - Members can create and edit projects and workbench items. @@ -142,12 +155,14 @@ Use QQQ for CRUD metadata, process actions, permissions, and admin-style screens ## UI Navigation ### App Shell + - Left sidebar for account switcher, project list, and primary navigation. - Top bar for current project/item name, save status, export action, and user menu. - Main region for the active workbench. - Right panel for settings, validation, dimensions, and export history. ### Primary Routes + - `/accounts` - `/a/{accountSlug}/projects` - `/a/{accountSlug}/p/{projectSlug}` @@ -155,12 +170,14 @@ Use QQQ for CRUD metadata, process actions, permissions, and admin-style screens - `/a/{accountSlug}/settings` ### Project View + - Search and filter project items. - Create new baseplate design. - Duplicate, rename, archive, and open items. - Show last modified time, printable size, tile count, and latest export type. ### Workbench Layout + - Center: 3D preview, using the existing mesh pipeline. - Left or top control strip: core dimensions, printer preset, envelope mode, and profile. - Right inspector: validation, derived dimensions, tile breakdown, connector settings, and export history. @@ -184,6 +201,7 @@ Use QQQ for CRUD metadata, process actions, permissions, and admin-style screens ## Phased Implementation ### Phase 1: Persistence Foundation + - Add QQQ/Postgres app skeleton. - Define account, user, membership, project, workbench item, baseplate design, printer profile, and export artifact tables. - Implement account-scoped security filters. @@ -191,6 +209,7 @@ Use QQQ for CRUD metadata, process actions, permissions, and admin-style screens - Keep artifact generation client-side if that shortens the first slice. ### Phase 2: Project Workbench + - Add signed-in app shell. - Add project list and project detail views. - Add create, rename, duplicate, and archive flows. @@ -198,23 +217,27 @@ Use QQQ for CRUD metadata, process actions, permissions, and admin-style screens - Preserve the existing v1 generator behavior inside a workbench item. ### Phase 3: Export History + - Persist export metadata. - Attach generated artifacts to a workbench item. - Add export history panel with download links. - Add input hash/version labeling so users can tell whether an export matches the current design. ### Phase 4: Server-Side Generation + - Move export generation to backend process if browser generation becomes too slow or artifact persistence requires trusted generation. - Add job status, progress, retries, and failure messages. - Store artifacts in object storage with Postgres metadata. ### Phase 5: Team And Operations + - Add invitation flow and membership management. - Add account settings. - Add activity events. - Add operational dashboards for failed generation jobs and storage growth. ## Open Questions + - What identity provider should VNext use: QQQ-native auth, external OAuth, or a simple early access login? - Should the first MVP allow multiple accounts per user, or start with exactly one account per user? - Are exports generated in the browser and uploaded, or generated on the backend from saved design input? @@ -225,6 +248,7 @@ Use QQQ for CRUD metadata, process actions, permissions, and admin-style screens - Should Workbench support anonymous drafts that can later be claimed by a signed-in account? ## MVP Acceptance Criteria + - A signed-in user can open the app, select an account, and create a project. - A user can create a Gridfinity baseplate workbench item inside a project. - The workbench loads the existing v1 controls, preview, validation, and export behavior. diff --git a/docs/adr/0001-distroless-base-images.md b/docs/adr/0001-distroless-base-images.md new file mode 100644 index 0000000..65f496d --- /dev/null +++ b/docs/adr/0001-distroless-base-images.md @@ -0,0 +1,54 @@ +# ADR 0001: Distroless Chainguard/Wolfi base images for all service images + +- Status: Accepted +- Date: 2026-05-29 + +## Context + +Our container runtime images previously used Alpine-based bases (for the +frontend, `nginxinc/nginx-unprivileged:1.27-alpine`) and patched OS-package CVEs +at build time with `apk --no-cache upgrade`. That approach has drawbacks: + +- The pinned base lags upstream OS-package fixes, so image scans block on known + CVEs until we manually re-patch. +- `apk upgrade` runs as root during build, adds a layer, and makes the image + non-reproducible (you get whatever the mirror serves that day). +- Alpine ships a full shell and package manager, enlarging the attack surface. +- Alpine uses musl libc. Several of our future workloads — Node native addons + and the JVM (the QQQ/Java backend) — are happier on glibc. + +## Decision + +Standardize on Chainguard/Wolfi distroless images for all service runtime +images: + +- Frontend (now): `cgr.dev/chainguard/nginx` +- Future Node services: `cgr.dev/chainguard/node` +- Future Java/QQQ backend: `cgr.dev/chainguard/jre` + +Multi-stage builds keep toolchain-heavy stages (e.g. `node:22-alpine` for the +frontend build) out of the final image; only the distroless runtime ships. + +## Rationale + +- Smallest practical runtime: no shell, no package manager, no extra tooling. +- glibc-based, so it works for Node native addons and the JVM (unlike musl). +- Minimal attack surface: nothing to drop into, nothing to exploit via a shell + or package manager. +- Daily-rebuilt by Chainguard with near-zero CVEs, so scans stay green without + build-time `apk upgrade` hacks. The frontend image currently scans with 0 + CVEs (Trivy, all severities). + +## Consequences + +- No in-container debugging shell. Use ephemeral debug containers, orchestrator + logs, and HTTP probes instead. +- No container `HEALTHCHECK` (no `wget`/shell). Health is delegated to + orchestrator probes: the Helm chart uses `httpGet`; the compose frontend + service has no healthcheck. +- Bases are pinned by digest (e.g. + `cgr.dev/chainguard/nginx:latest@sha256:...`) for reproducibility. Refresh via + Dependabot/Renovate or a manual + `docker buildx imagetools inspect cgr.dev/chainguard/nginx:latest`. + +This supersedes the Alpine + `apk --no-cache upgrade` approach. diff --git a/docs/adr/0002-production-deployment-architecture.md b/docs/adr/0002-production-deployment-architecture.md new file mode 100644 index 0000000..64107c5 --- /dev/null +++ b/docs/adr/0002-production-deployment-architecture.md @@ -0,0 +1,74 @@ +# ADR 0002: Production deployment via a raw-Kustomize CD repo onboarded into the app-of-apps + +- Status: Accepted +- Date: 2026-05-30 + +## Context + +Increment 0 takes the V1 generator live at `benchfinity.com`, and the platform +will grow into the voyage shape (QQQ backend, Postgres, Redis, object storage). +A self-hosted fleet already exists: Argo CD runs on `k8s-infra` and reconciles +into `k8s-prod` (`https://k8s-prod-vip.galaxy.lan:6443`), governed by +`KofTwentyTwo/k8s-app-of-apps`. Every workload there (kof22-website, voyage, +bigcapital, marketing-website) ships its manifests in a private per-app +`-cd` repo as **raw Kustomize** (`base/` + `overlays/{env}/` + `proxy/`), +onboarded via two Argo CD Applications: the workload and a sync-wave `-1` +sealed-secrets Application. There is no Helm in any CD repo; an upstream chart +(authentik) is `helm template`d and committed as static YAML. + +`voyage-cd` is the direct analog: a QQQ/Java backend with Postgres (Zalando +operator), Redis-sentinel, MinIO, B2 backup CronJobs, and a Synology +TLS-passthrough proxy, all raw Kustomize. Benchfinity additionally already +publishes a Helm chart and image to GHCR, both public. + +## Decision + +- Production runs through a new private repo **`BenchFinity/benchfinity-cd`**, + raw Kustomize modeled on `voyage-cd`, onboarded into `k8s-app-of-apps` + (`apps/`, `shared/` AppProject + sealed repo-creds, `infra/` sealed secrets, + `envs/production`). +- The published workbench **Helm chart stays as the external self-hoster + artifact**, not our production source of truth. "Fully Helm" applies to how + the app is packaged, not to the CD repo. +- **Scaffold the full voyage-shaped stack and run the data plane live now** + (Postgres/Redis/MinIO/backups). The `benchfinity-api` (QQQ) tier is scaffolded + but parked at `replicas: 0` until its image exists (#1). Only the static web + tier serves traffic at launch. +- **Production only** to start (namespace `benchfinity-prod`). +- Edge is a plain k8s `Ingress` + Traefik annotations + a cert-manager + `Certificate` on ClusterIssuer `letsencrypt-production`, matching voyage. + Synology services route through a `proxy/` Traefik TLS-passthrough to the + benchfinity NAS (`10.120.149.4`) for `dsm,calendar,chat,contacts,drive,file,mail.benchfinity.com`. +- The image is **pinned to the immutable release tag `0.1.0`**, produced by + cutting workbench `develop` to `main`. The image is public, so no pull secret. +- Secrets use Bitnami **sealed-secrets** (MinIO root, B2 backup key, Argo CD repo + deploy key). Postgres user credentials are operator-generated, not sealed. +- CI for `benchfinity-cd` is **GitHub Actions** (`kustomize build` + + `kubeconform` + `gitleaks`, with `kube-linter`/`yamllint` advisory), since the + BenchFinity org is GitHub-native. The app-of-apps additions ride that repo's + existing CircleCI/Munitor pipeline unchanged. + +## Rationale + +- Fleet consistency: every workload is raw-Kustomize-in-a-CD-repo, onboarded the + same way, and voyage proves the exact full-stack shape benchfinity needs. +- The chart keeps a real audience (external self-hosters) without becoming a + second production source of truth. +- Running the data plane early de-risks the stateful pieces (operator, storage, + backups, sealing) before the QQQ backend lands. +- Reusing the fleet B2 key under a `benchfinity-prod/` prefix isolates data + without minting new credentials; every consumer of that bucket must prefix. + +## Consequences + +- `benchfinity-cd` is the first GitHub-Actions-validated CD repo in an otherwise + CircleCI/Munitor fleet — a deliberate, documented one-off. +- A standing Postgres/Redis/MinIO footprint exists with no consumer until #1; + backup jobs run against an empty database initially. +- `benchfinity-api` is inert (`replicas: 0`) until #1 supplies an image and the + ConfigMap wiring is finalized. +- Synology subdomains only resolve end-to-end once both the `proxy/` and the NAS + box are reachable. +- `docs/DEPLOY.md` and the example `deploy/argocd/application.yaml` describe the + image as private and the chart as the deploy path; both are now stale and are + corrected as part of the rollout. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..82fa9e0 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,37 @@ +import js from "@eslint/js"; +import globals from "globals"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import tseslint from "typescript-eslint"; +import prettier from "eslint-config-prettier"; + +export default tseslint.config( + { ignores: ["dist", "coverage", "node_modules"] }, + js.configs.recommended, + ...tseslint.configs.recommended, + reactHooks.configs.flat["recommended-latest"], + { + files: ["**/*.{ts,tsx}"], + languageOptions: { + ecmaVersion: 2022, + globals: { ...globals.browser, ...globals.node }, + }, + plugins: { + "react-refresh": reactRefresh, + }, + rules: { + "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], + }, + }, + { + // Node-run build/tooling scripts (not part of the browser app). + files: ["scripts/**/*.{js,mjs}"], + languageOptions: { + ecmaVersion: 2022, + sourceType: "module", + globals: { ...globals.node }, + }, + }, + // Keep formatting concerns out of ESLint; Prettier owns them. + prettier, +); diff --git a/package-lock.json b/package-lock.json index 606f94a..8d8b568 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,28 +1,40 @@ { - "name": "benchfinity", + "name": "benchfinity-workbench", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "benchfinity", + "name": "benchfinity-workbench", "version": "0.1.0", + "license": "AGPL-3.0-only", "dependencies": { "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.4.0", "jszip": "^3.10.1", - "lucide-react": "^0.468.0", + "lucide-react": "^1.17.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "three": "^0.183.0" + "three": "^0.184.0" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", + "eslint": "^10.4.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "prettier": "^3.8.3", "typescript": "^5.7.2", + "typescript-eslint": "^8.60.0", "vite": "^6.0.3", "vitest": "^4.1.7" + }, + "engines": { + "node": ">=22" } }, "node_modules/@babel/code-frame": { @@ -764,6 +776,200 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.6.0.tgz", + "integrity": "sha512-ii6Bw9jJ2zi2cWA2Z+9/QZ/+3DX6kwaV5Q986D/CdP3Lap3w/pgQZ373FV7byY/i7L4IRH/G43I5dz1ClsCbpA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1019,9 +1225,6 @@ "arm" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1036,9 +1239,6 @@ "arm" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1053,9 +1253,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1070,9 +1267,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1087,9 +1281,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1104,9 +1295,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1121,9 +1309,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1138,9 +1323,6 @@ "ppc64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1155,9 +1337,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1172,9 +1351,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1189,9 +1365,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1206,9 +1379,6 @@ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1223,9 +1393,6 @@ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1398,6 +1565,13 @@ "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==", "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1405,6 +1579,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/offscreencanvas": { "version": "2019.7.3", "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz", @@ -1465,6 +1646,249 @@ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==", "license": "MIT" }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz", + "integrity": "sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/type-utils": "8.60.0", + "@typescript-eslint/utils": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.60.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.0.tgz", + "integrity": "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.0.tgz", + "integrity": "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.60.0", + "@typescript-eslint/types": "^8.60.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz", + "integrity": "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz", + "integrity": "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz", + "integrity": "sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/utils": "8.60.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.0.tgz", + "integrity": "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz", + "integrity": "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.60.0", + "@typescript-eslint/tsconfig-utils": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.0.tgz", + "integrity": "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz", + "integrity": "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "node_modules/@use-gesture/core": { "version": "10.3.1", "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz", @@ -1617,14 +2041,64 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, "engines": { - "node": ">=12" + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "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" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/base64-js": { @@ -1669,6 +2143,19 @@ "require-from-string": "^2.0.2" } }, + "node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, "node_modules/browserslist": { "version": "4.28.2", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", @@ -1840,6 +2327,13 @@ } } }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/detect-gpu": { "version": "5.0.70", "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz", @@ -1921,6 +2415,207 @@ "node": ">=6" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.4.0.tgz", + "integrity": "sha512-loXy6bWOoP3EP6JA7jo6p5jMpBJmHmsNZM5SFRHLdh1MGOPurMnNBj4ZlAbaqUAaQWbCr7jHV4P7gzAyryZWkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.6.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.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", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.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", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": "^9 || ^10" + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, "node_modules/estree-walker": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", @@ -1931,6 +2626,16 @@ "@types/estree": "^1.0.0" } }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -1941,6 +2646,27 @@ "node": ">=12.0.0" } }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -1965,6 +2691,57 @@ "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", "license": "MIT" }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -1990,12 +2767,55 @@ "node": ">=6.9.0" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.6.0.tgz", + "integrity": "sha512-sepffkT8stwnIYbsMBpoCHJuJM5l98FUF2AnE07hfvE0m/qp3R586hw4jF4uadbhvg1ooIdzuu7CsfD2jzCaNA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/glsl-noise": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz", "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==", "license": "MIT" }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/hls.js": { "version": "1.6.16", "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz", @@ -2022,18 +2842,61 @@ ], "license": "BSD-3-Clause" }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", "license": "MIT" }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", @@ -2084,6 +2947,27 @@ "node": ">=6" } }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -2109,6 +2993,30 @@ "setimmediate": "^1.0.5" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -2118,6 +3026,22 @@ "immediate": "~3.0.5" } }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -2129,12 +3053,12 @@ } }, "node_modules/lucide-react": { - "version": "0.468.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.468.0.tgz", - "integrity": "sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==", + "version": "1.17.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.17.0.tgz", + "integrity": "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==", "license": "ISC", "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc" + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/maath": { @@ -2172,6 +3096,22 @@ "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==", "license": "MIT" }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -2198,6 +3138,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.46", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.46.tgz", @@ -2219,12 +3166,72 @@ ], "license": "MIT" }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "license": "(MIT AND Zlib)" }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -2296,6 +3303,32 @@ "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", "license": "ISC" }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -2312,6 +3345,16 @@ "lie": "^3.0.2" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react": { "version": "19.2.6", "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz", @@ -2552,9 +3595,9 @@ } }, "node_modules/three": { - "version": "0.183.2", - "resolved": "https://registry.npmjs.org/three/-/three-0.183.2.tgz", - "integrity": "sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==", + "version": "0.184.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", + "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==", "license": "MIT" }, "node_modules/three-mesh-bvh": { @@ -2663,6 +3706,19 @@ "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==", "license": "MIT" }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, "node_modules/tunnel-rat": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz", @@ -2700,6 +3756,19 @@ } } }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -2714,6 +3783,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.0.tgz", + "integrity": "sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.60.0", + "@typescript-eslint/parser": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/utils": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", @@ -2745,6 +3838,16 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/use-sync-external-store": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", @@ -2977,6 +4080,16 @@ "node": ">=8" } }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -2984,6 +4097,42 @@ "dev": true, "license": "ISC" }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } + }, "node_modules/zustand": { "version": "5.0.13", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.13.tgz", diff --git a/package.json b/package.json index 7f6fb0e..7375e86 100644 --- a/package.json +++ b/package.json @@ -1,28 +1,55 @@ { - "name": "benchfinity", + "name": "benchfinity-workbench", "private": true, "version": "0.1.0", + "description": "Browser app for generating Gridfinity-compatible workbench baseplates, split print bundles, and Bambu Studio-style 3MF files.", + "license": "AGPL-3.0-only", + "author": "James Maes", + "homepage": "https://github.com/BenchFinity/workbench#readme", + "repository": { + "type": "git", + "url": "https://github.com/BenchFinity/workbench.git" + }, + "bugs": { + "url": "https://github.com/BenchFinity/workbench/issues" + }, "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "tsc -b && vite build", + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "format": "prettier --write .", + "format:check": "prettier --check .", + "typecheck": "tsc -b", "test": "vitest run", "preview": "vite preview" }, + "engines": { + "node": ">=22" + }, "dependencies": { "@react-three/drei": "^10.7.7", "@react-three/fiber": "^9.4.0", "jszip": "^3.10.1", - "lucide-react": "^0.468.0", + "lucide-react": "^1.17.0", "react": "^19.0.0", "react-dom": "^19.0.0", - "three": "^0.183.0" + "three": "^0.184.0" }, "devDependencies": { + "@eslint/js": "^10.0.1", "@types/react": "^19.0.2", "@types/react-dom": "^19.0.2", "@vitejs/plugin-react": "^4.3.4", + "eslint": "^10.4.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.6.0", + "prettier": "^3.8.3", "typescript": "^5.7.2", + "typescript-eslint": "^8.60.0", "vite": "^6.0.3", "vitest": "^4.1.7" } diff --git a/scripts/company-os/README.md b/scripts/company-os/README.md new file mode 100644 index 0000000..f56b028 --- /dev/null +++ b/scripts/company-os/README.md @@ -0,0 +1,46 @@ +# Company-OS sync + +Generates the **company operating system** (`BenchFinity/company`, local `../brand/`) division +showcases for the two divisions whose data Workbench owns: + +- `product/review.html` — product vision, the workspace-system model, and the roadmap. +- `software/review.html` — stack, architecture, modules, and status. + +## Why this exists + +The company OS holds `product/` and `software/`, but the underlying data lives here in Workbench. +Rather than hand-maintain copies in two repos (which drift), Workbench stays the **single source of +truth** and this generator emits the two review pages into the OS. The OS never edits them by hand. + +## Rules baked in + +- **Honest-state.** What ships today is present tense (`Ships today`); everything else is a labeled + `Roadmap` tag. Today that means: the Gridfinity-compatible **grid generator** + exports + the + deploy pipeline ship; bins, accounts, systems, the QQQ backend, and visual layout are roadmap. +- **Brand-compliant** (see `../brand/CLAUDE.md`, `../brand/brand/positioning.md`): never "AI" + (parametric/deterministic), no monetization/pricing framing, never "_a_ Gridfinity generator" + (it is _the_ platform that unifies Gridfinity generation), "workspace system" register. +- **Matches the convention**: same `--bf-*` tokens, IBM Plex Sans/Mono, the 3×3 mark, and the shared + CSS vocabulary as `../brand/brand/review.html` and `../brand/marketing/review.html`. + +## Run + +```bash +node scripts/company-os/generate.mjs # writes into ../brand +COMPANY_OS_DIR=/path/to/company node scripts/company-os/generate.mjs +GENERATED_STAMP="v0.2 · 2026-06-10" node scripts/company-os/generate.mjs # CI passes the date +``` + +Edit the content in `generate.mjs` (the `productPage` / `softwarePage` blocks); re-run to regenerate. + +## Not generated + +`../brand/index.html` is hand-maintained in the company OS (it spans every division). Its `product/` +and `software/` cards are updated by hand to link these showcases — only the two `review.html` pages +are generated here. + +## Sync to the company OS (CI — to wire) + +A GitHub Action runs this generator on changes and opens a PR into `BenchFinity/company` with the +regenerated pages. It needs a cross-repo token (PAT or GitHub App) with write access to the company +repo, stored as a secret. Until that's wired, regenerate locally and commit in the company repo. diff --git a/scripts/company-os/generate.mjs b/scripts/company-os/generate.mjs new file mode 100644 index 0000000..feb908b --- /dev/null +++ b/scripts/company-os/generate.mjs @@ -0,0 +1,480 @@ +#!/usr/bin/env node +// Generates the company-OS division showcases (product/review.html + software/review.html) +// from content owned HERE in Workbench, into the Benchfinity company OS (default ../brand). +// +// Why: the company OS (BenchFinity/company) holds product/ and software/, but the data +// lives in Workbench. This keeps Workbench the single source of truth and emits brand-matching, +// honest-state review pages into the OS so the two never fall out of sync by hand. +// +// Brand rules applied at the source (see ../../docs and ../brand/CLAUDE.md): +// - Honest-state: what SHIPS today is present tense; everything else is LABELED ROADMAP. +// - Never "AI" (parametric/deterministic), never paid/pricing/monetization, never "a Gridfinity +// generator" (it is THE platform that unifies Gridfinity generation), never hobbyist fluff. +// - "workspace system" + "engineered to the Gridfinity standard" + build-in-the-open register. +// - Mirrors brand/review.html + marketing/review.html: --bf-* tokens, IBM Plex Sans/Mono, +// the 3x3 mark, and the shared CSS vocabulary. +// +// Usage: node scripts/company-os/generate.mjs # writes into ../brand +// COMPANY_OS_DIR=/path/to/company node ... # override target + +import { writeFileSync, existsSync, mkdirSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = resolve(SCRIPT_DIR, "..", ".."); +const COMPANY_OS_DIR = process.env.COMPANY_OS_DIR + ? resolve(process.env.COMPANY_OS_DIR) + : resolve(REPO_ROOT, "..", "brand"); +const STAMP = process.env.GENERATED_STAMP || "v0.1"; // date is passed in by CI; kept out of source for determinism + +const REPO = "https://github.com/BenchFinity/Workbench"; +const BLOB = `${REPO}/blob/develop`; + +// ---- shared presentation (mirrors brand/marketing review.html) ------------------------------- + +const CSS = String.raw` + :root{ + --bf-bg:#0B0D0F; --bf-surface:#161B20; --bf-surface-2:#1E242B; --bf-border:#232A31; + --bf-accent:#4FA39D; --bf-accent-bright:#67C7BE; --bf-text:#E7ECEF; --bf-text-muted:#8D98A1; + --bf-signal:#E0A458; --bf-success:#5FA873; --bf-warning:#E0A458; --bf-error:#CD6457; --bf-info:#6E8FB5; + --bf-font:"IBM Plex Sans","Inter Tight",system-ui,sans-serif; + --bf-mono:"IBM Plex Mono",ui-monospace,monospace; + } + *{box-sizing:border-box} + html{scroll-behavior:smooth} + body{margin:0;background:var(--bf-bg);color:var(--bf-text);font-family:var(--bf-font);line-height:1.55;-webkit-font-smoothing:antialiased;} + .wrap{max-width:1040px;margin:0 auto;padding:64px 28px 120px;} + header{display:flex;flex-direction:column;gap:28px;padding-bottom:40px;border-bottom:1px solid var(--bf-border);} + .lock{width:min(440px,80%);} + .kicker{font-family:var(--bf-mono);font-size:12px;letter-spacing:3px;text-transform:uppercase;color:var(--bf-text-muted);} + h1{font-size:39px;font-weight:600;letter-spacing:-0.5px;margin:0;} + h2{font-size:25px;font-weight:600;letter-spacing:-0.3px;margin:0 0 4px;} + h3{font-size:15px;font-weight:600;margin:0;} + .lede{color:var(--bf-text-muted);max-width:66ch;margin:0;} + .meta{display:flex;gap:10px;flex-wrap:wrap;} + .pill{font-family:var(--bf-mono);font-size:11px;letter-spacing:1px;padding:5px 12px;border-radius:999px;border:1px solid var(--bf-border);background:var(--bf-surface);color:var(--bf-text-muted);text-decoration:none;} + .pill:hover{border-color:var(--bf-accent);color:var(--bf-accent-bright);} + section{padding:46px 0;border-bottom:1px solid var(--bf-border);} + .sec-label{font-family:var(--bf-mono);font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--bf-accent);margin-bottom:14px;} + .grid{display:grid;gap:16px;} + .cards-4{grid-template-columns:repeat(4,1fr);} + .cards-3{grid-template-columns:repeat(3,1fr);} + .cards-2{grid-template-columns:repeat(2,1fr);} + @media(max-width:780px){.cards-4,.cards-3,.cards-2{grid-template-columns:repeat(2,1fr);}} + @media(max-width:520px){.cards-4,.cards-3,.cards-2{grid-template-columns:1fr;}} + .card{background:var(--bf-surface);border:1px solid var(--bf-border);border-radius:10px;padding:20px;display:flex;flex-direction:column;gap:8px;} + .card.tcenter{align-items:center;justify-content:center;text-align:center;} + .card .label{font-family:var(--bf-mono);font-size:11px;letter-spacing:1.5px;text-transform:uppercase;color:var(--bf-text-muted);} + .card .big{font-size:26px;font-weight:600;letter-spacing:-0.5px;color:var(--bf-accent-bright);} + .card .ds{font-size:13px;color:var(--bf-text-muted);line-height:1.5;} + .tag{font-family:var(--bf-mono);font-size:10px;letter-spacing:1px;text-transform:uppercase;color:var(--bf-text-muted);border:1px solid var(--bf-border);border-radius:999px;padding:3px 9px;align-self:flex-start;} + .tag.lead{color:#0B0D0F;background:var(--bf-accent);border-color:var(--bf-accent);} + .tag.signal{color:#0B0D0F;background:var(--bf-signal);border-color:var(--bf-signal);} + .pos{background:var(--bf-surface);border-left:3px solid var(--bf-accent);border-radius:0 10px 10px 0;padding:18px 22px;color:var(--bf-text);} + .note{font-size:13px;color:var(--bf-text-muted);margin-top:12px;} + a.inline{color:var(--bf-accent-bright);text-decoration:none;} + .gate{display:inline-block;font-family:var(--bf-mono);font-size:11px;letter-spacing:1px;color:var(--bf-accent-bright);border:1px solid #2c4f4c;border-radius:999px;padding:4px 12px;} + .gate.road{color:var(--bf-signal);border-color:#5a4426;} + .tags{display:flex;flex-wrap:wrap;gap:8px;margin-top:6px;} + .tag-pill{font-family:var(--bf-mono);font-size:12px;padding:5px 11px;border-radius:999px;border:1px solid var(--bf-border);background:var(--bf-surface);} + .tag-pill.good{color:var(--bf-accent-bright);border-color:#2c4f4c;} + .tag-pill.bad{color:var(--bf-text-muted);text-decoration:line-through;} + .tag-pill.kw{color:var(--bf-text);} + .phase{display:flex;gap:16px;padding:16px 0;border-bottom:1px dashed var(--bf-border);} + .phase:last-child{border-bottom:none;} + .phase .pn{font-family:var(--bf-mono);font-size:12px;color:#0B0D0F;background:var(--bf-accent);border-radius:8px;width:42px;height:34px;flex:0 0 42px;display:flex;align-items:center;justify-content:center;font-weight:600;} + .phase .pn.road{background:var(--bf-signal);} + .phase .pb{display:flex;flex-direction:column;gap:3px;} + .phase .pt{font-size:15px;font-weight:600;} + .phase .pd{font-size:13px;color:var(--bf-text-muted);line-height:1.5;} + ul.files{list-style:none;padding:0;margin:18px 0 0;display:grid;grid-template-columns:1fr 1fr;gap:6px;} + @media(max-width:780px){ul.files{grid-template-columns:1fr;}} + ul.files a{font-family:var(--bf-mono);font-size:12.5px;color:var(--bf-text);text-decoration:none;display:block;padding:9px 12px;background:var(--bf-surface);border:1px solid var(--bf-border);border-radius:7px;} + ul.files a:hover{border-color:var(--bf-accent);color:var(--bf-accent-bright);} + ul.files .ly{color:var(--bf-text-muted);} + footer{margin-top:40px;color:var(--bf-text-muted);font-size:13px;} +`; + +const LOCKUP = String.raw` + + + + + + + + + BENCHFINITY + BUILD YOUR WORKSPACE SYSTEM +`; + +// ---- tiny render helpers --------------------------------------------------------------------- + +const esc = (s) => String(s).replace(/&/g, "&").replace(//g, ">"); +const cards = (items, cols = 3) => + `
` + + items + .map( + (c) => + `
${c.tag ? `${esc(c.tag)}` : ""}` + + `

${c.h}

${c.d}
`, + ) + .join("") + + `
`; +const phases = (list) => + `
` + + list + .map( + (p) => + `
${esc(p.n)}
` + + `
${p.t}${p.d}
`, + ) + .join("") + + `
`; +const files = (list) => + ``; + +const section = ({ label, h2, gate, gateRoad, lede, body }) => + `
+
${label}
+

${h2}${gate ? ` ${esc(gate)}` : ""}

+ ${lede ? `

${lede}

` : ""} + ${body || ""} +
`; + +const page = ({ titleSuffix, kicker, h1, lede, pills, sections }) => ` + + + + +Benchfinity — ${titleSuffix} + + + + + + + + +
+
+ ${esc(kicker)} + ${LOCKUP} +
+

${h1}

+

${lede}

+
+
${pills.map((p) => `${esc(p.t)}`).join("")}
+
+ ${sections.join("\n ")} +
Generated from BenchFinity/Workbench — the product and software data is owned there and rendered here so the two never drift. Honest-state: present tense is shipped; everything labeled Roadmap is planned, not built.
+
+ + +`; + +// ---- PRODUCT division ------------------------------------------------------------------------ + +const productPage = page({ + titleSuffix: "Product System Review", + kicker: `Product System Review · ${STAMP} · source: BenchFinity/Workbench`, + h1: "Product — one coordinated workspace system.", + lede: "What Benchfinity is and why, ahead of how it's coded. The product is owned in the Workbench repo; this page renders it for the company OS. Held to honest-state: what ships today is a Gridfinity-compatible grid generator — the rest is labeled roadmap.", + pills: [ + { t: "← Company OS", href: "../index.html" }, + { t: "Brand showcase ↗", href: "../brand/review.html" }, + { t: "Marketing showcase ↗", href: "../marketing/review.html" }, + { t: "PRODUCT-VISION ↗", href: `${BLOB}/docs/PRODUCT-VISION.md` }, + ], + sections: [ + section({ + label: "The frame", + h2: "Free, open source, organization-first", + gate: "Foundation ✓", + lede: "Benchfinity turns a whole collection into one coordinated workspace system — measure your real drawers and spaces, generate parts engineered to the Gridfinity standard, and compose them into reusable systems. Free and open source; monetization is a non-goal.", + body: cards( + [ + { + h: "Organization", + d: "A system for the workspace, not a pile of one-off parts.", + tag: "Purpose", + tagKind: "lead", + }, + { + h: "Engineered to the Gridfinity standard", + d: "Parts fit together and snap into what you already own. Parametric and deterministic — never “AI.”", + tag: "Register", + }, + { + h: "One place", + d: "The platform that unifies Gridfinity generation — not one more single-purpose generator.", + tag: "Scope", + }, + ], + 3, + ), + }), + section({ + label: "What it generates", + h2: "Grids today, a growing family next", + lede: "Everything starts with a grid fit to a measured drawer. Bins that sit on grids, and stand-alone boxes, are the planned families — each a parametric generator emitting a Gridfinity-compatible footprint.", + body: cards( + [ + { + h: "Grids (baseplates)", + d: "Measure a drawer; Benchfinity fits whole Gridfinity cells, centers and pads to the real space, and auto-splits to your printer bed. Exports STL, split ZIP, and Bambu-style 3MF.", + tag: "Ships today", + tagKind: "lead", + }, + { + h: "Bins", + d: "An open-ended family of parametric bin types — open, storage, tool-traced (from a captured outline), and collection-specific (e.g. model-railroad, sockets, wrenches). Added continuously.", + tag: "Roadmap", + }, + { + h: "Stand-alone boxes", + d: "Modular boxes that interoperate with grids and bins under the same compatibility contract, without being grid-bound.", + tag: "Roadmap", + }, + ], + 3, + ), + }), + section({ + label: "How a system is organized", + h2: "Collection → system, with reuse", + gate: "Roadmap", + gateRoad: true, + lede: "Signed in, parts compose into real furniture and reuse across it: a System (a tool chest, an HO-railroad table) holds measured Container Types (a drawer + its grid), stamped into many identical instances with different contents, filled from a shared Component Library.", + body: + cards( + [ + { + h: "Part reuse", + d: "A saved part (say a 2×1 scoop bin) dropped across any number of containers.", + tag: "Reuse axis", + }, + { + h: "Grid reuse", + d: "Define a drawer + grid once; stamp it across 12 identical drawers, each with unique contents.", + tag: "Reuse axis", + }, + { + h: "Layout reuse", + d: "Saved spatial arrangements of bins on a grid, remembered per drawer. The richer end-state.", + tag: "Reuse axis", + }, + ], + 3, + ) + + `
Access line. Anyone can generate a single part, preview it in real-time 3D, and export STL/3MF with no account. Saving, reuse, and full systems are the signed-in capability — all labeled roadmap until they ship.
`, + }), + section({ + label: "Roadmap", + h2: "Launch, then build — small increments to production", + lede: "The grid generator goes live first; depth is added against a running, public app. A logged-in backbone and an anonymous generators track advance in parallel. Full detail in ROADMAP.md.", + body: phases([ + { + n: "0", + t: "Launch — grid generator live", + d: "Deploy the existing grid generator to production at benchfinity.com. The launch-then-build beachhead.", + }, + { + n: "A", + t: "Platform spine", + d: "Accounts, persistence, and a signed-in app shell. Backend foundation.", + road: true, + }, + { + n: "B", + t: "Grid persisted & reusable", + d: "Save and reuse grids; container types and instances; export history. The pure generator is wrapped, never rewritten.", + road: true, + }, + { + n: "C", + t: "Bin generators", + d: "The parametric bin family + stand-alone boxes, shipping to the live anonymous app in parallel.", + road: true, + }, + { + n: "D", + t: "Composition & visual layout", + d: "Compose whole systems and place bins on grids visually — the rich end-state.", + road: true, + }, + ]), + }), + section({ + label: "The discipline", + h2: "Honest-state", + lede: "Shipped capability is present tense; the vision is labeled roadmap, never blurred. Today Benchfinity generates Gridfinity-compatible grids with real exports — orchestrating a whole collection into a coordinated system is where it's going.", + body: `
Output rights (fixed)The platform code is AGPL-3.0; the designs you generate are yours to use, share, sell, or print — no copyleft attaches to your output.
`, + }), + section({ + label: "Source (owned in Workbench)", + h2: "Where the product is defined", + body: files([ + { ly: "vis", t: "docs/PRODUCT-VISION.md (authoritative)", href: `${BLOB}/docs/PRODUCT-VISION.md` }, + { ly: "map", t: "docs/ROADMAP.md (phases)", href: `${BLOB}/docs/ROADMAP.md` }, + { ly: "ops", t: "AGENTS.md (operational guide)", href: `${BLOB}/AGENTS.md` }, + { ly: "src", t: "BenchFinity/Workbench (repo)", href: REPO }, + ]), + }), + ], +}); + +// ---- SOFTWARE division ----------------------------------------------------------------------- + +const softwarePage = page({ + titleSuffix: "Software System Review", + kicker: `Software System Review · ${STAMP} · source: BenchFinity/Workbench`, + h1: "Software — engineered in the open.", + lede: "The platform is open source (AGPL-3.0) and lives in a separate repo; this division holds company-side references. Rendered from Workbench so the stack, architecture, and status never drift. Honest-state throughout: shipped vs. labeled roadmap.", + pills: [ + { t: "← Company OS", href: "../index.html" }, + { t: "Brand showcase ↗", href: "../brand/review.html" }, + { t: "Workbench repo ↗", href: REPO }, + { t: "ROADMAP ↗", href: `${BLOB}/docs/ROADMAP.md` }, + ], + sections: [ + section({ + label: "The frame", + h2: "Open source, separate repo, brand-aligned", + gate: "Foundation ✓", + lede: "The code is BenchFinity/Workbench — public, AGPL-3.0, default branch develop. It is not mirrored into the company OS; this division references it. When it builds UI, it consumes the brand tokens rather than redefining them.", + body: cards( + [ + { + h: "AGPL-3.0", + d: "Copyleft on the code. Your generated designs are yours.", + tag: "License", + tagKind: "lead", + }, + { + h: "Client-only today", + d: "A Vite + React + Three.js app that runs entirely in the browser — near-zero hosting cost.", + tag: "Shipped", + }, + { + h: "Brand tokens, not redefined", + d: "The product UI should compile brand/colors.md + typography.md into its theme.", + tag: "Convention", + }, + ], + 3, + ), + }), + section({ + label: "The stack", + h2: "What runs, and what's planned", + lede: "The frontend and the deploy pipeline ship today. The backend is a deliberate, labeled roadmap addition for accounts and persistence.", + body: cards( + [ + { + h: "Frontend", + d: "Vite + React + TypeScript + Three.js. Real-time 3D preview; STL, split-ZIP, and Bambu-style 3MF export. The quality bar.", + tag: "Ships today", + tagKind: "lead", + }, + { + h: "Geometry & export core", + d: "Pure TypeScript (no React/DOM/storage) — centered-padded sizing, bed-fit split planning, mesh + 3MF generation. Tested.", + tag: "Ships today", + tagKind: "lead", + }, + { + h: "Delivery / ops", + d: "Distroless Chainguard images, multi-arch GHCR publish, Helm chart, HIGH+ security gates (audit, dependency-review, Trivy, CodeQL), signed commits.", + tag: "Ships today", + tagKind: "lead", + }, + { + h: "Backend", + d: "A QQQ + Postgres service for accounts, persistence, and admin — the spine that saved/reusable systems need.", + tag: "Roadmap", + }, + ], + 2, + ), + }), + section({ + label: "Architecture", + h2: "A pure core, wrapped — never rewritten", + lede: "The geometry/validation/export core stays free of framework, DOM, and storage. Persistence and new generators wrap it; the math is never reimplemented. This is also what makes a pluggable generator family possible.", + body: cards( + [ + { + h: "Pure generation core", + d: "Inputs in, geometry + export out. Deterministic and unit-tested, independent of any UI.", + tag: "Boundary", + }, + { + h: "Pluggable generators", + d: "Each part type (grid, each bin, stand-alone box) is a self-contained pure module emitting a Gridfinity-compatible footprint.", + tag: "Roadmap", + }, + { + h: "Persistence seam", + d: "Designs persist as inputs + derived metadata; 3D meshes are recomputed on load, never stored.", + tag: "Roadmap", + }, + ], + 3, + ), + }), + section({ + label: "Modules", + h2: "The software division map", + lede: "The company-OS software/ subfolders hold references; the implementations live in Workbench.", + body: phases([ + { + n: "web", + t: "Web app", + d: "The React + Three.js generator UI and app shell. Shipping (grid generator); app shell is roadmap.", + }, + { n: "core", t: "Generation core", d: "Pure geometry, validation, and split planning. Shipping." }, + { n: "gen", t: "Generators", d: "The bin/box plugin family. Roadmap.", road: true }, + { n: "cli", t: "CLI", d: "Headless/batch generation. Not started.", road: true }, + { + n: "int", + t: "Integrations", + d: "Slicer-friendly export and capture (photo/scan/measurement) inputs. Roadmap.", + road: true, + }, + ]), + }), + section({ + label: "Source (owned in Workbench)", + h2: "Where the software is defined", + body: files([ + { ly: "src", t: "BenchFinity/Workbench (repo)", href: REPO }, + { ly: "ops", t: "AGENTS.md (architecture boundaries)", href: `${BLOB}/AGENTS.md` }, + { ly: "map", t: "docs/ROADMAP.md", href: `${BLOB}/docs/ROADMAP.md` }, + { ly: "dep", t: "docs/DEPLOY.md", href: `${BLOB}/docs/DEPLOY.md` }, + ]), + }), + ], +}); + +// ---- write ----------------------------------------------------------------------------------- + +const targets = [ + { rel: "product/review.html", html: productPage }, + { rel: "software/review.html", html: softwarePage }, +]; + +if (!existsSync(COMPANY_OS_DIR)) { + console.error(`Company OS dir not found: ${COMPANY_OS_DIR} (set COMPANY_OS_DIR to override)`); + process.exit(1); +} + +for (const t of targets) { + const out = resolve(COMPANY_OS_DIR, t.rel); + mkdirSync(dirname(out), { recursive: true }); + writeFileSync(out, t.html); + console.log(`wrote ${out} (${t.html.length} bytes)`); +} diff --git a/src/App.tsx b/src/App.tsx index da5679c..fb55ef5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,7 +9,8 @@ import { buildExportFilename } from "./export/filenames"; import { createConnectorKeyPart, createPlateModels } from "./geometry/model"; import { deriveLayout } from "./geometry/layout"; import type { PlateInput } from "./geometry/types"; -import { CUSTOM_PRINTER_ID, findPrinterPreset, groupPrintersByBrand } from "./printers"; +import { CUSTOM_PRINTER_ID, groupPrintersByBrand } from "./printers"; +import { DESIGN_SCHEMA_VERSION, applyPrinterToInput } from "./design"; import { FACTORY_DEFAULTS, loadSavedDefaults, saveDefaults, type AppDefaults } from "./settings"; import { validateExport } from "./validation"; @@ -17,8 +18,8 @@ export function App() { const initialDefaults = useMemo(() => loadSavedDefaults(), []); const [savedDefaults, setSavedDefaults] = useState(initialDefaults); const [draftDefaults, setDraftDefaults] = useState(initialDefaults); - const [input, setInput] = useState(initialDefaults.input); - const [selectedPrinterId, setSelectedPrinterId] = useState(initialDefaults.selectedPrinterId); + const [input, setInput] = useState(initialDefaults.design.input); + const [selectedPrinterId, setSelectedPrinterId] = useState(initialDefaults.design.selectedPrinterId); const [exploded, setExploded] = useState(initialDefaults.exploded); const [settingsOpen, setSettingsOpen] = useState(false); const [exporting, setExporting] = useState(false); @@ -51,17 +52,7 @@ export function App() { setSelectedPrinterId(printerId); setStatus(null); setDownloadInfo(null); - - const preset = findPrinterPreset(printerId); - - if (preset) { - setInput((current) => ({ - ...current, - bedWidth: preset.bedWidth, - bedDepth: preset.bedDepth, - bedUnit: preset.unit, - })); - } + setInput((current) => applyPrinterToInput(current, printerId)); }; const reset = () => { @@ -80,8 +71,8 @@ export function App() { }; const applyDefaults = (defaults: AppDefaults) => { - setSelectedPrinterId(defaults.selectedPrinterId); - setInput(defaults.input); + setSelectedPrinterId(defaults.design.selectedPrinterId); + setInput(defaults.design.input); setExploded(defaults.exploded); }; @@ -96,8 +87,11 @@ export function App() { const useCurrentAsDefaults = () => { setDraftDefaults({ - input, - selectedPrinterId, + design: { + schemaVersion: DESIGN_SCHEMA_VERSION, + input, + selectedPrinterId, + }, exploded, }); }; @@ -116,7 +110,12 @@ export function App() { setStatus(null); try { - const exportFilename = buildExportFilename(input.projectName, layout.cols, layout.rows, exportFileExtension(format, models)); + const exportFilename = buildExportFilename( + input.projectName, + layout.cols, + layout.rows, + exportFileExtension(format, models), + ); const blob = await createExportBlob({ format, input, @@ -170,7 +169,11 @@ export function App() { void }) { +export function TextField({ + label, + value, + onChange, +}: { + label: string; + value: string; + onChange: (value: string) => void; +}) { return (