Commit Graph

22 Commits

Author SHA1 Message Date
857ec074aa fix(history): 鉴权改为 fail-closed,未配置 token 时拒绝一切请求
审计发现的方式与 admin-server 同源:requireToken 在 expected 为空时
直接 return true,等于「没配 NAVI_SYNC_TOKEN / NAVI_ADMIN_TOKEN 就没有
任何鉴权」。nginx 的 location /api/history/ 没有 auth_request,线上因此
可匿名读取全量浏览历史、DELETE 清空、逐条删除。

改法与 admin-server 的 requireAdminToken 保持同一套语义(不另起风格):
- 配了 token → 必须带对的 Bearer token
- 未配 token → 401 + JSON {"error":...},并在日志打出
  「未配置,已按 fail-closed 拒绝请求」的可诊断信息(只报事实,不打印 token 值,
  且按 key 去重避免刷日志)
- 只有显式 NAVI_HISTORY_ALLOW_UNAUTHENTICATED=1(1/true/yes/on)才放行,
  专用于本机调试,并会打警告日志

顺带收掉同一文件里等价的鉴权短路:新增 requireAdminToken 统一管理类入口
(DELETE / 清空、/item 逐条删除、/rebuild 重建)。它不再把「adminToken 为空」
当成免校验的短路:adminToken 优先,未配置但配了 syncToken 时接受 syncToken
(兼容旧部署,插件本来就用它写入),两者都没有则回到 requireToken 的
fail-closed 分支。

测试(history-server/auth_test.go)覆盖:
- 未配置 token → 401 且不写入任何数据,诊断日志含「未配置/拒绝」且不含 token 值
- 配置了 token:无头/错误 token/缺 Bearer 前缀 → 401;正确 token → 放行
- requireAdminToken 三种配置组合的判定顺序
- 显式放开开关可用但有警告
- 源码静态护栏:不得残留「未配置 token 则不校验」
反向验证过:把 requireToken 改回 fail-open 后,上述用例全部 FAIL。
2026-10-22 23:08:32 +08:00
292e77097d fix(history): 访问计数按设备去重 + 后台返回全量
- Entry 增加 DeviceVisits:同一 URL 多次/多设备上报取各设备最大值再求和,
  不再平方级放大(30 次访问曾被计成 465 次)
- handleGet 不再截断 100 条,返回 {items,total},后台显示当前条数
- 清理死代码(hostVisits/hostOf/visitCount/isCodeHostVal/dashySection)
- 守门测试:重复上报、多设备求和、陈旧数据、持久化、统计、门户文案、第 101 条可见性
2026-10-02 11:24:55 +08:00
b256a8d7f1 fix(favicon): 失败重试间隔不再挡住"缓存文件丢失后的重抓"
实测:删掉 item-icons 里三个被污染的图标后,rebuild 不再重抓,图标永久缺失
(只有重启容器才恢复)。根因:faviconWorker 在**每次**尝试后都写 faviconAttempted,
而 queueFavicon 会用 faviconRetryGap 挡住"近期尝试过"的 host —— 于是
cachedFavicon() 为空(文件确实不存在)也照样被跳过。

修法:成功时 delete(faviconAttempted, hostport),只给失败保留节流记录。
这样缓存文件被手工清理或数据卷恢复后,下一次 rebuild 就能立即重抓。
2026-09-13 09:02:50 +08:00
3d1ed788f3 fix(gateway/favicon): 被代理域名的 /favicon.ico 被门户图标顶掉(图标全错的真因)
用户问"同时被路由的网站为什么图标不正确"。实测三处证据:
  ① gateway.conf 里**每个** server 块都有
       location = /favicon.ico { auth_request off; try_files $uri =404; }
     而 try_files 在门户自己的 root(/app/portal) 里找文件;
  ② 于是 https://pi|ittools|vikunja.jianfgit.xyz/favicon.ico 三者返回**同一个**
     门户图标(md5 80498a88…),而它们内部上游的真实图标各不相同
     (ittools dc36588f… / vikunja 6d9507b6… / pi cf3332ac…);
  ③ 导航图标抓取优先抓 /favicon.ico → pi/ittools/vikunja 存下了同一张 1091B 门户图标。

修法两处:
- admin-server/gateway.go:门户 favicon 的两个 location 只在"门户自己渲染"的
  server 块(kind=portal/auth、默认 404 块)里输出;代理到上游的块不再输出,
  让 /favicon.ico 正常走 location / 落到上游。新增断言测试。
- history-server/favicon.go:抓取改为**优先走内部上游**(读 gateway.yml 把对外域名
  映射到内部地址,带 Host 头、容忍自签证书),失败才回落对外地址 —— 这样受统一登录
  保护的站点也能拿到真实图标,且不再依赖门户的 favicon 行为。
2026-09-13 08:48:42 +08:00
bcc05bd352 feat(nav): 历史访问不再排除站内服务(用户:尽最大可能显示)
用户要求:历史访问应尽最大可能显示,不要限制条目;并确认取消"排除已是导航项的站点"。

此前该组会跳过所有已是导航项的 host(避免与其它分组重复),副作用是**站内服务的真实
访问记录永远进不来**,这一组长期只有 1 条(外加插件安装入口)。现改为:整份浏览器历史
全部收录,按访问次数降序;首条仍是插件安装入口。条数上限此前已移除。

注:常用服务的候选集本就排除「历史访问」分组自身,所以站内服务进了历史访问后
不会反过来污染常用服务的计数与排序。
2026-09-13 08:36:07 +08:00
305eb27acc feat(nav): 「常用服务」与「历史访问」不再限制条目数
用户要求:这两个聚合分组应当尽最大可能显示,而不应限制条数。

- 常用服务:原先截断为 favTopN=5,现改为展示**所有被点击过**(visits>0)的导航项,
  仍按访问频次降序、同频次保持原序、按 host 去重。0 次点击的不算"常用",
  不塞进来(否则等同于把所有分组复制一份)。
- 历史访问:原先截断为 20 条(histItems >= 21 即 break),现改为全部非导航历史主机。
  首条仍是插件安装入口,排序仍按访问次数降序。
- 注释同步更新(rebuildDashySections 的 4 条说明与容量提示)。

数据实况(改动前):nav-visits.json 有 10 个 host 有计数,其中 8 个是导航项
→ 常用服务从 5 项变 8 项;history.json 去重主机仅 1 个 → 历史访问上限本就未触顶。
2026-09-13 08:29:57 +08:00
b980ce6292 fix(build): 测试文件 import 改为块形式(os/filepath/testing)
main_test.go 原为单行 import "testing",补 import 的替换锚点没命中,
导致 os/filepath 未导入、history-server 编译失败(承接上一提交的坏状态)。
本次改为块形式导入,go vet 与 go test 均通过。
2026-09-12 12:42:08 +08:00
6c34970f38 feat(nav): 导航项点击频次独立统计,常用服务据此取 top-5
用户指出:导航项的访问频率应当单独统计。此前「常用服务」的排序依据是浏览器
历史的 host 访问次数(把站外浏览混进来反推),并不等于"我常点哪个服务"。

- history-server 新增 POST /api/history/nav-visit(内部 /nav-visit):
  按 host 计数并落盘 data 卷的 nav-visits.json(临时文件 + rename 原子写),
  随后立即 rebuild,使「常用服务」按新计数重排
- rebuildDashySections:候选访问量改为只认 nav-visits 计数,不再使用浏览器历史;
  无任何点击数据时保持既有分组内容不动(不空转、不误清)
- web/navi.js:点击 http(s) 导航项时用 sendBeacon 异步上报(不阻塞跳转,失败静默)
- 测试:计数递增、落盘、文件生成
2026-09-12 12:41:24 +08:00
a9845f1fba feat(nav): 常用服务改为纯访问频次 top-5,取消 Gitea 置顶
用户要求:不要固定置顶 Gitea,且每个服务都应归到准确描述其功能的分组里。

- history-server/main.go:删除「强制 Gitea/代码托管置顶」逻辑(含 isCodeHostVal
  参与的首位组装),改为按访问频次取前 5;顶部说明同步更正
- 文档:README / AGENTS.md 里"(Gitea 置顶)"的描述改为"不置顶任何服务"
- 现场分组调整:新建「代码托管」分组并把 Gitea 归入(此前它只存在于
  系统管理的 常用服务 里,等于没有功能归属)

go vet / go test 通过。
2026-09-12 12:27:35 +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
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
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
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
24b2e6ab75 fix(history): 历史条目不再显示裸 URL
浏览器没给标题(或标题本身就是 URL)时,改为显示去掉 www. 的域名,
例如 https://www.docker.com/ -> docker.com。
2026-09-11 16:25:26 +08:00
0a6199b240 test(history): cover favicon/ICO logic; base format detection on body, not Content-Type
新增 history-server/favicon_test.go(仓库此前没有任何测试),覆盖:
- Dashy 渲染白名单(只认 svg/png/jpg,jpeg 归一为 jpg)
- sniffExt 必须拒绝 SPA 的 HTML 兜底页
- ICO 内嵌 PNG 提取(取最大条目)
- DIB(BMP) 解码:尺寸、像素顺序(自下而上)、全 0 alpha 视为不透明、
  不支持的位深/过短数据要报错
- 从多条目 ICO 中挑最大者
- host:port 文件名净化、中英文段落名匹配

写测试时当场抓到实现里的一个真漏洞:判定完全信任 Content-Type,
于是服务端谎报 image/svg+xml 而正文是 HTML 时会被存成 .svg。
现改为以**正文特征**为准(PNG/JPEG/ICO 魔数 + SVG 根元素校验),
Content-Type 只用于排除明显不是图片的类型,避免误存。

实测回归:google/github/docker/HN/wikipedia 及内网服务 6/6 拿到真实图标。
2026-09-11 10:57:30 +08:00
e7ea091e19 fix(history): give history entries real site favicons
历史条目的图标原先写死 fas fa-globe,整栏都是地球图标。

- 新增 history-server/favicon.go:后台抓取每个 host 的 favicon 并缓存到
  data/item-icons/,命中后 conf.yml 写本地文件名,与手工图标同一套解析。
  抓取顺序 <host>/favicon.ico → 首页 <link rel=icon>;3 个 worker,
  失败 6 小时后再试;完成后合并触发一次 conf.yml 重建。
- 只缓存 Dashy 能渲染的格式:其 isImage() 白名单是 [svg,png,jpg],
  写 .ico 进去完全渲染不出来。因此 .ico 先抽内嵌 PNG,若是传统 BMP/DIB
  编码则解码后转 PNG(Google 这类站点只给老式 .ico)。
- 内网地址用 host:port 区分(不同端口的服务不再共用一个图标),且只直连;
  外网站点直连失败时自动复用 SearXNG outgoing.proxies 的代理,
  也可用 NAVI_FAVICON_PROXY 覆盖。
- 修掉一个真实缺陷:导航项都没有访问数据时,原来会提前 return 并把
  「历史访问」清成只剩安装入口 —— 只浏览站外站点时真实历史会整个消失。
  现在只冻结「常用服务」,历史照常生成。
- Dockerfile 去掉 # syntax=docker/dockerfile:1:该指令会让 BuildKit 去 pull
  docker/dockerfile 前端镜像,无外网环境直接构建失败;本文件未用 BuildKit 语法。

实测:6/6 站点(含 google.com 的 DIB 型 ico、github.com、docker.com、
news.ycombinator.com 与两个内网服务)均得到真实图标,页面 naturalWidth>0。
2026-09-11 10:55:00 +08:00
ff0e6418f8 feat(nav): auto-detect browser extension and offer one-click install
导航页现在会自动检测浏览历史插件是否已安装:

- 扩展在用户配置的服务地址上动态注册 content script
  (Chrome: chrome.scripting.registerContentScripts,
   Firefox: browser.contentScripts.register),
  于 document_start 给 <html> 打上 data-navi-ext 标记,
  不在其他站点运行。
- history-server 始终把「安装浏览历史插件」放在「历史访问」第一条
  (入口 url=/extension/install.html),启动时也会重建一次,
  不再需要等下一次同步。
- web/navi.js 读到标记后隐藏该入口(连 .item-wrapper 一起隐藏,
  避免留下空网格位)。
- 新增 /extension/install.html:自动识别浏览器,给出对应安装包与步骤;
  支持在 data 卷放 extension-store.json 配置商店链接,
  有商店链接时显示真正的「一键安装」。
- 段落名匹配同时兼容中英文(常用服务/Favorites、历史访问/History),
  修掉了公开示例 conf.yml 用英文名时重建逻辑完全失效的问题。
- 扩展升到 1.1.0(新增 scripting 权限与 detect.js)。

Note: Chrome/Edge 不允许从网站直接安装非商店扩展,
因此自托管场景下只能下载后手动加载;发布到商店后才会出现一键安装。
2026-09-11 10:41:10 +08:00
0fe13fc90b feat: unified nav portal - Dashy + SearXNG + history sync + admin UI + browser extensions
- Single Docker container with nginx reverse proxy
- SearXNG search proxy with configurable proxy support
- Browser extensions (Chrome MV3 + Firefox MV2) with SSE-style history sync
- Admin WebUI for managing navigation items
- Full deployment tutorial including auth reverse proxy and webui4frpc tunnel
- Sanitized conf.yml for public distribution
- Fixed SearXNG search trailing-slash issue via nginx rewrite
- Fixed SearXNG proxy configuration for China network access
- Fixed SearXNG engine loading (removed broken keep_only, use default_settings)
2026-09-09 09:52:33 +08:00
4f1c65a616 fix: keep manual favorites when no nav visit data; external-only visits don't trigger reorder
- Favorites section only rebuilds from Top-4 by visit frequency once real
  navigation hosts have visit data; otherwise manual Gitea/HomeAgent/AgentMail
  items are preserved untouched
- Pure external-site history (wikipedia etc.) no longer causes favorites to
  borrow tiles from other sections
2026-09-08 22:53:22 +08:00
c8aa6946c1 feat: favorites section shows top-4 Dashy nav items by real visit frequency
- Rebuild favorites from ALL Dashy navigation items (across every section),
  ranked by aggregated host visit counts from browser history
- Code-hosting (Gitea/GitLab/git.*) always pinned first regardless of visits
- Host-level dedup prevents duplicate tiles from previous rebuild cycles
- Fix code-host detection false positives (git. substring in domains like
  example.com); precise gitea/github/gitlab/codeberg matching + unit test
- History section now only lists non-navigation external sites
2026-09-08 22:50:08 +08:00
f0b318be45 feat: complete browser extensions + token auth for history/admin APIs
- Extensions: rewrite both Chrome MV3 & Firefox MV2 with reliable
  chrome.history.search incremental pull (not onVisited), local search-engine
  filtering, URL dedup, 30s alarm, retry queue, Bearer token header
- Add icons (16/48/96/128) for both browsers
- Manifest fixes: chrome action popup, firefox options_ui + browser shim
- history-server: require NAVI_SYNC_TOKEN for POST /sync & GET history,
  NAVI_ADMIN_TOKEN for DELETE; fix RWMutex deadlock (unlock before
  sortDashyFavorites) that stalled syncs with valid token
- admin-server: require NAVI_ADMIN_TOKEN for item add/delete writes
2026-09-08 22:35:54 +08:00
7e49d2b51c feat: HomeDash - unified self-hosted navigation portal
Single-container navigation service combining:
- Dashy dashboard + SearXNG metasearch + history sync API + admin WebUI
- Go services for history/admin APIs, nginx as unified entry
- Browser extensions (Chrome MV3 / Firefox MV2) with 30s batched sync
- Production guide: portal-nginx unified auth reverse proxy + webui4frpc/frp tunnel
- Fully sanitized (no internal IPs/domains/credentials)
2026-09-08 22:25:00 +08:00