# 更新日志 本项目遵循[语义化版本](https://semver.org/lang/zh-CN/):`主版本.次版本.修订号`。 版本号在 `server.py` 的 `VERSION` 常量里,页脚与顶栏徽标由 `/api/status` 动态取。 ## [1.4.1] — 2026-10-01 只修界面:侧边栏收起后的图标对齐(纯前端样式,接口与数据都没动)。 ### 修复 - **收起侧边栏后,轨道里的图标全都歪了**:收起态一直是靠 `opacity: 0` + `width: 0` 藏文字的,但 `width: 0` **只是把元素压成 0 宽,它占的位置还在**,于是每个图标被推开的 距离都不一样(对着截图量出来的:卫星图标偏左 11px,历史、设备这两个带角标的偏左 17.5px,底部状态点偏左 15px,头像偏右 1.3px,中轴本该在 33px 处)。现在收起态把这些 「位置残渣」一并清掉:导航项 `gap: 0`、角标与顶栏标题 `display: none`(标题那 19px 的 行盒在纵向排布时还会白占一行)、账号区(头像 30 + 退出键 30 并排放不进 66-20 的宽度) 改纵向居中。规则限定在 `@media (min-width: 901px)`,窄屏抽屉(244px)不受影响。 - **收起之后那个折叠按钮根本点不到**:它原本靠 `margin-left: auto` 顶在顶栏右侧, 顶栏一行要放下「图标 19 + 两个 10px 间隙 + 按钮 30」=69px,而收起后只有 66px, 按钮被挤出轨道裁掉了 —— 现象是收起后只剩 Ctrl+B 能展开。现在顶栏在收起态改纵向排布, 按钮与图标都落在中轴上,按钮也回来了。 - **窄屏抽屉里不显示用户名**:`.app.collapsed .sidebar-user-text { display: none }` 写在 样式表末尾,而媒体查询**不增加优先级**,于是「在桌面收起过、之后再用手机打开」时, 抽屉里只剩头像和退出键。补一条写在它之后的 `@media (max-width: 900px)` 覆盖回来。 ## [1.4.0] — 2026-10-01 访问控制:加登录、加用户管理,默认账号首次登录强制改密。 ### 新增 - **登录页与访问控制**:所有 `/api/` 接口都需要登录(静态资源和 `/api/auth/state` 除外, 否则登录页自己都加载不出来)。默认账号 `admin` / `admin`,**首次登录必须修改密码**, 这一步由服务端拦截(未改密前访问业务接口一律 `403 password_change_required`), 不是靠前端藏按钮。 - **用户管理页**(侧边栏新增入口,仅管理员可见):新建账号、重置他人密码、 在 `admin` / `viewer` 之间切换角色、删除账号。列表里带**最近登录 IP**, 方便确认账号是不是被人从别的机器登过(旧记录没存过这个字段,会显示 `—`, 下次登录后就有了)。 - **两种角色**:`admin` 可以改设备 / 起扫描 / 管用户;`viewer` 只读——能看首页、 台账、历史,但所有改状态的请求都会被服务端拒绝,界面上对应的按钮也直接隐藏。 - **本地管理员令牌**:`data/local_token`(权限 0600,首次启动自动生成)供同机的 命令行工具使用(桌面启动脚本、测试)。**没有做「回环地址免认证」**——容器是 host 网络时,回环免认证等于把整个局域网敞口。 ### 安全 - 密码只存 **scrypt 加盐哈希**(n=2¹⁴,每次随机 salt),不落明文、不回显、不写日志; 校验用 `hmac.compare_digest` 常数时间比对。 - 会话是服务端令牌,盘上只存令牌的 **SHA-256 摘要**,文件泄露也不能直接拿来登录; 有效期 7 天。 - Cookie 带 `HttpOnly` + `SameSite=Lax`;改状态的请求额外校验 `Origin` 同源。 - **登录失败限速**:同一 (用户名, IP) 15 分钟内错 5 次、或同一 IP 错 20 次即锁定 15 分钟。 用户名那一维特意带上了 IP——只按用户名计数的话,局域网里任何人连错 5 次就能把 admin 锁死,等于用限速换了个更好用的拒绝服务。 - 响应补 `X-Content-Type-Options` / `X-Frame-Options` / `Referrer-Policy`。 - 新密码至少 8 位,且不能是常见弱密码、不能等于用户名、不能是同一字符重复。 ### 修复 - **拒绝请求时没读 body,会破坏 HTTP/1.1 长连接**:被拒的请求体残留在缓冲区里, 会被当成下一个请求的开头,表现为「同一连接上前一个请求被拒后,后续请求全部 400」。 curl 每次新建连接看不出来,浏览器和 node fetch 复用连接就会踩到。现在所有拒绝路径 都先排空请求体。 - **`data/local_token` 重启后失效**:文件里存的是 JSON,第一版直接读原始文本当令牌, 首次启动碰巧正确(刚生成的就是它),重启后读到的却是整段 JSON,导致同机命令行工具 全部 401。 - 用户表的时间列取错了格式化函数(重名函数被后声明的覆盖),空值没兜住会显示成 1970 年的 `08:00:00`。 - **`auth.py` 没被 COPY 进镜像**:加了模块却忘了同步 `Dockerfile`,镜像照样构建成功, 但 `docker run` 立刻崩在 `import auth`。构建不报错,很容易一路推到 Docker Hub 才发现。 现在 `tests/packaging.test.js` 会静态检查「被引用的本地模块是否都在 COPY 里」等 9 项。 - **HEALTHCHECK 打的是需要登录的接口**:加了认证后 `/api/status` 要登录,健康检查一直 401, 容器永远停在 `starting` / 变成 `unhealthy`。改打免登录的 `/api/auth/state`。 静态自检里也加了这条:健康检查的路径必须落在 `PUBLIC_API` 白名单内。 ### 变更 - 桌面启动脚本改用 `data/local_token` 访问 `/api/status`,不再依赖免认证接口。 - 「关于」页补上默认账号说明,并说明局域网内是明文 HTTP、跨公网请套 HTTPS 反代。 ## [1.3.0] — 2026-09-30 界面适配:设备总览可切四种密度,页面按常见分辨率重新收口。 ### 新增 - **首页可切换展示方式**:设备总览支持「列表 / 大图标 / 中图标 / 小图标」四种密度, 选择记在 `localStorage`,下次打开还是你选的那种。四种模式只切 `#home-grid` 的 `data-mode` 属性、布局全交给 CSS,所以切换是瞬时的,不会重新渲染、也不会丢掉滚动位置。 - 列表:一行一台,图标 + 名称 + IP + 类型 + 厂商/MAC,信息最全 - 大图标:类型与厂商都显示;中图标:多一行类型;小图标:只留图标 + 名称 + IP - **按常见分辨率适配**: - 大屏不再把内容限死在 1560px(2560 上两侧曾空出近 400px),按 1800 / 2200 / 2800 逐级放宽 - 顶栏标题不再被右侧信息挤成两行;窄屏(≤560px)收起那串本机信息 - 方块墙在手机上不再自己滚(内层滚动难操作、还会切掉最后一行),改由整页滚动 - 统计卡片在窄屏按内容自适应,`已知主机名` 这类长标签不再折行 - 设备台账表在手机上从 12 列精简到 6 列(≤480px 再收到 4 列),不用横向滚也能看全 ### 测试 `./tests/run.sh` 共 **9 套 157 项**(5 套离线 UI 145 项 + 3 套联调 + 1 套启动脚本 12 项)。 界面部分另外用无头浏览器在 390 / 768 / 1366 / 1440 / 2560 五个宽度实测, 四种密度与各断点都逐张截图核对过。 ## [1.2.0] — 2026-09-30 发布镜像、打磨日常使用的两处体验(看状态、发图标),并补上产品文档。 ### 新增 - **Docker Hub 公开镜像**:`hzjackboy/lan-device-scanner`,同时提供 `linux/amd64` 与 `linux/arm64` (NAS / x86 服务器 / 树莓派都能直接拉)。配套 `docker-push.sh` 一条命令构建并推送多架构镜像, 脚本会自动探测本地代理、校验登录状态、选对 buildx 构建器。 - **设备图标可人工指定**:设备管理编辑弹窗里提供 emoji 调色板(点选,也可粘贴任意 emoji), 留空则按设备类型自动识别。人工图标在**首页方块墙、设备管理列表、详情弹窗**里都优先生效。 调色板由自动识别那套图标去重生成,保证人工挑的图标和自动图标风格一致。 - **桌面启动器:状态面板常驻**。菜单顶部固定显示服务状态 / PID / 已运行时长 / 本机与局域网地址 / 版本·平台·网段 / 台账统计 / 定时重扫倒计时 / 当前任务,等待输入时每 3 秒**原地重画** (光标归位重写 + 清到屏幕末尾,不是 `clear`,所以不闪屏),不必再手动按 5 查状态。 - **桌面启动器:所有动作执行完都回到菜单**。每个分支(含输错序号)统一走倒计时, 倒数 6 秒自动回菜单、按回车立刻回;除「0) 退出」外没有任何路径会 `exit`。 - **`PRD.md`**:产品需求文档(背景与竞品缺口、用户画像、19 条需求及优先级、逐条验收标准、 非功能需求、数据模型、关键取舍、成功指标、风险、路线图)。 - **`tests/launcher.test.sh`**:桌面启动脚本回归测试 12 项。 ### 修复 - **启动器状态面板不刷新的假象**:`read -t` 超时在 macOS 自带 bash 3.2 上**返回 1**, 而不是常见的 `128+SIGALRM=142`,和 EOF 的退出码完全一样。原先按退出码判断超时, 于是每次空闲超时都被当成 EOF 直接退出菜单。改用「耗时是否等满超时秒数」区分。 - **`./tests/run.sh` 在最小环境里报 `node: command not found`**:Finder 双击的终端与 cron 的 PATH 不含 Homebrew,看起来像测试失败其实是环境问题。脚本里补了 PATH。 - **`tests/docker_smoke.sh` 在装了 buildx 后失败**:`docker build` 转走 BuildKit 并要写 `~/.docker/buildx`,受限环境报 `operation not permitted`。改为把 buildx 状态放进工作区。 ### 安全 - **Docker Hub 令牌不再明文落盘**:`docker login` 默认把凭据以 base64(等于明文) 写进 `~/.docker/config.json`,任何能读用户目录的进程都能拿到这个可推送镜像的令牌。 现在用 `docker-credential-osxkeychain` 存进 macOS 钥匙串,配置文件里只剩 `"credsStore": "osxkeychain"`。README 补了迁移步骤与那个反直觉的坑: 用了钥匙串后 `docker info` 的 `Username:` 行会消失,不能再用它判断登录状态。 ### 测试 `./tests/run.sh` 共 **9 套 138 项**(5 套离线 UI 126 项 + 3 套联调 + 1 套启动脚本 12 项), 另有 `tests/docker_smoke.sh` 容器冒烟测试 27 项。 ## [1.1.0] — 2026-09-28 从"能扫"做到"能管、能部署"。本次加入设备台账、定时重扫、容器化与回归测试。 ### 新增 - **设备管理(设备台账)**:按 MAC 为主键长期记录扫到过的设备,掉线的设备保留记录并标记离线。 支持人工编辑别名 / 分类 / 位置 / 标签 / 备注 / 类型覆盖,以及"关注""忽略"; 重扫不覆盖人工内容,可一键恢复自动识别。落盘 `data/devices.json`。 - **首页设备方块墙 + 在线离线环形饼图**:扫描结果以方块图标展示,饼图统计在线 / 离线占比。 - **首页定时重扫**:服务端 `AutoScheduler`(默认 3600 秒,最小 60 秒),关掉页面也继续跑, 配置落 `data/auto.json`,服务重启后按原节奏续排期。 - **侧边栏导航 + hash 路由**:首页 / 设备扫描 / 历史记录 / 设备管理 / 关于与说明。 - **批量删除**:设备管理支持行内删除、多选批量删除、一键清除离线 / 忽略 / 全部。 - **台账导出**:`/api/devices/export?format=csv|json`。 - **桌面启动器**:`desktop/局域网扫描服务.command` 单文件搞定启动 / 停止 / 重启 / 打开页面 / 看日志。 - **容器化部署**:`Dockerfile` + `docker-compose.yml`(**必须 host 网络**,否则 ARP 扫不到局域网)。 - **测试**:`./tests/run.sh` 共 8 套 113 项检查(5 套离线 UI + 3 套联调), 另有 `tests/docker_smoke.sh` 容器冒烟测试 27 项。 - **`AGENTS.md`**:压缩后的项目上下文,新会话自动加载。 ### 修复 - **设备管理改名后首页不同步**:台账别名此前只在 `GET /api/scan/` 贴上, SSE 推的帧没有;编辑保存后也没重取快照。现在三个出口都贴,前端 `applySnapshot` 记录 `scanId`。 - **macOS `arp -an` 省略前导零**(`8:9b:4b:...`)导致 OUI 查不到厂商, 27/57 台设备的厂商丢失。现在按 octet 补零。 - **NetBIOS 应答不回显 question 段**导致读出 `ROUP` 之类垃圾主机名,改为按 header `qdcount` 跳过。 - **mDNS 的 SRV 记录解析**:读 `rec["host"]` 而不是 `target`,此前 browse 恒返回 0 条。 - **设备类型误判**:`infer_kind` 调整判定顺序(强特征端口 → 主机名关键词 → 端口组合 → mDNS 服务 → 厂商 → 弱端口兜底),NAS 不再被 554 端口认成摄像头,MacBook 不再被认成 Apple TV。 - **busybox 版 ping 不认 `-n`**:此前会导致每台设备都判"离线"却不报错(静默全失败)。 现在遇到用法错误自动去掉 `-n` 重试一次,并在日志里提示。 - **演示设备污染台账**:演示模式的假设备不进真实台账。 - **跨网段误判掉线**:只有落在本次扫描网段内的设备才会被判离线,扫别的网段不误伤。 ### 已知限制 macOS 上(Docker Desktop 与 colima 都一样)host 网络仍只是虚拟机内网, 实测容器看到 `192.168.5.0/24` 而非宿主机的 `10.0.0.0/24`, 因此**真实 ARP 扫描请用宿主机直跑**,或把容器部署到 Linux 主机 / 软路由 / NAS / K8s(`hostNetwork: true`)上。 详见 README 的「Docker 部署」。 ## [1.0.0] — 2026-09-25 首个版本:零依赖局域网扫描服务。 - Python 标准库 HTTP 服务 + 原生 JS 前端,不需要 root、不装任何第三方包。 - 发现手段:ARP 缓存 → UDP 触发 ARP 解析(非 root 拿 MAC、能发现禁 ping 设备)→ 并发 ICMP → ping 后补读 ARP → mDNS/Bonjour 浏览 → 反向 DNS / NetBIOS → TCP 端口指纹。 - 自实现 mDNS(含压缩指针解析)与 NetBIOS NBSTAT 报文。 - OUI 厂商库:内置常用表 + IEEE 全量 `oui.csv`(4 万条,`python3 update_oui.py` 获取)。 - 实时 Web 界面:SSE 推送扫描进度,设备逐台出现,含网段分布热力图、CSV/JSON 导出。 [1.4.0]: https://github.com/hzjackboy/lan-device-scanner/releases/tag/v1.4.0 [1.3.0]: https://github.com/hzjackboy/lan-device-scanner/releases/tag/v1.3.0 [1.2.0]: https://github.com/hzjackboy/lan-device-scanner/releases/tag/v1.2.0 [1.1.0]: https://github.com/hzjackboy/lan-device-scanner/releases/tag/v1.1.0 [1.0.0]: https://github.com/hzjackboy/lan-device-scanner/releases/tag/v1.0.0