Commit Graph

79 Commits

Author SHA1 Message Date
bb41b61ad6 fix(gateway): 路由表接口只替换路由,不再整份配置重置(agent 友好)
真实验证(干净环境练兵)抓到:POST /admin/api/gateway/routes 会把整个
GatewayConfig 反序列化为零值后保存,因此
- 只提交单个路由对象 → {"routes":0,"status":"saved"}:**静默清空整张路由表**
- 提交 {"routes":[…] } → 其余字段被重置为默认(实测入口端口 13080 → 8081,
  TLS/认证地址一并丢失),随后 apply 生成的配置与预期完全不符

对 agent 尤其危险:照文档提交一条路由就会改掉入口端口与所有其它路由。

修复:
- 只补路由表:以现有配置为基准,仅替换 Routes,其它字段原样保留
- 请求体必须是 {"routes":[...]};缺该键 → 400 并说明正确形状(不再静默变空表)
- 非空表要清空必须显式传 force:true,否则 400 拒绝
- 测试:单个对象被拒;数组提交后 listenPort/TLS/认证地址/证书路径全部保留;
  清空被拒、force 允许
- README:修正路由示例为完整数组形状,补"替换整张表"与 /admin/api 前缀说明
2026-09-12 10:28:03 +08:00
92fd32e7a9 feat(api): 全量配置项暴露为接口 + 后台增加门户卡片编辑框
用户要求:所有可配置项都应当是 API,方便 agent 直接把站点配成用户喜欢的样子。

- 新增 GET /api/schema:一次返回
  · items:全部单值配置项(站点信息、登录页/门户页文案、备案号、页脚…),
    含当前值、类型、写入口与请求体示例;**当前为空的键也会列出**,否则 agent 发现不了
  · resources:结构化资源与其真正的写入口(导航、路由表+apply、门户卡片、壁纸、
    门户口令、历史、图标上传、重载),并在 hint 里写明"路由表改完必须 apply"这类约束
- 后台「站点信息」新增「门户页服务卡片」编辑框:载入 / 保存 / 清空即回内置,
  与 /api/portal-cards 同一份数据(界面能改的 agent 都能改)
- README 新增「配置接口(agent 可直接改)」:schema 用法、写入口对照表、可直接复制的示例
- 测试:schema 必须列出关键项与写入口;卡片接口写入生效、清空返回 reset 并回落内置
2026-09-12 10:19:22 +08:00
7c2bed2f2d feat: 门户页卡片可配置 + 升级前域名校验 + 清理明文门户口令
按用户选择实施三件事:

1) 门户页服务卡片可配置(不再写死真实域名)
   - portal/index.html 卡片区加 <!--NAVI_CARDS_START/END--> 标记,内置卡片改为通用示例
   - renderPortalPage 支持数据卷 <data>/portal/cards.html 覆盖标记区间(清空即回落内置)
   - 新增 GET/POST /api/portal-cards 读写该片段
   - 测试:覆盖替换成功、空白片段回落内置
   - 仓库内真实域名由此清零

2) 升级前必须提供跨子域单点登录变量(防止"去掉写死默认值"造成 SSO 静默失效)
   - zd-host.sh 在切换前检查 PORTAL_COOKIE_DOMAIN:缺失且未传 NAVI_COOKIE_DOMAIN
     时直接拒绝升级(退出码 2)并给出正确命令;PORTAL_HOME_URL 可用 NAVI_HOME_URL 补
   - README 新增「升级已有部署前必须确认的两个变量」含失败表现与正确命令

3) 清理明文门户口令
   - 迁移旧容器 env 时丢弃 PORTAL_PASSWORD(docker inspect 可见 = 泄露面)
   - 升级后写入强随机口令并通过接口生效,部署日志只打印一次;失败不阻断升级并提示后台可改
2026-09-12 10:14:22 +08:00
b99319b38b chore(sanitize): 备案号与域名一律可配置,仓库不再预置真实域名/备案号/内网地址
用户要求:备案号与域名都应当可配置,仓库内容要脱敏。

功能(不只是替换字面量):
- 被代理页面注入的备案号改为读后台「站点信息」的 icp/gongan,**未配置就不注入**;
  原先写死具体备案号,等于每个部署者都会把别人的备案信息印到自己页面上
- 注入内容做 nginx 安全处理(去引号/反斜杠/换行/美元符),并有测试覆盖
- auth.go:PORTAL_COOKIE_DOMAIN / PORTAL_HOME_URL 不再预置域名,
  cookie 默认 host-only、首页默认相对路径 "/"
- gateway.go:默认 loginUrl 不再预置域名(受保护路由缺配置会明确报错,不静默生成跳不出去的登录页)
- 首装脚本在给了 NAVI_DOMAIN 时自动注入 PORTAL_COOKIE_DOMAIN/PORTAL_HOME_URL,
  避免"去掉写死默认值"导致跨子域单点登录失效

脱敏:内网 IP(10.0.0.10)与真实备案号清零;真实域名仅剩门户页内容 8 处(待定);
测试夹具/示例/错误页/脚本/后台占位提示全部通用化。

README:新增「跨子域单点登录需要的两个环境变量」;环境变量表改为 NAVI_HOST 必填。
测试:新增备案号注入三条(用配置、未配置不注入、危险字符清理)。
2026-09-12 10:10:52 +08:00
b01e49de49 fix(gateway): http→https 跳转是否带端口改为显式开关(避免生产回归)
练兵发现:跳转写成 return 302 https://$host$request_uri(不带端口),
非 443 端口的新装会跳到 443 打不开。

但直接用'端口 != 443 就带端口'推导会把生产弄坏:生产容器监听 3080,
对外经 frp 映射为 443,带端口会把公网用户导向 https://host:3080。

因此新增 gateway.yml 字段 redirectWithPort(默认 false,行为不变):
- 默认:跳 https://host(前置 frp/443 的部署,含现生产)
- 全新安装且对外端口=入口端口:install-host.sh 自动置 true
测试补两条:开关为真时带端口、默认(生产形态)不带端口。
2026-09-12 09:52:20 +08:00
9612352bd1 fix(deploy): 首装 gateway.yml 字段名大小写错误导致 TLS 被静默忽略
TLS 练兵抓到(脚本自检 + 独立核对全挂,端口在监听但握手失败):
生成的配置里三个 listen 13080 都没有 ssl、且没有 ssl_certificate,
说明 ListenHTTPS 一直是 false。

根因:GatewayConfig 的 yaml 标签是 listenHttps / listenTlsPort / authUrl /
loginUrl(大小写敏感),而脚本写的是 listenHTTPS / listenTLSPort / authURL /
loginURL → 被 YAML 静默忽略。后果严重:HTTPS 端口实际是明文,而跳转端口
又 302 到该端口,浏览器必然报错;authUrl/loginUrl 丢失还会让受保护路由
报'未配置认证地址'。

修复:字段名对齐 + apply 后断言 TLS=1 时配置必须含 ssl_certificate(失败即停),
避免同类字段名错误再次静默通过。
2026-09-12 09:46:18 +08:00
fe5ddf274b feat(deploy): 首装脚本补 TLS 自检(跳转端口 302 / HTTP2 协商 / 证书可读)
TLS 是有证书用户的主路径,之前只在生产手工验过、脚本没覆盖。补上后
NAVI_TLS=1 的首装会自己验证:http 跳转端口是否 302 到 https、入口能否
协商 HTTP/2(对应曾踩过的 listen … ssl http2 写法)、证书是否能读出主题。
2026-09-12 09:45:13 +08:00
965c9cb41a fix(deploy): code_of 定义提到等待循环之前(自检退出码 127)
第二次练兵:路由表路径修复生效(apply routes: 2),但因等待监听的循环
调用了尚未定义的 code_of,脚本以 127 退出。把函数定义移到首次调用之前。
2026-09-12 09:41:01 +08:00
158217103d fix(deploy): 首装路由表路径写错(静默零路由)+ apply 空表安全阀
真实练兵(干净数据目录跑 install-host.sh)抓到:
1. 路由表源文件被写到 <数据卷>/gateway/gateway.yml,容器实际读取 <数据卷>/gateway.yml
   → apply 报 routes: 0,用户自备/生成的初始路由被静默忽略
2. 自检在 apply 后立即探测,nginx 还没 reload 完 → 误判连接失败;
   code_of 失败时重复输出 000
3. 缺安全阀:路由表为空时 apply 会照常生成零路由配置并覆盖,
   等于静默清空入口路由表

修复:
- install-host.sh 改写 <数据卷>/gateway.yml(并注释说明两个目录的区别)
- 自检前轮询等待入口监听就绪(最多 10s),code_of 只在成功时输出状态码
- handleGatewayApply 增加安全阀:路由表为空但现有配置含 server_name → 400 拒绝覆盖
- 新增测试 TestApplyRefusesEmptyRoutesOverExisting
2026-09-12 09:40:36 +08:00
c0cbdb5efc feat(deploy): 补首装路径,使他人/AI 可照教程从零部署
审计发现 5 处断点(3 处硬阻断):
1. Dockerfile 的 HTTP_PROXY/HTTPS_PROXY 默认指向内网代理 → 他人构建必失败
2. zd-host.sh 是升级脚本(要求已存在 host 模式 navi 容器)→ 新机器第一句过不去
3. docker-compose.yml 是桥接+端口映射,与'统一入口 host 模式'口径冲突
4. 证书文件名约定未文档化(实际来自 gateway.yml 的 certFile/keyFile)
5. 环境变量表不完整、缺 sshpass 依赖说明

本次:
- Dockerfile:代理默认置空,需要时 --build-arg 传入
- deploy/install-host.sh(新):干净机器首次安装,6 步并在任一步失败即停,
  含端口独占检查、证书检查、初始 gateway.yml 生成、apply、四项自检,
  令牌可自动生成并只打印一次;已存在 navi 容器时拒绝执行(引导去 zd-host.sh)
- docker-compose.yml:标注为试用形态,指向 install-host.sh
- README:新增「从零部署(干净机器)」——两条路径分工表、前置条件表、
  完整环境变量表、证书与初始网关配置说明、装完自检命令;自定义文件表登记新脚本

另修:脚本自检原用 /.navi-ping(该路径已按用户要求删除),改用 /conf.yml。
2026-09-12 09:37:45 +08:00
cb86557466 test(gateway): root 去重断言改为按 server 段计数
上一版断言用全局计数,而默认段与门户段各有一个 root(合法),因此必然失败;
真正要防的是同一段内重复。改为取门户段后再计数。
2026-09-12 09:20:35 +08:00
b4241741d6 fix(gateway): 门户段重复 root 导致 nginx -t 失败
v52 部署被 apply 的校验拦下("root directive is duplicate"):writeErrorPages 已写过
root /app/portal;,我又在 portal 分支写了一次。移除重复行,并加单测断言
该指令在生成结果里恰好出现一次。回滚机制按预期工作,服务未中断。
2026-09-12 09:20:18 +08:00
52164b6abe fix(gateway): 门户首页走 admin-server 渲染(上次改动未落盘)
上一轮声称改过 kind=portal 的生成逻辑,但 git log 显示 gateway.go 从未提交该改动
(我的修改没有真正落盘),导致:线上仍用内置模板,而新镜像的模板带 HTML 占位符
→ 门户页占位符裸露、标题为空、文案不可配置。

本次:portal 分支改为 location = / 反代到 :3005/portal-page(静态资源仍 root
/app/portal),并新增单测断言该引用存在——今后若再被改掉,测试会失败。
2026-09-12 09:17:34 +08:00
13ab2336c6 fix(deploy): host 部署后自动重新生成一次网关配置
事故:门户首页改为由 admin-server 渲染(/portal-page)后,镜像里的门户模板带
HTML 占位符,而线上 gateway.conf 仍是上一版生成的(未引用渲染器),导致
门户页占位符裸露、标题为空。

在 zd-host.sh 的入口自检之后加入 apply 步骤(用同一份 env 里的 admin token),
失败即中止并回滚,避免镜像已更新但配置没跟上这类静默不一致。
2026-09-12 09:16:38 +08:00
472d8d1cd0 feat(pages): 认证页与门户页文案可配置,不再写死
需求:登录页与门户页的内容应可自定义。

实现(配置存数据卷、后台可改、留空回落内置默认,不配置行为不变):
- pages.go:pageStr 读取 pageInfo;renderPortalPage 优先数据卷
  <data>/portal/index.html 整页覆盖,否则用内置模板替换占位符;
  authPageInfo 提供登录页字段
- auth.go:登录页标题/副标题/徽标/页脚改为模板数据(含备案号)
- gateway.go:kind=portal 的首页改由 admin-server /portal-page 渲染
  (静态资源仍由 nginx 提供),使文案可配置
- portal/index.html:加入 <!--PAGE_TITLE--> / <!--HERO_TITLE--> /
  <!--HERO_SUBTITLE--> / <!--FOOTER--> 占位符
- 后台「站点信息」新增六项文案输入(含回填与保存),并提示整页覆盖方式
- README 新增「认证页与门户页文案(可配置,不写死)」含字段表与自检命令

测试:默认回落、配置取值去空白、认证页字段读取、数据卷覆盖优先、空白覆盖忽略。
2026-09-12 09:12:13 +08:00
352aa570ec chore: 验收脚本入库(矩阵/工作空间/滚动)+ 修正脱敏自检命令
- deploy/verify-matrix.js:6 尺寸 × 3 视图 × 深浅色矩阵,断言纵向滚动(仅
  minimal/workspace)、文字亮度阈值、深色是否生效、零运行时异常
- deploy/verify-workspace.js:触发 workspace blocked 态,读弹窗卡片 computed style
- deploy/verify-scroll.js:多视口滚动复现(含 900x520 这类矮窗口)
  三者只从环境变量 PORTAL_COOKIE 取会话,无任何硬编码凭据
- README 自检命令改为排除占位符(<...>、${VAR:-}、change-me),避免误报
- 自定义文件表登记上述脚本
2026-09-12 08:55:04 +08:00
323d494615 docs: 补齐一体化模式教程、自定义文件清单与脱敏清单
- 新增「一体化模式」章节:单容器即统一入口(TLS + 统一登录 + 导航 + 搜索 + 历史/管理)
  · 端口与挂载(内部 18080 / 对外 3080、8443 / 跳转 3081;data 卷 + 证书只读卷)
  · host 模式部署命令与失败自动回滚;说明 zd.sh 双端口轮换在合并后失效的原因
  · 切换四步、服务转发字段(含逐条统一认证与导航绑定)、现有 nginx 配置导入
  · 统一登录账号(数据卷存加盐哈希,优先级与生效方式)
  · 深浅色/壁纸/状态点关闭的原因(含 background 简写清掉壁纸的坑)
  · 回滚路径(容器、配置备份、数据卷)
- 「部署需要的全部自定义文件」表补充:deploy/zd-host.sh、portal/、auth.go、
  gateway*.go、portal_auth.go
- 新增「绝不提交的内容(脱敏清单)」:conf.yml/备份、各类 token 与口令、portal-auth.json、
  证书、运行时 env 与 nginx 备份;并给出两条发布前自检命令
- 零停机部署与 portal-nginx 两节加指引,避免照旧流程操作
2026-09-12 08:54:33 +08:00
f08e0bf844 fix(workspace): 弹窗卡片在深色下仍是白卡(补齐)
实测(打开被拒绝内嵌的应用时弹出提示):
.navi-ws-modal 遮罩已是深色,但 .navi-ws-modal-card 仍为 rgba(255,255,255,.96),
深色下是刺眼白块且与浅色文字冲突;按钮同样是亮色。

补齐卡片/标题/正文/按钮的深色样式。内嵌区上一条已改为透明(去黑方块)。
2026-09-12 08:39:43 +08:00
b794aed18e fix(workspace): 去掉内嵌应用区的黑方块(改透明)
用户反馈工作空间深色下有黑色方块。实测:亮色时代该区域是 #fff,我在 v44
把它改成 #0f172a,于是打开应用时那块区域变成一整块不透明黑。

修法:内嵌区与 iframe 一律透明(露出壁纸/玻璃底),应用自身背景照常显示,
空态也不再出现突兀方块。深/浅两种模式行为一致。
2026-09-12 08:37:02 +08:00
9798b68d07 feat(status): 按用户要求彻底关闭可达性状态显示
用户决定不显示服务状态,避免不准确的信号误导。因此:
- 移除导航同步里写入的 statusCheck / statusCheckUrl
- 移除网关为每条路由生成的 /.navi-ping(不再需要,且不应保留一个
  免认证旁路路径)
- 删除对应单测

生产 conf.yml 的 statusCheck 在同一次部署中关闭(appConfig 置 false、
条目级该行删除),并重新 apply 生成网关配置以清掉 /.navi-ping。
2026-09-12 08:32:03 +08:00
da7a56e7ec fix(status): 修掉 Dashy 可达性检查"全黄"——改用免认证探测路径
用户反馈可达性全黄。实测:appConfig.statusCheck 已开启,但等 6 秒仍无状态
节点渲染;上游直连全部 200。机制:Dashy 用 no-cors 探测条目 URL,而受保护域名
会 302 跳到另一个域(登录页),no-cors 下跨域重定向直接报错 → 检查永远停在
"进行中" → 全黄。

修法:
- gateway 为每条路由生成 location = /.navi-ping:auth_request off + 反代到
  真实上游(上游挂了返回 5xx → 红;正常 2xx → 绿),portal 类型直接 204
- 导航同步时设置 NavItem.StatusCheck = true 与 statusCheckUrl 指向该路径
- 注意:statusCheck 是 NavItem 的结构体字段,不能塞进 inline Extra(yaml 会
  panic: conflicts with struct field)——已改为设置字段本身

测试:新增断言——探测路径存在、免认证、反代到真实上游、portal 返回 204。
2026-09-12 08:23:33 +08:00
2d3b1e31de fix(layout): 矮视口下备案号移出文档流,彻底修掉 minimal 纵向滚动
实测:仅压缩内边距后 .navi-footer-host 仍有 37px 高且在 flow 中,
900x520 下 minimal 的 scrollH=536 > viewH=520(溢出 16px)。
改为 position:fixed 后不再撑高页面;搜索栏与按钮组已在 bottom:60px,不会相撞。
workspace 视图原本就是这样处理。

同时记录另一处误判:矩阵里"900x520 失去深色"是我等待不足(2s)抢在
navi.js 应用主题前测量所致;复测 mq=true/navi-dark 均已生效,深色无问题。
2026-09-12 08:12:09 +08:00
67038ce4de fix(layout): 矮视口下备案号不再撑高页面(修 minimal 纵向滚动)
实测复现:900x520 时 .navi-footer-host 由 39px 换行涨到 69px,因处于文档流
(margin-top:auto)把页面顶高 47px,minimal 出现纵向滚动;1280x720 等常见尺寸
复现不出来,所以此前漏检。

只对 max-height:560px 收紧:缩小内边距/字号 + 禁止换行,并同步上移搜索栏
与按钮组,避免与备案号相撞。高窗口与移动端不受影响。
2026-09-12 07:55:22 +08:00
c8f6ac68fe fix(theme): workspace/minimal 深色补齐(侧栏白色药丸、内嵌应用白框)
实测(computed style):
- nav.side-bar 已是深色,但 .side-bar-item 仍为 rgba(255,255,255,.7) 白色药丸
- .work-space .web-content 为 rgb(255,255,255) 纯白,深色下形成刺眼白框
- .navi-ws-bar-btn / 工具条 / 提示框一并补齐深色

minimal 的纵向滚动在 1280x720 下复现不出来(scrollable=false),
需要用更矮视口复现后再修,避免凭猜测改动布局。
2026-09-12 07:53:08 +08:00
064f986a7f fix(theme): 深色遮罩交给服务端,避免双重压暗
实测发现深色下 body 的 background-image 仍是亮色的 70% 白遮罩(壁纸被洗白)。
遮罩由服务端 applyWallpaperBlock 写进 conf.yml,因此在那里补一条深色规则
(同一张壁纸 + rgba(6,12,24,dim+0.15) 遮罩),并移除 navi.css 里那层
body::before 遮罩,避免壁纸被双重压暗。加单测断言深浅两条规则共存、
且移除壁纸时一并清除。
2026-09-12 07:49:34 +08:00
1257164c3f fix(theme): 备案号深色药丸 + 确认不再用 background 简写
浏览器规则枚举确认元凶:navi.css 的 html.navi-dark body 用 background 简写
(特异性 0,2,1 > 壁纸规则 0,1,1),简写把 background-image 重置为 initial,
于是壁纸消失。本地已改为仅 background-color + body::before 深色遮罩。

同时按实测补上备案号药丸的深色样式(此前实测仍是 rgba(255,255,255,.55)),
并把内部元素强制透明,避免深底上贴一块白。
实测数据:备案号位于文档最底部(y=1079..1125 / 文档高 1125),与悬浮件无重叠。
2026-09-12 07:46:09 +08:00
e064ef5296 fix(theme): 深色不再抹掉壁纸,改为深色遮罩压暗
用户反馈深色下背景图片消失。原因:我在深色里用了 background 简写并带
!important(特异性 0,2,1 > 壁纸规则的 0,1,1),把壁纸的 background-image
清成了 none。

改为:只覆盖 background-color,另加 body::before 深色遮罩(与亮色模式
白色遮罩同思路,z-index:-1 是本站既有做法),壁纸依旧可见、只是变暗。
同时修正上一轮验证方法的缺陷:那几次"看到旧样式"是浏览器缓存造成的假象。
2026-09-12 07:40:56 +08:00
a78c5180ca fix(deploy): host 模式远端显式接收 VER
首次 host 部署在停旧容器后因远端 VER 未定义触发 unbound variable 而失败
(当时旧容器仅被改名未恢复,已手工恢复)。此处只补参数传递,不改动
heredoc 结构与回滚顺序。
2026-09-12 07:36:20 +08:00
817a5991b3 fix(theme): 基于真实浏览器渲染修复深色文字与右侧方块
此前只检查 CSS 规则存在,没有检查真实页面 computed style,导致标题、分组标题、
卡片正文仍被前面的亮色 !important 规则覆盖;右侧 dock/view-switch 布局容器也被
错误涂上背景,形成灰色方块。

这次通过共享 Chromium 实际登录并强制深色模式,逐项读取 computed style:
- 顶部标题 #f1f5f9
- 分组标题 #dce6f5
- 卡片主文字 #f1f5f9、说明 #a9b8cd
- 分组/卡片使用协调的深色玻璃层级
- dock 与 view-switch 布局容器完全透明,仅两个圆形按钮着色
- 备案号为完整深色胶囊,子元素透明
- 工作空间/极简视图同步补齐深色侧栏、工具条和弹窗

自动验证同时断言:文字亮度、深色面板、透明容器、圆形按钮、零运行时异常。
新增 deploy/zd-host.sh,供单容器正式接管入口后的 host 网络部署使用;失败自动回滚。
2026-09-12 07:30:00 +08:00
44e2792f0e fix(theme): 深色下主页底色与右下悬浮件仍不生效
现象:主页大面积没变深;右下角仍有一块灰色方块。

根因:
1. conf.yml 的玻璃主题用 html[data-theme] body 设了亮色背景,特异度与
   html.navi-dark body 相同(0,1,1),而 customCss 由 Dashy 运行时注入、排在
   navi.css 之后 —— 平局时后者胜出,所以页面底色没被改掉。
2. 右下那块是我们自己的悬浮件,但此前只逐个类名列举,漏掉的类名就保持原样。

改法:
- navi.js 把 navi-dark 同时打到 <html> 与 <body>(并在 DOMContentLoaded 再应用一次)
- navi.css 用 html.navi-dark body.navi-dark 提高特异度到 (0,2,1),压过 customCss,
  与注入顺序无关
- 兜住 Dashy 外层容器(#dashy/.home/.workspace/.minimal-home)与原生页脚
- 用 [class^="navi-"]/[class*=" navi-"] 整体兜住我们注入的悬浮件,避免再漏类名
2026-09-11 23:38:56 +08:00
b1e4367937 fix(theme): 深色改为整套主题变量覆盖,不再改写 data-theme
用户反馈"深色区域有的方形、有的根本没变深"。根因有两层,都是我的做法错了:

1. navi.js 会把 <html data-theme> 改成 "dark"。Dashy 的配色变量是按主题名
   作用域输出的(本站主题 jianf-glass),改掉它 → 整套主题变量失配 →
   有的区域完全没变深。
2. navi.css 之前只覆盖"页面底色 + 我们自己的悬浮件",Dashy 自己的卡片/侧栏/
   头部仍走原主题 → 与深底色拼在一起,就成了直角圆角混杂。

改法:
- navi.js 只切 <html class="navi-dark">(并设 color-scheme),绝不碰 data-theme
- navi.css 用 html.navi-dark 一次性覆盖 jianf-glass 的全部配色变量(47 个:
  背景/卡片分组/条目文字/侧栏/头部导航/搜索框/设置面板/弹层/滚动条/语义色),
  Dashy 自身界面随之整体一致变深;形状由元素自身样式决定,不再混杂
- 选择器 html.navi-dark 特异度(0,1,1) 高于 [data-theme=…](0,1,0),与注入顺序无关

需要目视确认配色层次(变量值只能看着调)。
2026-09-11 23:31:27 +08:00
1c20cab519 fix(admin): 后台设置页支持自动深浅色
反馈"设置页也没有":/admin/ 只有一套亮色变量(我写它时没做主题),
不跟随系统也不跟随导航页/登录页的手动选择。

- head 内联引导脚本:localStorage['navi-theme'] 优先,其次 prefers-color-scheme
- 深色下整体替换 CSS 变量,并补掉少数写死的白色
  (输入框/按钮/列表项/图片底/阴影),加 color-scheme 让原生控件与滚动条跟随
- 与导航页、登录页共用同一个 storage 键,三处观感一致
2026-09-11 23:23:15 +08:00
a8ce6ad759 feat(theme): 导航页恢复自动深浅色(跟随系统),保留隐藏原生设置入口
用户反馈:现在没有自动深/浅色。实测原因有两层:
1. Dashy 原生的日/夜切换按钮在设置面板里,而该面板按要求被隐藏;
2. navi.css 没有任何 prefers-color-scheme 分支,且多处写死亮色。

本提交按"我们自己接管"实现(不去赌 Dashy 压缩后的主题存储键):
- navi.js:默认跟 prefers-color-scheme;手动切换过以 localStorage.navi-theme 为准;
  系统偏好变化实时跟随;给 <html> 设 data-theme 与 .navi-dark,并设 color-scheme。
  Dashy 挂载后可能覆写 data-theme,用短暂定时对齐 + 属性观察器收口
  (仅在"与期望不一致"时回写,避免自激循环)。
- navi.css:深色下覆盖页面底色与文字,以及我们自己注入的悬浮控件
  (搜索条/收边控件/页脚/工作空间工具条与弹窗/迷你视图标签条)。
  选择器带 body 以压过主题注入的 customCss。

需要人工目视确认观感(深色下的玻璃层次只能靠眼睛调)。
2026-09-11 23:18:54 +08:00
c597fa1991 fix(auth): 登录页恢复自动深浅色(我在重写模板时造成的回归)
原 portal-auth 的登录页有 prefers-color-scheme 检测 + 手动切换 +
localStorage 记忆;我把它换成内置登录服务时只写了亮色,自动深浅色因此消失。

- 默认跟随系统(prefers-color-scheme),手动切换后写入 localStorage
- 深浅两套 CSS 变量(背景/卡片/边框/文字/输入框/错误提示/阴影)
- 右上角切换按钮,补 aria-label
- 加 color-scheme + prefers-reduced-motion
- 测试:断言主题检测、切换按钮、no-store;并断言 redirect 被转义(注入防护)
2026-09-11 23:14:45 +08:00
b941d8e8e3 fix(gateway): 错误页必须跳过 auth_request,否则上游故障会变成 401
实测:上游连接被拒(502)时 nginx 内部跳转 /500.html,而服务端的
auth_request 被这个内部请求继承 → 用户收到 nginx 默认 401 页,
自定义 404/500 永远显示不出来。
错误页及其静态资源一律 auth_request off,并加单测锁住。
2026-09-11 22:33:45 +08:00
1844586feb fix(gateway): TLS 段的 http2 必须写成 listen 参数
容器的 nginx 是 1.22,独立的 `http2 on;` 会报 unknown directive
(该指令 1.25.1 才引入;原 portal-nginx 用 nginx:latest 所以没暴露)。
改为 `listen PORT ssl http2;`,1.22 与 1.25+ 都可用,并加单测锁住。

本轮由旁路端口 TLS 验证发现;apply 的校验-保留机制正确拦下了这次坏配置。
2026-09-11 22:09:57 +08:00
53beff646a feat(admin): 现有配置导入 + 添加表单补齐认证/导航开关 + 统一登录账号设置 + 设置分组
针对反馈的五个问题:

1. 后台"没有映射":新增 /admin/api/gateway/import 与界面「从现有 nginx 配置导入」,
   把老统一入口的 nginx 配置贴进来即可解析出全部「域名 → 后端」初稿
   (含 requireLogin / 免认证路径 / SSE 流式 / 备案注入 / 自签证书 / auth 与 portal 类型)。
   解析只读不落盘,确认后才保存;导入默认不接管导航,避免一导入就改掉手工导航。
2. 添加转发时没有导航选项:添加表单补齐「同时加入 Dashy 导航 / 分组 / 名称 / 描述 /
   图标 / 自动抓取」。
3. 添加转发时没有 portal 保护选项:添加表单补齐「启用 portal 统一登录保护」,
   并补上 WebSocket 透传与上传上限。
4. 设置里无法配置 portal 账号:新增统一登录账号卡片,账号存数据卷
   (portal-auth.json,只存加盐哈希),优先级 数据卷 > 环境变量,
   改完立即生效、无需重新部署;会话校验按当前用户名,改账号后旧会话失效。
5. 设置项太乱:按用途分组(站点与外观 / 导航内容 / 历史记录 / 转发与统一登录 / 高级),
   外观卡片并入站点组。

另修两个本轮引入的缺陷:
- 容器假 unhealthy:内部端口改 18080 后,基础镜像的 HEALTHCHECK 仍指向 8080;
  部署脚本生成的 Dockerfile 现在显式覆盖为 :18080/healthz。
- kind=auth 不依赖 portal-auth 容器,指向内置登录服务;避免生成空 proxy_pass。

测试:nginx 配置解析(含兜底段忽略、多域名段、类型判定、默认不接管导航)、
垃圾输入报错、账号存储(不明文、可校验、改名后旧口令失效)、auth 类型生成。
2026-09-11 22:00:09 +08:00
994d93cc98 feat(gateway): 一体化统一入口——路由与 Dashy 导航同一份数据、内置登录、原自定义错误页
目标:一个 HomeDash 容器取代 navi + portal-nginx + portal-auth。

- Route 扩展为"反代 + 导航"一条记录:kind(proxy/portal/auth)、streaming、
  insecureTls、bypassPaths、injectBeian,以及 navEnabled/navSection/navName/
  navDescription/navUrl/navIcon/navAutoIcon
- 保存路由时**同时**写 gateway.yml 与 conf.yml 的导航项(gateway_nav.go):
  同 host 接管而不是重复、未勾选导航的会被清理、无关手工项与系统「历史访问」分组
  一律不动;两步任一步失败都回滚(避免后台显示与导航页各说各话)
- 未知域名与上游故障改用**内置自定义 404/500 页面**(portal/ 目录),
  不再是一行纯文本;HTTP→HTTPS 跳转段用 RedirectPort 复刻原 3081 设计
- 内置统一登录(auth.go):HMAC 签名 cookie、恒定时间比较、登录跳转白名单校验,
  portal-auth 容器可以退场
- 自动抓图标对托管导航项同样生效(history-server 识别 naviGatewayAutoIcon)
- 后台卡片补齐"加入导航/分组/名称/描述/图标/自动抓图标/服务类型"字段
- 内部入口端口 8080 → 18080:为一体化 host 模式让开宿主机 blog(:8080)
- 修掉上一轮编辑写坏的 deploy/zd.sh 一行(会让脚本直接失败)

测试:新增自定义错误页、portal 类型、导航字段校验、导航同步(接管/清理/保留)、
分组不存在必须报错等用例;admin-server 与 history-server 全绿。
2026-09-11 21:27:40 +08:00
5f6fca5c59 fix(gateway): 未知域名必须被兜底拒绝,不能落进某条路由的后端
实测发现:入口端口上若没有匹配 server_name 的请求,nginx 会交给该端口排序
第一的 server 块,于是一个打错的域名会访问到别的服务(若第一条恰好是开放
路由,等于把该服务暴露在错误域名下)。现在为 HTTP/HTTPS 各生成一个
default_server 返回 404,并在单测中断言其存在。
2026-09-11 20:34:20 +08:00
507632609d fix(deploy): 历史条数守卫改为与切换前对比,而不是写死条数
写死 4 条会把「用户自己在后台删了几条记录」误判成部署失败(本轮实际发生),
同时又发现不了真正的丢数据。改成切换前读一次旧容器的条数,切换后要求
新容器 >= 旧值:同步接口只累加不删除,所以变小即为异常。
2026-09-11 20:30:52 +08:00
19d082e70a feat(gateway): 统一入口路由表(后端基础层)——"哪些服务走统一认证"变成一条数据
目标:不再手工维护外部 nginx 配置,把"域名→内网后端、是否走统一登录认证"
变成后台可编辑的数据,由本进程生成 nginx 配置并热加载。

本提交只做基础层(纯 Go、可测、零行为变更):

- gateway.go:Route/H gatewayConfig 数据模型 + ValidateRoute + RenderGatewayConf
  · 认证**按路由**决定:RequireLogin 为真才生成 auth_request 与登录跳转,
    也就是"哪些服务受统一认证保护"完全由路由表决定
  · 支持 websocket 透传、单域名上传上限、HTTPS 段(可同时生成旁路端口)
  · 路由表单独存 gateway.yml(不混进 Dashy 的 conf.yml),写坏了也不影响导航页
- API:GET/POST /admin/api/gateway/routes(保存)、POST /admin/api/gateway/apply
  · apply 的顺序是 生成 → nginx -t 校验 → 通过才 reload;**校验失败把上一份可用
    配置写回去并把 nginx 原始报错回给界面**,避免"点了应用整个入口挂掉"
- nginx.conf 加 include /app/data/gateway/*.conf(生成物在容器自己的卷里,
  不碰宿主机文件,天然避开"单文件 bind mount 不能用 sed -i"的坑)
- 生成物默认监听旁路端口(8081/8443):可以先与正式入口并存验证,再切 80/443

测试:受保护域名必须有 auth_request、未受保护域名必须没有;websocket/上传上限;
空域名/带协议域名/非 http 后端/域名重复/需认证却无认证地址/启用 HTTPS 却无证书
六种非法输入必须报错;缺文件时回落默认配置、保存后回读一致。
2026-09-11 20:17:57 +08:00
a6b5513f88 docs+chore: 部署脚本入库、README 补零停机部署与部署物料清单
仓库此前缺少部署脚本,只能靠临时文件操作,新人无法复现。补齐:

- deploy/zd.sh:零停机部署(新容器先在对侧端口验证到底,含 /search 预热
  SearXNG;再同 inode 覆写门户上游 + nginx -t + reload;切后哨兵"不得为 502";
  失败自动回滚,旧容器保留为回滚点)。可用环境变量覆盖主机/路径/镜像。
- deploy/verify-ui.js:部署后浏览器验收(渲染开销、后台窄屏、收边两段式、
  被拒内嵌弹层、分组增删改与「历史访问」保护)。任一运行时异常即判失败。

README 新增:
- 「零停机部署」含**单文件 bind mount 不能用 sed -i** 的坑(换 inode → 容器读到
  旧值 → 全站 502,本项目真实踩过)与正确顺序(同 inode 覆写 → 断言容器看到 →
  nginx -t → reload)
- 「部署需要的全部自定义文件」清单表
- 「管理后台」补分组自由增删、历史分组保护、完整 API 表
- 「工作空间视图」补可内嵌性三态探测(转发 Cookie、WAF 403 误判)、被拒弹层、
  悬浮控件收边与"第一次交互只滑出"
2026-09-11 20:13:44 +08:00
26a0b15014 perf(web)+fix(admin): 撤掉卡片层模糊、补隐藏动画、后台窄屏适配、收边两段式
1) 卡顿:撤掉"重复且参与动画"的卡片层 backdrop-filter
   实测首页 37 个元素带模糊(24 张卡片 + 6 个分组容器 + 顶栏 + 悬浮件)。
   模糊要对背景逐帧重采样,而卡片又参与逐张淡入与悬停位移,叠加就是卡顿。
   现在只保留分组容器的玻璃感与悬浮件(搜索栏因宽/圆角/内边距一直在变也去掉)。
   实测:/ 从 37 → 12,/minimal 从 15 → 4。
   注意:选择器必须写成 html[data-theme] body a.item —— 主题的 customCss 由
   Dashy 运行时注入,同权重 + 同为 !important 时靠文档顺序决胜,写在 navi.css
   里的规则会输,必须提高特异性。

2) 隐藏/出现补动画
   ≤380px 让位隐藏按钮组:加上 opacity+位移过渡,并把 visibility 延迟到淡出
   结束再切(否则瞬间消失、看不到过渡)。弹层关闭先加 .closing 淡出再摘节点。
   降动效模式下不再用 transform:none 抹掉"收边"的位移,避免隐藏失效。

3) 工作空间收边改为"第一次交互只滑出"
   收起态点一下只滑出、不触发(否则贴边的按钮会被直接点中,比如想看搜索框
   结果把管理后台打开了);第二次点击才生效。判据是"元素是否真的露在视口内",
   所以鼠标悬停已展开时一次点击即生效。6 秒无操作或 Esc 自动收回。

4) 后台窄屏:分组行原本是内联 flex + 固定 max-width,窄屏既不能换行也不能
   收缩,输入框顶破卡片。改用类 + ≤560px 适配(竖排、整行占满、按钮右对齐、
   字号 16px 防 iOS 聚焦缩放)。实测 390px:scrollWidth == innerWidth、
   零横向溢出。
2026-09-11 20:13:44 +08:00
76a6a83e7d feat(web): 不支持内嵌时明确弹层;修掉被 promise 吞掉的 ReferenceError
1) 点不支持在工作区显示的应用 → 弹出提示卡片
   标题「该应用不支持在工作区显示」+ 域名 + 服务器返回的原因 +
   「在新标签页打开 / 知道了」两个按钮;面板里同时保留同样的说明。
   之前只在面板里显示一行字,用户点了之后缺少明确反馈。

2) 修 ReferenceError: openUrl is not defined(线上真实故障)
   setWorkspaceState 第 4 个参数早先从 openUrl 改名为 note 时,blocked
   分支漏改,于是每次被拒内嵌都在函数内部抛异常,异常沿 promise 链被吞,
   导致后面的弹层根本没有执行、面板提示也只渲染到一半(URL 行和按钮缺失)。
   教训:验收必须采集 Runtime.exceptionThrown,并且断言交互元素真的存在;
   只断言"状态=blocked + 提示文字前缀"会恰好掩盖这类错误。
2026-09-11 19:52:19 +08:00
0d9619e446 feat(admin): 导航分组可增 / 可改名 / 可删;初始状态无分组
后台此前只能往已有分组里加服务,分组本身是写死在 conf.yml 里的,
用户无法增删。现在新增「导航分组」卡片:

- POST   /admin/api/section         新增(重名 409、空名 400)
- POST   /admin/api/section/update  改名/换图标
- DELETE /admin/api/section         删除;非空分组必须 force=1(二次确认),
                                    避免误点一下连带删掉一组服务

「历史访问」分组由 history-server 依据浏览记录重建,改名/删除/手工新建
都会被它覆写回来,所以一律 409 并说明原因 —— 否则用户以为删掉了,
下次同步又"长"回来。

初始状态无分组:conf.example.yml 的 sections 改为空数组,后台在两个位置
给空态提示(「还没有任何分组」/「先创建分组」,并把"添加服务"置灰)。

补上原本缺失的一环:rebuildDashySections() 原先只更新已存在的
「历史访问」分组、从不创建它,新装用户永远看不到历史分组;现在缺了就补建
(完全空配置时不补,保持"初始无分组")。

测试覆盖:新增/重名/改历史分组/删历史分组/删非空(无 force 拒绝)/force 删除,
以及删除后的分组名列表断言。
2026-09-11 19:52:19 +08:00
f40e54287b fix(web): 极窄屏备案号不再顶到按钮;mini 叶签两端改为渐变淡出
1) 备案号与自定义按钮只剩 1px
   实测 320x640:备案号换行成两行、高 66px,占据距底 11..77;而搜索球/
   按钮组固定在 bottom:76px → 两者实测只剩 1px(视觉上已贴上)。
   修法:≤380px 时把按钮上移到 bottom:88px,并收紧页脚 gap/padding
   (两行高度仍 66px)→ 实测净间隙 11px。≥390px 时备案号是单行 39px,
   间隙本就充足,保持不变。

2) mini 顶部叶签被两侧按钮硬切
   结构是 [‹按钮 34px][滚动行][›按钮 34px],滚动行 overflow-x:auto
   (内容 858px / 可视 294px),卡片直接在滚动行边缘被切断,没有任何过渡。
   修法:给滚动行加 mask-image 两端各 24px 渐变淡出,并用 :has() 关联
   翻页按钮的 disabled 状态 —— 已经在尽头的一侧不再淡出(两端都到头则
   完全不遮罩),避免"到头了还在渐隐"的错觉。
   实测:最左时 mask 为「左侧实心 + 右侧渐隐」,滚到最右变为
   「左侧渐隐 + 右侧实心」。
2026-09-11 19:18:00 +08:00
8ba5e5117b feat(web): 工作空间内嵌网页 + 控件收边 + 页脚去重 + 窄屏不再重叠
1) 工作空间真的在中间打开网页
   Dashy 的面板就是 iframe,但它的 launchApp() 只在 target 不是 newtab 时
   才把 URL 灌进 iframe;而且侧栏项目大多没有 per-item target,Dashy 走的是
   Vue 直接改 iframe.src 的路径 —— 根本不经过 window.open,导致判定逻辑被
   绕过(实测:Gitea 仍被内嵌、空白)。现在两条路径统一进 verifyAndShow():
   先探测再决定内嵌还是明确提示;被拒时还会清空 iframe 的 src。

2) 控件在工作空间收边
   中间嵌的是别人的应用,常驻控件会遮挡。搜索球收左边缘、右下角两个按钮
   组成 dock 整组收右边缘、备案号收下边缘,各露 14px(备案号 13px),
   鼠标移入或点击、聚焦时滑出展开。只在工作空间生效。

3) 修 iframe 撑高页面 77px
   Dashy 的 .web-content iframe 是 position:absolute,包含块是 #dashy 而非
   .web-content,外层 overflow:hidden 拦不住它,它按 #dashy 高度撑到 814px
   (164+814=977 正好等于页面 scrollHeight)。补 position:static 后归零。

4) 首页不再出现两个备案号
   Vue 异步挂载,navi.js 可能先补了一份页脚,之后 Dashy 的页脚才出现;
   原来的守卫只判断 .navi-footer 是否存在,不会回收自己早先注入的那份。
   新增 dashyFooterNode() 区分两者,发现 Dashy 的就把我们补的移除。

5) 窄屏展开搜索框不再压住右下按钮组
   手机端展开宽度是 calc(100% - 86px),而按钮组占最后 114px → 重叠 38px。
   改为按按钮组真实占位预留(≥600px: min(560px, 100% - 158px);
   ≤599px: calc(100% - 132px));≤380px 时输入框会窄到不可用,改为
   临时隐藏按钮组(用 visibility 而非 transform,因为 prefers-reduced-motion
   会强制 transform:none)。
2026-09-11 19:04:38 +08:00
22c405d2a0 feat(admin): 新增可内嵌性三态探测 /admin/api/frameable
Dashy 的「工作空间」用 iframe 打开应用,被 X-Frame-Options /
CSP frame-ancestors 拒绝时浏览器只给一片空白,而且 JS 无法区分
「被拒绝」与「加载中」(onload 一样触发)。所以由服务端读响应头。

实测 24 个应用:19 个可内嵌;Gitea(含内网 IP) 与 docker.com 是
SAMEORIGIN,Dify 是 DENY。

三态而非布尔,因为探测本身会失真:
- conclusive=true  frameable=false → 有明确拒绝证据(XFO / frame-ancestors)
- conclusive=true  frameable=true  → 2xx 且无拒绝头
- conclusive=false                  → 探测被 WAF 拦成 403、或跳到了别的主机
  这类不给结论,前端照常尝试内嵌,但在工具条里说明原因并留出新标签页退路。

两个关键细节:
1) 转发浏览器自己的 Cookie。门户后面的应用对未登录请求会 302 到登录页
   (登录页没有 X-Frame-Options),不转发会话就会把「重定向到登录页」
   误判成「可内嵌」——这是线上真实发生过的误判。
2) 用普通浏览器 UA。自造的 "HomeDash embed check" UA 会被 WAF 当机器人拦成 403。

测试覆盖:无头/有 XFO/有 CSP frame-ancestors/WAF 403/带与不带 Cookie。
2026-09-11 19:04:38 +08:00
da196abcb5 feat(web): 隐藏原生设置、圆形视图切换、工作空间改列表、三视图标题统一
用户提出的四条,都基于实测数据而不是猜测:

1) 隐藏 Dashy 原生设置入口
   顶栏的 .config-launcher(齿轮+铅笔+调色板)与我们的 /admin/ 重复,
   连同 .options-trigger/.settings-outer 一起隐藏。

2) 视图切换图标改成圆形,紧贴自定义入口左侧
   原生切换按钮是侧栏底部的方形图标(.switch-view-buttons),已隐藏;
   改为右下角圆形悬浮按钮 .navi-view-switch,点开是「首页/极简/工作空间」
   菜单并标出当前视图。
   顺带修掉 2px 错位:管理入口是内容盒(48+2px 边框=50),视图按钮是
   边框盒(48),并排时差 2px;给管理入口补 box-sizing: border-box。

3) 工作空间「点了没反应」
   读 Dashy 自己的 Workspace 组件(assets/Workspace-*.js)得到结论:
   右侧面板就是一个 iframe,launchApp() 只有 target 不是 newtab 才把 URL
   灌进面板;我们全局 openingMethod: newtab,所以每次点击都走 window.open
   分支,this.url 恒为空 → 面板永远空白。
   就算灌进去也没用:Gitea 实测 X-Frame-Options: sameorigin 拒绝被嵌
   (Refused to display … in a frame)。
   所以改成:点左侧分组 → 右侧列出该分组应用卡片(数据取自
   /admin/api/sections),不依赖目标站点是否允许 iframe,首页仍保持新标签页。

4) 三视图标题割裂
   实测 /minimal 是 48px 居中 -1.92px,而 / 与 /workspace 是顶栏里 40px
   左对齐、无字距。现在三个视图共用同一条规则(clamp(1.75rem,4vw,2.75rem)、
   700、-0.035em、居中;移动端 clamp(1.6rem,8vw,2.2rem)),副标题也居中置灰。

另外:工作空间面板原本用 position: fixed 钉在视口中间(1364×654,上距顶栏
7px、下距搜索栏 8px),把页面切成「顶栏/大方块/底部」三段;改为正常文档流,
内容多时页面自然滚动。
2026-09-11 18:17:23 +08:00
c04d00d4de feat(admin): 历史记录可在后台逐条删除/清空
此前后台**没有任何历史管理入口**,只有一条需要 admin token 的
DELETE /api/history/(清空全部),所以用户根本无法删除记录。

- history-server: 新增 DELETE /item?url=<url> 删除单条并重建分组;
  CORS 允许 Authorization 头
- admin-server: 新增 /admin/api/history(GET 列表 / DELETE 清空)与
  /admin/api/history/item(DELETE 单条),服务端带 token 转发给
  history-server —— 令牌只在容器内部使用,浏览器侧无需保存
- admin UI: 「历史访问」卡片,列出记录 + 逐条 ✕ + 清空全部(均二次确认)
  标题为空或本身是 URL 时显示简洁域名
2026-09-11 17:03:33 +08:00