- 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.
-`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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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`.
-`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.
- 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.
| 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 |
| 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 |
| 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 |
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
### 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.
- 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.
- Added `prometheus/client_golang` and replaced custom hand-written Prometheus text builders with `prometheus.Collector` implementations plus `promhttp`.
- 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.
-`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.
- 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.
-`pnpm --filter @geo/desktop-client typecheck` failed before checking runtime changes because `electron.vite.config.ts` has a Vite 5/8 plugin type mismatch.
- 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 ` ` 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.
| 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 |
- Investigated the "普通通用模版" cards shown in the screenshot.
- Confirmed the four platform templates are stored in `article_templates` and currently seeded by `server/cmd/dev-seed/main.go`.
- Confirmed production deployment jobs only run database migrations, so a fresh environment can miss the four template records unless dev seed is run manually.
- Decision: add a production-safe platform-template initializer and wire it into the migrate job/image instead of running the broad dev seed path.
- Kept `deploy-config-ci.yml` validation-only after the user clarified it should not own images.
- Scoped `frontend-ci.yml` to `admin-web` and `ops-web` plus shared packages/Dockerfiles; removed browser-extension typecheck from this workflow.
- Added Gitea Registry publishing to frontend/backend CI with commit8 tags and skip-if-tag-exists behavior through `deploy/scripts/gitea-registry-images.sh`.
- Added commit8 image verification to compose/k3s/offline deployment paths so CD fails if CI has not produced the required image.
- Changed offline package defaults to use commit8 instead of tag names and to pull business images from the Gitea Registry.
- Verification passed: workflow YAML parse, deploy script `bash -n`, and `sh -n` over all workflow run blocks.
- Confirmed the existing repository topology uses application-side `pgxpool` and direct Postgres service names; no PgBouncer service/config existed before this change.
- Started Phase 40 to add stable PgBouncer deployment with session pooling, app runtime traffic through PgBouncer, and migration jobs remaining direct to Postgres.
- Added `pgbouncer` and `monitoring-pgbouncer` to production Docker Compose and k3s using `edoburu/pgbouncer:v1.24.1-p1`, `POOL_MODE=session`, and `AUTH_TYPE=scram-sha-256`.
- Changed runtime app configs to target PgBouncer while keeping Docker/k3s migration jobs direct to `postgres` / `monitoring-postgres`.
- Made deployment passwords flow from `POSTGRES_PASSWORD` / `OPS_POSTGRES_PASSWORD` placeholders and changed `DatabaseConfig.DSN()` to URL-escape credentials for stronger production passwords.
- Included PgBouncer in offline packaging and docs.