@bongos/core 1.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +5727 -0
- package/.claude/skills/blocker-review/SKILL.md +91 -0
- package/.claude/skills/blocker-solve/SKILL.md +96 -0
- package/.claude/skills/builder-backup/SKILL.md +54 -0
- package/.claude/skills/builder-box/SKILL.md +45 -0
- package/.claude/skills/builder-claim/SKILL.md +76 -0
- package/.claude/skills/builder-connect/SKILL.md +59 -0
- package/.claude/skills/builder-cost/SKILL.md +47 -0
- package/.claude/skills/builder-end/SKILL.md +81 -0
- package/.claude/skills/builder-exit/SKILL.md +88 -0
- package/.claude/skills/builder-key/SKILL.md +40 -0
- package/.claude/skills/builder-reauth/SKILL.md +50 -0
- package/.claude/skills/builder-redteam/SKILL.md +109 -0
- package/.claude/skills/builder-release/SKILL.md +41 -0
- package/.claude/skills/builder-setup/SKILL.md +62 -0
- package/.claude/skills/builder-ship/SKILL.md +79 -0
- package/.claude/skills/builder-stage/SKILL.md +51 -0
- package/.claude/skills/builder-start/SKILL.md +46 -0
- package/.claude/skills/close-box/SKILL.md +43 -0
- package/.claude/skills/design-sync/SKILL.md +69 -0
- package/.claude/skills/dev/SKILL.md +49 -0
- package/.claude/skills/feedback/SKILL.md +32 -0
- package/.claude/skills/figma-design-sync/SKILL.md +74 -0
- package/.claude/skills/goal-review/SKILL.md +89 -0
- package/.claude/skills/idea-triage/SKILL.md +144 -0
- package/.claude/skills/ideate/SKILL.md +82 -0
- package/.claude/skills/merge-mode/SKILL.md +169 -0
- package/.claude/skills/new-project/SKILL.md +108 -0
- package/.claude/skills/otb-character-review/SKILL.md +71 -0
- package/.claude/skills/otb-design-review/SKILL.md +47 -0
- package/.claude/skills/otb-feedback-capture/SKILL.md +45 -0
- package/.claude/skills/otb-figma-sync/SKILL.md +54 -0
- package/.claude/skills/otb-tile-generate/SKILL.md +45 -0
- package/.claude/skills/paint/SKILL.md +70 -0
- package/.claude/skills/planning-session/SKILL.md +299 -0
- package/.claude/skills/priority-session/SKILL.md +134 -0
- package/.claude/skills/read-session-export/SKILL.md +58 -0
- package/.claude/skills/recall/SKILL.md +52 -0
- package/.claude/skills/session-handoff/SKILL.md +46 -0
- package/.claude/skills/status/SKILL.md +33 -0
- package/.devcontainer/Dockerfile +69 -0
- package/.devcontainer/README.md +73 -0
- package/.devcontainer/devcontainer-lock.json +19 -0
- package/.devcontainer/devcontainer.json +93 -0
- package/.devcontainer/healthcheck.sh +84 -0
- package/.devcontainer/init-firewall.sh +198 -0
- package/.devcontainer/post-create.sh +153 -0
- package/.dockerignore +36 -0
- package/.env.local.example +32 -0
- package/.gitattributes +83 -0
- package/.gitignore +91 -0
- package/CLAUDE.md +242 -0
- package/CODE_OF_CONDUCT.md +46 -0
- package/CONTRIBUTING.md +51 -0
- package/Dockerfile +46 -0
- package/GOVERNANCE.md +44 -0
- package/LICENSE +661 -0
- package/README.md +88 -0
- package/bin/bongos.js +222 -0
- package/clients/bongos-client/README.md +48 -0
- package/clients/bongos-client/bongos-client.global.js +617 -0
- package/clients/bongos-client/examples/hello-world.mjs +62 -0
- package/clients/bongos-client/index.cjs +619 -0
- package/clients/bongos-client/index.d.ts +793 -0
- package/clients/bongos-client/index.mjs +614 -0
- package/clients/bongos-client/package.json +32 -0
- package/config/branding.neutral.json +60 -0
- package/config/design-tokens.neutral.json +51 -0
- package/config/design-tokens.schema.json +96 -0
- package/config/modules.neutral.json +9 -0
- package/docker-compose.yml +52 -0
- package/docker-entrypoint.sh +30 -0
- package/docs/adr/0001-stack-choice-single-vps.md +45 -0
- package/docs/adr/0002-digitalocean-over-hetzner.md +27 -0
- package/docs/adr/0003-cloudflare-origin-cert-over-letsencrypt.md +35 -0
- package/docs/adr/0004-amazonprimea-name-and-trademark-acceptance.md +45 -0
- package/docs/adr/0005-invisible-other-players-in-v1.md +37 -0
- package/docs/adr/0006-singleton-worldroom-plus-queueroom.md +30 -0
- package/docs/adr/0007-persistent-identity-via-localstorage-uuid.md +31 -0
- package/docs/adr/0008-family-based-tile-generation.md +86 -0
- package/docs/adr/0008-google-chat-oauth-user-auth.md +37 -0
- package/docs/adr/0009-autotile-architecture.md +181 -0
- package/docs/adr/0010-pms-architecture.md +70 -0
- package/docs/adr/0011-terrain-id-rendering-model.md +174 -0
- package/docs/adr/0012-ground-object-cell-split.md +208 -0
- package/docs/adr/0013-paths-as-object-overlays-with-autotile-dispatch.md +158 -0
- package/docs/adr/0014-families-canonical-over-vocabulary.md +103 -0
- package/docs/adr/0015-task-dependencies-and-auto-promotion.md +125 -0
- package/docs/adr/0016-trust-boundary-server-enforced-permissions.md +132 -0
- package/docs/adr/0017-code-simplification-and-repo-cleanliness.md +74 -0
- package/docs/adr/0018-three-rank-model-goes-live.md +97 -0
- package/docs/adr/0019-red-team-bounty-payout-table.md +52 -0
- package/docs/adr/0020-security-model-public-stub.md +70 -0
- package/docs/adr/0021-per-builder-skill-model-preferences.md +99 -0
- package/docs/adr/0022-secrets-policy.md +124 -0
- package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +118 -0
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +277 -0
- package/docs/adr/0024-multi-agent-system-architecture.md +125 -0
- package/docs/adr/0025-offsite-backup-vendor-digitalocean-spaces.md +59 -0
- package/docs/adr/0025-structured-criterion-task-link.md +44 -0
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +117 -0
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +170 -0
- package/docs/adr/0028-honest-llm-spend-accounting.md +109 -0
- package/docs/adr/0029-status-page-outage-resilience.md +59 -0
- package/docs/adr/0030-builders-hall-live-updates.md +59 -0
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +222 -0
- package/docs/adr/0032-discord-mirror-of-gds.md +50 -0
- package/docs/adr/0033-discord-bot-service-principal.md +82 -0
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +53 -0
- package/docs/adr/0035-builder-onboarding-three-paths.md +128 -0
- package/docs/adr/0036-builders-subdomain-and-ui-reorg.md +40 -0
- package/docs/adr/0037-discord-channel-management.md +51 -0
- package/docs/adr/0038-chromebook-ttyd-cloudflare-tunnel.md +111 -0
- package/docs/adr/0039-setup-first-onboarding-ux.md +49 -0
- package/docs/adr/0040-remote-control-default-browser-onramp.md +47 -0
- package/docs/adr/0041-temporary-grader-bypass-killswitch.md +40 -0
- package/docs/adr/0042-builder-self-deploy-ci-auto-merge.md +82 -0
- package/docs/adr/0043-git-ssh-trust-boundary-and-rank-floor-on-permission-paths.md +74 -0
- package/docs/adr/0044-mediterranean-palette-replacement.md +73 -0
- package/docs/adr/0044-per-box-live-game-preview.md +57 -0
- package/docs/adr/0045-devbox-desktop-app.md +133 -0
- package/docs/adr/0046-sandbox-first-review-gate.md +72 -0
- package/docs/adr/0047-discord-bugs-intake-anti-abuse.md +36 -0
- package/docs/adr/0048-works-in-progress-ia-split.md +32 -0
- package/docs/adr/0049-split-parallel-safety-contract.md +46 -0
- package/docs/adr/0050-device-flow-admission-invite-gated-by-default.md +43 -0
- package/docs/adr/0051-full-session-transcript-corpus.md +57 -0
- package/docs/adr/0052-sandbox-for-everyone-game-only-preview.md +70 -0
- package/docs/adr/0053-scoped-dev-box-session.md +55 -0
- package/docs/adr/0054-session-token-reward-cost-plus.md +63 -0
- package/docs/adr/0055-server-mediated-branch-publish.md +47 -0
- package/docs/adr/0056-prod-deploy-script-mirror-and-healthcheck-retry.md +62 -0
- package/docs/adr/0057-container-cost-ledger.md +28 -0
- package/docs/adr/0057-discord-archon-approval-channels.md +64 -0
- package/docs/adr/0058-automatic-gate-surface-gatekeeper.md +64 -0
- package/docs/adr/0059-single-approval-remove-devbox-approval-gate.md +37 -0
- package/docs/adr/0060-gds-retrieval-layer.md +97 -0
- package/docs/adr/0061-context-layer-decomposition.md +61 -0
- package/docs/adr/0062-medusa-instance-model-decoupling.md +107 -0
- package/docs/adr/0063-generated-repo-map-symbol-skeleton.md +44 -0
- package/docs/adr/0064-rename-medusa-to-cloud-bongos.md +49 -0
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +69 -0
- package/docs/adr/0066-generated-file-map-sections.md +51 -0
- package/docs/adr/0067-session-start-fast-forward-pull.md +43 -0
- package/docs/adr/0068-shared-art-key-for-newcomers.md +38 -0
- package/docs/adr/0069-hall-live-channel-sse-decouple.md +35 -0
- package/docs/adr/0070-deterministic-session-card-delivery.md +44 -0
- package/docs/adr/0071-box-confirm-before-destroyed-and-drift-reconcile.md +59 -0
- package/docs/adr/0072-bongos-app-mac-signed-first-windows-deferred.md +35 -0
- package/docs/adr/0072-dev-box-code-staleness-visibility.md +39 -0
- package/docs/adr/0073-builder-needs-signal-and-byok-gemini-key.md +64 -0
- package/docs/adr/0073-secrets-scan-exclude-uri-detector.md +37 -0
- package/docs/adr/0074-memory-map-stays-light.md +32 -0
- package/docs/adr/0075-per-system-cost-attribution.md +37 -0
- package/docs/adr/0076-in-session-deterministic-compaction.md +58 -0
- package/docs/adr/0077-content-addressed-llm-cache.md +32 -0
- package/docs/adr/0078-deterministic-first-cron.md +48 -0
- package/docs/adr/0079-value-gated-autonomous-engagement.md +39 -0
- package/docs/adr/0080-llm-cache-api-transport-and-trust-model.md +46 -0
- package/docs/adr/0081-tool-agnostic-design-layer.md +62 -0
- package/docs/adr/0082-productized-key-storage-provisioning.md +32 -0
- package/docs/adr/0082-server-side-merge-conflict-auto-resolution.md +55 -0
- package/docs/adr/0083-modular-architecture-strangler-build-plan.md +143 -0
- package/docs/adr/0084-auto-derived-per-task-rank-floor.md +86 -0
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +39 -0
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +123 -0
- package/docs/adr/0087-bongos-app-architecture-and-handoff-contract.md +49 -0
- package/docs/adr/0087-compete-on-governance-not-tooling-cursor.md +65 -0
- package/docs/adr/0088-consistent-character-generative-animation.md +169 -0
- package/docs/adr/0089-modular-multi-model-adversarial-grader.md +179 -0
- package/docs/adr/0090-metic-task-authoring.md +73 -0
- package/docs/adr/0091-bounding-the-kernel-and-db-carve.md +106 -0
- package/docs/adr/0092-hosting-module-and-on-prem-target.md +117 -0
- package/docs/adr/0093-tranche-2-core-carve-sequence.md +72 -0
- package/docs/adr/0094-repo-atlas-self-drawing-codebase-map.md +49 -0
- package/docs/adr/0095-borrowed-memory-and-retrieval-concepts.md +65 -0
- package/docs/adr/0095-cross-agent-context-management.md +58 -0
- package/docs/adr/0096-require-reward-before-workable.md +209 -0
- package/docs/adr/0097-one-active-claim-per-session-and-worktree-binding.md +46 -0
- package/docs/adr/0097-retroactive-reward-backfill.md +124 -0
- package/docs/adr/0098-public-mirror-publish-manifest.md +92 -0
- package/docs/adr/0099-delayed-redacted-mirror-export.md +55 -0
- package/docs/adr/0100-two-repo-instance-model.md +60 -0
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +90 -0
- package/docs/adr/0102-finish-bounding-the-kernel.md +36 -0
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +70 -0
- package/docs/adr/0103-gdsv4-bongos-consolidation.md +28 -0
- package/docs/adr/0104-trust-gds-api-channel-in-auto-mode.md +42 -0
- package/docs/adr/0105-instance-seed-migrations-out-of-core.md +47 -0
- package/docs/adr/0106-goal-owner-manager-authority.md +84 -0
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +83 -0
- package/docs/adr/0108-instance-composition-configurable-root.md +87 -0
- package/docs/adr/0109-self-describing-openapi-and-hosted-docs.md +44 -0
- package/docs/adr/0110-untrack-whole-file-generated-nav-docs.md +49 -0
- package/docs/adr/0111-instance-hosting-provisioning-module.md +135 -0
- package/docs/adr/0112-private-goals-invitations-join-requests.md +72 -0
- package/docs/adr/0113-sandbox-studio-descope-superseded-by-0044-0052.md +41 -0
- package/docs/adr/0114-instance-docs-discovery-contract.md +32 -0
- package/docs/adr/0115-scheduled-routines-portability.md +31 -0
- package/docs/adr/0116-api-error-envelope-and-code-taxonomy.md +62 -0
- package/docs/adr/0117-api-versioning-and-deprecation-policy.md +50 -0
- package/docs/adr/0118-api-contract-source-of-truth-code-first.md +47 -0
- package/docs/adr/0119-api-pagination-contract.md +34 -0
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +108 -0
- package/docs/adr/0121-greenfield-vs-brownfield-onboarding-adopt-existing-repo.md +97 -0
- package/docs/adr/0122-onboarding-diagram-renders-deploy-rendered-off-git.md +43 -0
- package/docs/adr/0123-box-idle-sweep-autosave-before-destroy.md +36 -0
- package/docs/adr/0124-typed-api-response-bodies.md +37 -0
- package/docs/adr/0125-control-plane-scaffolds-standalone-instance-repo.md +36 -0
- package/docs/adr/0126-dedicated-cloudbongos-control-plane-droplet.md +30 -0
- package/docs/adr/0127-cross-box-fleet-observability.md +64 -0
- package/docs/adr/0128-provisioning-runner-privilege-and-db-auth-model.md +46 -0
- package/docs/adr/0129-cloudbongos-control-plane-pull-deploy.md +34 -0
- package/docs/adr/0130-provision-remote-exec-over-ssh.md +61 -0
- package/docs/adr/0131-rank-scoped-skill-visibility.md +47 -0
- package/docs/adr/0132-cohost-fleet-control-plane-migration.md +54 -0
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +42 -0
- package/docs/adr/0134-private-first-npm-distribution.md +38 -0
- package/docs/adr/README.md +208 -0
- package/docs/api/openapi.json +13720 -0
- package/docs/api-reference.md +437 -0
- package/docs/architecture.md +244 -0
- package/docs/branding-contract.md +66 -0
- package/docs/branding-fill-prompt.md +25 -0
- package/docs/canonical-permissions.md +241 -0
- package/docs/design/container-exterior-tiles.md +49 -0
- package/docs/design/container-exterior.md +81 -0
- package/docs/design/container-interior.md +77 -0
- package/docs/design/modular-architecture/00-research-report.md +107 -0
- package/docs/design/modular-architecture/01-architecture.md +125 -0
- package/docs/design/modular-architecture/02-module-map.md +116 -0
- package/docs/design/modular-architecture/03-builder-flows.md +104 -0
- package/docs/design/modular-architecture/04-module-lifecycle.md +62 -0
- package/docs/design/modular-architecture/README.md +27 -0
- package/docs/design/vanilla-hall-ui-redesign-scope.md +284 -0
- package/docs/design/world-map-scroll.md +41 -0
- package/docs/design-contract.md +320 -0
- package/docs/file-map.md +418 -0
- package/docs/handoff-template.md +92 -0
- package/docs/modules-contract.md +293 -0
- package/docs/onboarding/browser-terminal-guide.md +73 -0
- package/docs/onboarding/diagrams/01-task-lifecycle.mmd +28 -0
- package/docs/onboarding/diagrams/02-rank-ladder.mmd +54 -0
- package/docs/onboarding/diagrams/03-drachmae-karma.mmd +37 -0
- package/docs/onboarding/diagrams/04-architecture.mmd +51 -0
- package/docs/onboarding/diagrams/05-versioning.mmd +29 -0
- package/docs/onboarding/diagrams/README.md +94 -0
- package/docs/onboarding/diagrams/assertions.json +170 -0
- package/docs/onboarding/diagrams/puppeteer-config.json +1 -0
- package/docs/onboarding/diagrams/render-config.json +9 -0
- package/docs/onboarding/drachmae-and-karma.md +178 -0
- package/docs/onboarding/primer.md +202 -0
- package/docs/project-context.template.md +67 -0
- package/docs/recipes/README.md +35 -0
- package/docs/recipes/audit-replay.md +100 -0
- package/docs/recipes/bongos-cli-release.md +66 -0
- package/docs/recipes/builder-box-lifecycle.md +152 -0
- package/docs/recipes/claude-design-loop.md +143 -0
- package/docs/recipes/claude-gemini-bongos-doors.md +155 -0
- package/docs/recipes/cloudbongos-staging-gate.md +139 -0
- package/docs/recipes/cloudbongos-standup.md +111 -0
- package/docs/recipes/devbox-app-release.md +93 -0
- package/docs/recipes/devbox-app-windows-test.md +128 -0
- package/docs/recipes/failed-ship-recovery-proof.md +102 -0
- package/docs/recipes/figma-design-loop.md +129 -0
- package/docs/recipes/gds-db-backup.md +255 -0
- package/docs/recipes/instance-deploy.md +37 -0
- package/docs/recipes/managed-settings-remote-control.md +416 -0
- package/docs/recipes/memory-backup.md +71 -0
- package/docs/recipes/memory-sync-conflicts.md +98 -0
- package/docs/recipes/multi-builder-merge.md +78 -0
- package/docs/recipes/ops-gotchas.md +212 -0
- package/docs/recipes/overnight-builder.md +53 -0
- package/docs/recipes/packaging-the-core.md +49 -0
- package/docs/recipes/parallel-load-results-2026-05-30.md +63 -0
- package/docs/recipes/parallel-load-test.md +32 -0
- package/docs/recipes/pixel-art-pipeline.md +411 -0
- package/docs/recipes/private-npm-distribution.md +79 -0
- package/docs/recipes/search-index-ingestion.md +67 -0
- package/docs/recipes/self-host.md +60 -0
- package/docs/recipes/ship-and-regrade.md +72 -0
- package/docs/recipes/staging-data.md +50 -0
- package/docs/recipes/staging-environment.md +215 -0
- package/docs/recipes/upgrading-the-core.md +58 -0
- package/docs/research/cursor-competitive-analysis.md +255 -0
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +227 -0
- package/migrations/003_pms.sql +284 -0
- package/migrations/004_pms_cost_idempotency.sql +13 -0
- package/migrations/005_pms_smoke_filter.sql +44 -0
- package/migrations/006_pms_v2.sql +378 -0
- package/migrations/007_pms_v2_hierarchy_trigger.sql +59 -0
- package/migrations/008_pms_v2_migration_allocator.sql +27 -0
- package/migrations/009_pms_v2_blockers_table.sql +68 -0
- package/migrations/010_uptime.sql +29 -0
- package/migrations/011_backfill_v2_v3_done_when.sql +20 -0
- package/migrations/012_pms_v2_task_lifecycle_states.sql +82 -0
- package/migrations/013_claim_locking.sql +22 -0
- package/migrations/014_pms_task_discipline.sql +86 -0
- package/migrations/015_pms_estimation_calibration.sql +140 -0
- package/migrations/016_pms_task_grades.sql +60 -0
- package/migrations/017_version_progress_exclude_abandoned.sql +49 -0
- package/migrations/019_rename_pms_v3_to_gds_v3.sql +65 -0
- package/migrations/020_task_dependencies.sql +99 -0
- package/migrations/021_v3_renumber_and_deps.sql +224 -0
- package/migrations/022_v3_reconcile_dep_gated_status.sql +41 -0
- package/migrations/023_pms_rank.sql +30 -0
- package/migrations/025_builder_exit_status.sql +15 -0
- package/migrations/027_builders_preferred_disciplines.sql +46 -0
- package/migrations/028_tasks_parallel_safe.sql +71 -0
- package/migrations/029_rank_three_live.sql +32 -0
- package/migrations/030_vulnerability_reports.sql +61 -0
- package/migrations/031_reward_simplification.sql +57 -0
- package/migrations/032_repo_health_snapshots.sql +56 -0
- package/migrations/033_audit_log.sql +64 -0
- package/migrations/034_lifecycle_breakdown_leaf_only.sql +62 -0
- package/migrations/035_auth_token_hygiene.sql +66 -0
- package/migrations/037_builder_skill_model_prefs.sql +44 -0
- package/migrations/038_task_override_requests.sql +51 -0
- package/migrations/039_newcomer_friendly_column.sql +124 -0
- package/migrations/040_builder_karma.sql +58 -0
- package/migrations/041_kind_multipliers_and_idea_bonus.sql +42 -0
- package/migrations/042_criterion_proposal_workflow.sql +78 -0
- package/migrations/043_builder_onboarding_observability.sql +66 -0
- package/migrations/044_tasks_requires_rank.sql +97 -0
- package/migrations/045_builder_onboarding_state.sql +147 -0
- package/migrations/046_cost_log_builder_attribution.sql +42 -0
- package/migrations/054_builder_memory.sql +108 -0
- package/migrations/055_task_criteria.sql +90 -0
- package/migrations/058_peer_votes.sql +41 -0
- package/migrations/059_onboarding_pick_disciplines.sql +59 -0
- package/migrations/060_access_requests.sql +46 -0
- package/migrations/061_backfill_override_ship_attribution.sql +29 -0
- package/migrations/062_session_records.sql +34 -0
- package/migrations/063_session_records_tool_errors_idx.sql +11 -0
- package/migrations/064_grade_attempts.sql +18 -0
- package/migrations/065_bfg_principal.sql +58 -0
- package/migrations/066_live_notify_triggers.sql +69 -0
- package/migrations/067_builder_boxes.sql +117 -0
- package/migrations/067_discord_bot_foundation.sql +69 -0
- package/migrations/068_thetes_requires_rank.sql +27 -0
- package/migrations/069_builder_ssh_keys.sql +66 -0
- package/migrations/070_box_intents.sql +101 -0
- package/migrations/071_box_first_provision_approval.sql +46 -0
- package/migrations/072_onboarding_ship_three_remove_primer.sql +80 -0
- package/migrations/073_box_terminal_access.sql +46 -0
- package/migrations/075_builder_sound_prefs.sql +28 -0
- package/migrations/078_box_claude_active.sql +22 -0
- package/migrations/079_box_first_connected_at.sql +20 -0
- package/migrations/081_tasks_needs_migration.sql +34 -0
- package/migrations/084_tasks_security_sensitive.sql +24 -0
- package/migrations/085_security_engagement_docs.sql +34 -0
- package/migrations/086_rename_disciplines.sql +125 -0
- package/migrations/088_builders_anthropic_link.sql +28 -0
- package/migrations/089_builders_monthly_budget.sql +19 -0
- package/migrations/090_box_scoped_sessions.sql +38 -0
- package/migrations/091_session_token_reward.sql +44 -0
- package/migrations/093_tasks_sandbox_review.sql +22 -0
- package/migrations/102_remove_box_first_provision_approval.sql +58 -0
- package/migrations/103_search_chunks.sql +90 -0
- package/migrations/104_builder_box_blocked.sql +34 -0
- package/migrations/105_box_host_keys.sql +35 -0
- package/migrations/111_medusa_customer0_framing.sql +35 -0
- package/migrations/112_drop_track_check.sql +25 -0
- package/migrations/113_merge_lease.sql +16 -0
- package/migrations/114_cloudbongos_rename_live_strings.sql +32 -0
- package/migrations/118_currency_label_decoupling.sql +23 -0
- package/migrations/119_builder_behavior_prefs.sql +31 -0
- package/migrations/121_box_code_version.sql +37 -0
- package/migrations/122_builder_gemini_key.sql +30 -0
- package/migrations/123_cost_log_tiers.sql +33 -0
- package/migrations/124_cost_baseline.sql +45 -0
- package/migrations/125_llm_cache.sql +54 -0
- package/migrations/126_autonomy_runs.sql +33 -0
- package/migrations/143_cost_calibration.sql +63 -0
- package/migrations/145_backfill_task_requires_rank.sql +84 -0
- package/migrations/160_goal_tier.sql +88 -0
- package/migrations/163_polymorphic_dependencies.sql +187 -0
- package/migrations/164_event_sound_prefs.sql +20 -0
- package/migrations/165_task_module_key.sql +15 -0
- package/migrations/171_claims_published_head_sha.sql +24 -0
- package/migrations/174_close_orphaned_claims_on_terminal_tasks.sql +30 -0
- package/migrations/175_task_grades_diff_hash.sql +9 -0
- package/migrations/176_promotion_respects_open_blockers.sql +58 -0
- package/migrations/176_ui_discipline.sql +40 -0
- package/migrations/177_require_reward_before_workable.sql +76 -0
- package/migrations/178_reward_gate_auto_assign_revert_autopromote.sql +61 -0
- package/migrations/179_search_chunk_usefulness.sql +33 -0
- package/migrations/180_one_active_claim_per_session.sql +54 -0
- package/migrations/181_cost_log_skill_name.sql +33 -0
- package/migrations/184_claim_release_requested.sql +25 -0
- package/migrations/185_module_upstreaming_goal_wiring.sql +59 -0
- package/migrations/187_goal_sort_order.sql +30 -0
- package/migrations/187_tasks_kind_check_add_verify.sql +21 -0
- package/migrations/README.md +88 -0
- package/migrations/core_188_goal_visibility_and_membership_requests.sql +71 -0
- package/migrations/core_189_core_upgrades_ledger.sql +37 -0
- package/migrations/core_189_credit_log_task_id_index.sql +19 -0
- package/migrations/core_190_rebuild_claimable_task_views.sql +71 -0
- package/migrations/instance/018_pms_v3_planning_seed.sql +131 -0
- package/migrations/instance/080_gds_v4_planning_seed.sql +75 -0
- package/migrations/instance/110_medusa_planning_seed.sql +70 -0
- package/migrations/instance/120_bongos_v1_planning_seed.sql +69 -0
- package/migrations/instance/144_bongos_v1_module_system_criterion.sql +26 -0
- package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +29 -0
- package/migrations/instance/172_v2_world_planning_seed.sql +163 -0
- package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +33 -0
- package/migrations/instance/182_gdsv4_bongos_consolidation.sql +132 -0
- package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +116 -0
- package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +69 -0
- package/modules/autonomy/CLAUDE.md +49 -0
- package/modules/autonomy/db.js +50 -0
- package/modules/autonomy/module.json +13 -0
- package/modules/autonomy/pollers/routine-timers.js +146 -0
- package/modules/autonomy/routes/autonomy.js +37 -0
- package/modules/builder-settings/CLAUDE.md +58 -0
- package/modules/builder-settings/builder-needs.js +115 -0
- package/modules/builder-settings/db.js +168 -0
- package/modules/builder-settings/display-name.js +84 -0
- package/modules/builder-settings/event-sound-prefs.js +169 -0
- package/modules/builder-settings/interaction-prefs.js +201 -0
- package/modules/builder-settings/model-alloc-prefs.js +147 -0
- package/modules/builder-settings/module.json +10 -0
- package/modules/builder-settings/render-prefs.js +103 -0
- package/modules/builder-settings/routes/settings.js +417 -0
- package/modules/builder-settings/settings.js +52 -0
- package/modules/builder-settings/skill-prefs.js +196 -0
- package/modules/builder-settings/sound-prefs.js +178 -0
- package/modules/builder-settings/wandering-prefs.js +282 -0
- package/modules/dev-box/CLAUDE.md +38 -0
- package/modules/dev-box/box-access.js +275 -0
- package/modules/dev-box/box-credential.js +94 -0
- package/modules/dev-box/box-onboard.js +486 -0
- package/modules/dev-box/boxes.js +842 -0
- package/modules/dev-box/db.js +39 -0
- package/modules/dev-box/module.json +14 -0
- package/modules/dev-box/routes/box.js +977 -0
- package/modules/discord/CLAUDE.md +50 -0
- package/modules/discord/auth.js +54 -0
- package/modules/discord/bug-attachments.js +210 -0
- package/modules/discord/db.js +96 -0
- package/modules/discord/discord-approvals.js +145 -0
- package/modules/discord/discord-audit.js +29 -0
- package/modules/discord/discord-bot.js +545 -0
- package/modules/discord/discord-bugs.js +257 -0
- package/modules/discord/discord-channels.js +412 -0
- package/modules/discord/discord-inbound.js +147 -0
- package/modules/discord/discord-message-buffer.js +54 -0
- package/modules/discord/discord-reaction.js +186 -0
- package/modules/discord/discord-role-sync.js +318 -0
- package/modules/discord/module.json +14 -0
- package/modules/discord/pollers/discord-bot.js +19 -0
- package/modules/discord/routes/bug-attachments.js +44 -0
- package/modules/discord/routes/discord.js +102 -0
- package/modules/discord/ship-broadcast.js +78 -0
- package/modules/discord/webhook.js +74 -0
- package/modules/economy/CLAUDE.md +48 -0
- package/modules/economy/achievements.js +594 -0
- package/modules/economy/cost-classify.js +331 -0
- package/modules/economy/cost-summary-shape.js +90 -0
- package/modules/economy/cost.js +438 -0
- package/modules/economy/credits.js +430 -0
- package/modules/economy/migrations/economy_001_fix_gds_shipper_description.sql +91 -0
- package/modules/economy/module.json +10 -0
- package/modules/economy/reward.js +61 -0
- package/modules/economy/routes/cost.js +97 -0
- package/modules/economy/routes/leaderboard.js +26 -0
- package/modules/economy/routes/reward.js +42 -0
- package/modules/economy/streak.js +285 -0
- package/modules/grading/CLAUDE.md +42 -0
- package/modules/grading/db.js +39 -0
- package/modules/grading/grader-rubric.json +151 -0
- package/modules/grading/grader-workers/efficiency.js +213 -0
- package/modules/grading/grader-workers/extract-json.js +80 -0
- package/modules/grading/grader-workers/fence.js +100 -0
- package/modules/grading/grader-workers/hacker.js +239 -0
- package/modules/grading/grader-workers/index.js +183 -0
- package/modules/grading/grader-workers/narc.js +207 -0
- package/modules/grading/grader-workers/quality.js +198 -0
- package/modules/grading/grader-workers/rubric.js +20 -0
- package/modules/grading/grader-workers/worker-prompt.js +89 -0
- package/modules/grading/grader-workers/worker-verdict.js +50 -0
- package/modules/grading/grader.js +1790 -0
- package/modules/grading/module.json +12 -0
- package/modules/grading/routes/grade.js +51 -0
- package/modules/hall-ui/CLAUDE.md +61 -0
- package/modules/hall-ui/module.json +14 -0
- package/modules/hall-ui/public/atlas.html +180 -0
- package/modules/hall-ui/public/atlas.js +336 -0
- package/modules/hall-ui/public/box-panel.js +423 -0
- package/modules/hall-ui/public/brand-apply.js +117 -0
- package/modules/hall-ui/public/builders.js +3028 -0
- package/modules/hall-ui/public/chime.wav +0 -0
- package/modules/hall-ui/public/copy-on-click.js +174 -0
- package/modules/hall-ui/public/cursors/arrow.png +0 -0
- package/modules/hall-ui/public/cursors/hand-pointer.png +0 -0
- package/modules/hall-ui/public/cursors/magnifier-out.png +0 -0
- package/modules/hall-ui/public/cursors/magnifier.png +0 -0
- package/modules/hall-ui/public/diagram-viewer.css +125 -0
- package/modules/hall-ui/public/diagram-viewer.js +330 -0
- package/modules/hall-ui/public/diagrams.html +113 -0
- package/modules/hall-ui/public/dom-utils.js +212 -0
- package/modules/hall-ui/public/drachmae.html +68 -0
- package/modules/hall-ui/public/drachmae.js +136 -0
- package/modules/hall-ui/public/gate.html +143 -0
- package/modules/hall-ui/public/gate.js +228 -0
- package/modules/hall-ui/public/goals-page.js +1297 -0
- package/modules/hall-ui/public/goals.html +376 -0
- package/modules/hall-ui/public/goals.js +119 -0
- package/modules/hall-ui/public/hall-widgets.js +189 -0
- package/modules/hall-ui/public/harbor.html +167 -0
- package/modules/hall-ui/public/harbor.js +335 -0
- package/modules/hall-ui/public/index.html +116 -0
- package/modules/hall-ui/public/instances-panel.js +172 -0
- package/modules/hall-ui/public/md-reader.js +234 -0
- package/modules/hall-ui/public/modules.html +119 -0
- package/modules/hall-ui/public/modules.js +154 -0
- package/modules/hall-ui/public/new-project-wizard.js +186 -0
- package/modules/hall-ui/public/not-ready.html +48 -0
- package/modules/hall-ui/public/pair.html +119 -0
- package/modules/hall-ui/public/pair.js +132 -0
- package/modules/hall-ui/public/primer.css +404 -0
- package/modules/hall-ui/public/primer.html +85 -0
- package/modules/hall-ui/public/primer.js +641 -0
- package/modules/hall-ui/public/profile.html +85 -0
- package/modules/hall-ui/public/profile.js +236 -0
- package/modules/hall-ui/public/ranks.html +69 -0
- package/modules/hall-ui/public/ranks.js +206 -0
- package/modules/hall-ui/public/roadmap.html +209 -0
- package/modules/hall-ui/public/roadmap.js +375 -0
- package/modules/hall-ui/public/sessions.html +92 -0
- package/modules/hall-ui/public/sessions.js +222 -0
- package/modules/hall-ui/public/settings.html +640 -0
- package/modules/hall-ui/public/settings.js +1219 -0
- package/modules/hall-ui/public/shell.js +495 -0
- package/modules/hall-ui/public/style.css +1748 -0
- package/modules/hall-ui/public/task-detail.js +253 -0
- package/modules/hall-ui/public/task.html +83 -0
- package/modules/hall-ui/public/task.js +326 -0
- package/modules/hall-ui/public/watch.html +333 -0
- package/modules/hall-ui/public/watch.js +859 -0
- package/modules/hall-ui/public/work.html +469 -0
- package/modules/hall-ui/public/work.js +1292 -0
- package/modules/ideas/CLAUDE.md +37 -0
- package/modules/ideas/blockers.js +100 -0
- package/modules/ideas/inbox.js +459 -0
- package/modules/ideas/module.json +10 -0
- package/modules/ideas/routes/blockers.js +120 -0
- package/modules/ideas/routes/inbox.js +230 -0
- package/modules/lifecycle/CLAUDE.md +63 -0
- package/modules/lifecycle/conflict-resolve.js +436 -0
- package/modules/lifecycle/db.js +4970 -0
- package/modules/lifecycle/done-when.js +355 -0
- package/modules/lifecycle/github-push.js +969 -0
- package/modules/lifecycle/goal-conflicts.js +133 -0
- package/modules/lifecycle/hierarchy.js +104 -0
- package/modules/lifecycle/lifecycle.js +58 -0
- package/modules/lifecycle/merge-lock.js +261 -0
- package/modules/lifecycle/migrations/lifecycle_001_goal_first_leads.sql +28 -0
- package/modules/lifecycle/migrations/lifecycle_002_backfill_orphan_task_goals.sql +36 -0
- package/modules/lifecycle/module.json +13 -0
- package/modules/lifecycle/pollers/publish-reconciler.js +20 -0
- package/modules/lifecycle/publish-reconciler.js +418 -0
- package/modules/lifecycle/routes/analytics.js +63 -0
- package/modules/lifecycle/routes/claims.js +506 -0
- package/modules/lifecycle/routes/dependencies.js +82 -0
- package/modules/lifecycle/routes/done-when.js +136 -0
- package/modules/lifecycle/routes/gate-approvals.js +197 -0
- package/modules/lifecycle/routes/goals.js +1419 -0
- package/modules/lifecycle/routes/lifecycle.js +43 -0
- package/modules/lifecycle/routes/tasks.js +1703 -0
- package/modules/lifecycle/routes/versions.js +128 -0
- package/modules/lifecycle/ship-card.js +505 -0
- package/modules/lifecycle/ship-preflight.js +225 -0
- package/modules/lifecycle/task-classifier.js +512 -0
- package/modules/lifecycle/version-close.js +106 -0
- package/modules/memory/CLAUDE.md +40 -0
- package/modules/memory/learnings.js +275 -0
- package/modules/memory/memory.js +572 -0
- package/modules/memory/module.json +12 -0
- package/modules/memory/routes/learnings.js +90 -0
- package/modules/memory/routes/memory.js +916 -0
- package/modules/memory/routes/search.js +188 -0
- package/modules/memory/search-chunker.js +252 -0
- package/modules/memory/search.js +320 -0
- package/modules/onboarding/CLAUDE.md +55 -0
- package/modules/onboarding/approval-broadcast.js +59 -0
- package/modules/onboarding/db.js +54 -0
- package/modules/onboarding/module.json +10 -0
- package/modules/onboarding/newcomer-restock.js +212 -0
- package/modules/onboarding/onboarding-state.js +532 -0
- package/modules/onboarding/onboarding.js +508 -0
- package/modules/onboarding/port.js +63 -0
- package/modules/onboarding/routes/access-requests.js +208 -0
- package/modules/onboarding/routes/onboarding.js +54 -0
- package/modules/security/CLAUDE.md +44 -0
- package/modules/security/module.json +10 -0
- package/modules/security/routes/reports.js +251 -0
- package/modules/security/secret-scrub.js +201 -0
- package/modules/security/security.js +294 -0
- package/modules/sessions/CLAUDE.md +39 -0
- package/modules/sessions/bfg.js +357 -0
- package/modules/sessions/db.js +326 -0
- package/modules/sessions/module.json +10 -0
- package/modules/sessions/pulse.js +149 -0
- package/modules/sessions/routes/sessions.js +331 -0
- package/modules/status-ui/CLAUDE.md +47 -0
- package/modules/status-ui/module.json +14 -0
- package/modules/status-ui/public/README.md +29 -0
- package/modules/status-ui/public/cursors/arrow.png +0 -0
- package/modules/status-ui/public/cursors/hand-pointer.png +0 -0
- package/modules/status-ui/public/index.html +363 -0
- package/modules/status-ui/public/snapshots/.gitkeep +0 -0
- package/modules/status-ui/public/status.js +1375 -0
- package/modules/status-ui/public/style.css +1342 -0
- package/package-lock.json +975 -0
- package/package.json +28 -0
- package/public-docs/index.html +35 -0
- package/public-docs/redoc.standalone.js +1838 -0
- package/public-docs/redoc.standalone.js.LICENSE.txt +102 -0
- package/public-landing/buddha.png +0 -0
- package/public-landing/create.html +868 -0
- package/public-landing/index.html +168 -0
- package/scripts/bongos-mcp.js +28 -0
- package/scripts/cron/style_guide_audit.sh +134 -0
- package/scripts/discord/README.md +52 -0
- package/scripts/discord/lib.js +80 -0
- package/scripts/discord/post.js +51 -0
- package/scripts/gds/CLAUDE.md +144 -0
- package/scripts/gds/abandon.js +58 -0
- package/scripts/gds/adopt-detect.js +123 -0
- package/scripts/gds/adopt-import.js +111 -0
- package/scripts/gds/adopt-preflight.js +121 -0
- package/scripts/gds/api.js +78 -0
- package/scripts/gds/architect-audit.js +288 -0
- package/scripts/gds/art-stage.js +152 -0
- package/scripts/gds/audit-authorship.sh +243 -0
- package/scripts/gds/audit-deps.js +121 -0
- package/scripts/gds/audit-exports.js +128 -0
- package/scripts/gds/audit-replay.sh +161 -0
- package/scripts/gds/audit-ships.js +251 -0
- package/scripts/gds/autonomy-gate.js +135 -0
- package/scripts/gds/backfill-cost.js +165 -0
- package/scripts/gds/backfill-sessions.js +351 -0
- package/scripts/gds/backfill-task-rewards.js +408 -0
- package/scripts/gds/backup.js +94 -0
- package/scripts/gds/bfg-principal-token.js +65 -0
- package/scripts/gds/box-auth-check.js +160 -0
- package/scripts/gds/box-connect-lib.js +453 -0
- package/scripts/gds/box.js +1258 -0
- package/scripts/gds/build-bongos.js +94 -0
- package/scripts/gds/capture.js +123 -0
- package/scripts/gds/cf-tunnel.js +419 -0
- package/scripts/gds/ci-grade.js +82 -0
- package/scripts/gds/claim-recall.js +112 -0
- package/scripts/gds/claim.js +333 -0
- package/scripts/gds/classify-discipline.js +153 -0
- package/scripts/gds/claude-materialize.js +176 -0
- package/scripts/gds/cleanup-orphans.js +74 -0
- package/scripts/gds/cli-lib.js +518 -0
- package/scripts/gds/code.js +101 -0
- package/scripts/gds/codemod-rename-src-gds.js +145 -0
- package/scripts/gds/conductor-dispatch.js +426 -0
- package/scripts/gds/context-pack.js +228 -0
- package/scripts/gds/coreB-carve-proof.sh +716 -0
- package/scripts/gds/cost.js +47 -0
- package/scripts/gds/db-backup-nightly.sh +160 -0
- package/scripts/gds/db-backup-offsite.sh +152 -0
- package/scripts/gds/demote.js +40 -0
- package/scripts/gds/design-sync.js +62 -0
- package/scripts/gds/diagnose-achievements.js +118 -0
- package/scripts/gds/diagram-facts.js +319 -0
- package/scripts/gds/discipline-modes.json +25 -0
- package/scripts/gds/discord-channels.js +66 -0
- package/scripts/gds/do-api.js +176 -0
- package/scripts/gds/docs-entropy.js +403 -0
- package/scripts/gds/doctor.js +389 -0
- package/scripts/gds/feedback-latest.js +94 -0
- package/scripts/gds/fetch-art-key.js +106 -0
- package/scripts/gds/fetch-sound-prefs.js +88 -0
- package/scripts/gds/figma-design-sync.js +61 -0
- package/scripts/gds/fitness.js +1446 -0
- package/scripts/gds/gate-review.js +322 -0
- package/scripts/gds/gating.js +325 -0
- package/scripts/gds/gds-db-backup.service +24 -0
- package/scripts/gds/gds-db-backup.timer +20 -0
- package/scripts/gds/gds-db-offsite.service +29 -0
- package/scripts/gds/gds-db-offsite.timer +18 -0
- package/scripts/gds/gen-api-client.js +508 -0
- package/scripts/gds/gen-api-docs.js +1037 -0
- package/scripts/gds/gen-atlas.js +366 -0
- package/scripts/gds/gen-diagrams.js +608 -0
- package/scripts/gds/gen-file-map.js +196 -0
- package/scripts/gds/gen-repo-map.js +519 -0
- package/scripts/gds/gen-session-index.js +210 -0
- package/scripts/gds/git-merge-regen.js +147 -0
- package/scripts/gds/grade-smoke.js +205 -0
- package/scripts/gds/hooks/pre-push.js +169 -0
- package/scripts/gds/http-api-client.js +88 -0
- package/scripts/gds/init.js +903 -0
- package/scripts/gds/install-git-hooks.js +114 -0
- package/scripts/gds/key-card.js +213 -0
- package/scripts/gds/key-status.js +126 -0
- package/scripts/gds/land-watch.js +168 -0
- package/scripts/gds/learning-capture.js +145 -0
- package/scripts/gds/lib.sh +44 -0
- package/scripts/gds/linkify-refs.js +473 -0
- package/scripts/gds/local-preview-lib.js +159 -0
- package/scripts/gds/local-preview.js +95 -0
- package/scripts/gds/login.js +225 -0
- package/scripts/gds/main-audit.js +279 -0
- package/scripts/gds/me.js +88 -0
- package/scripts/gds/memory-map.js +174 -0
- package/scripts/gds/merge-lock.js +71 -0
- package/scripts/gds/migration-namespace.js +200 -0
- package/scripts/gds/mirror-lineage-check.js +319 -0
- package/scripts/gds/mirror-redact.js +305 -0
- package/scripts/gds/module.js +482 -0
- package/scripts/gds/newcomer-floor.config.json +13 -0
- package/scripts/gds/newcomer-floor.js +415 -0
- package/scripts/gds/newcomer-floor.templates.json +86 -0
- package/scripts/gds/newcomer-floor.test.js +235 -0
- package/scripts/gds/next-number.js +163 -0
- package/scripts/gds/oauth-secret.js +197 -0
- package/scripts/gds/onboard.js +217 -0
- package/scripts/gds/onboarding-analyze.js +226 -0
- package/scripts/gds/onboarding-config.js +178 -0
- package/scripts/gds/onboarding-ship.js +92 -0
- package/scripts/gds/package-core.js +684 -0
- package/scripts/gds/paste-token.js +131 -0
- package/scripts/gds/portability-smoke-art.sh +85 -0
- package/scripts/gds/portability-smoke-memory.sh +147 -0
- package/scripts/gds/provision.js +1324 -0
- package/scripts/gds/publish-manifest.js +307 -0
- package/scripts/gds/pulse.js +309 -0
- package/scripts/gds/push-main.js +45 -0
- package/scripts/gds/read-session-export.js +359 -0
- package/scripts/gds/reauth.js +149 -0
- package/scripts/gds/recall.js +135 -0
- package/scripts/gds/release.js +71 -0
- package/scripts/gds/render-ideas.js +173 -0
- package/scripts/gds/repo-metrics.js +363 -0
- package/scripts/gds/resolve-skill-model.js +57 -0
- package/scripts/gds/run-routine.js +197 -0
- package/scripts/gds/run-unit-tests.js +178 -0
- package/scripts/gds/sandbox-stage.js +653 -0
- package/scripts/gds/search-ingest-db.js +209 -0
- package/scripts/gds/search-ingest-md.js +225 -0
- package/scripts/gds/security-baseline-check.sh +180 -0
- package/scripts/gds/seed-adopt-tasks.js +75 -0
- package/scripts/gds/seed-api-contract-goal-tasks.js +141 -0
- package/scripts/gds/seed-bongos-app-tasks.js +294 -0
- package/scripts/gds/seed-bongos-coreB-tranche1-tasks.js +81 -0
- package/scripts/gds/seed-bongos-coreB-tranche2-tasks.js +100 -0
- package/scripts/gds/seed-bongos-goal-hierarchy-tasks.js +307 -0
- package/scripts/gds/seed-bongos-module-system-tasks.js +346 -0
- package/scripts/gds/seed-bongos-tasks.js +214 -0
- package/scripts/gds/seed-gds-v4-tasks.js +624 -0
- package/scripts/gds/seed-governance-tasks.js +340 -0
- package/scripts/gds/seed-medusa-tasks.js +440 -0
- package/scripts/gds/seed-module-upstreaming.js +219 -0
- package/scripts/gds/seed-productization-tasks.js +235 -0
- package/scripts/gds/seed-provisioning-tasks.js +83 -0
- package/scripts/gds/seed-staging.sh +104 -0
- package/scripts/gds/seed-v2-world-tasks.js +283 -0
- package/scripts/gds/seed-v3-tasks.js +1981 -0
- package/scripts/gds/session-cost.js +393 -0
- package/scripts/gds/session-digest.js +179 -0
- package/scripts/gds/session-eval.js +576 -0
- package/scripts/gds/session.js +404 -0
- package/scripts/gds/setup.js +504 -0
- package/scripts/gds/shell.js +100 -0
- package/scripts/gds/ship.js +3618 -0
- package/scripts/gds/smoke-batch-claim.sh +213 -0
- package/scripts/gds/smoke-batch-release.sh +296 -0
- package/scripts/gds/smoke-box.sh +150 -0
- package/scripts/gds/smoke-claim-consistency.sh +212 -0
- package/scripts/gds/smoke-dependencies.sh +84 -0
- package/scripts/gds/smoke-gds.js +125 -0
- package/scripts/gds/smoke-write.sh +168 -0
- package/scripts/gds/start.js +782 -0
- package/scripts/gds/status.js +365 -0
- package/scripts/gds/triage.js +366 -0
- package/scripts/gds/upgrade.js +421 -0
- package/scripts/gds/validate-design.js +254 -0
- package/scripts/gds/version-close.js +549 -0
- package/scripts/migrate.sh +183 -0
- package/scripts/migrate_cost_ledger.sh +178 -0
- package/scripts/public-mirror-export.js +291 -0
- package/scripts/render-diagrams.sh +88 -0
- package/scripts/security/dep-audit.js +276 -0
- package/scripts/setup-builder.js +274 -0
- package/scripts/setup-builder.sh +28 -0
- package/scripts/snapshots/backfill-history.mjs +292 -0
- package/scripts/snapshots/maybe-render-on-deploy.sh +66 -0
- package/scripts/snapshots/render-pre-art-eras.mjs +329 -0
- package/scripts/snapshots/render-world.mjs +322 -0
- package/scripts/status-mirror-sync.js +192 -0
- package/scripts/status-probe.mjs +185 -0
- package/server.js +212 -0
- package/src/asset-version.js +71 -0
- package/src/bongos/CLAUDE.md +64 -0
- package/src/bongos/api-errors.js +281 -0
- package/src/bongos/api-path-404.js +67 -0
- package/src/bongos/api-prefix.js +38 -0
- package/src/bongos/app-pair.js +172 -0
- package/src/bongos/auth.js +979 -0
- package/src/bongos/cascade-dispatch.js +123 -0
- package/src/bongos/db-kernel.js +252 -0
- package/src/bongos/db.js +911 -0
- package/src/bongos/goal-scope-check.js +120 -0
- package/src/bongos/hierarchy-config.js +189 -0
- package/src/bongos/live-channel.js +145 -0
- package/src/bongos/llm-cache.js +260 -0
- package/src/bongos/llm-pricing.js +220 -0
- package/src/bongos/metrics.js +286 -0
- package/src/bongos/middleware/audit.js +274 -0
- package/src/bongos/middleware/error-envelope.js +93 -0
- package/src/bongos/middleware/rate-limit.js +195 -0
- package/src/bongos/module-scope-map.js +271 -0
- package/src/bongos/optimizer.js +166 -0
- package/src/bongos/path-match.js +47 -0
- package/src/bongos/permission-path-check.js +248 -0
- package/src/bongos/pool.js +55 -0
- package/src/bongos/repo-info.js +91 -0
- package/src/bongos/route-rank-check.js +420 -0
- package/src/bongos/routes/CLAUDE.md +43 -0
- package/src/bongos/routes/_helpers.js +369 -0
- package/src/bongos/routes/audit-log.js +59 -0
- package/src/bongos/routes/auth.js +755 -0
- package/src/bongos/routes/backup.js +104 -0
- package/src/bongos/routes/builders.js +526 -0
- package/src/bongos/routes/healthz.js +16 -0
- package/src/bongos/routes/instance.js +129 -0
- package/src/bongos/routes/live.js +125 -0
- package/src/bongos/routes/llm-cache.js +119 -0
- package/src/bongos/routes/me.js +380 -0
- package/src/bongos/routes/modules.js +131 -0
- package/src/bongos/routes/optimizer.js +54 -0
- package/src/bongos/routes/public.js +476 -0
- package/src/bongos/routes/security.js +207 -0
- package/src/bongos/routes.js +166 -0
- package/src/bongos/secret-box.js +177 -0
- package/src/bongos/sensitive-surfaces.js +30 -0
- package/src/bongos/serve-internal.js +889 -0
- package/src/bongos/uptime-poller.js +90 -0
- package/src/bongos-downloads.js +70 -0
- package/src/branding.js +182 -0
- package/src/build-info.js +77 -0
- package/src/devbox-downloads.js +67 -0
- package/src/instance-config.js +200 -0
- package/src/module-api.js +366 -0
- package/src/module-loader/loader.js +268 -0
- package/src/module-loader/manifest-schema.js +140 -0
- package/src/module-loader/provenance.js +28 -0
- package/src/module-loader/semver.js +100 -0
- package/src/module-seams.js +291 -0
- package/src/modules.js +254 -0
- package/src/platform-server.js +135 -0
- package/src/ui/adapters/claude-design/index.js +333 -0
- package/src/ui/adapters/figma/index.js +328 -0
- package/tests/abandon_task.mjs +138 -0
- package/tests/achievements_reconcile.mjs +167 -0
- package/tests/adopt_detect.mjs +141 -0
- package/tests/adopt_import.mjs +132 -0
- package/tests/adopt_preflight.mjs +105 -0
- package/tests/anti_grid.mjs +89 -0
- package/tests/api_client.mjs +231 -0
- package/tests/api_docs.mjs +283 -0
- package/tests/api_error_envelope_buildcheck.mjs +56 -0
- package/tests/api_error_envelope_middleware.mjs +99 -0
- package/tests/api_error_registry.mjs +108 -0
- package/tests/api_pagination_contract.mjs +51 -0
- package/tests/api_path_404.mjs +96 -0
- package/tests/app_pair.mjs +168 -0
- package/tests/architect.mjs +140 -0
- package/tests/art_key.mjs +121 -0
- package/tests/art_stage.mjs +121 -0
- package/tests/asset_version.mjs +94 -0
- package/tests/atlas.mjs +69 -0
- package/tests/audit_deps.mjs +74 -0
- package/tests/audit_middleware.mjs +385 -0
- package/tests/auth_admission_gate.mjs +164 -0
- package/tests/auth_anthropic_identity.mjs +95 -0
- package/tests/auth_duplicate_cookie.mjs +56 -0
- package/tests/auth_error_brand.mjs +77 -0
- package/tests/auth_flow_slice1.mjs +223 -0
- package/tests/auth_inactive_builder.mjs +94 -0
- package/tests/auth_page_gate.mjs +92 -0
- package/tests/auth_session_ttl.mjs +155 -0
- package/tests/autonomous_fit_protected_paths.mjs +115 -0
- package/tests/autonomy_gate.mjs +85 -0
- package/tests/autotile_variants.mjs +256 -0
- package/tests/autotile_xverify.mjs +133 -0
- package/tests/backfill_sessions.mjs +101 -0
- package/tests/backfill_task_rewards.mjs +152 -0
- package/tests/backup_cli.mjs +104 -0
- package/tests/bfg.mjs +327 -0
- package/tests/bfg_write.mjs +423 -0
- package/tests/bongos_box_connect.mjs +89 -0
- package/tests/bongos_dispatch.mjs +74 -0
- package/tests/bongos_downloads.mjs +118 -0
- package/tests/bongos_login.mjs +34 -0
- package/tests/box_access.mjs +186 -0
- package/tests/box_auth_check.mjs +114 -0
- package/tests/box_code_staleness.mjs +116 -0
- package/tests/box_connect_e2e.mjs +196 -0
- package/tests/box_cost.mjs +70 -0
- package/tests/box_dns_repoint.mjs +151 -0
- package/tests/box_host_keys.mjs +102 -0
- package/tests/box_onboard.mjs +317 -0
- package/tests/box_scope_session.mjs +115 -0
- package/tests/box_ship_permissions.mjs +71 -0
- package/tests/box_terminal_ssrf.mjs +90 -0
- package/tests/boxes.mjs +1255 -0
- package/tests/branding.mjs +88 -0
- package/tests/bug_attachments.mjs +189 -0
- package/tests/build_info.mjs +88 -0
- package/tests/builder_needs.mjs +99 -0
- package/tests/cap_newcomer_tasks.mjs +114 -0
- package/tests/cascade-dispatch.mjs +95 -0
- package/tests/cf_tunnel.mjs +459 -0
- package/tests/claim_recall.mjs +150 -0
- package/tests/claims_bind_session.mjs +47 -0
- package/tests/claude_materialize.mjs +148 -0
- package/tests/cli_box_scope.mjs +52 -0
- package/tests/cli_lib_formatters.mjs +87 -0
- package/tests/cli_token_reissue.mjs +147 -0
- package/tests/client_bundle_route.mjs +47 -0
- package/tests/cloud_bongos_pack.mjs +85 -0
- package/tests/conductor.mjs +496 -0
- package/tests/confirm_attribution.mjs +156 -0
- package/tests/conflict_resolve.mjs +462 -0
- package/tests/context_pack.mjs +240 -0
- package/tests/cost.mjs +94 -0
- package/tests/cost_audit.mjs +122 -0
- package/tests/cost_budget_status.mjs +78 -0
- package/tests/cost_reconcile.mjs +102 -0
- package/tests/cost_summary_shape.mjs +94 -0
- package/tests/credit_grant.mjs +376 -0
- package/tests/criterion_progress.mjs +220 -0
- package/tests/criterion_proposal.mjs +109 -0
- package/tests/criterion_ref_parse.mjs +53 -0
- package/tests/currency_label.mjs +119 -0
- package/tests/day_cycle.mjs +197 -0
- package/tests/dependencies_migration.mjs +77 -0
- package/tests/dependencies_polymorphic.mjs +212 -0
- package/tests/deploy_instance.mjs +78 -0
- package/tests/devbox_downloads.mjs +122 -0
- package/tests/diff_derived_signals.mjs +115 -0
- package/tests/discord_approvals.mjs +174 -0
- package/tests/discord_autojoin.mjs +158 -0
- package/tests/discord_bot.mjs +94 -0
- package/tests/discord_bugs.mjs +297 -0
- package/tests/discord_channels.mjs +337 -0
- package/tests/discord_foundation.mjs +76 -0
- package/tests/discord_inbound.mjs +190 -0
- package/tests/discord_message_buffer.mjs +87 -0
- package/tests/discord_oauth.mjs +55 -0
- package/tests/discord_reaction.mjs +170 -0
- package/tests/discord_role_sync.mjs +151 -0
- package/tests/display_name.mjs +123 -0
- package/tests/dns_preflight.mjs +74 -0
- package/tests/event_sound_prefs.mjs +194 -0
- package/tests/eviction_policy.mjs +279 -0
- package/tests/figma_adapter.mjs +199 -0
- package/tests/file_map.mjs +91 -0
- package/tests/first_admin_bootstrap.mjs +40 -0
- package/tests/fitness.mjs +435 -0
- package/tests/fitness_two_root.mjs +104 -0
- package/tests/fixtures/session-506-digest.json +34 -0
- package/tests/gate_approvals.mjs +451 -0
- package/tests/gate_review.mjs +152 -0
- package/tests/git_merge_regen.mjs +151 -0
- package/tests/github_push_ancestry.mjs +119 -0
- package/tests/github_push_land_proof.mjs +127 -0
- package/tests/github_push_merge_driver.mjs +175 -0
- package/tests/goal_abuse_matrix.mjs +187 -0
- package/tests/goal_achievement.mjs +146 -0
- package/tests/goal_conflicts.mjs +305 -0
- package/tests/goal_dependencies_read.mjs +73 -0
- package/tests/goal_member_roles.mjs +420 -0
- package/tests/goal_membership_requests.mjs +332 -0
- package/tests/goal_routes.mjs +203 -0
- package/tests/goal_scope_check.mjs +167 -0
- package/tests/goal_tier.mjs +312 -0
- package/tests/goal_wall_proof.mjs +112 -0
- package/tests/grade_attempts.mjs +73 -0
- package/tests/grade_attribution.mjs +117 -0
- package/tests/grade_diff_hash.mjs +191 -0
- package/tests/grade_server_authoritative.mjs +305 -0
- package/tests/grade_unavailable_display.mjs +66 -0
- package/tests/grader.mjs +1960 -0
- package/tests/grader_bypass.mjs +57 -0
- package/tests/grader_panel.mjs +252 -0
- package/tests/grader_prompt_fencing.mjs +192 -0
- package/tests/helpers.mjs +52 -0
- package/tests/hierarchy-config.mjs +81 -0
- package/tests/init.mjs +463 -0
- package/tests/instance_config.mjs +76 -0
- package/tests/instance_manifest.mjs +110 -0
- package/tests/interaction_prefs.mjs +72 -0
- package/tests/key-card.mjs +110 -0
- package/tests/key-status.mjs +45 -0
- package/tests/land_watch.mjs +115 -0
- package/tests/lifecycle-card.mjs +145 -0
- package/tests/lifecycle_breakdown_view.mjs +236 -0
- package/tests/linkify_refs.mjs +264 -0
- package/tests/live_channel.mjs +128 -0
- package/tests/live_sse.mjs +122 -0
- package/tests/llm-cache-route.mjs +110 -0
- package/tests/llm-cache.mjs +140 -0
- package/tests/llm-pricing.mjs +152 -0
- package/tests/local_preview.mjs +136 -0
- package/tests/main_audit.mjs +137 -0
- package/tests/memory_isolation.mjs +431 -0
- package/tests/memory_map.mjs +107 -0
- package/tests/memory_pull.mjs +429 -0
- package/tests/merge_lease.mjs +100 -0
- package/tests/merge_lock.mjs +312 -0
- package/tests/metrics.mjs +147 -0
- package/tests/migration_allocation.mjs +146 -0
- package/tests/migration_namespace.mjs +185 -0
- package/tests/migrations_fresh_replay_guard.mjs +38 -0
- package/tests/mirror_lineage_fulltree.mjs +45 -0
- package/tests/mirror_lineage_proof.mjs +72 -0
- package/tests/mirror_redact.mjs +193 -0
- package/tests/model_alloc_prefs.mjs +63 -0
- package/tests/module-scope-map.mjs +88 -0
- package/tests/module_api.mjs +110 -0
- package/tests/module_cli.mjs +292 -0
- package/tests/module_contributions.mjs +148 -0
- package/tests/module_loader.mjs +286 -0
- package/tests/module_manifest.mjs +194 -0
- package/tests/module_owned_tests.mjs +105 -0
- package/tests/module_provenance.mjs +34 -0
- package/tests/module_route_rank.mjs +147 -0
- package/tests/module_seams.mjs +68 -0
- package/tests/modules.mjs +156 -0
- package/tests/modules_route.mjs +38 -0
- package/tests/newcomer_floor.mjs +17 -0
- package/tests/newcomer_restock.mjs +80 -0
- package/tests/next_number.mjs +154 -0
- package/tests/oauth_secret.mjs +60 -0
- package/tests/onboard.mjs +70 -0
- package/tests/onboarding_gate.mjs +267 -0
- package/tests/onboarding_observability.mjs +344 -0
- package/tests/onboarding_route_signals.mjs +175 -0
- package/tests/onboarding_state.mjs +346 -0
- package/tests/one_active_claim_per_session.mjs +111 -0
- package/tests/orphaned_claim_ship.mjs +135 -0
- package/tests/override_attribution.mjs +142 -0
- package/tests/override_request_ownership.mjs +235 -0
- package/tests/package_core.mjs +435 -0
- package/tests/parallel-load.mjs +402 -0
- package/tests/path_match.mjs +62 -0
- package/tests/permission_path.mjs +151 -0
- package/tests/platform_boot.mjs +167 -0
- package/tests/pre_compact.mjs +60 -0
- package/tests/preview_server.mjs +127 -0
- package/tests/profile_route.mjs +197 -0
- package/tests/provision.mjs +805 -0
- package/tests/provisioning_github_app_routes.mjs +98 -0
- package/tests/public_mirror_export.mjs +124 -0
- package/tests/public_task_resolver.mjs +112 -0
- package/tests/publish_branch_route.mjs +422 -0
- package/tests/publish_manifest.mjs +184 -0
- package/tests/publish_reconciler.mjs +728 -0
- package/tests/rank_gate.mjs +463 -0
- package/tests/rate_limit_public.mjs +217 -0
- package/tests/read_session_export.mjs +89 -0
- package/tests/recall_cli.mjs +82 -0
- package/tests/recall_hints.mjs +119 -0
- package/tests/recent_shipped_window.mjs +80 -0
- package/tests/regrade_eligibility.mjs +202 -0
- package/tests/render_prefs.mjs +122 -0
- package/tests/repo_map.mjs +92 -0
- package/tests/reward_gate.mjs +284 -0
- package/tests/route_rank_check.mjs +243 -0
- package/tests/route_validation.mjs +490 -0
- package/tests/routine_timers.mjs +72 -0
- package/tests/rubric_fixtures.mjs +140 -0
- package/tests/run_routine.mjs +69 -0
- package/tests/sandbox_stage.mjs +582 -0
- package/tests/scrubber_corpus.mjs +257 -0
- package/tests/search_chunker.mjs +209 -0
- package/tests/search_ingest.mjs +96 -0
- package/tests/search_ingest_db.mjs +88 -0
- package/tests/search_isolation.mjs +204 -0
- package/tests/search_route.mjs +95 -0
- package/tests/secret_box.mjs +189 -0
- package/tests/security_docs.mjs +186 -0
- package/tests/security_route.mjs +108 -0
- package/tests/served_paths_shipped.mjs +118 -0
- package/tests/session-cards.mjs +142 -0
- package/tests/session_506_regression.mjs +69 -0
- package/tests/session_caps.mjs +238 -0
- package/tests/session_cost.mjs +133 -0
- package/tests/session_digest.mjs +73 -0
- package/tests/session_eval.mjs +386 -0
- package/tests/session_index.mjs +105 -0
- package/tests/session_records.mjs +369 -0
- package/tests/session_search.mjs +78 -0
- package/tests/session_start_freshness.mjs +123 -0
- package/tests/setup_smoke.mjs +154 -0
- package/tests/ship_autolinkify.mjs +60 -0
- package/tests/ship_automerge_injection.mjs +174 -0
- package/tests/ship_broadcast_routing.mjs +125 -0
- package/tests/ship_broadcast_server.mjs +175 -0
- package/tests/ship_card.mjs +280 -0
- package/tests/ship_ci_deploy.mjs +374 -0
- package/tests/ship_preflight.mjs +368 -0
- package/tests/ship_premerge.mjs +75 -0
- package/tests/ship_resume.mjs +129 -0
- package/tests/ship_upload_skip.mjs +89 -0
- package/tests/ship_zero_commit.mjs +60 -0
- package/tests/single_host_hall.mjs +98 -0
- package/tests/skill_prefs.mjs +150 -0
- package/tests/smoke_preflight.mjs +66 -0
- package/tests/snapshot_picker.mjs +233 -0
- package/tests/sound_prefs.mjs +198 -0
- package/tests/sprite_facing.mjs +35 -0
- package/tests/status-probe.mjs +89 -0
- package/tests/status_backfill.mjs +94 -0
- package/tests/stop_refcheck.mjs +127 -0
- package/tests/streak.mjs +175 -0
- package/tests/subdomain_auth.mjs +58 -0
- package/tests/task_classifier_vocab.mjs +41 -0
- package/tests/task_create_idempotency.mjs +96 -0
- package/tests/task_goal_link.mjs +93 -0
- package/tests/task_ownership_gate.mjs +125 -0
- package/tests/terrain_picker.mjs +298 -0
- package/tests/touches_overlap.mjs +75 -0
- package/tests/triage_development.mjs +140 -0
- package/tests/turn_in_place.mjs +280 -0
- package/tests/upgrade.mjs +357 -0
- package/tests/verify_task_guard.mjs +64 -0
- package/tests/version_close.mjs +76 -0
- package/tests/versions_route.mjs +93 -0
- package/tests/walkability.mjs +188 -0
- package/tests/wandering_prefs.mjs +176 -0
- package/tests/worktree_disambiguation.mjs +105 -0
- package/tests/worktree_guard.mjs +90 -0
|
@@ -0,0 +1,1297 @@
|
|
|
1
|
+
// goals-page.js — the dedicated Goals page (task 1734, owner request from the
|
|
2
|
+
// task-1723 review): list every goal, then per-goal detail — completion card,
|
|
3
|
+
// members with lead (manager) roles, work to do vs completed, and a conflicts
|
|
4
|
+
// panel. Hash-routed (#/goal/N) inside one page; the server gate is
|
|
5
|
+
// SIGNEDIN_BUILDERS_PAGE_RE in serve-internal.js.
|
|
6
|
+
//
|
|
7
|
+
// task 1888 — compact detail layout: the conflicts panel lives in the top band
|
|
8
|
+
// (Completion | Members | Conflicts) so a long work list can't bury it, and the
|
|
9
|
+
// two task tables share one Work card behind a To do / Completed tab toggle,
|
|
10
|
+
// each capped at ROW_CAP rows with a show-all pager.
|
|
11
|
+
//
|
|
12
|
+
// Reads: GET /goals, GET /goals/:id (members carry builder identity since task
|
|
13
|
+
// 1734's listGoalMembers enrichment), GET /tasks?goal_id=N, GET /goals/:id/conflicts
|
|
14
|
+
// (feature-detected — the server-side detector is task 1736; until it lands the
|
|
15
|
+
// panel computes a client-side exact-path overlap of declared touches[]),
|
|
16
|
+
// GET /builders/directory (task 1877 — every active builder, for the invite
|
|
17
|
+
// dropdown; supersedes task 1762's GET /leaderboard lookup, which was bounded
|
|
18
|
+
// to the top-100-by-credits window).
|
|
19
|
+
// Writes: POST /goals/:id/join | /leave | /members (task 1762 — owner/manager
|
|
20
|
+
// invite, mirrors the server's authorizeGoalManagement gate; task 1877 picks
|
|
21
|
+
// the invitee from a dropdown instead of typing a GitHub login). Lead role
|
|
22
|
+
// management is task 1735.
|
|
23
|
+
(function () {
|
|
24
|
+
'use strict';
|
|
25
|
+
const { escapeHtml, $, fmtNum, fmtDate, toast, rankLabel } = window.OTB;
|
|
26
|
+
const API = '/api/bongos';
|
|
27
|
+
// Generated typed client (task 1996 / R09), served as window.BongosClient by
|
|
28
|
+
// /lib/bongos-client.js. baseUrl:'' passes the full same-origin paths this file
|
|
29
|
+
// builds through unchanged; result-mode ({ status, ok, data }) preserves control flow 1:1.
|
|
30
|
+
const api = window.BongosClient.createClient({ baseUrl: '', credentials: 'same-origin', throwOnError: false });
|
|
31
|
+
|
|
32
|
+
const DONE_STATUSES = new Set(['completed', 'confirmed', 'shipped']);
|
|
33
|
+
// task 1888 — the Work tables and the conflicts list render capped; the pager
|
|
34
|
+
// under each expands to the full set.
|
|
35
|
+
const CONFLICT_CAP = 3;
|
|
36
|
+
// task 1828 — the band's Completion / Members / Conflicts columns each cap
|
|
37
|
+
// their list and offer a "View all →" into a dedicated full view (these grow:
|
|
38
|
+
// more members, more criteria). Conflicts uses CONFLICT_CAP above.
|
|
39
|
+
const MEMBER_CAP = 5;
|
|
40
|
+
const CRITERIA_CAP = 4;
|
|
41
|
+
// task 1932 — the Goals LIST (goal-card grid) is paginated with a "Per page"
|
|
42
|
+
// dropdown. Options + the localStorage key that persists the choice; below
|
|
43
|
+
// LIST_MIN_PER_PAGE the whole grid fits one page so the bar hides.
|
|
44
|
+
const LIST_PER_PAGE_OPTIONS = [10, 25, 50, 100];
|
|
45
|
+
const LIST_MIN_PER_PAGE = LIST_PER_PAGE_OPTIONS[0];
|
|
46
|
+
const GOALS_PER_PAGE_KEY = 'otb.goals.perPage';
|
|
47
|
+
function loadGoalsPerPage() {
|
|
48
|
+
try {
|
|
49
|
+
const v = Number(window.localStorage.getItem(GOALS_PER_PAGE_KEY));
|
|
50
|
+
return LIST_PER_PAGE_OPTIONS.includes(v) ? v : 25;
|
|
51
|
+
} catch (_) { return 25; }
|
|
52
|
+
}
|
|
53
|
+
// task 1956 — the goal-detail Work tables (To do / Completed) paginate like the
|
|
54
|
+
// grid; each reads/persists its own per-page key.
|
|
55
|
+
const WORK_PER_PAGE_KEY = { todo: 'otb.goals.todoPerPage', done: 'otb.goals.donePerPage' };
|
|
56
|
+
function loadPerPageFor(key) {
|
|
57
|
+
try {
|
|
58
|
+
const v = Number(window.localStorage.getItem(key));
|
|
59
|
+
return LIST_PER_PAGE_OPTIONS.includes(v) ? v : 25;
|
|
60
|
+
} catch (_) { return 25; }
|
|
61
|
+
}
|
|
62
|
+
// Terminal "deliberately cut" states — neither work-to-do nor done. Excluded
|
|
63
|
+
// from the todo list and the conflict scan the same way migration 017 keeps
|
|
64
|
+
// them out of version_progress ("abandoned is a terminal state (deliberately
|
|
65
|
+
// cut), not pending work"). Without this, an abandoned task shows up under a
|
|
66
|
+
// goal's "Work to do" as if it were still queued.
|
|
67
|
+
const CUT_STATUSES = new Set(['abandoned', 'dropped']);
|
|
68
|
+
// Genuinely-open work under a goal: not done, not cut → {backlog, ready,
|
|
69
|
+
// active, blocked}.
|
|
70
|
+
const isTodoStatus = (s) => !DONE_STATUSES.has(s) && !CUT_STATUSES.has(s);
|
|
71
|
+
// Record deep-links (#/task|idea|blocker/N) are owned by the shared overlay
|
|
72
|
+
// (task-detail.js). This page must NOT treat them as a route change (Bug 2).
|
|
73
|
+
const DEEPLINK_RE = /^#\/(?:task|idea|blocker)\/\d+$/;
|
|
74
|
+
const state = {
|
|
75
|
+
me: null,
|
|
76
|
+
goals: [], // list rows
|
|
77
|
+
details: new Map(), // goal_id -> {goal, members, criteria}
|
|
78
|
+
current: null, // open goal id (detail/conflicts view) or null (list)
|
|
79
|
+
view: 'list', // task 1828 — 'list' | 'detail' | 'conflicts'
|
|
80
|
+
inviteOpen: false, // task 1762 — the invite form's open/closed state
|
|
81
|
+
directory: null, // task 1877 — GET /builders/directory, fetched once and cached
|
|
82
|
+
workTab: 'todo', // task 1888 — active pane in the Work card ('todo' | 'done')
|
|
83
|
+
// task 1828 — per-table column sort. by=null → natural order (todo: server
|
|
84
|
+
// order; done: shipped-desc, applied at load). dir: 'desc' | 'asc'.
|
|
85
|
+
workSort: { todo: { by: null, dir: 'desc' }, done: { by: null, dir: 'desc' } },
|
|
86
|
+
work: { todo: [], done: [] }, // task 1888 — the open goal's tasks, split once on load
|
|
87
|
+
workLoaded: false, // task 1828 — tasks fetched? (gates member stats)
|
|
88
|
+
claimsByTask: new Map(), // task 1828 — String(task_id) → {claim_id, builder_id, login} (active claims; powers the Release button)
|
|
89
|
+
conflictPairs: null, // task 1888 — unified pairs (server or client estimate)
|
|
90
|
+
conflictSource: null, // 'server' | 'client' | null (while loading / on error)
|
|
91
|
+
listPager: { page: 1, perPage: loadGoalsPerPage() }, // task 1932 — goal-grid pagination
|
|
92
|
+
// task 1956 — goal-detail Work-table pagination (per table: To do / Completed).
|
|
93
|
+
workPager: {
|
|
94
|
+
todo: { page: 1, perPage: loadPerPageFor(WORK_PER_PAGE_KEY.todo) },
|
|
95
|
+
done: { page: 1, perPage: loadPerPageFor(WORK_PER_PAGE_KEY.done) },
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
async function getJSON(path) {
|
|
100
|
+
const res = await api.request('GET', path);
|
|
101
|
+
if (res.status === 401) {
|
|
102
|
+
window.location.assign(`${API}/auth/web/start?return=${encodeURIComponent(window.location.pathname + window.location.hash)}`);
|
|
103
|
+
throw new Error('redirecting to auth');
|
|
104
|
+
}
|
|
105
|
+
const data = res.data || {};
|
|
106
|
+
if (!res.ok) { const e = new Error(`${path} → ${res.status}`); e.status = res.status; e.data = data; throw e; }
|
|
107
|
+
return data;
|
|
108
|
+
}
|
|
109
|
+
async function postJSON(path, body) {
|
|
110
|
+
const res = await api.request('POST', path, { body: body || {} });
|
|
111
|
+
const data = res.data || {};
|
|
112
|
+
if (!res.ok) { const e = new Error(`${path} → ${res.status}`); e.status = res.status; e.data = data; throw e; }
|
|
113
|
+
return data;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const curLabel = () => window.OTBBranding?.currencyLabel?.() ?? 'credits';
|
|
117
|
+
// task 1957 — a task's drachmae is an ESTIMATE until it SETTLES (confirmed/
|
|
118
|
+
// shipped); after that the ledger figure (credits_awarded) is the real payout.
|
|
119
|
+
// The goal's Done tab groups 'completed' (unsettled, credits not yet landed)
|
|
120
|
+
// alongside confirmed/shipped, so per-row settle-awareness is required. Local
|
|
121
|
+
// wrappers with a defensive fallback for the (hall-wide) brand-apply helpers.
|
|
122
|
+
const taskSettled = (t) => window.OTBBranding?.taskSettled?.(t) ?? (!!t && (t.status === 'confirmed' || t.status === 'shipped'));
|
|
123
|
+
const creditVal = (t) => Number(window.OTBBranding?.taskCreditValue?.(t) ?? t?.credits_reward) || 0;
|
|
124
|
+
const selfId = () => state.me?.builder?.id ?? null;
|
|
125
|
+
const sameId = (a, b) => a != null && b != null && String(a) === String(b);
|
|
126
|
+
|
|
127
|
+
function statusBadge(status) {
|
|
128
|
+
const cls = status === 'achieved' ? 'goal-badge--achieved'
|
|
129
|
+
: (status === 'archived' ? 'goal-badge--archived' : 'goal-badge--open');
|
|
130
|
+
return `<span class="goal-badge ${cls}">${escapeHtml(status)}</span>`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function ring(satisfied, total, achieved, size = 42) {
|
|
134
|
+
const pct = total > 0 ? satisfied / total : (achieved ? 1 : 0);
|
|
135
|
+
const r = 16;
|
|
136
|
+
const circ = 2 * Math.PI * r;
|
|
137
|
+
const dash = (pct * circ).toFixed(1);
|
|
138
|
+
const gap = (circ - pct * circ).toFixed(1);
|
|
139
|
+
const stroke = achieved ? 'var(--ok)' : 'var(--accent)';
|
|
140
|
+
return `<svg class="goal-ring" width="${size}" height="${size}" viewBox="0 0 42 42" role="img" aria-label="${satisfied} of ${total} criteria satisfied">
|
|
141
|
+
<circle cx="21" cy="21" r="${r}" fill="none" stroke="var(--rule-soft)" stroke-width="4"></circle>
|
|
142
|
+
<circle cx="21" cy="21" r="${r}" fill="none" stroke="${stroke}" stroke-width="4" stroke-linecap="round" stroke-dasharray="${dash} ${gap}" transform="rotate(-90 21 21)"></circle>
|
|
143
|
+
<text x="21" y="25" text-anchor="middle" class="goal-ring__label">${satisfied}/${total}</text>
|
|
144
|
+
</svg>`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function memberName(m) {
|
|
148
|
+
return m.display_name || (m.github_login ? `@${m.github_login}` : `builder #${m.builder_id}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/* ---------------- list view ---------------- */
|
|
152
|
+
|
|
153
|
+
function populateVersionFilter() {
|
|
154
|
+
const sel = $('#f-goal-version');
|
|
155
|
+
if (!sel) return;
|
|
156
|
+
const seen = new Set();
|
|
157
|
+
for (const g of state.goals) {
|
|
158
|
+
if (g.version_id && !seen.has(g.version_id)) {
|
|
159
|
+
seen.add(g.version_id);
|
|
160
|
+
const opt = document.createElement('option');
|
|
161
|
+
opt.value = g.version_id;
|
|
162
|
+
opt.textContent = g.version_id;
|
|
163
|
+
sel.appendChild(opt);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// task 1760 — priority is a WITHIN-version ordering, so the drag affordance
|
|
169
|
+
// only makes sense once the grid is narrowed to exactly one version (the
|
|
170
|
+
// "any" filter mixes versions and dragging across them would be meaningless).
|
|
171
|
+
// Priority is always visible in that state; only an Archon gets the handle —
|
|
172
|
+
// the server-side rank gate on POST /goals/reorder is the real enforcement.
|
|
173
|
+
const isArchon = () => (state.me?.builder?.rank || '').toLowerCase() === 'archon';
|
|
174
|
+
|
|
175
|
+
// task 1932 — the current filtered goal set, in display order. Shared by
|
|
176
|
+
// renderList (what to page) and the drag-reorder dragend (which must submit
|
|
177
|
+
// the FULL version order, not just the visible page).
|
|
178
|
+
function filteredGoals() {
|
|
179
|
+
const ver = $('#f-goal-version')?.value || '';
|
|
180
|
+
const status = $('#f-goal-status')?.value || '';
|
|
181
|
+
const mineOnly = $('#f-goal-mine')?.value === 'mine';
|
|
182
|
+
const myId = selfId();
|
|
183
|
+
const iAmMember = (g) => {
|
|
184
|
+
const d = state.details.get(String(g.id));
|
|
185
|
+
return d ? (d.members || []).some((m) => sameId(m.builder_id, myId)) : false;
|
|
186
|
+
};
|
|
187
|
+
return state.goals.filter((g) =>
|
|
188
|
+
(!ver || g.version_id === ver) &&
|
|
189
|
+
(!status || g.status === status) &&
|
|
190
|
+
(!mineOnly || iAmMember(g)));
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// task 1932 — the goal grid's pagination bar (range, Per-page <select>,
|
|
194
|
+
// Prev/Next around "Page X of Y"). Hidden when the whole set fits one page
|
|
195
|
+
// (total <= LIST_MIN_PER_PAGE); otherwise shown so the per-page control stays
|
|
196
|
+
// reachable. Pure render from the slice math renderList computed.
|
|
197
|
+
function renderGoalsPager(total, page, totalPages, start, shownCount) {
|
|
198
|
+
const pager = $('#goals-pager');
|
|
199
|
+
if (!pager) return;
|
|
200
|
+
if (total <= LIST_MIN_PER_PAGE) { pager.hidden = true; return; }
|
|
201
|
+
pager.hidden = false;
|
|
202
|
+
const range = $('#goals-range');
|
|
203
|
+
if (range) range.textContent = total
|
|
204
|
+
? `Showing ${start + 1}–${start + shownCount} of ${total}`
|
|
205
|
+
: 'Showing 0 of 0';
|
|
206
|
+
const ind = $('#goals-page-ind');
|
|
207
|
+
if (ind) ind.textContent = `Page ${page} of ${totalPages}`;
|
|
208
|
+
const prev = $('#goals-prev');
|
|
209
|
+
if (prev) prev.disabled = page <= 1;
|
|
210
|
+
const next = $('#goals-next');
|
|
211
|
+
if (next) next.disabled = page >= totalPages;
|
|
212
|
+
const sel = $('#goals-per-page');
|
|
213
|
+
if (sel && sel.value !== String(state.listPager.perPage)) sel.value = String(state.listPager.perPage);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function renderList() {
|
|
217
|
+
const grid = $('#goal-grid');
|
|
218
|
+
if (!grid) return;
|
|
219
|
+
const ver = $('#f-goal-version')?.value || '';
|
|
220
|
+
// task 1932 — filter via the shared helper (drag-reorder reuses it).
|
|
221
|
+
const rows = filteredGoals();
|
|
222
|
+
const reorderable = !!ver && isArchon();
|
|
223
|
+
|
|
224
|
+
const count = $('#goals-count');
|
|
225
|
+
if (count) count.textContent = `${rows.length} goal${rows.length === 1 ? '' : 's'}`;
|
|
226
|
+
|
|
227
|
+
if (!rows.length) {
|
|
228
|
+
grid.innerHTML = '<div class="profile__placeholder">No goals match these filters.</div>';
|
|
229
|
+
state.listPager.page = 1;
|
|
230
|
+
const emptyPager = $('#goals-pager');
|
|
231
|
+
if (emptyPager) emptyPager.hidden = true;
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// task 1932 — paginate the grid. Clamp the page (the filtered set may have
|
|
236
|
+
// shrunk under a re-render), slice to the current page, then paint the bar.
|
|
237
|
+
// Priority numbers reflect the GLOBAL position (start + i + 1), not the
|
|
238
|
+
// page-local index, so drag-reorder priorities stay meaningful across pages.
|
|
239
|
+
const total = rows.length;
|
|
240
|
+
const perPage = state.listPager.perPage;
|
|
241
|
+
const totalPages = Math.max(1, Math.ceil(total / perPage));
|
|
242
|
+
if (state.listPager.page > totalPages) state.listPager.page = totalPages;
|
|
243
|
+
if (state.listPager.page < 1) state.listPager.page = 1;
|
|
244
|
+
const start = (state.listPager.page - 1) * perPage;
|
|
245
|
+
const pageRows = rows.slice(start, start + perPage);
|
|
246
|
+
grid.innerHTML = pageRows.map((g, i) => {
|
|
247
|
+
const d = state.details.get(String(g.id));
|
|
248
|
+
const crit = d ? (d.criteria || []) : [];
|
|
249
|
+
const sat = crit.filter((c) => c.satisfied).length;
|
|
250
|
+
const members = d ? (d.members || []) : [];
|
|
251
|
+
const leads = members.filter((m) => m.role === 'lead');
|
|
252
|
+
const joined = members.some((m) => sameId(m.builder_id, selfId()));
|
|
253
|
+
// task 1761: an open goal blocked by a not-yet-achieved prerequisite goal is
|
|
254
|
+
// greyed + flagged; the tooltip names what it waits on.
|
|
255
|
+
const unmetDeps = (d ? (d.dependencies || []) : []).filter((dep) => !dep.satisfied);
|
|
256
|
+
const blocked = g.status === 'open' && unmetDeps.length > 0;
|
|
257
|
+
const blockedPill = blocked
|
|
258
|
+
? `<span class="goal-badge goal-badge--blocked" title="Waiting on: ${escapeHtml(unmetDeps.map((u) => u.title || ('goal ' + u.id)).join(', '))}">🔒 blocked</span>`
|
|
259
|
+
: '';
|
|
260
|
+
const priority = ver ? `<span class="goal-card__priority" title="Priority within ${escapeHtml(ver)}">${start + i + 1}</span>` : '';
|
|
261
|
+
const handle = reorderable ? `<span class="goal-card__handle" draggable="true" title="Drag to reprioritize" aria-hidden="true">⠿</span>` : '';
|
|
262
|
+
return `<a class="goal-card${blocked ? ' goal-card--blocked' : ''}" href="#/goal/${escapeHtml(String(g.id))}" data-goal-id="${escapeHtml(String(g.id))}"${reorderable ? ' draggable="true"' : ''}>
|
|
263
|
+
<div class="goal-card__head">
|
|
264
|
+
${handle}${priority}
|
|
265
|
+
${d ? ring(sat, crit.length, g.status === 'achieved') : ''}
|
|
266
|
+
<span class="goal-card__title">${escapeHtml(g.title || 'Untitled goal')}</span>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="goal-card__meta">
|
|
269
|
+
<span>${escapeHtml(g.version_id || '')}</span>
|
|
270
|
+
${statusBadge(g.status)}
|
|
271
|
+
${blockedPill}
|
|
272
|
+
${joined ? '<span class="role-pill">joined</span>' : ''}
|
|
273
|
+
</div>
|
|
274
|
+
<div class="goal-card__foot">
|
|
275
|
+
<span><strong>${members.length}</strong> member${members.length === 1 ? '' : 's'}</span>
|
|
276
|
+
${leads.length ? `<span>lead: ${escapeHtml(leads.map(memberName).join(', '))}</span>` : ''}
|
|
277
|
+
</div>
|
|
278
|
+
</a>`;
|
|
279
|
+
}).join('');
|
|
280
|
+
renderGoalsPager(total, state.listPager.page, totalPages, start, pageRows.length);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ---- drag-to-reorder (task 1760, Archon-only, one version at a time) ------
|
|
284
|
+
let dragEl = null;
|
|
285
|
+
|
|
286
|
+
function wireDragReorder() {
|
|
287
|
+
const grid = $('#goal-grid');
|
|
288
|
+
if (!grid) return;
|
|
289
|
+
grid.addEventListener('dragstart', (e) => {
|
|
290
|
+
const el = e.target.closest('.goal-card');
|
|
291
|
+
if (!el || !el.hasAttribute('draggable')) return;
|
|
292
|
+
dragEl = el;
|
|
293
|
+
el.classList.add('goal-card--dragging');
|
|
294
|
+
e.dataTransfer.effectAllowed = 'move';
|
|
295
|
+
});
|
|
296
|
+
grid.addEventListener('dragover', (e) => {
|
|
297
|
+
if (!dragEl) return;
|
|
298
|
+
const over = e.target.closest('.goal-card');
|
|
299
|
+
if (!over || over === dragEl || !grid.contains(over)) return;
|
|
300
|
+
e.preventDefault();
|
|
301
|
+
const rect = over.getBoundingClientRect();
|
|
302
|
+
const after = (e.clientX - rect.left) > rect.width / 2;
|
|
303
|
+
grid.insertBefore(dragEl, after ? over.nextSibling : over);
|
|
304
|
+
});
|
|
305
|
+
grid.addEventListener('drop', (e) => { if (dragEl) e.preventDefault(); });
|
|
306
|
+
grid.addEventListener('dragend', async () => {
|
|
307
|
+
if (!dragEl) return;
|
|
308
|
+
const el = dragEl;
|
|
309
|
+
el.classList.remove('goal-card--dragging');
|
|
310
|
+
dragEl = null;
|
|
311
|
+
const ver = $('#f-goal-version')?.value || '';
|
|
312
|
+
if (!ver) return;
|
|
313
|
+
// task 1932 — with pagination the grid shows only the current PAGE, so the
|
|
314
|
+
// reorder must submit the FULL version order: the goals before this page +
|
|
315
|
+
// the page's cards in their new DOM order + the goals after it. Without
|
|
316
|
+
// this, POSTing only the visible page's ids would corrupt the priorities
|
|
317
|
+
// of the goals on other pages. Priority numbers reflect the global
|
|
318
|
+
// position (start + i + 1).
|
|
319
|
+
const filtered = filteredGoals();
|
|
320
|
+
const start = (state.listPager.page - 1) * state.listPager.perPage;
|
|
321
|
+
const cards = [...grid.querySelectorAll('.goal-card')];
|
|
322
|
+
cards.forEach((card, i) => {
|
|
323
|
+
const num = card.querySelector('.goal-card__priority');
|
|
324
|
+
if (num) num.textContent = String(start + i + 1);
|
|
325
|
+
});
|
|
326
|
+
const pageIds = cards.map((card) => Number(card.dataset.goalId));
|
|
327
|
+
const beforeIds = filtered.slice(0, start).map((g) => Number(g.id));
|
|
328
|
+
const afterIds = filtered.slice(start + pageIds.length).map((g) => Number(g.id));
|
|
329
|
+
const orderedIds = [...beforeIds, ...pageIds, ...afterIds];
|
|
330
|
+
try {
|
|
331
|
+
await postJSON(`${API}/goals/reorder`, { version_id: ver, ordered_ids: orderedIds });
|
|
332
|
+
toast('Priority order saved.');
|
|
333
|
+
} catch (err) {
|
|
334
|
+
toast((err && err.data && err.data.message) || 'Could not save the new order — reloading.');
|
|
335
|
+
state.goals = (await getJSON(`${API}/goals`).catch(() => ({ goals: state.goals }))).goals || state.goals;
|
|
336
|
+
renderList();
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/* ---------------- detail view ---------------- */
|
|
342
|
+
|
|
343
|
+
// task 1828 — the row's inline action, mirroring the Work Board:
|
|
344
|
+
// ready → Claim (POST /claims via doGoalClaim)
|
|
345
|
+
// active + mine → Release (POST /claims/:id/resolve via doGoalRelease)
|
|
346
|
+
// active + someone's → a muted "claimed · @who" (release is owner-only)
|
|
347
|
+
// Signed-out or any other status → empty. The row still opens the overlay on click.
|
|
348
|
+
function actionCell(t) {
|
|
349
|
+
if (!state.me) return '<td class="ledger__action"></td>';
|
|
350
|
+
const id = escapeHtml(String(t.id));
|
|
351
|
+
if (t.status === 'ready') {
|
|
352
|
+
return `<td class="ledger__action"><button class="goal-claim-btn" type="button" data-claim-task="${id}" aria-label="Claim task ${id}">Claim</button></td>`;
|
|
353
|
+
}
|
|
354
|
+
if (t.status === 'active') {
|
|
355
|
+
const c = state.claimsByTask.get(String(t.id));
|
|
356
|
+
if (c && sameId(c.builder_id, selfId())) {
|
|
357
|
+
return `<td class="ledger__action"><button class="goal-release-btn" type="button" data-release-claim="${escapeHtml(String(c.claim_id))}" data-release-task="${id}" aria-label="Release task ${id}">Release</button></td>`;
|
|
358
|
+
}
|
|
359
|
+
const who = c && (c.login || c.name) ? ` · @${escapeHtml(c.login || c.name)}` : '';
|
|
360
|
+
return `<td class="ledger__action"><span class="ledger__claimed" title="Claimed by another builder">claimed${who}</span></td>`;
|
|
361
|
+
}
|
|
362
|
+
return '<td class="ledger__action"></td>';
|
|
363
|
+
}
|
|
364
|
+
function taskRowsTodo(tasks) {
|
|
365
|
+
return tasks.map((t) => `
|
|
366
|
+
<tr class="ledger__row--task" data-task-id="${escapeHtml(String(t.id))}" style="cursor:pointer" title="Open task #${escapeHtml(String(t.id))}">
|
|
367
|
+
<td class="ledger__num">#${escapeHtml(String(t.id))}</td>
|
|
368
|
+
<td>${escapeHtml(t.title || '')}</td>
|
|
369
|
+
<td>${escapeHtml(t.kind || '—')}</td>
|
|
370
|
+
<td><span class="task__tag">${escapeHtml(t.status || '')}</span></td>
|
|
371
|
+
<td class="ledger__drachmae">${fmtNum(creditVal(t))}</td>
|
|
372
|
+
${actionCell(t)}
|
|
373
|
+
</tr>`).join('');
|
|
374
|
+
}
|
|
375
|
+
function taskRowsDone(tasks) {
|
|
376
|
+
return tasks.map((t) => `
|
|
377
|
+
<tr class="ledger__row--task" data-task-id="${escapeHtml(String(t.id))}" style="cursor:pointer" title="Open task #${escapeHtml(String(t.id))}">
|
|
378
|
+
<td class="ledger__num">#${escapeHtml(String(t.id))}</td>
|
|
379
|
+
<td>${escapeHtml(t.title || '')}</td>
|
|
380
|
+
<td>${escapeHtml(t.kind || '—')}</td>
|
|
381
|
+
<td class="ledger__drachmae">${fmtNum(creditVal(t))}${taskSettled(t) ? '' : ' <span class="ledger__est" title="Estimate — credits settle when the task is confirmed">est.</span>'}</td>
|
|
382
|
+
<td class="ledger__date">${fmtDate(t.shipped_at)}</td>
|
|
383
|
+
</tr>`).join('');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Client-side conflict estimate: exact/prefix overlap between the declared
|
|
387
|
+
// touches[] of not-yet-done tasks. Deliberately conservative — the glob-aware
|
|
388
|
+
// server-side detector (with claim collisions) is task 1736; when
|
|
389
|
+
// GET /goals/:id/conflicts exists this panel prefers it.
|
|
390
|
+
function pathOverlap(a, b) {
|
|
391
|
+
const norm = (p) => String(p).replace(/\/?\*+$/, '').replace(/\/+$/, '');
|
|
392
|
+
const hits = [];
|
|
393
|
+
for (const pa of a) {
|
|
394
|
+
for (const pb of b) {
|
|
395
|
+
const na = norm(pa); const nb = norm(pb);
|
|
396
|
+
if (!na || !nb) continue;
|
|
397
|
+
if (na === nb || na.startsWith(nb + '/') || nb.startsWith(na + '/')) {
|
|
398
|
+
hits.push(na.length >= nb.length ? na : nb);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return [...new Set(hits)];
|
|
403
|
+
}
|
|
404
|
+
function computeConflicts(tasks) {
|
|
405
|
+
const open = tasks.filter((t) => isTodoStatus(t.status) && Array.isArray(t.touches) && t.touches.length);
|
|
406
|
+
const out = [];
|
|
407
|
+
for (let i = 0; i < open.length; i++) {
|
|
408
|
+
for (let j = i + 1; j < open.length; j++) {
|
|
409
|
+
const paths = pathOverlap(open[i].touches, open[j].touches);
|
|
410
|
+
if (paths.length) out.push({ a: open[i], b: open[j], paths });
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// task 1888 — count pill helper (card headers + Work tabs).
|
|
417
|
+
function setCount(sel, n) {
|
|
418
|
+
const el = $(sel);
|
|
419
|
+
if (!el) return;
|
|
420
|
+
el.hidden = false;
|
|
421
|
+
el.textContent = String(n);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// task 1956 — a Work table's pagination bar (range, Per-page <select>, Prev/Next
|
|
425
|
+
// around "Page X of Y"), keyed by DOM prefix (goal-todo / goal-done). Hidden
|
|
426
|
+
// when the whole table fits one minimum page.
|
|
427
|
+
function renderWorkPager(prefix, pager, total, totalPages, start, shownCount) {
|
|
428
|
+
const bar = $(`#${prefix}-pager`);
|
|
429
|
+
if (!bar) return;
|
|
430
|
+
if (total <= LIST_MIN_PER_PAGE) { bar.hidden = true; return; }
|
|
431
|
+
bar.hidden = false;
|
|
432
|
+
const range = $(`#${prefix}-range`);
|
|
433
|
+
if (range) range.textContent = total
|
|
434
|
+
? `Showing ${start + 1}–${start + shownCount} of ${total}`
|
|
435
|
+
: 'Showing 0 of 0';
|
|
436
|
+
const ind = $(`#${prefix}-page-ind`);
|
|
437
|
+
if (ind) ind.textContent = `Page ${pager.page} of ${totalPages}`;
|
|
438
|
+
const prev = $(`#${prefix}-prev`);
|
|
439
|
+
if (prev) prev.disabled = pager.page <= 1;
|
|
440
|
+
const next = $(`#${prefix}-next`);
|
|
441
|
+
if (next) next.disabled = pager.page >= totalPages;
|
|
442
|
+
const sel = $(`#${prefix}-per-page`);
|
|
443
|
+
if (sel && sel.value !== String(pager.perPage)) sel.value = String(pager.perPage);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Slice `rows` to a Work table's current page (clamped), paint its bar, return
|
|
447
|
+
// the slice to render.
|
|
448
|
+
function paginateWork(rows, pager, prefix) {
|
|
449
|
+
const total = rows.length;
|
|
450
|
+
const totalPages = Math.max(1, Math.ceil(total / pager.perPage));
|
|
451
|
+
if (pager.page > totalPages) pager.page = totalPages;
|
|
452
|
+
if (pager.page < 1) pager.page = 1;
|
|
453
|
+
const start = (pager.page - 1) * pager.perPage;
|
|
454
|
+
const slice = rows.slice(start, start + pager.perPage);
|
|
455
|
+
renderWorkPager(prefix, pager, total, totalPages, start, slice.length);
|
|
456
|
+
return slice;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// task 1888 — the Work card's To do / Completed tab toggle. Both tables stay
|
|
460
|
+
// in the DOM (already rendered); switching only flips pane visibility.
|
|
461
|
+
function setWorkTab(tab) {
|
|
462
|
+
state.workTab = tab;
|
|
463
|
+
const isTodo = tab === 'todo';
|
|
464
|
+
$('#goal-tab-todo')?.setAttribute('aria-selected', String(isTodo));
|
|
465
|
+
$('#goal-tab-done')?.setAttribute('aria-selected', String(!isTodo));
|
|
466
|
+
const todoPane = $('#goal-todo-pane');
|
|
467
|
+
const donePane = $('#goal-done-pane');
|
|
468
|
+
if (todoPane) todoPane.hidden = !isTodo;
|
|
469
|
+
if (donePane) donePane.hidden = isTodo;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// task 1828 — sortable column key → header id, per table. Adding a column
|
|
473
|
+
// means an entry here + a matching <th id> in goals.html.
|
|
474
|
+
const WORK_SORT_HEADERS = {
|
|
475
|
+
todo: { kind: 'goal-todo-sort-kind', status: 'goal-todo-sort-status', credits: 'goal-todo-sort-credits' },
|
|
476
|
+
done: { kind: 'goal-done-sort-kind', credits: 'goal-done-sort-credits', shipped: 'goal-done-sort-shipped' },
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// Stable sort by the active column; null `by` returns the natural order
|
|
480
|
+
// untouched (todo: server order; done: shipped-desc from load).
|
|
481
|
+
function sortWorkRows(rows, sort) {
|
|
482
|
+
if (!sort || !sort.by) return rows.slice();
|
|
483
|
+
const dir = sort.dir === 'asc' ? 1 : -1;
|
|
484
|
+
const val = (t) => {
|
|
485
|
+
switch (sort.by) {
|
|
486
|
+
case 'kind': return String(t.kind || '').toLowerCase();
|
|
487
|
+
case 'status': return String(t.status || '').toLowerCase();
|
|
488
|
+
case 'credits': return creditVal(t);
|
|
489
|
+
case 'shipped': return new Date(t.shipped_at || 0).getTime();
|
|
490
|
+
default: return 0;
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
return rows
|
|
494
|
+
.map((row, idx) => ({ row, idx }))
|
|
495
|
+
.sort((a, b) => {
|
|
496
|
+
const av = val(a.row); const bv = val(b.row);
|
|
497
|
+
const diff = (typeof av === 'number')
|
|
498
|
+
? (av - bv) * dir
|
|
499
|
+
: String(av).localeCompare(String(bv)) * dir;
|
|
500
|
+
return diff !== 0 ? diff : a.idx - b.idx; // stable tiebreak
|
|
501
|
+
})
|
|
502
|
+
.map(({ row }) => row);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Reflect the active sort on that table's headers (only the active column
|
|
506
|
+
// shows an arrow; the rest reset to "none" — the ARIA-sort contract).
|
|
507
|
+
function reflectSortHeaders(table) {
|
|
508
|
+
const sort = state.workSort[table];
|
|
509
|
+
for (const [key, id] of Object.entries(WORK_SORT_HEADERS[table])) {
|
|
510
|
+
const th = document.getElementById(id);
|
|
511
|
+
if (!th) continue;
|
|
512
|
+
th.setAttribute('aria-sort',
|
|
513
|
+
sort.by === key ? (sort.dir === 'asc' ? 'ascending' : 'descending') : 'none');
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Cycle a column: pick → desc → asc → natural (clear). Mirrors the
|
|
518
|
+
// leaderboard's three-click-clears behaviour so there's no dead state.
|
|
519
|
+
function onWorkSortClick(table, key) {
|
|
520
|
+
const sort = state.workSort[table];
|
|
521
|
+
if (sort.by === key) {
|
|
522
|
+
if (sort.dir === 'desc') sort.dir = 'asc';
|
|
523
|
+
else { sort.by = null; sort.dir = 'desc'; }
|
|
524
|
+
} else {
|
|
525
|
+
sort.by = key; sort.dir = 'desc';
|
|
526
|
+
}
|
|
527
|
+
renderWork();
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function renderWork() {
|
|
531
|
+
const todoBody = document.querySelector('#goal-todo tbody');
|
|
532
|
+
const doneBody = document.querySelector('#goal-done tbody');
|
|
533
|
+
const todo = sortWorkRows(state.work.todo, state.workSort.todo);
|
|
534
|
+
const done = sortWorkRows(state.work.done, state.workSort.done);
|
|
535
|
+
// task 1956 — paginate each table (was ROW_CAP + a show-all toggle).
|
|
536
|
+
const todoSlice = paginateWork(todo, state.workPager.todo, 'goal-todo');
|
|
537
|
+
const doneSlice = paginateWork(done, state.workPager.done, 'goal-done');
|
|
538
|
+
if (todoBody) {
|
|
539
|
+
todoBody.innerHTML = todo.length
|
|
540
|
+
? taskRowsTodo(todoSlice)
|
|
541
|
+
: `<tr><td colspan="6" class="ledger__empty">Nothing queued under this goal. Members can author bounded tasks here${state.me ? '' : ' once signed in'}.</td></tr>`;
|
|
542
|
+
}
|
|
543
|
+
if (doneBody) {
|
|
544
|
+
doneBody.innerHTML = done.length
|
|
545
|
+
? taskRowsDone(doneSlice)
|
|
546
|
+
: '<tr><td colspan="5" class="ledger__empty">Nothing completed yet.</td></tr>';
|
|
547
|
+
}
|
|
548
|
+
setCount('#goal-todo-count', state.work.todo.length);
|
|
549
|
+
setCount('#goal-done-count', state.work.done.length);
|
|
550
|
+
reflectSortHeaders('todo');
|
|
551
|
+
reflectSortHeaders('done');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// task 1828 — claim a ready task straight from a goal's Work list. Mirrors the
|
|
555
|
+
// Work Board's doClaim (POST /claims, bind_session:false) + its typed-error
|
|
556
|
+
// messages; on success the goal's tasks reload so the row updates in place.
|
|
557
|
+
const pendingClaims = new Set();
|
|
558
|
+
function friendlyClaimError(status, data) {
|
|
559
|
+
switch (data && data.error) {
|
|
560
|
+
case 'ALREADY_CLAIMED': return 'Someone else claimed that task first. Reloading.';
|
|
561
|
+
case 'TASK_NOT_READY': return 'That task is no longer ready to be claimed.';
|
|
562
|
+
case 'TOUCHES_CONFLICT': {
|
|
563
|
+
const titles = Array.isArray(data.conflicts) ? data.conflicts.map((c) => c.title).filter(Boolean) : [];
|
|
564
|
+
return titles.length ? `That task overlaps an active claim: ${titles.join('; ')}.` : 'That task overlaps another active claim.';
|
|
565
|
+
}
|
|
566
|
+
case 'DEPS_NOT_SHIPPED': return 'That task waits on unshipped dependencies.';
|
|
567
|
+
case 'rank_too_low_for_task':
|
|
568
|
+
case 'XENOS_RESTRICTED':
|
|
569
|
+
case 'INSUFFICIENT_RANK': return 'Your rank is not yet high enough for that task.';
|
|
570
|
+
case 'newcomer_must_ship_first': return data.what_to_do || 'Ship your current claim before opening another.';
|
|
571
|
+
case 'NOT_YOUR_CLAIM': return 'That claim is not yours to release.';
|
|
572
|
+
case 'ALREADY_RESOLVED': return 'That claim was already resolved. Reloading.';
|
|
573
|
+
case 'BUILDER_INACTIVE': return 'Your account is inactive. Contact an Archon.';
|
|
574
|
+
default: return `The board could not do that (${status}). Try again.`;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
async function doGoalClaim(taskId, btn) {
|
|
578
|
+
const key = `claim:${taskId}`;
|
|
579
|
+
if (pendingClaims.has(key)) return;
|
|
580
|
+
pendingClaims.add(key);
|
|
581
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Claiming…'; }
|
|
582
|
+
try {
|
|
583
|
+
// bind_session:false — a web claim has no working session (matches the
|
|
584
|
+
// Work Board), so the builder can ship it later from any terminal.
|
|
585
|
+
await postJSON(`${API}/claims`, { task_id: Number(taskId), bind_session: false });
|
|
586
|
+
toast(`You claimed #${taskId}. Open a terminal to start work.`);
|
|
587
|
+
if (state.current != null) await loadGoalTasks(state.current);
|
|
588
|
+
} catch (err) {
|
|
589
|
+
if (err && err.message === 'redirecting to auth') return;
|
|
590
|
+
toast(friendlyClaimError(err.status, err.data));
|
|
591
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Claim'; }
|
|
592
|
+
} finally {
|
|
593
|
+
pendingClaims.delete(key);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// task 1828 — release an active claim from the goal's Work list. Mirrors the
|
|
598
|
+
// Work Board's doRelease: confirm, POST /claims/:id/resolve {abandoned} (there
|
|
599
|
+
// is no DELETE /claims/:id), owner-scoped server-side (NOT_YOUR_CLAIM). On
|
|
600
|
+
// success the task returns to 'ready' and the row reloads.
|
|
601
|
+
async function doGoalRelease(claimId, taskId, btn) {
|
|
602
|
+
const key = `release:${claimId}`;
|
|
603
|
+
if (pendingClaims.has(key)) return;
|
|
604
|
+
if (!window.confirm(`Release your claim on #${taskId}? It returns to the queue and no ${curLabel()} are earned.`)) return;
|
|
605
|
+
pendingClaims.add(key);
|
|
606
|
+
if (btn) { btn.disabled = true; btn.textContent = 'Releasing…'; }
|
|
607
|
+
try {
|
|
608
|
+
await postJSON(`${API}/claims/${encodeURIComponent(claimId)}/resolve`, { outcome: 'abandoned' });
|
|
609
|
+
toast(`Released #${taskId}. It returns to the queue.`);
|
|
610
|
+
if (state.current != null) await loadGoalTasks(state.current);
|
|
611
|
+
} catch (err) {
|
|
612
|
+
if (err && err.message === 'redirecting to auth') return;
|
|
613
|
+
toast(friendlyClaimError(err.status, err.data));
|
|
614
|
+
if (btn) { btn.disabled = false; btn.textContent = 'Release'; }
|
|
615
|
+
} finally {
|
|
616
|
+
pendingClaims.delete(key);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function conflictHtml(c) {
|
|
621
|
+
const sev = c.severity
|
|
622
|
+
? `<span class="conflict__sev conflict__sev--${escapeHtml(c.severity)}">${escapeHtml(c.severity)}</span>`
|
|
623
|
+
: '';
|
|
624
|
+
const cross = c.crossGoalTitle ? ` <small>(with ${escapeHtml(c.crossGoalTitle)})</small>` : '';
|
|
625
|
+
return `<div class="conflict">
|
|
626
|
+
<div class="conflict__pair">#${escapeHtml(String(c.a))} ${escapeHtml(c.titleA || '')} ↔ #${escapeHtml(String(c.b))} ${escapeHtml(c.titleB || '')}${cross}${sev}</div>
|
|
627
|
+
<div class="conflict__paths">${(c.paths || []).map(escapeHtml).join(' · ')}</div>
|
|
628
|
+
</div>`;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// task 1888/1828 — renders from state.conflictPairs (unified server/client
|
|
632
|
+
// shape: {a, b, titleA, titleB, paths, severity, crossGoalTitle}); null means
|
|
633
|
+
// the load failed. Fills a container with the given pairs, or a placeholder.
|
|
634
|
+
function fillConflicts(container, pairs) {
|
|
635
|
+
if (!container) return;
|
|
636
|
+
if (pairs == null) {
|
|
637
|
+
container.innerHTML = '<div class="profile__placeholder">Conflicts could not be computed.</div>';
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
const note = state.conflictSource === 'client'
|
|
641
|
+
? '<p class="conflict-note">Path-based estimate from declared files. Glob-aware detection with claim collisions — and lead resolution actions (sequencing, rescoping) — light up when the server-side detector lands (tasks 1735/1736).</p>'
|
|
642
|
+
: '';
|
|
643
|
+
container.innerHTML = (pairs.length
|
|
644
|
+
? pairs.map(conflictHtml).join('')
|
|
645
|
+
: '<div class="profile__placeholder">No conflicts detected among this goal’s open tasks.</div>') + note;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const currentDetail = () => (state.current == null ? null : state.details.get(String(state.current)));
|
|
649
|
+
|
|
650
|
+
// task 1828 — a band column shows the first `cap` of a growing list; when
|
|
651
|
+
// there's more, this foot offers "View all N <label> →" into the dedicated
|
|
652
|
+
// full view at #/goal/N/<view> (the shared pattern behind Conflicts, Members,
|
|
653
|
+
// and Completion). Hidden when nothing is truncated.
|
|
654
|
+
function renderViewAllFoot(footSel, total, cap, view, label) {
|
|
655
|
+
const foot = $(footSel);
|
|
656
|
+
if (!foot) return;
|
|
657
|
+
if (total > cap && state.current != null) {
|
|
658
|
+
foot.hidden = false;
|
|
659
|
+
foot.innerHTML = `<a class="pill-link" href="#/goal/${escapeHtml(String(state.current))}/${view}">View all ${total} ${label} →</a>`;
|
|
660
|
+
} else {
|
|
661
|
+
foot.hidden = true;
|
|
662
|
+
foot.innerHTML = '';
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// task 1828 — the band column shows the first CONFLICT_CAP; the foot routes
|
|
667
|
+
// to the dedicated full-width conflicts view. Count + amber accent on the card.
|
|
668
|
+
function renderConflicts() {
|
|
669
|
+
const pairs = state.conflictPairs;
|
|
670
|
+
const card = $('#goal-conflicts-card');
|
|
671
|
+
fillConflicts($('#goal-conflicts'), pairs == null ? null : pairs.slice(0, CONFLICT_CAP));
|
|
672
|
+
const total = pairs ? pairs.length : 0;
|
|
673
|
+
renderViewAllFoot('#goal-conflicts-foot', total, CONFLICT_CAP, 'conflicts', 'conflicts');
|
|
674
|
+
setCount('#goal-conflicts-count', total);
|
|
675
|
+
card?.classList.toggle('goal-conflicts-card--live', total > 0);
|
|
676
|
+
renderConflictsFull();
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// task 1828 — the standalone #/goal/N/conflicts view: the whole set, no cap.
|
|
680
|
+
function renderConflictsFull() {
|
|
681
|
+
const pairs = state.conflictPairs;
|
|
682
|
+
const total = pairs ? pairs.length : 0;
|
|
683
|
+
fillConflicts($('#goal-conflicts-full'), pairs);
|
|
684
|
+
setCount('#goal-conflicts-view-count', total);
|
|
685
|
+
$('#goal-conflicts-view-card')?.classList.toggle('goal-conflicts-card--live', total > 0);
|
|
686
|
+
const back = $('#goal-conflicts-back');
|
|
687
|
+
if (back && state.current != null) back.setAttribute('href', `#/goal/${state.current}`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/* ----- Completion (band column + #/goal/N/completion full view) ----- */
|
|
691
|
+
|
|
692
|
+
function completionSummaryHtml(g, crit, sat, ringSize) {
|
|
693
|
+
const line = g.status === 'achieved' ? 'Goal achieved — every criterion confirmed.'
|
|
694
|
+
: (g.status === 'archived' ? 'Archived.' : 'Open — satisfied criteria await confirmation in /goal-review.');
|
|
695
|
+
return `<div style="display:flex;align-items:center;gap:16px">
|
|
696
|
+
${ring(sat, crit.length, g.status === 'achieved', ringSize)}
|
|
697
|
+
<div>
|
|
698
|
+
<div style="font-weight:650">${sat} of ${crit.length} criteria satisfied</div>
|
|
699
|
+
<div style="font-size:12.5px;color:var(--ink-faint)">${line}</div>
|
|
700
|
+
</div>
|
|
701
|
+
</div>`;
|
|
702
|
+
}
|
|
703
|
+
// withDetail shows each criterion's full detail always inline — the extra room
|
|
704
|
+
// is the point of clicking into the dedicated completion view. The capped band
|
|
705
|
+
// (withDetail=false) instead reveals detail via a per-criterion <details> expand
|
|
706
|
+
// (BV1.R79) so the full text is one click away, not hover-only.
|
|
707
|
+
function critListHtml(list, withDetail) {
|
|
708
|
+
if (!list.length) return '<div class="profile__placeholder">No done-when criteria yet.</div>';
|
|
709
|
+
return `<ul class="crit-list${withDetail ? ' crit-list--full' : ''}">${list.map((c) => {
|
|
710
|
+
const parts = window.OTB.splitCriterion ? window.OTB.splitCriterion(c.criterion_md) : { label: c.criterion_md, detail: '' };
|
|
711
|
+
const body = withDetail
|
|
712
|
+
? `<div class="crit__col"><span class="crit__text">${escapeHtml(parts.label || '')}</span>${parts.detail ? `<span class="crit__detail">${escapeHtml(parts.detail)}</span>` : ''}</div>`
|
|
713
|
+
: (parts.detail
|
|
714
|
+
? `<details class="crit-d"><summary class="crit__text">${escapeHtml(parts.label || '')}</summary><div class="crit__detail">${escapeHtml(parts.detail)}</div></details>`
|
|
715
|
+
: `<span class="crit__text">${escapeHtml(parts.label || '')}</span>`);
|
|
716
|
+
return `<li class="crit${c.satisfied ? ' crit--done' : ''}"><span class="crit__mark">${c.satisfied ? '✓' : ''}</span>${body}</li>`;
|
|
717
|
+
}).join('')}</ul>`;
|
|
718
|
+
}
|
|
719
|
+
function renderCompletion() {
|
|
720
|
+
const d = currentDetail();
|
|
721
|
+
if (!d) return;
|
|
722
|
+
const g = d.goal;
|
|
723
|
+
const crit = d.criteria || [];
|
|
724
|
+
const sat = crit.filter((c) => c.satisfied).length;
|
|
725
|
+
const band = $('#goal-completion-body');
|
|
726
|
+
if (band) band.innerHTML = completionSummaryHtml(g, crit, sat, 56) + critListHtml(crit.slice(0, CRITERIA_CAP), false);
|
|
727
|
+
renderViewAllFoot('#goal-completion-foot', crit.length, CRITERIA_CAP, 'completion', 'criteria');
|
|
728
|
+
setCount('#goal-completion-count', crit.length);
|
|
729
|
+
const full = $('#goal-completion-full');
|
|
730
|
+
if (full) full.innerHTML = completionSummaryHtml(g, crit, sat, 64) + critListHtml(crit, true);
|
|
731
|
+
const back = $('#goal-completion-back');
|
|
732
|
+
if (back && state.current != null) back.setAttribute('href', `#/goal/${state.current}`);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/* ----- Members (band column + #/goal/N/members full view) ----- */
|
|
736
|
+
|
|
737
|
+
function memberAvatar(m) {
|
|
738
|
+
const name = memberName(m);
|
|
739
|
+
const initials = String(name).replace(/^@/, '').trim().split(/\s+/).map((w) => w[0]).slice(0, 2).join('').toUpperCase();
|
|
740
|
+
return m.avatar_url ? `<img src="${escapeHtml(m.avatar_url)}" alt="">` : escapeHtml(initials);
|
|
741
|
+
}
|
|
742
|
+
function memberLi(m) {
|
|
743
|
+
return `<li class="member">
|
|
744
|
+
<span class="member__avatar">${memberAvatar(m)}</span>
|
|
745
|
+
<span class="member__name">${escapeHtml(memberName(m))} ${m.rank ? `<small>· ${escapeHtml(rankLabel(m.rank))}</small>` : ''}</span>
|
|
746
|
+
<span class="role-pill${m.role === 'lead' ? ' role-pill--lead' : ''}">${m.role === 'lead' ? 'Manager' : 'Member'}</span>
|
|
747
|
+
</li>`;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// task 1828 — per-member contribution TO THIS GOAL, from the goal's shipped
|
|
751
|
+
// tasks (state.work.done carries shipped_by + credits_reward). Returns a Map
|
|
752
|
+
// keyed by String(builder_id) → { shipped, earned }.
|
|
753
|
+
function goalMemberStats() {
|
|
754
|
+
const stats = new Map();
|
|
755
|
+
for (const t of state.work.done) {
|
|
756
|
+
if (t.shipped_by == null) continue;
|
|
757
|
+
const key = String(t.shipped_by);
|
|
758
|
+
const s = stats.get(key) || { shipped: 0, earned: 0 };
|
|
759
|
+
s.shipped += 1;
|
|
760
|
+
// Only SETTLED tasks have really-earned drachmae; a 'completed' (pre-confirm)
|
|
761
|
+
// task in state.work.done has not landed credits yet, so it contributes 0.
|
|
762
|
+
s.earned += taskSettled(t) ? creditVal(t) : 0;
|
|
763
|
+
stats.set(key, s);
|
|
764
|
+
}
|
|
765
|
+
return stats;
|
|
766
|
+
}
|
|
767
|
+
function memberStatLi(m, stats) {
|
|
768
|
+
const s = stats.get(String(m.builder_id)) || { shipped: 0, earned: 0 };
|
|
769
|
+
const cur = curLabel();
|
|
770
|
+
const statLine = state.workLoaded
|
|
771
|
+
? `<strong>${fmtNum(s.shipped)}</strong> shipped · <strong>${fmtNum(s.earned)}</strong> ${escapeHtml(cur)}`
|
|
772
|
+
: '<span class="member__stats--pending">…</span>';
|
|
773
|
+
return `<li class="member member--stat">
|
|
774
|
+
<span class="member__avatar">${memberAvatar(m)}</span>
|
|
775
|
+
<span class="member__name">${escapeHtml(memberName(m))} ${m.rank ? `<small>· ${escapeHtml(rankLabel(m.rank))}</small>` : ''}</span>
|
|
776
|
+
<span class="member__stats" title="Contribution to this goal">${statLine}</span>
|
|
777
|
+
<span class="role-pill${m.role === 'lead' ? ' role-pill--lead' : ''}">${m.role === 'lead' ? 'Manager' : 'Member'}</span>
|
|
778
|
+
</li>`;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
function renderMembers() {
|
|
782
|
+
const d = currentDetail();
|
|
783
|
+
if (!d) return;
|
|
784
|
+
const members = d.members || [];
|
|
785
|
+
const lede = $('#goal-members-lede');
|
|
786
|
+
if (lede) {
|
|
787
|
+
const leads = members.filter((m) => m.role === 'lead');
|
|
788
|
+
lede.textContent = leads.length
|
|
789
|
+
? 'Leads manage this goal’s scope and (soon) resolve its conflicts.'
|
|
790
|
+
: 'No lead assigned yet — an Archon can appoint one.';
|
|
791
|
+
}
|
|
792
|
+
setCount('#goal-members-count', members.length);
|
|
793
|
+
const list = $('#goal-members');
|
|
794
|
+
if (list) {
|
|
795
|
+
list.innerHTML = members.length
|
|
796
|
+
? members.slice(0, MEMBER_CAP).map(memberLi).join('')
|
|
797
|
+
: '<li class="member"><span class="profile__placeholder">No members yet — be the first to join.</span></li>';
|
|
798
|
+
}
|
|
799
|
+
renderViewAllFoot('#goal-members-foot', members.length, MEMBER_CAP, 'members', 'members');
|
|
800
|
+
renderMembersFull(d);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// task 1828 — the standalone #/goal/N/members view: every member plus their
|
|
804
|
+
// contribution to this goal, and a goal-level aggregate header.
|
|
805
|
+
function renderMembersFull(d) {
|
|
806
|
+
const members = d.members || [];
|
|
807
|
+
setCount('#goal-members-view-count', members.length);
|
|
808
|
+
const stats = goalMemberStats();
|
|
809
|
+
const agg = $('#goal-members-stats');
|
|
810
|
+
if (agg) {
|
|
811
|
+
if (state.workLoaded) {
|
|
812
|
+
const shipped = state.work.done.length;
|
|
813
|
+
const earned = state.work.done.reduce((n, t) => n + (taskSettled(t) ? creditVal(t) : 0), 0);
|
|
814
|
+
const open = state.work.todo.length;
|
|
815
|
+
const cur = curLabel();
|
|
816
|
+
agg.innerHTML = `
|
|
817
|
+
<span class="mstat"><strong>${fmtNum(members.length)}</strong> member${members.length === 1 ? '' : 's'}</span>
|
|
818
|
+
<span class="mstat"><strong>${fmtNum(shipped)}</strong> task${shipped === 1 ? '' : 's'} shipped</span>
|
|
819
|
+
<span class="mstat"><strong>${fmtNum(earned)}</strong> ${escapeHtml(cur)} earned</span>
|
|
820
|
+
<span class="mstat"><strong>${fmtNum(open)}</strong> open</span>`;
|
|
821
|
+
} else {
|
|
822
|
+
agg.innerHTML = '<span class="mstat profile__placeholder">Loading this goal’s work…</span>';
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
const full = $('#goal-members-full');
|
|
826
|
+
if (full) {
|
|
827
|
+
full.innerHTML = members.length
|
|
828
|
+
? `<ul class="member-list">${members.map((m) => memberStatLi(m, stats)).join('')}</ul>`
|
|
829
|
+
: '<div class="profile__placeholder">No members yet — be the first to join.</div>';
|
|
830
|
+
}
|
|
831
|
+
const back = $('#goal-members-back');
|
|
832
|
+
if (back && state.current != null) back.setAttribute('href', `#/goal/${state.current}`);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function renderDetail(goalId) {
|
|
836
|
+
const d = state.details.get(String(goalId));
|
|
837
|
+
if (!d) return;
|
|
838
|
+
const g = d.goal;
|
|
839
|
+
const crit = d.criteria || [];
|
|
840
|
+
const members = d.members || [];
|
|
841
|
+
const sat = crit.filter((c) => c.satisfied).length;
|
|
842
|
+
const joined = members.some((m) => sameId(m.builder_id, selfId()));
|
|
843
|
+
const iAmLead = members.some((m) => sameId(m.builder_id, selfId()) && m.role === 'lead');
|
|
844
|
+
|
|
845
|
+
const h = $('#goal-detail-h');
|
|
846
|
+
if (h) h.textContent = g.title || 'Untitled goal';
|
|
847
|
+
const desc = $('#goal-detail-desc');
|
|
848
|
+
if (desc) desc.textContent = g.description || '';
|
|
849
|
+
const meta = $('#goal-detail-meta');
|
|
850
|
+
if (meta) {
|
|
851
|
+
const scope = Array.isArray(g.scope_modules) && g.scope_modules.length
|
|
852
|
+
? `<span title="Module scope — tasks authored in this goal are bounded to these modules">scope: ${g.scope_modules.map(escapeHtml).join(', ')}</span>` : '';
|
|
853
|
+
// task 1761: this goal's prerequisite goals, each ✓ (achieved) or ○ (not yet).
|
|
854
|
+
// An open goal with any unmet prerequisite is flagged blocked.
|
|
855
|
+
const deps = d.dependencies || [];
|
|
856
|
+
const unmet = deps.filter((dep) => !dep.satisfied);
|
|
857
|
+
const depsSpan = deps.length
|
|
858
|
+
? `<span class="goal-deps${(g.status === 'open' && unmet.length) ? ' goal-deps--blocked' : ''}" title="This goal is blocked until each prerequisite goal is achieved">${(g.status === 'open' && unmet.length) ? '🔒 ' : ''}depends on: ${deps.map((dep) => `${escapeHtml(dep.title || ('goal ' + dep.id))} ${dep.satisfied ? '✓' : '○'}`).join(', ')}</span>`
|
|
859
|
+
: '';
|
|
860
|
+
meta.innerHTML = `<span>${escapeHtml(g.version_id || '')}</span> ${statusBadge(g.status)} ${scope} ${depsSpan}`;
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
const actions = $('#goal-detail-actions');
|
|
864
|
+
if (actions) {
|
|
865
|
+
actions.innerHTML = g.status === 'open'
|
|
866
|
+
? (joined
|
|
867
|
+
? '<button class="btn-ghost" id="goal-leave-btn" type="button">Leave goal</button>'
|
|
868
|
+
: '<button class="landing__btn" id="goal-join-btn" type="button">Join goal</button>')
|
|
869
|
+
: '';
|
|
870
|
+
$('#goal-join-btn')?.addEventListener('click', () => joinLeave(g.id, 'join'));
|
|
871
|
+
$('#goal-leave-btn')?.addEventListener('click', () => joinLeave(g.id, 'leave'));
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// task 1828 — Completion + Members each render into their band column AND
|
|
875
|
+
// their dedicated full view (capped in the band, "View all →" into the view).
|
|
876
|
+
renderCompletion();
|
|
877
|
+
// task 1762: the goal's own owner/manager (or an Archon) can invite without
|
|
878
|
+
// an Archon in the loop — mirrors the server's authorizeGoalManagement gate.
|
|
879
|
+
// A protected-scope goal stays Archon-only on the SERVER (goals.js); this
|
|
880
|
+
// client gate deliberately doesn't re-derive the protected-module list (that
|
|
881
|
+
// roster lives server-side only, module-scope-map.js) — a non-Archon owner/
|
|
882
|
+
// manager sees the control on a protected-scope goal and gets a clear toast
|
|
883
|
+
// if the server 403s (see inviteMember's error handling below).
|
|
884
|
+
const isOwner = sameId(g.created_by, selfId());
|
|
885
|
+
const canInvite = g.status === 'open' && (isArchon() || isOwner || iAmLead);
|
|
886
|
+
renderInviteBox(g.id, canInvite, members);
|
|
887
|
+
renderMembers();
|
|
888
|
+
|
|
889
|
+
loadGoalTasks(g.id);
|
|
890
|
+
void sat; void crit; // now read inside renderCompletion()
|
|
891
|
+
void iAmLead; // lead-only resolution actions arrive with tasks 1735/1736
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
// task 1828 — the active-claim feed (all builders' in-flight claims) powers the
|
|
895
|
+
// Release button + the "claimed · @who" label. Best-effort: the feed is
|
|
896
|
+
// primer-gated server-side, so a 403/error just leaves the map empty (Claim
|
|
897
|
+
// still works; Release simply doesn't show). Keyed by String(task_id).
|
|
898
|
+
async function loadActiveClaims() {
|
|
899
|
+
try {
|
|
900
|
+
const c = await getJSON(`${API}/tasks/claimable`);
|
|
901
|
+
const map = new Map();
|
|
902
|
+
for (const ac of (c.active_claims || [])) {
|
|
903
|
+
map.set(String(ac.task_id), { claim_id: ac.claim_id, builder_id: ac.builder_id, login: ac.github_login, name: ac.display_name });
|
|
904
|
+
}
|
|
905
|
+
state.claimsByTask = map;
|
|
906
|
+
} catch (_err) {
|
|
907
|
+
state.claimsByTask = new Map();
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
async function loadGoalTasks(goalId) {
|
|
912
|
+
try {
|
|
913
|
+
const [resp] = await Promise.all([
|
|
914
|
+
getJSON(`${API}/tasks?goal_id=${encodeURIComponent(goalId)}`),
|
|
915
|
+
loadActiveClaims(), // fills state.claimsByTask (best-effort)
|
|
916
|
+
]);
|
|
917
|
+
if (!sameId(state.current, goalId)) return; // navigated away while loading
|
|
918
|
+
const tasks = resp.tasks || [];
|
|
919
|
+
state.work = {
|
|
920
|
+
todo: tasks.filter((t) => isTodoStatus(t.status)),
|
|
921
|
+
done: tasks.filter((t) => DONE_STATUSES.has(t.status))
|
|
922
|
+
.sort((a, b) => new Date(b.shipped_at || 0) - new Date(a.shipped_at || 0)),
|
|
923
|
+
};
|
|
924
|
+
state.workLoaded = true; // task 1828 — member stats can now compute
|
|
925
|
+
renderWork();
|
|
926
|
+
renderMembers(); // refresh per-member goal stats now that tasks landed
|
|
927
|
+
// Prefer the server-side detector when it exists (task 1736); any failure
|
|
928
|
+
// (404/501 pre-landing, or a transient error) falls back to the client
|
|
929
|
+
// estimate from declared touches[].
|
|
930
|
+
try {
|
|
931
|
+
const server = await getJSON(`${API}/goals/${encodeURIComponent(goalId)}/conflicts`);
|
|
932
|
+
if (!sameId(state.current, goalId)) return;
|
|
933
|
+
state.conflictPairs = (server.conflicts || []).map((c) => ({
|
|
934
|
+
a: c.task_a, b: c.task_b, titleA: c.title_a || '', titleB: c.title_b || '',
|
|
935
|
+
paths: c.overlapping_paths || [], severity: c.severity || null,
|
|
936
|
+
crossGoalTitle: c.cross_goal ? (c.goal_b_title || '') : '',
|
|
937
|
+
}));
|
|
938
|
+
state.conflictSource = 'server';
|
|
939
|
+
} catch (err) {
|
|
940
|
+
if (!sameId(state.current, goalId)) return;
|
|
941
|
+
state.conflictPairs = computeConflicts(tasks).map((c) => ({
|
|
942
|
+
a: c.a.id, b: c.b.id, titleA: c.a.title || '', titleB: c.b.title || '',
|
|
943
|
+
paths: c.paths, severity: null, crossGoalTitle: '',
|
|
944
|
+
}));
|
|
945
|
+
state.conflictSource = 'client';
|
|
946
|
+
}
|
|
947
|
+
renderConflicts();
|
|
948
|
+
} catch (err) {
|
|
949
|
+
if (err && err.message === 'redirecting to auth') return;
|
|
950
|
+
console.error('[goals] tasks load failed', err);
|
|
951
|
+
if (!sameId(state.current, goalId)) return;
|
|
952
|
+
const todoBody = document.querySelector('#goal-todo tbody');
|
|
953
|
+
const doneBody = document.querySelector('#goal-done tbody');
|
|
954
|
+
if (todoBody) todoBody.innerHTML = '<tr><td colspan="6" class="ledger__empty">Tasks could not be loaded.</td></tr>';
|
|
955
|
+
if (doneBody) doneBody.innerHTML = '<tr><td colspan="5" class="ledger__empty">Tasks could not be loaded.</td></tr>';
|
|
956
|
+
state.conflictPairs = null;
|
|
957
|
+
state.conflictSource = null;
|
|
958
|
+
renderConflicts();
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
// task 1877 — the any-builder-readable directory backing the invite dropdown
|
|
963
|
+
// (superseded task 1762's GET /leaderboard lookup, which missed anyone
|
|
964
|
+
// outside the top-100-by-credits window). Fetched once per page load and
|
|
965
|
+
// cached — the roster doesn't change fast enough to warrant a re-fetch per
|
|
966
|
+
// goal, and loadDetail's force-reload on invite already refreshes members.
|
|
967
|
+
async function loadDirectory() {
|
|
968
|
+
if (state.directory) return state.directory;
|
|
969
|
+
try {
|
|
970
|
+
const d = await getJSON(`${API}/builders/directory`);
|
|
971
|
+
state.directory = d.builders || [];
|
|
972
|
+
} catch (err) {
|
|
973
|
+
state.directory = [];
|
|
974
|
+
}
|
|
975
|
+
return state.directory;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// task 1762/1877 — owner/manager invite control. Shows a compact form (a
|
|
979
|
+
// builder dropdown + role) in place of a button while open. Idempotent
|
|
980
|
+
// re-render: always rebuilds the box from scratch, so state.inviteOpen is the
|
|
981
|
+
// only thing that survives a renderDetail() call while the form is open.
|
|
982
|
+
function renderInviteBox(goalId, canInvite, members) {
|
|
983
|
+
const box = $('#goal-invite');
|
|
984
|
+
if (!box) return;
|
|
985
|
+
if (!canInvite) {
|
|
986
|
+
box.hidden = true;
|
|
987
|
+
box.innerHTML = '';
|
|
988
|
+
state.inviteOpen = false;
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
box.hidden = false;
|
|
992
|
+
if (!state.inviteOpen) {
|
|
993
|
+
box.innerHTML = '<button class="btn-ghost" id="goal-invite-open" type="button">+ Invite member</button>';
|
|
994
|
+
$('#goal-invite-open')?.addEventListener('click', async () => {
|
|
995
|
+
state.inviteOpen = true;
|
|
996
|
+
await loadDirectory();
|
|
997
|
+
renderInviteBox(goalId, canInvite, members);
|
|
998
|
+
});
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
const memberIds = new Set((members || []).map((m) => String(m.builder_id)));
|
|
1002
|
+
const invitable = (state.directory || []).filter((b) => !memberIds.has(String(b.id)));
|
|
1003
|
+
const options = invitable
|
|
1004
|
+
.map((b) => `<option value="${escapeHtml(String(b.id))}">${escapeHtml(b.display_name || ('@' + b.github_login))}</option>`)
|
|
1005
|
+
.join('');
|
|
1006
|
+
box.innerHTML = `
|
|
1007
|
+
<form class="goal-invite__form" id="goal-invite-form">
|
|
1008
|
+
<select id="goal-invite-builder" required ${invitable.length ? '' : 'disabled'}>
|
|
1009
|
+
${invitable.length ? `<option value="" disabled selected>Choose a builder…</option>${options}`
|
|
1010
|
+
: '<option value="">Everyone is already a member</option>'}
|
|
1011
|
+
</select>
|
|
1012
|
+
<select id="goal-invite-role">
|
|
1013
|
+
<option value="member">Member</option>
|
|
1014
|
+
<option value="lead">Manager</option>
|
|
1015
|
+
</select>
|
|
1016
|
+
<button class="landing__btn" type="submit" ${invitable.length ? '' : 'disabled'}>Invite</button>
|
|
1017
|
+
<button class="btn-ghost" id="goal-invite-cancel" type="button">Cancel</button>
|
|
1018
|
+
</form>`;
|
|
1019
|
+
$('#goal-invite-cancel')?.addEventListener('click', () => {
|
|
1020
|
+
state.inviteOpen = false;
|
|
1021
|
+
renderInviteBox(goalId, canInvite, members);
|
|
1022
|
+
});
|
|
1023
|
+
$('#goal-invite-form')?.addEventListener('submit', (ev) => {
|
|
1024
|
+
ev.preventDefault();
|
|
1025
|
+
const builderId = $('#goal-invite-builder')?.value;
|
|
1026
|
+
const role = $('#goal-invite-role')?.value || 'member';
|
|
1027
|
+
if (builderId) inviteMember(goalId, builderId, role);
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
async function inviteMember(goalId, builderId, role) {
|
|
1032
|
+
try {
|
|
1033
|
+
await postJSON(`${API}/goals/${encodeURIComponent(goalId)}/members`, { builder_id: Number(builderId), role });
|
|
1034
|
+
toast('Invited.');
|
|
1035
|
+
state.inviteOpen = false;
|
|
1036
|
+
await loadDetail(goalId, { force: true });
|
|
1037
|
+
renderDetail(goalId);
|
|
1038
|
+
renderList();
|
|
1039
|
+
} catch (err) {
|
|
1040
|
+
const code = err?.data?.error;
|
|
1041
|
+
if (code === 'archon_required_for_protected_scope') toast('This goal is scoped to a protected module — only an Archon may add members.');
|
|
1042
|
+
else if (code === 'metic_required_for_protected_scope') toast('A protected-scope goal requires a Metic+ member.');
|
|
1043
|
+
else if (code === 'not_owner_or_manager') toast('Only this goal\'s owner or a manager (or an Archon) may invite.');
|
|
1044
|
+
else if (code === 'goal_not_open') toast('This goal is not open.');
|
|
1045
|
+
else toast('Could not invite that builder just now.');
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
async function joinLeave(goalId, action) {
|
|
1050
|
+
try {
|
|
1051
|
+
await postJSON(`${API}/goals/${encodeURIComponent(goalId)}/${action}`);
|
|
1052
|
+
toast(action === 'join' ? 'Joined the goal.' : 'Left the goal.');
|
|
1053
|
+
await loadDetail(goalId, { force: true });
|
|
1054
|
+
renderDetail(goalId);
|
|
1055
|
+
renderList();
|
|
1056
|
+
} catch (err) {
|
|
1057
|
+
const code = err?.data?.error;
|
|
1058
|
+
if (code === 'archon_approval_required') toast('This goal is Archon-mediated — an Archon must add you.');
|
|
1059
|
+
else if (code === 'not_a_member') toast('You are not a member of this goal.');
|
|
1060
|
+
else if (code === 'goal_not_open') toast('This goal is not open.');
|
|
1061
|
+
else toast(`Could not ${action} just now.`);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/* ---------------- data + routing ---------------- */
|
|
1066
|
+
|
|
1067
|
+
async function loadDetail(goalId, { force = false } = {}) {
|
|
1068
|
+
if (!force && state.details.has(String(goalId))) return state.details.get(String(goalId));
|
|
1069
|
+
const d = await getJSON(`${API}/goals/${encodeURIComponent(goalId)}`);
|
|
1070
|
+
state.details.set(String(goalId), d);
|
|
1071
|
+
return d;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// task 1888 — a freshly opened goal starts from the compact defaults: To do
|
|
1075
|
+
// tab selected, capped lists, counts hidden until its own data lands.
|
|
1076
|
+
function resetWorkUi() {
|
|
1077
|
+
state.workTab = 'todo';
|
|
1078
|
+
// task 1956 — a freshly opened goal starts both Work tables on page 1 (per-page persists).
|
|
1079
|
+
state.workPager.todo.page = 1;
|
|
1080
|
+
state.workPager.done.page = 1;
|
|
1081
|
+
state.workSort = { todo: { by: null, dir: 'desc' }, done: { by: null, dir: 'desc' } };
|
|
1082
|
+
state.work = { todo: [], done: [] };
|
|
1083
|
+
state.workLoaded = false;
|
|
1084
|
+
state.claimsByTask = new Map();
|
|
1085
|
+
state.conflictPairs = null;
|
|
1086
|
+
state.conflictSource = null;
|
|
1087
|
+
setWorkTab('todo');
|
|
1088
|
+
['#goal-todo-count', '#goal-done-count', '#goal-conflicts-count', '#goal-conflicts-view-count',
|
|
1089
|
+
'#goal-members-count', '#goal-members-view-count', '#goal-completion-count'].forEach((sel) => {
|
|
1090
|
+
const el = $(sel);
|
|
1091
|
+
if (el) { el.hidden = true; el.textContent = ''; }
|
|
1092
|
+
});
|
|
1093
|
+
['#goal-todo-pager', '#goal-done-pager', '#goal-conflicts-foot', '#goal-members-foot', '#goal-completion-foot'].forEach((sel) => {
|
|
1094
|
+
const el = $(sel);
|
|
1095
|
+
if (el) el.hidden = true;
|
|
1096
|
+
});
|
|
1097
|
+
['#goal-conflicts-card', '#goal-conflicts-view-card'].forEach((sel) => $(sel)?.classList.remove('goal-conflicts-card--live'));
|
|
1098
|
+
reflectSortHeaders('todo');
|
|
1099
|
+
reflectSortHeaders('done');
|
|
1100
|
+
const todoBody = document.querySelector('#goal-todo tbody');
|
|
1101
|
+
const doneBody = document.querySelector('#goal-done tbody');
|
|
1102
|
+
if (todoBody) todoBody.innerHTML = '<tr><td colspan="6" class="ledger__empty">…</td></tr>';
|
|
1103
|
+
if (doneBody) doneBody.innerHTML = '<tr><td colspan="5" class="ledger__empty">…</td></tr>';
|
|
1104
|
+
const conflicts = $('#goal-conflicts');
|
|
1105
|
+
if (conflicts) conflicts.innerHTML = '<div class="profile__placeholder">…</div>';
|
|
1106
|
+
const conflictsFull = $('#goal-conflicts-full');
|
|
1107
|
+
if (conflictsFull) conflictsFull.innerHTML = '<div class="profile__placeholder">…</div>';
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
// task 1828 — views sharing this page: the list, a goal's detail, and the
|
|
1111
|
+
// goal's three full views (conflicts / members / completion). `view` defaults
|
|
1112
|
+
// from goalId (null → list).
|
|
1113
|
+
const SUB_VIEWS = ['conflicts', 'members', 'completion'];
|
|
1114
|
+
const VIEW_SECTION = {
|
|
1115
|
+
list: '#goals-list-view',
|
|
1116
|
+
detail: '#goal-detail-view',
|
|
1117
|
+
conflicts: '#goal-conflicts-view',
|
|
1118
|
+
members: '#goal-members-view',
|
|
1119
|
+
completion: '#goal-completion-view',
|
|
1120
|
+
};
|
|
1121
|
+
const SUB_SUBTITLE = {
|
|
1122
|
+
conflicts: 'Goal conflicts — every open task pair whose declared files overlap.',
|
|
1123
|
+
members: 'Goal members — who’s on this goal and what they’ve contributed to it.',
|
|
1124
|
+
completion: 'Goal completion — every done-when criterion, with its full detail.',
|
|
1125
|
+
};
|
|
1126
|
+
function setView(goalId, view) {
|
|
1127
|
+
view = view || (goalId == null ? 'list' : 'detail');
|
|
1128
|
+
if (goalId !== state.current) {
|
|
1129
|
+
state.inviteOpen = false;
|
|
1130
|
+
if (goalId != null) resetWorkUi();
|
|
1131
|
+
}
|
|
1132
|
+
state.current = goalId;
|
|
1133
|
+
state.view = view;
|
|
1134
|
+
for (const [name, sel] of Object.entries(VIEW_SECTION)) {
|
|
1135
|
+
const el = $(sel);
|
|
1136
|
+
if (el) el.hidden = view !== name;
|
|
1137
|
+
}
|
|
1138
|
+
const sub = $('#goals-sub');
|
|
1139
|
+
if (sub) {
|
|
1140
|
+
sub.textContent = view === 'list'
|
|
1141
|
+
? 'Shared goals: progress, members, and the work remaining.'
|
|
1142
|
+
: (SUB_SUBTITLE[view] || 'Goal detail — completion, members, conflicts, and the work queue.');
|
|
1143
|
+
}
|
|
1144
|
+
window.scrollTo({ top: 0 });
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
async function route() {
|
|
1148
|
+
const hash = window.location.hash;
|
|
1149
|
+
// A record deep-link (#/task|idea|blocker/N) belongs to the shared overlay
|
|
1150
|
+
// (task-detail.js), which opens ON TOP of this page. Leave whatever goal
|
|
1151
|
+
// view is underneath intact — collapsing to the list here is what yanked
|
|
1152
|
+
// the task board out from behind the card (task 1753 regression). On a cold
|
|
1153
|
+
// load straight to a deep-link there's no detail open yet, so render the
|
|
1154
|
+
// list as the backdrop behind the card.
|
|
1155
|
+
if (DEEPLINK_RE.test(hash)) {
|
|
1156
|
+
if (state.current == null) renderList();
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
// task 1828 — a goal's dedicated full views (conflicts / members /
|
|
1160
|
+
// completion). renderDetail renders all three full views + kicks
|
|
1161
|
+
// loadGoalTasks, so a cold load straight to one still fills in once data
|
|
1162
|
+
// lands (member stats / work / conflicts refresh async).
|
|
1163
|
+
const mSub = /^#\/goal\/(\d+)\/(conflicts|members|completion)$/.exec(hash);
|
|
1164
|
+
if (mSub) {
|
|
1165
|
+
const id = mSub[1];
|
|
1166
|
+
const view = mSub[2];
|
|
1167
|
+
try {
|
|
1168
|
+
await loadDetail(id);
|
|
1169
|
+
setView(id, view);
|
|
1170
|
+
renderDetail(id);
|
|
1171
|
+
} catch (err) {
|
|
1172
|
+
if (err && err.message === 'redirecting to auth') return;
|
|
1173
|
+
toast(err?.status === 404 ? 'No such goal.' : 'Goal could not be loaded.');
|
|
1174
|
+
window.location.hash = '';
|
|
1175
|
+
setView(null);
|
|
1176
|
+
renderList();
|
|
1177
|
+
}
|
|
1178
|
+
return;
|
|
1179
|
+
}
|
|
1180
|
+
const m = /^#\/goal\/(\d+)$/.exec(hash);
|
|
1181
|
+
if (m) {
|
|
1182
|
+
const id = m[1];
|
|
1183
|
+
try {
|
|
1184
|
+
await loadDetail(id);
|
|
1185
|
+
setView(id, 'detail');
|
|
1186
|
+
renderDetail(id);
|
|
1187
|
+
} catch (err) {
|
|
1188
|
+
if (err && err.message === 'redirecting to auth') return;
|
|
1189
|
+
toast(err?.status === 404 ? 'No such goal.' : 'Goal could not be loaded.');
|
|
1190
|
+
window.location.hash = '';
|
|
1191
|
+
setView(null);
|
|
1192
|
+
}
|
|
1193
|
+
} else {
|
|
1194
|
+
setView(null);
|
|
1195
|
+
renderList();
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
async function boot() {
|
|
1200
|
+
try {
|
|
1201
|
+
state.me = await getJSON(`${API}/me`).catch(() => null);
|
|
1202
|
+
// task 1828 — "My goals" needs an identity; disable it when signed out.
|
|
1203
|
+
const mineSel = $('#f-goal-mine');
|
|
1204
|
+
if (mineSel && !state.me) { mineSel.disabled = true; mineSel.title = 'Sign in to filter to your goals'; }
|
|
1205
|
+
const gres = await getJSON(`${API}/goals`);
|
|
1206
|
+
state.goals = gres.goals || [];
|
|
1207
|
+
populateVersionFilter();
|
|
1208
|
+
// Prefetch details (ring + members on the cards). Sequentially-parallel,
|
|
1209
|
+
// failure-tolerant: a card without detail still renders.
|
|
1210
|
+
await Promise.allSettled(state.goals.map((g) => loadDetail(g.id)));
|
|
1211
|
+
await route();
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
if (err && err.message === 'redirecting to auth') return;
|
|
1214
|
+
console.error('[goals] boot failed', err);
|
|
1215
|
+
const sub = $('#goals-sub');
|
|
1216
|
+
if (sub) sub.textContent = 'Goals could not be loaded. Refresh to try again.';
|
|
1217
|
+
const grid = $('#goal-grid');
|
|
1218
|
+
if (grid) grid.innerHTML = '<div class="profile__placeholder">Goals could not be loaded.</div>';
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// task 1932 — any list filter change re-paginates the grid from page 1.
|
|
1223
|
+
const onListFilterChange = () => { state.listPager.page = 1; renderList(); };
|
|
1224
|
+
$('#f-goal-version')?.addEventListener('change', onListFilterChange);
|
|
1225
|
+
$('#f-goal-status')?.addEventListener('change', onListFilterChange);
|
|
1226
|
+
$('#f-goal-mine')?.addEventListener('change', onListFilterChange); // task 1828 — My goals filter
|
|
1227
|
+
// task 1932 — the goal grid's pagination controls (per-page dropdown + prev/next).
|
|
1228
|
+
$('#goals-per-page')?.addEventListener('change', (e) => {
|
|
1229
|
+
const v = Number(e.target.value);
|
|
1230
|
+
if (!LIST_PER_PAGE_OPTIONS.includes(v)) return;
|
|
1231
|
+
state.listPager.perPage = v;
|
|
1232
|
+
state.listPager.page = 1;
|
|
1233
|
+
try { window.localStorage.setItem(GOALS_PER_PAGE_KEY, String(v)); } catch (_) { /* private mode → in-memory only */ }
|
|
1234
|
+
renderList();
|
|
1235
|
+
});
|
|
1236
|
+
$('#goals-prev')?.addEventListener('click', () => {
|
|
1237
|
+
if (state.listPager.page > 1) { state.listPager.page -= 1; renderList(); }
|
|
1238
|
+
});
|
|
1239
|
+
$('#goals-next')?.addEventListener('click', () => {
|
|
1240
|
+
state.listPager.page += 1; renderList(); // renderList clamps to the last page
|
|
1241
|
+
});
|
|
1242
|
+
$('#goal-back')?.addEventListener('click', (e) => { e.preventDefault(); window.location.hash = ''; });
|
|
1243
|
+
// task 1828 — "← Back to goal" from any of the full views.
|
|
1244
|
+
['#goal-conflicts-back', '#goal-members-back', '#goal-completion-back'].forEach((sel) => {
|
|
1245
|
+
$(sel)?.addEventListener('click', (e) => {
|
|
1246
|
+
e.preventDefault();
|
|
1247
|
+
if (state.current != null) window.location.hash = `#/goal/${state.current}`;
|
|
1248
|
+
});
|
|
1249
|
+
});
|
|
1250
|
+
// task 1888 — the Work card's tab toggle.
|
|
1251
|
+
$('#goal-tab-todo')?.addEventListener('click', () => setWorkTab('todo'));
|
|
1252
|
+
$('#goal-tab-done')?.addEventListener('click', () => setWorkTab('done'));
|
|
1253
|
+
// task 1956 — pagination controls for each Work table (per-page dropdown + prev/next).
|
|
1254
|
+
[['goal-todo', 'todo'], ['goal-done', 'done']].forEach(([prefix, key]) => {
|
|
1255
|
+
$(`#${prefix}-per-page`)?.addEventListener('change', (e) => {
|
|
1256
|
+
const v = Number(e.target.value);
|
|
1257
|
+
if (!LIST_PER_PAGE_OPTIONS.includes(v)) return;
|
|
1258
|
+
state.workPager[key].perPage = v;
|
|
1259
|
+
state.workPager[key].page = 1;
|
|
1260
|
+
try { window.localStorage.setItem(WORK_PER_PAGE_KEY[key], String(v)); } catch (_) { /* private mode → in-memory only */ }
|
|
1261
|
+
renderWork();
|
|
1262
|
+
});
|
|
1263
|
+
$(`#${prefix}-prev`)?.addEventListener('click', () => {
|
|
1264
|
+
if (state.workPager[key].page > 1) { state.workPager[key].page -= 1; renderWork(); }
|
|
1265
|
+
});
|
|
1266
|
+
$(`#${prefix}-next`)?.addEventListener('click', () => {
|
|
1267
|
+
state.workPager[key].page += 1; renderWork(); // renderWork clamps to the last page
|
|
1268
|
+
});
|
|
1269
|
+
});
|
|
1270
|
+
// task 1828 — click / keyboard sort on the Work-table column headers.
|
|
1271
|
+
Object.entries(WORK_SORT_HEADERS).forEach(([table, cols]) => {
|
|
1272
|
+
Object.entries(cols).forEach(([key, id]) => {
|
|
1273
|
+
const th = document.getElementById(id);
|
|
1274
|
+
if (!th) return;
|
|
1275
|
+
th.addEventListener('click', () => onWorkSortClick(table, key));
|
|
1276
|
+
th.addEventListener('keydown', (e) => {
|
|
1277
|
+
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onWorkSortClick(table, key); }
|
|
1278
|
+
});
|
|
1279
|
+
});
|
|
1280
|
+
});
|
|
1281
|
+
// task 1753/1828: a row opens the shared task-detail overlay; the inline Claim
|
|
1282
|
+
// button (task 1828) claims without opening it.
|
|
1283
|
+
['#goal-todo', '#goal-done'].forEach((sel) => {
|
|
1284
|
+
$(sel)?.addEventListener('click', (e) => {
|
|
1285
|
+
const claimBtn = e.target.closest('[data-claim-task]');
|
|
1286
|
+
if (claimBtn) { e.stopPropagation(); doGoalClaim(claimBtn.dataset.claimTask, claimBtn); return; }
|
|
1287
|
+
const releaseBtn = e.target.closest('[data-release-claim]');
|
|
1288
|
+
if (releaseBtn) { e.stopPropagation(); doGoalRelease(releaseBtn.dataset.releaseClaim, releaseBtn.dataset.releaseTask, releaseBtn); return; }
|
|
1289
|
+
const row = e.target.closest('.ledger__row--task');
|
|
1290
|
+
if (row && row.dataset.taskId && window.OTBTaskDetail) window.OTBTaskDetail.open(row.dataset.taskId);
|
|
1291
|
+
});
|
|
1292
|
+
});
|
|
1293
|
+
window.addEventListener('hashchange', route);
|
|
1294
|
+
wireDragReorder();
|
|
1295
|
+
|
|
1296
|
+
boot();
|
|
1297
|
+
})();
|