refactor: foms 改为纯业务模块,应用层迁至纯伞仓 foms_app

foms 只保留业务逻辑:
  · foms/ 包(init.py + failover.py 切换编排器)
  · models/(5 张表)、json/(CRUD 定义)、wwwroot/api/(端点)
  · scripts/(foms-agent 及部署脚本)
  · docs/deploy-operations.md

应用层迁出至 foms_app 纯伞仓:
  · app/(入口)、build.sh、start/stop.sh、conf/、init/data.yaml
  · scripts/init_rbac.py / load_path.py(重写为新 URL 布局)
  · wwwroot/index.ui(重写,指向各模块前缀路径)

同时修复 foms/__init__.py:移除对拆分时已删除函数的导出。
This commit is contained in:
yumoqing 2026-08-27 11:07:00 +08:00
parent fa257f36b1
commit dedb7bfc57
13 changed files with 49 additions and 758 deletions

12
.gitignore vendored
View File

@ -7,14 +7,6 @@ dist/
*.pid
logs/
files/
pkgs/
bricks/
py3/
models/mysql.ddl.sql
.DS_Store
.env
*.swp
*.swo
# CRUD-generated directories (xls2ui)
wwwroot/clusters_list/
wwwroot/nodes_list/
wwwroot/sync_databases_list/
wwwroot/sync_directories_list/

View File

@ -1,61 +1,41 @@
# FOMS — Failover Management System
# foms — 主备切换核心模块
主备机切换管理系统。运行在管理中心 C 上,管理主机 A 和备机 B 之间的:
FOMS 的**领域核心**(业务逻辑层),独立可安装模块。
应用壳(构建/配置/启动/首页)在纯伞仓 **foms_app**
- **MySQL 主从复制** — 实时数据同步
- **文件目录同步** — rsync 定期同步
- **健康监控** — 节点心跳 + 复制状态
- **故障切换** — 自动检测 A 故障 → 切换到 B
- **故障切回** — A 修复后 → 切回 A
## 内容
## 架构
| 部分 | 文件 | 说明 |
|---|---|---|
| 包入口 | `foms/init.py` | 集群/节点 CRUD、心跳、仪表盘、远程命令队列、`load_foms()` |
| 切换编排器 | `foms/failover.py` | 六步状态机 + 逆向回滚 + 防脑裂 + 健康判定器 |
| 表定义 | `models/*.json` | clusters、nodes、switchover_logs、switchover_steps、node_commands |
| CRUD 定义 | `json/*.json` | clusters_list、nodes_list |
| 端点 | `wwwroot/api/*.dspy` | 心跳、命令、切换、仪表盘等 |
| Agent | `scripts/foms-agent.py` | 被管主机守护进程(独立部署,非本包代码) |
| 运维文档 | `docs/deploy-operations.md` | 部署 + 运维手册 |
## 切换闭环2026-08-26 补全)
健康判定器(防抖告警,半自动)→ 人工确认 → 六步状态机:
```
管理中心 C (FOMS Web App)
├── 主机 A ←──MySQL复制/rsync──→ 备机 B
└── Agent 守护进程 (部署在 A & B)
precheck → fence → promote → flip_sync → verify → converge
```
失败逆向回滚防脑裂不变量fence 先于 promotefence 失败硬中止。
详见 `docs/deploy-operations.md`
## 快速开始
## 安装
```bash
# 1. 构建
./build.sh
# 2. 配置 .env
vim .env
# 3. 初始化数据库 (手动执行 models/mysql.ddl.sql)
mysql -u root < models/mysql.ddl.sql
# 4. 注册RBAC权限
cd py3/bin && python ../../scripts/load_path.py
# 5. 启动
./start.sh
pip install -e .
```
由 foms_app 的 build.sh 统一组装。`load_foms()` 时自动注册切换编排器
并拉起健康判定器后台任务(幂等)。
## 模块
## 邻接模块(独立仓库)
| 模块 | 路径 | 说明 |
|------|------|------|
| 集群管理 | /clusters_list | 配置主备集群 |
| 节点管理 | /nodes_list | 注册A/B节点(SSH信息) |
| 数据库同步 | /sync_databases_list | MySQL主从复制配置 |
| 文件同步 | /sync_directories_list | rsync目录同步配置 |
- dbbackup — 数据库备份 + 日志同步binlog 复制)
- filesync — 运行文件同步rsync
- hostwatch — 日志监控 + 日志分析 + 主机指标
## Agent部署
在主机A和备机B上
```bash
# 配置环境变量
export FOMS_MANAGEMENT_URL=http://C的IP:9080
export FOMS_NODE_ID=<节点ID>
export FOMS_AGENT_TOKEN=<安全令牌>
# 运行Agent
python3 scripts/foms-agent.py
```
仪表盘对这三模块的表做同库只读聚合(松耦合)。

View File

@ -1,38 +0,0 @@
#!/usr/bin/env python3
"""FOMS — Failover Management System 主入口"""
import os, sys
# Add root so sibling modules are importable
root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, root_dir)
from bricks_for_python.init import load_pybricks
from ahserver.webapp import webapp
from ahserver.serverenv import ServerEnv
# Foundation modules
from rbac.init import load_rbac
from appbase.init import load_appbase
# Business modules核心 + 三个拆出的独立模块)
from foms.init import load_foms
from dbbackup.init import load_dbbackup
from filesync.init import load_filesync
from hostwatch.init import load_hostwatch
from app.global_func import set_globalvariable
def init():
set_globalvariable()
load_pybricks()
load_appbase()
load_rbac()
load_foms() # 核心:集群/节点/切换/心跳/远程命令
load_dbbackup() # 数据库备份 + 日志同步binlog 复制)
load_filesync() # 运行文件同步rsync
load_hostwatch() # 日志监控 + 日志分析 + 主机指标
if __name__ == '__main__':
webapp(init)

View File

@ -1,12 +0,0 @@
"""FOMS global functions — injected into dspy context"""
from ahserver.serverenv import ServerEnv
def get_module_dbname(mname):
"""All FOMS modules share one database."""
return 'foms'
def set_globalvariable():
env = ServerEnv()
env.get_module_dbname = get_module_dbname

View File

@ -1,78 +0,0 @@
#!/usr/bin/env bash
set -e
cdir=$(pwd)
echo "=== FOMS Build ==="
# 1. Create venv
[ ! -d py3 ] && python3 -m venv py3
source py3/bin/activate
# 2. Install foundation packages
mkdir -p pkgs
for m in apppublic sqlor ahserver bricks-for-python xls2ddl; do
cd $cdir/pkgs
[ ! -d "$m" ] && git clone https://git.opencomputing.cn/yumoqing/$m
cd $m && $cdir/py3/bin/pip install -e .
done
# 3. Build bricks frontend
cd $cdir/pkgs
[ ! -d "bricks" ] && git clone https://git.opencomputing.cn/yumoqing/bricks
cd bricks/bricks && ./build.sh
ln -sf $cdir/pkgs/bricks/dist $cdir/bricks
# 4. Install auth modules
for m in appbase rbac; do
cd $cdir/pkgs
[ ! -d "$m" ] && git clone https://git.opencomputing.cn/yumoqing/$m
cd $m && $cdir/py3/bin/pip install -e .
done
# 5. Install FOMS 核心
cd $cdir && $cdir/py3/bin/pip install -e .
# 5.5 Install 业务模块dbbackup / filesync / hostwatch
for m in dbbackup filesync hostwatch; do
cd $cdir/pkgs
[ ! -d "$m" ] && git clone https://git.opencomputing.cn/yumoqing/$m
cd $m && $cdir/py3/bin/pip install -e .
# 生成模块 DDL
if [ -d models ]; then
json2ddl mysql models/ > models/mysql.ddl.sql 2>/dev/null || echo " skip ddl($m)"
fi
done
# 6. Generate FOMS 核心 DDL
cd $cdir
[ -d models ] && json2ddl mysql models/ > $cdir/models/mysql.ddl.sql 2>/dev/null || echo " skip ddl"
# 7. Generate FOMS 核心 CRUD UI (xls2ui for json/)
cd $cdir
if [ -d json ]; then
cd json
for f in *.json; do
echo " xls2ui foms $f"
xls2ui -m ../models -o ../wwwroot foms "$f" 2>/dev/null || echo " skipped"
done
fi
# 7.5 生成模块 CRUD + 软链模块 wwwroot → 主 wwwroot/<模块名>
cd $cdir
for m in dbbackup filesync hostwatch; do
md=$cdir/pkgs/$m
# 生成模块 CRUD 页面
if [ -d "$md/json" ]; then
cd "$md/json"
for f in *.json; do
echo " xls2ui $m $f"
xls2ui -m ../models -o ../wwwroot "$m" "$f" 2>/dev/null || echo " skipped"
done
fi
# 软链模块 wwwroot → 主 wwwroot/<模块名>URL 前缀 /dbbackup/ 等)
ln -sfn "$md/wwwroot" "$cdir/wwwroot/$m"
done
# 8. Create runtime dirs
mkdir -p $cdir/logs $cdir/files
echo "=== FOMS Build Complete ==="

View File

@ -1,42 +0,0 @@
{
"password_key": "foms_secret_key_change_me",
"logger": {
"name": "foms",
"level": "INFO",
"file": "$[workdir]$/logs/foms.log"
},
"filesroot": "$[workdir]$/files",
"databases": {
"foms": {
"driver": "aiomysql",
"kwargs": {
"host": "127.0.0.1",
"port": 3306,
"user": "root",
"password": "",
"db": "foms",
"charset": "utf8mb4",
"autocommit": true
}
}
},
"website": {
"paths": [
["$[workdir]$/wwwroot", ""],
["$[workdir]$/bricks", "/bricks"]
],
"port": 9080,
"indexes": ["index.ui"],
"session_max_time": 86400,
"session_issue_time": 3600,
"processors": [
[".dspy", "dspy"],
[".ui", "bui"],
[".tmpl", "tmpl"],
[".js", "staticfile"],
[".css", "staticfile"],
[".png", "staticfile"],
[".svg", "staticfile"]
]
}
}

View File

@ -1,12 +1,26 @@
"""foms — 主备切换核心业务模块FOMS 的领域核心)。
独立可安装模块由纯伞仓 foms_app 组装
内容
· clustersnodesswitchover_logsswitchover_stepsnode_commands
· 集群/节点 CRUDAgent 心跳仪表盘远程命令队列
· 切换编排器六步状态机 + 逆向回滚 + 防脑裂 failover.py
· 健康判定器后台循环 + 防抖 + 半自动告警
· scripts/foms-agent.py被管主机守护进程非本包代码独立部署
邻接能力已拆为独立模块不在本包内
· dbbackup数据库备份 + 日志同步filesync文件同步hostwatch日志监控分析
"""
from .init import (
load_foms,
create_cluster, update_cluster, delete_cluster,
create_node, update_node, delete_node,
create_sync_db, update_sync_db, delete_sync_db,
create_sync_dir, update_sync_dir, delete_sync_dir,
agent_heartbeat, agent_report_health,
agent_heartbeat,
trigger_switchover, trigger_switchback,
get_dashboard_stats,
agent_report_metrics, agent_push_logs,
get_node_detail, get_node_metrics_history,
exec_remote_command, agent_poll_commands, agent_report_command_result,
)
from .failover import (
confirm_switchover, run_switchover, health_judge_loop, register_failover,
)

View File

@ -1,171 +0,0 @@
appcodes:
- id: cluster_status
name: 集群状态
hierarchy_flg: 0
- id: node_role
name: 节点角色
hierarchy_flg: 0
- id: node_status
name: 节点状态
hierarchy_flg: 0
- id: sync_status
name: 同步状态
hierarchy_flg: 0
- id: sync_result
name: 同步结果
hierarchy_flg: 0
- id: switchover_event_type
name: 切换事件类型
hierarchy_flg: 0
- id: switchover_trigger
name: 触发方式
hierarchy_flg: 0
- id: switchover_status
name: 切换执行状态
hierarchy_flg: 0
- id: org_type
name: 机构类型
hierarchy_flg: 0
- id: user_status
name: 用户状态
hierarchy_flg: 0
- id: log_level
name: 日志级别
hierarchy_flg: 0
appcodes_kv:
- id: cluster_status_running
parentid: cluster_status
k: running
v: 运行中
- id: cluster_status_stopped
parentid: cluster_status
k: stopped
v: 已停止
- id: cluster_status_switching
parentid: cluster_status
k: switching
v: 切换中
- id: cluster_status_error
parentid: cluster_status
k: error
v: 异常
- id: node_role_master
parentid: node_role
k: master
v: 主机
- id: node_role_standby
parentid: node_role
k: standby
v: 备机
- id: node_status_online
parentid: node_status
k: online
v: 在线
- id: node_status_offline
parentid: node_status
k: offline
v: 离线
- id: node_status_error
parentid: node_status
k: error
v: 异常
- id: sync_status_not_configured
parentid: sync_status
k: not_configured
v: 未配置
- id: sync_status_running
parentid: sync_status
k: running
v: 同步中
- id: sync_status_error
parentid: sync_status
k: error
v: 异常
- id: sync_status_failed
parentid: sync_status
k: failed
v: 失败
- id: sync_result_success
parentid: sync_result
k: success
v: 成功
- id: sync_result_failed
parentid: sync_result
k: failed
v: 失败
- id: sync_result_running
parentid: sync_result
k: running
v: 同步中
- id: switchover_event_failover
parentid: switchover_event_type
k: failover
v: 故障切换
- id: switchover_event_failback
parentid: switchover_event_type
k: failback
v: 故障切回
- id: switchover_event_manual
parentid: switchover_event_type
k: manual
v: 手动操作
- id: switchover_trigger_auto
parentid: switchover_trigger
k: auto
v: 自动检测
- id: switchover_trigger_manual
parentid: switchover_trigger
k: manual
v: 手动触发
- id: switchover_status_pending
parentid: switchover_status
k: pending
v: 等待执行
- id: switchover_status_in_progress
parentid: switchover_status
k: in_progress
v: 执行中
- id: switchover_status_success
parentid: switchover_status
k: success
v: 成功
- id: switchover_status_failed
parentid: switchover_status
k: failed
v: 失败
- id: org_type_foms
parentid: org_type
k: foms_org
v: FOMS运维组织
- id: user_status_active
parentid: user_status
k: 0
v: 正常
- id: user_status_disabled
parentid: user_status
k: 1
v: 停用
- id: log_level_info
parentid: log_level
k: INFO
v: 信息
- id: log_level_warn
parentid: log_level
k: WARN
v: 警告
- id: log_level_error
parentid: log_level
k: ERROR
v: 错误
- id: log_level_crit
parentid: log_level
k: CRIT
v: 严重

View File

@ -1,155 +0,0 @@
#!/usr/bin/env python3
"""
FOMS RBAC 初始化 创建机构类型机构角色用户
用法: cd /d/ymq/repos/foms && source py3/bin/activate && python scripts/init_rbac.py
"""
import os, sys, asyncio
# Add project root
root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, root)
from appPublic.uniqueID import getID
from appPublic.timeUtils import curDateString
from sqlor.dbpools import DBPools
from ahserver.serverenv import ServerEnv
DBNAME = 'foms'
ORG_ID = 'foms_org_001'
ROLES = {
'foms_superadmin': '超级管理员',
'foms_operator': '运维操作员',
'foms_viewer': '只读观察者',
}
USERS = {
'admin': {'name': '系统管理员', 'password': 'Admin@123', 'role': 'foms_superadmin'},
'operator': {'name': '运维操作员', 'password': 'Ops@123456', 'role': 'foms_operator'},
}
async def main():
env = ServerEnv()
dbname = env.get_module_dbname('rbac') if hasattr(env, 'get_module_dbname') else DBNAME
db = DBPools()
async with db.sqlorContext(dbname) as sor:
# 1. Create organization
existing = await sor.R('organization', {'id': ORG_ID})
if existing:
print(f"[SKIP] organization {ORG_ID} already exists")
else:
await sor.C('organization', {
'id': ORG_ID,
'orgname': 'FOMS运维中心',
'orgabbr': 'FOMS',
'org_type': 'foms_org',
})
print(f"[OK] organization created: FOMS运维中心")
# 2. Create roles
for role_id, role_name in ROLES.items():
existing = await sor.R('role', {'id': role_id})
if existing:
print(f"[SKIP] role {role_id} exists")
continue
await sor.C('role', {
'id': role_id,
'orgtypeid': 'foms_org',
'name': role_name,
})
print(f"[OK] role created: {role_id} ({role_name})")
# 3. Create users
for username, info in USERS.items():
uid = f'user_{username}'
existing = await sor.R('users', {'username': username})
if existing:
print(f"[SKIP] user {username} exists")
else:
encoded_pw = env.password_encode(info['password'])
await sor.C('users', {
'id': uid,
'username': username,
'name': info['name'],
'nick_name': info['name'],
'password': encoded_pw,
'orgid': ORG_ID,
'user_status': '0',
'created_at': curDateString(),
})
print(f"[OK] user created: {username}")
# 4. Assign role to user
ur_id = f'ur_{username}_{info["role"]}'
existing_ur = await sor.R('userrole', {'userid': uid, 'roleid': info['role']})
if existing_ur:
print(f"[SKIP] userrole {username}{info['role']} exists")
continue
await sor.C('userrole', {
'id': ur_id,
'userid': uid,
'roleid': info['role'],
})
print(f"[OK] userrole: {username}{info['role']}")
# 5. Assign permissions to roles
# Get all FOMS-specific permissions (paths not from rbac/appbase)
all_perms = await sor.sqlExe(
"SELECT id, path FROM permission WHERE path IN ("
"SELECT path FROM permission WHERE path LIKE '/api/%' OR path LIKE '/%_list%'"
") ORDER BY path", {}
)
# Most paths start with / for FOMS root-mapped module
foms_perms = [p for p in all_perms if hasattr(p, 'path')]
if not foms_perms:
print("[WARN] No FOMS permissions found — run scripts/load_path.py first")
else:
# superadmin: all FOMS paths
# operator: all paths (can trigger switchover)
# viewer: read-only (no create/update/delete)
for perm in foms_perms:
pid = getattr(perm, 'id', '')
path = getattr(perm, 'path', '')
is_write = any(x in path for x in ['create', 'update', 'delete', 'trigger'])
is_agent = 'agent_' in path
# superadmin gets everything
await _add_roleperm(sor, 'foms_superadmin', pid, path)
# operator gets everything except agent paths (internal)
if not is_agent:
await _add_roleperm(sor, 'foms_operator', pid, path)
# viewer gets read-only
if not is_write and not is_agent:
await _add_roleperm(sor, 'foms_viewer', pid, path)
print(f"\n=== FOMS RBAC 初始化完成 ===")
print(f" 机构: FOMS运维中心 ({ORG_ID})")
print(f" 角色: {', '.join(f'{k}({v})' for k, v in ROLES.items())}")
print(f" 用户:")
for u, i in USERS.items():
print(f" {u} / {i['password']} (角色: {i['role']})")
print(f"\n 角色权限分配:")
print(f" superadmin → 全部路径")
print(f" operator → 全部路径 (不含agent内部端点)")
print(f" viewer → 只读路径 (查看dashboard/列表)")
async def _add_roleperm(sor, role_id, perm_id, path):
"""Add rolepermission if not exists"""
rp_id = f'rp_{role_id}_{perm_id}'[:32]
existing = await sor.R('rolepermission', {'roleid': role_id, 'permid': perm_id})
if existing:
return
await sor.C('rolepermission', {
'id': rp_id,
'roleid': role_id,
'permid': perm_id,
})
if __name__ == '__main__':
asyncio.run(main())

View File

@ -1,97 +0,0 @@
#!/usr/bin/env python3
"""FOMS RBAC permission registration"""
import os, sys
# Find Sage root for set_role_perm.py
sage_root = None
for c in [os.path.expanduser("~/repos/sage"), os.path.expanduser("~/test/sage")]:
if os.path.isdir(os.path.join(c, "py3", "bin")):
sage_root = c
break
if not sage_root:
print("ERROR: Sage root not found")
sys.exit(1)
sys.path.insert(0, os.path.join(sage_root, "py3", "bin"))
from set_role_perm import set_path_perm
# Paths accessible without login
PATHS_ANY = [
"/",
"/index.ui",
"/api/login.dspy",
]
# Paths requiring authentication
PATHS_LOGINED = [
"/api/clusters_create.dspy",
"/api/clusters_update.dspy",
"/api/clusters_delete.dspy",
"/api/nodes_create.dspy",
"/api/nodes_update.dspy",
"/api/nodes_delete.dspy",
"/api/sync_databases_create.dspy",
"/api/sync_databases_update.dspy",
"/api/sync_databases_delete.dspy",
"/api/sync_directories_create.dspy",
"/api/sync_directories_update.dspy",
"/api/sync_directories_delete.dspy",
"/api/agent_heartbeat.dspy",
"/api/agent_report_health.dspy",
"/api/trigger_switchover.dspy",
"/api/trigger_switchback.dspy",
"/api/dashboard_stats.dspy",
# CRUD list pages
"/clusters_list",
"/clusters_list/index.ui",
"/nodes_list",
"/nodes_list/index.ui",
"/sync_databases_list",
"/sync_databases_list/index.ui",
"/sync_directories_list",
"/sync_directories_list/index.ui",
# CRUD generated files
"/clusters_list/get_clusters_list.dspy",
"/clusters_list/add_clusters_list.dspy",
"/clusters_list/update_clusters_list.dspy",
"/clusters_list/delete_clusters_list.dspy",
"/nodes_list/get_nodes_list.dspy",
"/nodes_list/add_nodes_list.dspy",
"/nodes_list/update_nodes_list.dspy",
"/nodes_list/delete_nodes_list.dspy",
"/sync_databases_list/get_sync_databases_list.dspy",
"/sync_databases_list/add_sync_databases_list.dspy",
"/sync_databases_list/update_sync_databases_list.dspy",
"/sync_databases_list/delete_sync_databases_list.dspy",
"/sync_directories_list/get_sync_directories_list.dspy",
"/sync_directories_list/add_sync_directories_list.dspy",
"/sync_directories_list/update_sync_directories_list.dspy",
"/sync_directories_list/delete_sync_directories_list.dspy",
# Metrics & logs
"/api/agent_report_metrics.dspy",
"/api/agent_push_logs.dspy",
"/api/node_detail.dspy",
"/api/node_metrics_history.dspy",
# Remote command
"/api/exec_remote_command.dspy",
"/api/agent_poll_commands.dspy",
"/api/agent_report_command_result.dspy",
]
# Agent heartbeat endpoint: no auth (agents use token)
PATHS_ANY.append("/api/agent_heartbeat.dspy")
PATHS_ANY.append("/api/agent_report_health.dspy")
PATHS_ANY.append("/api/agent_report_metrics.dspy")
PATHS_ANY.append("/api/agent_push_logs.dspy")
PATHS_ANY.append("/api/agent_poll_commands.dspy")
PATHS_ANY.append("/api/agent_report_command_result.dspy")
if __name__ == '__main__':
for p in PATHS_ANY:
set_path_perm("any", p)
print(f" any {p}")
for p in PATHS_LOGINED:
set_path_perm("logined", p)
print(f" logined {p}")
print("Done. Restart FOMS to apply.")

View File

@ -1,6 +0,0 @@
#!/usr/bin/env bash
cd "$(dirname "$0")"
[ -f .env ] && source .env
nohup $PWD/py3/bin/python $PWD/app/foms.py -p ${FOMS_PORT:-9080} -w $PWD > $PWD/logs/foms.out 2>&1 &
echo $! > foms.pid
echo "FOMS started (PID: $(cat foms.pid)) on port ${FOMS_PORT:-9080}"

View File

@ -1,9 +0,0 @@
#!/usr/bin/env bash
cd "$(dirname "$0")"
if [ -f foms.pid ]; then
pid=$(cat foms.pid)
kill $pid 2>/dev/null && echo "FOMS stopped (PID: $pid)" || echo "FOMS not running"
rm -f foms.pid
else
echo "No PID file found"
fi

View File

@ -1,87 +0,0 @@
{
"widgettype": "VBox",
"options": {"width": "100%", "height": "100%", "padding": "24px", "backgroundColor": "#F5F7FA"},
"subwidgets": [
{
"widgettype": "Text",
"options": {"text": "FOMS — 主备切换管理系统", "fontSize": "28px", "fontWeight": "bold", "marginBottom": "8px"}
},
{
"widgettype": "Text",
"options": {"text": "Failover Management System · 实时监控 · 自动切换 · 数据同步", "fontSize": "14px", "color": "#666", "marginBottom": "24px"}
},
{
"widgettype": "ResponsableBox",
"options": {"gap": "16px", "minWidth": "220px", "marginBottom": "24px"},
"subwidgets": [
{
"widgettype": "VBox",
"options": {"backgroundColor": "#FFFFFF", "padding": "20px", "borderRadius": "8px", "cursor": "pointer"},
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.foms_content", "options": {"url": "{{entire_url('clusters_list')}}"}, "mode": "replace"}],
"subwidgets": [
{"widgettype": "Text", "options": {"text": "集群管理", "fontSize": "16px", "fontWeight": "bold", "marginBottom": "8px"}},
{"widgettype": "RefreshWidget", "options": {"interval": 30000, "data_url": "{{entire_url('api/dashboard_stats.dspy')}}"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "{{data.total_clusters}} 个集群", "fontSize": "14px", "color": "#666"}}
]
}
]
},
{
"widgettype": "VBox",
"options": {"backgroundColor": "#FFFFFF", "padding": "20px", "borderRadius": "8px", "cursor": "pointer"},
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.foms_content", "options": {"url": "{{entire_url('nodes_list')}}"}, "mode": "replace"}],
"subwidgets": [
{"widgettype": "Text", "options": {"text": "节点管理", "fontSize": "16px", "fontWeight": "bold", "marginBottom": "8px"}},
{"widgettype": "RefreshWidget", "options": {"interval": 30000, "data_url": "{{entire_url('api/dashboard_stats.dspy')}}"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "{{data.nodes_online}} 在线 / {{data.nodes_offline}} 离线", "fontSize": "14px", "color": "#666"}}
]
}
]
},
{
"widgettype": "VBox",
"options": {"backgroundColor": "#FFFFFF", "padding": "20px", "borderRadius": "8px", "cursor": "pointer"},
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.foms_content", "options": {"url": "{{entire_url('sync_databases_list')}}"}, "mode": "replace"}],
"subwidgets": [
{"widgettype": "Text", "options": {"text": "数据库同步", "fontSize": "16px", "fontWeight": "bold", "marginBottom": "8px"}},
{"widgettype": "RefreshWidget", "options": {"interval": 30000, "data_url": "{{entire_url('api/dashboard_stats.dspy')}}"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "{{data.dbs_synced}} 正常 / {{data.dbs_error}} 异常", "fontSize": "14px", "color": "#666"}}
]
}
]
},
{
"widgettype": "VBox",
"options": {"backgroundColor": "#FFFFFF", "padding": "20px", "borderRadius": "8px", "cursor": "pointer"},
"binds": [{"wid": "self", "event": "click", "actiontype": "urlwidget", "target": "app.foms_content", "options": {"url": "{{entire_url('sync_directories_list')}}"}, "mode": "replace"}],
"subwidgets": [
{"widgettype": "Text", "options": {"text": "文件同步", "fontSize": "16px", "fontWeight": "bold", "marginBottom": "8px"}},
{"widgettype": "RefreshWidget", "options": {"interval": 30000, "data_url": "{{entire_url('api/dashboard_stats.dspy')}}"},
"subwidgets": [
{"widgettype": "Text", "options": {"text": "{{data.dirs_synced}} 同步正常", "fontSize": "14px", "color": "#666"}}
]
}
]
}
]
},
{
"widgettype": "VBox",
"id": "foms_content",
"options": {"width": "100%", "flex": "1", "backgroundColor": "#FFFFFF", "borderRadius": "8px", "padding": "20px", "minHeight": "400px"},
"subwidgets": [
{
"widgettype": "Text",
"options": {"text": "欢迎使用 FOMS 主备切换管理系统", "fontSize": "18px", "color": "#999", "textAlign": "center", "marginTop": "80px"}
},
{
"widgettype": "Text",
"options": {"text": "点击上方卡片进入各功能模块", "fontSize": "14px", "color": "#BBB", "textAlign": "center"}
}
]
}
]
}