Commit Graph
179 Commits
Author SHA1 Message Date
mole404 2b57bcbbbb fix(images): 按老师要求回滚「点图片不勾选」,并去掉标签行内的状态字样
1. 回滚第 3 条:删掉整套「单击图片不改变勾选」逻辑(捕获阶段记按下前选中 + 松手还原),
   小圆勾的 click 恢复全平台生效 —— 点图片本身照旧参与勾选(老师决定)。
   保留本轮的另一半修复:Zoom 坐标修正(html zoom:1.1 与 DragSelect 的冲突)与
   图片/卡片原生拖拽禁用(这两条是「框选起点偏右、松手不释放」的真因)。
2. 标签管理每一行不再显示任何状态字样(已有的「已有 / 移除 / 部分有」全删,DOM 里也没了),
   状态只看左边那个勾:实心勾=有、方框=无、减号=部分有、红色方框=待移除。

验收:jsdom 全仓 396(标签那份 109/109);真机 51/51 全绿(含手机 390 一段)。
2026-10-02 02:14:21 +08:00
mole404 8108cae88b fix(images): 按老师第二轮反馈修 7 条(框选坐标/点选语义/标签窗口版式/入口)
1. 框选「起点偏右、最左侧拖不动、松手不释放」的真因:common.less 给 <html> 加了 zoom:1.1,
   而 DragSelect 把 .ds-selector-area(position:fixed)与 .ds-selector 的坐标按视觉像素写进
   内联样式,浏览器再乘一次 zoom(实测区域真实 (282,120) 被当 (310,132),正好 ×1.1)。
   修法:盯这两个元素的内联样式,按 zoom 折算回布局单位再写回(数值容差比较,避免反复折算)。
   另:图片原生拖拽会吃掉 mousemove(框不跟随、松手不解锁)——卡片 <a href> 本身默认可拖拽,
   所以 -webkit-user-drag 要同时禁 .images-item 与里面的 img,并用 dragstart 兜底。
2. + 3. 单击图片不再顺手勾选(老师要求只有右上角小圆勾才勾选):捕获阶段记录按下前的选中,
   松手时若位移 < 3px 就还原成按下前那份(连盖两拍,压住库自己的「点哪个选哪个」);
   小圆勾改成幂等的集合运算;这条逻辑桌面走这里、手机仍旧走 .image-selector 的 click。
4. 标签管理窗口:条目行 overflow-hidden(行内方角背景会盖住圆角 → 蓝框左上/左下看着缺角);
   去掉勾选后的「添加」字样;副标题只留「已选择 N 张图片」;新建标签那行提到首行。
5. 「新建标签」不整页刷新 = 正常(不改图片与角标,只更新标签下拉);改名/删除会影响角标与筛选所以刷新。
6. 右上角标签下拉里的「标签管理」入口已删(老师要求)。
7. 标签按钮图标按截图实测下移 0.5px(文字墨迹中心 90.0 / 图标 89.5)。

验收:jsdom 全仓 396 → 标签那份 109/109;真机(一次性容器 + 生产库只读副本)51 条全绿,
含「从图片上起手拖动框选 8 张、框精确跟手 600×300、松手解锁、单击不改勾选、小圆勾正好 1 张」。
2026-10-02 01:40:40 +08:00
mole404 13d834506b fix(images): 框选失效真因修复 + 标签按反馈重构
按老师 5 条反馈改(全部在「我的图片」页):
1. 合并标签窗口:删掉多余的「管理标签」,新建/改名/删除并进原「修改标签」窗口,
   标题统一为「标签管理」;工具栏 / 手机菜单 / 右键菜单 / 标签下拉都只留这一个入口。
2. 两态勾选:勾上 = 给选中图片加上该标签,取消 = 移除;部分选中的图片上有该标签时
   显示半勾(「部分有」)表示现状,点一下变全勾。废弃原「不变/添加/移除」三态。
3. 右上角标签下拉新增「显示图片标签」开关(状态存 localStorage),默认开。
4. 标签按钮改 inline-flex items-center gap-1.5(py 2 收 1.5 补偿高度),
   实测图标与文字中心差值 < 0.5px、按钮高度不变。
5. 框选失效真因修复:DragSelect 给图片墙套的 .ds-selector-area 包装盒只在初始化时
   量过一次(实测缓存 59px vs 实际 1697px),图片墙长高后鼠标按下被判成「在区域之外」,
   拖动根本不启动。修法:挂 jg.complete / jg.resize,每次排完版调 ds.Interaction.init()
   让它重新测量(注意 justifiedGallery('norewind') 是空操作,不能写在它后面)。
   同时放宽 predragstart 判据:卡片带 10px 内边距、缝隙落在卡片盒子里,退回上游判据会
   连从缝隙起手也框不出来(实测 0 张);控件(小圆勾等)仍旧 break()。

真机验收(一次性容器 + 生产库只读副本,未碰生产):缝隙起手能框选、小圆勾点选正好 1 张、
单击仍开看图器、两态/半勾真写库通过、无 JS 报错。jsdom 全仓 396(标签那份 105)。
已知遗留:按在图片正中间起手时浏览器会起原生图片拖拽,选择框不跟随(只选中 1 张),
按老师要求本轮不动这条路。
2026-10-02 00:21:45 +08:00
mole404 67b0e6e9d5 ci: 测试分支触发模式加 test(分支由 test/memory-tuning 改名) 2026-10-01 22:53:42 +08:00
mole404 539abe13a3 ci: 迁移数守卫基线 10 → 12
上游 pin 快照 10 条未动,新增的 2 条是本 fork 标签功能的表(create_tags_table /
create_image_tag_table,均为新增表、不改原表)。守卫继续钉住 12:之后再变数就报红。
注释里写明这 2 条同样要人工 migrate(线上升级:起新镜像后立刻 php artisan migrate --force)。
2026-10-01 22:04:37 +08:00
mole404 51b6658007 feat(tags): 图库标签适配(筛选/详情/角标/批量修改/标签管理)+ 下线「图片权限」界面口径
界面侧
- 图库页:原「设置权限」按钮、权限筛选下拉、详情卡「权限」行全部下线,换成标签:
  顶部标签多选筛选(后端 AND,带「清除筛选」)、详情卡标签行(可增/可删/现场新建)、
  卡片左下角标签角标(pointer-events-none,不抢选中与预览)、批量「修改标签」
  (桌面工具栏 + 手机 ⋯ 菜单两套入口,行内 44px,添加/移除三态循环)
- 新增「标签管理」弹窗:新建 / 重命名 / 删除(删除二次确认,文案说明会从所有图片上移除);
  入口两处:标签筛选下拉底部「管理标签」、图片右键菜单「管理标签」
- 用户设置页删掉「图片默认权限」整块;后台图片页删详情「权限」行与搜索语法
  is:public/is:private 帮助行;接口文档删 permission 参数行
- 连带:UserSettingRequest 的 configs.default_permission 由 required 放宽为 nullable
  (表单不再提交该项,不放宽则保存设置直接 422;上传默认权限仍取库里已有值)
- 后台搜索后端也不再见识别 is:public / is:private(删除该 match 分支与 ImagePermission 引用)

后端
- permission 列与既有接口形状保持不动;仅 ImageController 的列表/详情序列化补 tags 字段
  (卡片角标与详情卡需要),标签相关接口沿用上一步已就绪的 GET/POST/PUT/DELETE user/tags
  与 PUT user/images/tags

测试与产物
- 新增 test/tag-feature.test.mjs(88 条:静态契约 + jsdom 行为,含筛选 AND/清除、详情增删新建、
  批量三态与 payload、标签管理增删改、右键菜单注入、文案标准化、无「公开/私有」残留)
- images-modal / swal-theme 里钉住旧「权限」口径的断言随之更新
- 重建前端产物(public/css/app.css),重钉 Dockerfile 自证 md5(routes/web.php、
  images.blade.php 与 .code-revision),重生成 patches/ios-longpress.patch
- tools/diff-vs-upstream.sh 的已知偏离清单补登记本批改动文件
2026-10-01 22:02:03 +08:00
mole404 2da63c7e9a chore(mem): MaxConnectionsPerChild 默认值改回 5(低流量下取最严内存纪律)
老师决定:他这台图床流量约 3 请求/分钟,MCP=5 意味着每 ~3 分钟才退休一个 worker,
换班开销可忽略;换班越勤,单个 worker 允许积累的内存天花板越低。故取回他原来的 5。
(中途试过 30,现按实测与他的偏好改回;env 覆盖机制 APACHE_MAX_CONNECTIONS_PER_CHILD
与 0=永不回收 的语义完全保留。)

同步改回:entrypoint.sh 默认值与旁注、build-image.yaml 6 处期望值(断言 + 自证清单 + 覆盖用例)、
Dockerfile 概览注释、docker-compose.yaml 注释。README 参数表本来就是 5,无需改。

验证:一次性容器集成跑 5 用例(默认→5、env=10→10、env=0→0、脏值 abc→回退 5 且不启动失败、
PHP_MEMORY_LIMIT 仍生效)+ apache2ctl -t = Syntax OK + memory_limit 仍 64M;
全仓 grep 无遗留的 30 期望值;Dockerfile 9 条 md5 自证 9/9。
2026-10-01 18:06:03 +08:00
mole404 0ad094fac9 merge master:去掉图片右键菜单气泡尖角(测试版同步) 2026-10-01 17:28:44 +08:00
mole404 7c3632a079 style(ui): 去掉图片右键菜单的气泡尖角(电脑端与手机端统一)
来源:src/resources/css/context-js.less 里 .dropdown-context:before/:after 的两条 border 三角
(编译产物 public/css/context-js/context-js.css)。桌面与手机共用同一份规则,无媒体查询。

做法:把这两条伪元素的 content 由 '' 改为 none(装饰性箭头,删掉不影响任何几何与行为);
另在 common.less 加一条 .dropdown-context:before/:after{content:none!important} 兜底 ——
context-js.css 的 URL 上没有版本串(iOS 会强缓存),common.css 走 assetVersion(),加兜底可避免
老缓存里还留着箭头。

覆盖面(已核实不共用、可安全全局去掉):.dropdown-context 只由 resources/js/context-js.js 生成,
blade 模板里 0 处引用;顶栏下拉是 Alpine 的 .ls-menu(样式在 app.css),与本节无交集。
A/B 实测顶栏菜单 420×200 区域像素 diff = 0px,computed style 完全相等。

验证(真 Chromium 153 + 真产物 CSS/JS + 同构 DOM 夹具):
- 桌面 1440×900 右键 / 手机 390×844 长按:::before/::after content "" → none,
  菜单 rect 改前后完全相等(桌面 687.5,412.48,176×390.47;手机 182,432,160×355),
  整套菜单区域像素 diff 56px 且 100% 落在尖角矩形内、外侧 0px;翻转态(-up)同理。
- 回归:1440/1280/1920 三种宽度 × 5 个点位共 15 次(含 zoom 1.2 / 1.0 抽查共 25 次)
  全部 inVP=True、对齐 ≤2px、子菜单同样在视口内;滚动关闭菜单三种视口均仍生效。
- jsdom 290/290;Dockerfile 9 条 md5 自证 9/9(本次不动被自证的文件,无需改期望值)。
产物 md5:context-js.css 90eaca7c…→49eca653…;common.css 9a7ddc36…→4b477e2a…;
app.css / app.js / context-js.js(两份) / images.blade.php 均未变。
2026-10-01 17:28:42 +08:00
mole404 d202908ecc merge master:带上桌面端右键菜单错位修复(测试版同步修复) 2026-10-01 15:40:13 +08:00
mole404 da378e56cf fix(ui): 修好桌面端右键菜单错位且超出视口(桌面 110% 缩放导致的二次缩放)
根因:6d17ea9 给桌面端加的 html{zoom:1.1} 会把菜单写入的 left/top 长度再放大一次;
而 MouseEvent.pageX/pageY、getBoundingClientRect、documentElement.clientWidth 都是
视口物理像素 → 渲染位置 = (点击坐标 - 13) × zoom,偏差 ≈ 0.1×点击坐标 - 14.3,
越靠右下偏得越多,靠近右/下边缘时被拉出视口而无法完整显示。

实测(真 Chromium 153,1440×900,菜单左上角相对点击点):
  居中 (720,450) 改前 +57.7/+58.2 → 改后 -13/+12
  右上/右下/左下/底部中央 改前均越界(menu right=1575 / bottom=983>900 等)→ 改后全部在视口内
  无缩放对照组偏差恒为 -13/+12,证明偏差就是这 0.1 倍缩放;1280×800 / 1920×1080 同规律。

修法(src/resources/js/context-js.js,同步 public/js/context-js/context-js.js,两份逐字节一致):
- zoomFactor():运行时读 getComputedStyle(documentElement).zoom,缺省 1(不写死 1.1)
- measureBox():用 getBoundingClientRect 取真实尺寸(display:none 时沿用临时显示测量)
- clampMenu()/fitSubmenu()/openMenu():所有位置与尺寸先在「元素本地单位」里换算
  (物理量一律先除以 z),保证四边都落在视口内,靠边缘时翻转到另一侧而非硬夹
- z === 1 时表达式逐字退化为原算术 → <768px 行为逐字节不变(手机端 Δ=0)

验证:3 种桌面宽度 × 5 个点击位置 = 25 次,改前 23 处失败 → 改后 0 处;
zoom 改 1.2 抽查 0 处失败;zoom 1.0 与改前逐字节相同;
滚动关闭菜单行为保留(3 种视口均验 open→scroll→closed);jsdom 290/290。
未改动 images.blade.php 与 README;patches/ios-longpress.patch 已同步再生成
(git apply 到上游快照可还原 src/ 三文件,逐字节一致)。
2026-10-01 15:40:11 +08:00
mole404 5554c28c02 feat(mem): 测试版 —— PHP/opcache 收紧 + MaxConnectionsPerChild 放宽 + CI 断言
本提交只在 test/memory-tuning 分支上,master 保持 = 线上稳定版不动。

- 先量后改:镜像里实际生效的 memory_limit 是 512M(上游 memory-limit.ini),
  不是 PHP 编译期默认的 128M。本轮改为默认 64M,并做成运行期可覆盖:
  entrypoint 新增 PHP_MEMORY_LIMIT(默认 64M),只接受 <数字>[KMG],脏值只忽略该项
  并警告、绝不启动失败,幂等;改 env 即可回 128M/256M,不用重建镜像。
- opcache 实测:配置 128M/10000;真实预热集合只用了 827 个脚本 / 25.0MB;
  全树编译上限 9949 个脚本 / 255.9MB(走不到)→ memory_consumption 收成 64(约实测 2.6 倍,
  省下 64MB 共享内存),max_accelerated_files 保持 10000,validate_timestamps=1 显式写死。
- MaxConnectionsPerChild 默认 5 → 30(减少无谓的 worker 换班),
  APACHE_MAX_CONNECTIONS_PER_CHILD 覆盖机制与 0=永不回收 的语义原样保留。
- CI:Verify published image 新增两条断言(memory_limit == 64M、mpm.conf 有
  MaxConnectionsPerChild 30),沿用先落文件再比较(不碰 pipefail + grep -q 那个坑);
  MPM 自证步骤里的期望值 5 → 30。
- 分支安全:push 触发增加 test/*(保留 master);Promote to latest 本就有
  github.ref == refs/heads/master 的条件(本轮补强注释),分支构建只产不可变 sha- 镜像;
  retag.yaml 只由 workflow_dispatch 触发且硬拒 latest 目标,无需额外守卫。
2026-10-01 12:57:09 +08:00
mole404 cac154aa07 feat(ui): 桌面端默认按 110% 呈现(等价浏览器整页缩放),并修好断点口径
老师习惯把浏览器整页缩放到 110% 用,希望默认就是这个状态;手机端必须不变。

一、为什么不能只写 zoom
- 浏览器整页缩放会让布局视口一起变小(1440 窗口在 110% 下页面眼里只有约 1309px),
  因此大断点不再命中、内边距变小、内容自然铺满两侧;
- 而 CSS zoom 只改布局尺寸、不改 media query 的判定宽度 → 断点照旧命中 →
  在 1536~1689.6 这一带会误命中 2xl:px-60(每侧多 220px 留白),
  在 768~844.8 这一带会误命中 md:(每侧多 17.6px)。这正是"缩放比例对了、排版没对"的根因。

二、做法
- tailwind.config.js:screens 的 md/lg/xl/2xl 全部 ×1.1(844.8 / 1126.4 / 1408 / 1689.6),
  sm 保持 640 不动(低于 768,改它会动手机)。
- resources/css/common.less:新增 @media(min-width:768px){html{zoom:1.1}},以及三个
  补偿类(.ls-zoom-minh-screen / .ls-zoom-h-screen / .ls-zoom-mt-0,带 html 前缀以压过
  app.css,因为 common.css 在它之前加载);同时覆盖 .container 的 max-width ——
  阈值 ×1.1 但值保持原值(Tailwind 把 max-width 写成断点值,跟着 ×1.1 会宽 1.1 倍),
  swal 动画档阈值同步 767→844.79。
- gallery.less:768/1024/1280/1536 四条阈值同步 ×1.1(640 不动,规则值是百分比)。
- blade:原先用 md:min-h-[calc(100vh/1.1)] 的 6 处改成 ls-zoom-minh-screen,弹窗的
  md:h-[…] 改成 ls-zoom-h-screen,容量块 md:mt-2 改成 ls-zoom-mt-0 ——
  必须换掉 md:,因为 md 现在是 844.8,而缩放从 768 就开,否则 768~844.8 会"开了缩放没补偿"。
- 侧栏左下角容量块:mt-10 → ls-zoom-mt-0(桌面多留 20.6px 底部余量,手机间距不动)。

三、验证(真 Chromium 153 + 真产物 CSS + 从真 blade 程序化抽取的同构 DOM)
- 18 档宽度(800~1920,含所有断点边界)A/B 对比:容器几何全部 ≤0.50px。
- 穷举指纹:7 类页面 × 4 档宽度、3468 个盒子,最大偏差 0.76px 横向 / 1.02px 纵向;
  横向残差仅来自 1.1 除不尽时的取整(W/1.1 为整数时塌到 0.25px),纵向残差是 CSS zoom
  自身的次像素累积(用无任何 media query 的合成页隔离验证,与断点无关)。
- 真实视口 390 及 640/700/767:与改动前逐项对比 1854 个盒子 Δ=0.0000(含 13~84 个具名指标/页)。
- 横向溢出:6 类页面 × 8 档宽度(390~2560)全部 0px。
- 容量块:1440×800 下底部余量 +20.61px(此前 +11.81px)。
- jsdom 290/290 全绿;三个钉住产物 md5 未变;Dockerfile 9 条 md5 自证逐条实算 9/9。
2026-10-01 11:36:15 +08:00
mole404 7d25a587e4 Update README.md 2026-09-30 21:23:12 +08:00
mole404 0284d6ca02 Update README.md 2026-09-30 21:17:32 +08:00
mole404 d47cfade1d Update README.md 2026-09-30 21:16:02 +08:00
mole404 f5893d277e Update README.md 2026-09-30 21:15:05 +08:00
mole404 fab050964e fix(ui): 顶栏两端内缩各加到 8px(左右相等)
老师看过上一版后要求左右两端都再往里收一点点:
- 折叠按钮 left-[3px] → left-2(3px → 8px)
- 右上那组 right-1 → right-2(4px → 8px)
两边相等,视觉上完全对称。用的都是已在产物里的标准类,未重建前端。

标题位置不受影响:按钮是绝对定位,内缩量不参与标题排布,标题左缘仍是容器左缘 + 48
(1440 展开 304)。连带效果是标题与按钮右缘的间距从 13px 变成 8px(仍满足 ≥8 的硬指标)。
注释里两处 3px/4px 同步改正。
2026-09-30 09:36:13 +08:00
mole404 5443073f06 fix(ui): 右上角那组元素与左侧按钮对称贴边(内缩 4px)
老师最终确认的其实不是位置错了,而是视觉不对称:折叠按钮贴到容器左缘内缩 3px 之后,
右侧那组(主题切换/存储策略/用户菜单)仍受容器内边距影响 —— 图片页距右缘 24px、
普通页 40px、2xl 下甚至 240px,显得左边紧右边松。

改成与左侧按钮同源的绝对定位:absolute right-1 inset-y-0。容器是 relative,
absolute 相对它的 padding box,所以容器内边距推不动它(左侧按钮同理是 absolute
left-[3px])—— 左右因此落在同一套规则下:左 3px、右 4px,且与路由、断点无关。

左侧按钮与标题的类逐字未改:右侧那组脱流后容器里只剩标题一个在流子元素,
justify-between 会把它放在行首,位置与之前一致。

已重建前端产物:public/css/app.css 1a6270e1 → d2c69ffa(新增 .right-1 / .inset-y-0);
三个钉住产物未变。
2026-09-30 09:28:56 +08:00
mole404 c9a4ea1471 docs: README 整篇重写为介绍 + 部署;补 CI/构建卫生
README(659 行 → 176 行):只保留「这是什么」与「怎么部署/维护」——快速开始、首次安装、
升级(注明不需要 --force-recreate)、回滚(给真实存在的 sha-<commit> 标签)、备份与恢复
(整目录 tar,并说明 SQLite 开了 WAL 会短暂出现 -wal/-shm)、常见配置(端口/证书路径/
APACHE_* 六个变量与实际默认值)、许可与致谢。不再记录任何修复史或差异逐条。
所有具体值(镜像名、容器名、端口、卷路径、WEB_PORT/HTTPS_PORT、MPM 默认值、PHP/Laravel
版本、回滚标签)都对着源码与实机逐条核过。

构建与 CI:
- .dockerignore 补 src/storage/logs/*(+ 保留 .gitignore 占位):本机开发日志会被
  COPY src/ 带进镜像(含本机绝对路径与错误栈)。用一份 moby 语义 matcher 验过排除生效、
  且不影响 fontawesome 字体等构建必需品。
- update-composer-lock.yaml:两处 curl … | php 改成与 Dockerfile 同款「先落盘 → 官方
  installer.sig → 校验 96 位 hex → sha384sum -c - → 再执行 → 删临时文件」(哈希不写死);
  注释里的 9.52.17 与「5 条公告」校正为 9.52.22 / 4 条(OSV 实查)。
- build-image.yaml:扩展自证那段的结论文案改成与实际一致(硬断言只有 pdo_sqlite,
  其余仅打印供人工比对)——sqlite3 对本站不是必需,钉死它会在无害变动时误红。
- tools/vendor-upstream.sh:去掉不存在的 test/run.sh,改为 cd test && npm test。
2026-09-30 09:03:04 +08:00
mole404 edad6a4247 fix(ui): 图片页右上角那组图标归位(撤掉顶栏容器后加的 md:px-10)
上一版为了让图片页标题不压按钮,给顶栏容器补了 md:px-10 —— 但内边距是左右两边,
图片页原本只有 24px 右边距被顶成 40,右上角那组图标整体往里推了,老师截图反馈。

- 顶栏容器回到只传 px-6:图片页右边距回到 24(= 改动前的原值),普通页不变(仍取容器默认的 40)。
- 标题左边距改成按路由区分(与 components/container.blade.php 的 routeIs 同源,一行三元):
  图片页 pl-6、普通页 pl-6 md:pl-2 —— 两边标题绝对 x 都是容器左缘 + 48,跨断点不跳。
- 左侧按钮(left-[3px] / w-8 h-8 / mt-3)与标题像素一个都没动:真 Chromium 实测
  按钮 259–291、标题 304、与按钮右缘间距恒 13px;48 种情形横向溢出 0、间距违规 0。
- 只用已存在的 Tailwind 类,未重建前端产物(public/css/app.css 保持 1a6270e1…)。
2026-09-30 09:03:04 +08:00
mole404 92bb0f5434 fix(ui): 修好“我的图片”页标题压折叠按钮的回归 + 按钮内缩 3px、标题微右移
根因(源码 + 真浏览器量出来的):components/container.blade.php 的 full 属性是
request()->routeIs('images','admin.images'),命中时容器默认类里的水平内边距整段消失
(只剩 h-full mx-auto sm:ml-64)。顶栏自己只传了 px-6,于是图片页顶栏内边距 24px、
内容 0px,而普通页两边都是 40px。按钮是 absolute left-*,相对容器 padding box,
内边距推不开它、占位恒为 [容器左缘, +32];标题却在内边距之内 ——
图片页标题 = 左缘+24 = 280 < 按钮右缘 288,正好 -8px 重叠(就是截图里 ‹ 压“我”)。

- 顶栏容器补 md:px-10,把顶栏内边距变成与路由无关的 40(普通路由本来就有,空操作)。
- 按钮 left-0 → left-[3px]:整体内缩 3px,不再顶到边上(老师要求)。
- 标题 pl-4 md:pl-0 → pl-6 md:pl-2:24/8 与内边距 24/40 的差正好抵消,
  标题 x 恒为左缘+48,与按钮右缘的间距恒为 13px。
- 真 Chromium 量 16 种情形(普通页/图片页 × 4 断点 × 侧栏展开/折叠):
  改前 6 种违反硬约束(图片页 ≥768 与 1440 折叠时重叠),改后 16/16 通过;
  普通页标题 296 → 304,只右移 8px,没有回到上一轮 +40 那个坑。
- 已重建产物:public/css/app.css 233facb6 → 1a6270e1(新增 .left-[3px] / .md:pl-2 /
  .pl-6,消失的 w-9 / mr-2 / pl-0 全仓零引用);钉住产物零变化;jsdom 290/290 全绿。
2026-09-30 08:34:42 +08:00
mole404 9ca9d9aa57 fix(security): 收窄信任代理,堵住伪造 X-Forwarded-For 绕过按 IP 限流
上游默认 $proxies = '*'(信任所有转发头)。而外层 nginx 用的是
$proxy_add_x_forwarded_for —— 追加而不是覆盖 —— 客户端自己带的
X-Forwarded-For 会排在列表最前面,Laravel 取客户端 IP 时又会采信它。
于是所有按 IP 限流的地方(Breeze 登录失败计数、throttle 中间件)都能靠
伪造一个头无限重置:实测写 '*' 时应用解析出的客户端 IP 就是伪造进去的值
(1.2.3.4),改成下面这组之后解析出真实客户端(203.0.113.9)。

- $proxies 改为 ['127.0.0.1', '::1', '172.16.0.0/12', '10.0.0.0/8', '192.168.0.0/16']:
  只列私有网段而不是写死某个网关 IP —— 容器网络重建后网关会变,写死会静默失效
  (届时应用不再认转发头,所有人被当成同一个 IP)。公网攻击者不可能以私有地址
  连进来,所以这样信任是安全的:从右往左找第一个不受信地址,得到的仍是真实客户端。
- 实测副作用检查:HTTPS 判定不受影响(X-Forwarded-Proto 仍被采信),
  正常用户解析出的客户端 IP 与改动前完全一致(只是伪造的头不再生效)。
- 已登记进 tools/diff-vs-upstream.sh 的已知偏离清单(第 9 组),README 同步。
2026-09-30 08:34:42 +08:00
mole404 5020c4f01a ci,ui,docs: 响应头断言 + 真升级用例基线动态化 + 钉 SHA 升级 + 顶栏对齐微调
CI:
- Verify published image 步骤新增两条断言:响应头不得出现 X-Powered-By、Server 不得带
  具体版本号(沿用『先落文件再 grep』,不碰 SIGPIPE 那个坑);两条都双向验证过
  (只注 PHP ini 或只注 Apache conf 都会红)。
- setup-node 换到 v7.0.0(node24)、upload-artifact 换到 v7.0.1(node24),
  同时清掉第三个 workflow update-composer-lock.yaml 里那个 node20 的 upload-artifact;
  其余 docker/* 与 checkout 本来就是 node24,不动。全部钉不可移动的 commit SHA。
- tools/upgrade-test.sh:真升级用例的基线从写死的 tag 改成动态解析(匿名读 GHCR
  tags/list,逐个查镜像 config 的 created 时间取最新,并排除本次 commit 与自身),
  不依赖 tags/list 的返回顺序;解析失败明确失败(不再静默变绿),仍可用仓库变量覆盖。
  防漂移校验(tag 名里的 sha vs 镜像自带 .code-revision 的 fork_sha)原样保留。

顶栏(src/resources/views/layouts/header.blade.php):
- 折叠按钮 44×44 → 32×32(mt-3 保持 56px 顶栏内垂直居中)。
- 标题组的 pl-10 位置补偿改为 pl-4 md:pl-0:顶栏与页面内容共用同一个 x-container、
  同断点内边距相同,所以 md 以上去掉补偿即等于内容左缘(实测 Δ=0.0);
  窄屏内容左缘落在按钮底下,物理无法对齐,退回 pl-4(按钮右缘 +8px)。
- 已重建前端产物(public/css/app.css,新增 .pl-4/.pl-0/md:pl-0),钉住产物未变。

工具/文档:
- KNOWN_DEVIATIONS 同步到本轮全部有意偏离;README 补差异清单、CI 说明与验收清单。
2026-09-30 08:00:46 +08:00
mole404 d77ba67d5c fix(security,deps): 认证端点节流 + 镜像信息收敛 + 两条依赖公告
认证(src/routes/auth.php):
- login / register / confirm-password 加 throttle:5,1,forgot-password 加 throttle:6,1。
  实测发现登录原本并非零防护 —— Breeze 的 LoginRequest 有按(邮箱|IP)的 5 次/60 秒
  RateLimiter(CACHE_DRIVER=file,真生效),它挡单账号撞库;路由级节流补的是
  『换邮箱撒网 / 换 IP 轮换』这个口,两者互补。
- verification.verify(6,1) 与 verification.send(3,1) 原样未动。
- 已知取舍(未改,已登记):TrustProxies 配的是 $proxies='*',节流键取自 X-Forwarded-For
  且外层 nginx 用 $proxy_add_x_forwarded_for 追加 → 客户端自带的假 XFF 会排在前面并被采信,
  即限流键可伪造。修法是把信任代理收窄到 nginx 的地址/网段。

镜像信息收敛(Dockerfile):
- 新增 /usr/local/etc/php/conf.d/zz-lsky-hardening.ini(expose_php=0 / display_errors=0),
  zz- 前缀保证不被别的 ini 覆盖;构建期用 php -r 真读回 ini_get 断言。
- 新增 /etc/apache2/conf-enabled/zz-lsky-hardening.conf(ServerTokens Prod /
  ServerSignature Off)。放 conf-enabled 而不是 vhost 模板:ServerTokens 是全局指令,
  写进 VirtualHost 会被 Apache 拒绝加载。
- 自证从 8 条加到 9 条(新增 routes/auth.php 钉住),本地实算 9/9 通过。

依赖公告(OSV 扫描到 11 条,本次清掉两条有修复版本的):
- psy/psysh v0.11.12 → v0.11.23(它在生产依赖树里、随镜像发布;Dockerfile 的
  composer.lock md5 自证同步为新值),并从 osv-scanner.toml 删掉那条白名单。
- sweetalert2 11.4.6 → 11.22.4(前端依赖,会打进浏览器包)。升级后新版默认样式
  div:where(.swal2-container) .swal2-input 与我们的皮肤权重打平并靠注入顺序取胜,
  实测把输入框高度从 38px 压成 36.75px → 把皮肤前缀从 html 提到 html:root 修掉,
  并同步 3 条断言;jsdom 290/290 全绿。
- OSV 门禁复跑:exit 0,已过滤清单里只剩 laravel 9.x 那 4 条(上游无修复版本)
  与 debugbar / phpunit 等 dev-only 项。
2026-09-30 08:00:46 +08:00
mole404 235f72e68c ci: 真升级用例 + retag digest 校验 + OSV 依赖门禁 + 修试构建 digest 冲突
- build-image.yaml:试构建时按请求版本自动从 Registry API 解析 php-cli / php-apache 的
  index digest 一并传参。实测 tag@digest 时 digest 优先且 tag 被静默忽略 → 旧写法会构建出
  默认版本、再被『PHP 版本校验』判红,白烧一次全量构建。另加 deps-scan job 作为 build 前置。
- 新增 tools/upgrade-test.sh:旧镜像 → 新镜像真升级用例(独立容器 / 卷 / 端口 18090),
  断言数据面逐字节不变(.env / installed.lock / SQLite 文件与指纹 / 缩略图 / 编译缓存)、
  代码确实换成新镜像那一版、终局日志必须是『已同步进卷』而非『跳过同步』。
  本机真跑通过,并已用变异副本证明用例真能红(改数据面会红、tag 被挪会红)。
- retag.yaml:retag 后断言目标 digest 与源逐字一致,不一致则打印两个 digest 并失败。
- osv-scanner.toml:11 条已知且接受的告警入库(laravel 9.x 那几条上游无修复版本、
  dev-only 与 test-only 各若干),新增漏洞会让 CI 红;抽掉白名单实测确实会红。
2026-09-30 07:20:43 +08:00
mole404 5865d04816 docs(tools): diff-vs-upstream 改为已知偏离清单,README 同步
旧判据是『除三个补丁文件外不许有别处与上游不同』,而上游之后整个前端换过,
实际有意偏离已达 92 个文件 → 旧脚本会把它们全报成手滑改动。
改成按 7 组写明每个文件为什么偏离的已知偏离清单:内容不同的集合必须恰好落在清单内,
否则失败;并额外报告『登记了但已无差异』的过时条目。
2026-09-30 07:20:43 +08:00
mole404 de891b4a35 chore(image): .code-revision 补 composer_lock_md5 与构建期断言
只升级 vendor 依赖(composer.lock 变了、其他文件没变)的镜像,在已有卷上不会触发代码同步;
同类的『只改前端产物』口子这轮已用 app_js_md5 等补上,这是剩下唯一一条。
标记生成端在 Dockerfile(烘焙进镜像),入口脚本只做镜像标记与卷标记的整份比较,故无需改动。
另加一条构建期 grep 断言,保证该行真的写进 /var/www/lsky/.code-revision。
2026-09-30 07:20:43 +08:00
mole404 aa9edf313e fix(db): SQLite 开 WAL + 收紧 busy_timeout,消掉偶发 database is locked
- config/database.php:sqlite 连接加 options[PDO::ATTR_TIMEOUT]=5。实测 Laravel 9.52 的
  SQLiteConnector / SQLiteConnection / Connector::getOptions() 都不读 journal_mode 与
  busy_timeout 这两个 config 键 → 写了也是死键、被静默忽略(实测过)。
- AppServiceProvider:监听 ConnectionEstablished,懒加载地把 sqlite 连接设 PRAGMA
  journal_mode = WAL(挂事件而不是 boot 里 getPdo(),避免每个请求一引导就强行连库)。
- 实测:busy_timeout=0 时并发写 1ms 就 database is locked;5000ms / WAL 下并发写成功。
- 注:pdo_sqlite 默认 busy_timeout 本就是 60000ms,5 秒是刻意收紧(别让请求挂死 60 秒);
  真正防「读事务升级成写事务立刻 BUSY」的是 WAL,busy_timeout 对它不生效。
2026-09-30 07:20:43 +08:00
mole404 be94ecac1b fix(ci): 代码同步自证别再让 pipefail 把「命中」当失败 + 等待信号改成等分支最终行
根因(CI 实撞):该步骤开了 set -euo pipefail,而它用 docker logs <c> | grep -q '<pat>' 判存在 —— grep -q 一命中就退出,docker logs 随即吃 SIGPIPE(141),pipefail 把整条管道判成失败,于是「日志里明明打印了】也判红(正是本项目技能里第 9 条记过的坑)。修法:统一先落文件再 grep;失败时把完整容器日志打出来。另:case C 的等待信号原来是「出现第一条 [lsky]」,入口脚本新增的自签证书日志行排在最前,把时序带偏(技能第 16 条:等待信号必须是该分支最后打的那行),改成直接等「跳过同步」。本地拿真镜像 sha-ae6ec156 复现过:入口脚本行为本身正确(跳过同步 + 手动修补保留)。
2026-09-30 06:29:31 +08:00
mole404 64c03f5514 fix(image): 修正 F21 —— Debian 自带 default-ssl 仍指向已删的 snakeoil 证书,导致 Apache 起不来(安装页 000)
CI 实撞的栈:AH00526 SSLCertificateFile '/etc/ssl/certs/ssl-cert-snakeoil.pem' does not exist → Apache 整个起不来,容器进程在但安装页 000。原因是 F21 只改了 000-default.conf.template 的 HTTPS vhost,漏了 a2ensite 启用的 Debian 自带 default-ssl.conf。改法:把它也指向入口脚本运行期生成的 /etc/apache2/ssl/lsky-selfsigned.{crt,key},并加构建期护栏(按文件路径判,不按 snakeoil 这个词判 —— 该文件里的注释提到它是正常的)。顺带把 ENV 的 legacy 写法改成 key=value,消掉两条 Dockerfile lint 警告。
2026-09-30 06:24:23 +08:00
mole404 0dc245494d docs(ci): README 同步 + 测试入口跑全量 + CI 钉 Node 版本 2026-09-30 06:18:56 +08:00
mole404 32859cc5cb fix(frontend): 收窄 infiniteScroll 点击判据 + 顶栏折叠按钮贴左(含产物重建)
F14:click 委托判据从「容器内任意 span」收窄到插件自己插入的哨兵专属类(closest 保留子元素与 .disabled 语义),绑定/解绑改用 click.infiniteScroll 命名空间 —— destroy() 不再摘掉同一容器上别人的委托(图片页还有一条 .image-selector)。header:折叠按钮绝对定位贴内容区左缘(桌面 292→256/折叠 100→64/手机 20→0),点击区 36→44,标题视觉 x 全程 Δ0。产物已真重建(npm run prod,Node 18):public/ 下只有 public/js/app.js 变(c4678319→7459c100),钉住的两个补丁产物字节未变。jsdom 288 条全绿。
2026-09-30 06:18:56 +08:00
mole404 f3aa1c191c chore(image): 供应链加固 + Laravel 9.52.22 + 健康检查
F15:两个阶段的基镜像都钉 OCI digest(附取 digest 的方法)。F16:构建期两个下载源改为先落盘再校验(composer installer 用官方 installer.sig 的 sha384;install-php-extensions 钉 2.12.0 + 写死 sha256),验不过即构建失败。F19:laravel/framework 9.52.21→9.52.22(另 7 个 symfony 6.4.x 补丁级,共 8 个包,content-hash 未变)。F10:加 HEALTHCHECK。F12:.dockerignore 排除 node_modules/vendor/开发机 .env/编译产物(vendor 只排 src/vendor —— 用 **/vendor 会误伤 fontawesome 的字体文件,前端图标会全空)。
2026-09-30 06:18:56 +08:00
mole404 7dd45a9339 fix(entrypoint): 版本标记原子化、端口校验兜底、缓存作废对称、运行期自签证书
F2:同步分支排除 .code-revision 并在解包完成后才写标记(原先靠 tar 流带进去,解包中断会留下「新标记+半套代码」永久跳过同步)。F11:WEB_PORT/HTTPS_PORT 校验+兜底,渲染失败保留旧配置而非截成空文件。F13:清编译视图/packages.php/services.php 抽成函数,播种与同步两条分支都调用。F21:不再随镜像发货公开的 snakeoil 私钥,改为入口脚本幂等生成到 /etc/apache2/ssl/(无 openssl 只警告不阻断)。
2026-09-30 06:18:56 +08:00
mole404 95a4c5b0fe feat(security): 彻底移除 SVG 支持
上传白名单去掉 svg(convention.php + 后台 GroupRequest 的 in: 校验),并在 Group::$configs 读取侧硬过滤存量 DB 配置里的 svg/svgz —— 只改默认值挡不住已有实例。顺带清掉因上传关闭而不可达的 svg 死代码分支(ImageService 的跳过处理/跳过扫描/缩略图直拷、Image 的缩略图后缀、Controller 的白名单例外)。非 svg 格式行为逐条实测不变(10 份真图片缩略图 sha256 相同)。
2026-09-30 06:18:56 +08:00
mole404 a2081e859c fix(ci): MPM 自证步骤改为严格串行(每用例独立容器 + 独立卷)
上一版六个用例共用同一个卷、六个容器并发跑 entrypoint:A 的 chown -R 与 B 对
同一批文件的播种写入互踩(chown: ... No such file or directory),entrypoint 是
set -eu,chown 失败容器当场退出,后续 docker exec 报 No such container 而失败。

改法(只动 CI,产品代码零改动 —— 真实用户只跑一个容器,不存在该竞争):
- 每个用例独立容器 + 独立卷,严格串行:run → 等就绪 → 断言 → rm 容器+卷 → 下一个,
  任何时刻最多一个容器在跑 entrypoint。
- 就绪信号:CMD 改为 `sh -c 'echo "[lsky-mpm] READY"; sleep 300'`;entrypoint 末尾是
  exec "$@",故 READY 必然晚于播种/同步 + chown -R/chgrp -R/chmod -R 全部完成,
  即上一个卷的写入者已收工,才进入下一个用例。
- 轮询容器状态,遇到 exited/dead 立即报错并 dump 日志;每个 docker exec 前
  need_running 复核容器仍在运行;断言完即 finish_case 释放。
- 四类断言语义、apache2ctl -t 语法检查与 DUMP_RUN_CFG 信息性输出全部保留。

验证:yaml.safe_load 通过;抽取 run 脚本 bash -n 通过;静态走查并发(全脚本仅 1 处
docker run、无 & 后台化、run 前后各有 rm -f);用假 docker 替身真跑该步骤脚本 ——
新脚本 0 条并发违规、四类断言全绿 EXIT=0;同一替身跑旧脚本复现 5 条并发违规 +
15 条共用卷(与线上失败现象一致);并验证"容器启动即退出"路径能快速失败并清理干净。
2026-09-30 00:33:57 +08:00
mole404 c5b74cca9e feat(apache): MPM 参数内置为镜像默认值(低配单用户),支持 APACHE_* 逐项覆盖
背景:镜像此前完全吃 Debian/php-apache 的出厂 MPM 值(prefork: StartServers 5 /
MinSpareServers 5 / MaxSpareServers 10 / MaxRequestWorkers 150 /
MaxConnectionsPerChild 0),低配单用户机上光是常驻 10 个空闲 PHP 子进程就已过重。

① 新增 mpm.conf.template(照 000-default.conf.template / ports.conf.template 的既有套路),
   Dockerfile COPY 进 /etc/apache2/ 并加构建期护栏(模板缺失 / 占位符被删 → 构建失败)。
② entrypoint.sh 在 exec 之前渲染到 /etc/apache2/conf-enabled/mpm.conf —— Debian 的
   apache2.conf 先 include mods-enabled 再 include conf-enabled,后者覆盖出厂 prefork 值。
   内置默认:StartServers 2 / MinSpareServers 1 / MaxSpareServers 3 /
   MaxRequestWorkers 5 / MaxConnectionsPerChild 5 / KeepAlive Off。
③ 六项各自独立覆盖(写哪个改哪个,没写的保持默认),并带校验与兜底:
   非整数 / 越下限 / 一致性不满足(MaxRequestWorkers ≥ MaxSpareServers ≥ MinSpareServers)
   → 该项退回默认值 + [lsky] 警告;KeepAlive 只认 On/Off(大小写不敏感,统一渲染成 On/Off);
   MaxConnectionsPerChild 允许 0(0 = 永不回收,合法值)。
   渲染或落盘失败只警告,绝不影响容器启动(set -eu 下全部走 if 守卫,另挡掉
   「目标路径被目录占住时 mv 静默假成功」那条实测发现的坑)。
④ APACHE_KEEPALIVE_TIMEOUT / APACHE_MAX_KEEPALIVE_REQUESTS 仅在 KeepAlive=On 且显式
   设置时才写入;未设则不写该行,跟随 Apache 发行版默认(5 秒 / 100 个请求)。
⑤ README 新增「Apache MPM」一节:默认值、变量表、脏值策略、默认态日志摘要与自查命令、
   高配参考值、以及「MaxRequestWorkers × 单进程内存 ≈ 可用内存」的估算提醒;
   docker-compose.yaml 的 environment 加注释示例(不改任何生效值)。
⑥ CI 新增真容器步骤「Verify Apache MPM defaults and overrides」:默认态与期望的 6 行
   逐行 diff;单项覆盖生效且其余仍默认;脏值退回默认且日志有警告;KeepAlive=On 时
   「不设 timeout → 两行都不出现」「设了 → 两行出现且值正确」;大小写归一化;
   并跑 apache2ctl -t 确认渲染结果不会让 Apache 报语法错。

本机验证(无 docker,镜像层由 CI 验):bash -n 通过;用抽取的入口脚本真代码跑 15 个场景
(全默认 / 单项覆盖 / 六项全脏 / KeepAlive 各形态 / 前导零 / 一致性破坏 / 目标被目录占住 /
模板缺失)全部 exit 0 且输出符合预期;Dockerfile 的 8 组 md5 自检与补丁字节校验未受影响
(未碰 patches/ 与 src/)。
2026-09-30 00:26:45 +08:00
mole404 3893e58069 fix(移动端): 图片墙滑到底第一下不自动加载 —— 改用哨兵相对可视视口判断
现象:手机端「我的图片」滑到最底部,第一次不触发自动加载,再往下拖一下才加载(桌面正常)。

根因:window 滚动分支用 `window.innerHeight + scrollTop >= $(document).height() - 30` 判定到底。
移动浏览器(安卓/Edge)的可滚动高度按「地址栏收起后的大视口」算,而 innerHeight 报的是
「地址栏可见时的小视口」,两者差 50~60px > 30px 余量 → 真滑到底那一发条件仍不成立;
再拖一下使地址栏收起、innerHeight 变大,条件才刚好成立。

修法:window 分支改用哨兵(本方法自己插在列表末尾的 .infinite-scroll)相对可视视口的
位置判断 `getBoundingClientRect().top <= window.innerHeight + offset` —— rect 是对当前可视
视口量的,天然免疫大小视口不一致;顺带把触发点提前到"哨兵进入视口"(体验更好)。
找不到哨兵时退回原公式(别把自己搞死),offset 默认值仍为 30 未动。
容器滚动分支(两个弹窗的列表)原封不动 —— 容器内滚动没有地址栏问题,且那两个弹窗里哨兵是
display:none,用 rect 判断会失效。

新增回归测试 test/infinite-scroll-bottom-mobile.test.mjs(13 条),含 RED→GREEN 证据:
打补丁前 10/13,失败项正是「★到底了自动加载」(gets=1,即到底了没触发);补丁后 13/13。
反向用例(哨兵离得远不触发)与容器分支用例两次都 PASS,证明测的是真实差异。

全仓库仅图片墙一处使用 root:'window'(images.blade.php:406),改动只惠及它。
两份 context-js.js 未改动(仍 b13fc1e4…);补丁重生成后与上游字节一致。
2026-09-29 23:42:41 +08:00
mole404 89f9d28fbe fix(回归): infiniteScroll 销毁时误杀 window 滚动监听,图片墙不再自动加载
① app.js 的 infiniteScroll.destroy() 原来无条件执行 $(window).off('scroll.infiniteScroll'),
   而该命名空间在 window 上是全局唯一 —— 相册弹窗(容器版实例)一关闭,就把图片墙(window 版)
   的整页滚动监听一起解绑了:桌面「我的图片」滚到底不再自动加载,只能手动点哨兵文字。
   改为只有自己就是 window 版(useWindowScroll)时才解绑 window;容器版只解绑自己的 selector。
   该 bug 旧抽屉关闭时同样存在(抽屉用得少,一直没暴露)。
② 相册行名称 <span class="name"> → <div class="name">:infiniteScroll 第 133 行的委托会把
   列表容器里任意 span 当作「加载更多」,点相册名会顺带多拉一页相册(改前实测请求 2→3,
   改后 2→2)。只改模板侧,不动那个全站共用的委托选择器(改它需单独做全站回归)。
   「移动到相册」行的名称与两处 ls-badge 徽标改前已是 div,无需改动。
③ 桌面端相册弹窗宽度 420px → 520px(@media min-width:768px;手机底部抽屉不受影响)。
④ 新增回归测试 test/infinite-scroll-destroy.test.mjs(12 条,含 RED→GREEN 证据:补丁前 10/12,
   失败项正是「销毁容器版后 window 上的监听是否仍在」「销毁后整页滚到底是否仍自动加载」)。
   同步:Dockerfile 的 images_blade_md5 = cab1ae9abf815ae3ca30a63608dbf004、
   patches/ios-longpress.patch 重生成(3115 行、打回上游字节一致)、两套测试更新。
   两份 context-js.js 未改动(仍 b13fc1e4…)。
2026-09-29 23:25:07 +08:00
mole404 776e374ee0 fix(UI): 相册弹窗删掉「完成」按钮 + 相册行改为分区结构(44px 常显操作按钮)
① 删除底部「完成」按钮:markup(含整层包裹 div)、事件绑定、相关注释一并清掉;
   列表成为弹窗唯一主体,max-h/滚动不变(顺手多出约 52px 垂直空间)。
② 相册行改「左侧切换区 + 右侧操作区」:
   - 编辑/删除由裸 span(可点区≈13.5px 图标)改为 44×44 真按钮,所有设备常显、带 aria-label
   - 操作按钮移出 <a>(根因:点不中图标就命中行链接 → 跳进相册);data-id/data-json 同时
     挂在外层 .albums-row 与 <a> 上,编辑/删除从按钮往上找 row 取 id
   - 取消「张数 ↔ 按钮」的 hover 互换(行内容不再抽动);张数仍是 <div>(编辑路径依赖)
   - 当前相册高亮改打在外层 row(边框包住两个按钮)
   - 选择器同步:ALBUM_ITEM → ALBUM_ROW、'>a' → '> .albums-row';切换委托改为 .albums-item,
     仍在 #albums-container 上委托,追加页/重拉/清空都不受影响
   - 删掉上轮为「触摸常显」加的 @media (hover:none) 规则(按钮本就常显)
③ 新增 4 个 Tailwind 类(w-11 / items-stretch / border-l / rounded-l-lg)→ 已重跑前端构建,
   并逐个确认已出现在 public/css/app.css(否则按钮会退回图标大小)
④ 同步:Dockerfile 的 images_blade_md5 = c2014bb78cec8e2e9bfe6e3572eda26b、
   patches/ios-longpress.patch 重生成(3108 行、打回上游字节一致)、两套测试同步。
   两份 context-js.js 未改动(仍 b13fc1e4…)。
2026-09-29 23:04:34 +08:00
mole404 69efaca5db feat(UI): 相册切换从右侧抽屉彻底重做为弹窗 + 侧栏高度改用 dvh
① 相册切换(彻底重做,不是搬运):
   - 新增 #album-switch-modal(复用 <x-modal>,手机端随组件变成底部抽屉,
     自带 100dvh 与 overscroll-contain,滚动链问题不会复发)
   - 内容:16px/600 标题「相册」+ 新增本地搜索框(按名称即时过滤,不打接口)
     + 列表(每行 ≥44px 点击区、左名称右图片数、当前相册高亮 + 「当前」徽标、
     点一行立即切换)+ 底部「完成」
   - 补齐原来没有的状态:加载中、空状态(「还没有相册」/「没有匹配的相册」)
   - 原有能力一条不丢:创建相册(同表单同接口)、重命名/编辑、删除、相册图片数、
     无限滚动(每页 40)
   - 旧抽屉连根删除:#drawer / #drawer-mask / #drawer-title / #drawer-content 的
     markup、const drawer、以及三处 drawer.close() 逐处按语义收敛到
     openAlbums() / closeAlbums() / resetAlbums(),不留死代码
② 侧栏高度:#app-sidebar 由 h-screen(100vh) 改为 100vh→100dvh 兜底写法。
   手机地址栏可见时 100vh 高于真实可视区,侧栏底部(容量使用区)被推到可视区外,
   地址栏收起才出现。用 id 选择器写进 common.less,避免与 Tailwind 工具类的
   生成顺序打架。
③ 同步更新:Dockerfile 的 images_blade_md5、patches/ios-longpress.patch(重生成,
   打回上游字节一致)、三套断言旧抽屉 DOM 的测试。
   两份 context-js.js 未改动,md5 仍为 b13fc1e4…。
2026-09-29 22:45:06 +08:00
mole404 213f7736d4 fix(UI): 头像尖角改圆润、左上角改用 Lsky 官方 logo(透明底)
① 默认头像:身体由「半圆 + 平底」路径改为整圆,整体 clip 进外圆 —— 平底两端在 28px
   下呈两个尖角,改成整圆后底部由外圆弧收边。配色/视口不变(#7d8794、96),
   对比度仍为亮 3.52:1 / 暗 4.50:1。
② 侧栏左上角:去掉 FontAwesome 的 fa-image 与 bg-brand 底色方块,改用 Lsky Pro 官方
   logo(从 public/favicon.ico 的 128x128/32bpp 帧导出为带透明通道的
   public/static/lsky-logo.png),透明底裸放、不套背景方块;静态资源走 ?v=assetVersion。
   路径 public/static/ 已核对不在应用顶层路由段内(不会像 public/images/ 那样撞路由)。
2026-09-29 22:16:59 +08:00
mole404 655f080b82 ci: /images 回归断言只钉「首跳不许 301」(未安装状态的下游页 500 不算回归,改为告警 + dump 日志) 2026-09-29 20:45:00 +08:00
mole404 a4efab82e8 fix(严重): 默认头像挪出 public/images —— 它撞上了 /images 路由,导致整站打不开
事故:为放默认头像新建了真实目录 public/images/,而「我的图片」页路由正是 /images。
Apache 的文件系统优先,遇到真实目录就 301 跳到 /images/(那里没有 index → 403),
而首页 / 会 302 到 /images → 用户一打开就中招(日志证据:GET / → 302、GET /images → 301)。

修复:
① 头像移到 public/static/default-avatar.svg(static 不在顶层路由里;上游自己也只用
   css/js/fonts/thumbnails 这类不撞路由的目录名),Utils::getAvatar() 同步改路径。
② entrypoint 无条件清掉卷里残留的 public/images/default-avatar.svg 并删空目录 ——
   卷同步是「只增不删」,不做这步旧卷永远残留这个目录。只删我们自己放过的那一个文件。
③ CI 冒烟增加回归断言:/images 首跳不许是 301、跟随重定向必须 200、默认头像必须 200。
2026-09-29 20:41:38 +08:00
mole404 22b4ac8ce4 feat(细节四): 头像改本地资源、主菜单标题闪烁、上传页链接结果区手机排版
① 头像不再拉远程:Utils::getAvatar() 由 https://cravatar.cn/avatar/<md5(email)>?... 改为同源静态文件
   asset('images/default-avatar.svg').'?v='.assetVersion(...)(与 app.css/app.js 同一套缓存失效写法),
   新增 public/images/default-avatar.svg(96x96 viewBox、透明底、中性灰 #7d8794 人形轮廓、861 字节)。
   grep 确认全仓仅此一处拼外站地址;User::avatar 访问器让所有视图/API/JS 一次性都变本地。
   实测(真浏览器):网络请求主机只有本机、含 cravatar/gravatar 的请求 0 笔;头像 1 笔 200 image/svg+xml;
   人形灰对 bg-surface-2 的对比度 亮 3.52:1 / 暗 4.50:1(均过 WCAG 图形元素 3:1);28x28 盒不抖、
   首字兜底机制原样保留。
② 长按主菜单标题「图片操作」黑一下变灰:真因是上一轮防误触守卫把 background-color/color 的
   !important 覆盖写在了 `.dropdown-context.menu-arming > li` 上,连标题 li 一起被压成继承色(近黑),
   窗口过后才弹回灰色。修法:pointer-events:none 仍留在 li,把 background-color/color 收窄到
   `> li > a`(真正的菜单项)。实测标题计算颜色:修前窗口内 rgb(22,25,29) → 窗口后 rgb(139,149,163)(会闪);
   修后窗口内/外恒为 rgb(139,149,163)(逐帧 trace 1283ms 无第二种颜色)。防误触效果不变:
   窗口内 elementFromPoint 命中面板而非项、钉死 :hover 也不跳色;窗口外恢复可点(真点「刷新」触发)。
③ 上传页链接结果区手机排版:标签栏由 flex-wrap 改成手机单行可横滑(flex-nowrap + overflow-x-auto +
   scrollbar-none,标签加 whitespace-nowrap shrink-0;sm 起恢复 flex-wrap),「复制全部/清除」改到
   独立一行右对齐(桌面靠 flex-1 撑开仍在右侧同排),链接行改 min-w-0 + truncate(原来无省略号且溢出)。
   实测 390x844:标签栏 4 行 152px → 1 行 38px 可滑(maxScroll 295,滑到底最后一个标签完整可见、
   6 个逐个可达);按钮不再与标签重叠(overlapsAnyTab 空);链接行越界 0/6(原 6 行里 5 行文字超宽且
   无省略号);缩略图 40x40 未被压;点行仍能选中完整 URL、#copy-all 取到完整 URL。桌面 1280x800 重建后
   与改前一致(2 行、按钮同排)。行为/钩子零改动(#links-container/#links/#copy-all/#clear-all/#link-tabs、
   data-tab/data-url/data-operate=preview、ClipboardJS、tab 切换与清除逻辑全部原样)。
验证:longpress 79/79、sidebar 6/6、images-modal 35/35、images-modal-dom 35/35、swal-theme 48/48;
webpack compiled successfully;新类 flex-nowrap/sm:flex-wrap/sm:items-end 已确认进产物;
补丁钉住值同步为 context-js.js b13fc1e4…(images.blade.php 仍 09ba35f3…),补丁重生成 2818 行且
「打回上游 = 仓库文件」字节一致。
2026-09-29 18:19:42 +08:00
mole404 db05433596 feat(细节三): 确认框动画与背景位移、主菜单防误触高亮、头像阻塞加载、弹窗滚动链
① 三个(实为四个)sweetalert2 确认框(设置权限/重命名/删除/删相册):保留原实现,只改动画 +
   斩断它对 body 的动手脚。四处 Swal.fire 加 heightAuto:false + scrollbarPadding:false;
   common.less 里把 .swal2-show/.swal2-hide 换成与 x-modal 同一套(桌面 300ms ease-out 淡入+
   scale .95→1、200ms ease-in 淡出;<768px 换成 translateY(16px)),遮罩过渡同源,并用
   html body.swal2-shown:not(...) 兑掉 sweetalert2 对 body 的 overflow/padding 注入(不用 !important)。
   实测:修前 4 个弹窗×2 视口都会让整页位移(桌面滚动条消失 +8px、手机注入 padding-right 8px 且
   文档高 +20px、工具栏换行下移);修后逐项相等(关闭后复原也逐项相等)。动画实测 opacity 落在与
   x-modal 同一条贝塞尔曲线上(如 ct=100/300 实测 0.6853 vs 曲线 0.6851,线性应为 0.3333)。
② 长按主菜单防误触高亮:把二级菜单那套手势级 arming 复用过来(menu-arming,420ms + 抬手后 260ms,
   跟手势走),只在「这一发菜单确实紧随一次触摸」时武装,桌面右键路径一行都不走。
   CDP 真触摸实测(390x844、iPhone UA、手指压在菜单项上):修前 elementFromPoint 命中 <a>(会高亮,
   钉住粘滞 :hover 后整行变蓝 = 老师截图那个现象);修后按住期间/抬手+60ms 命中的都是外层容器、
   不跳色,抬手+480ms 恢复正常可点;冗余窗内真点「复制图片」仍触发、点「复制链接」仍就地展开子菜单、
   桌面鼠标 hover/点击行为与改前逐项一致。
③ 右上角头像:真因查明 —— 它是外部服务 https://cravatar.cn/avatar/<md5(email)>?s=96&d=mp&r=g
   (Utils::getAvatar),实测 376B、0.85~0.96s、每次 1 跳重定向,且页面 load 事件被它拖住(实测
   慢源下 load 3214ms)、加载失败时没有任何兜底(所以空着)。改法:先渲染固定 28×28 占位(不抖动),
   真实地址放 data-avatar-src,页面 load 之后再异步接上;加 decoding=async + onerror 失败兜底
   (名字首字色块圆圈)、成功则移除首字占位。实测:慢源下 load 3214ms → 470ms(不再等它),
   404 时从"空白"变成"首字圆圈",且头像块与整个按钮的位置尺寸与修前逐项相等。
④ 手机在弹窗内下滑底部空一块:真因是**滚动链**——弹窗内滚到尽头后滚动传给背后文档(实测修前
   document.scrollTop 从 0 冲到 2628),安卓上地址栏随之收起、底部露出空带。修法:弹窗滚动容器加
   overscroll-contain 阻断接力;高度链上残留的 100vh 换成「同一块内先 vh 后 dvh」的内联声明
   (CSSOM 实测 min-height 解析为 100dvh,祖先链已无 vh)。实测:修后 6 组手势(手机/桌面 × 长/短
   内容 × wheel/touch)背后文档 scrollTop 恒为 0,弹窗内仍能正常滚到 maxScroll;桌面各项与改前逐项一致。
验证:longpress 79/79、sidebar 6/6、images-modal 35/35、images-modal-dom 35/35、swal-theme 48/48;
webpack compiled successfully;补丁钉住值同步为 context-js.js 3614ec17…、images.blade.php 09ba35f3…,
补丁重生成 2802 行且「打回上游 = 仓库文件」字节一致。另清理了子代理遗留的调试脚本 test/dbg-gap.mjs。
2026-09-29 17:02:01 +08:00
mole404 5773d432fb feat(细节二): 弹窗观感统一 + 三个旧弹窗换新装 + 返回键间距真修好 + 手机弹窗高度适配 + 上传全部按队列显隐补漏
①弹窗:
- 「移动到相册」弹窗:相册列表下那行「我也是有底线的~」(无限加载的哨兵)按弹窗 id 收窄隐藏,
  footer 多余的横线(border-t)去掉;列表仍可滚动、触底仍会继续加载相册。
- 两个新弹窗排版统一:标题 16px semibold、标签 13px / 值 14px、行高宽松、字段行装进一张
  divide-y 卡片等距排列;只用设计令牌色(亮/暗自动正确)。
- 右键菜单的「删除」「重命名」「设置权限」三个 sweetalert2 确认框换成同一套观感
  (12px 圆角、16px 标题、13/14px 正文、按钮 = ls-btn/ls-btn-primary/ls-btn-danger、
  输入框 = ls-input);JS 一行未改,确认/取消/校验流程原样。皮肤不用 !important,
  靠 html 前缀提权重压过 swal2 运行时注入的样式。
②「<返回」间距真因(上一轮判断错了):标记里那个 mr-1 工具类**从未被 Tailwind 生成**
  (tailwind.config.js 的 content 只扫 blade,扫不到 resources/js 里的类名),图标 margin-right
  实测 0px;且 context-js.css 在页面里没有版本串、iOS 会吃缓存。修法:规则同时写进
  context-js.less 与 common.less(后者由 layout 用 ?v=assetVersion() 引入,必然重新拉取)。
  真触摸复现实测:图标右缘→文字左缘 = 7px(修前 0px),旧缓存 CSS 情形下同样 7px。
③手机弹窗底部空一块:共用组件 modal.blade.php 的容器改 h-[100dvh]、内层保留 min-h-screen 兜底
  (CSSOM 实测 dvh 声明排在 vh 之后,支持则用 dvh、不支持自动退回 100vh);桌面端逐项实测
  与改前完全一致(容器/卡片高度、居中位置、页面可滚动高度均未变)。
④「上传全部」显隐补漏:漏在「逐张手动删除」那条路径上(走的是另一个函数),现已接上,
  共覆盖 入队/状态变化/自动清除/逐张删除/一键清空 五条路径;文案去掉「同时」二字。
  另修一处会让构建失败的坑:context-js.less 注释里写了 resources/views/**/*.blade.php,
  其中的 **/ 提前闭合了 /* */ 注释 → LESS "Unrecognised input"(已改写该句)。
验证:longpress 79/79、sidebar 6/6、images-modal 35/35、images-modal-dom 35/35、
swal-theme 48/48(新增,含 jsdom 复刻注入顺序验证皮肤真生效 + 一条反证);php -l + view:cache
exit 0;重建产物核对 .text-[16px]/.h-16/.transition-colors/.divide-y/.min-h-[100dvh]/.h-[100dvh]/
#image-movements-modal/.swal2-popup 全部存在;钉住的 context-js.js 两份 md5 仍 = b916555a…,
images.blade.php md5 已同步为 00f38dbf…,补丁重生成 2578 行且字节级验证通过。
2026-09-29 15:41:13 +08:00
mole404 30862d602f feat(图片页 1/2): 详细信息与移动到相册改用居中卡片弹窗、二级菜单返回键加空隙
#1 「详细信息」「移动到相册」弃用右侧抽屉,改复用 x-modal 的居中卡片弹窗(新增
   #image-detail-modal / #image-movements-modal 及其内容容器,三个新模板):
   - 详细信息:顶部小缩略图 + dl 行式布局(<640px 自动上下堆叠),标签 13px / 值 14px /
     leading-6;字段顺序按老师要求 = 上传时间 → 图片名称 → 其余 10 项(原抽屉里 12 项一个没删,
     只换位置);弹窗内不放复制链接/下载/删除(单一职责)。
   - 移动到相册:单选列表(当前所在相册带「当前」徽标)、行 min-h-[44px](手机点击区)、
     未选中时「移动」disabled;请求逐字沿用原实现(同 route user.images.movement、同 payload);
     点「移动」才提交,成功关弹窗 + resetImages + toastr。
   - 右侧抽屉保留本职(工具栏「相册列表」入口、自带无限加载、相册增删改查)原样不动;
     桌面右键、手机长按、工具栏三个入口都走新弹窗。
#2 二级菜单「<返回」箭头与文字间距:context-js.less 加 7px(选择器权重压过标记自带 mr-1)。

关键:本轮的 images.blade.php 改动不属于补丁文件(context-js.js 两份 md5 全程 = Dockerfile 钉住的
b916555a…,未动),Dockerfile 的 images_blade_md5 已随自证同步更新为 27bb3f20…,补丁重生成 2563 行
且「打回上游 = 仓库文件」字节一致。

验证:四套回归我自己跑过 —— longpress 79/79、sidebar 6/6、images-modal 35/35(静态契约:
12 字段与顺序 / 13-14px 字号 / min-h-44 / 请求不变 / 无硬编码色 / 返回键 7px / 补丁 md5 未变)、
images-modal-dom 35/35(jsdom 真跑页面内联脚本:点菜单 → 内容进新弹窗、抽屉全程为空且未被推开、
单选互斥、点「移动」实发 {selected,id,album_id} 后关弹窗、抽屉路径仍能筛选相册);
php -l + view:cache exit 0;重建产物含 text-[13px]/[14px]/min-h-[44px]/max-h-[50vh]/ls-badge 等。
2026-09-29 14:34:25 +08:00
mole404 1a57f7660c feat(细节优化 3/4/5/6/7): 三个小按钮统一圆形、上传全部按队列显隐、最小号字抬档加深、侧栏容量上移、清空✕常显
#3 右上角三个小按钮统一为圆形:主题按钮 w-9 圆角方块 → w-10 h-10 rounded-full;存储策略去掉
   内层带边框的小圆圈(就是"套娃"来源),并统一 h-10 w-10 rounded-full + 同 hover;头像本来就是圆。
   实测三个 border-radius 均 9999px、高 40px(窄屏三个都是 40px 正圆;宽屏后两个带文字所以是胶囊)。
#4 「上传全部」只在队列里还有待上传文件时出现:新增 syncQueueActions(),在 状态变化/入队/自动清除/
   手动清空 四个时机统一 toggle;按钮初始 hidden。
#5 最小号字 12.5px → 13px,同行的最浅灰 ink-3 → ink-2(仪表盘 2、控制台 8、上传页 3 处)。
   亮暗双主题实测对比度:ink-2 = 6.0:1(亮)/ 6.92:1(暗),均 ≥4.5 小字可读线。
#6 侧栏「容量使用」mb-5 → mb-16,避开浏览器左下角链接预览的遮挡。
#7 清空队列的 ✕ 从 hidden group-hover:flex → 常显 opacity-40、hover 不透明(触屏没 hover,手机原来点不到)。

验证:重建产物确认 .text-\[13px\] / .opacity-40 / .mb-16 / hover:opacity-100 均已生成;
php -l + view:cache exit 0;jsdom longpress 79/79、sidebar 6/6。
2026-09-29 14:24:25 +08:00