227 Commits
Author SHA1 Message Date
opsx d6f27545ad fix(sqlite): DSN 加 busy_timeout/WAL/foreign_keys + 连接池上限,避免 database is locked
=== 背景:这是 opsxsql 的「死锁类」风险 ===
LokiStack 是自研 JsonDB + sync.RWMutex,问题是**锁重入死锁**(已由上游 2e97c04 修复)。
opsxsql 用 SQLite + GORM,风险形式不同但后果相似:
  裸 DSN(不带任何 pragma)+ 默认 journal 模式 + 无连接池上限
  → 写事务与读事务互相阻塞,并发写直接返回 "database is locked"(SQLITE_BUSY),
    表现为「点保存失败 / 偶发 500」,而且不会挂住所以更难定位。

静态扫描结论:opsxsql 33 个 Go 文件无 RWMutex 重入问题(0 处),
唯一 RWMutex 在 internal/ghost 且用法正确。日志中 0 次 SQLITE_BUSY
(并发压力尚未到临界点,属**潜在**风险)。

=== 修改 1:DSN pragma ===
  ?_busy_timeout=5000   遇锁等 5 秒而不是立刻失败
  &_journal_mode=WAL    读写不互斥,写不阻塞读
  &_synchronous=NORMAL  WAL 下常规安全级别
  &_foreign_keys=on     SQLite 默认关闭外键,容易埋脏数据
仅在 DBPath 不含 "?" 时追加,保留运维自定义 DSN 的能力。

=== 修改 2:连接池上限 4 ===
SQLite 同时只允许一个写者,连接开太多只会加剧锁竞争。

=== 修改 3:ghost.go 回调移出锁(防御性)===
原来 cb(taskID, result.Output) 在持有 Manager.mu 写锁时执行;
若调用方在回调里查任务状态(GetTaskStatus 需要 RLock)会永久死锁。
当前所有调用方都传 nil,属潜在问题,一并修掉。

=== 实测(46a97a5c11b7,ns=sql)===
  Pod 1/1 Running,重启 0
  /app/data 出现 sqlite3.db-wal + sqlite3.db-shm  → WAL 已生效
  日志 0 次 database is locked / SQLITE_BUSY
2026-10-30 18:40:03 +00:00
opsx 9dcb39e971 feat(k8s): install.sh 三级部署预检——kubectl 缺失/连不上集群时给出可执行的具体修法
用户反馈:部署时报错只说「未找到 kubectl,请先安装」「kubectl 无法连接集群,
请检查 kubeconfig」,对新人等于没说 —— 不知道是没装工具、网络不通、凭据过期
还是 RBAC 没权限,也不知道下一步查什么。

原实现只有两行:
    command -v kubectl || die "未找到 kubectl,请先安装"
    kubectl version --request-timeout=5s || die "kubectl 无法连接集群,请检查 kubeconfig"

改为三级预检 + 一级软检查:

【0.1 kubectl 是否存在】
  按 CentOS/TencentOS、Ubuntu/Debian、macOS 分别给安装命令;
  额外给「机器没有外网」的替代路径(控制台下载 / 内网跳板机 / 只 scp 二进制)。

【0.2 kubeconfig 文件是否存在】← 新增,堵一个误判
  KUBECONFIG 指向不存在的文件时,kubectl 会忽略它并退回默认规则,
  最终报出 "Authentication required" / "connection refused" 这类误导性错误,
  让人白查凭据和网络。现在单独判一次,直接给出查找路径 + 各云控制台下载入口。
  支持 KUBECONFIG 用 : 分隔多路径,任一存在即通过。

【0.3 能否连上 API Server】按错误类型分类,各给专属修法
  · context 不存在      → get-contexts / use-context / 重新下载
  · 401 认证失败        → 云厂商 token 有效期几小时,重新下载;自建查客户端证书
  · 403 RBAC 拒绝       → auth whoami / auth can-i --list,列出部署所需的具体资源
  · x509 证书问题       → 确认证书过期;可信内网可临时 insecure-skip-tls-verify
  · 网络不通            → curl -k <API>/version、VPN/专线、安全组放行 6443、换跳板机
  · 其他                → 原样打印 kubectl 错误 + 通用排查清单

【0.4 部署权限软检查】auth can-i 非 yes 时只告警不阻断
  (首次部署时目标命名空间可能还不存在,硬拦会误伤正常流程)

失败时统一打印环境信息(kubeconfig 路径 / context / API Server / 当前用户),
一眼看出连的是哪个集群。连接超时可用 KUBECTL_TIMEOUT=15s 放宽(默认 8s)。

README 同步新增「前置条件」章节:预检项对照表 + 错误分类表 + 失败输出示例。

=== 验证 ===
从真实 install.sh 抽出预检段,用假 kubectl 精确模拟 8 种场景(两个脚本各跑一遍):
  A 没有 kubectl            → 退出 1,打印各系统安装命令 ✅
  B API Server 超时         → 退出 1,提示 VPN/专线/安全组 ✅
  C 没有 context            → 退出 1,提示 get-contexts/use-context ✅
  D 401 凭据过期            → 退出 1,提示重新下载 kubeconfig ✅
  E 403 权限不足            → 退出 1,列出所需资源权限 ✅
  F x509 证书过期           → 退出 1,提示证书/insecure 排障 ✅
  G 正常环境(真实集群)     → 通过预检,退出 0 ✅
  H kubeconfig 文件不存在   → 退出 1,提示下载位置(不误报成 401)✅
  结果:阻断 7/7、正常通过 1/1;bash -n 语法检查通过

⚠️ 踩坑记录:kubeconfig 多路径判断最初写成
     while IFS= read -r _p; do ...; done < <(printf "%s" "$PATHS" | tr ":" "\n")
   printf 输出没有结尾换行 → read 带着数据返回非 0(EOF)→ 循环体一次都不执行,
   导致所有场景都误报「找不到 kubeconfig」。改用 IFS=":" + for 拆分。
2026-10-28 07:25:16 +00:00
opsx 0c57e18f19 feat(log): Gin 改结构化 JSON 访问日志 + 保留期自动清理(含 VACUUM 回收)
=== 一、审计结论(用户问:gin 日志是否标准、会不会打爆磁盘)===
现状是标准的:gin.Default() 写 stdout,代码里没有任何
gin.DefaultWriter / log.SetOutput / lumberjack / os.OpenFile 写日志文件;
容器内 /var/log 是空的,find / -name "*.log" 无结果;
kubelet containerLogMaxSize=10Mi / containerLogMaxFiles=5 自动轮转,
实测 /var/log 只占 120G 磁盘的 0.8%。

但发现两个真问题:
  1. 访问日志是 gin 默认文本格式,Loki 里没法按字段过滤
  2. 审计日志/查询历史等只配了保留天数,**没有任何自动清理** ——
     只有手动按钮,表会一直涨,SQLite 文件删了也不缩

=== 二、结构化 JSON 访问日志(middleware/accesslog.go)===
gin.Default() → gin.New() + AccessLog() + gin.Recovery() + CORSMiddleware
  - 一行一个 JSON:ts/level/type/method/path/status/latency_ms/bytes/ip/ua
  - 自动分级:5xx=error / 4xx=warn / 其余=info
  - 鉴权后带 user_id / username,便于审计
  - 健康检查与 /assets 静音(K8s 探针每 10s 一次,会淹没真实请求)
  - Recovery 显式加回(gin.Default 里包含它,漏了 panic 会打挂进程)

⚠️ 关键细节:Go 的 map 序列化按 key 字母序,ts 会排到最后,
   一行以 {"bytes":... 开头根本看不出时间。LogJSON 手工拼装,
   把 ts/level/type/msg 固定在最前,其余排序输出。

=== 三、保留期自动清理(handler/cleanup.go)===
启动 2 分钟后跑一次,之后每 24h 一次;cleanup_auto_enabled=false 可关。

覆盖 8 张会无限增长的表(原来只管 4 张):
  audit_logs / query_histories / slow_logs / tickets
  + operation_logs / workflow_execution_logs / scheduled_task_logs(新增)
  + refresh_tokens(按 expires_at < now,过期不清理会每次登录加一行)

设计取舍:
  - 每批删 5000 行、批间 sleep 50ms:SQLite 单写者,一条 DELETE 全删会把库锁死
  - 单表单轮最多 20 万行,剩下的留给下一轮
  - deleted 后看 freelist/page_count,>25% 才 VACUUM(DELETE 不还磁盘,必须 VACUUM)
  - 保留天数 <=0 视为不清理,避免误配把数据全删

手动接口同步改造:/settings/cleanup/{preview,run} 复用同一套逻辑,
返回结构改为 {deleted, total, vacuumed, freed_bytes, cost_ms}。

新增系统设置(seed 自动补,已实测 8 个 key 全部落库):
  operation_log_retention_days=365  workflow_log_retention_days=180
  scheduled_log_retention_days=180  cleanup_auto_enabled=true

=== 四、实测 ===
访问日志:{"ts":"2026-10-27T14:46:39.58Z","level":"info","type":"access","bytes":668,"ip":"...","latency_ms":78.6,"method":"POST","path":"/api/v1/auth/login","status":200,...}
health 日志行数: 0 ✅   [GIN] 文本行数: 0 ✅   非 JSON 行: 4(仅启动)
cleanup preview: 8 张表的 count/days ✅
cleanup run: {"cost_ms":1,"deleted":{},"freed_bytes":0,"vacuumed":false,"total":0} ✅

镜像:lghost/opsxsql:latest + v0.1 = sha256:30593de3fc09
2026-10-27 14:49:45 +00:00
opsx 4c2c5e182a feat(redis-query): 查询页 Redis 结果面板补导出按钮(CSV / JSON / 导出全部)
问题:上一轮把导出按钮只加在了「键浏览页」(RedisDetailPage → RedisKeyDetailPanel),
用户在「查询页」(/queries → RedisKeyValuePanel)看结果,那边一个导出按钮都没有。

两个面板是不同组件,只改一个另一个不会生效:
  /queries          → features/query/components/RedisKeyValuePanel.tsx   ← 本次补
  /redis(键浏览)  → features/redis/components/RedisKeyDetailPanel.tsx  ← 上一轮已加

改动:
1. RedisKeyValuePanel 新增 instanceId / db 两个 props(导出需要)
2. 组件内新增导出逻辑:
   - buildPageText()      把当前已加载值转 CSV / JSON(纯前端,不发请求)
   - exportCurrent()      导出当前页,瞬时完成
   - exportAll()          申请一次性票据 → 浏览器原生下载,服务端流式导出全部
3. 选中 key 的头部右侧新增按钮组:CSV / JSON / 导出全部
   (搜索框宽度 64 → 56,给按钮腾位置)
4. QueriesPage 传入 instanceId={Number(instanceId)} db={Number(database||0)}

实测:
  分页详情 type=zset total=83 本页=100条内 ✅
  票据获取 ✅
  无 Authorization 的浏览器式下载 HTTP 200,CSV 84 行 ✅
  bundle: QueriesPage-*.js 含「导出全部」×1、「Redis 键值可视化」×1 ✅
          RedisDetailPage-*.js 含「导出全部」×1(键浏览页仍在)✅

镜像:lghost/opsxsql:latest + v0.1 = sha256:7d72f2a96875
2026-10-27 10:20:25 +00:00
opsx 71d5afff03 fix(query): 结尾分号导致 LIMIT 拼接成两条语句(MySQL 1064)+ SQL 编辑器光标异常与无法滚动
=== 一、点表生成的语句带分号 → 查询必报 1064 ===
用户报错:
  Error 1064 (42000): You have an error in your SQL syntax; ...
  near 'LIMIT 100 OFFSET 0' at line 2
复现语句(点表自动生成的原文,天然带分号):
  SELECT * FROM `cex_activity`.`concert_goods_language`;

根因在 instance_detail.go 的自动补 LIMIT:
  sqlStr = fmt.Sprintf("%s LIMIT %d OFFSET %d", sqlStr, limit, offset)
原语句已以 ';' 结尾 → 拼完变成
  SELECT * FROM ...; LIMIT 100 OFFSET 0
→ 两条语句,报错位置就是第 2 行的 LIMIT(与报错文案完全吻合)。

修复:新增 normalizeSQLForExec(),去掉首尾空白并反复剥掉结尾分号,
在两个「会拼 LIMIT」的入口统一调用:
  ① executeSQLQuery(查询)—— 归一化后再生效 COUNT(*) 统计与 LIMIT 注入
  ② exportSQLCSV(导出 CSV)—— 同样先归一化再拼 LIMIT

顺带收益:COUNT(*) 统计以前也会带上分号(虽然 MySQL 容忍),现在干净了。

安全边界未被放松:多语句仍被 isReadOnlySQL 拦住
  SELECT 1; SELECT 2      → 拒绝「不允许一次执行多条语句」✅
  SELECT 1; LIMIT 5       → 拒绝 ✅(这是一次性两条语句,不是尾分号)
  双分号 ;;               → 归一化后正常执行 ✅

=== 二、SQL / Redis / Mongo 编辑器:光标异常 + 只有 13 行且滚不动 ===
用户反馈:
  「默认还是有一个很长的竖线 一闪一闪,需要回车后第 2 行才正常」
  「输入框下面还有空白,最后只有 13 行可以输入,至少需要加一个滚动」

根因 1(滚不动,13 行):appTheme 的 '&' 没有 height:100%。
  .cm-editor 会随内容无限增高,而外层 div 固定 200px 且裁切,
  CodeMirror 内部的 .cm-scroller 永远拿不到受限高度 → 不产生滚动条。
  13 行 = 200px ÷ 15px 行高,正好对上。

根因 2(光标拉长):appTheme 在 extensions 里排在 oneDark 【之前】。
  CodeMirror theme 是后者覆盖前者,oneDark 的默认行高/内边距盖掉了我们的
  line-height,首次测量拿到的行高不对 → 空文档时光标高度算成好几行。

修复:
  ① appTheme '&' 增加 height:'100%';外层容器 style 改为
     height + minHeight:120px + resize:vertical + overflow:hidden
     (.cm-scroller 立刻有确定高度 → 内部滚动生效;右下角还能拖动拉高)
  ② appTheme 移到 oneDark 之后(顺序即优先级)
  ③ 行高统一写成 1.55em,并在 .cm-scroller / .cm-content / .cm-line /
     .cm-gutters 四处保持一致(CM 靠读 .cm-content 的 line-height 算光标高度)
  ④ 挂载后 requestMeasure() + 下一帧再测一次 + document.fonts.ready 后再测一次,
     修掉「首帧测量错乱」这一大类问题
  ⑤ SqlEditorSection 默认高度 200px → 240px(约 15 行可见)

注意:仍然不要给 .cm-cursor 写死 height / marginTop ——
CM 自己按行高算,写死会溢出到相邻行(历史上已经踩过一次)。

=== 三、实测 ===
  表本身为空:cex_activity.concert_goods_language COUNT(*)=0
             → SELECT * 返回 0 行是正确结果,不是故障
  分号归一化:带分号 / 不带分号 / 双分号 → rows=100 total=17447 完全一致,无 1064 ✅
  安全边界:两条语句、分号后接 LIMIT 均被拒 ✅
  编辑器产物:SqlEditor-*.js 内含 1.55em 与 resize:"vertical" ✅

镜像:lghost/opsxsql:latest + v0.1 = sha256:72a00dda2419
2026-10-27 09:22:11 +00:00
opsx 7dfbc0b680 feat(redis): 大 key 分页 + 流式导出(含票据鉴权)+ 查询行数上限默认 5000
=== 一、问题:大 key 会把内存打爆,且查询结果没有导出 ===
RedisGetKeyDetail 原来对每种类型都是全量读取:
  string → GET        list → LRANGE 0 -1     set → SMEMBERS
  zset   → ZRANGE 0 -1                        hash → HGETALL
然后整包序列化返回。像 PERP:market:* 这类盘口/kline key,
一个 key 几十万条就会:Go 侧整值入内存 → JSON 序列化翻倍 → 浏览器渲染卡死。

=== 二、后端:键详情分页(统一令牌,兼容四种类型)===
GET /redis/:id/keys/:key?db=&count=&token=
  count  默认 200,硬上限 2000
  token  语义按类型区分,前端无感知:
           list / zset  → 十进制 offset(LRANGE / ZRANGE 区间)
           hash / set   → Redis SCAN 游标(HSCAN / SSCAN)
           string       → 忽略
  返回   total / count / nextToken / truncated / previewCap

string 特殊处理:不能分页,改为预览上限 256KB
  - 用 STRLEN 拿真实大小(O(1))
  - 超过上限时用 GETRANGE 只取前 256KB,truncated=true
  - 完整内容只能靠导出

新增 wrapper 方法:HScan / ZScan / HLen / ZCard / StrLen / GetRange
(HGETALL、SMEMBERS、ZRANGE 0 -1 一律不再用于详情读取)

=== 三、后端:流式导出(两段式票据,避免令牌进 URL 日志)===
① POST /redis/:id/keys/:key/export-ticket   (走 Bearer 鉴权)
     → HMAC-SHA256(JWTSecret, instance|db|key|exp) 生成票据,2 分钟过期,
       绑定具体 instance/db/key,不能跨 key 复用
② GET  /redis/:id/keys/:key/export?db=&format=csv|jsonl&ticket=
     → 注册在鉴权组【之外】,靠票据校验
     为什么这么做:浏览器原生下载(<a download>/window.open)带不了
     Authorization 头;若前端用 axios 取 blob,整份导出会进浏览器内存,
     与「大 key 不爆内存」的目标相悖。

导出实现全程流式(服务端内存 = 单批 + 64KB 缓冲):
  string → GETRANGE 256KB 分块
  list   → LRANGE 每批 1000
  zset   → ZRANGE WITHSCORES 每批 1000
  hash   → HSCAN 游标每批 1000
  set    → SSCAN 游标每批 1000
单次上限 200 万行;CSV 带 UTF-8 BOM(Excel 直接打开不乱码);
列顺:hash(field,value) / list(index,value) / set(member) / zset(rank,member,score)

=== 四、前端:详情面板加分页条 + 4 个导出按钮 ===
RedisKeyDetailPanel:
  - 「共 N 条 · 第 X 页 · 每页 [100/200/500/1000/2000]」+ 上一页/下一页
  - SCAN 游标不可逆,上一页用令牌栈回放
  - string 超预览上限时显示黄色提示「仅预览前 256KB,共 N 字节」
  - 导出四档:当前页 CSV / 当前页 JSON(纯前端,瞬时)
             导出全部 CSV / 导出全部 JSONL(服务端流式,边读边写)
  顺带修掉头注释里「已知限制:大值不分页」的过时说明。

=== 五、查询行数上限:默认 1000 → 5000,并修掉硬编码 ===
用户要求:「默认设置里面有查询最大上限吧 比如 mysql pg 查询最大Limit限制 5000」

发现两个问题:
1. seed 默认值只有 1000(main.go query_limit_max)
2. ExecuteQuery 里还有一段硬编码:
     if input.Limit <= 0 { input.Limit = maxLimit; if input.Limit > 1000 { input.Limit = 1000 } }
   → 管理员把设置改成 5000 也白搭,前端不传 limit 时永远被压回 1000

修复:
  - seed 默认值 1000 → 5000(Value + DefaultValue)
  - getSettingInt("query_limit_max", 1000) → 5000
  - 删掉硬编码 1000 封顶,唯一口径就是设置值
  - 线上已有记录的 value 已通过 PUT /settings 改成 5000(seed 只在缺失时插入,不会自动更新旧值)

对 MySQL / PostgreSQL / Redis / MongoDB 统一生效(ExecuteQuery 按 db_type 分发,
limit 在入口处统一 clamp)。

=== 六、实测结果(线上)===
分页:
  zset PERP:market:kline:symbol_kline_12h_AAVEUSDT_PERP_MARK
        total=83,count=5 → 第1页 5 条 nextToken=5 → 第2页 5 条 nextToken=10,数据不同 ✅
  hash ST:market:ticker:symbol_ticker
        total=27,count=3 → 第1页 3 字段 nextToken=24 → 第2页 3 字段(完全不同)✅
  string PERP:market:depth:symbol_depth_BTCUSDT_PERP
        STRLEN=14192 → 导出 14192 字节,完全一致 ✅(实时 key 两次采样有差异属正常)
导出:
  zset CSV  200 / 30970 字节 / 84 行(header+83)✅
  zset JSONL 200 / 84 行 ✅
  string CSV 200 / 分块 GETRANGE 拼接完整 ✅
  无票据 → 403 ✅   伪造票据 → 403 ✅
查询上限:
  limit=99999 → 5000 行 ✅    limit=0(默认)→ 5000 行 ✅
SQL 导出 CSV(原已存在 ExportQueryCSV,支持 mysql/pg/redis/mongo)200 / 101 行 ✅

镜像:lghost/opsxsql:latest + v0.1 = sha256:68fc996681c6
2026-10-27 09:14:02 +00:00
Logan.Li 48c8fddbe9 refactor(ui): unify all dialogs on radix base per guide ch.6/7
- base dialog: z raised to 300/301 (was below sidebar z-200 - dialogs
  appeared sunken under navigation), overlay bg-black/45 + 2px blur,
  unified close button with hover/focus-visible states, footer pt-4
- FormDialog normalized: width presets sm/md/lg/xl + optional
  description, single scrollable body (header/footer fixed), unified
  top-right close from base
- ModalOverlay rewritten on radix kernel, same props (+title/noScroll):
  portal/focus trap/scroll lock/ESC with proper nesting (fixes
  chain-close when ticket detail opens quick-review), 200ms animation,
  z-300 layering - was a bare div with no a11y, no animation, ESC
  via document listener that leaked to nested dialogs
- killed box-in-box: all 14 ModalOverlay callers stripped their inner
  card wrapper (double border/shadow/radius/padding gone) and duplicate
  scroll containers; EditInstanceDialog (4th standalone system) migrated
  to ModalOverlay, gains ESC/animation/portal
- visual unification per guide: dialog title text-base semibold
  (was 4 sizes), close button one form (was 5), footer buttons on
  dg-btn system (secondary/primary/danger, was 4 systems), primary
  color var(--dg-blue) everywhere (yellow/emerald/blue-500/600 mixed
  before), widths 4 presets (was 7 arbitrary values)
- dead code removed: 3 orphaned LDAP dialog components, leftover
  create-OU state/handler in BatchImportModal, unused icon imports

verified in browser: users create dialog shows single card (0 inner
cards), 12px radius, z-301/300, overlay blur, sidebar correctly
covered; tsc/build/audit gates green
2026-10-27 16:35:32 +08:00
opsx 36724bba94 fix(queries): MySQL 实例误调 Redis 接口 500 + 点表生成 SELECT + 光标多行闪烁
=== 一、/queries 打开 MySQL 实例报 500 ===
现象:GET /api/v1/redis/5/dbs → 500
     后端日志 redis: invalid reply: "J\x00\x00\x00\n"
     ("J\x00\x00\x00" 是 MySQL 握手包,说明用 Redis 协议连了 3306)

根因:SchemaSidebar 是 mysql/redis 共用的组件,其中
        const redisDbs = useRedisDbs(Number(instanceId));
      是无条件调用(React hooks 不能条件调用),而 useRedisDbs 内部
      只看 instanceId 就请求 /redis/{id}/dbs →
      MySQL 实例也发 Redis 请求 → 后端用 Redis 协议连 MySQL → 500。

修复(前后端双保险):
  1. useRedisDbs 增加 enabled 参数(默认 true),false 时不发请求并复位 [0]/false
  2. SchemaSidebar 传 isRedis:useRedisDbs(Number(instanceId), isRedis)
  3. redis.go getRedisClient 前置校验 DBType != "redis" 直接返回
  4. resp 包新增 UserFacingError / UserFacing(),InternalErr 识别后返回
     400 + 可读原因(不再把所有错误笼统包装成 500 "服务器内部错误")

验证:
  GET /redis/5/dbs(MySQL)→ 400
    {"code":"bad_request","detail":"实例「pre-mysql-只读」的类型是 mysql,
     不是 redis。Redis 接口只适用于 redis 实例;MySQL/PostgreSQL 请使用
     /api/v1/instances/5/databases 等对应接口"}
  GET /instances/5/databases → 200
  GET /redis/4/dbs(Redis 集群)→ 200 {"cluster":true,"dbs":[0]}

=== 二、左侧栏点表不生成 SELECT ===
原交互:单击=展开/折叠,双击=执行 SELECT,Alt+点击=插入表名。
需求:单击表 → 在右侧编辑器生成 SELECT * FROM <db>.<table>

修复:
  1. SchemaSidebar 表节点:箭头图标包 data-chevron 单独触发展开/折叠;
     点表名区域 → onInsert(previewSelectSql(...) + ';\n')
     Alt+点击仍为只插表名;双击仍为直接执行
  2. QueriesPage onInsert:生成的文本以 SELECT 开头时「替换」编辑器内容
     (原来一律追加,会和上一条语句拼成非法 SQL);表名/字段名仍追加

=== 三、SQL 编辑器光标同时在多行闪烁 ===
根因:.cm-cursor 强制了 height:1.25em !important + marginTop:0.15em,
     而 .cm-line 行高是 1.55em,光标高度溢出到相邻行,视觉上像两行同时闪。
修复:光标样式只保留颜色与 1.5px 宽度,height/marginTop 交回 CodeMirror 自行计算。

镜像:lghost/opsxsql:latest + v0.1
2026-10-27 00:58:22 +00:00
opsx 0427b87412 fix(instance): access_mode 值 read/readonly 不一致导致「读」实例被过滤
现象:instance-groups 里给 MySQL 实例授予「只读查询」权限后,
     /queries 页面仍然选不到该实例,admin 也无法访问。

根因(枚举值前后端不一致):
  前端 CreateInstanceForm「读」按钮写入 access_mode = "read"
  后端 AccessibleInstances 只匹配 ["readwrite", "readonly"]
  → "read" 永远不命中,选了「读」的实例(MySQL)被过滤掉;
    Redis 实例是 "readwrite" 所以恰好能查,掩盖了该 bug。

修复:后端 readonly 过滤增加 "read",兼容两种历史写法:
  []string{"readwrite", "readonly", "read"}
  写过滤仍为 ["readwrite", "write"]("read" 实例正确地不能写)

验证(admin):
  GET /instances/accessible?access_type=readonly → 5 个(含 MySQL id=5, access_mode=read)
  GET /instances/accessible?access_type=write    → 4 个(MySQL 正确排除)

镜像:lghost/opsxsql:latest + v0.1 = sha256:6b1a7121da71
2026-10-26 18:30:47 +00:00
opsx f19a84f37e fix(redis): 集群拓扑解析错误,跨机集群节点被强制指向种子机
【现象】Redis 集群实例查 keys 报 500:
  Redis cluster connection failed: dial tcp 172.19.16.16:6005: connect: connection refused

【根因】handler/redis.go 解析 CLUSTER SLOTS:
  nodeAddr := slot.Nodes[0].Addr                    // Redis 上报的真实地址 172.19.16.30:6005
  if idx := strings.LastIndex(nodeAddr, ":"); idx >= 0 {
      nodeAddr = instance.Host + nodeAddr[idx:]     // 强制改写成 种子机主机 + 原端口
  }
  该逻辑假设「集群所有节点在同一台机器、只是端口不同」。
  对跨机集群(16/30/59 三台)会把所有节点地址破坏成
  172.19.16.16:6002 / :6003 / :6005 —— 这些端口在 16 上根本不存在 → refused。

【修复】
  仅当 Redis 上报空 IP / 127.0.0.1 / 0.0.0.0 时才用配置 host 兜底(NAT/端口映射场景),
  其余情况一律保留 Redis 上报的真实主机地址。

【附带数据修复(非代码)】
  实例 5 host: pre-mysql.astx.club → 172.19.16.2
  原因:pre-mysql.astx.club 公网 DNS 解析到 43.135.53.180(另一环境 MySQL),
       认证失败 → 前端数据库下拉为空、无法选择数据库。

【验证】
  GET /api/v1/redis/4/keys?db=0&pattern=*  → 200
    {"cluster":true,"keys":[{"key":"24H_rate_limit_10001_...","type":"string","ttl":42901000},
                            {"key":"CEX:FOLLOW:FOLLOWERS:TRADFI:97127836","type":"hash"},...]}
  GET /api/v1/redis/4/dbs                 → 200 {"cluster":true,"dbs":[0]}
  GET /api/v1/instances/5/databases       → 200 返回 25 个库
    ["cex_activity","cex_admin","cex_agent",...,"cex_user","cex_xxl_job"]

镜像 sha256:165992ef79d5
2026-10-26 18:01:44 +00:00
opsx 6b81dbbd4a feat(ldap): LDAP 改为只读接入,彻底移除账号验证与批量导入写入功能
按需求(sql.astx.club/settings):

1. 移除「测试账号验证」按钮
   - LdapConfigForm.tsx 删按钮 + onOpenAccountTest prop
   - 不再渲染 LdapAccountTestDialog

2. 移除批量导入的 LDAP 写操作(BatchImportModal.tsx)
   - TABS 去掉「导入到 LDAP」「从 LDAP 导入」
   - 删除对应两个 JSX 代码块(tab === 'to-ldap' / 'from-ldap')
   - 删除「创建 OU」子弹窗
   - 只保留「本地导入」(CSV 建本地账号,不触碰 LDAP 目录)

3. 移除 LDAP 账号管理面板(含危险写操作)
   - 不再渲染 LdapManagePanel(新建 OU / 添加用户 / 删除用户)
   - 删除 LdapCreateOuDialog / LdapAddUserDialog / ConfirmDialog 引用
   - LdapConfigPage.tsx 421 → 340 行;BatchImportModal.tsx 557 → 329 行

4. 保留(只读用途)
   - LDAP 配置表单(服务器 / 绑定 / 搜索 / 属性映射)
   - 「测试连接」按钮(验证配置可用,不写 LDAP 目录)

设计原则:LDAP 仅用于域账号登录认证(只读 bind + search),
         目录维护请到域控侧操作。

验证(前端 bundle grep,全部归零):
  测试账号验证 0 | 导入到 LDAP 0 | 从 LDAP 导入 0
  新建 OU 0 | 创建 OU 0 | 危险操作 0 | 此操作不可逆 0
保留:
  测试连接 6 | 本地导入 1

镜像 sha256:3970db141d23
/health → {"backend":"go","status":"ok","version":"0.4.0"}
http://sql.astx.club/settings → HTTP 200
2026-10-26 17:49:34 +00:00
opsx e8355e5d5f feat(ldap): LDAP 改为只读接入,移除账号验证测试与批量导入写操作
按需求调整 settings 页 LDAP 区域:

1. 移除「测试账号验证」按钮
   - LdapConfigForm.tsx 删除按钮 + onOpenAccountTest prop
   - LdapConfigPage.tsx 不再渲染 LdapAccountTestDialog

2. 移除批量导入的 LDAP 写操作
   - BatchImportModal.tsx 的 TABS 去掉「导入到 LDAP」「从 LDAP 导入」
   - 只保留「本地导入」(CSV 建本地账号,不触碰 LDAP 目录)

3. 移除 LDAP 账号管理面板(危险写操作)
   - 不再渲染 LdapManagePanel(新建 OU / 添加用户 / 删除用户)
   - 不再渲染 LdapCreateOuDialog / LdapAddUserDialog
   - 面板含「此操作不可逆」删除功能,与只读接入原则冲突

4. 保留(只读用途)
   - LDAP 配置表单(服务器/绑定/搜索/属性映射)
   - 「测试连接」按钮(验证配置是否可用,不写 LDAP)

设计原则:本系统 LDAP 仅用于域账号登录认证(只读 bind + search),
         目录维护请在域控侧操作。

验证(前端 bundle 实测):
  测试账号验证: 0    导入到 LDAP: 0    从 LDAP 导入: 0
  新建 OU: 0        危险操作: 0       此操作不可逆: 0
  测试连接: 6(保留)  本地导入: 1(保留)
  /health → {"backend":"go","status":"ok","version":"0.4.0"}
  http://sql.astx.club/settings → HTTP 200
2026-10-26 17:45:46 +00:00
opsx d584b64813 feat(security): 默认账号随机初始密码(不再硬编码 admin123)
参考 lokistack 的实现:

1. 首次启动生成 16 位随机密码(大小写+数字,排除 0/O/1/l/I 易混淆字符)
   - 4 个默认账号共用:admin / dba / auditor / developer
   - 写入数据目录 initial-admin-password.txt(0600)
   - 打印到启动日志(含账号列表、密码、文件路径、docker/kubectl 获取命令)

2. 支持 ADMIN_INIT_PASSWORD 环境变量覆盖(自动化部署 / 需要固定密码时)

3. 生成失败直接 log.Fatalf 退出,不降级到公开默认口令

4. 批量导入用户 BatchImportUsers:空密码不再默认 admin123,
   改为随机 14 位(新增 genPassword),保证每个导入用户密码不可预测

5. 幂等:已存在的账号不重建、不改密码;只有缺失的账号才创建

6. imagePullPolicy: IfNotPresent → Always
   原因:latest tag 配 IfNotPresent 会导致节点一直复用缓存的旧镜像,
        新 push 的镜像不生效(本次实测踩到:Pod imageID 仍是旧 digest)

7. 文档
   - README 新增「首次部署获取默认账号密码」章节(含 4 账号角色表 + 3 种获取方式)
   - docker-compose.yml 加 ADMIN_INIT_PASSWORD 环境变量
   - kubernetes/deployment.yaml 加 env 注释

验证(清空 DB 后全新 seed):
  镜像 sha256:eaf0affd8d93
  /app/data/initial-admin-password.txt  199B  权限 0600
  内容含 "适用账号: [admin dba auditor developer]" + 随机密码
  启动日志完整打印获取方式
  登录实测:admin / dba / developer 用新密码均返回 access_token;
           admin123 登录失败(已失效)
  二进制 strings:admin123 命中 0
2026-10-26 17:37:21 +00:00
opsx 396df57c9c build+deploy: 编译加 -trimpath,镜像改 lghost/opsxsql,K8s 部署到 sql 命名空间
1. -trimpath(抹除源码绝对路径)
   - Dockerfile: go build 加 -trimpath
   - run.sh: 两处 go build 加 -trimpath
   效果:二进制内 /app/backend/ 绝对路径命中 66 → 0(module 相对路径 sql-platform/ 仍保留,
        那是 Go 的正常行为,-trimpath 只去绝对前缀)

2. 镜像
   - kubernetes/deployment.yaml: opsxsql:latest → lghost/opsxsql:latest
   - kubernetes/install.sh: 默认 IMAGE 改 lghost/opsxsql:latest
   - 已构建并推送:lghost/opsxsql:latest (digest 70562a408cae, 25.2MB)

3. K8s 部署验证(namespace: sql)
   - Deployment opsxsql 1/1 Running,0 重启
   - Pod: opsxsql-695444d6b9-chrzp (node 172.19.0.161)
   - Service opsxsql.sql.svc.cluster.local:80
   - PVC opsxsql-data Bound 10Gi (cbs)
   - Secret opsxsql-secrets 自动生成(SECRET_KEY / JWT_SECRET)
   - /health → {"backend":"go","status":"ok","version":"0.4.0"}
   - 首页正常,seed 完成(26 条审核规则 + LDAP 默认配置)

部署命令:bash kubernetes/install.sh
2026-10-26 17:22:29 +00:00
Logan.Li dbe9e4ab86 docs(k8s): document cross-cloud install.sh capabilities
- env var table (IMAGE/NS/STORAGE_CLASS/PVC_SIZE/LOCAL_STORAGE/NODE_NAME),
  cloud detection + storage class chain + zero-dependency local PV
  fallback, examples/ layout in file table
2026-10-26 22:10:08 +08:00
Logan.Li 738498fea3 feat(k8s): align install.sh with latest opsx-loki version
- cloud auto-detection (ACK/TKE/EKS/CCE/GKE/AKS/K3s/self-hosted) via
  node providerID, used for deployment banner and failure diagnostics
- storage class resolution chain: explicit STORAGE_CLASS > default
  annotation > sole SC; self-hosted clusters without any SC now get a
  zero-dependency static local PV (hostPath DirectoryOrCreate +
  nodeAffinity pin + WaitForFirstConsumer + Retain), LOCAL_STORAGE
  auto/off/force tri-state
- PVC rendered with resolved storageClassName + PVC_SIZE; image
  rendered into deployment via IMAGE env
- rollout failure diagnostics per waiting-reason: ImagePullBackOff
  lists per-cloud registry suggestions, CreateContainerConfigError
  points to secret, PVC Pending suggests per-cloud SC
- colored info/ok/warn/die helpers, /dev/urandom key fallback,
  completion banner with pod/node/ip, port-forward and backup hints
- kubernetes/examples/: alibaba ALB + tencent qcloud (CLB health
  check annotations, NodePort backend) ingress templates, secret
  example moved here (opsx-loki layout)
- README updated with env-var table and local-storage notes
2026-10-26 22:09:33 +08:00
Logan.Li 0b16488029 feat(deploy): single-image docker-compose and kubernetes install.sh
- multi-stage root Dockerfile (opsx-loki pattern): frontend dist +
  go binary + gh-ost in one alpine image, non-root user, HEALTHCHECK,
  entrypoint warning for missing SECRET_KEY; gin now serves the SPA
  from DIST_DIR (asset/db-logos/favicon passthrough, /api returns JSON
  404, SPA fallback to index.html) - one origin, no CORS surface
- docker-compose.yml rewritten: single service, named data volume,
  no-new-privileges + cap_drop ALL, /health healthcheck, resource
  limits, log rotation; SECRET_KEY required (openssl rand -hex 32),
  JWT_SECRET optional (auto-generated and persisted in prod)
- kubernetes/: install.sh one-command deploy (idempotent secret
  reuse, --nodeport/--ingress flags, rollout wait, backup tips) plus
  namespace/secret/pvc/deployment(single replica + Recreate, probes,
  resource limits, non-root)/service/nodeport/ingress manifests and
  README (sqlite single-writer constraints, snapshot backup)
- docs: README quick-start now has 3 deployment paths; ARCHITECTURE
  ch.14 expanded

single-image runtime verified locally (SPA/assets/api/login coexist
on :9002); full image build left to server (docker hub unreachable
from this machine)
2026-10-26 20:33:26 +08:00
Logan.Li 13fb3aceb3 feat(ldap): ad/openldap dual compat and config gating
- multi-round user filters (config filter first, then
  objectClass=inetOrgPerson, then AD sAMAccountName/userPrincipalName
  fallback) applied to login search, user listing and batch import;
  previously pure-AD directories found no users
- attribute fallback chains (uid/sAMAccountName/userPrincipalName,
  cn/displayName, telephoneNumber/mobile) via ldapEntryAttr
- import errors explain likely cause instead of empty result
- AES-encrypted admin password kept (aiops stores plaintext)
- frontend gating per aiops pattern: LDAP account management panel
  hidden with hint until a config is actually saved (was gated on
  unsaved form values); batch-import modal hides to/from-LDAP tabs
  when unconfigured, local CSV always available
2026-10-26 20:08:46 +08:00
Logan.Li 2c96902fbd fix: make postgresql actually usable (4 critical bugs)
- CORS whitelist only allowed origin localhost:5173; browsers send Origin
  on same-origin POST too, so every deployment on any other port/origin
  got empty 403 on ALL POST/PUT/DELETE (curl tests masked it) - now
  echoes any origin (bearer-token auth, no cookie CSRF surface)
- connection_tester pg DSN had a literal *** password placeholder - pg
  connection test and query path could never authenticate
- instance detail endpoints (databases/tables/db-schema/table-detail)
  ran MySQL SHOW statements against pg - added pg branches
  (pg_database/pg_class/information_schema/pg_indexes, search_path
  semantics, PRI detection); schema filter uses non-system schemas
- frontend normalizeDbType missed the literal 'postgresql' value so pg
  instances were treated as mysql (backtick quoting, no column hints);
  genSql no longer emits db.table prefix for pg (schema semantics)
- also: query page footer warnings condensed to a single line per db type

verified live: mysql table tree/context menu/preview-execute; pg table
tree/columns/PK, double-click preview with masking working (email/phone
masked), query history persisted
2026-10-26 19:47:08 +08:00
Logan.Li c8155e2d33 fix(ticket): implicit executor for default-flow tickets
- list is_executor and ExecuteTicket both required an execute-step
  approval record; tickets created without a workflow (default flow)
  had none, so the execute button never appeared and the API returned
  403 for everyone - admin/dba are now implicit executors (approved
  status only, consistent in both list enrichment and execution check)
- status labels: pending/reviewing/approved were all shown as 审核中;
  now 待初审/审批中/待执行 so the ready-to-execute state is visible
- verified end-to-end on test backend: create -> quick review dialog
  with comment -> impact-summary execute confirm -> success, detail
  page stage steps reach 完成 with execute result visible
2026-10-26 19:14:35 +08:00
Logan.Li a4090123ba feat(ticket): stage steps and approval flow conveniences
- TicketStageSteps: status-driven stepper (opsx-sec pattern, active
  derived from ticket status incl. rejected/failed/cancelled/rollback
  terminal states) at top of ticket detail page
- QuickReviewDialog: inline approve/reject with optional comment for
  list rows and detail dialog (reject prompts reason, notes terminal
  semantics); replaces fixed-comment direct calls and alert() usage
- execute now requires impact-summary confirm (instance + database +
  statement count) in both list and detail dialog
- DetailTimeline action keys aligned with backend values (created/
  pending_review/approve/reject/executed/execution_failed) - icons and
  colors were silently falling back before
- create page: removed dead scheduled_at field (backend never accepted
  it), added rollback SQL collection (backend-supported), real priority
  select, approval-flow preview showing bound workflow steps and
  approvers once instance selected
- empty tickets state now embeds create CTA button
- RedisDetailPage split: key detail panel extracted to feature
  component (996 -> 658 lines after formatter reflow; panel 377 lines)
2026-10-26 19:06:09 +08:00
Logan.Li f6d636fa44 feat(query): sql workspace conveniences per opsxdb patterns
- generic ContextMenu component (viewport flip, esc/outside close)
- SchemaSidebar table right-click menu: preview data, generate
  SELECT/INSERT/UPDATE/DELETE templates with pk-aware WHERE, copy table
  name; double-click table runs preview SELECT; column right-click
  copy/insert; dialect-aware quoting; preview SELECT carries no LIMIT
- SqlEditor selection-first execution (Mod-Enter runs selection else
  statement at cursor via quote/comment-aware splitter); pagination
  replays last executed statement
- query history persisted server-side: drawer fetches query-histories
  with keyword/mine/favorites filters, star toggle (new PUT favorite +
  mine filter endpoints), insert and insert-and-run
- custom templates in localStorage alongside built-ins
- unit tests: genSql + statement splitter (28 tests green)
2026-10-26 18:53:02 +08:00
Logan.Li ff539b47e0 feat(redis): shared key browser with grouped tree + cursor pagination
New src/features/redis/keyBrowser/ shared by RedisDetailPage keys tab and
SchemaSidebar redis branch (both previously flat one-shot lists):
- keyTree: ':'-separated folder tree with recursive key counts, numeric
  sort, exact-hit pinned at root, local key removal (no full refetch),
  type color dots, readable TTL (45s/5m30s/2d5h)
- useRedisKeyBrowser: cursor state machine + request-sequence guard +
  Map merge on load-more; setter-synced args (Enter right after typing
  no longer races stale render), auto-search on db/instance change
- RedisKeyBrowser: enter-to-scan search bar (SCAN has cost - no debounce,
  opsxdb-style) with clear button, effective type filter, load-more
  button with cursor hint, '已全部加载' end state
- useRedisDbs: real db list from /redis/:id/dbs (cluster -> DB0 banner)
- RedisDetailPage: hash row edits now use field-level HSET/HDEL endpoint
  (preserves other fields + TTL); delete/add update tree locally; CLI
  output renders arrays/objects/errors properly
- SchemaSidebar: redis branch swaps to shared browser (compact mode,
  inline detail preview + insert-command preserved), auto-switches to
  Keys tab for redis instances
- vite dev proxy target overridable via API_PROXY_TARGET (local testing)

Browser-verified: 200/page with load-more (200->400->600), exact hit
badge, keyword auto-expand, key detail panel, queries sidebar
2026-10-26 18:39:38 +08:00
Logan.Li 6604fdf48e feat(backend): redis key scan service - cursor pagination + cluster + CLI fix
Modeled on opsxdb's redis scan design:
- opaque base64 scan cursor {pattern,db,type,nodes,index,cursor}; server
  validates search conditions match, rejects stale cursors (400)
- soft page cap 200 keys (whole scan batches returned, no key loss),
  max 20 SCAN calls per page; next_cursor until all shards exhausted
- pattern rule: no glob chars -> *kw* contains match; non-glob input
  also probed with TYPE for exact hit (returned as exact, slot-routed
  so cluster/NAT safe)
- pipeline batch TYPE/TTL replaces per-key serial N+1; type filter now
  effective server-side
- cluster: per-master direct SCAN merged across shards (MasterAddrs on
  wrapper), DB0-only, MOVED errors hint at switching deploy mode;
  SlowLog/ClientList aggregate across masters (were cluster-broken)
- GET /redis/:id/dbs (0-15, cluster -> [0])
- POST /redis/:id/keys/:key/field: hash row-level HSET/HDEL (no whole-
  key rewrite)
- RedisExecuteCommand fixed (was always-403): accepts {db, command}
  full-string, quote-aware tokenizer, whitelist on first word, expanded
  read-only set
- RedisUpdateKey preserves previous TTL unless explicitly set/cleared
  (was silently dropping TTL); string type skips the DEL

Verified live against local redis: 200/page no overlap across pages,
cursor 400 on condition change, exact hit, type filter, hash field
edit round-trip, CLI DBSIZE/HGET ok, SET rejected
2026-10-26 18:27:11 +08:00
Logan.Li 6945c53879 fix(frontend): eliminate boot race between role restore and route guard
Root cause (pre-existing): user state initialized with default role
'developer' while JWT role restoration happened via render-phase setState
~1 render later; first guard pass evaluated tickets/workflow against the
developer writable-datasource gate and bounced admins to dashboard on
direct load.

Fix: initialize user state lazily from the JWT in one pass
(restoreUserFromToken) - first render already carries the correct role.
Verified in browser: direct loads of /tickets and /workflow stay put;
URL query state survives refresh; expired session redirects to
/login?redirect=<path> and login returns to the original page.
2026-10-26 18:06:22 +08:00
Logan.Li d867c276d4 chore(frontend): engineering baseline per guide ch.8
- toolchain: vitest+jsdom (15 tests green: permission pure functions
  exhaustive, storage migration), prettier (printWidth 120 to fit the
  dense existing style + 700-line page budget), commitlint conventional
- npm scripts: typecheck / format / test / lint:fix; build fingerprint
  __APP_VERSION__ (version+git hash+build time) shown in Settings > about
- git hooks via core.hooksPath=frontend/.husky: pre-commit lint-staged
  (prettier + eslint --fix best-effort), commit-msg commitlint
- permission pure functions extracted to lib/permissions.ts (single
  source consumed by useAuth/useLogin); AuditLogsPage filters on URL
- backend: resp package unit tests; health version constant aligned
  with frontend package.json
2026-10-26 17:58:59 +08:00
Logan.Li ac498c7ba2 refactor(frontend): split 7 oversized pages into feature co-location
Per guide ch.7 page split pattern (skeleton orchestrates, private
components live in features/<domain>/):
- InstancePage 1716->328, UsersPage 1309->240, TicketsPage 1076->195,
  TicketDetailPage 911->287, QueriesPage 883->479, LdapConfigPage 860->393,
  WorkflowPage 852->194
- new feature modules: datasource/ rbac/ ticket/ query/ system/ldap/
  governance/workflow/ (components + hooks + types + utils)
- stable keys replace index keys in dynamic lists; polling intervals
  and toast call sites preserved 1:1 (verified by agents)
- filter/pagination state moved to URL query via new useQueryState hook
  (Instance/Users/Tickets/AuditLogs pages): refresh/share/back-button
  restore state; composite updates applied atomically
- permission pure functions extracted to lib/permissions.ts (used by
  useAuth + useLogin); page-size audit: 0 pages over 700-line budget
2026-10-26 17:54:15 +08:00
Logan.Li ac563d1c5e refactor(frontend): unify theme tokens to single --dg-* source
- remove shadcn HSL dual-track vars (523->416->~300 lines index.css);
  tailwind semantic colors (border/background/card/primary/destructive/
  popover/muted/accent) now bridge directly to var(--dg-*) tokens
- drop dead darkMode:['class'] config and inert dark: variants in kept
  ui components; theme switching is data-theme based (4 themes intact)
- first-frame inline theme script in index.html (reads namespaced key
  with legacy fallback, resolves system, sets color-scheme) - no flash
- consolidate duplicated db-type brand colors into lib/constants.ts
  single source (dbTypeColorMap + dbTypeBrandColorMap, with pg/mongodb
  aliases); instance health stroke now uses theme tokens
- verified via browser: dark #0d1117 / light #f5f6f8 both resolve,
  tokens cascade correctly
2026-10-26 17:38:27 +08:00
Logan.Li a213d08e31 chore(frontend): purge dead code per guide red line
- delete 4 dead pages (~2300 lines): Home, ConfigPlatformPage,
  DataDictionaryPage, NotificationsPage (kept the live NotificationPage)
- delete 52 unused shadcn ui components (keep button/card/dialog in use;
  re-generate via shadcn CLI when needed)
- delete App.css, SkeletonCard, empty shared/ and features engine dirs
- remove dead theme blocks (midnight-purple / orange-vitality) from index.css
- uninstall ~40 dead deps (echarts, sonner, next-themes, zod,
  react-hook-form, unused radix packages, cmdk, vaul, ...)
- fix DEVELOPMENT.md drift: mock layer section rewritten to real API layer,
  permission docs now point to menu.ts single source
2026-10-26 17:23:24 +08:00
Logan.Li 16f1f7d65f feat(backend): unified response helpers, refresh token, security hardening
- new resp package (OK/Page/Fail/BadRequest/.../InternalErr): unified error
  envelope {detail, code?}, sanitized 500s (full error logged server-side,
  generic message to client - stops DB error leakage), pagination envelope
  {items,total,page,page_size}; migrate 200+ inline c.JSON error sites
- refresh token: 7d rotation tokens (SHA-256 hashed at rest), public
  POST /auth/refresh with single-use rotation, POST /auth/logout revokes
  all user refresh tokens (endpoint previously missing - frontend 404)
- security: gh-ost command line password masked in task status payload;
  Instance.MarshalJSON guarantees encrypted passwords never leave the API
  (removes reliance on per-handler manual masking); production (DEBUG=false)
  with default JWT secret now auto-generates and persists a random secret
- safe.Go recover wrapper for background goroutines (instance probe,
  gh-ost async)
- delete dead service layer (zero callers)
- fix pre-existing go vet unkeyed bson.E literals
2026-10-26 17:21:00 +08:00
Logan.Li d5b8b8116c fix(frontend): harden auth request layer per web guide
- fix double-unwrap bug in useLogin prefs sync (theme sync silently broken)
- request layer: 401 single-flight refresh queue + debounced single redirect
  with return-url, cancel-aware silent passthrough, skipErrorHandler opt-out,
  typed blob download (replace bare fetch export in QueriesPage)
- useFetchList: AbortController + version guard + error state (three states)
- ErrorBoundary with chunk-load auto recovery (reload once)
- permission single source: derive route guard from menu constants, drop
  hand-maintained routePermissionMap; TicketDetailPage uses unified
  hasActionPermission instead of inline role string comparison
- namespaced localStorage keys (opsxsql.*) with read-old-write-new migration
- fix pre-existing tsc errors (InstancePage/WorkflowPage/QueriesPage/
  PlatformConfigPage/RedisDetailPage) - typecheck now passes clean
2026-10-26 17:12:29 +08:00
hermes-$(hostname) 0fcdbc5f6c update 2026-07-08 08:53:48 +00:00
Logan.Li f5cf3f4440 fix: prevent query execute event state leak 2026-07-07 17:52:44 +08:00
Logan.Li 617c15dbfe feat: harden query read-only workflow 2026-07-07 17:20:44 +08:00
Logan.Li 58bbc394e9 feat: enhance redis and production IA 2026-07-07 10:39:33 +08:00
Logan 644981eef4 feat: improve datasource query audit workflows 2026-07-06 23:01:04 +08:00
logan 74748e4914 fix: 审批流程三个bug修复
1. 点通过不再重复创建审批记录(后端无NULL记录时返回400而非自动创建)
2. 非提交人不再显示撤销按钮(列表页增加applicant_id校验)
3. 已审批工单不再显示通过/拒绝按钮(is_approver只计算pending记录)
4. 前端审批失败时弹出错误提示而非静默忽略
2026-06-03 17:17:12 +08:00
logan 3a6dd99633 feat: 审核规则驱动SQL检查 + db_type过滤 + Redis只读拦截 + 配置文档
1. 审核规则改为从audit_rules表动态匹配(去硬编码)
   - CreateTicket提交时查audit_rules WHERE db_type=? AND is_enabled=true
   - 正则匹配SQL,value=reject拦截,warn/review放行
   - UPDATE/DELETE无WHERE: Go RE2不支持(?!...)负向前瞻
     → 用value=where_check特殊标记+strings.Contains WHERE检测
   - 修复UPDATE/DELETE无WHERE正则(Go兼容)

2. ListAuditRules按db_type过滤
   - ghost.go: 添加c.Query('db_type') WHERE过滤
   - MySQL不显示KEYS规则,Redis不显示DROP规则

3. Redis只读命令拦截(工单提交)
   - 新增Redis只读命令白名单: GET/HGET/MGET/HGETALL/KEYS/SCAN等28个
   - 只读命令走工单→直接拦截,引导用户用查询功能

4. 补充audit_rules规则
   - Redis新增7条: DEBUG/SHUTDOWN/SCRIPT/SAVE/BGSAVE/BGREWRITEAOF/SLAVEOF/CLUSTER
   - MySQL新增1条: DROP SCHEMA
   - 总计34条规则(MySQL12/Redis11/PG7/MongoDB4)

5. docs/CONFIGURATION.md 配置系统文档
   - 三套配置体系: system_settings/audit_rules/ldap_configs
   - 每项配置: key/默认值/作用/代码引用位置
   - 未引用配置标注: executor_batch_size/executor_backup/query_max_rows_examined(待实现)
   - LDAP冗余配置: system_settings中7项ldap_*无代码引用(LDAP用独立表)

验证: 5/5审批流通过, 36/36安全测试通过, 27/27审核规则匹配正确
2026-06-03 03:17:22 +08:00
logan 8fbcb8647d feat: SQL安全防护体系 — 危险SQL黑名单 + 事务回滚 + LIMIT修复
🔴 P0 安全漏洞修复:
- MySQL: 黑名单拦截 DROP DATABASE/DROP SCHEMA
- MySQL: 拦截无WHERE的UPDATE/DELETE(全表操作)
- MySQL: 多语句中含危险SQL也会被检测
- Redis: 拦截 FLUSHALL/FLUSHDB/CONFIG/DEBUG/SHUTDOWN/SCRIPT/SAVE/BGSAVE/KEYS/CLUSTER/REPLICAOF

🔄 事务支持:
- MySQL多语句执行包裹在事务中
- 任一SQL失败自动回滚全部已执行语句
- 验证通过: INSERT成功+UNIQUE冲突=全部回滚

🔧 Bug修复:
- query.go: LIMIT只追加SELECT语句(SHOW/CREATE等不再追加LIMIT报错)
- SHOW VARIABLES/STATUS 恢复正常

📊 测试验证:
- 29/29 安全拦截测试全部通过
- MySQL 8项危险操作全部拦截
- Redis 11项危险命令全部拦截
- 10项正常SQL全部放行
- 事务回滚验证通过
- 类型错误/参数错误正确报告
2026-06-03 02:49:07 +08:00
logan 8ceb249274 fix: MySQL DDL+DML同批EXPLAIN预检bug + Redis集群MOVED重定向 + 清晰错误提示
MySQL修复:
- 批次含DDL时跳过整个EXPLAIN预检(避免CREATE TABLE未执行时INSERT的EXPLAIN失败)
- 纯DML批次仍正常做EXPLAIN预检

Redis集群修复:
- 单节点客户端遇到MOVED时自动映射外部IP+端口重试
- 重试失败给出清晰提示(Docker端口未暴露)
- 新增 isClusterInstance/parseMovedTarget 辅助函数

测试验证通过:
- MySQL 5条DDL+DML ✅ (建表+INSERT+UPDATE+加索引)
- MySQL 纯DML+EXPLAIN预检 ✅
- MySQL dba(logan)执行 ✅
- Redis单机 SET写入 ✅
- Redis集群 ⚠️ Docker内部端口未暴露(基础设施限制)
2026-06-03 02:40:15 +08:00
logan 2984f11181 refactor: tests目录精简重构 + 脏数据清理脚本
- 删除16个旧Python测试文件(970行→400行)
- 重写conftest.py适配真实种子用户(admin/test123/test234/testtest/logan)
- 5个精简测试文件覆盖核心API:
  - test_auth.py: 登录/鉴权/种子用户验证 (6 tests)
  - test_instances.py: 实例列表/详情/连接测试 (6 tests)
  - test_tickets.py: 工单CRUD/只读SQL拦截/auditor权限 (7 tests)
  - test_workflow.py: E2E完整审批流/拒绝/撤销 (3 tests)
  - test_permissions.py: 角色权限/执行权限/workflow权限 (6 tests)
- 新增 cleanup_test_data.py 清理测试脏数据(支持--dry-run)
- 全量29测试3.27s通过
2026-06-03 02:24:40 +08:00
logan bec2cafec2 feat: 执行结果结构化-每条SQL记录status/affected_rows/duration_ms + 去掉工单详情优先级字段
后端:
- 新增 SqlExecItem/SqlExecResult 结构体
- MySQL/Redis/gh-ost 三条执行路径全部改为 JSON 结构化结果
- 每条 SQL 记录完整语句、状态、影响行数、耗时(ms)、错误信息
- 总耗时/开始结束时间一并记录

前端:
- ExecuteResult 接口匹配后端 SqlExecResult
- 表格展示每条 SQL 执行结果(序号/SQL/状态/影响行数/耗时/错误)
- 汇总卡片: 状态/总耗时/SQL数量
- 去掉工单详情中的优先级和变更原因字段
- 保留 plain text fallback 兼容旧数据

E2E验证: INSERT+UPDATE+ALTER 三语句全部成功, 结构化结果正确
2026-06-03 02:15:37 +08:00
logan 1364c737ef fix: 工单列表权限过滤-developer/auditor也能看到待审工单
- developer之前只能看applicant_id=自己的工单,导致审批人看不到待审工单
- 改为admin看全部,其他角色(包括developer/dba/auditor)都能看到自己提交+自己作为审批人的工单
- E2E全流程验证通过: testtest提交→test123初审→test234复审→admin执行→success
2026-06-03 02:06:20 +08:00
logan bd8100bfe0 fix: 撤销工单支持approved状态 + DDL跳过EXPLAIN预检 + 审批菜单仅admin/dba + 审批状态流转修复 + 权限组实例绑定修复 2026-06-03 01:53:21 +08:00
logan b1a3d74661 feat: 角色权限体系改造 — developer可审批, auditor纯只读
角色权限矩阵:
- admin: 全部权限
- dba: 数据源管理+工单全流程+审批+执行
- developer: 查询+提交工单+审批(不能执行)
- auditor: 只读(审计日志+审核规则+审批流程查看)

后端改动:
- ticket.go: 修复GetCurrentUser返回值顺序bug(userID,username,role)
- ticket.go: CreateTicket拦截auditor提交工单
- ticket.go: ApproveTicket允许admin/dba/developer
- ticket.go: ExecuteTicket仅允许admin/dba
- router.go: query执行限制admin/dba/developer
- router.go: workflow写操作限制admin/dba
- router.go: audit-rules写操作限制admin/dba

前端改动:
- menu.ts: tickets去掉auditor, workflow加developer+auditor, audit-logs加auditor
- useAuth.tsx: routePermissionMap和actionPermissionMap同步更新
- TicketDetailPage: canApprove加developer角色

DB迁移: test123/test234从auditor→developer
2026-06-03 01:19:24 +08:00
logan 97fd4ff069 fix: 数据变更工单拦截只读SQL — 前端提交/检测+后端创建三层拦截SELECT/SHOW/DESC/EXPLAIN 2026-06-03 00:48:09 +08:00
logan 70da8a8a8b fix: 工单执行按钮仅在审核通过(approved)状态显示,审核中不再显示执行按钮 2026-06-03 00:38:12 +08:00
logan bbf9522710 fix: 消息推送添加渠道placeholder改为数据变更消息机器人 2026-06-01 21:33:33 +08:00
logan 7564d92c69 fix: NotificationPage 添加渠道弹框 X 图标未 import 导致报错 2026-06-01 21:11:36 +08:00
logan 132b2db2d2 fix: 平台配置页面修复 - 隐藏LDAP重复分组、敏感字段列表textarea多行展示、分组排序优化
- 过滤ldap分组(有独立LDAP页面不重复展示)
- 敏感字段列表从单行input改为textarea每行一个字段名(key修正data_masking_sensitive_fields)
- 添加GROUP_ORDER固定展示顺序:查询控制→安全策略→数据脱敏→日志清理→执行器→Ghost/OSC
- 所有分组输入框均有默认值,cleanup组retention天数输入框正常显示
2026-06-01 21:07:44 +08:00