# 2026-09-12 ## 构建「软件开发部关键任务追踪看板」 - 数据源:`/Users/jiliu/WorkSpace/定开管理/软件开发部管理工作执行表.xlsx` - `周工作情况总结`:稀疏三层结构,时间(周期) → 工作内容(项目) → 内容说明(子任务) → 完成情况(执行记录);仅填完成情况的行是上一条记录的续行,需合并。 - `项目明细表`:客户项目台账(名称/区域/金额/类别/状态说明),部分行 F/G 列错位,需按类别白名单校验。 - 数据跨度 2024-03 ~ 2025-08,共 49 个周期、84 个项目、428 子任务、482 条执行记录、39 条台账。 - 技术栈:FastAPI + SQLAlchemy 2.0(SQLite,DATABASE_URL 可切 MySQL)+ 零依赖原生前端(无构建)。 - 关键实现: - `Period.sort_key = year*10000 + month*100 + week`,月视图按 (year, month) 聚合周周期,不单独存月记录。 - 导入时自动归档:最近 10 个周期内无更新且非重点项目 → `archived=True`(归档 77 个,活跃 7 个)。 - 状态判定 `_guess_status`:先负面关键词 → 再用百分比(>=100 才 done)→ 最后才用「完成」类词;进度 <100 时强制把 done 降为 in_progress。 - AI 整理:OpenAI 兼容协议 + 本地规则兜底(无 Key 也可用)。 - 坑: - SQLAlchemy `Table()` 必须传 `Column` 对象,不能传元组。 - 后台起服务要用 Bash 的 `run_in_background`,用 `(&)` 脱离会被沙箱杀掉。 - 前端 `donutChart` 需要 `{name,color,value}`,而接口 `category_dist` 给的是 `count`,必须映射。 - 启动:`./run.sh`(端口 8770),重启后需重新执行 `scripts/import_excel.py --reset` 才会应用新的解析规则。 ## 拆分定制交付项目(第二版需求) - 用户要求:定制项目关心每个子项目的交付阶段(签单/开发/交付完成/计收)与按进度统计收入,且与常规项目数据表分开。 - 新增独立表:`custom_projects`(区域/客户/合同额/阶段/开发进度/手动确认比例/已计收/签单·交付·计收日期)+ `custom_updates`(周期/阶段/本期新增下单/本期新增计收)。 - 迁移脚本 `scripts/migrate_custom.py`: - 《项目明细表》39 条台账 + 原「定制开发」92 个子任务 → 合并成 119 个 CustomProject(同名合并,金额互补)。 - 子任务名尾部的「(39,400.00)」正则解析为合同额(`normalize_custom_name`)。 - 常规表里的「定制开发」容器整体删除,常规看板只剩 6 个平台产品类项目。 - 执行前先 `cp data/board.db data/board.db.bak`。 - 收入确认(完工百分比法):商机 0 / 签单 10% / 开发中 10%+40%×进度 / 交付完成 70% / 已验收 90% / 已计收 100% / 暂停 30%;`manual_ratio` 可覆盖。定义在 `app/constants.py`。 - 阶段判定坑:文本同时含「已计收」和「进度 60%」时必须判为 developing,否则会被关键词带偏 → `guess_custom_stage` 采用「负面 → 完成 → 在制 → 商务」顺序,并用 `extract_percent` 辅助。 - 区域:从项目名反推省/直辖市(`infer_region`,REGION_KEYWORDS 表),回填 68 个,剩 18 个未标注。 - 现状:合同总额 395 万,按进度可确认 99.8 万(25.3%),已计收为 0(历史数据无计收金额,需后续维护)。 ## 启动脚本修复 - 用户报 `./run.sh` → `zsh: permission denied`;`sudo ./run.sh` → `command not found`。原因是文件权限为 644(无 +x),sudo 不会在当前目录找脚本。**不能用 sudo**(会把 data/board.db 变成 root 属主)。 - `chmod +x run.sh` 解决;同时重写 run.sh 使其健壮: - 自动挑选带依赖的 Python 解释器(`.venv` → workbuddy venv → python3 → /usr/bin/python3),都没有依赖则 pip install。 - 启动前用 `/health` 探测端口,已在运行则直接打印地址并退出(避免重复起进程报错)。 - 首次运行才走建库 + import_excel + migrate_custom。 - 开关:`PORT=` 换端口、`DEV=1` 开 --reload、`NO_OPEN=1` 不自动开浏览器。 - 默认不以 --reload 启动(日常使用更稳),需要热重载时用 DEV=1。 ## 新增周期能力 + 年份整体平移 - 需求:录入新周期数据时能新增周期(如「9月第1周」);把数据里 2024→2025、2025→2026(用户原话「2024年改为20205年」为笔误,按整体 +1 理解)。 - 已执行 `python scripts/shift_year.py`:49 个周期全部 +1 年,2024→2025 / 2025→2026,sort_key 重算且校验无重复;执行前备份 `data/board_before_year_shift.bak`。 - **关键坑**:2024 与 2025 年份同时存在,必须按年份从大到小更新,否则 2024→2025 会撞上尚未更新的 2025。 - 周期标签解析统一到 `app/constants.py::parse_period_label`(正则放宽以支持「2025/08-第1周」这种没有「月」字的写法),`scripts/import_excel.py` 改为复用,去掉了重复实现。 - `POST /api/periods` 年份推断规则:**只比较月份**。month >= 最新周期月份 → 同年;month < 最新周期月份 → 次年。 - **踩过的坑**:最初用 `(month, week) <= (latest.month, latest.week)` 判断,导致「9月第1周」在已有「9月第1周」时被判为跨年,重复创建出 2027 年的周期。改为只比月份后,补录同月更早周次也能正确留在同年。 - `DELETE /api/periods/{id}` 只允许删除无任何记录(通用 + 定制)的空周期;设置页新增「周期管理」表格,列出各周期记录数并提供删除按钮。 - 前端三个新建入口:顶部周期下拉、更新录入页(常规/定制各一个)周期下拉、设置页周期管理,都带「+ 新建周期…」。 - 当前状态:50 个周期(2025 年 30 个 / 2026 年 20 个),最新为 2026年9月第1周。 ## 项目合并 + 项目库增删改(2026-09-13 补记) - 需求:把「智空无人机平台」并入「智飞无人机平台」;项目库提供更名 / 删除 / 归档。 - 新增通用合并脚本 `scripts/merge_project.py --from A --to B`(备份 `data/board_before_merge.bak`)。 - **三个必须记住的坑**: 1. `Project`/`Task` 的 relationship 带级联,直接 `db.delete(task)` 会把它名下 Update 的外键**置空**(Task.updates 没配 cascade → 默认 nullify)。所以执行记录改挂一律用 bulk `db.query(Update).filter(...).update({...}, synchronize_session=False)`,不经过 ORM 关系。 2. session 是 `autoflush=False`,改完不 flush 就查询会读到旧数据(第一版统计出"孤儿记录 8 条",实际是重复遍历同一批对象)。改完必须显式 `db.flush()`。 3. 删源项目前要 `db.expire_all()`,否则 identity map 里的旧关系对象会被级联误删已转挂的数据。 - 结果:归并同名子任务 7 个、迁移独有子任务 32 个,执行记录总数 454 条前后一致(无丢失无重复);子任务 428→329。 - 项目库功能:表格行内加 `更名 / 归档 / 删除`,抽屉里也有同名操作;定制交付的项目明细表与抽屉同步支持。 - 后端 `PATCH /projects/{id}` 增加名称非空 + 重名校验(400 明确提示);`DELETE` 返回 `{name, deleted_tasks, deleted_updates}` 供前端提示。 - 删除是硬删除且级联,已用孤儿检查确认干净(tasks/updates/custom_updates 均无悬空外键)。 - 查孤儿数据时踩到:SQL 的 `LIKE '__%'` 里 `_` 是单字符通配符,会匹配所有记录;要查字面下划线得转义或在 Python 侧过滤。 ## 入库时间 / 区域规范 / Excel 导出 - 需求三件套:定制明细加入库时间、区域改规范下拉并支持筛选、提供 Excel 导出。 - **入库时间 `custom_projects.entry_date`**(口径:该项目最早记录所在周期的第一天) - 回填脚本 `scripts/add_entry_date.py`(备份 `data/board_before_entry_date.bak`),119/119 全覆盖,范围 2025-04-14 ~ 2026-08-17。 - 取值优先级:Excel《项目明细表》台账周(优先,项目正式登记入册的地方)→《周工作情况总结》定制开发块 → custom_updates 最早周期 → period_id → 创建日期。 - **踩坑**:一开始只从 custom_updates/period_id 取,结果大部分落到"创建日期"——因为当初导入台账时周期列(合并单元格的稀疏列)**没做向下继承**,period_id 大多是 NULL。改为回查原始 Excel 才补全。 - 周期→日期:`period_first_day()`,以该月第一个周一为第 1 周,第 N 周顺延 7×(N-1) 天。2026-08-01 是周六 → 8月第1周是 08-03,8月第4周是 08-24。 - **区域规范化**:`REGION_GROUPS` 定义 34 个省级行政区并按大区分组(华北/东北/华东/华中/华南/西南/西北),下拉用 optgroup;筛选下拉只列实际有数据的区域。用简称写法与历史数据保持一致,避免迁移;`normalize_region` 会把「内蒙古自治区」等归一。 - **Excel 导出**:`app/export.py` 通用 `build_xlsx()`(表头样式/冻结首行/自动筛选/列宽自适应/金额千分位),`app/routers/export.py` 提供 4 个端点,前端 `download()` 触发。中文文件名用 RFC 5987 编码。 - **SQLAlchemy 加字段的坑**:`Base.metadata.create_all()` 只建新表,**不会给已有表加列**。必须显式 `inspect(engine).get_columns()` 判断后 `ALTER TABLE ... ADD COLUMN`。 ## 热加载 + 项目管理改名/操作列可见 + 抽屉周期切换(2026-09-13) - **热加载**:`run.sh` 的 DEV 分支加 `--reload-include "*.js" "*.css" "*.html" ".env"`(前端和 .env 改动也重启);`app/config.py` 加 `DEV`,`app/main.py` 生成 `BOOT_ID` 并由 `/health` 返回 `{dev, boot}`;新增 `web/livereload.js` 轮询 `/health`,`boot` 变了就 `location.reload()`。 - **坑 1**:macOS 自带 bash 3.2 下空数组 + `set -u` 会 unbound variable,展开要写 `${RELOAD_ARGS[@]+"${RELOAD_ARGS[@]}"}`。 - **坑 2**:`--reload-include` 是按 glob 匹配的,`data/*.db` 不在列表里,所以写库不会触发重启(否则自己把自己刷死)。livereload 只在 `boot` 变化时刷新,轮询本身不会死循环。 - **项目库 → 项目管理**:导航、卡片标题、导出表名/文件名、README 全部改名(`app/routers/export.py` 的 sheet 名和文件名一起)。 - **「更名/归档/删除 看不到」的原因不是功能缺失**,是表格总宽超出容器后被横向截断,最右的操作列掉到可视区外。修法是两手:① 收紧各列(分类 92 / 负责人 76 / 状态 84 / 进度 56 / 标签 96,名称 min-width 170)+ 操作列按钮 `padding: 0 7px`,1280 宽下 `scrollWidth - clientWidth == 0`;② 操作列直接复用已有的 `col-ops` sticky 类(`position: sticky; right: 0`),窗口窄的时候按钮始终吸在右边。 - **坑 3**:`border-collapse: collapse` 时 `position: sticky` 的 `td` 不生效,必须 `border-collapse: separate; border-spacing: 0`。 - 一开始另写了个 `.col-act` 类和 `.col-ops` 一模一样,属于重复,已合并回 `col-ops`。 - **详情抽屉的周期反馈改成可切周期**:`state.dw = {group, range, back}`,`feedUnits()` 按周/按月归组,`FEED_RANGES` 给两套范围(周:本周/近4/近12/全部;月:本月/近3/近6/全部),`‹ ›` 按 `range` 步进翻页,到边界自动 disabled。锚点取顶栏选中周期所在单元,所以抽屉和看板顶部口径一致。 - `renderFeed()` 自己 `$("#dwFeed")` 取宿主节点,切组/切范围/翻页只重渲染这一块,不重新请求接口。 - **坑 4**:`d.timeline` 后端已按 `sort_key` 升序,归组时直接 `map`/`forEach` 保持顺序即可,不要再 `reverse()`(历史进度走势那一块才需要倒序)。 - 「更早还有 7 月」会被读成「7 月份」,月的量词统一用「个月」。 ## 项目管理列表页去掉标签列(2026-09-13 下午) - 需求「列表页不显示标签字段」按**祈使句**理解 = 列表页不要标签列(不是「标签没显示出来」的 bug 报告)。依据:`tags`、`project_tags` 两张表都是 0 行,全库没有标签数据,那一列在 5 行里全是空输入框 + 被截断的 placeholder,只是白占名称列宽度。 - 改动(`web/app.js`):删行内 `` 的 tags 单元格、删表头 `标签`,并连带删掉因此不可达的 `else if (field === "tags")` 保存分支(自己产生的孤儿代码)。抽屉里的标签编辑 `#dwTags` 和周期看板卡片上的标签胶囊**保留**,用户只说了列表页。 - 回归验证:表头 `[项目名称, 分类, 负责人, 状态, 进度%, 最近更新, 操作]`、`scrollWidth-clientWidth == 0`、操作列仍 sticky;`[data-f]` 只剩 `category_id/owner/status/progress`。 - **坑**:`locator.fill()` 只写 DOM value,**不触发 change 事件**,PATCH 根本没发出去,会误判成「保存功能坏了」。要验证行内保存必须走真实键盘:AX `click` 聚焦 → `typeText` → `pressKey Tab`。测完 toast「已保存」、副本库 owner 更新、真实库 owner 与 tasks/updates 计数(329/454)前后一致。 - 写操作用 `DATABASE_URL=sqlite:////tmp/copy.db`(`config.py` 的 `_load_dotenv` 用 `setdefault`,外部 export 优先)起独立实例,绝不连真实库。 - 另:`run.sh ... | tail -30` 会把 uvicorn 日志憋到进程退出才吐,排障时别接 tail。 - **发现 8770 上的服务已停止**(原 PID 17513 不在、端口无监听,不确定是用户自己停的还是上一轮误杀),已用 `NO_OPEN=1 nohup ./run.sh` 重启,`/health` 返回 `dev:false`(不带 --reload)。 ## 「8770 还是老系统」排查(2026-09-13 下午) - 结论:**不是服务端跑旧代码**。`sha256(web/*)` 与 `curl /static/*` 返回的内容逐个比对全部一致(app.js bd9b07b1…、styles.css 817480ff…、index.html a983b5af…)。前端零构建、`StaticFiles` 每次请求现读磁盘,所以**即使有旧 Python 进程在跑,界面也只由 `web/` 目录决定**——界面旧必然是客户端问题。 - 两个叠加原因: 1. 用户 `./run.sh` 其实没启动成功:8770 被我(Codex)起的实例占着,run.sh 的端口探测命中后打印「端口 8770 上已有服务在运行」就 `exit 0`,看起来像启动成功了。**教训:不要长期占着用户的服务端口**,最后 `launchctl remove com.unis.board` 交还端口,只留一个普通 uvicorn(非 launchd)方便 `lsof -ti:8770 | xargs kill`。 2. 浏览器缓存:`/`、`/static/*` 之前只有 ETag/Last-Modified,**没有 Cache-Control**,Chrome 走启发式新鲜度缓存,可能长时间不回源;SPA 又只在加载时读一次 app.js。 - 修法(`app/main.py`):加 `no_cache_frontend` 中间件,对 `/` 和 `/static/*` 发 `Cache-Control: no-cache`。验证:四个静态资源响应头都带 no-cache,带 `If-None-Match` 复访返回 **304**(本地零开销);`/api/*` 不受影响。 - **坑**:Starlette 1.6.0(fastapi 0.141.1)的 `StaticFiles.__init__` **已经没有 `headers` 参数**了(`(directory, packages, html, check_dir, follow_symlink)`),传 `headers=` 直接 TypeError。要加响应头只能上中间件。 - 已缓存的旧副本仍需用户**硬刷一次**(Cmd+Shift+R)强制回源,之后才自动走 no-cache。 ## 列表只留「修改」+ 更名收进详情面板 + 周期反馈倒序(2026-09-13 傍晚) - 需求里的「修改」按钮**原本不存在**(列表上是 详情/更名/归档/删除 和 改名/归档/删除)。按「每行只留 1 个按钮,标签为『修改』,点了开详情面板」实现;改名/归档/删除两个抽屉里本来就有,功能不减。定制交付的「列表」= 项目明细 tab(阶段看板/收入分析不动)。 - `web/app.js`: - 两处操作单元格压成一个按钮(`data-open` / `data-copen`,处理器原本就有);删掉随之失效的 6 个行内处理器(`[data-rename] [data-arch] [data-delp] [data-crename] [data-carch] [data-cdelp]`)和只被它们调用的 `findCustomRow()`(孤儿)。`openRenameCustomDialog` → 通用 `openRenameDialog(url, name, onDone)`,两处调用传各自的 PATCH 地址。项目管理抽屉的 `#dwRename` 原来是 `prompt()`,跟全站风格不一致,一并改成走同一个 modal。 - **坑**:参数改名时漏了模板里的 `value="${esc(name)}"`,而浏览器里 `name` 是全局 `window.name`,**不会报 ReferenceError**,只会静默渲染成空输入框。`node --check` 也查不出来。改全局作用域里的变量名要专门 grep 一遍裸 `name`。 - **周期反馈倒序**:只在展示层 `win.slice().reverse()` + 月组内 `u.periods.slice().reverse()`。**不能反转 `feedUnits()` 的返回值**——`anchor/first/last/back/step` 全依赖升序下标,反了以后 ‹ › 翻页和「更早还有 N 周」会全错。实测:近 4 周显示 8月第4周→8月第1周,点 ‹ 变 7月第4周→6月第4周 且 meta 从「更早还有 8 周」变「4 周」,语义不变。 - 验证仍走 `DATABASE_URL=sqlite:////tmp/copy.db` 的副本实例(8801):项目管理/定制明细操作列均只剩 `["修改"]` 且 sticky;两条更名链路(含重名报错「已存在同名项目…请换一个名称」保留在 modal 内、确定按钮重新可用)都通过;真实库未动。 ## 列表彻底只读 + 详情/修改双模式(2026-09-13 傍晚第二轮) - 需求「列表保留 详情/修改/归档、详情面板不改基本信息、基础信息按钮与附表更新信息要区分、删除要逐字输入项目名」。**上一轮刚把操作列压成一个「修改」,这一轮又拆回三个**——需求在反复,别再往一个方向优化,改成 `openDrawer(pid, mode)` / `openCustomDrawer(cid, mode)` 双档后,按钮数量怎么变都只是模板里几行。 - 改动(只在 `web/app.js`、`web/styles.css`,Python 一行没动): - 两张列表彻底去行内编辑:`renderProjects` 删 `[data-pf]` 单元格与 change 处理器,负责人/状态/进度改 `esc()`/`statusPill()`/`NN%`;`renderCustomList` 删 `[data-cf]` 与 `bindCustomListEdit` 里的 PATCH 块,函数更名 `bindCustomList`(调用点同步),金额列统一 `fen()` 并在表头标 `/万`,`stagePill`/日期纯文本。两处星标改 `class="star readonly"` 去掉 `data-star`(快速切重点没了,抽屉里有按钮)。 - 抽屉双档:`mode` 只影响「基础信息」区块——view 出 `.ro` 只读行 + `.sec-actions`「基础信息 · 操作」(修改/重点/归档/删除),edit 出表单 + `.sec-actions`「基础信息 · 编辑」(只有保存/取消 + `#dwErr`/`#cdErr` 行内报错)。附表(周期反馈、历史走势、周期执行记录、阶段流转、收入确认计算值)一律挂 `…只读`,和基础信息的操作条在视觉上分开。 - 改名从独立弹窗并入「修改」表单首行(`#dwName`/`#cdName`,后端两个 PATCH 本来就收 name 且做非空+重名校验),`openRenameDialog` 变成孤儿已删。保存失败写 `#dwErr` 并解禁按钮,成功后回 view 档并 `renderProjects()`。 - 删除:`confirmDialog({danger, okText:"永久删除", requireText: p.name})`,`#mOk` 初始 disabled,`#mConfirmText` 的 input 事件里 `trim() !== requireText` 就继续禁用。定制与项目管理两处都走这条。 - **两个自己发现的设计破绽(已修)**: 1. 定制抽屉「收入确认」区块在 edit 档仍标着「计算值 · 只读」,却渲染了开票/回款输入框 → 徽标改成随 mode 切换(`开票与回款可编辑`),两个输入框包进 `.sec-actions` 并补一句「随上方「保存」一并提交」(保存按钮在基础信息那条操作条里,不然用户不知道点哪提交)。 2. `
%
` 里裸的 `%` 是**第三个 grid item**,`.form-row` 只有两列,`%` 会掉到下一行。包一层 `
` 解决(`#dwProgress`、`#cdProgress` 各一处)。 - **严重事故(上一轮):`src.index(marker)` 静默删掉 174 行**。用 `src[a:b]` 做区间替换时锚点 `b` 取的是「下一个 marker 的位置」,而那个 marker 在文件里出现在预期之外的位置,于是中间 174 行被整段吞掉。**`node --check` 依然通过**(因为吞掉的是一整个完整函数),语法检查完全查不出来。 - 教训:非 git 仓库里做区间替换,改完必须**比对函数名清单**(`grep -n "function " old new` 做集合差),不能只看语法;优先用「唯一锚点整串替换 + `assert src.count(old)==1`」,别用 index 区间。 - **并发写入风险(本轮实测)**:`web/app.js` 在被本任务修改期间出现 `14:02 → 14:04 → 14:05 → 14:06` 的整文件重写,说明另一个会话也在改同一个文件(不是 git 仓库,没有回滚手段)。开工前 `stat` 连测 45s 确认 mtime 不动了才动手;接手时务必先 `stat` + `grep` 特征标记(`data-pedit`/`data-cedit` 应存在、`openRenameDialog` 应为 0)。 - 验证(副本实例 8801 + `DATABASE_URL=sqlite:////tmp/copy.db`,真实库未动):两张表操作列集合分别 = `{详情/修改/归档}`、归档行 = `{详情/修改/恢复}`;`tbody` 内 `input,select,textarea` 计数 = 0;详情抽屉可编辑控件 = 0;改名为已存在名称 → `#dwErr`「保存失败:已存在同名项目「AI Agent」,请换一个名称」且按钮解禁;真实改名往返成功(列表与抽屉标题同步);删除弹窗输入前缀「NEX云桌面」按钮仍 disabled、输入全名才解禁;侧栏分类/只看重点/含归档/全局搜索、7 个视图逐个点开均正常,`tab.dev.logs()` 空。 ## 列表/详情职责边界 + 附表写入分流(2026-09-13 傍晚第三轮) - 需求四条:① 列表只留 详情/修改/归档;② 详情≠修改(详情面板不改基本信息,外层也不给直接改);③ 详情面板里基础信息的按钮与附表更新信息要区分清楚;④ 删除必须逐字输入项目名。接手时 ①②④ 已由上一轮做完,本轮补 ③ 和两处「外层还能直接写数据」的漏网。 - **边界判断(都记下来,下次别重新猜)**: - 「项目」= 项目管理 + 定制交付两张表。**每周重点工作的条目不是项目**,它的删除仍用普通危险确认,不要求逐字输入名称(条目名短且可重建,逐个敲全名太折磨人);用户如果要统一,再改。 - **工作看板右栏的环节日期 / 开票 / 回款行内快填保留**。那是「本周快录」区,不是项目详情面板,砍掉会让每周例行录入多跳三次页。用户的原话是「详情面板不做基本信息的修改」,范围限定在详情面板。 - 附表(周期反馈 / 交付记录)**写入统一走「更新录入」**,不在抽屉里复刻一套录入表单:录入页有「粘贴原文 → AI 整理 → 确认写入」的整链路,抽屉里再造一个简版表单必然出现两条口径不一致的写入路径。 - 「+ 录一笔」落在**顶栏当前选中的那一周**(`state.weekId`),和看板其余部分同一个锚点,避免抽屉里选 A 周、顶栏显示 B 周。 - 本轮改动(`web/app.js` + `web/styles.css` + README,Python 没动): - **定制交付阶段看板卡片的 ★ 曾直接 `PATCH /custom/projects/{id} {is_key}`**,是「外层不再直接写数据」的漏网。改成 `class="star readonly"` + `title="重点标记在详情面板中维护"`,删掉 `data-star` 处理器和 kcard 点击里的 `if (e.target.dataset.star) return;` 守卫;重点标记只剩抽屉里那一个按钮。 - `.sec-actions` 加两个变体:**`.base`**(蓝实线左边框)= 基础信息,两条 edit 条各加 `.sec-hint` 明确「只改基础信息,不会动下面的周期记录」;**`.upd`**(绿底 + 绿虚线边框)= 附表 · 更新记录,放「+ 录一笔「2026年9月第1周」执行记录 / 交付记录」,handler 里 `state.intakeType` + `state.intakePeriodId` + `intakeProjectId`/`customIntakeId` → `closeDrawer()` → `setView("intake")`。 - 周期反馈的范围切换标签从「查看范围」改成「**查看范围(只读筛选)**」,和动作按钮区分开,避免被当成一种写操作。 - **本轮最大的坑:补丁脚本静默 no-op,还打印成功。** 上一轮的 `patch2.py` 写了 `new = old + '''…样式…'''` 但**漏了 `src = new`**,最后 `f.write(src)` 把原文原样写回,脚本照样 print「styles.css patched ok」。结果 `.sec-actions.base/.upd`、`.sec-hint` 三条规则全都不存在,界面能跑但两类操作条**完全没有颜色区分**(即需求 ③ 事实上没生效),而上一轮的自述里写着「已追加」。 - 发现方式极便宜:`diff /tmp/um_bak2/styles.css web/styles.css` 是空的,而 mtime 却是补丁时间。 - 教训:补丁脚本必须 `assert out != src` + 写完后 **grep 特征串复检**,绝不能信脚本自己 print 的成功;改完先看 diff 再看语法。同类 bug 在 `patch1.py` 里没出现,是因为它用 `rep()` 里 `global src; src = src.replace(...)`,写 src 反而是对的 —— 说明「看脚本最后一行写了什么变量」不够,得看**产物**。 - **CUA 浏览器沙箱的硬限制(本轮全部踩过)**:`tab.playwright.evaluate` 里 **`el.click()` 不是函数、`MouseEvent` 不是构造器**、没有 `fetch`、拿不到页面闭包里的 `state`。点击只能走 `tab.playwright.locator(sel).click()` / `getByRole(...).click()`(注意 `getByRole`/`locator` 挂在 `tab.playwright` 上,`tab` 本身没有)。 - strict mode 会因多匹配直接超时:`getByRole('button',{name:'定制交付'})` 命中导航/侧栏分类/录入页签 3 个。用属性选择器收窄,如 `nav [data-view="custom"]`、`#view-custom [data-ctab="list"]`。`locator(sel, {hasText})` 在这层封装里**不生效**。 - `#view-custom` 有 4 个页签,默认「交付流水线」,`#view-custom tbody tr` 第一个表是**卡点项目表**(操作列只有「处理」);要测项目明细必须先点 `[data-ctab="list"]`,否则会误报「定制列表操作列丢了」。 - `[id^=view-]` 元素的 `offsetParent` **永远非 null**(全部报 VISIBLE),判断当前视图别看这个。看 `select` 的选中项 / `.chip.active` 更可靠。 - `tab.getScreenshot()` 返回 Uint8Array,直接 `nodeRepl.emitImage(...)`;`JSON.stringify` 会喷出数万 token。 - 实测(副本实例 8801 + `/tmp/copy.db`,真实库未动;`tab.dev.logs()` 空):两张列表操作列 = `[详情, 修改, 归档]`、`tbody` 内 input/select/textarea = 0;详情抽屉可编辑控件 = 0,修改档 6 个控件 + hint;两条操作条 computedStyle 分别是 `3px solid rgb(37,99,235)` 与 `3px solid rgb(22,163,74)`(绿虚线底);删除闸门:`""`/`NEX`/`NEX云桌` → disabled,`NEX云桌面` 与首尾带空格的同名 → enabled(`trim()` 后比对);两处「+ 录一笔」都正确落到更新录入并预选项目与周期(常规 → `intakeProject`/`intakePeriod`,定制 → `cInProject`/`cInPeriod` 且页签切到「定制交付项目」)。 ## 四模块收尾:台账残留退役 + README 按新结构重写(9/13 下午) - 上一轮把功能做完了,**README 还停在「平台产品类 / 周看板」的旧结构**,本轮唯一硬缺口就是文档;顺手清掉旧结构残留的「项目台账」。 - **台账退役的判断顺序**:先 `sqlite3 data/board.db "select count(*) from ledger_items"` → **0 行**,确认删界面无数据损失,才动代码。删的只有「暴露出去的那层」:`GET /api/ledger` 函数、`/api/meta` 里的 `ledger_categories`、`index.html` 的 `#view-ledger`、`app.js` 的 `renderLedger`/`state.ledger`/`refresh()` 分支。**保留** `LedgerItem` 模型 + `constants.LEDGER_CATEGORIES` + `scripts/import_excel.py` 的台账解析(导入链路是历史 Excel 的完整还原,删它属于扩大范围;要不要连导入一起退役,留给用户拍)。改完 `/api/ledger` 在 8770/8802 都返回 404。 - **README 重写的取舍**:开篇「两套体系」表改成「四模块 + 三支撑页」表;`## 功能` 拆成 `### 1. 工作看板` / `### 2. 项目管理` / `### 3. 定制交付` / `### 4. 每周重点工作`;原「周看板」小节升格成「工作看板」并补第三组清单的来源;原「执行面板」不再单列模块,写成看板底部「统计概览」折叠区(`
` + `GET /api/stats`)。数据模型加 `WeeklyFocus`,并给 `LedgerItem` 标注「只剩模型与导入解析,界面无入口、接口已下线」;API 速览补 `# 每周重点工作` 段、`/api/export/focus`、`/api/categories`,把 `/api/meta` 的说明从「状态字典 / 分类」改成「状态字典 / 优先级 / AI 配置」(**`/api/meta` 从来不含分类**,侧栏分类来自 `GET /api/categories`;`/api/board` 页面主链路已不调用)。 - **本轮自己的坑:列表切片赋值得到 1804 行。** `L[a:b+1] = board` 里 `board` 是**字符串**,Python 会把切片赋值的右值当可迭代对象**逐字符展开**,README 从 344 行炸到 1804 行且 `assert out != src` 照样通过。改成 `board.split("\n")` 才对。教训:切片赋值右值必须是 list;补丁脚本除了 `assert out != src`,还要 **断言行数量级**(`344 -> 384` 一眼合理),否则 diff 阶段才发现就晚了。 - **验证(副本实例 8802 + `/tmp/audit.db`,真实库 mtime 全程未变)**:`node --check` 通过;`grep -o 'function [A-Za-z0-9_$]*'` 多重集 diff 唯一差异是 `renderLedger`(预期删除),无新增孤儿;`py_compile` 通过;`/api/meta` 键 = `statuses/priorities/ai`(无 `ledger_categories`);8770 与 8802 的 `/static/{app.js,styles.css,index.html,livereload.js}` sha1 与磁盘一致;浏览器七视图(board/projects/custom/focus/staff/intake/settings)全部渲染、`tab.dev.logs()` 为空;看板三组 = `重点项目 3 / 定制项目 6 / 每周重点工作 2(1 条逾期)`,KPI 含「本周重点工作 1 · 逾期 1 · 已完成 1」;项目列表行操作列 = `[详情, 修改, 归档]` 且 `tbody` 内控件数 0;详情抽屉 view 档控件 0、两条操作条 `.sec-actions.base`(基础信息·操作)与 `.sec-actions.upd`(附表·更新记录);删除闸门 `initial/partial → disabled`、逐字全名 → `enabled`,取消后未删。 ## 每周重点工作改独立录入面板 + 面板动作条分上下(2026-09-13 傍晚第四轮) - 需求:① 周重点工作的新增改**单独面板**(列表顶部那张内嵌卡撤掉);② 预计完成时间录成「**1周 / 2周**」这种相对周;③ 提供**删除**;④ **所有基础信息相关的按钮放面板上方,更新相关的按钮放列表附表上方**。 - **周口径必须复用既有函数,别再手写一套**。`app/constants.py:period_first_day(y,m,w)` 定义「该月第一个周一为第 1 周」,「入库时间」已经是这个口径。第一版自己在 `focus.py` 里写了个 `_first_monday` 推导,跨月周次的第 5 周算歪,第二轮删掉它改成 `date.fromisoformat(constants.period_first_day(...))` 推日期再回算目标周的 `week`,前后端才一致(前端 `firstMondayDay(y,m)` 是它的镜像,只用来把「3周」显示成「2026年10月第1周」,落库换算以后端为准)。核对样本:2026-08 首个周一=03 ⇒ 8月第4周=08-24(与 README 一致)、09=07、2027-01=04。 - **相对周换算放后端**(`_resolve_period`):`weeks` + `base_period_id` → 目标周;目标周在 `periods` 里不存在就**自动建周**(label `"{m}月第{w}周"`,`sort_key = y*10000+m*100+w`),否则用户选到跨月的第 5 周会直接 400。`period_id` 从必填改成可空并保留旧用法;`weeks`/`period_id` 都不给 → 400,`weeks` 越界 → 422(`Field(ge=1, le=52)`)。PATCH 时前端只在**下拉改了**才发 `weeks`(编辑档首项 `不改(当前 …)` 的 value 是空串),否则改个错别字会把预计完成周悄悄挪走。 - **动作条上下分层的落地方式**:把原来嵌在「基础信息」section 里的 `sec-actions base` 整块**摘出来**当 `baseBar`,前置到 `#drawerBody` 的第一个子元素,加 `.top` 让它排在 `#drawerBody` 第一个子元素。我先做成了 `position:sticky;top:0` + 通栏负 margin + 阴影(吸顶常驻),**用户看完要求「按钮面板不要悬浮,嵌入到面板中」**,最终形态就是 `.sec-actions.top { margin: 0 0 14px; }` —— 继承 `.sec-actions` 的卡片样式(浅底 + 圆角 + 1px 边框 + 蓝色左边框)当普通块放在面板最上方。别再自作主张加 sticky/负 margin/阴影:面板内的动作条属于内容,不该漂在滚动容器上。`sec-actions.upd` 原样留在附表 section 标题下方,不动。项目抽屉 / 定制抽屉 / 重点工作抽屉三处同一套。 - **删除的确认强度按对象分档**(沿用上一轮定的边界):重点工作条目 = 普通危险确认,**不要求逐字输入**;逐字输入项目全名只用在项目管理与定制交付的项目删除上。列表行与抽屉顶部共用同一个 `deleteFocus()`,删完刷新计数、抽屉开着就一起关掉。 - **补丁脚本又踩两次同一个坑**: - 剪一段代码的 helper 写 `src[:i] + new + src[j+len(end):]` 把**结束锚点一起吃掉了**(`
` 少了半个),得写 `src[:i] + new + src[j:]` 保留锚点。 - 结束锚点 ` const body = shown.length ?` 在文件里**不唯一**,唯一性断言只对起始锚点做就会漏。要么加长锚点,要么对两端都 assert。 - 验证(副本实例 8803 + `/tmp/audit2.db`,**8770 真实库 mtime 全程未动**):`weeks=1/2/3` 分别落到本周边/跨月第5周且自动建周;不给时间 400、`weeks=99` 422、老 `period_id` 仍可用;`node --check` + `py_compile` 通过;函数名多重集 diff = +4(`firstMondayDay`/`weekShiftKey`/`dueWeeksHtml`/`deleteFocus`)−1(`weekSelectHtml` 成孤儿后删除);`sec-actions base top` 全库 6 处;浏览器侧列表内嵌卡已消失、行操作 `[详情,修改,归档,删除]`、新增面板下拉 `1周 · 2026年9月第3周 … 12周`、三个抽屉的首个子元素均为 `sec-actions base top` 且 `position:sticky`、view 档可编辑控件 0、`tab.dev.logs()` 为空。 - **待用户拍**(两轮未答,别自己决定):`scripts/import_excel.py` 里台账 `LedgerItem` 的导入解析要不要一起退役。 ## 抽屉 Title Bar 改「标准按钮 Bar」(2026-09-13 傍晚第五轮) - 需求原话:**「Title Bar 重新设计,不要采用当前的方式。就采用标准的按钮 Bar 模式。」** Default 模式下 `request_user_input` 不可用,按最大可能性解释并**在交付时明确声明了这个假设**:改的是**详情抽屉的标题栏**(`.drawer-head`),页面顶部那条周期栏(周/月、周期下拉、搜索、更新录入)没动,已在回复里说明「如指顶部周期栏可继续」。 - **别再自造「基础信息 · 操作」这种带标签的动作条了**。上一轮把动作条从 sticky 改成嵌在正文最上方,用户还是不满意 —— 他要的是**标准对话框按钮栏**:标题在左、动作按钮成排在右、✕ 最右,主按钮在最右、危险动作在最左。正文里那条自造的动作条整块删掉。用户这几轮的收敛方向是一致的:**去装饰、少自造、往通用 UI 约定靠**(去标签列、按钮别悬浮、按钮栏要标准)。 - **落地方式(三个抽屉同一套)**:`index.html` 抽屉头加 `
`(在 ✕ 之前)+ 标题包一层 `.drawer-head-text{flex:1;min-width:0}`;`app.js` 加 `drawerActions(html)` 一个 helper,`openFocusDrawer`/`openCustomDrawer`/`openDrawer` 各自把原来的 `topBar`/`baseBar` 变量改成 `drawerActions(...)` 调用;`closeDrawer()` 与三个抽屉的「加载中」处都清空按钮条(不清会残留上一个抽屉的按钮和它的闭包 pid)。**事件绑定一行都不用改**:`$("#dwEdit")` 之类是全局查表,按钮搬到标题栏照样命中。 - 跟着搬的两处:**报错提示**从动作条里挪到 `#drawerBody` 第一个子元素(`