diff --git a/README.md b/README.md index c18bdb4..d7d1476 100644 --- a/README.md +++ b/README.md @@ -1,271 +1,249 @@ # JobPilotAI -JobPilotAI 是一个基于 C++ 高性能后端架构的 AI 求职自动化平台:公开来源采集 + -人工采集 + 岗位导入/检索 + 投递管理 + AI 匹配。 +[English](README.md) | [简体中文](README.zh-CN.md) -> 本页只描述**当前真实能力**。未实现的部分在「未实现 / 非目标」一节明确列出。 +JobPilotAI is a self-hosted job-search workspace: Python source adapters collect +public job postings, a C++17 backend searches and ranks them, explains matches +deterministically, and tracks applications. An optional AI worker provides +task-based assistance; the core workflow does not require an AI provider or API +key. -## Quick Start (English) +| Job detail with an explainable match | Application status board | +|---|---| +| ![Job detail showing a rule-based match score and reasons](docs/images/web-detail.png) | ![Application status board with a resume upload and tracked applications](docs/images/web-status.png) | + +The screenshots show the local Web UI with synthetic demo data. The current UI +labels are in Chinese. -JobPilotAI runs locally with MySQL and Redis only, and **no AI provider key is -required**: deterministic `rule_v1` matching and `tailor_rule_v1` drafting work -without one, and a provider is optional and bring-your-own-key. +## Quick Start (English) -Prerequisites: Docker with the compose plugin, CMake ≥ 3.22, a C++17 compiler, -`pkg-config` plus the MySQL client development files, and Python 3 with pip (for -Conan 2). `tools/dev/bootstrap.sh` checks all of them and names whatever is -missing. +Prerequisites: Docker Engine with Compose v2, `curl`, and Python 3. The first +run builds the backend image, so Docker must be able to pull its base images and +complete that build. ```bash git clone https://github.com/cjs1102/JobPilotAI.git cd JobPilotAI - -tools/dev/bootstrap.sh # resolves the pinned Conan graph (service/conan.lock) -cmake --preset release # debug / asan / ubsan / tsan also work -cmake --build --preset release -j"$(nproc)" - -cp .env.example .env # fill MYSQL_ROOT_PASSWORD and MYSQL_PASSWORD -docker compose up -d # MySQL 8.0 + Redis 7 - -docker compose --profile app build backend # the service image (needs a registry) -docker compose --profile app up -d # ... now also the service on :8080 -curl -fsS http://127.0.0.1:8080/health - -tools/dev/demo-gate.sh # end-to-end gate: import -> search -> match -> tailor -> track +./tools/dev/demo-up.sh ``` -- The binary is `build/presets/release/service/jobpilot_service` (root entry) or - `service/build/presets/release/jobpilot_service` (service entry). -- `tools/dev/demo-gate.sh` starts an isolated compose project of its own — own - container names, host ports, volumes and database — and tears it down - afterwards, so it never touches an existing stack or its data. -- Database bootstrap and upgrades: [`database/schema/README.md`](database/schema/README.md). -- Contributors and AI agents: start at [`AGENTS.md`](AGENTS.md), then - [`docs/ai/`](docs/ai/) (workflow rules, current state) and the executed plans - under [`docs/exec-plans/`](docs/exec-plans/); [`docs/README.md`](docs/README.md) - is a short reading order. - -## Community +The launcher: -- Security policy and private reporting: [`SECURITY.md`](SECURITY.md). -- Build, test, PR and review workflow: [`CONTRIBUTING.md`](CONTRIBUTING.md). -- Code of Conduct is deferred until a reporting channel is available; see - `CONTRIBUTING.md`. +- checks Docker, Compose, `curl`, and Python; +- creates a `.env` with random MySQL and internal-token secrets only when + `.env` is missing, writes it with mode `0600`, and never prints the secret + values; an existing `.env` is left untouched; +- starts MySQL 8 and Redis 7, builds and starts the backend, and serves the Web + UI on port `8081` — through nginx when `nginx:1.27-alpine` is available, or + through the bundled Python static/proxy server otherwise; +- imports 12 synthetic jobs by default and prints a line such as + `DEMO_READY url=http://127.0.0.1:8081`. -## 最小 Web UI(0022) +Open the printed URL. The default backend is on `http://127.0.0.1:8080`. To +start without the sample jobs, pass `--no-seed`. Ports and the Compose project +name can be overridden with `DEMO_API_PORT`, `DEMO_WEB_PORT`, +`DEMO_MYSQL_PORT`, `DEMO_REDIS_PORT`, and `DEMO_PROJECT`. -`web/` is a native HTML/CSS/JS UI with no build step. The optional `web` compose -profile serves it with nginx and reverse-proxies `/api/*` and `/health` to the -existing C++ service (same origin, no CORS, P1 untouched): +Stop the stack with: ```bash -cp .env.example .env -docker compose up -d -docker compose --profile app build backend -docker compose --profile app --profile web up -d -# open http://127.0.0.1:8081 +docker compose --env-file .env -p jobpilot-demo --profile app --profile web down ``` -Flow: register/login → upload a resume (required before favourite) → search or -recommended jobs → explainable match → detail with original/apply links → -favourite → application status board. The UI never submits an application; it -only opens the original page for manual application. +Add `-v` only if you also want to delete the demo database and uploaded files. -If the host cannot pull `nginx:1.27-alpine`, the documented fallback runner -serves the same `web/` files and proxies the same paths: +### Build from source (optional) + +The one-command demo uses the containerized backend. To build and run the C++ +service directly, install a C++17 toolchain, CMake, `pkg-config`, and the MySQL +client development files, then: ```bash -python3 tools/dev/web_static_proxy.py \ - --root web --port 8081 --backend http://127.0.0.1:8080 +tools/dev/bootstrap.sh # resolves the pinned Conan graph +cmake --preset release +cmake --build --preset release -j"$(nproc)" ``` -Full walkthrough, troubleshooting and screenshot steps: -[`docs/guides/local-demo.md`](docs/guides/local-demo.md). Isolated smoke: -`tools/dev/web-smoke.sh` (prints `WEB_SMOKE_OK`). - -## 当前能力 - -### C++ 服务(`service/`,P1 网络层行为冻结) - -- 用户与鉴权:注册、登录、Redis 会话、`Authorization` 鉴权、`me`。 -- 画像与技能:登录用户的画像读写、技能增删改查。 -- 简历:上传、分页列表、文本版本生成、版本列表。 -- 岗位读取:公开岗位搜索与分页、岗位详情与技能要求、`rule_v1` 确定性推荐排名 - (薪资格式/技能硬过滤、分数明细、稳定分页)。 -- 申请链接(0020):列表、推荐、详情新增 `apply_url`(string|null)。仅当来源 - **显式提供**时非空(Lever/Ashby 的 `applyUrl`、nowcoder 原生岗的 - `redirectExternalUrl`);Greenhouse 与 nowcoder 聚合岗为 `null`,不从 - `source_url` 推导/拼接。 -- 投递:创建/列表/状态/统计,岗位与简历版本校验、重复投递保护、状态机。 -- 岗位导入(内部):`POST /internal/jobs/import`,token 保护、`source_url` 幂等 - upsert、事务内整组替换技能要求,批次 `1..100`。 -- 来源观测(内部):`POST /internal/jobs/source-sync`,`complete` 完整性语义、 - 消失宽限与重新出现、freshness 可见性(`fresh`/`aging`/`stale`/`closed`)。 -- AI 匹配(0013):公开 `POST /api/v1/ai/match` + `GET /api/v1/ai/tasks/{id}`, - token 保护的 `/internal/ai/tasks/*`,确定性 `match_rule_v1` 分数永不被告覆写。 -- 岗位列表可用性与入库质量(0015A):公开岗位搜索默认只返回 `clean` - (`AND j.review_status = 0`),`needs_review` 只进入内部 token 保护的复核面 - (`GET /internal/jobs/review-queue`、`POST /internal/jobs/review/{id}`、 - `POST /internal/jobs/review/backfill`);列表响应新增 `source_url`,并支持 - `education_max` / `job_nature` / `deadline_before` / `deadline_after` 过滤与 - `sort`(`created_desc` / `deadline_asc` / `education_asc`);描述长度按 UTF-8 - 字节硬上限 48 KiB,超限走可诊断的拒绝/复核,绝不静默截断。 -- 画像候选数据(0015 Phase A):`profiles.education_level` / `graduation_year` / - `campus_status` 三个可空列(NULL = 未知,无回填);简历 PDF 文本状态是**派生**值 - (`ok` / `text_empty` / `pdf_no_text` / `text_unavailable`),不新增列。 -- 可解释匹配(0015 Phase B):`rule_v1` 权重与分数口径不变; - `GET /api/v1/jobs?with_match=true` 的每行追加 7 个可加性键(`score` / - `score_version` / `matched_skills` / `missing_skills` / `reasons[]` / - `deal_breakers[]` / `deadline_urgency`),`GET /api/v1/jobs/recommended` 同样给出 - 这些匹配解释键;`with_match` 缺省时列表响应与旧版**逐字节一致**;`strict=true` 在 - 列表自身排序窗口内分页,`total` 为过滤后数量,窗口触顶时 `truncated=true`。 -- 简历定制产物(0015 Phase D):`tailor_rule_v1` 确定性中文 ATS 纯文本草稿 - (每条事实带来源标注,无法推导处是 `【请填写:…】`);机械事实门 - (`fact_check_status=2` 阻止确认);需登录的 `POST`/`GET /api/v1/resumes/artifacts`、 - `GET /api/v1/resumes/artifacts/{id}`、`POST /api/v1/resumes/artifacts/{id}/confirm`; - `confirmed_at IS NULL` = 草稿,确认只是状态变更。 - -### Python `source_adapters/` - -- **公开来源适配器(0012 / 0018)**:Greenhouse / Lever / Ashby,以及 - **nowcoder(牛客校招,0018)**,fixture 驱动,通过 - `POST /internal/jobs/source-sync` 发布观测;适配器从不直连数据库。 -- **nowcoder(0018)**:读取 `https://www.nowcoder.com/jobs/school/jobs` 的 - 免登录 SSR 列表(robots 允许 `/jobs/*`),支持 `` / `|` - 多板配置,板间最小间隔 5 秒;单次查询最多 20 条、无翻页、顺序有抖动,因此 - **始终 `complete=false`**——这是有界观测窗口,不是全量扫描。两种记录形态 - (原生岗 `ext` JSON、内推聚合岗 HTML description)均已归一化;不再依赖 - “聚合详情页无 SSR”的旧判断。来源登记与 2026-10-19 证据见 - [`source_adapters/README.md`](source_adapters/README.md)。 -- **0014 采集框架**:robots.txt 逐路径判定(fail-closed)、Crawl-delay 下限、 - 有界重试与退避、每次请求超时、诚实 User-Agent、charset 优先级 - (BOM > header > meta > utf-8)、HTML/JSON-LD/RSS/sitemap/JSON 解析、SSRF 守卫 - (含重定向目标与 DNS 复查)、响应体积上限、总请求预算与页数上限 - (触顶即 `complete=false`)、薪资只发布「月薪千元」可确定形态、F13 北京时间 - 截止时间归一化。 -- **人工采集(0014 Step 6,可用)**:浏览器书签小工具只读取当前已登录页面 - (无凭据、不发任何网络请求)→ 剪贴板 JSON;本机 publisher - `python -m jobpilot_sources publish-capture` 复用既有 - `POST /internal/jobs/import`(`source="manual"`),失败可诊断且绝不误报成功。 - -### Python 离线工具(`tools/`,仅标准库) - -- **只读岗位匹配报告(0015 Phase C,可选)**:`tools/job_report/render_report.py` - 只渲染公开接口**已经返回**的内容——不重新打分、不重新排序、不补数据、不写数据库、 - 不新增服务端接口——输出**单个自包含 HTML**(内联 CSS、无外部资源、无脚本);行序 = - API 返回顺序 + 位置 `rank`,每行保留原始岗位链接,没有 `reasons[]` 的行显示显式标记 - `无匹配理由`。`--input` 模式零网络 I/O,`--url` 模式只发一次带既有 Bearer token 的 - `GET`。用法见 [`tools/job_report/README.md`](tools/job_report/README.md)。 -- **禁止投递路径检查(0015 Phase D)**:`tools/checks/no_submission_path.py` - 全树扫描自动投递/自动提交类标识符与语义化路由,退出码 0 = 未发现(1 = 有发现)。 - -### 0014 关键契约(已实现并有测试) - -- **F13 截止时间**:中国招聘截止时间按北京时间(`Asia/Shanghai`,固定 UTC+8) - 解释,绝不用服务器本地时区;来源只给日期时按「北京正午 → UTC `04:00:00`」 - 存储并置 `deadline_date_only=true`,读回按北京日期渲染(不渲染成正午到期时刻); - 来源给了具体时刻则保留真实瞬时;类型未知保持 `NULL` 不臆造。权威约定见 - [`docs/03-database-design.md`](docs/03-database-design.md) §9.1。 -- **F14 人工优先**:`source-sync` 观测命中 `source='manual'` 行时整条跳过 - (不改任何列、不改 `source`、不动 freshness、不替换技能要求、不关闭), - 并在响应里给出 `skipped_manual` / `skipped_manual_urls`。 -- **既有库迁移**:`jobs` 新增 6 列(全部可空、无回填,含 - `deadline_date_only`)+ 0 索引;`docs/03` §9.4 的 ALTER 已在 pre-0014 库上实跑并 - 用同一套 schema 断言复验。 - -## 未实现 / 非目标(不要误读为已具备) - -- **国内来源只有 nowcoder(0018)一个,且是有界窗口**。0014 Step 5 的国聘/清华 - 结论仍是环境受限:国聘岗位列表只能通过客户端签名访问;清华列表主机 - `career.cic.tsinghua.edu.cn` 的 `robots.txt` 持续 503。0018 另行复核了牛客 - `/jobs/school/jobs`(免登录、robots 允许、SSR),并将其注册为 `nowcoder`; - 但单次最多 20 条、无翻页、顺序抖动,**`complete=false` 是强制的**,不得描述为 - “全量扫描”。其他国内来源仍未注册;当前 allowlist 为 - `greenhouse` / `lever` / `ashby` / `nowcoder`。证据: - [`source_adapters/README.md`](source_adapters/README.md) 的「来源登记表」、 - [`cn_source_probe_2026-10-09.json`](source_adapters/jobpilot_sources/tests/fixtures/cn_source_probe_2026-10-09.json) - 与 - [`cn_source_probe_2026-10-19.json`](source_adapters/jobpilot_sources/tests/fixtures/cn_source_probe_2026-10-19.json)。 -- **不自动化登录墙**:不做自动登录、绕 CAPTCHA、伪造 cookie/UA、代理池;登录墙岗位 - 只允许人工采集(书签小工具)。 -- **不做通用爬虫/自动投递**:没有通用 HTML 爬虫、没有自动投递、没有 RabbitMQ / - Gateway / SaaS RBAC。0015 的定制简历产物同样只有草稿与人工确认,**没有任何投递/ - 发送端点**(全树检查:`tools/checks/no_submission_path.py`)。 -- **0015 不做 LLM 措辞润色**:D6(ai_worker 的 `resume_optimize` 措辞通道)按所有者 - 决定延后(0015 计划 §17),`generator='rule_v1'` / `source_version='tailor_rule_v1'` - 是唯一生成器,不需要 AI key。 -- **截止时间不驱动生命周期**:`deadline` 只是展示字段,本阶段没有「过期自动关闭」 - 或过期提示;过期的岗位仍然可见(已知可见性限制)。 -- **书签小工具的发布链路已在真实采集样本上完成端到端验收(审核路径)**。离线测试(node + - fixture)覆盖提取逻辑;在真实登录墙岗位页(Edge)运行可生成 JSON,且未观察到书签发起的网络 - 请求——该请求侧观察是**一次运行的结果**,不构成形式化安全证明,「无网络请求、无凭据」 - 以代码与离线测试为准。`tools/dev/capture-acceptance.sh` 现在覆盖两条路径:干净样本必须 - 变成公开可见的岗位;被 0015A 预检路由到审核队列的真实样本则在隔离栈上完成 - dry-run → 导入 → 审核队列 → 数据库 → 公共可见性逐项断言(11 项,`CAPTURE_ACCEPTANCE_OK`), - 并且**不会**出现在公共列表、搜索或按 id 详情里。 - 该真实样本仍在审核队列中(`resolution=open`):它的 `company` 取到的是平台名、 - `description` 混入了导航/推荐岗位/页脚,因此需要人工复核后再决定是否公开——这类未经清洗的 - 真实岗位不应被当作可直接展示的数据。验证范围与限制见 - [`docs/exec-plans/completed/0014-china-campus-sources-and-manual-capture.md`](docs/exec-plans/completed/0014-china-campus-sources-and-manual-capture.md) - §19.6。 - -## 技术栈 - -### Backend - -- C++17、CMake、Conan - -### Infrastructure - -- MySQL 8.0、Redis 7、Docker Compose - -### Libraries - -- spdlog、yaml-cpp、nlohmann/json、hiredis、GoogleTest -- Python 侧(`source_adapters/`、`ai_worker/`):标准库 + PyYAML;`tools/` 仅标准库 - -## 项目结构 - -```text -JobPilotAI -├── backend/ # jobpilot_core:网络/HTTP、config/logging、DB、cache -├── service/ # jobpilot_service_core + 可运行服务与测试 -│ ├── config/config.yaml -│ ├── include/jobpilot/service/ -│ ├── src/ -│ └── tests/ -├── source_adapters/ # Python 采集:公开来源适配器 + 0014 框架 + 人工采集 -├── ai_worker/ # 0013 可选 AI worker(Python) -├── tools/ # 离线工具:0015 Phase C 只读岗位报表 + 禁止投递路径检查 -├── database/schema/ # MySQL 初始化脚本 -├── docs/ # 03 数据库 / 04 API / ADR / ExecPlan / ai agent 规则 -│ └── guides/local-demo.md # 0022 本地 Web UI 演示与故障排查 -├── web/ # 0022 原生 HTML/CSS/JS UI(无构建步骤) -├── docker/ # 0022 nginx 静态托管配置 -├── docker-compose.yml -├── CMakeLists.txt # 统一开发者入口(root preset) -├── CMakePresets.json -└── .env.example -``` +See [`docs/quality/README.md`](docs/quality/README.md) for presets, test levels, +and sanitizer runs. The binary is +`build/presets/release/service/jobpilot_service`. -## 构建与验证 +### Manual setup and the nginx fallback -依赖先由 `tools/dev/bootstrap.sh` 解析(完整流程见上面的英文 Quick Start): +If you prefer to manage the services yourself: ```bash -tools/dev/bootstrap.sh # 生成 service/build/conan_toolchain.cmake(已忽略提交) -cmake --preset debug && cmake --build --preset debug -ctest --preset release -L job # 受影响标签;L4 见 docs/ai/agent-workflow.md +cp .env.example .env # set MYSQL_ROOT_PASSWORD and MYSQL_PASSWORD +docker compose up -d # MySQL + Redis +docker compose --profile app build backend +docker compose --profile app --profile web up -d +``` -# 0015 Phase C 只读报表(标准库) -python3 -m unittest discover -s tools/job_report/tests -v +If `nginx:1.27-alpine` cannot be pulled, the same `web/` files can be served by +the Python standard-library runner: -# Python 采集与人工采集 -cd source_adapters && python3 -m unittest discover -s jobpilot_sources/tests -t . -python3 -m jobpilot_sources capture-bookmarklet --html # 人工采集小工具 +```bash +python3 tools/dev/web_static_proxy.py \ + --root web --port 8081 --backend http://127.0.0.1:8080 ``` -- 验证等级与何时测试:[`docs/ai/agent-workflow.md`](docs/ai/agent-workflow.md)。 -- 数据库契约:[`docs/03-database-design.md`](docs/03-database-design.md)。 -- API 契约:[`docs/04-api-design.md`](docs/04-api-design.md)。 -- 当前阶段与下一步:[`docs/ai/project-state.md`](docs/ai/project-state.md)。 +`tools/dev/demo-up.sh` performs this fallback automatically. See the +[local demo guide](docs/guides/local-demo.md) for troubleshooting. + +## Try the demo + +1. Register a local account. +2. Open the application status board, go to resume setup, and upload a PDF, TXT, + or Markdown resume. A resume version is required before a job can be + favourited. +3. Open the jobs view and search for `C++`; try the location, salary, education, + job-nature, and sort filters. +4. Open a result and read the match score, matched and missing skills, + `reasons`, and `deal_breakers`. Matching is deterministic and needs no AI + key. +5. Favourite the job. On the status board, move it through the application state + machine (favourite, to apply, applied, written test, interviews, offer) or + mark it rejected. +6. Use the apply-link button when an apply URL exists, otherwise use the + original-announcement button. These links open the original page in a new + tab; JobPilotAI does not fill in or submit an application. + +The demo user, resume, and jobs are local fixtures. Do not use real credentials +or personal resumes in a shared environment. + +## How to use + +### Collect or import jobs + +JobPilotAI reads public, login-free sources through Python source adapters: +Greenhouse, Lever, Ashby, and nowcoder for China campus listings. Adapters +publish observations through the token-protected internal API and never write +to the database directly. A manual-capture bookmarklet is available for pages +behind a login wall; it reads the page the user already has open and does not +send credentials or make network requests. A manual import endpoint also +accepts a JSON batch. + +The source adapters are documented in +[`source_adapters/README.md`](source_adapters/README.md), including the source +register, robots handling, and per-source limits. Treat a `complete=false` +observation as a bounded sample, not a full crawl. + +### Search, filter, and rank + +The public job list supports pagination, keyword search, salary and skill +filters, location and education filters, deadline filters, and sorting. Signed-in +users can request the recommended view. Only jobs that passed the intake quality +gate are visible publicly; rows that need review stay behind the internal +review surface. + +### Read the match explanation + +`rule_v1` is a deterministic ranking and explanation model. It returns a score, +matched and missing skills, additive `reasons`, `deal_breakers`, and deadline +urgency. There is no hidden LLM step in this path. The AI task pipeline is +optional and cannot overwrite the deterministic score. + +### Prepare a resume draft + +The resume tailor produces a deterministic plain-text ATS draft with source +annotations for each factual claim; unknown facts remain explicit placeholders. +A mechanical fact gate must pass before a draft can be confirmed. This is a +draft workflow, not an automatic application generator. + +### Track applications and apply manually + +Favourites and application status changes are recorded in the local database. +The backend enforces the status state machine. When it is time to apply, open +the original posting or the explicit apply link and complete the application in +the browser. There is no auto-submit, form-filling, or notification path. + +All demo data stays in local Docker volumes. Network access happens only when +you run a source adapter or configure an optional AI provider. + +## What's included + +- **Source ingestion** — public ATS adapters, a nowcoder campus source, manual + capture, JSON import, source observations, freshness, and an intake quality + gate. +- **Search and ranking** — keyword and structured filters, stable pagination, a + recommended view, and deterministic `rule_v1` scoring. +- **Explainable matching** — scores, matched and missing skills, reasons, + deal-breakers, and deadline urgency on the job list and detail pages. +- **Resume tailoring** — deterministic ATS drafts with source annotations and a + fact-check gate. +- **Application tracking** — favourites, a validated status state machine, + statistics, and a local status board. +- **Local Web UI** — native HTML/CSS/JavaScript with no build step, same-origin + `/api` and `/health` proxying, and a stdlib fallback when nginx is unavailable. +- **Optional AI worker** — a Python worker for task-based assistance; the core + product degrades gracefully without a provider key. +- **Operational tooling** — Docker Compose, a committed container image + definition, CI, sanitizer presets, benchmark harnesses, a release preflight, + and a check that no submission path exists. + +## Verification + +The release-readiness baseline recorded on 2026-10-21: + +- **312 C++ tests** through CTest. The Debug, ASan, and UBSan runs each + completed `312/312` with zero failures and zero sanitizer diagnostics. One + review-backfill test is skipped on the developer database and passes on the + isolated scratch database. +- **TSan**: 52/52 harness tests and 17/17 external-runtime tests, with zero + diagnostics. TSan does not cover the entire suite. +- **Python**: 481 source-adapter tests and 190 AI-worker tests pass in their + pinned environment. +- **CI**: the `ci` and `image` workflows were green on `main` after the + release-readiness merge. The image workflow builds the committed Dockerfile + and runs an end-to-end demo gate. +- **Benchmark** on a 4-vCPU host: 13.4–16.1k requests/second on `/health` and + 180–194 requests/second on the database-backed jobs endpoint. These are + host-specific measurements, not a service-level objective or a pass/fail + threshold. Environment and raw results are in + [`docs/benchmarks/README.md`](docs/benchmarks/README.md). + +## Limits and non-goals + +- **China source coverage is narrow.** nowcoder is the only registered China + campus source. It reads a bounded login-free list: at most 20 rows per query, + no pagination, and always `complete=false`. Other China sources are not + registered. +- **Some postings have no apply URL.** Greenhouse and nowcoder aggregated rows + do not expose a source-provided `apply_url`; the UI falls back to the original + announcement. It never derives or guesses an apply URL. Moka has not been + verified and is not registered. +- **Login walls are not bypassed.** There is no automatic login, CAPTCHA + bypass, cookie or user-agent spoofing, or proxy pool. Login-walled jobs use + manual capture only. +- **No automatic application.** There is no auto-apply, auto-submit, form + filling, or notification path. There is also no general-purpose crawler, + message queue, API gateway, or SaaS RBAC. +- **Deadlines are display-only.** An expired deadline does not automatically + close a job. +- **The UI is a local demo.** Authentication uses HTTP and browser + `localStorage`, not production authentication or TLS. +- **No legal conclusion.** Source terms, robots directives, and data-use rules + remain the operator's responsibility. + +## Documentation + +- [Documentation map](docs/README.md) and + [project documentation index](docs/PROJECT_DOCUMENTATION_INDEX.md) +- [Local Web UI guide](docs/guides/local-demo.md) +- [Architecture](docs/architecture/open-source-mvp.md) and + [architecture decisions](docs/adr/README.md) +- [API contract](docs/04-api-design.md) and + [database contract](docs/03-database-design.md) +- [Source adapters](source_adapters/README.md) and + [benchmarks](docs/benchmarks/README.md) + +## Contributing and security + +Read [`CONTRIBUTING.md`](CONTRIBUTING.md) before opening a pull request. Report +security issues privately as described in [`SECURITY.md`](SECURITY.md); do not +open a public issue for a vulnerability. A code of conduct is deferred until a +reporting channel is available. + +## License + +MIT. See [`LICENSE`](LICENSE) and [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md). diff --git a/README.zh-CN.md b/README.zh-CN.md new file mode 100644 index 0000000..46e92b1 --- /dev/null +++ b/README.zh-CN.md @@ -0,0 +1,211 @@ +# JobPilotAI + +[English](README.md) | 简体中文 + +JobPilotAI 是一个自托管的求职工作台:Python 来源适配器采集公开招聘信息, +C++17 后端负责检索、排序、确定性匹配解释和投递跟踪。可选的 AI Worker 提供 +任务型辅助;核心流程不需要配置 AI 服务商或 API Key。 + +| 带可解释匹配的岗位详情 | 申请状态板 | +|---|---| +| ![岗位详情,展示基于规则的匹配分数与理由](docs/images/web-detail.png) | ![申请状态板,包含简历上传与投递跟踪](docs/images/web-status.png) | + +截图展示的是使用合成演示数据的本地 Web UI,当前界面文案为中文。 + +## 快速开始 + +前置条件:Docker Engine 与 Compose v2、`curl`、Python 3。首次运行会构建后端 +镜像,Docker 需要能够拉取基础镜像并完成构建。 + +```bash +git clone https://github.com/cjs1102/JobPilotAI.git +cd JobPilotAI +./tools/dev/demo-up.sh +``` + +启动脚本会: + +- 检查 Docker、Compose、`curl` 和 Python; +- 仅在 `.env` 不存在时生成包含随机 MySQL 密码和内部令牌的 `.env`,权限设为 + `0600`,且不打印任何密钥值;已存在的 `.env` 不会被修改; +- 启动 MySQL 8 和 Redis 7,构建并启动后端,在 `8081` 端口提供 Web UI:可用 + `nginx:1.27-alpine` 时使用 nginx,否则使用仓库内置的 Python 静态文件/反向代理 + 服务; +- 默认导入 12 条合成岗位,并打印类似 + `DEMO_READY url=http://127.0.0.1:8081` 的地址。 + +打开打印出的地址即可。后端默认位于 `http://127.0.0.1:8080`。加上 `--no-seed` +可以不导入示例岗位。可通过 `DEMO_API_PORT`、`DEMO_WEB_PORT`、`DEMO_MYSQL_PORT`、 +`DEMO_REDIS_PORT` 和 `DEMO_PROJECT` 覆盖端口与 Compose 项目名。 + +停止演示环境: + +```bash +docker compose --env-file .env -p jobpilot-demo --profile app --profile web down +``` + +只有在需要同时删除演示数据库和上传文件时,才加 `-v`。 + +### 从源码构建(可选) + +一条命令演示使用的是容器化后端。如果需要直接构建并运行 C++ 服务,请先安装 +C++17 工具链、CMake、`pkg-config` 和 MySQL 客户端开发文件,然后执行: + +```bash +tools/dev/bootstrap.sh # 解析固定的 Conan 依赖图 +cmake --preset release +cmake --build --preset release -j"$(nproc)" +``` + +Preset、测试等级和 Sanitizer 运行方式见 +[`docs/quality/README.md`](docs/quality/README.md)。二进制位于 +`build/presets/release/service/jobpilot_service`。 + +### 手动启动与 nginx 回退 + +如果希望自行管理各服务: + +```bash +cp .env.example .env # 填写 MYSQL_ROOT_PASSWORD 和 MYSQL_PASSWORD +docker compose up -d # MySQL + Redis +docker compose --profile app build backend +docker compose --profile app --profile web up -d +``` + +如果无法拉取 `nginx:1.27-alpine`,可以用 Python 标准库运行器提供同一套 `web/` +文件: + +```bash +python3 tools/dev/web_static_proxy.py \ + --root web --port 8081 --backend http://127.0.0.1:8080 +``` + +`tools/dev/demo-up.sh` 会自动执行这一回退。故障排查见 +[本地演示指南](docs/guides/local-demo.md)。 + +## 体验演示 + +1. 注册一个本地账号。 +2. 打开 **申请状态板 → 简历设置**,上传 PDF、TXT 或 Markdown 简历。收藏岗位前 + 必须先有一个简历版本。 +3. 打开 **岗位**,搜索 `C++`,并尝试地点、薪资、学历、岗位性质和排序筛选。 +4. 打开一个岗位,查看匹配分数、命中与缺失技能、`reasons` 和 + `deal_breakers`。匹配是确定性的,不需要 AI Key。 +5. 收藏该岗位。在 **申请状态板** 中按后端状态机推进:收藏、待投递、已投递、 + 笔试、面试、Offer,或标记为拒绝。 +6. 有申请链接时点击 **打开申请链接**,否则点击 **打开原始公告**。链接只会在新 + 标签页打开原始页面,JobPilotAI 不会代填或提交申请。 + +演示用户、简历和岗位均为本地合成数据。不要在共享环境中使用真实凭据或个人简历。 + +## 使用方式 + +### 采集或导入岗位 + +JobPilotAI 通过 Python 来源适配器读取公开、免登录的来源:Greenhouse、Lever、 +Ashby,以及面向国内校招的 nowcoder。适配器通过受内部令牌保护的接口发布观测, +不直接写数据库。对于登录墙后的页面,可以使用人工采集书签小工具;它只读取用户 +已经打开的页面,不发送凭据,也不发起网络请求。此外还提供 JSON 批量导入接口。 + +来源适配器、来源登记表和每个来源的限制见 +[`source_adapters/README.md`](source_adapters/README.md)。`complete=false` 的 +观测应视为有界样本,而不是全量抓取。 + +### 搜索、筛选与排序 + +公开岗位列表支持分页、关键词搜索、薪资与技能筛选、地点与学历筛选、截止时间 +筛选和排序。登录用户可以查看推荐视图。只有通过入库质量门禁的岗位才会公开可见; +需要复核的记录保留在内部复核界面。 + +### 阅读匹配解释 + +`rule_v1` 是确定性的排序与解释模型,返回分数、命中与缺失技能、可加性的 +`reasons`、`deal_breakers` 和截止时间紧迫度。该路径没有隐藏的大模型步骤。 +AI 任务管线是可选的,且不能覆盖确定性分数。 + +### 生成简历草稿 + +简历定制功能会生成确定性的纯文本 ATS 草稿,每一条事实都带来源标注;无法推导 +的事实保留为显式占位符。草稿必须通过机械事实门禁才能确认。这是草稿工作流, +不是自动投递生成器。 + +### 跟踪投递并手动申请 + +收藏和投递状态变更会记录在本地数据库,后端会校验状态机。需要申请时,打开原始 +公告或显式提供的申请链接,在浏览器中手动完成申请。系统没有自动提交、代填表单 +或通知路径。 + +所有演示数据都保存在本地 Docker 卷中。只有运行来源适配器或配置可选 AI 服务商时 +才会发生网络访问。 + +## 已包含的能力 + +- **来源采集**:公开 ATS 适配器、nowcoder 校招来源、人工采集、JSON 导入、来源 + 观测、时效性与入库质量门禁。 +- **搜索与排序**:关键词与结构化筛选、稳定分页、推荐视图,以及确定性 + `rule_v1` 评分。 +- **可解释匹配**:列表与详情中的分数、命中与缺失技能、理由、否决项和截止时间 + 紧迫度。 +- **简历定制**:带来源标注和事实校验的确定性 ATS 草稿。 +- **投递跟踪**:收藏、经后端校验的状态机、统计信息和本地状态板。 +- **本地 Web UI**:原生 HTML/CSS/JavaScript,无构建步骤;同源代理 `/api` 和 + `/health`,nginx 不可用时自动回退到标准库服务。 +- **可选 AI Worker**:Python 任务型辅助;没有服务商密钥时核心产品仍可正常使用。 +- **工程与运维工具**:Docker Compose、已提交的容器镜像定义、CI、Sanitizer + Preset、基准测试、发布预检,以及禁止自动投递路径的检查。 + +## 验证情况 + +2026-10-21 记录的发布准备基线: + +- **312 个 C++ 测试**(通过 CTest)。Debug、ASan、UBSan 各完成 `312/312`, + 无失败且没有 Sanitizer 诊断。一个复核回填测试在开发数据库上跳过,在隔离的 + 临时数据库上通过。 +- **TSan**:测试框架 52/52,外部运行时 17/17,均无诊断。TSan 不覆盖完整测试集。 +- **Python**:来源适配器 481 个测试、AI Worker 190 个测试,在固定依赖环境中 + 全部通过。 +- **CI**:发布准备合并后,`main` 上的 `ci` 和 `image` 工作流均为绿色。镜像 + 工作流会构建已提交的 Dockerfile,并运行端到端演示门禁。 +- **基准测试**:在 4 vCPU 主机上,`/health` 为 13.4–16.1k 请求/秒,数据库支撑的 + 岗位接口为 180–194 请求/秒。这些是特定主机上的测量值,不是服务等级目标,也 + 不是通过/失败阈值。环境说明与原始结果见 + [`docs/benchmarks/README.md`](docs/benchmarks/README.md)。 + +## 限制与非目标 + +- **国内来源覆盖范围有限。** nowcoder 是目前唯一注册的国内校招来源,只读取 + 免登录的有界列表:单次查询最多 20 条,不翻页,且始终 `complete=false`。 + 其他国内来源尚未注册。 +- **部分岗位没有申请链接。** Greenhouse 与 nowcoder 聚合岗不提供来源侧的 + `apply_url`,界面会回退到原始公告。系统不会推导或猜测申请链接。Moka 尚未 + 验证,也未注册。 +- **不绕过登录墙。** 系统不做自动登录、验证码绕过、Cookie 或 User-Agent 伪造, + 也不使用代理池。登录墙后的岗位只能通过人工采集处理。 +- **不自动投递。** 没有自动申请、自动提交、表单代填或通知路径,也没有通用爬虫、 + 消息队列、API 网关或 SaaS RBAC。 +- **截止时间仅用于展示。** 截止时间过期不会自动关闭岗位。 +- **界面是本地演示。** 认证基于 HTTP 和浏览器 `localStorage`,不是生产级认证, + 也没有 TLS。 +- **不构成法律结论。** 来源条款、robots 指令和数据使用规则仍由运营者负责。 + +## 文档 + +- [文档地图](docs/README.md)与 + [项目文档索引](docs/PROJECT_DOCUMENTATION_INDEX.md) +- [本地 Web UI 指南](docs/guides/local-demo.md) +- [架构说明](docs/architecture/open-source-mvp.md)与 + [架构决策](docs/adr/README.md) +- [API 契约](docs/04-api-design.md)与 + [数据库契约](docs/03-database-design.md) +- [来源适配器](source_adapters/README.md)与 + [基准测试](docs/benchmarks/README.md) + +## 贡献与安全 + +提交 Pull Request 前请阅读 [`CONTRIBUTING.md`](CONTRIBUTING.md)。安全漏洞请按 +[`SECURITY.md`](SECURITY.md) 中的方式私下报告,不要创建公开 Issue。行为准则 +暂缓,待有可用的报告渠道后再补充。 + +## 许可证 + +MIT。见 [`LICENSE`](LICENSE) 与 [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md)。 diff --git a/docs/CLAUDE_CODE_HANDOFF.md b/docs/CLAUDE_CODE_HANDOFF.md index 9263b72..46b99f6 100644 --- a/docs/CLAUDE_CODE_HANDOFF.md +++ b/docs/CLAUDE_CODE_HANDOFF.md @@ -169,6 +169,18 @@ Status vocabulary: `DONE` (works, verified) · `PARTIAL` · `FRAMEWORK_ONLY` · > is historical. 0019 recorded the 2026-10-19 dry-run: Greenhouse **232**, > Lever **10**, Ashby **167**, all `board_complete=True`; raw log > `build/0019/ats-live-smoke.log`. +> +> **Correction 2026-10-21 (0025), row 9:** the “real capture still +> `resolution=open`” sentence is historical. The 2026-10-21 check of the dev +> `jobpilot-mysql` database found **0** `job_review_items` (0 open), **0** jobs +> with `review_status <> 0`, and four jobs that are all DAO test fixtures +> (`source_url` `job-dao-*`), not the real capture. No real capture row exists +> in the dev database, so no review-row reject/delete action applied. A raw +> real-capture file at `$HOME/capture-acceptance.json` (6846 bytes, SHA-256 +> `1a625a3be8a42008c8def2b31740551c6d12b3bf57bdb001400d6499162f58d0`) was +> found outside the repository and deleted on 2026-10-21; no remaining copy was +> found under the user home or `/tmp`. See `docs/ai/project-state.md` +> `## Active Plan (0025)` and `build/0025/capture-retention-check.txt` (ignored). ## 6. 已完成的重要工作(有证据的) @@ -218,6 +230,16 @@ Status vocabulary: `DONE` (works, verified) · `PARTIAL` · `FRAMEWORK_ONLY` · human should decide whether it becomes public (its `company` is the platform name and its `description` contains page chrome). + > **Resolved 2026-10-21 (0025):** the current dev database contains no real + > capture row and no open review item; the four remaining jobs are DAO test + > fixtures. The raw real-capture JSON at + > `$HOME/capture-acceptance.json` was deleted and no copy remains under the + > user home or `/tmp`; no review-row reject/delete applied because no row + > existed. Redacted acceptance logs remain as evidence. This item is closed + > for the current environment; if a future real capture is imported, the + > no-retention rule still requires an explicit review decision (reject keeps + > the row for audit; deleting is a separate explicit action). + ## 8. 已知 Bug No confirmed, open functional bug is known at this baseline. For the record, the diff --git a/docs/PROJECT_DOCUMENTATION_INDEX.md b/docs/PROJECT_DOCUMENTATION_INDEX.md index 1263b10..d59e40f 100644 --- a/docs/PROJECT_DOCUMENTATION_INDEX.md +++ b/docs/PROJECT_DOCUMENTATION_INDEX.md @@ -1,9 +1,10 @@ # JobPilotAI Project Documentation Index -Maintained for the Claude Code handoff (2026-10-11). It answers one question per -document: **what is this, who should read it, and is it still true?** When a -document disagrees with the code, the row says so and the disagreement is -recorded in §12 instead of being silently "fixed". +Maintained for the Claude Code handoff; refreshed 2026-10-21 for the README and +demo tooling. It answers one question per document: **what is this, who should +read it, and is it still true?** When a document disagrees with the code, the +row says so and the disagreement is recorded in §12 instead of being silently +"fixed". Status vocabulary used below: @@ -16,7 +17,8 @@ Status vocabulary used below: ## 0. Reading order for a new contributor or agent -1. `README.md` — what the project is, how to run it. +1. `README.md` / `README.zh-CN.md` — what the project is, the one-command demo, + and how to use it. 2. `AGENTS.md` — the project's hard constraints, P1 freeze, task routing. 3. `docs/CLAUDE_CODE_HANDOFF.md` — the current state, gaps and the protocol for the first phase (the document you are likely to need next). @@ -32,7 +34,9 @@ Status vocabulary used below: | Path | Document | Purpose | Audience | Validity | Related code | |---|---|---|---|---|---| -| `README.md` | Project README | What the project is, English Quick Start, current capabilities, explicit non-goals, contracts (F13/F14), build/verify commands | Everyone, first file to read | `CURRENT` | whole repo | +| `README.md` | Project README (English) | What the project is, two screenshots, one-command Quick Start, demo flow, usage, verification, limits, community links | Everyone, first file to read | `CURRENT` (2026-10-21) | whole repo | +| `README.zh-CN.md` | Chinese README | Chinese mirror of the English README with the same sections, screenshots and commands; no mixed-language paragraphs | Chinese-speaking users and contributors | `CURRENT` (2026-10-21) | whole repo | +| `docs/images/` | README screenshots | `web-detail.png` (explainable match) and `web-status.png` (application board), both 1366×682 from the local UI with synthetic data | Everyone | `CURRENT` (2026-10-21; regenerated with the browser smoke after the temporary originals were removed) | `web/`, `tools/dev/web-smoke.sh` | | `AGENTS.md` | Project Guide | Hard constraints (dependency direction, P1 freeze, secrets, JSON/SQL/RAII rules), task routing, verification policy | Every agent/contributor | `CURRENT` | whole repo | | `.agents/skills/jobpilot-reactor-review/SKILL.md` | Reactor review skill | Mandatory checklist before touching `backend/{include,src}/jobpilot/{network,http}/` | Anyone changing the network layer | `CURRENT` | `backend/src/network`, `backend/src/http` | | `SECURITY.md` | Security policy | Supported versions, private reporting, local-demo boundaries, no-secrets policy, A history disclosure (facts only) | Everyone, release reviewer | `CURRENT` (2026-10-20) | `tools/checks/` | @@ -47,7 +51,7 @@ Status vocabulary used below: |---|---|---|---|---|---| | `docs/ai/README.md` | AI context router | Which document to read for which task; diff-first reading policy | Agents | `CURRENT` | — | | `docs/ai/agent-workflow.md` | Agent workflow and verification rules | Verification levels L1–L5, when a suite may run, harness prerequisites, one review per stage | Agents | `CURRENT` | `build/l4-0017/` (scratch harness) | -| `docs/ai/project-state.md` | Current project state (959 lines) | Stage-by-stage state, carried-forward items, what is genuinely open | Agents, maintainer | `CURRENT` (updated 2026-10-21) | whole repo | +| `docs/ai/project-state.md` | Current project state (1017 lines) | Stage-by-stage state, carried-forward items, what is genuinely open | Agents, maintainer | `CURRENT` (updated 2026-10-21) | whole repo | | `docs/ai/CURRENT_STATE.md` | Handoff verification report (2026-10-19) | Git/CI facts, §23 fact checklist, document-code conflicts, capability-matrix update; 2026-10-20 resolution note | Agents, maintainer, release reviewer | `CURRENT` (dated 2026-10-19; point-in-time report plus dated resolution note) | whole repo | | `docs/ai/archive/next-phase-plan-2026-10.md` | Archived next-phase plan | An earlier plan sketch; kept for context | Agents | `REFERENCE` (archived) | — | | `docs/ai/references/github_resources.md` | GitHub resource research | 655-line survey used for the open-source plan | Maintainer | `REFERENCE` | — | @@ -77,7 +81,7 @@ Status vocabulary used below: |---|---|---|---|---|---| | `docs/04-api-design.md` | API contract (1428 lines) | Every route, request/response, error codes, internal token contract, public visibility rule (updated 2026-10-11), source-registration contract, reserved-route markers and route counts (2026-10-20) | Anyone changing an API | `CURRENT` | `service/src/*/*_controller.cpp`, `service/src/router.cpp` | | `docs/03-database-design.md` | Database contract (834 lines) | Tables, columns, indexes, F13 deadline rule, 0015A quality status, current 13-table DDL, migration notes (§9.4) | Anyone changing the schema | `CURRENT` | `database/schema/0*.sql` | -| `docs/guides/local-demo.md` | Local Web UI demo guide | Dependencies, nginx/Python-fallback startup, demo flow, resume prerequisite, screenshots, troubleshooting, security boundary | Users, reviewers, maintainers | `CURRENT` (2026-10-20) | `web/`, `docker/nginx/`, `tools/dev/web-smoke.sh` | +| `docs/guides/local-demo.md` | Local Web UI demo guide | Dependencies, one-command launcher, nginx/Python-fallback startup, demo flow, resume prerequisite, screenshots, troubleshooting, security boundary | Users, reviewers, maintainers | `CURRENT` (2026-10-21) | `web/`, `docker/nginx/`, `tools/dev/demo-up.sh`, `tools/dev/web-smoke.sh` | | `database/schema/README.md` | Schema guide | How the schema files are ordered and applied | Anyone touching the DB | `CURRENT` | `database/schema/` | | `docs/06-coding-standard.md` | Coding standard | Naming, error handling, SQL, RAII, logging rules | Contributors | `CURRENT` | whole repo | | `docs/project-structure.md` | Project structure | Directory/module responsibilities | Contributors | `HISTORICAL` (Superseded banner) | — | @@ -90,7 +94,7 @@ Status vocabulary used below: | `docs/quality/test-overlap-audit.md` | Test overlap audit (0017-B) | Proof that the suite has no mergeable duplicate case; the 66 evaluated pairs; counts block read by the guard | Maintainer | `CURRENT` | `service/tests/`, `tools/checks/test_coverage_map.py` | | `docs/benchmarks/README.md` | Benchmark baseline | Recorded performance numbers and how to re-run them | Maintainer | `REFERENCE` (numbers age) | `build/` presets | | `docs/RELEASE_CHECKLIST.md` | Release checklist (333 lines) | The gate: automated checks, licence, secrets, publish-safety review, owner actions, dated evidence blocks | Release owner | `CURRENT` (dated blocks, refreshed 2026-10-11) | whole repo | -| `docs/examples/` | Example payloads | Synthetic fixtures used in docs | Contributors | `CURRENT` | — | +| `docs/examples/` | Example payloads | Synthetic fixtures used in docs, including `demo-jobs.json` (12 synthetic jobs imported by the one-command demo) | Contributors | `CURRENT` (2026-10-21) | — | ## 6. Execution plans @@ -103,6 +107,7 @@ Status vocabulary used below: | `docs/exec-plans/completed/0021-docs-reconciliation.md` | Completed plan 0021 | `docs/03`–`docs/04` reconciliation of DB-1/2/3 and API-1/2/3/4; dated handoff corrections; docs-only L0; merged at `d75b14e` after VERDICT-0021-closeout PASS | Agents, maintainer | `COMPLETED` (2026-10-20) | `docs/03`, `docs/04`, `docs/CLAUDE_CODE_HANDOFF.md` | | `docs/exec-plans/completed/0022-web-ui.md` | Completed plan 0022 | Minimal native Web UI: nginx static + same-origin proxy, auth, jobs/filter/match, detail/apply links, resume prerequisite, favourite and status board; Python fallback + Firefox smoke; merged at `38bd484` | Agents, maintainer | `COMPLETED` (2026-10-20) | `web/`, `docker/nginx/`, `tools/dev/web-smoke.sh` | | `docs/exec-plans/completed/0023-release-readiness.md` | Completed plan 0023 | Release readiness: security/contributing docs, Python locks, MariaDB client image, TSan first run, libbcrypt provenance, benchmark refresh, release-checklist state; merged at `539202f` after the exclusive benchmark rework | Agents, maintainer, release reviewer | `COMPLETED` (2026-10-21) | `SECURITY.md`, `CONTRIBUTING.md`, `Dockerfile`, `backend/benchmarks/` | +| `docs/exec-plans/active/0025-readme-and-usage.md` | Active plan 0025 | External-facing English README plus a separate Chinese mirror, two real UI screenshots, one-command demo launcher/seed, 12-job fixture, docs index/state; L0 + shell smoke, no L4 | Agents, maintainer, release reviewer | `ACTIVE` (implementation on `docs/readme-demo`; closeout pending) | `README.md`, `README.zh-CN.md`, `tools/dev/demo-up.sh`, `tools/dev/demo-seed.sh` | | `docs/exec-plans/completed/0001…0017A` | 20 archived plans | The history of every stage, each with its evidence, decisions and L4 results. Notable: `0014` (China sources + manual capture, 1264 lines), `0015`/`0015A` (tailoring, explainable matching, quality gate), `0016` (packaging, demo gate), `0017` (ops hardening, L4), `0017A` (the `published_at` contract gap) | Agents needing the "why" | `HISTORICAL` by definition, but each is the **authority for its stage's decisions** | the module named in each plan | ## 7. Ingestion, sources and tools @@ -142,10 +147,11 @@ Status vocabulary used below: | Path | Why it is needed | Suggested content | |---|---|---| -| `SECURITY.md` | Anything that becomes public needs a disclosure channel | Supported versions, how to report privately, no-secrets policy | -| `CONTRIBUTING.md` | External contributors need the rules — today they live inside `AGENTS.md`, which is agent-oriented | Build/verify commands, commit style, PR expectations, the P1 freeze | -| `CODE_OF_CONDUCT.md` | Optional for a small project; conventional for public repos | Standard contributor covenant | -| `docs/guides/` | Installation and local walkthroughs have no current home; `docs/08` is superseded and `README` is a Quick Start only | `local-demo.md`, `installation.md`, `web-ui.md` (future) | +| `CODE_OF_CONDUCT.md` | Optional for a small project; conventional for public repos | Standard contributor covenant; deferred until a reporting channel exists | + +`SECURITY.md`, `CONTRIBUTING.md` and `docs/guides/local-demo.md` were added after +this section was first written; they are tracked in §1 and §4 and are no longer +missing. ## 11. Handoff documents added on 2026-10-11 diff --git a/docs/README.md b/docs/README.md index b76a209..3943f95 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,8 +5,9 @@ the documents below are the contracts and the evidence behind it. ## To run it -1. [`../README.md`](../README.md) — Quick Start (English) plus the list of what - actually works today, and what deliberately does not. +1. [`../README.md`](../README.md) / [`../README.zh-CN.md`](../README.zh-CN.md) — + the one-command local demo, how to use it, what works today, and what + deliberately does not. 2. [`../database/schema/README.md`](../database/schema/README.md) — create a database, upgrade an existing one, verify it, and the isolation rule for demo and test data. diff --git a/docs/ai/project-state.md b/docs/ai/project-state.md index 02a17ef..97c3a83 100644 --- a/docs/ai/project-state.md +++ b/docs/ai/project-state.md @@ -57,6 +57,12 @@ > Next stage: **0024 owner publication** (owner-only; no worker implementation). > The release-readiness work is merged; A history disclosure remains > accepted/no-rewrite. Wait for the owner’s 0024 instruction; do not start it. +> +> **2026-10-21 update — 0025 implementation in progress.** The plan-only PR #7 +> merged at `7acc58f`; the docs/tooling implementation is on branch +> `docs/readme-demo` and is **not merged yet**. The README rewrite, screenshots, +> one-command demo and the capture no-retention check are recorded in +> `## Active Plan (0025)` below. 0024 remains owner-only and has not started. Repository files and Git history are the cross-agent fact source. This concise state plus the active or most recent ExecPlan should be enough for a new Agent to @@ -231,6 +237,58 @@ libbcrypt upstream unresolved with SHA-256 manifest. PR image `39461251212` passed `Verify the MariaDB client linkage` (`MARIA_DB_LINK_OK`) + demo gate; main CI after merge is green (`ci` `39462589981`, `image` `39462589965`). +## Active Plan (0025) + +[`active/0025-readme-and-usage.md`](../exec-plans/active/0025-readme-and-usage.md) +— plan-only PR #7 merged at `7acc58f`; implementation is in progress on branch +`docs/readme-demo`, **not yet reviewed or merged**. Scope: external-facing +English README + separate `README.zh-CN.md`, two UI screenshots, one-command +`tools/dev/demo-up.sh`/`demo-seed.sh`, 12-job fixture, docs index/state. This is +docs + developer tooling only: no core/API/schema/CI/P1 change; L0 + shell smoke, +no L4. + +Evidence collected on the branch (2026-10-21): + +- **Screenshots**: `docs/images/web-detail.png` and `docs/images/web-status.png`, + both 1366×682 PNG. The planned `/tmp/0022-browser-final` originals were no + longer present, so they were regenerated with + `tools/dev/web-smoke.sh --browser firefox` (Python proxy fallback); the exact + original images are not recoverable. Both READMEs reference the new files. +- **README**: `README.md` is English and `README.zh-CN.md` is a separate Chinese + mirror; both have the language switch, two screenshots, one-command Quick + Start, demo flow, usage, verification, limits and community links. Stage + numbers are absent from both bodies. +- **Demo smoke** (isolated `jobpilot-demo-0025`; env + `/tmp/jobpilot-demo-0025.env`, later removed; ports 18180/18181/13326/16399; + backend image `jobpilot-backend:verify`, `DEMO_BUILD_BACKEND=0`): + `demo-up.sh` produced `DEMO_READY url=http://127.0.0.1:18181`, used the + Python proxy fallback because `nginx:1.27-alpine` was unavailable, and seeded + `total=12 created=12`. A second run preserved the `.env` byte-for-byte + (SHA-256 unchanged) and seeded `total=12 created=0 updated=12`; the page and + `GET /api/v1/jobs?page=1&page_size=20` remained reachable with `total=12`. + A `C++` keyword request returned 5 jobs. The generated `.env` was mode `0600`; + none of its generated secret values appeared in the launcher log. + `docker compose ... down -v` removed all containers, volumes and the network; + the temporary env file was deleted. Raw logs: `build/0025/` (ignored). +- **Capture sample / no retention**: the 2026-10-21 query against the dev + `jobpilot-mysql` returned `jobs_total=4`, `jobs_review_status_nonzero=0`, + `review_items_total=0`, `review_items_open=0`. All four jobs are DAO test + fixtures (`source_url` `job-dao-*`, source `manual`, `review_status=0`), not + a real capture. No real capture row exists in the dev database, so no + review-row reject/delete action applied. A raw real-capture file at + `$HOME/capture-acceptance.json` (6846 bytes, SHA-256 + `1a625a3be8a42008c8def2b31740551c6d12b3bf57bdb001400d6499162f58d0`) was + found outside the repository and was **deleted on 2026-10-21**; a search of + the user home and `/tmp` found no remaining copy. Redacted acceptance logs under + `build/0017a/` and `build/capture-0017/` remain as evidence and contain no raw + payload. The old handoff statement that the real sample is still + `resolution=open` now carries dated corrections in §5 row 9 and §7 item 8. + Raw DB output: `build/0025/capture-retention-check.txt` (ignored). + +Remaining for this branch: closeout request, owner-created PR/merge, and the +post-merge main CI verification. Do not archive the plan or mark this stage +completed until the owner merges it. + ## Current Stage - Architecture direction is now open-source first and self-hosted, with a diff --git a/docs/examples/demo-jobs.json b/docs/examples/demo-jobs.json new file mode 100644 index 0000000..4d85bad --- /dev/null +++ b/docs/examples/demo-jobs.json @@ -0,0 +1,190 @@ +{ + "jobs": [ + { + "company": "Example Systems", + "title": "C++ Backend Engineer", + "location": "Shenzhen", + "salary_min": 20, + "salary_max": 35, + "salary_currency": "CNY", + "description": "Build Linux backend services for a job platform. Work on HTTP APIs, data ingestion and observability.", + "source": "import", + "source_url": "https://example.invalid/jobs/demo-cpp-backend-1", + "apply_url": "https://example.invalid/apply/demo-cpp-backend-1", + "requirements": [ + { "skill": "C++", "level": 3 }, + { "skill": "Linux", "level": 2 } + ] + }, + { + "company": "Data Platform Labs", + "title": "Distributed Systems Engineer", + "location": "Shanghai", + "salary_min": 25, + "salary_max": 40, + "salary_currency": "CNY", + "description": "Design reliable services for distributed workloads. Focus on correctness, failure handling and data access.", + "source": "import", + "source_url": "https://example.invalid/jobs/demo-distributed-2", + "requirements": [ + { "skill": "C++", "level": 3 }, + { "skill": "MySQL", "level": 2 } + ] + }, + { + "company": "Northstar Quant", + "title": "C++ Trading Systems Engineer", + "location": "Beijing", + "salary_min": 30, + "salary_max": 50, + "salary_currency": "CNY", + "description": "Low-latency C++ services for market data and order routing. Linux and performance profiling are part of the daily work.", + "source": "import", + "source_url": "https://example.invalid/jobs/demo-trading-3", + "requirements": [ + { "skill": "C++", "level": 3 }, + { "skill": "Linux", "level": 3 } + ] + }, + { + "company": "Cedar Pay", + "title": "Backend Engineer, Payments", + "location": "Guangzhou", + "salary_min": 18, + "salary_max": 30, + "salary_currency": "CNY", + "description": "Build payment APIs and reconciliation jobs. The team values clear error handling and a small, auditable service surface.", + "source": "import", + "source_url": "https://example.invalid/jobs/demo-payments-4", + "apply_url": "https://example.invalid/apply/demo-payments-4", + "requirements": [ + { "skill": "C++", "level": 2 }, + { "skill": "MySQL", "level": 2 }, + { "skill": "Redis", "level": 1 } + ] + }, + { + "company": "Orbit Cloud", + "title": "Platform Engineer", + "location": "Hangzhou", + "salary_min": 22, + "salary_max": 38, + "salary_currency": "CNY", + "description": "Own platform services and developer tooling. Experience with Linux, containers and backend APIs is useful.", + "source": "import", + "source_url": "https://example.invalid/jobs/demo-platform-5", + "requirements": [ + { "skill": "C++", "level": 2 }, + { "skill": "Linux", "level": 3 } + ] + }, + { + "company": "Embedded Works", + "title": "Embedded Software Engineer", + "location": "Shenzhen", + "salary_min": 20, + "salary_max": 32, + "salary_currency": "CNY", + "description": "Develop embedded C/C++ software for connected devices. Work close to hardware, Linux and test automation.", + "source": "import", + "source_url": "https://example.invalid/jobs/demo-embedded-6", + "requirements": [ + { "skill": "C++", "level": 2 }, + { "skill": "Linux", "level": 2 } + ] + }, + { + "company": "Pixel Forge", + "title": "Game Backend Engineer", + "location": "Chengdu", + "salary_min": 18, + "salary_max": 30, + "salary_currency": "CNY", + "description": "Build game backend services for player state, matchmaking and leaderboards. Redis and latency matter here.", + "source": "import", + "source_url": "https://example.invalid/jobs/demo-game-7", + "requirements": [ + { "skill": "C++", "level": 2 }, + { "skill": "Redis", "level": 2 } + ] + }, + { + "company": "Meridian Data", + "title": "Database Infrastructure Engineer", + "location": "Shanghai", + "salary_min": 25, + "salary_max": 42, + "salary_currency": "CNY", + "description": "Operate and improve MySQL-backed services. Build safe migrations, query tooling and observability for data paths.", + "source": "import", + "source_url": "https://example.invalid/jobs/demo-database-8", + "requirements": [ + { "skill": "C++", "level": 2 }, + { "skill": "MySQL", "level": 3 }, + { "skill": "Linux", "level": 2 } + ] + }, + { + "company": "Packet Harbor", + "title": "Network Software Engineer", + "location": "Beijing", + "salary_min": 24, + "salary_max": 40, + "salary_currency": "CNY", + "description": "Work on TCP services, protocol handling and performance. The role is Linux-heavy and values careful resource ownership.", + "source": "import", + "source_url": "https://example.invalid/jobs/demo-network-9", + "requirements": [ + { "skill": "C++", "level": 3 }, + { "skill": "Linux", "level": 3 } + ] + }, + { + "company": "Bluefin Robotics", + "title": "C++ SDK Engineer", + "location": "Shenzhen", + "salary_min": 22, + "salary_max": 36, + "salary_currency": "CNY", + "description": "Design a C++ SDK used by robotics integrators. Focus on API stability, documentation and cross-platform builds.", + "source": "import", + "source_url": "https://example.invalid/jobs/demo-sdk-10", + "requirements": [ + { "skill": "C++", "level": 3 }, + { "skill": "Linux", "level": 2 } + ] + }, + { + "company": "Lumen Labs", + "title": "Backend Engineer, Growth", + "location": "Beijing", + "salary_min": 16, + "salary_max": 28, + "salary_currency": "CNY", + "description": "Build small backend services for growth experiments. Expect rapid iteration, clear metrics and careful data handling.", + "source": "import", + "source_url": "https://example.invalid/jobs/demo-growth-11", + "apply_url": "https://example.invalid/apply/demo-growth-11", + "requirements": [ + { "skill": "C++", "level": 1 }, + { "skill": "MySQL", "level": 2 } + ] + }, + { + "company": "Atlas Commerce", + "title": "Site Reliability Engineer", + "location": "Hangzhou", + "salary_min": 20, + "salary_max": 35, + "salary_currency": "CNY", + "description": "Keep commerce services reliable. Work with Linux, Redis, MySQL and service-level monitoring; C++ debugging is a plus.", + "source": "import", + "source_url": "https://example.invalid/jobs/demo-sre-12", + "requirements": [ + { "skill": "Linux", "level": 3 }, + { "skill": "C++", "level": 1 }, + { "skill": "Redis", "level": 2 } + ] + } + ] +} diff --git a/docs/exec-plans/active/0025-readme-and-usage.md b/docs/exec-plans/active/0025-readme-and-usage.md index 9dfb897..c3e3e71 100644 --- a/docs/exec-plans/active/0025-readme-and-usage.md +++ b/docs/exec-plans/active/0025-readme-and-usage.md @@ -1,8 +1,8 @@ # ExecPlan 0025 — README and external usage guide -> 状态:**PLANNING(等待 VERDICT-0025-plan;批准前不改 README/图片/既有文档)** -> 分支:`docs/0025-readme-and-usage` -> 基线:`main@f788b06`(0023 merged;PR #6 `539202f`) +> 状态:**IMPLEMENTATION(closeout pending;计划已由 PR #7 合入)** +> 分支:`docs/readme-demo`(实施);计划原始分支 `docs/0025-readme-and-usage` +> 基线:`main@7acc58f`(计划 PR #7;0023 merge `539202f`) > 类型:**docs + developer demo tooling**(不碰 core/API/schema/CI);L0 + shell smoke,不跑 L4;不启动 0024。 ## 1. 背景 @@ -251,10 +251,23 @@ git diff --check ## 10. Progress ```text -P0 plan + REQUEST [x] 本文件;等待 VERDICT-0025-plan -P1 demo scripts + seed fixture [ ] 未开始 -P2 screenshots + README + one-command Quick Start [ ] 未开始 -P3 README.zh-CN [ ] 未开始 -P4 index/state [ ] 未开始 -P5 L0 + shell smoke + closeout [ ] 未开始 +P0 plan + REQUEST [x] 本文件;plan-only PR #7 merged at 7acc58f +P1 demo scripts + seed fixture [x] demo-up.sh / demo-seed.sh / 12-job fixture +P2 screenshots + README + one-command Quick Start [x] screenshots regenerated; README rewritten +P3 README.zh-CN [x] 独立中文镜像 +P4 index/state [x] docs index + project-state + handoff corrections +P5 L0 + shell smoke + closeout [x] smoke 完成;REQUEST-0025-closeout 已写;owner PR/merge 待办 ``` + +Execution note (2026-10-21): the planned `/tmp/0022-browser-final/*.png` +originals were missing, so both screenshots were regenerated with +`tools/dev/web-smoke.sh --browser firefox` (Python proxy fallback). The exact +originals are not recoverable; the new files are 1366×682 and use the same +synthetic UI. The isolated launcher smoke used `jobpilot-demo-0025`, ports +18180/18181/13326/16399 and `jobpilot-backend:verify`; it printed +`DEMO_READY`, seeded 12 jobs, preserved `.env` on re-run, and the stack was torn +down with `down -v`. The capture no-retention check found 0 review items and +only DAO test fixtures; the raw real-capture file +`$HOME/capture-acceptance.json` was also deleted, with no copy found under the +user home or `/tmp`. See `docs/ai/project-state.md` and the handoff +corrections. Closeout request is written; owner PR/merge remains. diff --git a/docs/guides/local-demo.md b/docs/guides/local-demo.md index db3a988..22214c3 100644 --- a/docs/guides/local-demo.md +++ b/docs/guides/local-demo.md @@ -17,6 +17,16 @@ frontend. ## 启动(nginx 形态) +一条命令启动(缺少 `.env` 时生成随机密码,构建后端,启动 UI,并默认导入 +12 条示例岗位): + +```bash +./tools/dev/demo-up.sh +``` + +打开脚本打印的 `DEMO_READY` 地址即可;nginx 镜像不可用时脚本会自动回退到 +`web_static_proxy.py`。手动等价步骤: + ```bash cp .env.example .env # fill MYSQL_ROOT_PASSWORD and MYSQL_PASSWORD @@ -88,23 +98,23 @@ does not replace nginx in a deployment. 7. Reload the page: the resume version and applications persist; the token is re-validated with `GET /users/me`. -The sample dataset `docs/examples/job-import.json` uses `example.invalid` URLs; -they are intentionally fake. The UI does not derive an apply URL from -`source_url`. +The sample datasets `docs/examples/job-import.json` and +`docs/examples/demo-jobs.json` use `example.invalid` URLs; they are intentionally +fake. The UI does not derive an apply URL from `source_url`. ## 截图步骤(可选) Use synthetic/demo data only; do not put personal data in a screenshot. 1. Register a demo user, upload a synthetic resume, import - `docs/examples/job-import.json` through the internal token or use an - existing demo stack. -2. Capture **岗位列表** with a match score/reason, **岗位详情** with the - apply/original links, and **申请状态板** with at least one application. -3. Suggested filenames: `web-list.png`, `web-detail.png`, `web-status.png`. - The repository `.gitignore` ignores `*.png`; 0022 deliberately does **not** - add images to the README. Add owner-approved screenshots in a later change if - desired. + `docs/examples/demo-jobs.json` (or `job-import.json`) through the internal + token, or use an existing demo stack. +2. Capture **岗位详情** with the match explanation and **申请状态板** with at + least one application. +3. The two README screenshots are tracked at `docs/images/web-detail.png` and + `docs/images/web-status.png` (1366×682, synthetic data, regenerated with the + browser smoke). If you recapture them, keep the same filenames and update the + READMEs if the filenames change. 4. Use the browser/OS screenshot tool; do not include cookies, tokens, email addresses, or real resumes. diff --git a/docs/images/web-detail.png b/docs/images/web-detail.png new file mode 100644 index 0000000..e2ed4aa Binary files /dev/null and b/docs/images/web-detail.png differ diff --git a/docs/images/web-status.png b/docs/images/web-status.png new file mode 100644 index 0000000..608df42 Binary files /dev/null and b/docs/images/web-status.png differ diff --git a/tools/dev/demo-seed.sh b/tools/dev/demo-seed.sh new file mode 100755 index 0000000..e2e4c91 --- /dev/null +++ b/tools/dev/demo-seed.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Seed a running demo stack with synthetic jobs (10-20, including C++ backend). +# +# Usage: +# tools/dev/demo-seed.sh +# DEMO_ENV_FILE=/tmp/demo.env DEMO_API_PORT=18080 tools/dev/demo-seed.sh +# +# The import is idempotent on source_url; re-running updates the same rows. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +ENV_FILE="${DEMO_ENV_FILE:-$ROOT/.env}" +FIXTURE="${DEMO_JOBS_FILE:-$ROOT/docs/examples/demo-jobs.json}" + +if [ ! -f "$ENV_FILE" ]; then + echo "demo-seed: $ENV_FILE is missing; run tools/dev/demo-up.sh first" >&2 + exit 2 +fi +if [ ! -f "$FIXTURE" ]; then + echo "demo-seed: fixture not found: $FIXTURE" >&2 + exit 2 +fi + +# Load the generated/compose env. DEMO_INTERNAL_TOKEN wins over a stale/empty +# value in the file only for this invocation; the file is never modified. +set -a +# shellcheck disable=SC1090 +. "$ENV_FILE" +set +a + +API_PORT="${DEMO_API_PORT:-${JOBPILOT_BACKEND_HOST_PORT:-8080}}" +TOKEN="${DEMO_INTERNAL_TOKEN:-${INTERNAL_API_TOKEN:-}}" +BASE="http://127.0.0.1:${API_PORT}" + +if [ -z "$TOKEN" ]; then + echo "demo-seed: INTERNAL_API_TOKEN is empty; set it in $ENV_FILE or DEMO_INTERNAL_TOKEN" >&2 + exit 2 +fi + +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT + +for _ in $(seq 1 60); do + if curl -fsS --max-time 2 "$BASE/health" >/dev/null 2>&1; then + break + fi + sleep 0.5 +done +curl -fsS --max-time 2 "$BASE/health" >/dev/null || { + echo "demo-seed: backend is not healthy at $BASE/health" >&2 + exit 1 +} + +echo "demo-seed: importing $FIXTURE through $BASE/internal/jobs/import" +CODE="$(curl -sS -o "$TMP/import.json" -w '%{http_code}' -X POST \ + "$BASE/internal/jobs/import" \ + -H 'Content-Type: application/json' \ + -H "X-Internal-Token: $TOKEN" \ + -d @"$FIXTURE")" +if [ "$CODE" -ge 400 ]; then + cat "$TMP/import.json" >&2 + echo "demo-seed: import returned HTTP $CODE" >&2 + exit 1 +fi + +python3 - "$TMP/import.json" <<'PY' +import json +import sys + +body = json.load(open(sys.argv[1])) +created = int(body.get("created", 0)) +updated = int(body.get("updated", 0)) +total = int(body.get("total", 0)) +rejected = int(body.get("rejected", 0)) +needs_review = int(body.get("needs_review", 0)) +assert total >= 10, body +assert created + updated <= total, body +assert rejected == 0, body +assert needs_review == 0, body +print("DEMO_SEED_OK total=%d created=%d updated=%d" % (total, created, updated)) +PY diff --git a/tools/dev/demo-up.sh b/tools/dev/demo-up.sh new file mode 100755 index 0000000..0fee912 --- /dev/null +++ b/tools/dev/demo-up.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash +# One-command local demo: MySQL/Redis -> backend -> UI -> seed -> URL. +# +# tools/dev/demo-up.sh +# +# It creates a random .env only when .env is missing, never prints the secret +# values, and falls back to tools/dev/web_static_proxy.py when the nginx image +# is unavailable. The default is to seed 12 synthetic jobs; --no-seed opts out. +# +# Useful overrides for tests or an existing stack: +# DEMO_PROJECT, DEMO_ENV_FILE, DEMO_API_PORT, DEMO_WEB_PORT, +# DEMO_MYSQL_PORT, DEMO_REDIS_PORT, DEMO_BACKEND_IMAGE, +# DEMO_BUILD_BACKEND=0, DEMO_INTERNAL_TOKEN +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +cd "$ROOT" + +PROJECT="${DEMO_PROJECT:-jobpilot-demo}" +ENV_FILE="${DEMO_ENV_FILE:-$ROOT/.env}" +API_PORT="${DEMO_API_PORT:-8080}" +WEB_PORT="${DEMO_WEB_PORT:-8081}" +MYSQL_PORT="${DEMO_MYSQL_PORT:-3306}" +REDIS_PORT="${DEMO_REDIS_PORT:-6379}" +SEED=1 + +while [ "$#" -gt 0 ]; do + case "$1" in + --no-seed) SEED=0; shift ;; + --no-build) DEMO_BUILD_BACKEND=0; shift ;; + -h|--help) + sed -n '2,18p' "$0" + exit 0 + ;; + *) + echo "demo-up: unknown argument: $1" >&2 + exit 2 + ;; + esac +done + +say() { printf '\n=== %s ===\n' "$1"; } +fail() { echo "demo-up: $*" >&2; exit 1; } + +command -v docker >/dev/null 2>&1 || fail "Docker is not on PATH" +docker compose version >/dev/null 2>&1 || fail "Docker Compose v2 is not available" +command -v curl >/dev/null 2>&1 || fail "curl is not on PATH" +command -v python3 >/dev/null 2>&1 || fail "python3 is not on PATH" + +gen_secret() { + if command -v openssl >/dev/null 2>&1; then + openssl rand -hex 24 + else + python3 -c 'import secrets; print(secrets.token_hex(24))' + fi +} + +if [ ! -f "$ENV_FILE" ]; then + say "create $ENV_FILE" + mkdir -p "$(dirname "$ENV_FILE")" + umask 077 + cat > "$ENV_FILE" </dev/null 2>&1 || fail "backend image $IMAGE is not available" +fi + +say "start backend" +compose --profile app up -d +for _ in $(seq 1 60); do + if curl -fsS --max-time 2 "$API/health" >/dev/null 2>&1; then + break + fi + sleep 1 +done +curl -fsS --max-time 2 "$API/health" >/dev/null || { + compose logs --tail=60 backend || true + fail "backend did not become healthy at $API/health" +} +echo "backend health: $API/health" + +say "start Web UI" +WEB_MODE="nginx" +if ! compose --profile app --profile web up -d > "$ROOT/build/demo-web-up.log" 2>&1; then + WEB_MODE="python-proxy" + echo "nginx web profile unavailable; falling back to web_static_proxy.py" +fi +if [ "$WEB_MODE" = "nginx" ]; then + for _ in $(seq 1 20); do + if curl -fsS --max-time 2 "$WEB/" >/dev/null 2>&1; then + break + fi + sleep 0.5 + done + if ! curl -fsS --max-time 2 "$WEB/" >/dev/null 2>&1; then + WEB_MODE="python-proxy" + compose --profile web stop web >/dev/null 2>&1 || true + echo "nginx web service did not become reachable; falling back to web_static_proxy.py" + fi +fi + +if [ "$WEB_MODE" = "python-proxy" ]; then + # A failed compose up can still leave a created web container behind; stop + # it so the Python fallback can bind the same host port. + compose --profile web stop web >/dev/null 2>&1 || true + if [ -f "$ROOT/build/demo-web-proxy.pid" ]; then + old_pid="$(cat "$ROOT/build/demo-web-proxy.pid" 2>/dev/null || true)" + [ -n "$old_pid" ] && kill "$old_pid" >/dev/null 2>&1 || true + fi + nohup python3 "$ROOT/tools/dev/web_static_proxy.py" \ + --root "$ROOT/web" --port "$WEB_PORT" --backend "$API" \ + > "$ROOT/build/demo-web-proxy.log" 2>&1 & + echo $! > "$ROOT/build/demo-web-proxy.pid" + for _ in $(seq 1 30); do + if curl -fsS --max-time 2 "$WEB/" >/dev/null 2>&1; then + break + fi + sleep 0.5 + done + curl -fsS --max-time 2 "$WEB/" >/dev/null || { + cat "$ROOT/build/demo-web-proxy.log" >&2 || true + fail "web fallback did not become reachable at $WEB" + } +fi + +if [ "$SEED" = "1" ]; then + say "seed demo jobs" + DEMO_ENV_FILE="$ENV_FILE" DEMO_API_PORT="$API_PORT" \ + DEMO_INTERNAL_TOKEN="$TOKEN" \ + "$ROOT/tools/dev/demo-seed.sh" +fi + +say "ready" +echo "DEMO_READY url=$WEB" +echo "env=$ENV_FILE project=$PROJECT backend=$API" +if [ "$WEB_MODE" = "python-proxy" ]; then + echo "web_proxy_pid=$(cat "$ROOT/build/demo-web-proxy.pid")" + echo "web_proxy_log=$ROOT/build/demo-web-proxy.log" +fi +echo "stop: docker compose --env-file $ENV_FILE -p $PROJECT --profile app --profile web down"