style(dashboard): move version footer out of card
All checks were successful
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Successful in 6m30s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Successful in 1m37s

This commit is contained in:
agent
2026-04-09 15:23:16 +08:00
parent 10da51c203
commit 089d0b4b58
23 changed files with 2114 additions and 525 deletions

View File

@@ -13,77 +13,87 @@
--border: #30363d; --text: #e6edf3; --text-muted: #8b949e;
--accent: #58a6ff; --accent-hover: #79b8ff;
}
body { background: var(--bg); color: var(--text); font-family: 'Inter', sans-serif; min-height: 100vh; }
body { background: #0d1117; color: #c9d1d9; font-family: 'Inter', sans-serif; min-height: 100vh; }
.layout { display: flex; min-height: 100vh; }
.sidebar {
width: 220px; flex-shrink: 0; background: var(--surface); border-right: 1px solid var(--border);
padding: 24px 16px; display: flex; flex-direction: column; gap: 20px;
width: 200px; flex-shrink: 0; background: #0b1220; border-right: 1px solid rgba(240,246,252,0.08);
padding: 20px 12px; display: flex; flex-direction: column; gap: 20px;
}
.sidebar-logo { font-family: 'JetBrains Mono', monospace; font-size: 16px; font-weight: 600;
color: var(--text); text-decoration: none; padding: 0 10px; }
.sidebar-logo span { color: var(--accent); }
.sidebar-menu { display: flex; flex-direction: column; gap: 6px; }
.sidebar-logo { font-family: 'Inter', sans-serif; font-size: 16px; font-weight: 700;
color: #fff; text-decoration: none; padding: 0 10px; }
.sidebar-menu { display: grid; gap: 6px; }
.sidebar-link {
padding: 10px 12px; border-radius: 8px; color: var(--text-muted); text-decoration: none;
border: 1px solid transparent; font-size: 13px; font-weight: 500;
padding: 10px 12px; border-radius: 10px; color: #8b949e; text-decoration: none;
font-size: 13px; font-weight: 500;
}
.sidebar-link:hover { background: var(--surface2); color: var(--text); }
.sidebar-link:hover { background: rgba(56,139,253,0.14); color: #fff; }
.sidebar-link.active {
background: rgba(88,166,255,0.12); color: var(--text); border-color: rgba(88,166,255,0.35);
background: rgba(56,139,253,0.14); color: #fff;
}
.content-shell { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.topbar {
background: var(--surface); border-bottom: 1px solid var(--border); padding: 0 24px;
display: flex; align-items: center; gap: 12px; min-height: 52px;
background: transparent; border-bottom: none; padding: 0 24px;
display: flex; align-items: center; gap: 12px; min-height: 44px;
}
.topbar-spacer { flex: 1; }
.nav-user { font-size: 13px; color: var(--text-muted); }
.lang-bar { display: flex; gap: 2px; background: var(--surface2); border-radius: 6px; padding: 2px; }
.lang-btn { padding: 3px 9px; border: none; background: none; color: var(--text-muted);
font-size: 12px; cursor: pointer; border-radius: 4px; }
.lang-btn.active { background: var(--border); color: var(--text); }
.nav-user { font-size: 14px; color: #8b949e; }
.lang-bar { display: flex; gap: 2px; background: rgba(240,246,252,0.06); border-radius: 8px; padding: 2px; }
.lang-btn { padding: 4px 10px; border: none; background: none; color: #8b949e;
font-size: 12px; cursor: pointer; border-radius: 6px; }
.lang-btn.active { background: rgba(240,246,252,0.1); color: #fff; }
.btn-sign-out {
padding: 5px 12px; border-radius: 6px; border: 1px solid var(--border);
background: none; color: var(--text); font-size: 12px; text-decoration: none; cursor: pointer;
padding: 6px 14px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.12);
background: #161b22; color: #c9d1d9; font-size: 13px; text-decoration: none; cursor: pointer;
}
.btn-sign-out:hover { background: var(--surface2); }
.main { padding: 32px 24px 40px; flex: 1; }
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
padding: 24px; width: 100%; max-width: 1180px; margin: 0 auto; }
.btn-sign-out:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.main { padding: 16px 16px 24px; flex: 1; }
.card { background: #111827; border: 1px solid rgba(240,246,252,0.08); border-radius: 18px;
padding: 20px; width: 100%; }
.card-title-row {
display: flex; align-items: center; flex-wrap: wrap; gap: 8px;
margin-bottom: 20px;
margin-bottom: 18px;
}
.card-title { font-size: 20px; font-weight: 600; margin: 0; }
.card-title { font-size: 22px; font-weight: 700; margin: 0; color: #fff; }
.card-title-count {
display: inline-flex;
align-items: center;
min-height: 24px;
padding: 0 8px;
border: 1px solid var(--border);
border: 1px solid rgba(240,246,252,0.08);
border-radius: 999px;
background: var(--bg);
color: var(--text-muted);
background: #0d1117;
color: #8b949e;
font-size: 12px;
font-weight: 600;
line-height: 1;
font-family: 'JetBrains Mono', monospace;
}
.empty { color: var(--text-muted); font-size: 14px; padding: 20px 0; }
.empty { color: #8b949e; font-size: 14px; padding: 20px 0; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; vertical-align: top; padding: 12px 10px; border-top: 1px solid var(--border); }
th { color: var(--text-muted); font-size: 12px; font-weight: 600; }
td { font-size: 13px; }
th, td { text-align: left; vertical-align: top; padding: 14px 12px; border-top: 1px solid rgba(240,246,252,0.08); }
th { color: #8b949e; font-size: 12px; font-weight: 600; }
td { font-size: 13px; color: #c9d1d9; }
.mono { font-family: 'JetBrains Mono', monospace; }
.detail {
background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
padding: 10px; white-space: pre-wrap; word-break: break-word; font-size: 12px;
max-width: 460px;
.col-detail { min-width: 260px; max-width: 460px; }
.detail-scroll {
height: calc(1.5em * 3 + 20px);
min-height: calc(1.5em * 3 + 20px);
overflow: auto;
resize: vertical;
white-space: pre-wrap;
word-break: break-word;
padding: 10px;
background: #0d1117;
border: 1px solid rgba(240,246,252,0.08);
border-radius: 10px;
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
margin: 0;
}
@media (max-width: 900px) {
.layout { flex-direction: column; }
.sidebar {
width: 100%; border-right: none; border-bottom: 1px solid var(--border);
width: 100%; border-right: none; border-bottom: 1px solid rgba(240,246,252,0.08);
padding: 16px; gap: 14px;
}
.sidebar-menu { flex-direction: row; }
@@ -93,42 +103,43 @@
.topbar { padding: 12px 16px; flex-wrap: wrap; }
table, thead, tbody, th, td, tr { display: block; }
thead { display: none; }
tr { border-top: 1px solid var(--border); padding: 12px 0; }
tr { border-top: 1px solid rgba(240,246,252,0.08); padding: 12px 0; }
td { border-top: none; padding: 6px 0; }
td::before {
display: block; color: var(--text-muted); font-size: 11px;
display: block; color: #8b949e; font-size: 11px;
margin-bottom: 4px; text-transform: uppercase;
content: attr(data-label);
}
.detail { max-width: none; }
}
.pagination {
display: flex; align-items: center; gap: 8px; margin-top: 20px;
display: flex; align-items: center; gap: 12px; margin-top: 18px;
justify-content: center; padding: 12px 0;
}
.page-btn {
padding: 6px 14px; border-radius: 6px; border: 1px solid var(--border);
background: var(--surface); color: var(--text); text-decoration: none;
padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.12);
background: #161b22; color: #c9d1d9; text-decoration: none;
font-size: 13px; cursor: pointer;
}
.page-btn:hover { background: var(--surface2); }
.page-btn:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.page-btn-disabled {
padding: 6px 14px; border-radius: 6px; border: 1px solid var(--border);
background: var(--surface); color: var(--text-muted); font-size: 13px;
padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.12);
background: #161b22; color: #6e7681; font-size: 13px;
opacity: 0.5; cursor: not-allowed;
}
.page-info {
color: var(--text-muted); font-size: 13px; font-family: 'JetBrains Mono', monospace;
color: #8b949e; font-size: 13px; font-family: 'JetBrains Mono', monospace;
}
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a href="/dashboard" class="sidebar-logo"><span>secrets</span></a>
<a href="/dashboard" class="sidebar-logo">secrets</a>
<nav class="sidebar-menu">
<a href="/dashboard" class="sidebar-link" data-i18n="navMcp">MCP</a>
<a href="/entries" class="sidebar-link" data-i18n="navEntries">条目</a>
<a href="/trash" class="sidebar-link" data-i18n="navTrash">回收站</a>
<a href="/audit" class="sidebar-link active" data-i18n="navAudit">审计</a>
</nav>
</aside>
@@ -172,7 +183,7 @@
<td class="col-time mono" data-label="时间"><time class="audit-local-time" datetime="{{ entry.created_at_iso }}">{{ entry.created_at_iso }}</time></td>
<td class="col-action mono" data-label="动作">{{ entry.action }}</td>
<td class="col-target mono" data-label="目标">{{ entry.target }}</td>
<td class="col-detail" data-label="详情"><pre class="detail">{{ entry.detail }}</pre></td>
<td class="col-detail" data-label="详情">{% if !entry.detail.is_empty() %}<pre class="detail-scroll">{{ entry.detail }}</pre>{% endif %}</td>
</tr>
{% endfor %}
</tbody>
@@ -198,7 +209,7 @@
</main>
</div>
</div>
<script src="/static/i18n.js"></script>
<script src="/static/i18n.js?v={{ version }}"></script>
<script>
(function () {
I18N_PAGE = {
@@ -228,6 +239,7 @@
el.title = raw + ' (UTC)';
}
});
applyLang();
})();
</script>

View File

@@ -18,110 +18,108 @@
.layout { display: flex; min-height: 100vh; }
.sidebar {
width: 220px; flex-shrink: 0; background: var(--surface); border-right: 1px solid var(--border);
padding: 24px 16px; display: flex; flex-direction: column; gap: 20px;
width: 200px; flex-shrink: 0; background: #0b1220; border-right: 1px solid rgba(240,246,252,0.08);
padding: 20px 12px; display: flex; flex-direction: column; gap: 20px;
}
.sidebar-logo { font-family: 'JetBrains Mono', monospace; font-size: 16px; font-weight: 600;
color: var(--text); text-decoration: none; padding: 0 10px; }
.sidebar-logo span { color: var(--accent); }
.sidebar-menu { display: flex; flex-direction: column; gap: 6px; }
.sidebar-logo { font-family: 'Inter', sans-serif; font-size: 16px; font-weight: 700;
color: #fff; text-decoration: none; padding: 0 10px; }
.sidebar-menu { display: grid; gap: 6px; }
.sidebar-link {
padding: 10px 12px; border-radius: 8px; color: var(--text-muted); text-decoration: none;
border: 1px solid transparent; font-size: 13px; font-weight: 500;
}
.sidebar-link:hover { background: var(--surface2); color: var(--text); }
.sidebar-link.active {
background: rgba(88,166,255,0.12); color: var(--text); border-color: rgba(88,166,255,0.35);
padding: 10px 12px; border-radius: 10px; color: #8b949e; text-decoration: none;
font-size: 13px; font-weight: 500;
}
.sidebar-link:hover { background: rgba(56,139,253,0.14); color: #fff; }
.sidebar-link.active { background: rgba(56,139,253,0.14); color: #fff; }
.content-shell { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.topbar {
background: var(--surface); border-bottom: 1px solid var(--border); padding: 0 24px;
display: flex; align-items: center; gap: 12px; min-height: 52px;
background: transparent; border-bottom: none; padding: 0 24px;
display: flex; align-items: center; gap: 12px; min-height: 44px;
}
.topbar-spacer { flex: 1; }
.nav-user { font-size: 13px; color: var(--text-muted); }
.lang-bar { display: flex; gap: 2px; background: var(--surface2); border-radius: 6px; padding: 2px; }
.lang-btn { padding: 3px 9px; border: none; background: none; color: var(--text-muted);
font-size: 12px; cursor: pointer; border-radius: 4px; }
.lang-btn.active { background: var(--border); color: var(--text); }
.btn-sign-out { padding: 5px 12px; border-radius: 6px; border: 1px solid var(--border);
background: none; color: var(--text); font-size: 12px; cursor: pointer; }
.btn-sign-out:hover { background: var(--surface2); }
.nav-user { font-size: 14px; color: #8b949e; }
.lang-bar { display: flex; gap: 2px; background: rgba(240,246,252,0.06); border-radius: 8px; padding: 2px; }
.lang-btn { padding: 4px 10px; border: none; background: none; color: #8b949e;
font-size: 12px; cursor: pointer; border-radius: 6px; }
.lang-btn.active { background: rgba(240,246,252,0.1); color: #fff; }
.btn-sign-out {
padding: 6px 14px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.12);
background: #161b22; color: #c9d1d9; font-size: 13px; text-decoration: none; cursor: pointer;
}
.btn-sign-out:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
/* Main content column */
.main { display: flex; flex-direction: column; align-items: center;
padding: 24px 20px 8px; min-height: 0; }
.main { padding: 16px 16px 0; flex: 1; min-height: 0; display: flex; flex-direction: column; }
.app-footer {
margin-top: auto;
text-align: center;
padding: 4px 20px 12px;
font-size: 12px;
color: #9da7b3;
padding: 12px 0;
font-size: 11px;
color: var(--text-muted);
font-family: 'JetBrains Mono', monospace;
margin-top: auto;
}
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
padding: 24px; width: 100%; max-width: 980px; }
.card-title { font-size: 18px; font-weight: 600; margin-bottom: 24px; }
.card { background: #111827; border: 1px solid rgba(240,246,252,0.08); border-radius: 18px;
padding: 20px; width: 100%; }
.card-title { font-size: 22px; font-weight: 700; margin-bottom: 24px; color: #fff; }
/* Form */
.field { margin-bottom: 12px; }
.field label { display: block; font-size: 12px; color: var(--text-muted); margin-bottom: 5px; }
.field input { width: 100%; background: var(--bg); border: 1px solid var(--border);
color: var(--text); padding: 9px 12px; border-radius: 6px;
.field label { display: block; font-size: 12px; color: #8b949e; margin-bottom: 5px; }
.field input { width: 100%; background: #0d1117; border: 1px solid rgba(240,246,252,0.08);
color: #c9d1d9; padding: 9px 12px; border-radius: 10px;
font-size: 13px; outline: none; }
.field input:focus { border-color: var(--accent); }
.field input:focus { border-color: rgba(56,139,253,0.5); }
.pw-field { position: relative; }
.pw-field > input { padding-right: 42px; }
.pw-toggle {
position: absolute; right: 6px; top: 50%; transform: translateY(-50%);
display: flex; align-items: center; justify-content: center;
width: 32px; height: 32px; border: none; border-radius: 6px;
background: transparent; color: var(--text-muted); cursor: pointer;
width: 32px; height: 32px; border: none; border-radius: 8px;
background: transparent; color: #8b949e; cursor: pointer;
}
.pw-toggle:hover { color: var(--text); background: var(--surface2); }
.pw-toggle:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.pw-toggle:hover { color: #c9d1d9; background: rgba(240,246,252,0.06); }
.pw-toggle:focus-visible { outline: 2px solid rgba(56,139,253,0.5); outline-offset: 2px; }
.pw-icon svg { display: block; }
.pw-icon.hidden { display: none; }
.error-msg { color: var(--danger); font-size: 12px; margin-top: 6px; display: none; }
.error-msg { color: #f85149; font-size: 12px; margin-top: 6px; display: none; }
/* Buttons */
.btn-primary { display: inline-flex; align-items: center; gap: 6px; width: 100%;
justify-content: center; padding: 10px 20px; border-radius: 7px;
border: none; background: var(--accent); color: #0d1117;
justify-content: center; padding: 10px 20px; border-radius: 10px;
border: none; background: #388bfd; color: #fff;
font-size: 14px; font-weight: 600; cursor: pointer; transition: background 0.15s; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-primary:hover { background: #58a6ff; }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-sm { display: inline-flex; align-items: center; gap: 4px; padding: 5px 12px;
border-radius: 5px; border: 1px solid var(--border); background: none;
color: var(--text-muted); font-size: 12px; cursor: pointer; }
.btn-sm:hover { color: var(--text); border-color: var(--text-muted); }
.btn-sm { display: inline-flex; align-items: center; gap: 4px; padding: 8px 12px;
border-radius: 10px; border: 1px solid rgba(240,246,252,0.12); background: #161b22;
color: #8b949e; font-size: 13px; cursor: pointer; font-family: inherit; }
.btn-sm:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.btn-copy { display: flex; align-items: center; gap: 8px; width: 100%; justify-content: center;
padding: 11px 20px; border-radius: 7px; border: 1px solid var(--success);
background: rgba(63,185,80,0.1); color: var(--success);
font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.15s; }
padding: 11px 20px; border-radius: 10px; border: 1px solid #3fb950;
background: rgba(63,185,80,0.1); color: #3fb950;
font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.15s; font-family: inherit; }
.btn-copy:hover { background: rgba(63,185,80,0.2); }
.btn-copy.copied { background: var(--success); color: #0d1117; border-color: var(--success); }
.btn-copy.copied { background: #3fb950; color: #0d1117; border-color: #3fb950; }
/* Config format switcher */
.config-tabs { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; margin-bottom: 12px; }
.config-tab { padding: 12px 14px; border-radius: 10px; border: 1px solid var(--border);
background: var(--surface2); color: var(--text-muted); cursor: pointer;
.config-tab { padding: 12px 14px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.08);
background: #161b22; color: #8b949e; cursor: pointer;
font-family: inherit; text-align: left; transition: border-color 0.15s, background 0.15s, transform 0.15s; }
.config-tab:hover { color: var(--text); border-color: var(--accent); transform: translateY(-1px); }
.config-tab.active { background: rgba(88,166,255,0.1); color: var(--text); border-color: var(--accent); }
.config-tab:hover { color: #c9d1d9; border-color: rgba(56,139,253,0.45); transform: translateY(-1px); }
.config-tab.active { background: rgba(56,139,253,0.14); color: #fff; border-color: rgba(56,139,253,0.3); }
.config-tab-title { display: block; font-size: 13px; font-weight: 600; color: inherit; }
/* Config box */
.config-wrap { position: relative; margin-bottom: 14px; }
.config-box { background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
.config-box { background: #0d1117; border: 1px solid rgba(240,246,252,0.08); border-radius: 10px;
padding: 16px; font-family: 'JetBrains Mono', monospace; font-size: 11px;
line-height: 1.7; color: var(--text); overflow-x: auto; white-space: pre; }
.config-box.locked { color: var(--text-muted); filter: blur(3px); user-select: none;
line-height: 1.7; color: #c9d1d9; overflow-x: auto; white-space: pre; }
.config-box.locked { color: #8b949e; filter: blur(3px); user-select: none;
pointer-events: none; }
.config-key { color: #79c0ff; }
.config-str { color: #a5d6ff; }
.config-val { color: var(--accent); }
.config-val { color: #58a6ff; }
/* Divider */
.divider { border: none; border-top: 1px solid var(--border); margin: 20px 0; }
.divider { border: none; border-top: 1px solid rgba(240,246,252,0.08); margin: 20px 0; }
/* Actions row */
.actions-row { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
@@ -135,34 +133,29 @@
.modal-bd { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.75);
z-index: 100; align-items: center; justify-content: center; }
.modal-bd.open { display: flex; }
.modal { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
.modal { background: #111827; border: 1px solid rgba(240,246,252,0.08); border-radius: 18px;
padding: 28px; width: 100%; max-width: 420px; }
.modal h3 { font-size: 16px; font-weight: 600; margin-bottom: 16px; }
.modal h3 { font-size: 18px; font-weight: 700; margin-bottom: 16px; color: #fff; }
.modal-actions { display: flex; gap: 8px; margin-top: 16px; }
.btn-modal-ok { flex: 1; padding: 8px; border-radius: 6px; border: none;
background: var(--accent); color: #0d1117; font-size: 13px;
font-weight: 600; cursor: pointer; }
.btn-modal-ok:hover { background: var(--accent-hover); }
.btn-modal-cancel { padding: 8px 16px; border-radius: 6px; border: 1px solid var(--border);
background: none; color: var(--text); font-size: 13px; cursor: pointer; }
.btn-modal-cancel:hover { background: var(--surface2); }
.btn-modal-ok { flex: 1; padding: 8px; border-radius: 10px; border: none;
background: #388bfd; color: #fff; font-size: 13px;
font-weight: 600; cursor: pointer; font-family: inherit; }
.btn-modal-ok:hover { background: #58a6ff; }
.btn-modal-cancel { padding: 8px 16px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.12);
background: #161b22; color: #c9d1d9; font-size: 13px; cursor: pointer; font-family: inherit; }
.btn-modal-cancel:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
@media (max-width: 900px) {
.layout { flex-direction: column; }
.sidebar {
width: 100%; border-right: none; border-bottom: 1px solid var(--border);
width: 100%; border-right: none; border-bottom: 1px solid rgba(240,246,252,0.08);
padding: 16px; gap: 14px;
}
.sidebar-menu { flex-direction: row; }
.sidebar-link { flex: 1; text-align: center; }
}
@media (max-width: 720px) {
.config-tabs { grid-template-columns: 1fr; }
.sidebar-menu { flex-direction: row; flex-wrap: wrap; }
.sidebar-link { flex: 1; text-align: center; min-width: 72px; }
.main { padding: 20px 12px 28px; }
.card { padding: 16px; }
.topbar { padding: 12px 16px; flex-wrap: wrap; }
.main { padding: 16px 12px 6px; }
.app-footer { padding: 4px 12px 10px; }
.card { padding: 18px; }
}
</style>
@@ -171,11 +164,12 @@
<div class="layout">
<aside class="sidebar">
<a href="/dashboard" class="sidebar-logo"><span>secrets</span></a>
<a href="/dashboard" class="sidebar-logo">secrets</a>
<nav class="sidebar-menu">
<a href="/dashboard" class="sidebar-link active">MCP</a>
<a href="/entries" class="sidebar-link">条目</a>
<a href="/audit" class="sidebar-link">审计</a>
<a href="/dashboard" class="sidebar-link active" data-i18n="navMcp">MCP</a>
<a href="/entries" class="sidebar-link" data-i18n="navEntries">条目</a>
<a href="/trash" class="sidebar-link" data-i18n="navTrash">回收站</a>
<a href="/audit" class="sidebar-link" data-i18n="navAudit">审计</a>
</nav>
</aside>
@@ -293,13 +287,11 @@
<button class="btn-sm" onclick="confirmRegenerate()" data-i18n="btnRegen">重置 API Key</button>
</div>
</div>
</div>
</div>
<footer class="app-footer">{{ version }}</footer>
</div>
</div>
</div>
</div><!-- /main -->
</div><!-- /content-shell -->
</div><!-- /layout -->
<!-- ── Change passphrase modal ──────────────────────────────────────────────── -->
<div class="modal-bd" id="change-modal">
@@ -351,6 +343,7 @@
const T = {
'zh-CN': {
navMcp: 'MCP', navEntries: '条目', navTrash: '回收站', navAudit: '审计',
signOut: '退出',
lockedTitle: '获取 MCP 配置',
labelPassphrase: '加密密码',
@@ -388,6 +381,7 @@ const T = {
ariaHidePw: '隐藏密码',
},
'zh-TW': {
navMcp: 'MCP', navEntries: '條目', navTrash: '回收站', navAudit: '審計',
signOut: '登出',
lockedTitle: '取得 MCP 設定',
labelPassphrase: '加密密碼',
@@ -425,6 +419,7 @@ const T = {
ariaHidePw: '隱藏密碼',
},
'en': {
navMcp: 'MCP', navEntries: 'Entries', navTrash: 'Trash', navAudit: 'Audit',
signOut: 'Sign out',
lockedTitle: 'Get MCP Config',
labelPassphrase: 'Encryption password',

View File

@@ -13,45 +13,44 @@
--border: #30363d; --text: #e6edf3; --text-muted: #8b949e;
--accent: #58a6ff; --accent-hover: #79b8ff;
}
body { background: var(--bg); color: var(--text); font-family: 'Inter', sans-serif; min-height: 100vh; }
body { background: #0d1117; color: #c9d1d9; font-family: 'Inter', sans-serif; min-height: 100vh; }
.layout { display: flex; min-height: 100vh; }
.sidebar {
width: 220px; flex-shrink: 0; background: var(--surface); border-right: 1px solid var(--border);
padding: 24px 16px; display: flex; flex-direction: column; gap: 20px;
width: 200px; flex-shrink: 0; background: #0b1220; border-right: 1px solid rgba(240,246,252,0.08);
padding: 20px 12px; display: flex; flex-direction: column; gap: 20px;
}
.sidebar-logo { font-family: 'JetBrains Mono', monospace; font-size: 16px; font-weight: 600;
color: var(--text); text-decoration: none; padding: 0 10px; }
.sidebar-logo span { color: var(--accent); }
.sidebar-menu { display: flex; flex-direction: column; gap: 6px; }
.sidebar-logo { font-family: 'Inter', sans-serif; font-size: 16px; font-weight: 700;
color: #fff; text-decoration: none; padding: 0 10px; }
.sidebar-menu { display: grid; gap: 6px; }
.sidebar-link {
padding: 10px 12px; border-radius: 8px; color: var(--text-muted); text-decoration: none;
border: 1px solid transparent; font-size: 13px; font-weight: 500;
padding: 10px 12px; border-radius: 10px; color: #8b949e; text-decoration: none;
font-size: 13px; font-weight: 500;
}
.sidebar-link:hover { background: var(--surface2); color: var(--text); }
.sidebar-link:hover { background: rgba(56,139,253,0.14); color: #fff; }
.sidebar-link.active {
background: rgba(88,166,255,0.12); color: var(--text); border-color: rgba(88,166,255,0.35);
background: rgba(56,139,253,0.14); color: #fff;
}
.content-shell { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.topbar {
background: var(--surface); border-bottom: 1px solid var(--border); padding: 0 24px;
display: flex; align-items: center; gap: 12px; min-height: 52px;
background: transparent; border-bottom: none; padding: 0 24px;
display: flex; align-items: center; gap: 12px; min-height: 44px;
}
.topbar-spacer { flex: 1; }
.nav-user { font-size: 13px; color: var(--text-muted); }
.lang-bar { display: flex; gap: 2px; background: var(--surface2); border-radius: 6px; padding: 2px; }
.lang-btn { padding: 3px 9px; border: none; background: none; color: var(--text-muted);
font-size: 12px; cursor: pointer; border-radius: 4px; }
.lang-btn.active { background: var(--border); color: var(--text); }
.nav-user { font-size: 14px; color: #8b949e; }
.lang-bar { display: flex; gap: 2px; background: rgba(240,246,252,0.06); border-radius: 8px; padding: 2px; }
.lang-btn { padding: 4px 10px; border: none; background: none; color: #8b949e;
font-size: 12px; cursor: pointer; border-radius: 6px; }
.lang-btn.active { background: rgba(240,246,252,0.1); color: #fff; }
.btn-sign-out {
padding: 5px 12px; border-radius: 6px; border: 1px solid var(--border);
background: none; color: var(--text); font-size: 12px; text-decoration: none; cursor: pointer;
padding: 6px 14px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.12);
background: #161b22; color: #c9d1d9; font-size: 13px; text-decoration: none; cursor: pointer;
}
.btn-sign-out:hover { background: var(--surface2); }
.main { padding: 32px 24px 40px; flex: 1; }
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
padding: 24px; width: 100%; max-width: 1480px; margin: 0 auto; }
.card-title { font-size: 20px; font-weight: 600; margin-bottom: 8px; }
.card-subtitle { color: var(--text-muted); font-size: 13px; margin-bottom: 20px; }
.btn-sign-out:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.main { padding: 16px 16px 24px; flex: 1; }
.card { background: #111827; border: 1px solid rgba(240,246,252,0.08); border-radius: 18px;
padding: 20px; width: 100%; }
.card-title { font-size: 22px; font-weight: 700; margin-bottom: 8px; color: #fff; }
.card-subtitle { color: #8b949e; font-size: 14px; margin-bottom: 18px; }
.folder-tabs {
display: flex;
flex-wrap: wrap;
@@ -60,40 +59,40 @@
}
.folder-tab {
text-decoration: none;
color: var(--text-muted);
border: 1px solid var(--border);
color: #8b949e;
border: 1px solid rgba(240,246,252,0.08);
border-radius: 999px;
padding: 6px 10px;
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
background: var(--bg);
background: #0d1117;
}
.folder-tab:hover { color: var(--text); border-color: var(--text-muted); }
.folder-tab:hover { color: #c9d1d9; border-color: rgba(240,246,252,0.2); }
.folder-tab.active {
background: rgba(88,166,255,0.12);
border-color: rgba(88,166,255,0.35);
color: var(--text);
background: rgba(56,139,253,0.14);
border-color: rgba(56,139,253,0.3);
color: #fff;
}
.filter-bar {
display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px 16px;
margin-bottom: 20px; padding: 16px; background: var(--bg); border: 1px solid var(--border);
border-radius: 10px;
margin-bottom: 18px; padding: 16px; background: #0d1117; border: 1px solid rgba(240,246,252,0.08);
border-radius: 12px;
}
.filter-field { display: flex; flex-direction: column; gap: 6px; min-width: 140px; flex: 1; }
.filter-field label { font-size: 12px; color: var(--text-muted); font-weight: 500; }
.filter-field label { font-size: 12px; color: #8b949e; font-weight: 500; }
.filter-field input {
background: var(--surface); border: 1px solid var(--border); border-radius: 6px;
color: var(--text); padding: 8px 10px; font-size: 13px; font-family: 'JetBrains Mono', monospace;
background: #161b22; border: 1px solid rgba(240,246,252,0.08); border-radius: 8px;
color: #c9d1d9; padding: 8px 10px; font-size: 13px; font-family: 'JetBrains Mono', monospace;
outline: none; width: 100%;
}
.filter-field select {
background-color: var(--surface);
background-color: #161b22;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3 4.5l3 3 3-3' fill='none' stroke='%238b949e' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 14px center;
background-size: 12px 12px;
border: 1px solid var(--border); border-radius: 6px;
color: var(--text);
border: 1px solid rgba(240,246,252,0.08); border-radius: 8px;
color: #c9d1d9;
padding: 8px 2.8rem 8px 10px;
font-size: 13px; font-family: 'JetBrains Mono', monospace;
outline: none; width: 100%;
@@ -103,24 +102,24 @@
-moz-appearance: none;
}
.filter-field input:focus,
.filter-field select:focus { border-color: var(--accent); }
.filter-field select:focus { border-color: rgba(56,139,253,0.5); }
.filter-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.btn-filter {
padding: 8px 16px; border-radius: 6px; border: none; background: var(--accent); color: #0d1117;
padding: 8px 16px; border-radius: 10px; border: none; background: #388bfd; color: #fff;
font-size: 13px; font-weight: 600; cursor: pointer;
}
.btn-filter:hover { background: var(--accent-hover); }
.btn-filter:hover { background: #58a6ff; }
.btn-clear {
padding: 8px 14px; border-radius: 6px; border: 1px solid var(--border); background: transparent;
color: var(--text-muted); font-size: 13px; text-decoration: none; cursor: pointer;
padding: 8px 14px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.12); background: transparent;
color: #8b949e; font-size: 13px; text-decoration: none; cursor: pointer;
}
.btn-clear:hover { background: var(--surface2); color: var(--text); }
.empty { color: var(--text-muted); font-size: 14px; padding: 20px 0; }
.btn-clear:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.empty { color: #8b949e; font-size: 14px; padding: 20px 0; }
.table-wrap {
overflow: auto;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--bg);
border: 1px solid rgba(240,246,252,0.08);
border-radius: 12px;
background: #0d1117;
}
table {
width: 100%;
@@ -128,20 +127,20 @@
border-collapse: separate;
border-spacing: 0;
}
th, td { text-align: left; vertical-align: middle; padding: 12px 10px; border-top: 1px solid var(--border); }
th, td { text-align: left; vertical-align: middle; padding: 14px 12px; border-top: 1px solid rgba(240,246,252,0.08); }
th {
color: var(--text-muted);
color: #8b949e;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
position: sticky;
top: 0;
z-index: 2;
background: var(--surface);
background: #111827;
text-align: center;
vertical-align: middle;
}
td { font-size: 13px; line-height: 1.45; }
td { font-size: 13px; line-height: 1.45; color: #c9d1d9; }
tbody tr:nth-child(2n) td { background: rgba(255, 255, 255, 0.01); }
.mono { font-family: 'JetBrains Mono', monospace; }
.col-type { min-width: 108px; width: 1%; text-align: center; vertical-align: middle; }
@@ -163,13 +162,13 @@
white-space: pre-wrap;
word-break: break-word;
padding: 8px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 8px;
background: #0d1117;
border: 1px solid rgba(240,246,252,0.08);
border-radius: 10px;
font-size: 12px;
}
.detail {
background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
background: #0d1117; border: 1px solid rgba(240,246,252,0.08); border-radius: 10px;
padding: 10px; white-space: pre-wrap; word-break: break-word; font-size: 12px;
max-width: 360px; max-height: 120px; overflow: auto;
}
@@ -180,11 +179,11 @@
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid var(--border);
border: 1px solid rgba(240,246,252,0.08);
border-radius: 999px;
padding: 3px 8px;
font-size: 11px;
background: var(--surface2);
background: #161b22;
font-family: 'JetBrains Mono', monospace;
max-width: 100%;
min-width: 0;
@@ -196,8 +195,8 @@
white-space: nowrap;
}
.secret-type {
color: var(--text-muted);
border-left: 1px solid var(--border);
color: #8b949e;
border-left: 1px solid rgba(240,246,252,0.08);
padding-left: 6px;
}
.btn-unlink-secret {
@@ -224,17 +223,17 @@
.secret-name-input {
width: 100%;
min-width: 0;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
background: #0d1117;
border: 1px solid rgba(240,246,252,0.08);
border-radius: 8px;
color: #c9d1d9;
padding: 6px 10px;
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
outline: none;
}
.secret-name-input:focus {
border-color: var(--accent);
border-color: rgba(56,139,253,0.5);
}
.secret-name-input.invalid {
border-color: #f85149;
@@ -245,10 +244,10 @@
.secret-type-select {
width: 100%;
min-width: 0;
background: var(--surface2);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
background: #161b22;
border: 1px solid rgba(240,246,252,0.08);
border-radius: 8px;
color: #c9d1d9;
padding: 6px 10px;
font-size: 12px;
font-family: 'JetBrains Mono', monospace;
@@ -256,7 +255,7 @@
cursor: pointer;
}
.secret-type-select:focus {
border-color: var(--accent);
border-color: rgba(56,139,253,0.5);
}
.secret-type-select.invalid {
border-color: #f85149;
@@ -274,13 +273,13 @@
font-size: 11px;
line-height: 1.3;
margin-top: 2px;
color: var(--text-muted);
color: #8b949e;
}
.secret-name-status:empty {
display: none;
}
.secret-name-status.checking {
color: var(--accent);
color: #58a6ff;
}
.secret-name-status.error {
color: #f85149;
@@ -289,11 +288,11 @@
color: #3fb950;
}
.btn-row {
padding: 4px 10px; border-radius: 6px; font-size: 12px; cursor: pointer;
border: 1px solid var(--border); background: var(--surface2); color: var(--text-muted);
padding: 8px 12px; border-radius: 10px; font-size: 13px; cursor: pointer;
border: 1px solid rgba(240,246,252,0.12); background: #161b22; color: #8b949e;
font-family: inherit;
}
.btn-row:hover { color: var(--text); border-color: var(--text-muted); }
.btn-row:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.btn-row.danger:hover { border-color: #f85149; color: #f85149; }
.modal-overlay {
position: fixed; inset: 0; background: rgba(1, 4, 9, 0.65); z-index: 200;
@@ -301,19 +300,19 @@
}
.modal-overlay[hidden] { display: none !important; }
.modal {
background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
background: #111827; border: 1px solid rgba(240,246,252,0.08); border-radius: 18px;
padding: 22px; width: 100%; max-width: 520px; max-height: 90vh; overflow: auto;
box-shadow: 0 16px 48px rgba(0,0,0,0.45);
}
.modal.modal-wide {
max-width: 800px;
}
.modal-title { font-size: 16px; font-weight: 600; margin-bottom: 14px; }
.modal-title { font-size: 18px; font-weight: 700; margin-bottom: 14px; color: #fff; }
.modal-field { margin-bottom: 12px; }
.modal-field label { display: block; font-size: 12px; color: var(--text-muted); margin-bottom: 5px; }
.modal-field label { display: block; font-size: 12px; color: #8b949e; margin-bottom: 5px; }
.modal-field input, .modal-field textarea {
width: 100%; background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
color: var(--text); padding: 8px 10px; font-size: 13px; font-family: 'JetBrains Mono', monospace;
width: 100%; background: #0d1117; border: 1px solid rgba(240,246,252,0.08); border-radius: 10px;
color: #c9d1d9; padding: 8px 10px; font-size: 13px; font-family: 'JetBrains Mono', monospace;
outline: none;
}
.modal-field textarea { resize: vertical; }
@@ -323,22 +322,23 @@
}
.modal-field textarea.metadata-edit { min-height: 140px; }
.modal-readonly-value {
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
color: var(--text-muted); padding: 8px 10px; font-size: 13px;
background: #0d1117; border: 1px solid rgba(240,246,252,0.08); border-radius: 10px;
color: #8b949e; padding: 8px 10px; font-size: 13px;
font-family: 'JetBrains Mono', monospace;
}
.modal-secrets .secret-list { display: flex; flex-direction: column; gap: 10px; }
.modal-error { color: #f85149; font-size: 12px; margin-bottom: 10px; display: none; }
.modal-error.visible { display: block; }
.modal-footer { display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; margin-top: 16px; }
.btn-modal { padding: 8px 16px; border-radius: 6px; font-size: 13px; cursor: pointer; font-family: inherit; border: 1px solid var(--border); background: transparent; color: var(--text); }
.btn-modal.primary { background: var(--accent); color: #0d1117; border-color: transparent; font-weight: 600; }
.btn-modal.primary:hover { background: var(--accent-hover); }
.btn-modal { padding: 8px 16px; border-radius: 10px; font-size: 13px; cursor: pointer; font-family: inherit; border: 1px solid rgba(240,246,252,0.12); background: transparent; color: #c9d1d9; }
.btn-modal:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.btn-modal.primary { background: #388bfd; color: #fff; border-color: transparent; font-weight: 600; }
.btn-modal.primary:hover { background: #58a6ff; }
.btn-modal.danger { border-color: #f85149; color: #f85149; }
@media (max-width: 900px) {
.layout { flex-direction: column; }
.sidebar {
width: 100%; border-right: none; border-bottom: 1px solid var(--border);
width: 100%; border-right: none; border-bottom: 1px solid rgba(240,246,252,0.08);
padding: 16px; gap: 14px;
}
.sidebar-menu { flex-direction: row; flex-wrap: wrap; }
@@ -349,10 +349,10 @@
.table-wrap { max-height: none; border: none; background: transparent; }
table, thead, tbody, th, td, tr { display: block; min-width: 0; width: 100%; }
thead { display: none; }
tr { border-top: 1px solid var(--border); padding: 12px 0; }
tr { border-top: 1px solid rgba(240,246,252,0.08); padding: 12px 0; }
td { border-top: none; padding: 6px 0; max-width: none; }
td::before {
display: block; color: var(--text-muted); font-size: 11px;
display: block; color: #8b949e; font-size: 11px;
margin-bottom: 4px; text-transform: uppercase;
content: attr(data-label);
}
@@ -362,26 +362,26 @@
.detail, .notes-scroll, .secret-list { max-width: none; }
}
.pagination {
display: flex; align-items: center; gap: 8px; margin-top: 20px;
display: flex; align-items: center; gap: 12px; margin-top: 18px;
justify-content: center; padding: 12px 0;
}
.page-btn {
padding: 6px 14px; border-radius: 6px; border: 1px solid var(--border);
background: var(--surface); color: var(--text); text-decoration: none;
padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.12);
background: #161b22; color: #c9d1d9; text-decoration: none;
font-size: 13px; cursor: pointer;
}
.page-btn:hover { background: var(--surface2); }
.page-btn:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.page-btn-disabled {
padding: 6px 14px; border-radius: 6px; border: 1px solid var(--border);
background: var(--surface); color: var(--text-muted); font-size: 13px;
padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.12);
background: #161b22; color: #6e7681; font-size: 13px;
opacity: 0.5; cursor: not-allowed;
}
.page-info {
color: var(--text-muted); font-size: 13px; font-family: 'JetBrains Mono', monospace;
color: #8b949e; font-size: 13px; font-family: 'JetBrains Mono', monospace;
}
.view-secret-row {
display: flex; flex-direction: column; gap: 4px; padding: 8px 0;
border-bottom: 1px solid var(--border);
border-bottom: 1px solid rgba(240,246,252,0.08);
}
.view-secret-row:last-child { border-bottom: none; }
.view-secret-header {
@@ -389,59 +389,60 @@
}
.view-secret-name {
font-family: 'JetBrains Mono', monospace; font-size: 12px;
color: var(--text); font-weight: 600;
color: #c9d1d9; font-weight: 600;
}
.view-secret-type {
font-family: 'JetBrains Mono', monospace; font-size: 11px;
color: var(--text-muted); background: var(--surface2);
border: 1px solid var(--border); border-radius: 4px; padding: 1px 6px;
color: #8b949e; background: #161b22;
border: 1px solid rgba(240,246,252,0.08); border-radius: 6px; padding: 1px 6px;
}
.view-secret-actions { margin-left: auto; display: flex; gap: 6px; }
.view-secret-value-wrap { position: relative; }
.view-secret-value {
font-family: 'JetBrains Mono', monospace; font-size: 12px;
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
background: #0d1117; border: 1px solid rgba(240,246,252,0.08); border-radius: 10px;
padding: 7px 10px; word-break: break-all; white-space: pre-wrap;
max-height: 140px; overflow: auto; color: var(--text); line-height: 1.5;
max-height: 140px; overflow: auto; color: #c9d1d9; line-height: 1.5;
}
.view-secret-value.masked { letter-spacing: 2px; user-select: none; filter: blur(4px); }
.btn-icon {
padding: 3px 8px; border-radius: 5px; font-size: 11px; cursor: pointer;
border: 1px solid var(--border); background: var(--surface2); color: var(--text-muted);
padding: 6px 10px; border-radius: 8px; font-size: 12px; cursor: pointer;
border: 1px solid rgba(240,246,252,0.12); background: #161b22; color: #8b949e;
font-family: inherit;
}
.btn-icon:hover { color: var(--text); border-color: var(--text-muted); }
.btn-icon:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.view-locked-msg {
font-size: 13px; color: var(--text-muted); padding: 16px 0;
font-size: 13px; color: #8b949e; padding: 16px 0;
line-height: 1.6; text-align: center;
}
.view-locked-msg a { color: var(--accent); }
.view-locked-msg a { color: #58a6ff; }
.view-secret-name-wrap { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; flex: 1; min-width: 0; }
.view-secret-name-input {
width: 180px; max-width: 100%; background: var(--bg); border: 1px solid var(--border);
border-radius: 4px; color: var(--text); padding: 2px 8px; font-size: 12px;
width: 180px; max-width: 100%; background: #0d1117; border: 1px solid rgba(240,246,252,0.08);
border-radius: 8px; color: #c9d1d9; padding: 2px 8px; font-size: 12px;
font-family: 'JetBrains Mono', monospace; outline: none;
}
.view-secret-name-input:focus { border-color: var(--accent); }
.view-secret-name-input:focus { border-color: rgba(56,139,253,0.5); }
.view-secret-type-select {
background: var(--surface2); border: 1px solid var(--border); border-radius: 4px;
color: var(--text); padding: 2px 6px; font-size: 11px;
background: #161b22; border: 1px solid rgba(240,246,252,0.08); border-radius: 8px;
color: #c9d1d9; padding: 2px 6px; font-size: 11px;
font-family: 'JetBrains Mono', monospace; outline: none; cursor: pointer;
}
.view-secret-type-select:focus { border-color: var(--accent); }
.btn-view-edit { color: var(--accent); }
.view-secret-type-select:focus { border-color: rgba(56,139,253,0.5); }
.btn-view-edit { color: #58a6ff; }
.btn-view-save { color: #3fb950; }
.btn-view-cancel { color: var(--text-muted); }
.btn-view-cancel { color: #8b949e; }
.btn-view-unlink { color: #f85149; font-size: 14px; }
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a href="/dashboard" class="sidebar-logo"><span>secrets</span></a>
<a href="/dashboard" class="sidebar-logo">secrets</a>
<nav class="sidebar-menu">
<a href="/dashboard" class="sidebar-link" data-i18n="navMcp">MCP</a>
<a href="/entries" class="sidebar-link active" data-i18n="navEntries">条目</a>
<a href="/trash" class="sidebar-link" data-i18n="navTrash">回收站</a>
<a href="/audit" class="sidebar-link" data-i18n="navAudit">审计</a>
</nav>
</aside>
@@ -480,6 +481,10 @@
<label for="filter-name" data-i18n="filterNameLabel">名称</label>
<input id="filter-name" name="name" type="text" value="{{ filter_name }}" data-i18n-ph="filterNamePlaceholder" placeholder="输入关键字" autocomplete="off">
</div>
<div class="filter-field">
<label for="filter-metadata-query" data-i18n="filterMetadataLabel">元数据值</label>
<input id="filter-metadata-query" name="metadata_query" type="text" value="{{ filter_metadata_query }}" data-i18n-ph="filterMetadataPlaceholder" placeholder="搜索元数据值" autocomplete="off">
</div>
<div class="filter-field">
<label for="filter-type" data-i18n="filterTypeLabel">类型</label>
<select id="filter-type" name="type" onchange="this.form.requestSubmit();">
@@ -506,17 +511,34 @@
<th data-i18n="colType">类型</th>
<th data-i18n="colNotes">备注</th>
<th data-i18n="colTags">标签</th>
<th data-i18n="colRelations">关联</th>
<th data-i18n="colSecrets">密文</th>
<th data-i18n="colActions">操作</th>
</tr>
</thead>
<tbody>
{% for entry in entries %}
<tr data-entry-id="{{ entry.id }}" data-entry-folder="{{ entry.folder }}" data-entry-metadata="{{ entry.metadata_json }}" data-entry-secrets="{{ entry.secrets_json }}" data-updated-at="{{ entry.updated_at_iso }}">
<tr data-entry-id="{{ entry.id }}" data-entry-folder="{{ entry.folder }}" data-entry-metadata="{{ entry.metadata_json }}" data-entry-secrets="{{ entry.secrets_json }}" data-entry-parents="{{ entry.parents_json }}" data-updated-at="{{ entry.updated_at_iso }}">
<td class="col-name mono cell-name" data-label="名称">{{ entry.name }}</td>
<td class="col-type mono cell-type" data-label="类型">{{ entry.entry_type }}</td>
<td class="col-notes cell-notes" data-label="备注">{% if !entry.notes.is_empty() %}<div class="notes-scroll cell-notes-val">{{ entry.notes }}</div>{% endif %}</td>
<td class="col-tags mono cell-tags-val" data-label="标签">{{ entry.tags }}</td>
<td class="col-relations" data-label="关联">
<div class="secret-list">
{% for parent in entry.parents %}
<a class="secret-chip" href="{{ parent.href }}" title="{{ parent.folder }} / {{ parent.name }}">
<span class="secret-name">{{ parent.name }}</span>
<span class="secret-type" data-i18n="relationParentBadge">上级</span>
</a>
{% endfor %}
{% for child in entry.children %}
<a class="secret-chip" href="{{ child.href }}" title="{{ child.folder }} / {{ child.name }}">
<span class="secret-name">{{ child.name }}</span>
<span class="secret-type" data-i18n="relationChildBadge">下级</span>
</a>
{% endfor %}
</div>
</td>
<td class="col-secrets" data-label="密文">
<div class="secret-list">
{% for s in entry.secrets %}
@@ -543,13 +565,13 @@
{% if total_count > 0 %}
<div class="pagination">
{% if current_page > 1 %}
<a href="?{% if !filter_folder.is_empty() %}folder={{ filter_folder | urlencode }}&{% endif %}{% if !filter_type.is_empty() %}type={{ filter_type | urlencode }}&{% endif %}{% if !filter_name.is_empty() %}name={{ filter_name | urlencode }}&{% endif %}page={{ current_page - 1 }}" class="page-btn" data-i18n="prevPage">上一页</a>
<a href="?{% if !filter_folder.is_empty() %}folder={{ filter_folder | urlencode }}&{% endif %}{% if !filter_type.is_empty() %}type={{ filter_type | urlencode }}&{% endif %}{% if !filter_name.is_empty() %}name={{ filter_name | urlencode }}&{% endif %}{% if !filter_metadata_query.is_empty() %}metadata_query={{ filter_metadata_query | urlencode }}&{% endif %}page={{ current_page - 1 }}" class="page-btn" data-i18n="prevPage">上一页</a>
{% else %}
<span class="page-btn page-btn-disabled" data-i18n="prevPage">上一页</span>
{% endif %}
<span class="page-info">{{ current_page }} / {{ total_pages }}</span>
{% if current_page < total_pages %}
<a href="?{% if !filter_folder.is_empty() %}folder={{ filter_folder | urlencode }}&{% endif %}{% if !filter_type.is_empty() %}type={{ filter_type | urlencode }}&{% endif %}{% if !filter_name.is_empty() %}name={{ filter_name | urlencode }}&{% endif %}page={{ current_page + 1 }}" class="page-btn" data-i18n="nextPage">下一页</a>
<a href="?{% if !filter_folder.is_empty() %}folder={{ filter_folder | urlencode }}&{% endif %}{% if !filter_type.is_empty() %}type={{ filter_type | urlencode }}&{% endif %}{% if !filter_name.is_empty() %}name={{ filter_name | urlencode }}&{% endif %}{% if !filter_metadata_query.is_empty() %}metadata_query={{ filter_metadata_query | urlencode }}&{% endif %}page={{ current_page + 1 }}" class="page-btn" data-i18n="nextPage">下一页</a>
{% else %}
<span class="page-btn page-btn-disabled" data-i18n="nextPage">下一页</span>
{% endif %}
@@ -572,6 +594,12 @@
<div class="modal-field"><label for="edit-tags" data-i18n="modalTags">标签(逗号分隔)</label><input id="edit-tags" type="text" autocomplete="off"></div>
<div class="modal-field"><label data-i18n="modalUpdated">更新</label><div id="edit-updated-at" class="modal-readonly-value" aria-live="polite"></div></div>
<div class="modal-field"><label for="edit-metadata" data-i18n="modalMetadata">元数据JSON 对象)</label><textarea id="edit-metadata" class="metadata-edit"></textarea></div>
<div class="modal-field">
<label for="edit-parent-search" data-i18n="modalParents">上级条目</label>
<div id="edit-parent-list" class="secret-list"></div>
<input id="edit-parent-search" type="text" autocomplete="off" data-i18n-ph="parentSearchPlaceholder" placeholder="按名称搜索条目">
<div id="edit-parent-results" class="secret-list" style="margin-top:8px"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn-modal" id="edit-cancel" data-i18n="modalCancel">取消</button>
<button type="button" class="btn-modal primary" id="edit-save" data-i18n="modalSave">保存</button>
@@ -579,31 +607,19 @@
</div>
</div>
<div id="delete-overlay" class="modal-overlay" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="delete-title">
<div class="modal-title" id="delete-title" data-i18n="deleteTitle">确认删除</div>
<div id="delete-error" class="modal-error"></div>
<div class="modal-field">
<div id="delete-message" style="font-size:14px;line-height:1.6;color:var(--text)"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn-modal" id="delete-cancel" data-i18n="modalCancel">取消</button>
<button type="button" class="btn-modal danger" id="delete-confirm" data-i18n="deleteConfirm">删除</button>
</div>
</div>
</div>
<div id="view-overlay" class="modal-overlay" hidden>
<div class="modal modal-wide" role="dialog" aria-modal="true" aria-labelledby="view-title">
<div class="modal-title" id="view-title" data-i18n="viewTitle">查看条目密文</div>
<div id="view-entry-name" style="font-size:13px;color:var(--text-muted);margin-bottom:14px;font-family:'JetBrains Mono',monospace;"></div>
<div id="view-entry-name" style="font-size:13px;color:#8b949e;margin-bottom:14px;font-family:'JetBrains Mono',monospace;"></div>
<div id="view-body"></div>
<div class="modal-footer">
<button type="button" class="btn-modal" id="view-close" data-i18n="modalCancel">关闭</button>
</div>
</div>
</div>
<script src="/static/i18n.js"></script>
<script src="/static/i18n.js?v={{ version }}"></script>
<script id="secret-type-options" type="application/json">{{ secret_type_options_json|safe }}</script>
<script>
var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-options').textContent);
@@ -611,10 +627,13 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
I18N_PAGE = {
'zh-CN': {
pageTitle: 'Secrets — 条目',
navTrash: '回收站',
entriesTitle: '我的条目',
allTab: '全部',
filterNameLabel: '名称',
filterNamePlaceholder: '输入关键字',
filterMetadataLabel: '元数据值',
filterMetadataPlaceholder: '搜索元数据值',
filterTypeLabel: '类型',
filterTypeAll: '全部',
filterSubmit: '筛选',
@@ -624,8 +643,11 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
colType: '类型',
colNotes: '备注',
colTags: '标签',
colRelations: '关联',
colSecrets: '密文',
colActions: '操作',
relationParentBadge: '上级',
relationChildBadge: '下级',
rowEdit: '编辑条目',
rowDelete: '删除',
modalTitle: '编辑条目信息',
@@ -636,16 +658,15 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
modalTags: '标签(逗号分隔)',
modalUpdated: '更新',
modalMetadata: '元数据JSON 对象)',
modalParents: '上级条目',
modalSecrets: '密文',
modalCancel: '取消',
modalSave: '保存',
deleteTitle: '确认删除',
deleteConfirm: '删除',
deleteMessage: '确定删除条目「{name}」?此操作不可撤销。',
mobileLabelName: '名称',
mobileLabelType: '类型',
mobileLabelNotes: '备注',
mobileLabelTags: '标签',
mobileLabelRelations: '关联',
mobileLabelSecrets: '密文',
mobileLabelActions: '操作',
errInvalidJson: '元数据不是合法 JSON',
@@ -679,13 +700,19 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
viewSaveChanges: '保存更改',
viewChangesSaved: '已保存',
viewUnlinkConfirm: '确定解除密文关联「{name}」?',
parentSearchPlaceholder: '按名称搜索条目',
parentSearchEmpty: '没有匹配的条目',
removeParent: '移除上级',
},
'zh-TW': {
pageTitle: 'Secrets — 條目',
navTrash: '回收站',
entriesTitle: '我的條目',
allTab: '全部',
filterNameLabel: '名稱',
filterNamePlaceholder: '輸入關鍵字',
filterMetadataLabel: '中繼資料值',
filterMetadataPlaceholder: '搜尋中繼資料值',
filterTypeLabel: '類型',
filterTypeAll: '全部',
filterSubmit: '篩選',
@@ -695,8 +722,11 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
colType: '類型',
colNotes: '備註',
colTags: '標籤',
colRelations: '關聯',
colSecrets: '密文',
colActions: '操作',
relationParentBadge: '上級',
relationChildBadge: '下級',
rowEdit: '編輯條目',
rowDelete: '刪除',
modalTitle: '編輯條目資訊',
@@ -707,16 +737,15 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
modalTags: '標籤(逗號分隔)',
modalUpdated: '更新時間',
modalMetadata: '中繼資料JSON 物件)',
modalParents: '上級條目',
modalSecrets: '密文',
modalCancel: '取消',
modalSave: '儲存',
deleteTitle: '確認刪除',
deleteConfirm: '刪除',
deleteMessage: '確定刪除條目「{name}」?此操作不可復原。',
mobileLabelName: '名稱',
mobileLabelType: '類型',
mobileLabelNotes: '備註',
mobileLabelTags: '標籤',
mobileLabelRelations: '關聯',
mobileLabelSecrets: '密文',
mobileLabelActions: '操作',
errInvalidJson: '中繼資料不是合法 JSON',
@@ -750,13 +779,19 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
viewSaveChanges: '儲存變更',
viewChangesSaved: '已儲存',
viewUnlinkConfirm: '確定解除密文關聯「{name}」?',
parentSearchPlaceholder: '依名稱搜尋條目',
parentSearchEmpty: '沒有符合的條目',
removeParent: '移除上級',
},
en: {
pageTitle: 'Secrets — Entries',
navTrash: 'Trash',
entriesTitle: 'My entries',
allTab: 'All',
filterNameLabel: 'Name',
filterNamePlaceholder: 'Enter keywords',
filterMetadataLabel: 'Metadata value',
filterMetadataPlaceholder: 'Search metadata values',
filterTypeLabel: 'Type',
filterTypeAll: 'All',
filterSubmit: 'Filter',
@@ -766,8 +801,11 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
colType: 'Type',
colNotes: 'Notes',
colTags: 'Tags',
colRelations: 'Relations',
colSecrets: 'Secrets',
colActions: 'Actions',
relationParentBadge: 'Parent',
relationChildBadge: 'Child',
rowEdit: 'Edit entry',
rowDelete: 'Delete',
modalTitle: 'Edit entry details',
@@ -778,16 +816,15 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
modalTags: 'Tags (comma separated)',
modalUpdated: 'Updated',
modalMetadata: 'Metadata (JSON object)',
modalParents: 'Parent entries',
modalSecrets: 'Secrets',
modalCancel: 'Cancel',
modalSave: 'Save',
deleteTitle: 'Confirm Delete',
deleteConfirm: 'Delete',
deleteMessage: 'Are you sure you want to delete entry "{name}"? This action cannot be undone.',
mobileLabelName: 'Name',
mobileLabelType: 'Type',
mobileLabelNotes: 'Notes',
mobileLabelTags: 'Tags',
mobileLabelRelations: 'Relations',
mobileLabelSecrets: 'Secrets',
mobileLabelActions: 'Actions',
errInvalidJson: 'Metadata is not valid JSON',
@@ -821,6 +858,9 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
viewSaveChanges: 'Save changes',
viewChangesSaved: 'Saved',
viewUnlinkConfirm: 'Unlink secret "{name}"?',
parentSearchPlaceholder: 'Search entries by name',
parentSearchEmpty: 'No matching entries',
removeParent: 'Remove parent',
}
};
@@ -836,6 +876,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
'.col-type': 'mobileLabelType',
'.col-notes': 'mobileLabelNotes',
'.col-tags': 'mobileLabelTags',
'.col-relations': 'mobileLabelRelations',
'.col-secrets': 'mobileLabelSecrets',
'.col-actions': 'mobileLabelActions'
};
@@ -855,11 +896,98 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
var editTags = document.getElementById('edit-tags');
var editMetadata = document.getElementById('edit-metadata');
var editUpdatedAt = document.getElementById('edit-updated-at');
var deleteOverlay = document.getElementById('delete-overlay');
var deleteError = document.getElementById('delete-error');
var deleteMessage = document.getElementById('delete-message');
var editParentList = document.getElementById('edit-parent-list');
var editParentSearch = document.getElementById('edit-parent-search');
var editParentResults = document.getElementById('edit-parent-results');
var currentEntryId = null;
var pendingDeleteId = null;
var selectedParents = [];
var parentSearchTimer = null;
function renderSelectedParents() {
editParentList.innerHTML = '';
selectedParents.forEach(function (parent) {
var chip = document.createElement('span');
chip.className = 'secret-chip';
var name = document.createElement('span');
name.className = 'secret-name';
name.textContent = parent.name;
var type = document.createElement('span');
type.className = 'secret-type';
type.textContent = parent.entry_type || parent.type || '';
var remove = document.createElement('button');
remove.type = 'button';
remove.className = 'btn-icon btn-view-unlink';
remove.textContent = '×';
remove.title = t('removeParent');
remove.addEventListener('click', function () {
selectedParents = selectedParents.filter(function (item) { return item.id !== parent.id; });
renderSelectedParents();
});
chip.appendChild(name);
chip.appendChild(type);
chip.appendChild(remove);
editParentList.appendChild(chip);
});
}
function renderParentSearchResults(options) {
editParentResults.innerHTML = '';
if (!options.length) {
var empty = document.createElement('div');
empty.className = 'view-locked-msg';
empty.textContent = t('parentSearchEmpty');
editParentResults.appendChild(empty);
return;
}
options.forEach(function (option) {
if (selectedParents.some(function (item) { return item.id === String(option.id); })) return;
var button = document.createElement('button');
button.type = 'button';
button.className = 'secret-chip';
button.style.background = '#161b22';
button.style.cursor = 'pointer';
button.innerHTML = '<span class="secret-name"></span><span class="secret-type"></span>';
button.querySelector('.secret-name').textContent = option.name;
button.querySelector('.secret-type').textContent = [option.folder, option.type].filter(Boolean).join(' / ');
button.addEventListener('click', function () {
selectedParents.push({
id: String(option.id),
name: option.name,
folder: option.folder || '',
entry_type: option.type || ''
});
editParentSearch.value = '';
editParentResults.innerHTML = '';
renderSelectedParents();
});
editParentResults.appendChild(button);
});
}
function searchParentEntries() {
var query = editParentSearch.value.trim();
if (!query || !currentEntryId) {
editParentResults.innerHTML = '';
return;
}
fetch('/api/entries/options?q=' + encodeURIComponent(query) + '&exclude_id=' + encodeURIComponent(currentEntryId), {
credentials: 'same-origin'
}).then(function (response) {
return response.json().then(function (body) {
if (!response.ok) throw new Error(body.error || ('HTTP ' + response.status));
return body;
});
}).then(function (options) {
renderParentSearchResults(options || []);
}).catch(function (error) {
showEditErr(error.message || String(error));
});
}
// ── View secrets modal ────────────────────────────────────────────────────
var viewOverlay = document.getElementById('view-overlay');
@@ -1264,6 +1392,14 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
} catch (err) {
editMetadata.value = md;
}
try {
selectedParents = JSON.parse(tr.getAttribute('data-entry-parents') || '[]');
} catch (err) {
selectedParents = [];
}
editParentSearch.value = '';
editParentResults.innerHTML = '';
renderSelectedParents();
editOverlay.hidden = false;
}
@@ -1273,37 +1409,31 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
showEditErr('');
editUpdatedAt.textContent = '';
editUpdatedAt.title = '';
editParentSearch.value = '';
editParentResults.innerHTML = '';
selectedParents = [];
renderSelectedParents();
}
editParentSearch.addEventListener('input', function () {
clearTimeout(parentSearchTimer);
parentSearchTimer = setTimeout(searchParentEntries, 180);
});
document.getElementById('edit-cancel').addEventListener('click', closeEdit);
editOverlay.addEventListener('click', function (e) {
if (e.target === editOverlay) closeEdit();
});
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && !editOverlay.hidden) closeEdit();
if (e.key === 'Escape' && !deleteOverlay.hidden) closeDelete();
if (e.key === 'Escape' && !viewOverlay.hidden) closeView();
});
function showDeleteErr(msg) {
deleteError.textContent = msg || '';
deleteError.classList.toggle('visible', !!msg);
}
function openDelete(id, name) {
pendingDeleteId = id;
deleteMessage.textContent = tf('deleteMessage', { name: name });
showDeleteErr('');
deleteOverlay.hidden = false;
}
function closeDelete() {
deleteOverlay.hidden = true;
pendingDeleteId = null;
showDeleteErr('');
}
function refreshListAfterSave(entryId, body) {
if (body.parent_ids) {
window.location.reload();
return;
}
var tr = document.querySelector('tr[data-entry-id="' + entryId + '"]');
if (!tr) { window.location.reload(); return; }
var nameCell = tr.querySelector('.cell-name');
@@ -1368,27 +1498,6 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
}
}
document.getElementById('delete-cancel').addEventListener('click', closeDelete);
deleteOverlay.addEventListener('click', function (e) {
if (e.target === deleteOverlay) closeDelete();
});
document.getElementById('delete-confirm').addEventListener('click', function () {
if (!pendingDeleteId) return;
fetch('/api/entries/' + encodeURIComponent(pendingDeleteId), { method: 'DELETE', credentials: 'same-origin' })
.then(function (r) {
return r.json().then(function (data) {
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
return data;
});
})
.then(function () {
var deletedId = pendingDeleteId;
closeDelete();
refreshListAfterDelete(deletedId);
})
.catch(function (e) { showDeleteErr(e.message || String(e)); });
});
document.getElementById('edit-save').addEventListener('click', function () {
if (!currentEntryId) return;
var meta;
@@ -1409,7 +1518,8 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
name: editName.value.trim(),
notes: editNotes.value,
tags: tags,
metadata: meta
metadata: meta,
parent_ids: selectedParents.map(function (parent) { return parent.id; })
};
showEditErr('');
fetch('/api/entries/' + encodeURIComponent(currentEntryId), {
@@ -1433,17 +1543,23 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
document.querySelectorAll('tr[data-entry-id]').forEach(function (tr) {
var viewBtn = tr.querySelector('.btn-view-secrets');
if (viewBtn) {
var hasSecrets = tr.querySelectorAll('.secret-chip').length > 0;
var hasSecrets = tr.querySelectorAll('.col-secrets .secret-chip').length > 0;
if (!hasSecrets) viewBtn.disabled = true;
viewBtn.addEventListener('click', function () { openView(tr); });
}
tr.querySelector('.btn-edit').addEventListener('click', function () { openEdit(tr); });
tr.querySelector('.btn-del').addEventListener('click', function () {
var id = tr.getAttribute('data-entry-id');
var nameEl = tr.querySelector('.cell-name');
var name = nameEl ? nameEl.textContent.trim() : '';
if (!id) return;
openDelete(id, name);
fetch('/api/entries/' + encodeURIComponent(id), { method: 'DELETE', credentials: 'same-origin' })
.then(function (r) {
return r.json().then(function (data) {
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
return data;
});
})
.then(function () { refreshListAfterDelete(id); })
.catch(function (e) { alert(e.message || String(e)); });
});
});

View File

@@ -3,6 +3,7 @@ var I18N_SHARED = {
pageTitleBase: 'Secrets',
navMcp: 'MCP',
navEntries: '条目',
navTrash: '回收站',
navAudit: '审计',
signOut: '退出',
mobileLabelTime: '时间',
@@ -14,6 +15,7 @@ var I18N_SHARED = {
pageTitleBase: 'Secrets',
navMcp: 'MCP',
navEntries: '條目',
navTrash: '回收站',
navAudit: '審計',
signOut: '登出',
mobileLabelTime: '時間',
@@ -25,6 +27,7 @@ var I18N_SHARED = {
pageTitleBase: 'Secrets',
navMcp: 'MCP',
navEntries: 'Entries',
navTrash: 'Trash',
navAudit: 'Audit',
signOut: 'Sign out',
mobileLabelTime: 'Time',

View File

@@ -0,0 +1,272 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="icon" href="/favicon.svg?v={{ version }}" type="image/svg+xml">
<title>Secrets — 回收站</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Inter:wght@400;500;600&display=swap');
:root {
--bg: #0d1117; --surface: #161b22; --surface2: #21262d;
--border: #30363d; --text: #e6edf3; --text-muted: #8b949e;
--accent: #58a6ff; --accent-hover: #79b8ff;
}
body { background: var(--bg); color: var(--text); font-family: 'Inter', sans-serif; min-height: 100vh; }
.layout { display: flex; min-height: 100vh; }
.sidebar {
width: 200px; flex-shrink: 0; background: #0b1220; border-right: 1px solid rgba(240,246,252,0.08);
padding: 20px 12px; display: flex; flex-direction: column; gap: 20px;
}
.sidebar-logo { font-family: 'Inter', sans-serif; font-size: 16px; font-weight: 700;
color: #fff; text-decoration: none; padding: 0 10px; }
.sidebar-menu { display: grid; gap: 6px; }
.sidebar-link {
padding: 10px 12px; border-radius: 10px; color: #8b949e; text-decoration: none;
font-size: 13px; font-weight: 500;
}
.sidebar-link:hover { background: rgba(56,139,253,0.14); color: #fff; }
.sidebar-link.active { background: rgba(56,139,253,0.14); color: #fff; }
.content-shell { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.topbar {
background: transparent; border-bottom: none; padding: 0 24px;
display: flex; align-items: center; gap: 12px; min-height: 44px;
}
.topbar-spacer { flex: 1; }
.nav-user { font-size: 14px; color: #8b949e; }
.lang-bar { display: flex; gap: 2px; background: rgba(240,246,252,0.06); border-radius: 8px; padding: 2px; }
.lang-btn { padding: 4px 10px; border: none; background: none; color: #8b949e;
font-size: 12px; cursor: pointer; border-radius: 6px; }
.lang-btn.active { background: rgba(240,246,252,0.1); color: #fff; }
.btn-sign-out {
padding: 6px 14px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.12);
background: #161b22; color: #c9d1d9; font-size: 13px; text-decoration: none; cursor: pointer;
}
.btn-sign-out:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.main { padding: 16px 16px 24px; flex: 1; }
.card { background: #111827; border: 1px solid rgba(240,246,252,0.08); border-radius: 18px;
padding: 20px; width: 100%; }
.card-title { font-size: 22px; font-weight: 700; margin-bottom: 8px; color: #fff; }
.card-subtitle { color: #8b949e; font-size: 14px; margin-bottom: 18px; }
table { width: 100%; border-collapse: collapse; }
th, td { text-align: left; padding: 14px 12px; border-top: 1px solid rgba(240,246,252,0.08); vertical-align: top; }
th { color: #8b949e; font-size: 12px; font-weight: 600; }
td { font-size: 13px; color: #c9d1d9; }
.mono { font-family: 'JetBrains Mono', monospace; }
.row-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.btn { border: 1px solid rgba(240,246,252,0.12); background: #161b22; color: #c9d1d9; border-radius: 10px; padding: 8px 12px; cursor: pointer; font-size: 13px; font-family: inherit; }
.btn:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.btn-danger { color: #f85149; }
.empty { color: #8b949e; font-size: 14px; padding: 20px 0; }
.pagination {
display: flex; align-items: center; gap: 12px; margin-top: 18px;
justify-content: center; padding: 12px 0;
}
.page-btn {
padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.12);
background: #161b22; color: #c9d1d9; text-decoration: none;
font-size: 13px; cursor: pointer;
}
.page-btn:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
.page-btn.disabled {
padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.12);
background: #161b22; color: #6e7681; font-size: 13px;
opacity: 0.5; cursor: not-allowed;
}
.page-info {
color: #8b949e; font-size: 13px; font-family: 'JetBrains Mono', monospace;
}
@media (max-width: 900px) {
.layout { flex-direction: column; }
.sidebar {
width: 100%; border-right: none; border-bottom: 1px solid rgba(240,246,252,0.08);
padding: 16px; gap: 14px;
}
.sidebar-menu { flex-direction: row; flex-wrap: wrap; }
.sidebar-link { flex: 1; text-align: center; min-width: 72px; }
.main { padding: 20px 12px 28px; }
.card { padding: 16px; }
.topbar { padding: 12px 16px; flex-wrap: wrap; }
table, thead, tbody, th, td, tr { display: block; }
thead { display: none; }
tr { border-top: 1px solid rgba(240,246,252,0.08); padding: 12px 0; }
td { border-top: none; padding: 6px 0; }
td::before {
display: block; color: #8b949e; font-size: 11px;
margin-bottom: 4px; text-transform: uppercase;
content: attr(data-label);
}
}
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<a href="/dashboard" class="sidebar-logo">secrets</a>
<nav class="sidebar-menu">
<a href="/dashboard" class="sidebar-link" data-i18n="navMcp">MCP</a>
<a href="/entries" class="sidebar-link" data-i18n="navEntries">条目</a>
<a href="/trash" class="sidebar-link active" data-i18n="navTrash">回收站</a>
<a href="/audit" class="sidebar-link" data-i18n="navAudit">审计</a>
</nav>
</aside>
<div class="content-shell">
<div class="topbar">
<span class="topbar-spacer"></span>
<span class="nav-user">{{ user_name }}{% if !user_email.is_empty() %} · {{ user_email }}{% endif %}</span>
<div class="lang-bar">
<button class="lang-btn" onclick="setLang('zh-CN')"></button>
<button class="lang-btn" onclick="setLang('zh-TW')"></button>
<button class="lang-btn" onclick="setLang('en')">EN</button>
</div>
<form action="/auth/logout" method="post" style="display:inline">
<button type="submit" class="btn-sign-out" data-i18n="signOut">退出</button>
</form>
</div>
<main class="main">
<section class="card">
<div class="card-title" data-i18n="trashTitle">回收站</div>
<div class="card-subtitle" data-i18n="trashSubtitle">已删除条目会保留 3 个月,可在此恢复或永久删除。</div>
{% if entries.is_empty() %}
<div class="empty" data-i18n="emptyTrash">回收站为空。</div>
{% else %}
<table>
<thead>
<tr>
<th data-i18n="colName">名称</th>
<th data-i18n="colType">类型</th>
<th data-i18n="colFolder">文件夹</th>
<th data-i18n="colDeletedAt">删除时间</th>
<th data-i18n="colActions">操作</th>
</tr>
</thead>
<tbody>
{% for entry in entries %}
<tr data-trash-entry-id="{{ entry.id }}">
<td class="mono" data-label="名称">{{ entry.name }}</td>
<td class="mono" data-label="类型">{{ entry.entry_type }}</td>
<td class="mono" data-label="文件夹">{{ entry.folder }}</td>
<td class="mono" data-label="删除时间" title="{{ entry.deleted_at_iso }}">{{ entry.deleted_at_label }}</td>
<td data-label="操作">
<div class="row-actions">
<button type="button" class="btn btn-restore" data-i18n="btnRestore">恢复</button>
<button type="button" class="btn btn-danger btn-purge" data-i18n="btnPurge">永久删除</button>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endif %}
{% if total_count > 0 %}
<div class="pagination">
{% if current_page > 1 %}
<a class="page-btn" href="/trash?page={{ current_page - 1 }}" data-i18n="prevPage">上一页</a>
{% else %}
<span class="page-btn disabled" data-i18n="prevPage">上一页</span>
{% endif %}
<span class="page-info">{{ current_page }} / {{ total_pages }}</span>
{% if current_page < total_pages %}
<a class="page-btn" href="/trash?page={{ current_page + 1 }}" data-i18n="nextPage">下一页</a>
{% else %}
<span class="page-btn disabled" data-i18n="nextPage">下一页</span>
{% endif %}
</div>
{% endif %}
</section>
</main>
</div>
</div>
<script src="/static/i18n.js?v={{ version }}"></script>
<script>
(function () {
I18N_PAGE = {
'zh-CN': {
navMcp: 'MCP', navEntries: '条目', navTrash: '回收站', navAudit: '审计',
signOut: '退出', trashTitle: '回收站', trashSubtitle: '已删除条目会保留 3 个月,可在此恢复或永久删除。',
emptyTrash: '回收站为空。', colName: '名称', colType: '类型', colFolder: '文件夹',
colDeletedAt: '删除时间', colActions: '操作', btnRestore: '恢复', btnPurge: '永久删除',
prevPage: '上一页', nextPage: '下一页',
mobileLabelName: '名称', mobileLabelType: '类型', mobileLabelFolder: '文件夹',
mobileLabelDeletedAt: '删除时间', mobileLabelActions: '操作'
},
'zh-TW': {
navMcp: 'MCP', navEntries: '條目', navTrash: '回收站', navAudit: '審計',
signOut: '退出', trashTitle: '回收站', trashSubtitle: '已刪除條目會保留 3 個月,可在此恢復或永久刪除。',
emptyTrash: '回收站為空。', colName: '名稱', colType: '類型', colFolder: '文件夾',
colDeletedAt: '刪除時間', colActions: '操作', btnRestore: '恢復', btnPurge: '永久刪除',
prevPage: '上一頁', nextPage: '下一頁',
mobileLabelName: '名稱', mobileLabelType: '類型', mobileLabelFolder: '文件夾',
mobileLabelDeletedAt: '刪除時間', mobileLabelActions: '操作'
},
en: {
navMcp: 'MCP', navEntries: 'Entries', navTrash: 'Trash', navAudit: 'Audit',
signOut: 'Sign out', trashTitle: 'Trash', trashSubtitle: 'Deleted entries are kept for 3 months. Restore or permanently delete them here.',
emptyTrash: 'Trash is empty.', colName: 'Name', colType: 'Type', colFolder: 'Folder',
colDeletedAt: 'Deleted at', colActions: 'Actions', btnRestore: 'Restore', btnPurge: 'Purge',
prevPage: 'Previous', nextPage: 'Next',
mobileLabelName: 'Name', mobileLabelType: 'Type', mobileLabelFolder: 'Folder',
mobileLabelDeletedAt: 'Deleted at', mobileLabelActions: 'Actions'
}
};
window.applyPageLang = function () {
document.querySelectorAll('tbody tr').forEach(function (tr) {
['Name', 'Type', 'Folder', 'DeletedAt', 'Actions'].forEach(function (col) {
var td = tr.querySelector('[data-label]');
});
});
};
applyLang();
})();
document.querySelectorAll('tr[data-trash-entry-id]').forEach(function (row) {
var entryId = row.getAttribute('data-trash-entry-id');
var restoreButton = row.querySelector('.btn-restore');
var purgeButton = row.querySelector('.btn-purge');
restoreButton.addEventListener('click', function () {
fetch('/api/trash/' + encodeURIComponent(entryId) + '/restore', {
method: 'POST',
credentials: 'same-origin'
}).then(function (response) {
return response.json().then(function (body) {
if (!response.ok) throw new Error(body.error || ('HTTP ' + response.status));
return body;
});
}).then(function () {
row.remove();
if (!document.querySelector('tr[data-trash-entry-id]')) window.location.reload();
}).catch(function (error) {
window.alert(error.message || String(error));
});
});
purgeButton.addEventListener('click', function () {
if (!window.confirm(t('confirmPurge') || '确定永久删除该条目?此操作不可撤销。')) return;
fetch('/api/trash/' + encodeURIComponent(entryId), {
method: 'DELETE',
credentials: 'same-origin'
}).then(function (response) {
return response.json().then(function (body) {
if (!response.ok) throw new Error(body.error || ('HTTP ' + response.status));
return body;
});
}).then(function () {
row.remove();
if (!document.querySelector('tr[data-trash-entry-id]')) window.location.reload();
}).catch(function (error) {
window.alert(error.message || String(error));
});
});
});
</script>
</body>
</html>