个人网站的管理后台前端:在浏览器里查看服务器、管理博客、连接终端。
侧栏共 8 个页面,面板常驻挂载、只切换显隐;页面由地址栏决定(/system、/tools/bitwarden 等),刷新、前进后退与收藏都保持在同一页。
- 系统:CPU / 内存 / 磁盘 / 网络的实时读数与历史趋势;实时走流式采样,趋势可按秒 / 分 / 时 / 天切换粒度
- 博客:文章与合集管理,走
/api/blog/admin/*;正文用 CodeMirror 6 编辑,文章 / 合集以雪花public_id标识,副标题(subtitle)与摘要(excerpt)是相互独立的字段 - 管理:登录设备会话、接口令牌(明文仅在生成时展示一次)、重置验证器
- SSH 公钥:管理可登录服务器的公钥白名单,停用 / 删除在下一次连接立即生效
- 终端:浏览器内连服务器终端(ttyd + tmux),多标签;站点登录之外再验一次终端口令,票据随 iframe 传给服务端 wrapper 校验
- Hermes:以 iframe 嵌入构建期注入的地址
- 数据库:以 iframe 同源嵌入 DBGate(
/db/),打开即用 - 工具:
/tools是导航列表;Bitwarden 解锁在独立的/tools/bitwarden页(可直接打开、刷新、收藏;主密码换 session,供服务器上的 bw CLI 与 Hermes 使用,密码不保存)
Ctrl/⌘ + K 打开命令面板,可切页、重置验证器、退出登录。窄屏(<1024px)侧栏收为左侧滑出抽屉,顶部菜单按钮常驻。
npm install
npm run dev # Vite 开发服务器(/api 代理到 127.0.0.1:3100)
npm run build # 构建,产物写入 vite.config.ts 的 build.outDir(默认生产目录,可用 BUILD_OUT_DIR 覆盖)
npm run preview
npm run check # typecheck + lint + lint:css + check:tokens + teste2e/ 有独立的 package.json(Playwright),根目录 npm install 不会安装它。
构建期环境变量,真实值只写在本地 .env(已 gitignore),仓库只留 .env.example 占位:
| 名称 | 默认值 | 说明 |
|---|---|---|
VITE_HERMES_DASHBOARD_URL |
空;回退 https://hermes.example.com |
侧栏「Hermes」页 iframe 地址 |
VITE_SITE_URL |
空;回退 https://site.example.com |
公共站点基址(含协议、末尾无斜杠),博客外链据此拼绝对地址 |
服务端环境变量 AUTH_MODE 决定认证模式(见下),前端启动时探测,构建时不读取。
- 构建产物是纯静态文件(目录由
vite.config.ts的build.outDir指定,当前为/var/www/admin;emptyOutDir: true会在构建时清空该目录),交给自己的 Web 服务器托管,无 Node 进程常驻。 - 同源接口
/api/*反代到后端;终端/term/反代到 ttyd。 - 环境变量在构建期注入,改动后需重新构建。
后台是可安装的 PWA:Chromium / Edge 显示安装入口,iOS Safari 可「添加到主屏幕」,安装后以 standalone 无地址栏显示。离线时显示应用外壳 + 明确提示,不提供离线写入;网络不可达时(fetch 直接失败)启动渲染「当前离线」页,收到 HTTP 4xx/5xx 则维持原有处理。
- 入口文件:
public/manifest.webmanifest、public/sw.js、public/offline.html、public/icons/*.png;index.html里挂manifest与apple-touch-icon。 - 缓存策略(在
sw.js内按请求类型判定,不看响应头):- 导航请求 network-first;失败回退同路径缓存,后台入口(
/、/index.html)再回退缓存的index.html;其他路径回退offline.html。 /assets/*(Vite 带 hash)cache-first。/api/*、/_auth/*、/term/*network-only(不缓存、不拦截;认证相关路径永不缓存)。- 其余同源静态资源(字体、图片)stale-while-revalidate。
- 跨域请求不接管。
- 安装时只预缓存
offline.html+ 图标等极小清单,其余按需缓存。 - 写缓存前有响应头守卫:带
Set-Cookie,或Cache-Control含no-store/private的响应一律不写入(导航与静态两个分支共用同一守卫)。
- 导航请求 network-first;失败回退同路径缓存,后台入口(
- 更新机制:SW 内
skipWaiting()+clients.claim()自动接管;注册侧用updateViaCache: 'none'绕过 HTTP 缓存检查sw.js。新版本接管时页面弹出克制提示「有新版本,刷新即可更新」,由用户点刷新切换(不自动刷新,避免丢弃未保存的编辑)。缓存名带版本常量,activate时删除所有旧版本缓存。 sw.js部署注意:不得被长缓存(不要给/sw.js设Cache-Control: max-age很大的头,建议no-cache),否则浏览器发现不了新版本;/sw.js、/manifest.webmanifest、/offline.html应可被同源直接访问。- 重新生成图标:
python3 scripts/gen-pwa-icons.py(需 Python PIL / Pillow)。纯几何菱形,配色暖纸白底 / 墨黑 / 琥珀点缀,幂等可重跑,产物在public/icons/。
前端身份由同源会话 cookie 证明,不读写 localStorage token;src/api.ts 是唯一鉴权入口,业务组件不另写一套鉴权逻辑。
启动时探测 GET /api/admin/auth-mode 并缓存,按模式分流;探测失败(网络错误 / 非 JSON / 旧后端 404)一律按 sso,不回退 builtin。
| 模式 | 行为 |
|---|---|
builtin(默认) |
自带账号:本地登录页 + POST /api/admin/login(6 位 TOTP 动态码);GET /api/admin/me 401 显示登录页;退出 POST /api/admin/logout;任意接口 401 回登录页 |
sso |
关掉自带口令:身份由前置认证层注入;GET /_auth/me;任意接口 401 整页跳 /_auth/login?next=…;退出跳 /_auth/logout |
验证器重置分两阶段:POST /api/admin/totp/reset 生成 pending secret,再用 POST /api/admin/totp/confirm 提交新码转正。
深浅色默认跟随系统,可手动切换,选择记在 localStorage.admin_theme。样式为手写 CSS,设计令牌与 atrium 同一套(暖纸 / 墨色 / 单一琥珀点缀),Inter Tight / Inter / JetBrains Mono 字体自托管于 src/fonts/。
MIT