Files
geo/progress.md
T
root f3679e6176 feat(deploy): add k3s deployment foundation and complete offline images
Introduce a self-contained kustomize stack under deploy/k3s covering
namespace, config, placeholder secrets, infra StatefulSets, app
Deployments/Services, migration and minio-init Jobs, and Traefik
Ingress for both tenant and ops hosts. Add Dockerfile.ops-web so the
operations frontend can ship as an nginx image, and extend the offline
package script to build and bundle ops-api, kol-assist-worker, and
ops-web alongside the k3s manifests. Wire the new ops/security knobs
through .env.example and the compose stack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 23:19:41 +08:00

694 lines
51 KiB
Markdown

# Progress Log
## Session: 2026-04-30
### Phase 37: k3s Deployment Foundation
- **Status:** complete
- Actions taken:
- Recovered the existing Docker Compose deployment topology and runtime command set.
- Added a self-contained `deploy/k3s` kustomize deployment with namespace, config, placeholder secrets, infra StatefulSets, app Deployments/Services, migration/minio-init Jobs, and Traefik Ingress.
- Included all server-side runtime services: `tenant-api`, `worker-generate`, `kol-assist-worker`, `scheduler`, `ops-api`, plus `frontend` and `ops-web`.
- Added `Dockerfile.ops-web` so the operations frontend can be built as a deployable nginx image.
- Updated the offline packaging script to build/save `ops-api`, `kol-assist-worker`, and `ops-web`, and to include the k3s directory in generated deploy packages.
- Updated the offline compose override and `.env.example` with the missing ops/security knobs.
- Files created/modified:
- `Dockerfile.ops-web` (created)
- `deploy/k3s/` (created)
- `deploy/docker-compose.yaml` (updated)
- `deploy/scripts/package.sh` (updated)
- `deploy/docker-compose.offline.yaml` (updated)
- `deploy/.env.example` (updated)
- `task_plan.md` / `findings.md` / `progress.md` (updated)
### Phase 38: k3s Verification
- **Status:** complete
- Verification:
- `kubectl kustomize deploy/k3s` rendered 30 Kubernetes objects.
- Local YAML parsing of the rendered manifest passed.
- `bash -n deploy/scripts/package.sh && bash -n deploy/scripts/load-and-start.sh` passed.
- `git diff --check` passed.
- `pnpm --filter ops-web build` passed.
- `pnpm --filter admin-web typecheck` passed.
- Notes:
- `kubectl apply --dry-run=client` could not complete locally because the configured kube-apiserver at `127.0.0.1:26443` is not running; run live API validation on the target k3s node.
## Session: 2026-04-20
### Phase 20: Tracking Collect-Now Repair
- **Status:** complete
- Actions taken:
- Re-read the `admin-web` tracking page, monitoring API client, and tenant monitoring handler/service chain to isolate why `立即采集` still depended on the removed browser-plugin runtime.
- Confirmed the frontend was still blocking on `monitoring-plugin` readiness and trying to kickoff a plugin cycle even though monitoring collection is now meant to run silently through `desktop-client`.
- Added monitoring dashboard runtime state so the page now knows whether the current logged-in user's own desktop client is online.
- Tightened `collect-now` server validation to only accept requests when the current actor has an online desktop client, instead of falling back to another online client in the same workspace.
- Updated `TrackingView.vue` to remove the plugin kickoff path, call only the backend `collect-now` endpoint, and disable the CTA with an explicit reason tooltip when brand/keyword/client-online requirements are not met.
- Added the missing `desktop_client_offline` frontend error mapping plus new i18n strings for the button gating reasons.
- Files created/modified:
- `task_plan.md` (updated)
- `findings.md` (updated)
- `progress.md` (updated)
- `apps/admin-web/src/views/TrackingView.vue` (updated)
- `apps/admin-web/src/i18n/messages/zh-CN.ts` (updated)
- `apps/admin-web/src/i18n/messages/en-US.ts` (updated)
- `apps/admin-web/src/lib/errors.ts` (updated)
- `packages/shared-types/src/index.ts` (updated)
- `server/internal/tenant/app/monitoring_service.go` (updated)
### Phase 15: Desktop Scheduler Design & Integration
- **Status:** complete
- Actions taken:
- Re-read the planning files and recovered the current desktop AI monitoring scope from the existing repo changes.
- Audited the desktop runtime controller, process metrics sampler, runtime snapshot, session registry, and monitor adapter surface.
- Confirmed the current desktop task executor is global-serial, monitor-task staleness dropping is already implemented, and no durable local queue/cache exists yet.
- Confirmed `playwright-core` is installed but `connectOverCDP()` is not wired, while hidden Electron execution already exists via `BrowserWindow` and `WebContentsView`.
- Added `monitor-scheduler.ts` as a durable file-backed local scheduler for monitor tasks, including same-day persistence, stale cross-day pruning, per-platform mutual exclusion, question-level cooldowns, and lease-miss backoff.
- Refactored `runtime-controller.ts` so publish tasks keep a conservative explicit queue while monitor tasks are selected through the local scheduler with adaptive max concurrency from Electron process metrics.
- Extended runtime diagnostics to surface the monitor scheduler snapshot and made the lease manager track multiple active leases instead of a single global lease placeholder.
- Files created/modified:
- `task_plan.md` (updated)
- `findings.md` (updated)
- `progress.md` (updated)
- `apps/desktop-client/src/main/monitor-scheduler.ts` (created)
- `apps/desktop-client/src/main/runtime-controller.ts` (updated)
- `apps/desktop-client/src/main/lease-manager.ts` (updated)
- `apps/desktop-client/src/main/runtime-snapshot.ts` (updated)
### Phase 16: Hidden Playwright CDP Execution Layer
- **Status:** complete
- Actions taken:
- Added `playwright-cdp.ts` as a reusable hidden-browser manager that launches hidden Electron `BrowserWindow` instances on account-bound sessions and attaches Playwright via `chromium.connectOverCDP()`.
- Enabled Electron remote debugging in bootstrap so the app exposes a CDP endpoint for local Playwright attachment.
- Extended adapter context with an optional Playwright `{ browser, page }` handle and wired the runtime controller to acquire/release hidden Playwright pages when an adapter declares `executionMode: "playwright"`.
- Kept the existing hidden `WebContentsView` path intact so current adapters like Doubao continue to work unchanged.
- Files created/modified:
- `apps/desktop-client/src/main/playwright-cdp.ts` (created)
- `apps/desktop-client/src/main/bootstrap.ts` (updated)
- `apps/desktop-client/src/main/adapters/base.ts` (updated)
- `apps/desktop-client/src/main/runtime-controller.ts` (updated)
### Phase 17: Verification & Delivery
- **Status:** in_progress
- Actions taken:
- Added optional scheduler metadata to desktop task events in shared types and server publishers so the local scheduler can make safer fan-out decisions before leasing.
- Ran `gofmt` on the modified Go files after the event metadata changes.
- Verified `pnpm --filter @geo/desktop-client typecheck` passes.
- Verified `pnpm --filter @geo/desktop-client build` passes.
- Verified `pnpm --filter admin-web typecheck` still passes after the shared-type event additions.
- Verified `go test ./internal/tenant/app/...` passes from `server/`.
- Files created/modified:
- `packages/shared-types/src/index.ts` (updated)
- `server/internal/tenant/app/desktop_task_events.go` (updated)
- `server/internal/tenant/app/desktop_task_service.go` (updated)
- `server/internal/tenant/app/publish_job_service.go` (updated)
### Phase 18: Qwen Adapter & Auth Relaxation
- **Status:** complete
- Actions taken:
- Ported the browser-extension `qwen` monitoring flow into a new desktop Playwright adapter that drives Tongyi's in-page managers via `page.evaluate()` and extracts answer/source data from `__qianwenChatAPI`.
- Registered the new `qwen` monitor adapter in the desktop runtime so monitor tasks can execute through the hidden Playwright CDP layer.
- Tightened generic AI auth fingerprinting to ignore CSRF-only cookies like `XSRF-TOKEN`, which Qwen sets even for anonymous visits.
- Added a Qwen-specific bind detector so the auth window can auto-close once a real session footprint is present, even if the page has not yet rendered an obvious avatar/name signal.
- Updated monitor preflight policy so only `yuanbao`, `kimi`, and `deepseek` require active login before execution; `qwen`, `doubao`, and `wenxin` are allowed to run anonymously when needed.
- Files created/modified:
- `task_plan.md` (updated)
- `findings.md` (updated)
- `progress.md` (updated)
- `apps/desktop-client/src/main/adapters/qwen.ts` (created)
- `apps/desktop-client/src/main/adapters/index.ts` (updated)
- `apps/desktop-client/src/main/runtime-controller.ts` (updated)
- `apps/desktop-client/src/main/account-binder.ts` (updated)
### Phase 19: Qwen Verification & Delivery
- **Status:** complete
- Actions taken:
- Verified `pnpm --filter @geo/desktop-client typecheck` passes after the Qwen adapter, bind-flow, and auth-policy updates.
- Verified `pnpm --filter @geo/desktop-client build` passes after the same changes.
- Followed up on a real Qwen bind stall reported by the user, inspected the machine's persisted `pending-qwen-*` partition, and confirmed logged-in cookies like `tongyi_sso_ticket`, `tongyi_sso_ticket_hash`, and `b-user-id` are present.
- Reworked Qwen account detection so bind/probe can succeed from those persisted auth cookies even when the page nickname/avatar heuristics do not trigger.
- Files created/modified:
- `task_plan.md` (updated)
- `findings.md` (updated)
- `progress.md` (updated)
## Session: 2026-04-04
### Phase 13: Prompt Centralization
- **Status:** complete
- Actions taken:
- Read the current planning files and reopened repository context for the new prompt-centralization request.
- Searched runtime and seed paths for all currently used hard-coded prompts instead of only matching the obvious template seed strings.
- Confirmed the prompt surface spans template seeds, template generation fallback prompts, template assist fallback prompts, prompt-rule generation supplements, and knowledge prompt injection.
- Added a dedicated `server/internal/tenant/prompts` package to centralize platform template seed prompts and runtime prompt fragments.
- Refactored `cmd/dev-seed`, `template_prompt.go`, `template_assist.go`, `prompt_generate_service.go`, and `knowledge_service.go` to read prompt text from the new package.
- Corrected invalid JSON quoting in the seed `custom_placeholder` fields while moving the template card-config blobs.
- Files created/modified:
- `task_plan.md` (updated)
- `findings.md` (updated)
- `progress.md` (updated)
- `server/internal/tenant/prompts/runtime.go` (created)
- `server/internal/tenant/prompts/template_seeds.go` (created)
- `server/cmd/dev-seed/main.go` (updated)
- `server/internal/tenant/app/template_prompt.go` (updated)
- `server/internal/tenant/app/template_assist.go` (updated)
- `server/internal/tenant/app/prompt_generate_service.go` (updated)
- `server/internal/tenant/app/knowledge_service.go` (updated)
### Phase 14: Prompt Verification & Delivery
- **Status:** complete
- Actions taken:
- Ran `gofmt` across all changed Go files after the prompt extraction.
- Re-scanned the runtime and seed callers to confirm the long prompt literals were removed from business functions and now only live under `server/internal/tenant/prompts`.
- Ran `go test ./internal/tenant/... ./cmd/dev-seed` from `server/` and confirmed the affected packages compile and tests pass.
- Files created/modified:
- `task_plan.md` (updated)
- `findings.md` (updated)
- `progress.md` (updated)
## Session: 2026-03-31
### Phase 10: Interface Coverage Audit & Visual Mapping
- **Status:** complete
- Actions taken:
- Re-read the backend router and transport/app layers to inventory every currently exposed tenant-facing HTTP interface.
- Confirmed the remaining frontend-backed API groups are `templates/articles` and `brands`, while `custom/media/tracking/knowledge` still lack matching backend routes in this repo.
- Recovered the reference screenshot layouts with position-aware OCR so the implementation can follow actual placement and grouping instead of just textual labels.
- Files created/modified:
- `task_plan.md` (updated)
- `findings.md` (updated)
- `progress.md` (updated)
### Phase 11: Admin-Web Full Interface Delivery
- **Status:** in_progress
- Actions taken:
- Added a minimal `vue-i18n` setup with `zh-CN` and `en-US` message bundles, locale persistence, and Ant Design locale switching.
- Reworked `LoginView`, `WorkspaceView`, `TemplatesView`, `ArticleDetailDrawer`, `TemplateGenerationDrawer`, `PageHero`, and `BrandsView` to stop using inline styles and to use component-local `<style>` blocks.
- Restored `AppShell.vue` to the user-preferred version and only moved its inline style attributes into named classes.
- Fixed the broken `pnpm dev:admin` startup path by adding the missing `BrandsView.vue`, running `pnpm install`, and re-running `pnpm typecheck:admin` plus `pnpm dev:admin`.
- Files created/modified:
- `apps/admin-web/package.json` (updated)
- `apps/admin-web/src/i18n/index.ts` (created)
- `apps/admin-web/src/i18n/messages/zh-CN.ts` (created)
- `apps/admin-web/src/i18n/messages/en-US.ts` (created)
- `apps/admin-web/src/main.ts` (updated)
- `apps/admin-web/src/App.vue` (updated)
- `apps/admin-web/src/router/meta.d.ts` (updated)
- `apps/admin-web/src/router/index.ts` (updated)
- `apps/admin-web/src/lib/api.ts` (updated)
- `apps/admin-web/src/lib/display.ts` (updated)
- `apps/admin-web/src/components/PageHero.vue` (created)
- `apps/admin-web/src/components/ArticleDetailDrawer.vue` (created)
- `apps/admin-web/src/components/TemplateGenerationDrawer.vue` (created)
- `apps/admin-web/src/views/LoginView.vue` (updated)
- `apps/admin-web/src/views/WorkspaceView.vue` (updated)
- `apps/admin-web/src/views/TemplatesView.vue` (created)
- `apps/admin-web/src/views/BrandsView.vue` (created)
- `apps/admin-web/src/views/FeatureStubView.vue` (updated)
- `apps/admin-web/src/layouts/AppShell.vue` (updated)
- `packages/shared-types/src/index.ts` (updated)
- `apps/admin-web/src/styles.css` (pending next cleanup pass)
- `task_plan.md` (updated)
- `findings.md` (updated)
- `progress.md` (updated)
### Phase 6: Frontend Scope Recovery
- **Status:** complete
- Actions taken:
- Read the planning files, Claude-side plan artifact, and architecture documents to recover the interrupted task context.
- Confirmed the backend chain is complete locally and the repository still has no `apps/` or `packages/` directories.
- Locked the next useful slice to a tenant-facing `admin-web` foundation rather than platform-side frontend work.
- Files created/modified:
- `task_plan.md` (updated)
- `findings.md` (updated)
- `progress.md` (updated)
### Phase 7: Frontend Foundation
- **Status:** complete
- Actions taken:
- Created the root pnpm workspace, root package scripts, and frontend-oriented `.gitignore`.
- Added shared frontend packages for TS config, API types, and an Axios-based client with auth refresh handling.
- Scaffolded `apps/admin-web` with Vite 8, Vue 3, TypeScript, Ant Design Vue, Pinia, Vue Router, and Vue Query.
- Files created/modified:
- `.gitignore` (created)
- `package.json` (created)
- `pnpm-workspace.yaml` (created)
- `packages/tsconfig/base.json` (created)
- `packages/shared-types/package.json` (created)
- `packages/shared-types/src/index.ts` (created)
- `packages/http-client/package.json` (created)
- `packages/http-client/src/index.ts` (created)
- `apps/admin-web/package.json` (created)
- `apps/admin-web/.env.example` (created)
- `apps/admin-web/index.html` (created)
- `apps/admin-web/tsconfig.json` (created)
- `apps/admin-web/vite.config.ts` (created)
### Phase 8: Admin-Web Initial Features
- **Status:** complete
- Actions taken:
- Implemented the app bootstrap, Ant Design locale wiring, session persistence helpers, and auth store.
- Implemented the login page with seeded local credentials and redirect-aware sign-in flow.
- Implemented the protected shell layout, navigation, quota teaser, workspace dashboard, and placeholder routes for the rest of the information architecture.
- Files created/modified:
- `apps/admin-web/src/App.vue` (created)
- `apps/admin-web/src/main.ts` (created)
- `apps/admin-web/src/styles.css` (created)
- `apps/admin-web/src/lib/session.ts` (created)
- `apps/admin-web/src/lib/errors.ts` (created)
- `apps/admin-web/src/lib/api.ts` (created)
- `apps/admin-web/src/stores/pinia.ts` (created)
- `apps/admin-web/src/stores/auth.ts` (created)
- `apps/admin-web/src/router/meta.d.ts` (created)
- `apps/admin-web/src/router/index.ts` (created)
- `apps/admin-web/src/layouts/AppShell.vue` (created)
- `apps/admin-web/src/views/LoginView.vue` (created)
- `apps/admin-web/src/views/WorkspaceView.vue` (created)
- `apps/admin-web/src/views/FeatureStubView.vue` (created)
### Phase 9: Frontend Verification & Delivery
- **Status:** complete
- Actions taken:
- Ran `pnpm install` for the new workspace and generated `pnpm-lock.yaml`.
- Fixed the initial frontend build errors related to Node typings and typed Axios wrapper method parameters.
- Switched Ant Design Vue registration from full-plugin mode to per-component registration to reduce the main bundle size.
- Ran `pnpm build:admin` successfully and confirmed production assets were emitted.
- Ran `pnpm --filter admin-web preview --host 127.0.0.1 --port 4173` and verified `/login` returned `HTTP/1.1 200 OK` with built asset references.
- Attempted browser automation verification, then fell back to HTTP verification after the local Playwright MCP environment failed on `/.playwright-mcp`.
- After Playwright became usable again, started the Vite dev server plus `tenant-api`, discovered the login CTA was not submitting on click, fixed the handler binding, and re-ran browser verification successfully with Playwright CLI.
- Files created/modified:
- `apps/admin-web/package.json` (updated)
- `apps/admin-web/tsconfig.json` (updated)
- `apps/admin-web/vite.config.ts` (updated)
- `apps/admin-web/src/main.ts` (updated)
- `apps/admin-web/src/views/LoginView.vue` (updated)
- `packages/http-client/src/index.ts` (updated)
- `pnpm-lock.yaml` (created)
- `task_plan.md` (updated)
- `findings.md` (updated)
- `progress.md` (updated)
### Phase 1: Progress Verification
- **Status:** complete
- **Started:** 2026-03-31
- Actions taken:
- Read the task-specific design document.
- Inspected top-level repository layout and backend directory tree.
- Read the planning-with-files skill instructions and initialized planning files.
- Inspected `server/Makefile`, `server/cmd/tenant-api/main.go`, and `server/internal/bootstrap/bootstrap.go`.
- Verified there are 19 migration files and inspected `cmd/dev-seed/main.go`.
- Inspected `sqlc.yaml`, repository directory contents, and current repository integration test placeholders.
- Inspected auth/workspace/template/article/brand services and route registration to compare actual implementation with the design doc.
- Files created/modified:
- `task_plan.md` (created)
- `findings.md` (created)
- `progress.md` (created)
### Phase 2: Gap Analysis and Scope Lock
- **Status:** complete
- Actions taken:
- Compared the repository contents against the design doc's expected `sqlc` and repository layout.
- Confirmed Step 4 is the earliest incomplete milestone because the generated code directory and repository wrappers are missing.
- Ran `make sqlc-generate` and observed a failing schema path in `sqlc.yaml`.
- Files created/modified:
- `task_plan.md` (updated)
- `findings.md` (updated)
- `progress.md` (updated)
### Phase 3: Implementation
- **Status:** complete
- Actions taken:
- Fixed `server/internal/tenant/repository/sqlc.yaml` and regenerated `sqlc` output under `server/internal/tenant/repository/generated/`.
- Added repository wrappers for auth, workspace, template, article, quota, and audit queries.
- Refactored auth/workspace/template services and handlers to use the new repository layer.
- Reworked template generation flow to use repository-backed transactional operations.
- Implemented atomic refresh token rotation in Redis and added `session_store` tests.
- Cleaned existing lint violations that were blocking local verification.
- Files created/modified:
- `server/internal/tenant/repository/sqlc.yaml` (updated)
- `server/internal/tenant/repository/generated/*` (generated)
- `server/internal/tenant/repository/common.go` (created)
- `server/internal/tenant/repository/auth_repo.go` (created)
- `server/internal/tenant/repository/workspace_repo.go` (created)
- `server/internal/tenant/repository/template_repo.go` (created)
- `server/internal/tenant/repository/article_repo.go` (created)
- `server/internal/tenant/repository/quota_repo.go` (created)
- `server/internal/tenant/repository/audit_repo.go` (created)
- `server/internal/tenant/app/auth_service.go` (updated)
- `server/internal/tenant/app/workspace_service.go` (updated)
- `server/internal/tenant/app/template_service.go` (updated)
- `server/internal/shared/auth/session_store.go` (updated)
- `server/internal/shared/auth/session_store_test.go` (created)
- `server/internal/tenant/transport/auth_handler.go` (updated)
- `server/internal/tenant/transport/workspace_handler.go` (updated)
- `server/internal/tenant/transport/template_handler.go` (updated)
### Phase 4: Testing & Verification
- **Status:** complete
- Actions taken:
- Re-ran `make sqlc-generate` after the final code state and confirmed generation succeeds.
- Ran `go test ./...`, then `make test`, and confirmed all tests pass.
- Ran `make lint`, fixed the reported warnings, and re-ran until it passed cleanly.
- Verified the tenant-scope guard script exists and checked for CI workflow files.
- Files created/modified:
- `server/internal/tenant/repository/tx.go` (updated)
- `server/cmd/dev-seed/main.go` (updated)
- `server/internal/shared/auth/middleware_test.go` (updated)
- `server/internal/tenant/app/article_service.go` (updated)
- `server/internal/tenant/app/brand_service.go` (updated)
- `task_plan.md` (updated)
- `findings.md` (updated)
- `progress.md` (updated)
### Phase 5: Delivery
- **Status:** complete
- Actions taken:
- Added backend CI workflow under `.github/workflows/`.
- Added `tenant-scope-guard` to the backend `Makefile` so CI and local verification use the same entrypoint.
- Re-ran `make tenant-scope-guard`, `make test`, and `make lint` after the CI-related edits.
- Updated the backend `Makefile` to run `golang-migrate` through `go run -tags postgres` instead of requiring a preinstalled `migrate` binary.
- Ran `make dev-init` successfully, including Docker startup, migrations, and seed data.
- Started `tenant-api` and verified health endpoints plus auth login/me/refresh/logout behavior with positive and negative checks.
- Files created/modified:
- `.github/workflows/backend-ci.yml` (created)
- `server/Makefile` (updated)
- `task_plan.md` (updated)
- `findings.md` (updated)
- `progress.md` (updated)
## Test Results
| Test | Input | Expected | Actual | Status |
|------|-------|----------|--------|--------|
| Repository inspection | `ls`, `rg --files`, `sed` | Establish current backend coverage | Backend skeleton, migrations, services, and transport files exist | pass |
| Migration count | `find server/migrations -name '*.up.sql' | wc -l` | 19 schema migrations | 19 | pass |
| SQLC generation | `make sqlc-generate` | Generate typed code | Passed after fixing `sqlc.yaml` schema path | pass |
| Go tests | `go test ./...` | All packages compile and tests pass | Passed | pass |
| Make tests | `make test` | Roadmap acceptance command passes | Passed | pass |
| Lint | `make lint` | Roadmap acceptance command passes | Passed after fixing errcheck/ineffassign warnings | pass |
| CI workflow presence | `find . -path '*/.github/workflows/*' -type f` | At least one CI workflow if Step 9 is complete | `.github/workflows/backend-ci.yml` exists | pass |
| Tenant scope guard | `make tenant-scope-guard` | Tenant SQL filter guard passes | Passed | pass |
| Dev init | `make dev-init` | Infra, migrations, and seed complete successfully | Passed after fixing Makefile-managed migrate driver tags | pass |
| Live health | `GET /api/health/live` | HTTP 200 with alive status | Returned `code=0`, `status=alive` | pass |
| Ready health | `GET /api/health/ready` | HTTP 200 with ready status | Returned `code=0`, `status=ready` | pass |
| Login | `POST /api/auth/login` with `admin@geo.local / Admin@123` | Receive access/refresh tokens and user info | Passed | pass |
| Me | `GET /api/auth/me` with fresh access token | Current user payload returned | Passed | pass |
| Refresh | `POST /api/auth/refresh` with original refresh token | Receive rotated access/refresh token pair | Passed | pass |
| Logout revoke | `POST /api/auth/logout`, then `GET /api/auth/me` with old access token | Old access token rejected | Returned `40103 token_revoked` | pass |
| Refresh rotate invalidate | Reuse old refresh token after refresh | Old refresh token rejected | Returned `40121 refresh_session_expired` | pass |
| Workspace overview | `GET /api/tenant/workspace/overview` | Aggregated stats return successfully | Returned `code=0`, article/brand counts present | pass |
| Workspace recent articles | `GET /api/tenant/workspace/recent-articles` | Recent list returns successfully | Returned `code=0` | pass |
| Workspace quota summary | `GET /api/tenant/workspace/quota-summary` | Quota summary returns successfully | Returned `code=0`, balance/total present | pass |
| Workspace template cards | `GET /api/tenant/workspace/template-cards` | Template cards return successfully | Returned `code=0`, count=4 | pass |
| Template generate flow | `POST /api/tenant/templates/{id}/generate` + article detail/version checks | Article generation completes end-to-end | Article `1` completed, versions count `1`, quota `100 -> 99` | pass |
| Brand soft delete and recreate | Create brand, delete, recreate same name | Recreate succeeds after soft delete | Original brand `1` deleted, recreated as brand `2` | pass |
| Question version history | Create question, update, list versions | Version history retained | Returned 2 versions, latest text updated | pass |
| Competitor CRUD basic flow | Create competitor then list | Competitor visible under brand | Returned count `1` | pass |
| Frontend workspace install | `pnpm install` | Workspace dependencies install cleanly | Passed | pass |
| Frontend build | `pnpm build:admin` | `admin-web` compiles and emits production assets | Passed | pass |
| Frontend preview login HTML | `curl -I http://127.0.0.1:4173/login` | Preview route returns `200 OK` | Returned `HTTP/1.1 200 OK` | pass |
| Frontend preview asset shell | `curl -s http://127.0.0.1:4173/login` | HTML references built CSS/JS assets | Returned built asset tags for `index-CtRpygQP.js` and `index-DFz5-Rsr.css` | pass |
| Frontend browser login flow | `pnpm dlx @playwright/test test .tmp-playwright/admin-web.spec.js --reporter=line` | Login succeeds and workspace renders | Passed after binding the login CTA directly to `handleSubmit` | pass |
## Error Log
| Timestamp | Error | Attempt | Resolution |
|-----------|-------|---------|------------|
| 2026-03-31 | None yet | 1 | N/A |
| 2026-03-31 | `make sqlc-generate` failed due to invalid schema path in `sqlc.yaml` | 1 | Fixed by changing schema path to `../../../migrations/` |
| 2026-03-31 | `make lint` failed on several existing warnings | 1 | Fixed all reported warnings and re-ran lint successfully |
| 2026-03-31 | `make dev-init` failed because go-run migrate lacked the postgres driver tag | 1 | Updated `MIGRATE_CMD` to `go run -tags postgres ...` and reran successfully |
| 2026-03-31 | Gemini CLI returned `429 MODEL_CAPACITY_EXHAUSTED` on `gemini-3.1-pro-preview` | 1 | Stopped pursuing Gemini once the user said not to use Gemini |
| 2026-03-31 | Initial frontend build failed on missing Node typings and implicit `any` parameters | 1 | Removed the Node type dependency from app TS config, simplified the Vite config, and typed the API client methods explicitly |
| 2026-03-31 | Playwright MCP browser open failed on `/.playwright-mcp` | 1 | Used preview HTTP verification instead of blocking on browser environment setup |
| 2026-03-31 | Browser click on the login CTA did not submit the form, so Playwright stayed on `/login` | 1 | Added an explicit `@click=\"handleSubmit\"` binding to the CTA and re-ran the E2E flow successfully |
### Phase 22: Monitoring Daily Collection Review
- **Status:** complete
- Actions taken:
- Read the existing planning files and restored context for the monitoring scheduler/runtime work.
- Inspected `cmd/scheduler`, the shared cron helper, `ScheduleDispatchWorker`, monitoring handlers, monitoring service, phase2 desktop task dispatch, migrations, and deployment config.
- Queried local Postgres/Monitoring Postgres containers for current task evidence.
- Verified the data-detail/dashboard read path with existing 2026-04-24 monitoring data.
- Findings so far:
- Current DB evidence shows 2026-04-24 monitoring collection tasks are `desktop/manual/plugin_standard`, not daily automatic tasks.
- `tenant_monitoring_quotas` is configured as `collect_frequency = daily`, but the inspected runtime does not contain a daily monitoring task generation worker.
### Phase 23: Monitoring Daily Task Generation Repair
- **Status:** complete
- Actions taken:
- Added `server/internal/tenant/app/monitoring_daily_task_worker.go`.
- Wired `MonitoringDailyTaskWorker` into `server/cmd/scheduler/main.go`.
- Added `server/internal/tenant/app/monitoring_daily_task_worker_test.go` for midnight-after materialization, existing-task idempotency, and deterministic plan ordering.
- Implemented stable jitter after midnight, global/plan/brand batch caps, daily hard-cap selection, and brand-level advisory locking.
- Implemented due desktop dispatch claim with `FOR UPDATE SKIP LOCKED`, `last_dispatched_at`, `dispatch_attempts`, and a retry cooldown.
- Kept automatic daily desktop tasks pending when no eligible desktop account/client is available; no legacy fallback is applied for this path.
- Fixed the P0 rollout-disabled case: daily generation now requires `desktop_tasks` rollout plus `primary_client_id`; otherwise it skips the plan and records `SkippedPlanCount` rather than creating orphan `automatic/legacy` rows.
- Removed Go runtime and dev-seed dependency on `tenant_monitoring_quotas`: daily plans now come from `desktop_clients`, dashboard/projection use default monitoring platforms, and primary client selection is derived from recent/registered desktop clients.
- Fixed the P0 RabbitMQ publish context issue in daily worker: `publishMonitorDesktopTaskAvailable` now receives a 5-second child context derived from the worker tick context instead of `context.Background()`.
- Verification:
- `go test ./internal/tenant/app -run 'Test.*MonitoringDaily|Test.*Monitoring|TestEncodeMonitoringProjectionRebuildEnvelope|TestBuildMonitoringRawPayload' -count=1` passed.
- `go test ./cmd/scheduler ./cmd/dev-seed ./internal/tenant/app -count=1` passed.
- `docker exec geo-monitoring-postgres psql ... EXPLAIN ...` passed for the new claim SQL.
### Phase 24: Monitoring Daily Observability Hardening
- **Status:** complete
- Actions taken:
- Completed the plan/brand scoped error-isolation path so transient failures increment summary failure counts and do not stop other tenants/brands in the same tick.
- Added `monitoring_daily_task_metrics.go` with atomic counters/gauges for worker runs, fatal and partial failures, plan/brand failures, skipped plans, task stage counts, last timestamps, duration, business date, and last fatal error code/message.
- Recorded metrics on every `runOnce` result, including zero-work ticks and fatal errors.
- Exposed `/metrics` in Prometheus text format and `/api/internal/metrics/monitoring/daily-tasks` as JSON.
- Started the scheduler's health/metrics HTTP server on `scheduler.http_port` default `8081`.
- Reworked daily hard-cap accounting so existing materialized tasks are counted from DB, full candidate horizons are preserved for de-duplication, and remaining capacity is decremented only by rows inserted in the current tick.
- Split daily materialization limit from desktop dispatch limit, so a full daily cap does not stop already-pending tasks from being dispatched.
- Replaced the hard-coded daily plan defaults with subscription/config-driven rules: active subscription required, max brands from plan/config, platforms from live bound desktop accounts, and per-brand task cap from configured active questions capped by `brand_library`.
- Changed dashboard and projection platform selection so unbound platforms are not silently shown or scheduled.
- Verification:
- `go test ./internal/tenant/app -run 'Test.*MonitoringDaily' -count=1` passed.
- `go test ./internal/tenant/app -run 'Test.*MonitoringDaily|TestReconcile|TestResolve|TestIntersect|TestFilter' -count=1` passed.
- `go test ./cmd/scheduler ./internal/bootstrap ./internal/shared/config -count=1` passed.
- `go test ./cmd/scheduler ./cmd/dev-seed ./internal/tenant/app -count=1` passed.
- `go test ./...` passed from `server/`.
- `git diff --check` passed.
### Phase 25: Heartbeat Primary Stability Hardening
- **Status:** complete
- Actions taken:
- Replaced heartbeat primary resolution with a per-tenant/per-workspace `desktop_client_primary_leases` lease row.
- The current primary renews the lease on heartbeat; other desktop clients are non-primary while the incumbent lease is live.
- Failover now happens only when the lease has expired or the recorded primary desktop client is unavailable/revoked.
- Updated daily monitoring plan selection to prefer the sticky lease primary before deterministic fallback.
- Removed `last_seen_at` ordering from the current-user registered-client lookup so UI platform selection is not heartbeat-driven.
- Added migration `20260424130000_add_desktop_primary_leases` with the lease table, backfill, and active desktop-client lookup indexes.
- Added a unit test for the lease-claim decision matrix.
- Verification:
- `go test ./internal/tenant/app -run 'TestShouldClaimHeartbeatPrimaryLease|Test.*MonitoringDaily|TestReconcile|TestResolve|TestIntersect|TestFilter' -count=1` passed.
- `go test ./...` passed from `server/`.
- `git diff --check` passed.
### Phase 26: Scheduler Metrics HTTP Hardening
- **Status:** complete
- Actions taken:
- Added `scheduler.http_host` with default `127.0.0.1`.
- Added `scheduler.internal_metrics_token`, with env overrides `SCHEDULER_INTERNAL_METRICS_TOKEN` / `SCHEDULER_METRICS_TOKEN`.
- Protected `/metrics` and `/api/internal/metrics/monitoring/daily-tasks` with token auth.
- Changed `scheduler.http_port < 0` to mean explicit HTTP disablement; `0` still defaults to `8081`.
- Replaced `app.Engine.Run(... Fatalf ...)` with `http.Server.ListenAndServe`, non-fatal error logging, and graceful `Shutdown`.
- Added scheduler HTTP auth/address tests and config normalization/env tests.
- Verification:
- `go test ./cmd/scheduler ./internal/shared/config -count=1` passed.
- `go test ./...` passed from `server/`.
- `git diff --check` passed.
### Phase 27: Scheduler Worker Graceful Shutdown
- **Status:** complete
- Actions taken:
- Added blocking `Run(ctx)` methods to scheduler workers while keeping existing `Start(ctx)` wrappers.
- Updated `cmd/scheduler` to create workers explicitly and run them under a `sync.WaitGroup`.
- On signal, scheduler now stops the metrics HTTP server, cancels worker loops, and waits up to 60 seconds for workers to finish.
- Changed scheduler worker loops so shutdown cancellation prevents the next tick but does not immediately cancel an active `runOnce`; active work still has the existing per-worker timeout.
- Added tests for worker wait completion and timeout behavior.
- Verification:
- `go test ./cmd/scheduler ./internal/scheduler ./internal/tenant/app -run 'TestScheduler|TestShouldClaimHeartbeatPrimaryLease|Test.*MonitoringDaily' -count=1` passed.
- `go test ./...` passed from `server/`.
- `git diff --check` passed.
### Phase 28: Monitoring Platform Authorization UI Contract
- **Status:** complete
- Actions taken:
- Added explicit monitoring platform authorization status to dashboard runtime and question-detail API responses.
- Added shared frontend type `MonitoringPlatformAuthorizationStatus`.
- Updated `TrackingView` to keep dashboard platform rows stable and disable collect-now for no-authorized or selected-unauthorized platform states without rendering a no-authorized dashboard alert.
- Updated `TrackingQuestionDetailView` to show an explicit unavailable state and suppress the platform tabs/detail grid when no platforms are authorized.
- Added i18n strings for the new no-desktop, no-authorized, and selected-unauthorized states.
- Added a Go unit test for the authorization-status decision matrix.
- Verification:
- `go test ./internal/tenant/app -run 'TestMonitoringPlatformAuthorizationStatus|TestReconcileEnabledMonitoringPlatforms|TestFilterMonitoringPlatforms|TestResolveDashboardDateWindowAt|TestParseDetailDateRangeAt' -count=1` passed.
- `pnpm typecheck:admin` passed.
- Browser regression against admin-web with `platform_authorization_status = no_authorized_platforms` passed for dashboard and question detail; dashboard does not show a no-authorized alert.
- `go test ./...` passed from `server/`.
- `pnpm build:admin` passed; it still reports existing bundle/CSS warnings unrelated to this change.
- `git diff --check` passed.
### Phase 29: Monitoring Six-Platform Display Contract
- **Status:** complete
- Actions taken:
- Changed `loadMonitoringProjectionPlatforms` to return the canonical six-platform catalog instead of deriving display rows from same-day task rows.
- Changed dashboard platform breakdown to use the same six-platform display catalog, so platforms with zero tasks/samples still appear.
- Added `authorized_monitoring_platform_ids` to dashboard runtime and shared frontend types.
- Updated `TrackingView` collect-now gating to use the explicit authorized platform ID list instead of inferring authorization from visible platform rows.
- Added unit coverage for the six-platform catalog copy and projection platform selection.
- Verification:
- `go test ./internal/tenant/app -run 'TestDefaultMonitoringPlatformMetadata|TestLoadMonitoringProjectionPlatforms|TestMonitoringPlatformAuthorizationStatus|TestReconcileEnabledMonitoringPlatforms|TestFilterMonitoringPlatforms' -count=1` passed.
- `pnpm typecheck:admin` passed.
- Browser regression confirmed a no-authorized-platform dashboard still renders all 6 platform names and keeps collect-now disabled.
- `go test ./...` passed from `server/`.
- `pnpm build:admin` passed; existing CSS/chunk warnings remain unrelated.
- `git diff --check` passed.
### Phase 30: Workspace Scope And Daily Config Visibility
- **Status:** complete
- Actions taken:
- Added current-workspace context helpers and a `WorkspaceScope` middleware with `X-Workspace-ID` support plus membership validation for non-primary workspace requests.
- Wired the tenant route group through `WorkspaceScope`.
- Updated monitoring dashboard, question detail, and collect-now paths to use the current request workspace for quota/client/platform-account/access-state resolution.
- Documented `findLatestRegisteredClientForUser` as created-at ordered and intentionally independent of online health.
- Changed daily worker platform JSON decoding to return errors instead of swallowing malformed JSON.
- Counted malformed daily platform config as a plan failure and logged it with tenant/workspace/business-date context.
- Added unit coverage for current workspace fallback/override, workspace-id parsing, and daily platform JSON decode failures.
- Verification:
- `go test ./internal/shared/auth ./internal/shared/middleware ./internal/tenant/app -run 'TestCurrentWorkspaceID|TestWorkspaceScope|TestRequestedWorkspaceID|TestDecodeDailyMonitoringEnabledPlatforms|Test.*MonitoringDaily|TestMonitoringPlatformAuthorizationStatus|TestDefaultMonitoringPlatformMetadata|TestLoadMonitoringProjectionPlatforms' -count=1` passed.
- `go test ./internal/tenant/transport -run TestProtectedRoutes -count=1` passed.
- `go test ./...` passed from `server/`.
- `git diff --check` passed.
### Phase 31: Heartbeat Primary Lease Scalability
- **Status:** complete
- Actions taken:
- Replaced per-heartbeat `BEGIN` / `SELECT ... FOR UPDATE` / `UPDATE` primary lease handling with read-mostly lease resolution.
- Normal primary heartbeats now return from a plain read unless the renew interval has elapsed.
- Non-primary heartbeats return from a plain read while the incumbent lease is live and available.
- Missing, expired, unavailable, and race cases use single-statement `INSERT` or CAS-style `UPDATE` writes instead of an explicit lock transaction.
- Added heartbeat primary lease metrics for hit/miss outcomes, DB writes, contention, errors, and duration.
- Added token-protected tenant-api internal metrics endpoints for heartbeat primary lease JSON and Prometheus output.
- Documented the renew throttle relative to the 25s desktop heartbeat and 90s TTL.
- Verification:
- `go test ./internal/tenant/app -run 'TestDecideHeartbeatPrimaryLease|TestMonitoringHeartbeatPrimaryLeaseMetrics|TestBuildMonitoringRawPayloadKimi' -count=1` passed.
- `go test ./internal/tenant/transport -run 'TestProtectedRoutes|TestPublicRoutes' -count=1` passed.
- `go test ./...` passed from `server/`.
- `git diff --check` passed.
### Phase 32: Monitoring Heartbeat Snapshot Scalability
- **Status:** complete
- Actions taken:
- Changed `MonitoringCallbackService.Heartbeat` to batch-load current access snapshot state before deciding whether a monitoring DB transaction is required.
- Stable unchanged platform access reports now skip snapshot upserts; unchanged rows refresh at most every 10 minutes.
- Primary-client inaccessible-platform reconciliation now first checks whether matching pending tasks exist, so stable inaccessible reports with no pending work do not open a write transaction.
- Added heartbeat snapshot metrics for platform reports, transaction opens, snapshot insert/update/refresh/skip counts, reconciled tasks, projection rebuilds, errors, and duration.
- Exposed heartbeat snapshot metrics through token-protected tenant-api internal JSON and Prometheus endpoints.
- Added a monitoring DB partial index for pending-platform reconciliation lookup.
- Verification:
- `go test ./internal/tenant/app -run 'TestDecideMonitoringHeartbeatSnapshot|TestMonitoringHeartbeatSnapshotMetrics|TestDecideHeartbeatPrimaryLease|TestMonitoringHeartbeatPrimaryLeaseMetrics' -count=1` passed.
- `go test ./internal/tenant/app -count=1` passed.
- `go test ./internal/tenant/transport -count=1` passed.
- `go test ./...` passed from `server/`.
- `git diff --check` passed.
### Phase 33: Prometheus Metrics Exposition Hardening
- **Status:** complete
- Actions taken:
- Added `prometheus/client_golang` and replaced custom hand-written Prometheus text builders with `prometheus.Collector` implementations plus `promhttp`.
- Kept existing JSON snapshot endpoints unchanged.
- Changed scheduler `/metrics` to serve a dedicated registry containing daily-task metrics plus Go runtime and process collectors.
- Changed tenant-api internal heartbeat Prometheus endpoints to serve through `promhttp` as well.
- Removed raw error-message labels from heartbeat snapshot Prometheus output; detailed messages remain available in JSON snapshots.
- Added explicit carriage-return normalization for label values before creating const metrics, because the local client_golang text path does not escape raw CR.
- Added tests covering scheduler runtime metrics and CR label normalization.
- Verification:
- `go test ./internal/tenant/app -run 'TestMonitoringDailyTaskMetrics|TestMonitoringDailyTaskPrometheus|TestMonitoringSchedulerMetricsPrometheusHandler|TestMonitoringHeartbeatPrimaryLeaseMetrics|TestMonitoringHeartbeatSnapshotMetrics' -count=1` passed.
- `go test ./cmd/scheduler ./internal/tenant/transport -run 'TestScheduler|TestProtectedRoutes|TestPublicRoutes|TestDesktopClientRoutes' -count=1` passed.
- `go test ./...` passed from `server/`.
- `git diff --check` passed.
### Phase 34: Daily Task Batch Materialization
- **Status:** complete
- Actions taken:
- Changed `ensureDailyMonitoringCollectTasks` from per-candidate `tx.Exec` to a single batched `INSERT ... SELECT FROM unnest(...)`.
- Batched arrays carry `question_id`, `question_hash`, `ai_platform_id`, and `dispatch_after`; tenant/brand/business date/collector/run mode/target client/shard key/execution owner remain constant per call.
- Kept `ON CONFLICT DO NOTHING`, so duplicate materialization still returns only the number of newly inserted rows via `RowsAffected`.
- Verification:
- `go test ./internal/tenant/app -run 'Test.*MonitoringDaily|TestMonitoringDailyTaskMetrics|TestMonitoringDailyTaskPrometheus|TestMonitoringSchedulerMetricsPrometheusHandler' -count=1` passed.
- `go test ./...` passed from `server/`.
- `git diff --check` passed.
### Phase 35: Daily Plan SQL Decomposition
- **Status:** complete
- Actions taken:
- Split `loadDailyMonitoringPlans` into `loadDailyMonitoringPrimaryClientPlans`, `loadDailyMonitoringPlanPlatformJSON`, and a pure assembly helper.
- Kept the first query focused on active subscription admission, lease-preferred primary client selection, workspace scoping, and subscription/config-derived brand/question limits.
- Moved authorized platform aggregation to a second batched query keyed by `(tenant_id, workspace_id, client_id)`.
- Added a helper test covering the race where a selected client loses its platform snapshot between the two reads; that candidate is skipped instead of becoming an empty executable plan.
- Verification:
- `go test ./internal/tenant/app -run 'TestBuildDailyMonitoringPlansFromSelectedClients|Test.*MonitoringDaily' -count=1` passed.
- `go test ./internal/tenant/app -run 'Test.*MonitoringDaily|TestMonitoringDailyTaskMetrics|TestMonitoringDailyTaskPrometheus|TestMonitoringSchedulerMetricsPrometheusHandler' -count=1` passed.
- `go test ./...` passed from `server/`.
- `git diff --check` passed.
### Phase 36: Monitoring Authorization And Detail Display Bug Fix
- **Status:** complete
- Actions taken:
- Changed monitoring platform authorization to mean a bound, non-deleted platform account on a supported AI platform; `platform_accounts.health` no longer has to be `live`.
- Removed `health = 'live'` from daily scheduled plan eligibility and platform aggregation, so daily automatic collection can materialize tasks for offline clients and non-live account states.
- Kept immediate collection gated by desktop-client online state, and made the online check target the same desktop client that owns the authorized platform accounts.
- Changed question detail to display the canonical platform history instead of hiding existing data behind current `platform_authorization_status`.
- Updated frontend date validation to use the monitoring business date, so today's date such as 2026-04-24 is not rejected by local/browser timezone drift.
- Verification:
- `go test ./internal/tenant/app -run 'Test.*MonitoringDaily|TestMonitoringPlatformAuthorizationStatus|TestReconcileEnabledMonitoringPlatforms|TestFilterMonitoringPlatforms|TestDefaultMonitoringPlatformMetadata|TestLoadMonitoringProjectionPlatforms' -count=1` passed.
- `pnpm --filter admin-web typecheck` passed.
- `go test ./...` passed from `server/`.
- `pnpm --filter admin-web build` passed.
- `git diff --check` passed.
### Phase 21: Foreground Publish Admission
- **Status:** complete
- Actions taken:
- Confirmed the current runtime only starts publish when `state.activeExecutions.size === 0`, so an already-running monitor can block media publish.
- Locked the implementation model: publish is foreground, monitor is background, publish receives reserved runtime capacity, and publish platform policy stays in a lightweight in-memory scheduler.
- Added `apps/desktop-client/src/main/publish-scheduler.ts` with in-memory queueing, same-platform active locks, and 3-6 second platform cooldowns.
- Refactored `runtime-controller.ts` so publish can start while monitor is running, monitor pauses behind any publish backlog, and lease-in-flight is tracked separately for publish vs. monitor.
- Added hardware/runtime adaptive total concurrency with `GEO_DESKTOP_MAX_TOTAL_CONCURRENCY` as an optional cap and a reserved publish slot for monitor budgeting.
- Exposed `publishScheduler` in the desktop runtime snapshot and added scheduler unit tests.
- Verification:
- `pnpm --filter @geo/desktop-client build` passed.
- `pnpm --filter @geo/desktop-client test` passed: 5 files, 26 tests.
- `git diff --check` passed.
- `pnpm --filter @geo/desktop-client typecheck` failed before checking runtime changes because `electron.vite.config.ts` has a Vite 5/8 plugin type mismatch.
### 2026-04-28 Dongchedi Publish Debug
- **Status:** in_progress
- Actions taken:
- Queried local `geo-postgres` through `docker exec` because `psql` is not installed on the host.
- Verified article `95` reconstructs to normal markdown through `repository.LoadArticleVersionContent`; removed the temporary inspection test after use.
- Updated `apps/desktop-client/src/main/adapters/dongchedi.ts` so body images first try Dongchedi `spice/image`; local/relative images use ImageX upload and then run through `spice/image` before replacement.
- Added redacted Dongchedi publish rejection diagnostics to the adapter error payload/logs.
- Added `apps/desktop-client/src/main/adapters/dongchedi-content.ts` and a unit test to downgrade table HTML to paragraph rows before Dongchedi submit.
- Followed up after another `server exception` retry where diagnostics showed no table tags, no local asset URL, and one body image.
- Removed `&nbsp;` entities from Dongchedi table-downgraded body HTML.
- Changed body image handling so ImageX-uploaded local images try Dongchedi `spice/image` through both session and page-context requests before replacement.
- After the platform returned `dongchedi_image_upload_failed:content_images_missing_1_of_1`, tried an ImageX direct URL fallback; the next retry still returned `server exception` with `content_image_upload_strategies = ['imagex_direct_fallback']`.
- Removed the direct ImageX正文图 fallback. Local正文图片 that cannot be converted to a Dongchedi `spice` URL are now removed from `content` instead of failing early or submitting a rejected ImageX URL.
- After a no-body-image retry still returned `server exception`, changed `extra.content_word_cnt` from the reference extension's hardcoded `267` to the actual text count of the final submitted HTML.
- Added `content_word_cnt` and `computed_text_count` to Dongchedi rejection diagnostics.
- Expanded rejection diagnostics with body image host summaries, body image upload strategies, tag/entity counts, content end excerpt, and cover upload dimensions/hosts.
- Verification:
- `pnpm --filter @geo/desktop-client typecheck` passed.
- `pnpm --filter @geo/desktop-client test -- dongchedi` passed.
- `pnpm --filter @geo/desktop-client test` passed.
- `pnpm --filter @geo/desktop-client build` passed and refreshed `out/main/bootstrap.cjs`.
- `git diff --check` passed.
## 5-Question Reboot Check
| Question | Answer |
|----------|--------|
| Where am I? | Phase 9 complete |
| Where am I going? | Next useful slice is turning the placeholder routes into real pages, starting with template creation and brand management |
| What's the goal? | Resume the interrupted task by adding the missing `admin-web` frontend foundation on top of the finished backend |
| What have I learned? | The tenant backend is stable enough to support a real login + dashboard frontend slice, and a real browser pass was valuable because it caught a submit wiring bug that static checks missed |
| What have I done? | Added the pnpm frontend workspace, built `apps/admin-web`, wired auth and dashboard data to `tenant-api`, reduced the bundle size, and passed install/build/preview plus browser login verification |