38 KiB
38 KiB
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,必须映射。
- SQLAlchemy
- 启动:
./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。
- 自动挑选带依赖的 Python 解释器(
新增周期能力 + 年份整体平移
- 需求:录入新周期数据时能新增周期(如「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)。- 三个必须记住的坑:
Project/Task的 relationship 带级联,直接db.delete(task)会把它名下 Update 的外键置空(Task.updates 没配 cascade → 默认 nullify)。所以执行记录改挂一律用 bulkdb.query(Update).filter(...).update({...}, synchronize_session=False),不经过 ORM 关系。- session 是
autoflush=False,改完不 flush 就查询会读到旧数据(第一版统计出"孤儿记录 8 条",实际是重复遍历同一批对象)。改完必须显式db.flush()。 - 删源项目前要
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变化时刷新,轮询本身不会死循环。
- 坑 1:macOS 自带 bash 3.2 下空数组 +
- 项目库 → 项目管理:导航、卡片标题、导出表名/文件名、README 全部改名(
app/routers/export.py的 sheet 名和文件名一起)。- 「更名/归档/删除 看不到」的原因不是功能缺失,是表格总宽超出容器后被横向截断,最右的操作列掉到可视区外。修法是两手:① 收紧各列(分类 92 / 负责人 76 / 状态 84 / 进度 56 / 标签 96,名称 min-width 170)+ 操作列按钮
padding: 0 7px,1280 宽下scrollWidth - clientWidth == 0;② 操作列直接复用已有的col-opssticky 类(position: sticky; right: 0),窗口窄的时候按钮始终吸在右边。 - 坑 3:
border-collapse: collapse时position: sticky的td不生效,必须border-collapse: separate; border-spacing: 0。 - 一开始另写了个
.col-act类和.col-ops一模一样,属于重复,已合并回col-ops。
- 「更名/归档/删除 看不到」的原因不是功能缺失,是表格总宽超出容器后被横向截断,最右的操作列掉到可视区外。修法是两手:① 收紧各列(分类 92 / 负责人 76 / 状态 84 / 进度 56 / 标签 96,名称 min-width 170)+ 操作列按钮
- 详情抽屉的周期反馈改成可切周期:
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):删行内<td>的 tags 单元格、删表头<th>标签</th>,并连带删掉因此不可达的else if (field === "tags")保存分支(自己产生的孤儿代码)。抽屉里的标签编辑#dwTags和周期看板卡片上的标签胶囊保留,用户只说了列表页。 - 回归验证:表头
[项目名称, 分类, 负责人, 状态, 进度%, 最近更新, 操作]、scrollWidth-clientWidth == 0、操作列仍 sticky;[data-f]只剩category_id/owner/status/progress。- 坑:
locator.fill()只写 DOM value,不触发 change 事件,PATCH 根本没发出去,会误判成「保存功能坏了」。要验证行内保存必须走真实键盘:AXclick聚焦 →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/目录决定——界面旧必然是客户端问题。 - 两个叠加原因:
- 用户
./run.sh其实没启动成功:8770 被我(Codex)起的实例占着,run.sh 的端口探测命中后打印「端口 8770 上已有服务在运行」就exit 0,看起来像启动成功了。教训:不要长期占着用户的服务端口,最后launchctl remove com.unis.board交还端口,只留一个普通 uvicorn(非 launchd)方便lsof -ti:8770 | xargs kill。 - 浏览器缓存:
/、/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。
- 坑:Starlette 1.6.0(fastapi 0.141.1)的
列表只留「修改」+ 更名收进详情面板 + 周期反馈倒序(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行内报错)。附表(周期反馈、历史走势、周期执行记录、阶段流转、收入确认计算值)一律挂<span class="sec-kind ro">…只读</span>,和基础信息的操作条在视觉上分开。 - 改名从独立弹窗并入「修改」表单首行(
#dwName/#cdName,后端两个 PATCH 本来就收 name 且做非空+重名校验),openRenameDialog变成孤儿已删。保存失败写#dwErr并解禁按钮,成功后回 view 档并renderProjects()。 - 删除:
confirmDialog({danger, okText:"永久删除", requireText: p.name}),#mOk初始 disabled,#mConfirmText的 input 事件里trim() !== requireText就继续禁用。定制与项目管理两处都走这条。
- 两张列表彻底去行内编辑:
- 两个自己发现的设计破绽(已修):
- 定制抽屉「收入确认」区块在 edit 档仍标着「计算值 · 只读」,却渲染了开票/回款输入框 → 徽标改成随 mode 切换(
开票与回款可编辑),两个输入框包进.sec-actions并补一句「随上方「保存」一并提交」(保存按钮在基础信息那条操作条里,不然用户不知道点哪提交)。 <div class="form-row"><label>进度</label><input/> %</div>里裸的%是第三个 grid item,.form-row只有两列,%会掉到下一行。包一层<div style="display:flex">解决(#dwProgress、#cdProgress各一处)。
- 定制抽屉「收入确认」区块在 edit 档仍标着「计算值 · 只读」,却渲染了开票/回款输入框 → 徽标改成随 mode 切换(
- 严重事故(上一轮):
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 区间。
- 教训:非 git 仓库里做区间替换,改完必须比对函数名清单(
- 并发写入风险(本轮实测):
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。
- strict mode 会因多匹配直接超时:
- 实测(副本实例 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. 每周重点工作;原「周看板」小节升格成「工作看板」并补第三组清单的来源;原「执行面板」不再单列模块,写成看板底部「统计概览」折叠区(<details class="ov">+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):]把结束锚点一起吃掉了(</div>少了半个),得写src[:i] + new + src[j:]保留锚点。 - 结束锚点
const body = shown.length ?在文件里不唯一,唯一性断言只对起始锚点做就会漏。要么加长锚点,要么对两端都 assert。
- 剪一段代码的 helper 写
- 验证(副本实例 8803 +
/tmp/audit2.db,8770 真实库 mtime 全程未动):weeks=1/2/3分别落到本周边/跨月第5周且自动建周;不给时间 400、weeks=99422、老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抽屉头加<div class="btn-bar" id="drawerActions"></div>(在 ✕ 之前)+ 标题包一层.drawer-head-text{flex:1;min-width:0};app.js加drawerActions(html)一个 helper,openFocusDrawer/openCustomDrawer/openDrawer各自把原来的topBar/baseBar变量改成drawerActions(...)调用;closeDrawer()与三个抽屉的「加载中」处都清空按钮条(不清会残留上一个抽屉的按钮和它的闭包 pid)。事件绑定一行都不用改:$("#dwEdit")之类是全局查表,按钮搬到标题栏照样命中。 - 跟着搬的两处:报错提示从动作条里挪到
#drawerBody第一个子元素(<div class="notice err" id="dwErr|cdErr|fdErr" style="display:none">,只在 edit/create 档渲染,JS 里err.textContent=…/style.display=""原样可用);「只改基础信息,不会动下面的周期记录」这类 hint 挪进它自己的.sec-title下面一行。按钮文字去掉对象名后缀(删除项目/删除条目 → 删除、恢复项目/恢复条目 → 恢复),标题栏已经写了对象名。定制抽屉那条「随上方「保存」一并提交」改成「随标题栏「保存」一并提交」—— 按钮搬家,指路的文案必须跟着搬。 - 被本次改动变成孤儿的 CSS 要清:
.sec-actions.base、.sec-actions.top、.sec-actions .notice三条全删(.sec-actions与.sec-actions.upd留着,附表的「+ 录一笔」还在用)。.drawer-head改align-items:center,h3与.drawer-sub都加nowrap+overflow:hidden;text-overflow:ellipsis,标题栏才不会被「★ 青岛中车四方园区治安道路项目」这种长名撑成三行。 - ⚠️ 同一 workspace 有两个会话在写文件:本轮开工时
web/app.js在 15:45:49 / 15:46:14 / 15:46:51 被另一个 agent 连续改写(3050 → 3153 行,锚点行号整段漂移)。教训:动手前先轮询shasum到连续 60s 不变,再cp一份新备份(/tmp/um_bak6/),补丁脚本一律「唯一锚点 +assert count==1+assert out!=src」,改完node --check+ grep 特征串复检。绝不要拿着几分钟前读到的行号去改。 - 验证(全程只在 8770 只读浏览,点的全是取消/关闭,真实库一条没动):
weekly_focus=1 / max(periods.id)=50 / projects=82 / custom_projects=119与开工前一致,/health200。浏览器实测:项目 view 档标题栏按钮[删除,归档,取消重点,修改]且正文 0 个可编辑控件、正文仅剩.sec-actions.upd;edit 档[取消,保存]、正文首元素notice err (display:none)、hint 已进 section;定制 view[删除,归档,标记重点,修改]/ edit[取消,保存];重点工作 create[取消,添加]、view[删除,归档,修改];.drawer-headalign-items:center、标题 ellipsis、drawer-head高 73px(标题 1 行 + 副标题 1 行);tab.dev.logs()为空。