Compare commits
2 Commits
0d83bcc19f
...
b5b59f1c3b
| Author | SHA1 | Date |
|---|---|---|
|
|
b5b59f1c3b | |
|
|
ba7f181501 |
18
CHANGELOG.md
18
CHANGELOG.md
|
|
@ -1,5 +1,23 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v2026.07.02
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- **用户管理页面**:用户列表、角色编辑(admin/editor/viewer),API 权限守卫
|
||||||
|
- **角色权限管理页面**:11 个权限的开关式管理,admin/editor/viewer 三角色独立配置
|
||||||
|
- **审计日志页面**:分页、筛选(操作类型/对象类型)、真实数据展示
|
||||||
|
- **容器操作管理**:服务管理页面增加重启/停止/查看日志功能,通过 SSH 执行 Docker 命令,安全边界(不挂载 rw docker.sock),所有操作记录审计日志
|
||||||
|
- **服务 CRUD 表单**:完整的添加/编辑/删除服务对话框,支持 HTTP/Docker 多维度检查配置
|
||||||
|
- **仪表盘路由**:`/` 重定向到 `/dashboard`
|
||||||
|
|
||||||
|
### 优化
|
||||||
|
|
||||||
|
- **设计规范更新**:`tlyq-design-system.md` 新增「光标系统」章节,所有可交互非按钮/链接元素添加 `cursor-pointer`
|
||||||
|
- **服务表单改进**:新增服务默认包含一条检查方式,至少保留一条检查不允许删除
|
||||||
|
- **用户管理**:表格显示用户名/显示名/邮箱/角色/状态,支持内联编辑角色
|
||||||
|
- **审计日志**:支持按操作类型筛选(登录/创建/更新/删除等),按对象类型筛选(认证/服务/告警渠道/用户/角色)
|
||||||
|
|
||||||
## v2026.07.01
|
## v2026.07.01
|
||||||
|
|
||||||
### 新增
|
### 新增
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,7 @@ export default function RolesPage() {
|
||||||
<span className="text-sm text-slate-400">admin 角色拥有全部 {allPermissions.length} 个权限</span>
|
<span className="text-sm text-slate-400">admin 角色拥有全部 {allPermissions.length} 个权限</span>
|
||||||
) : editing === role.role ? (
|
) : editing === role.role ? (
|
||||||
allPermissions.map(p => (
|
allPermissions.map(p => (
|
||||||
<label key={p} className="flex items-center gap-2 text-sm py-1">
|
<label key={p} className="flex items-center gap-2 text-sm py-1 cursor-pointer">
|
||||||
<input type="checkbox" checked={perms.includes(p)} onChange={() => togglePerm(p)}
|
<input type="checkbox" checked={perms.includes(p)} onChange={() => togglePerm(p)}
|
||||||
className="rounded border-slate-300 text-indigo-600" />
|
className="rounded border-slate-300 text-indigo-600" />
|
||||||
{permLabels[p] || p}
|
{permLabels[p] || p}
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ export default function DashboardPage() {
|
||||||
{services.map((s) => {
|
{services.map((s) => {
|
||||||
const isAbnormal = s.current_status === 'abnormal'
|
const isAbnormal = s.current_status === 'abnormal'
|
||||||
return (
|
return (
|
||||||
<div key={String(s.id)} className={`rounded-xl p-5 border transition-all ${isAbnormal ? 'bg-red-50 dark:bg-red-500/10 border-red-600 dark:border-red-400' : 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 hover:shadow-sm'}`}>
|
<div key={String(s.id)} className={`rounded-xl p-5 border transition-all cursor-pointer ${isAbnormal ? 'bg-red-50 dark:bg-red-500/10 border-red-600 dark:border-red-400' : 'bg-white dark:bg-slate-900 border-slate-200 dark:border-slate-800 hover:shadow-sm'}`}>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="flex items-center gap-2 text-sm font-semibold">
|
<span className="flex items-center gap-2 text-sm font-semibold">
|
||||||
<span className={`w-2.5 h-2.5 rounded-full ${isAbnormal ? 'bg-red-600 shadow-[0_0_8px_var(--danger)]' : 'bg-emerald-600'}`} />
|
<span className={`w-2.5 h-2.5 rounded-full ${isAbnormal ? 'bg-red-600 shadow-[0_0_8px_var(--danger)]' : 'bg-emerald-600'}`} />
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { Plus, RotateCw, Square, ScrollText, X, Save, Trash2, Edit3 } from 'luci
|
||||||
interface Check { type: string; containerName?: string; url?: string }
|
interface Check { type: string; containerName?: string; url?: string }
|
||||||
interface Service { id: number; name: string; category: string; alert_level: string; checks: Check[]; check_interval: number; enabled: number; current_status: string }
|
interface Service { id: number; name: string; category: string; alert_level: string; checks: Check[]; check_interval: number; enabled: number; current_status: string }
|
||||||
|
|
||||||
const emptyService = { name: '', category: 'endpoint', alert_level: 'warning', checks: [] as Check[], check_interval: 30, check_timeout: 10, enabled: 1 }
|
const emptyService = { name: '', category: 'endpoint', alert_level: 'warning', checks: [{ type: 'http', url: '' }] as Check[], check_interval: 30, check_timeout: 10, enabled: 1 }
|
||||||
|
|
||||||
export default function ServicesPage() {
|
export default function ServicesPage() {
|
||||||
const [services, setServices] = useState<Service[]>([])
|
const [services, setServices] = useState<Service[]>([])
|
||||||
|
|
@ -63,6 +63,7 @@ export default function ServicesPage() {
|
||||||
}
|
}
|
||||||
const removeCheck = (i: number) => {
|
const removeCheck = (i: number) => {
|
||||||
const checks = [...((editing?.checks as Check[]) || [])]
|
const checks = [...((editing?.checks as Check[]) || [])]
|
||||||
|
if (checks.length <= 1) return // 至少保留一条检查方式
|
||||||
checks.splice(i, 1)
|
checks.splice(i, 1)
|
||||||
update('checks', checks)
|
update('checks', checks)
|
||||||
}
|
}
|
||||||
|
|
@ -247,7 +248,9 @@ export default function ServicesPage() {
|
||||||
<input value={c.containerName || ''} onChange={e => updateCheck(i, 'containerName', e.target.value)} placeholder="容器名,如 authelia"
|
<input value={c.containerName || ''} onChange={e => updateCheck(i, 'containerName', e.target.value)} placeholder="容器名,如 authelia"
|
||||||
className="flex-1 px-2 py-1.5 rounded border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-xs" />
|
className="flex-1 px-2 py-1.5 rounded border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-xs" />
|
||||||
)}
|
)}
|
||||||
<button onClick={() => removeCheck(i)} className="text-red-400 hover:text-red-600 p-1"><X size={14} /></button>
|
{((editing.checks as Check[]) || []).length > 1 && (
|
||||||
|
<button onClick={() => removeCheck(i)} className="text-red-400 hover:text-red-600 p-1"><X size={14} /></button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@ export default function SettingsPage() {
|
||||||
{ key: 'level_warning', label: 'Warning' },
|
{ key: 'level_warning', label: 'Warning' },
|
||||||
{ key: 'level_info', label: 'Info' },
|
{ key: 'level_info', label: 'Info' },
|
||||||
].map(l => (
|
].map(l => (
|
||||||
<label key={l.key} className="flex items-center gap-2 text-sm">
|
<label key={l.key} className="flex items-center gap-2 text-sm cursor-pointer">
|
||||||
<input type="checkbox" checked={!!(editing as Record<string, unknown>)[l.key]} onChange={e => updateField(l.key, e.target.checked ? 1 : 0)}
|
<input type="checkbox" checked={!!(editing as Record<string, unknown>)[l.key]} onChange={e => updateField(l.key, e.target.checked ? 1 : 0)}
|
||||||
className="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500" />
|
className="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500" />
|
||||||
{l.label}
|
{l.label}
|
||||||
|
|
@ -182,7 +182,7 @@ export default function SettingsPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300">
|
<label className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300 cursor-pointer">
|
||||||
<input type="checkbox" checked={!!editing.quiet_start} onChange={e => {
|
<input type="checkbox" checked={!!editing.quiet_start} onChange={e => {
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
updateField('quiet_start', '23:00')
|
updateField('quiet_start', '23:00')
|
||||||
|
|
@ -210,7 +210,7 @@ export default function SettingsPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<label className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300">
|
<label className="flex items-center gap-2 text-sm text-slate-700 dark:text-slate-300 cursor-pointer">
|
||||||
<input type="checkbox" checked={editing.enabled === 1} onChange={e => updateField('enabled', e.target.checked ? 1 : 0)}
|
<input type="checkbox" checked={editing.enabled === 1} onChange={e => updateField('enabled', e.target.checked ? 1 : 0)}
|
||||||
className="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500" />
|
className="rounded border-slate-300 text-indigo-600 focus:ring-indigo-500" />
|
||||||
启用渠道
|
启用渠道
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue