Commit Graph

226 Commits

Author SHA1 Message Date
qinglong 557135c376 security: 配置文件模板化 — base_config.yaml 不再入库
- base_config.yaml → .gitignore (含真实密码不入库)
- 新增 base_config.yaml.example 模板 (password: CHANGE_ME)
- InitService: 检测缺失时自动从模板复制
- 首次启动自动创建配置文件

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 15:20:14 +08:00
qinglong a2361cf452 fix: Sentinel WebUI — 合并为单页面(仪表盘+节点管理Tab)
插件 Web 页面 key 必须匹配 plugin_name (/plugin/sentinel)
改为单页面 Tab 切换: 仪表盘 + 节点管理

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 15:17:17 +08:00
qinglong 634496d975 security: Sentinel 路由鉴权 + API Key CSRF + SSRF 防护
修复:
- Sentinel 路由 require_auth=False → True (严重: 未认证可访问)
- API Key 创建/删除加 csrf_protect=True
- SSRF 防护: 测试连接仅允许 http/https scheme
- 插件网络鉴权: 已加载插件自动放行 (不再要求 plugin.network.access)
- Sentinel 权限列表补充 plugin.network.access

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 15:13:56 +08:00
qinglong 5e74b0aad7 feat: 🛡️ Sentinel 集群监控插件 + API Key 系统 + 框架设置页
== API Key 系统 ==
- services/web_panel/routes/apikeys.py: CRUD + 权限模板 + 过期控制
- panel_auth + _check_plugin_auth 双重验证 (Session → API Key)
- 模板: readonly(只读) / monitor(监控) / full(管理)
- 持久化到 SenSuDB + 过期自动清理

== WebUI 框架设置页 ==
- static/web_panel/pages/settings.html: API Key 管理界面
- 创建对话框 (名称/权限/TTL), 列表表格, 一键删除

== 热重载修复 ==
- PluginService._reload_plugin: 路由器冻结时跳过 (避免破坏已注册路由)

== 🛡️ Sentinel 插件 ==
- 多节点 SenSu 集群性能监控
- 后台 urllib+run_in_executor 轮询 (避免事件循环死锁)
- SSE 实时推送仪表盘
- 节点管理页 (增删改+连接测试+备注)
- 路径: /sentinel/api/nodes

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 15:05:38 +08:00
qinglong 66ad51002c feat: WebUI 框架设置页面 + API Key 管理 UI
新增:
- static/web_panel/pages/settings.html — API Key CRUD 界面
- 侧边栏 '框架设置' 导航项 (齿轮图标)
- BUILTIN_PAGES 增加 'settings'
- UI 功能: 创建对话框(名称/权限/过期), 列表表格, 一键删除

权限模板:
- readonly — framework.status.read + plugin.info.read
- monitor — + framework.event.subscribe
- full — admin (全部)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 14:28:59 +08:00
qinglong b92b1cc4fc security: API Key 权限范围 + 过期时间 + 用量追踪
增强:
- 权限模板: readonly / monitor / full
- 每 key 独立权限列表, 注入 user['perms']
- 可选过期时间 (ttl 参数)
- 过期 key 自动清理 (加载时 + 验证时)
- request_count 用量追踪
- 标记 is_api_key = True

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 14:27:07 +08:00
qinglong 9f6eb29820 feat: API Key 系统 — CRUD + panel_auth 集成 + 持久化
新增:
- services/web_panel/routes/apikeys.py — 创建/列表/删除 API Key
- panel_auth() 双重验证: Session Store → API Key fallback
- _check_plugin_auth() 支持 API Key
- API Key 持久化到 SenSuDB (config_kv 表)
- 格式: sk- + 48 hex chars
- 脱敏显示 (前8后4)
- 删除后立即失效 (401)

WebPanelManager 注册 apikeys 路由

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 14:25:55 +08:00
qinglong e88d472c63 docs: API 认证与数据访问全流程文档 2026-06-13 14:14:34 +08:00
qinglong 2da52e80d0 security: P3+ — 面板密码哈希 + 上传类型白名单 + CSRF Token
面板密码安全:
- 支持哈希存储 (password_hash/password_salt)
- 登录使用 secrets.compare_digest 时序安全比较
- 回退明文兼容旧配置
- 默认密码 admin 时打印 CRITICAL 警告

文件上传安全:
- 拒绝危险扩展名: .exe/.dll/.so/.sh/.bat/.ps1 等
- 拒绝敏感文件名: .htaccess/Makefile/Dockerfile 等
- 上传时检查并返回 403

CSRF 防护:
- panel_auth(csrf_protect=True) 参数
- 写操作需 X-CSRF-Token header 匹配 session token
- 保护范围: 文件删除/写入/创建/上传/重命名 + 项目启停 + 代理管理

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 14:07:38 +08:00
qinglong d835b66ca9 chore: gitignore data/ 目录 (运行时数据,不应入库) 2026-06-13 13:57:12 +08:00
qinglong ff02b2e16d security: P3 — 插件隔离默认开启 + SenSuDB 注册 + 审计日志
- plugins.isolation 默认 true (生产环境进程隔离)
- main.py 注册 SenSuDB 服务
- WebPanelManager 注入 sensu_db 到 app context
- 文件删除/写入操作写入审计日志 (audit_log 表)
- 修复 Session 持久化所需的数据库依赖

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 13:56:58 +08:00
qinglong c1d2767f52 fix: web 命令端点 execute_command → process_command (方法名错误)
Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 13:54:37 +08:00
qinglong fcac02d920 security: P2 生产深度加固 — 路径/鉴权/脱敏/校验/持久化/过期
4.3 文件管理器路径收紧:
- 默认移除 Path('/') 全文件系统访问
- 仅允许项目目录 + data/ + 环境变量 SENSU_FILE_ROOTS 指定路径

3.4 插件路由鉴权修复:
- _check_plugin_auth 增加 panel_token 用户身份验证
- 先验证用户登录, 再检查插件权限

4.2 错误脱敏:
- security middleware 捕获异常 → 通用 'Internal server error'
- 堆栈详情仅写入日志, 不暴露给客户端

4.4 命令参数校验:
- POST /api/command 拒绝 shell 元字符 (;&|`$(){}!#~<>)
- 防止命令注入

4.5 Session 持久化:
- 登录/退出时保存到 SenSuDB.config_kv
- 框架重启后自动恢复已持久化会话

4.6 Token 过期: 24h → 2h

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 13:50:51 +08:00
qinglong 2f9063d6d4 fix: 登录频率限制 bug — lock_until=0 导致计数被清零
根因: 初始 lock_until=0 时 now >= 0 + 60 恒为真, 每次请求都清除计数
修复: lock_until 改用 None 表示未锁定, 仅非 None 时做过期判断

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 13:46:29 +08:00
qinglong a75b9d0975 security: 前端 WebSocket 连接传递 panel_token 认证
- dashboard.js / logs.js: WS URL 追加 ?token= 查询参数
- app.js: 添加 getCookie() 工具函数

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 13:38:38 +08:00
qinglong f7b4908322 security: 公网生产环境加固 — P0/P1 全部修复
API 认证 (16 个未保护端点 → 全部加 panel_auth):
- 文件管理器: 11 端点 (list/mkdir/delete/upload/download/read/write/...)
- 项目管理: 6 端点 (list/run/stop/logs/stdin/page), 修复硬编码路径前缀
- 代理管理: 3 端点 (list/add/remove)
- 系统状态: 2 HTTP + 1 WS (token 校验)
- 插件页面: 3 端点 (page/sse/event)
- 内部路由: 3 端点 (plugins/commands/data)

认证系统加固:
- 密码哈希: 固定盐 → 每用户独立 secrets.token_hex(16) 随机盐
- 默认密码警告: 启动时检测并打印 critical 级别日志
- 登录频率限制: 5 次失败 / IP → 锁定 60 秒, 返回 429

基础设施:
- 安全响应头: X-Content-Type-Options/X-Frame-Options/X-XSS-Protection/Referrer-Policy
- WebSocket 鉴权: query string token 校验

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 13:37:14 +08:00
qinglong 2dc3c16787 fix: 系统监控改用 asyncio task 驱动,抗 Android 后台挂起
- SystemMonitor/PluginStatusPanel: 移除 set_interval, 改用外部 refresh()
- TuiService._refresh_monitors(): asyncio.sleep(2) 循环刷新
- asyncio.sleep 在 Android 恢复前台后能正常续跑
- shutdown 时 cancel refresh task

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 12:50:06 +08:00
qinglong 10d1a1fcb1 fix: 标题栏 背景改 + 行高加到2 增强可见性 2026-06-13 12:18:50 +08:00
qinglong feca81842b fix: 回到 grid 布局 + 顶部留空行 — 5行: 空/标题/状态栏/主区/输入
grid-rows: 1 1 auto 1fr 3
- Row 1: 空白行
- Row 2: 标题栏 (Static, bold)
- Row 3: 状态栏 (auto 高度)
- Row 4: 主显示区 (1fr 填满剩余)
- Row 5: 输入栏 (固定3行)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 12:17:30 +08:00
qinglong ae8586bc4a fix: 用 Static 标题栏替代 Textual Header,确保始终显示
- 自定义 #title-bar (dock top, height 1, bold)
- 移除不可靠的 Header widget
- _setup_title 更新 title_bar 内容

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 12:00:36 +08:00
qinglong 18f9fcfcd3 fix: Header dock top 防止被 monitor-area 遮挡 2026-06-13 11:56:58 +08:00
qinglong f7117825ae fix: 日志区改用 dock 布局自动占满剩余空间
- monitor-area: dock top
- input-area: dock bottom, height 3
- main-area: 自动填充中间所有空白
- 去掉 grid 布局避免高度塌缩

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 11:54:27 +08:00
qinglong 7caf35281b fix: 移除输入区外部边框 2026-06-13 11:52:00 +08:00
qinglong 74fd337a4c fix: 输入框高度 5→3 + 主显示区 min-height 防塌缩 2026-06-13 11:49:57 +08:00
qinglong d88438be3b style: Header 与状态栏之间加一行间距 (padding-top: 1) 2026-06-13 11:46:23 +08:00
qinglong 9517afabd9 fix: 输入框置底 — 移除冲突的 height 属性,用 align center bottom
grid-rows 已设置行高 (auto auto 1fr 5),CSS height 会与 grid 冲突
改为 align: center bottom 让 Input 贴底

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 11:46:11 +08:00
qinglong cbc2dd6b82 fix: TUI 布局调整 — 标题恢复 + 增加输入/状态栏高度
- 恢复 Header 标题栏
- 4 行布局: auto(Header) + auto(Monitor) + 1fr(Main) + 5(Input)
- 状态栏高度 2→3 行
- 输入栏高度 3→5 行

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 11:42:08 +08:00
qinglong 6382c8019d refactor: TUI 布局重构 — 经典三行:状态栏 + 主显示区 + 固定底部输入
布局改为 grid-rows: auto 1fr 3:
- Row 1 (auto): 系统监控 + 插件状态 紧凑状态栏
- Row 2 (1fr): 合并日志/消息为单一可滚动主显示区
- Row 3 (3 lines): 固定高度输入栏,始终可见

同时:
- 移除 Header/Footer 节省空间
- show_message 路由到 log_display
- 清理未使用的 message_display 分离逻辑
- 移除未使用的 reactive/Horizontal imports

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 11:34:45 +08:00
qinglong 7518f4fd17 fix: textual.suggestion 兼容老版 Textual + 升级最低版本约束
- textual.suggestion 在 Textual < 2.0 中不存在
- 添加 try/except fallback 构造兼容 Suggestion 类
- requirements.txt: textual>=0.40.0 → >=0.47.0

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 11:15:09 +08:00
qinglong 249ef5d91e docs: 更新 ROADMAP v0.7.0 — 反映全部已实现功能
v0.2.2-v0.5 所有里程碑已完成
版本号: Alpha 0.2.1 → 0.7.0

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 11:09:15 +08:00
qinglong fca2aa5a05 feat(v0.5.2): 插件索引仓库 + install 命令
PluginIndex 服务:
- 从远程 JSON 索引获取可用插件列表
- 支持 zip 包和单文件 .py 两种格式安装
- 自动解压/移动到 plugins/ 目录
- 默认索引地址: GitHub Pages

install 内置命令:
- install --list → 列出远程可用插件
- install <name> → 下载并安装
- 自动缓存索引,避免重复请求

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 11:08:47 +08:00
qinglong 7ce495de2a feat(v0.5.1): 插件进程隔离 — 集成到 PluginService
- PluginService._should_isolate() 检查全局配置 + 插件级配置
- _load_plugin_isolated() 使用 IsolatedPlugin (multiprocessing.Process)
- unload_plugin() 兼容隔离/非隔离两种实例
- 配置: plugins.isolation (全局默认) + settings.isolation (插件级)
- 插件崩溃不影响框架主进程

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 11:07:30 +08:00
qinglong 37d1110f2b feat(v0.4.3): TUI 命令 Tab 补全
CommandSuggester 实现 Textual Suggester API:
- 根据已注册命令列表提供前缀匹配补全
- 前缀无匹配时 fallback 到包含匹配
- 按长度排序,优先最短匹配
- 在 Input placeholder 提示 'Tab 补全'

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 11:06:31 +08:00
qinglong 2d0a37ba19 feat(v0.4): TUI 系统监控面板 + 插件状态面板
v0.4.1 系统监控面板:
- 复用共享 SystemInfoCollector (TUI + Web面板同一数据源)
- CPU/MEM 彩色进度条 + 网络实时速率
- 2 秒刷新,set_interval 驱动

v0.4.2 插件状态面板:
- 展示已加载插件的名称/版本/命令数
- 3 秒刷新,PluginService 延迟注入

SystemInfoCollector:
- 注册为共享服务 (sys_collector)
- Web panel status.py 优先从 service_manager 获取
- 零重复数据采集

TUI 布局升级: 3行 → 4行 (monitor + log + message + input)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 11:05:56 +08:00
qinglong f402af58d0 feat(v0.3.4): 插件命令 HTTP API 自动暴露
PluginNetworkBridge.register_command_routes():
- 自动扫描插件 @plugin_command/cmd_* 方法
- 每个命令 → POST /api/plugin/{cmd_name}
- JSON body: {"args": [...], "kwargs": {...}}
- 返回: {"ok": true, "result": "..."}

PluginService.load_plugin() 在插件初始化后自动调用

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 11:02:45 +08:00
qinglong d5388057d9 feat(v0.2.2): 插件热重载 — watchdog 监听 plugins/ 自动重载
- PluginService.start() 中启动 watchdog Observer
- 监听 plugins/ 下 .py/.yaml/.yml 文件变更
- 1秒防抖,避免重复触发
- 变更后自动卸载→重新加载对应插件
- 插件重载后自动重连网络路由
- PluginService.stop() 清理 observer
- 配置项 hot_reload: true/false 控制

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 11:01:00 +08:00
qinglong dcef1173f2 feat(v0.2.2): 调试服务器自启动 — InitService.start_auto_scripts()
通用自动启动脚本机制:
- base_config.yaml 新增 auto_start_scripts 配置段
- InitService 在日志服务就绪后扫描并拉起配置的脚本
- 脚本不存在时优雅跳过,不影响框架启动
- 每个脚本独立子进程 + 独立进程组 (os.setsid)
- 框架 shutdown 时自动终止 (SIGTERM → killpg)
- 模块级单例跟踪所有进程,避免重复启动

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-13 10:58:01 +08:00
qinglong 9de094ea54 fix: 移除遗留fmfuncs目录创建 + 清理init_service无用导入 + 文件管理器SVG图标 + 示例插件MD3改造
- init_service: 删除_load_fmfuncs方法和fmfuncs目录创建(已迁移到sdk/)
- init_service: 清理未使用的importlib.util和sys导入
- 文件管理器: 全部emoji替换为MD3 SVG矢量图标(文件夹/文件类型/右键菜单)
- 示例插件: dashboard.html改为MD3风格 CSS变量自动适配日夜主题
- 面包屑: 加底色+模糊+左右外边距
- CSS: .btn::after加pointer-events:none
- 插件开发指南: 更新2.6节MD3主题同步和完善的CSS变量/组件类参考表

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-12 19:00:19 +08:00
qinglong 9b997d8bda fix: 文件选择器改为页内iframe弹窗 + 乱码编码修复 + API路径修复
- pickPath()从window.open改为页内iframe模态弹窗(遮罩+关闭按钮)
- files.html添加meta charset+CSS link,独立模式自动加载files.js
- API路径统一通过_apiUrl()构建,适配主面板和iframe两种上下文
- 独立iframe模式注入body/container覆写样式(无topbar)
- 选择模式栏移至底部margin-top:auto紧凑排列
- MD3风格toggle开关(显示隐藏复选框)
- CSS版本号缓存破坏

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 21:51:42 +08:00
qinglong d275c78aed docs: 插件开发指南新增 pickPath() 便捷调用说明
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 21:13:25 +08:00
qinglong dc0bb79a7e feat: 全局文件选择器 + 项目管理路径选择按钮
- app.js新增window.pickPath(mode,callback)全局复用
- 项目管理「工作目录」+Git部署「目标目录」添加📁选择按钮
- Git部署不再硬编码data/projects,改用用户选择的目标目录
- files.js picker模式读取URL cb参数实现callback_id回传
- app.js版本号更新至v=0701

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 21:11:50 +08:00
qinglong 99c3e3e853 feat: hash路由 + 插件页面内联加载 + 侧边栏插件面板展开项
- Hash路由: #/dashboard #/files #/plugins/xxx 支持刷新保持/前进后退
- 登录过期保留hash,登录后自动跳回原页面
- 插件WebUI页面内联加载到page-container(不再弹新窗)
- 插件页面HTML自动处理: style提取/body剥离/script执行/padding归零
- 侧边栏新增「插件面板」可折叠项,展开列出所有已注册插件页面
- 内存卡片显示 已用/总容量(百分比右侧)
- 插件开发指南新增 2.6 WebUI页面开发章节

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 20:56:39 +08:00
qinglong eeca8e37e1 docs: 更新README+架构文档 反映v0.6.0文件管理器/WS推送/WebUI重构
- README: 新增文件管理器特性/Web面板路由/架构更新
- 框架架构文档: 新增第十章 v0.6.0 WebUI重构+文件管理器+WS推送+MD3主题

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 18:28:50 +08:00
qinglong fd715eac2b feat: 文件管理器 + Windows兼容 + SVG主题适配 (v0.6.0)
新增:
- services/web_panel/routes/files.py — 全功能文件管理后端API (11个端点)
- pages/files.html + files.js — 文件管理前端 (浏览/编辑/上传/删除/右键菜单)
- 插件Picker API: window.open+postMessage唤出文件选择器
- 符号链接目录双层面包屑 (逻辑路径+物理路径)
- 文件系统全访问+跨平台 (Linux/Windows/macOS)
- permission_rules.yaml新增4个filemanager.*权限

修复:
- SVG fill=currentColor 日夜模式自适应
- 特殊目录容错 (/dev/fd损坏符号链接/proc)
- 面包屑每层级独立可点击+可编辑路径跳转
- ..行返回上级+data-is-dir补全
- rmlint→lstat回退 损坏符号链接不炸页面

文档:
- 插件开发指南新增第八章(文件管理器集成)
- 开发踩坑记录新增4条(特殊目录/双面包屑/SVG颜色/Windows)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 18:23:32 +08:00
qinglong 4a989bf50f feat: WS实时推送、网速双线图、按钮MD3修复、页面加载器修复 (v0.6.0)
后端:
- 新增 /api/system/ws WebSocket端点 替代HTTP轮询 (每2s推送系统+框架数据)
- 修复 uptime始终为0 (sm.start_time时间戳)
- 网络采集新增TX上行+实时网速(delta法)
- 进程内存采集 (/proc/self/status VmRSS)
- 抑制aiohttp内部WS帧日志防刷爆日志文件

前端:
- 仪表盘: HTTP轮询→WS连接+指数退避自动重连
- 实时日志: 修复onclose重连bug+批量渲染30fps+500行上限防卡死
- 网速卡片: DualLineChart双线图(下行实线/上行虚线)
- Y轴零点偏移-5% 防止零网速贴底
- 所有按钮修复MD3组合类(btn+btn-tonal+btn-sm)
- 页面加载器: 改动态script元素执行 修复onclick全局作用域问题
- emoji图标→Material Design SVG图标
- CSS去残留</style>+新增.nav-item svg约束
- 登录页输入框+标签左对齐+按钮MD3样式
- 多个版本号显示修复(去双重v前缀)
- chart.js/app.js/HTML页面统一加版本号防浏览器缓存
- .gitignore新增docs/目录

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-11 16:23:31 +08:00
qinglong fd0a97fec5 Fix login redirect: add /SenSu/index.html route, restart picks up new app.js
- /SenSu/index.html now serves login page (was 404 after cookie expiry)
- Fresh restart with latest page loader + inline script execution
- All routes verified: 200 OK
2026-06-11 14:47:37 +08:00
qinglong c07cea4ff7 Fix page loader: execute inline scripts, JS module loading optional
- innerHTML strips script tags, now extract and eval them
- External .js module: 404 = skip gracefully (no error)
- Progress bar: completes regardless of JS module
- Proxy/projects pages now render with CSS + inline JS executes
2026-06-11 14:43:24 +08:00
qinglong dd8a31e29a Fix: proxy API prefix, version from config, remove auth from framework/system endpoints
- Proxy routes now use /SenSu/api/proxy (was missing prefix)
- Projects routes use dynamic prefix
- Version reads from config/framework/base_config.yaml
- Framework/System APIs no longer require auth (public info)
- Proxy init separated from route setup (avoids frozen router error)
- Version updated to v0.6.0
2026-06-11 14:37:28 +08:00
qinglong 6918ee6514 Fix headless mode: use sleep loop instead of CLI mode (prevent EOF exit)
- Headless now keeps server alive with asyncio.sleep(1) loop
- CLI mode only for non-headless fallback
- Enables running SenSu as daemon/service
2026-06-11 13:02:56 +08:00
qinglong 885929f3ff Fix proxy/projects pages: error handling when API unavailable
- proxy: .catch() shows 'service not ready' instead of stuck loading
- projects: shows 'engine not ready' on API error
- Both: show 'no items' placeholder instead of blank
2026-06-11 12:47:39 +08:00