1711 lines
72 KiB
HTML
1711 lines
72 KiB
HTML
<!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: #0d1117; color: #c9d1d9; 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; }
|
||
.folder-tabs {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-bottom: 14px;
|
||
}
|
||
.folder-tab {
|
||
text-decoration: none;
|
||
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: #0d1117;
|
||
}
|
||
.folder-tab:hover { color: #c9d1d9; border-color: rgba(240,246,252,0.2); }
|
||
.folder-tab.active {
|
||
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: 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: #8b949e; font-weight: 500; }
|
||
.filter-field input {
|
||
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: #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 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%;
|
||
box-sizing: border-box;
|
||
appearance: none;
|
||
-webkit-appearance: none;
|
||
-moz-appearance: none;
|
||
}
|
||
.filter-field input:focus,
|
||
.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: 10px; border: none; background: #388bfd; color: #fff;
|
||
font-size: 13px; font-weight: 600; cursor: pointer;
|
||
}
|
||
.btn-filter:hover { background: #58a6ff; }
|
||
.btn-clear {
|
||
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 { border-color: rgba(56,139,253,0.45); color: #fff; }
|
||
.btn-col-toggle {
|
||
padding: 8px 12px; border-radius: 10px; border: 1px solid rgba(240,246,252,0.12);
|
||
background: transparent; color: #8b949e; font-size: 16px; cursor: pointer;
|
||
}
|
||
.btn-col-toggle:hover { border-color: rgba(56,139,253,0.45); color: #fff; }
|
||
.col-menu { position: relative; }
|
||
.col-panel {
|
||
display: none; position: absolute; top: calc(100% + 6px); right: 0; z-index: 20;
|
||
background: #161b22; border: 1px solid rgba(240,246,252,0.12); border-radius: 10px;
|
||
padding: 10px 14px; min-width: 180px; box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
||
}
|
||
.col-panel.open { display: block; }
|
||
.col-panel-group { font-size: 11px; color: #6e7681; text-transform: uppercase; letter-spacing: 0.5px; margin: 8px 0 4px; }
|
||
.col-panel-group:first-child { margin-top: 0; }
|
||
.col-panel-item {
|
||
display: flex; align-items: center; gap: 8px; padding: 4px 0;
|
||
font-size: 13px; color: #c9d1d9; cursor: pointer; user-select: none;
|
||
}
|
||
.col-panel-item input[type="checkbox"] {
|
||
accent-color: var(--accent); width: 15px; height: 15px; cursor: pointer;
|
||
}
|
||
.col-panel-item.disabled { color: #6e7681; cursor: default; }
|
||
.col-panel-item.disabled input[type="checkbox"] { cursor: default; }
|
||
.empty { color: #8b949e; font-size: 14px; padding: 20px 0; }
|
||
.table-wrap {
|
||
overflow: auto;
|
||
border: 1px solid rgba(240,246,252,0.08);
|
||
border-radius: 12px;
|
||
background: #0d1117;
|
||
}
|
||
table {
|
||
width: 100%;
|
||
min-width: 1100px;
|
||
border-collapse: separate;
|
||
border-spacing: 0;
|
||
table-layout: fixed;
|
||
}
|
||
col[data-col="name"] { width: 220px; }
|
||
col[data-col="type"] { width: 120px; }
|
||
col[data-col="notes"] { width: 320px; }
|
||
col[data-col="tags"] { width: 220px; }
|
||
col[data-col="relations"] { width: 220px; }
|
||
col[data-col="secrets"] { width: 320px; }
|
||
col[data-col="actions"] { width: 132px; }
|
||
th, td { text-align: left; vertical-align: middle; padding: 14px 12px; border-top: 1px solid rgba(240,246,252,0.08); }
|
||
th {
|
||
color: #8b949e;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 2;
|
||
background: #111827;
|
||
text-align: center;
|
||
vertical-align: middle;
|
||
}
|
||
td { font-size: 13px; line-height: 1.45; color: #c9d1d9; }
|
||
tbody tr:nth-child(2n) td { background: rgba(255, 255, 255, 0.01); }
|
||
tbody tr:nth-child(2n) td.col-name { background: #0f1620; }
|
||
.mono { font-family: 'JetBrains Mono', monospace; }
|
||
.col-type { text-align: center; vertical-align: middle; }
|
||
.col-secrets { vertical-align: middle; }
|
||
.col-secrets .secret-list { max-height: 120px; overflow: auto; }
|
||
.col-actions { text-align: right; vertical-align: middle; }
|
||
.col-name { position: sticky; left: 0; z-index: 1; background: #0d1117; overflow: hidden; }
|
||
th.col-name { z-index: 3; background: #111827; }
|
||
.col-name::after {
|
||
content: ''; position: absolute; top: 0; right: -8px; bottom: 0; width: 8px;
|
||
background: linear-gradient(to right, rgba(0,0,0,0.15), transparent);
|
||
pointer-events: none;
|
||
}
|
||
th[data-col="actions"], td[data-col="actions"] { text-align: right; }
|
||
[data-col].col-hidden { display: none !important; }
|
||
.cell-name, .cell-tags-val {
|
||
overflow-wrap: anywhere;
|
||
word-break: break-word;
|
||
}
|
||
.cell-notes { min-width: 260px; max-width: 360px; }
|
||
.notes-scroll {
|
||
height: calc(1.5em * 2 + 16px);
|
||
min-height: calc(1.5em * 2 + 16px);
|
||
overflow: auto;
|
||
resize: vertical;
|
||
white-space: pre-wrap;
|
||
word-break: break-word;
|
||
padding: 8px;
|
||
background: #0d1117;
|
||
border: 1px solid rgba(240,246,252,0.08);
|
||
border-radius: 10px;
|
||
font-size: 12px;
|
||
}
|
||
.detail {
|
||
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;
|
||
}
|
||
.col-actions { white-space: nowrap; }
|
||
.row-actions { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; align-items: center; }
|
||
.secret-list { display: flex; flex-wrap: wrap; gap: 6px; max-width: 100%; }
|
||
.secret-chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
border: 1px solid rgba(240,246,252,0.08);
|
||
border-radius: 999px;
|
||
padding: 3px 8px;
|
||
font-size: 11px;
|
||
background: #161b22;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
max-width: 100%;
|
||
min-width: 0;
|
||
}
|
||
.secret-name {
|
||
min-width: 0;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
}
|
||
.secret-type {
|
||
color: #8b949e;
|
||
border-left: 1px solid rgba(240,246,252,0.08);
|
||
padding-left: 6px;
|
||
}
|
||
a.secret-chip {
|
||
color: var(--accent);
|
||
text-decoration: none;
|
||
cursor: pointer;
|
||
transition: color 0.15s, border-color 0.15s, background 0.15s;
|
||
}
|
||
a.secret-chip:hover {
|
||
color: var(--accent-hover);
|
||
border-color: var(--accent);
|
||
background: rgba(88,166,255,0.12);
|
||
}
|
||
.btn-unlink-secret {
|
||
border: none;
|
||
background: transparent;
|
||
color: #f85149;
|
||
cursor: pointer;
|
||
font-size: 12px;
|
||
line-height: 1;
|
||
padding: 0;
|
||
}
|
||
.secret-edit-row {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0;
|
||
padding: 4px 0;
|
||
}
|
||
.secret-edit-main {
|
||
display: grid;
|
||
grid-template-columns: 120px minmax(0, 1fr) auto;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.secret-name-input {
|
||
width: 100%;
|
||
min-width: 0;
|
||
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: rgba(56,139,253,0.5);
|
||
}
|
||
.secret-name-input.invalid {
|
||
border-color: #f85149;
|
||
}
|
||
.secret-name-input.valid {
|
||
border-color: #3fb950;
|
||
}
|
||
.secret-type-select {
|
||
width: 100%;
|
||
min-width: 0;
|
||
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;
|
||
outline: none;
|
||
cursor: pointer;
|
||
}
|
||
.secret-type-select:focus {
|
||
border-color: rgba(56,139,253,0.5);
|
||
}
|
||
.secret-type-select.invalid {
|
||
border-color: #f85149;
|
||
}
|
||
.btn-unlink-secret {
|
||
border: none;
|
||
background: transparent;
|
||
color: #f85149;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
line-height: 1;
|
||
padding: 0 4px;
|
||
}
|
||
.secret-name-status {
|
||
font-size: 11px;
|
||
line-height: 1.3;
|
||
margin-top: 2px;
|
||
color: #8b949e;
|
||
}
|
||
.secret-name-status:empty {
|
||
display: none;
|
||
}
|
||
.secret-name-status.checking {
|
||
color: #58a6ff;
|
||
}
|
||
.secret-name-status.error {
|
||
color: #f85149;
|
||
}
|
||
.secret-name-status.success {
|
||
color: #3fb950;
|
||
}
|
||
.btn-row {
|
||
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 { 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;
|
||
display: flex; align-items: center; justify-content: center; padding: 16px;
|
||
}
|
||
.modal-overlay[hidden] { display: none !important; }
|
||
.modal {
|
||
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: 18px; font-weight: 700; margin-bottom: 14px; color: #fff; }
|
||
.modal-field { margin-bottom: 12px; }
|
||
.modal-field label { display: block; font-size: 12px; color: #8b949e; margin-bottom: 5px; }
|
||
.modal-field input, .modal-field textarea {
|
||
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; }
|
||
#edit-notes {
|
||
height: calc(1.5em * 2 + 16px);
|
||
min-height: calc(1.5em * 2 + 16px);
|
||
}
|
||
.modal-field textarea.metadata-edit { min-height: 140px; }
|
||
.modal-readonly-value {
|
||
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: 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 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-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 rgba(240,246,252,0.08); padding: 12px 0; }
|
||
td { border-top: none; padding: 6px 0; max-width: none; }
|
||
td::before {
|
||
display: block; color: #8b949e; font-size: 11px;
|
||
margin-bottom: 4px; text-transform: uppercase;
|
||
content: attr(data-label);
|
||
}
|
||
.col-name, .col-type, .col-actions { text-align: left; }
|
||
.col-name { position: static; }
|
||
.col-name::after { display: none; }
|
||
.col-panel { position: fixed; left: 12px; right: 12px; width: auto; }
|
||
th, td { vertical-align: top; }
|
||
.row-actions { justify-content: flex-start; }
|
||
.detail, .notes-scroll, .secret-list { max-width: none; }
|
||
}
|
||
.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;
|
||
}
|
||
.view-secret-row {
|
||
display: flex; flex-direction: column; gap: 4px; padding: 8px 0;
|
||
border-bottom: 1px solid rgba(240,246,252,0.08);
|
||
}
|
||
.view-secret-row:last-child { border-bottom: none; }
|
||
.view-secret-header {
|
||
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
||
}
|
||
.view-secret-name {
|
||
font-family: 'JetBrains Mono', monospace; font-size: 12px;
|
||
color: #c9d1d9; font-weight: 600;
|
||
}
|
||
.view-secret-type {
|
||
font-family: 'JetBrains Mono', monospace; font-size: 11px;
|
||
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: #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: #c9d1d9; line-height: 1.5;
|
||
}
|
||
|
||
.btn-icon {
|
||
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 { border-color: rgba(56,139,253,0.45); color: #fff; }
|
||
.view-locked-msg {
|
||
font-size: 13px; color: #8b949e; padding: 16px 0;
|
||
line-height: 1.6; text-align: center;
|
||
}
|
||
.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: #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: rgba(56,139,253,0.5); }
|
||
.view-secret-type-select {
|
||
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: rgba(56,139,253,0.5); }
|
||
.btn-view-edit { color: #58a6ff; }
|
||
.btn-view-save { color: #3fb950; }
|
||
.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">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>
|
||
|
||
<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="entriesTitle">我的条目</div>
|
||
|
||
<div class="folder-tabs">
|
||
{% for tab in folder_tabs %}
|
||
<a href="{{ tab.href }}" class="folder-tab{% if tab.active %} active{% endif %}" {% if loop.first %}data-all-tab="1" data-count="{{ tab.count }}"{% endif %}>
|
||
{{ tab.name }} ({{ tab.count }})
|
||
</a>
|
||
{% endfor %}
|
||
</div>
|
||
|
||
<form class="filter-bar" method="get" action="/entries">
|
||
{% if !filter_folder.is_empty() %}
|
||
<input type="hidden" name="folder" value="{{ filter_folder }}">
|
||
{% endif %}
|
||
<div class="filter-field">
|
||
<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();">
|
||
<option value="" data-i18n="filterTypeAll">全部</option>
|
||
{% for t in type_options %}
|
||
<option value="{{ t }}"{% if filter_type.as_str() == t.as_str() %} selected{% endif %}>{{ t }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
<div class="filter-actions">
|
||
<button type="submit" class="btn-filter" data-i18n="filterSubmit">筛选</button>
|
||
<a href="/entries" class="btn-clear" data-i18n="filterClear">清空</a>
|
||
<div class="col-menu">
|
||
<button type="button" class="btn-col-toggle" id="col-toggle-btn" data-i18n-title="columnSettings" title="显示列">▥</button>
|
||
<div class="col-panel" id="col-panel"></div>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
|
||
{% if entries.is_empty() %}
|
||
<div class="empty" data-i18n="emptyEntries">暂无条目。</div>
|
||
{% else %}
|
||
<div class="table-wrap">
|
||
<table>
|
||
<colgroup>
|
||
<col data-col="name">
|
||
<col data-col="type">
|
||
<col data-col="notes">
|
||
<col data-col="tags">
|
||
<col data-col="relations">
|
||
<col data-col="secrets">
|
||
<col data-col="actions">
|
||
</colgroup>
|
||
<thead>
|
||
<tr>
|
||
<th data-col="name" data-i18n="colName">名称</th>
|
||
<th data-col="type" data-i18n="colType">类型</th>
|
||
<th data-col="notes" data-i18n="colNotes">备注</th>
|
||
<th data-col="tags" data-i18n="colTags">标签</th>
|
||
<th data-col="relations" data-i18n="colRelations">关联</th>
|
||
<th data-col="secrets" data-i18n="colSecrets">密文</th>
|
||
<th data-col="actions" 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-entry-parents="{{ entry.parents_json }}" data-updated-at="{{ entry.updated_at_iso }}">
|
||
<td class="col-name mono cell-name" data-col="name" data-label="名称">{{ entry.name }}</td>
|
||
<td class="col-type mono cell-type" data-col="type" data-label="类型">{{ entry.entry_type }}</td>
|
||
<td class="col-notes cell-notes" data-col="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-col="tags" data-label="标签">{{ entry.tags }}</td>
|
||
<td class="col-relations" data-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-col="secrets" data-label="密文">
|
||
<div class="secret-list">
|
||
{% for s in entry.secrets %}
|
||
<span class="secret-chip">
|
||
<span class="secret-name" title="{{ s.name }}">{{ s.name }}</span>
|
||
<span class="secret-type">{{ s.secret_type }}</span>
|
||
</span>
|
||
{% endfor %}
|
||
</div>
|
||
</td>
|
||
<td class="col-actions" data-col="actions" data-label="操作">
|
||
<div class="row-actions">
|
||
<button type="button" class="btn-row btn-view-secrets" data-i18n="rowView">查看密文</button>
|
||
<button type="button" class="btn-row btn-edit" data-i18n="rowEdit">编辑条目</button>
|
||
<button type="button" class="btn-row danger btn-del" data-i18n="rowDelete">删除</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
{% endfor %}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
{% 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 %}{% 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 %}{% 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 %}
|
||
</div>
|
||
{% endif %}
|
||
{% endif %}
|
||
</section>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="edit-overlay" class="modal-overlay" hidden>
|
||
<div class="modal modal-wide" role="dialog" aria-modal="true" aria-labelledby="edit-title">
|
||
<div class="modal-title" id="edit-title" data-i18n="modalTitle">编辑条目信息</div>
|
||
<div id="edit-error" class="modal-error"></div>
|
||
<div class="modal-field"><label for="edit-name" data-i18n="modalName">名称</label><input id="edit-name" type="text" autocomplete="off"></div>
|
||
<div class="modal-field"><label for="edit-type" data-i18n="modalType">类型</label><input id="edit-type" type="text" autocomplete="off"></div>
|
||
<div class="modal-field"><label for="edit-folder" data-i18n="modalFolder">文件夹</label><input id="edit-folder" type="text" autocomplete="off"></div>
|
||
<div class="modal-field"><label for="edit-notes" data-i18n="modalNotes">备注</label><textarea id="edit-notes"></textarea></div>
|
||
<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>
|
||
</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:#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?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);
|
||
(function () {
|
||
I18N_PAGE = {
|
||
'zh-CN': {
|
||
pageTitle: 'Secrets — 条目',
|
||
columnSettings: '显示列',
|
||
fixedColumns: '固定列',
|
||
optionalColumns: '可选列',
|
||
navTrash: '回收站',
|
||
entriesTitle: '我的条目',
|
||
allTab: '全部',
|
||
filterNameLabel: '名称',
|
||
filterNamePlaceholder: '输入关键字',
|
||
filterMetadataLabel: '元数据值',
|
||
filterMetadataPlaceholder: '搜索元数据值',
|
||
filterTypeLabel: '类型',
|
||
filterTypeAll: '全部',
|
||
filterSubmit: '筛选',
|
||
filterClear: '清空',
|
||
emptyEntries: '暂无条目。',
|
||
colName: '名称',
|
||
colType: '类型',
|
||
colNotes: '备注',
|
||
colTags: '标签',
|
||
colRelations: '关联',
|
||
colSecrets: '密文',
|
||
colActions: '操作',
|
||
relationParentBadge: '上级',
|
||
relationChildBadge: '下级',
|
||
rowEdit: '编辑条目',
|
||
rowDelete: '删除',
|
||
modalTitle: '编辑条目信息',
|
||
modalName: '名称',
|
||
modalType: '类型',
|
||
modalFolder: '文件夹',
|
||
modalNotes: '备注',
|
||
modalTags: '标签(逗号分隔)',
|
||
modalUpdated: '更新',
|
||
modalMetadata: '元数据(JSON 对象)',
|
||
modalParents: '上级条目',
|
||
modalSecrets: '密文',
|
||
modalCancel: '取消',
|
||
modalSave: '保存',
|
||
mobileLabelName: '名称',
|
||
mobileLabelType: '类型',
|
||
mobileLabelNotes: '备注',
|
||
mobileLabelTags: '标签',
|
||
mobileLabelRelations: '关联',
|
||
mobileLabelSecrets: '密文',
|
||
mobileLabelActions: '操作',
|
||
errInvalidJson: '元数据不是合法 JSON',
|
||
errMetadataObject: '元数据必须是 JSON 对象',
|
||
confirmDeleteEntry: '确定删除条目「{name}」?',
|
||
confirmUnlinkSecret: '确定解除密文关联「{name}」?',
|
||
unlinkTitle: '解除关联',
|
||
renameSecretTitle: '重命名密文',
|
||
renameSecretPlaceholder: '输入新名称',
|
||
errRenameSecret: '重命名失败:{error}',
|
||
checkingSecretName: '检查中...',
|
||
secretNameAvailable: '名称可用',
|
||
secretNameTaken: '该名称已被使用',
|
||
secretNameInvalid: '名称不合法',
|
||
secretNameCheckError: '校验失败,请重试',
|
||
secretNameFixBeforeSave: '请先修复密文名称校验问题后再保存',
|
||
secretTypePlaceholder: '选择类型',
|
||
secretTypeInvalid: '类型不能为空',
|
||
prevPage: '上一页',
|
||
nextPage: '下一页',
|
||
rowView: '查看密文',
|
||
viewTitle: '查看条目密文',
|
||
viewNoSecrets: '该条目没有关联的密文字段。',
|
||
viewLockedMsg: '请先前往 <a href="/dashboard">MCP 配置页</a> 解锁密码短语,然后再查看密文。',
|
||
viewDecryptError: '解密失败,请确认密码短语与加密时一致。',
|
||
viewCopy: '复制',
|
||
viewCopied: '已复制',
|
||
viewLoading: '解密中…',
|
||
viewSaveChanges: '保存更改',
|
||
viewChangesSaved: '已保存',
|
||
viewUnlinkConfirm: '确定解除密文关联「{name}」?',
|
||
parentSearchPlaceholder: '按名称搜索条目',
|
||
parentSearchEmpty: '没有匹配的条目',
|
||
removeParent: '移除上级',
|
||
},
|
||
'zh-TW': {
|
||
pageTitle: 'Secrets — 條目',
|
||
columnSettings: '顯示列',
|
||
fixedColumns: '固定列',
|
||
optionalColumns: '可選列',
|
||
navTrash: '回收站',
|
||
entriesTitle: '我的條目',
|
||
allTab: '全部',
|
||
filterNameLabel: '名稱',
|
||
filterNamePlaceholder: '輸入關鍵字',
|
||
filterMetadataLabel: '中繼資料值',
|
||
filterMetadataPlaceholder: '搜尋中繼資料值',
|
||
filterTypeLabel: '類型',
|
||
filterTypeAll: '全部',
|
||
filterSubmit: '篩選',
|
||
filterClear: '清除',
|
||
emptyEntries: '暫無條目。',
|
||
colName: '名稱',
|
||
colType: '類型',
|
||
colNotes: '備註',
|
||
colTags: '標籤',
|
||
colRelations: '關聯',
|
||
colSecrets: '密文',
|
||
colActions: '操作',
|
||
relationParentBadge: '上級',
|
||
relationChildBadge: '下級',
|
||
rowEdit: '編輯條目',
|
||
rowDelete: '刪除',
|
||
modalTitle: '編輯條目資訊',
|
||
modalName: '名稱',
|
||
modalType: '類型',
|
||
modalFolder: '資料夾',
|
||
modalNotes: '備註',
|
||
modalTags: '標籤(逗號分隔)',
|
||
modalUpdated: '更新時間',
|
||
modalMetadata: '中繼資料(JSON 物件)',
|
||
modalParents: '上級條目',
|
||
modalSecrets: '密文',
|
||
modalCancel: '取消',
|
||
modalSave: '儲存',
|
||
mobileLabelName: '名稱',
|
||
mobileLabelType: '類型',
|
||
mobileLabelNotes: '備註',
|
||
mobileLabelTags: '標籤',
|
||
mobileLabelRelations: '關聯',
|
||
mobileLabelSecrets: '密文',
|
||
mobileLabelActions: '操作',
|
||
errInvalidJson: '中繼資料不是合法 JSON',
|
||
errMetadataObject: '中繼資料必須是 JSON 物件',
|
||
confirmDeleteEntry: '確定刪除條目「{name}」?',
|
||
confirmUnlinkSecret: '確定解除密文關聯「{name}」?',
|
||
unlinkTitle: '解除關聯',
|
||
renameSecretTitle: '重新命名密文',
|
||
renameSecretPlaceholder: '輸入新名稱',
|
||
errRenameSecret: '重新命名失敗:{error}',
|
||
checkingSecretName: '檢查中...',
|
||
secretNameAvailable: '名稱可用',
|
||
secretNameTaken: '該名稱已被使用',
|
||
secretNameInvalid: '名稱不合法',
|
||
secretNameCheckError: '校驗失敗,請重試',
|
||
secretNameFixBeforeSave: '請先修復密文名稱校驗問題後再儲存',
|
||
secretTypePlaceholder: '選擇類型',
|
||
secretTypeInvalid: '類型不能為空',
|
||
prevPage: '上一頁',
|
||
nextPage: '下一頁',
|
||
rowView: '查看密文',
|
||
viewTitle: '查看條目密文',
|
||
viewNoSecrets: '該條目沒有關聯的密文欄位。',
|
||
viewLockedMsg: '請先前往 <a href="/dashboard">MCP 設定頁</a> 解鎖密碼短語,再查看密文。',
|
||
viewDecryptError: '解密失敗,請確認密碼短語與加密時一致。',
|
||
viewCopy: '複製',
|
||
viewCopied: '已複製',
|
||
viewLoading: '解密中…',
|
||
viewSaveChanges: '儲存變更',
|
||
viewChangesSaved: '已儲存',
|
||
viewUnlinkConfirm: '確定解除密文關聯「{name}」?',
|
||
parentSearchPlaceholder: '依名稱搜尋條目',
|
||
parentSearchEmpty: '沒有符合的條目',
|
||
removeParent: '移除上級',
|
||
},
|
||
en: {
|
||
pageTitle: 'Secrets — Entries',
|
||
columnSettings: 'Columns',
|
||
fixedColumns: 'Fixed',
|
||
optionalColumns: 'Optional',
|
||
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',
|
||
filterClear: 'Clear',
|
||
emptyEntries: 'No entries.',
|
||
colName: 'Name',
|
||
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',
|
||
modalName: 'Name',
|
||
modalType: 'Type',
|
||
modalFolder: 'Folder',
|
||
modalNotes: 'Notes',
|
||
modalTags: 'Tags (comma separated)',
|
||
modalUpdated: 'Updated',
|
||
modalMetadata: 'Metadata (JSON object)',
|
||
modalParents: 'Parent entries',
|
||
modalSecrets: 'Secrets',
|
||
modalCancel: 'Cancel',
|
||
modalSave: 'Save',
|
||
mobileLabelName: 'Name',
|
||
mobileLabelType: 'Type',
|
||
mobileLabelNotes: 'Notes',
|
||
mobileLabelTags: 'Tags',
|
||
mobileLabelRelations: 'Relations',
|
||
mobileLabelSecrets: 'Secrets',
|
||
mobileLabelActions: 'Actions',
|
||
errInvalidJson: 'Metadata is not valid JSON',
|
||
errMetadataObject: 'Metadata must be a JSON object',
|
||
confirmDeleteEntry: 'Delete entry "{name}"?',
|
||
confirmUnlinkSecret: 'Unlink secret "{name}"?',
|
||
unlinkTitle: 'Unlink',
|
||
renameSecretTitle: 'Rename secret',
|
||
renameSecretPlaceholder: 'Enter new name',
|
||
errRenameSecret: 'Failed to rename: {error}',
|
||
checkingSecretName: 'Checking...',
|
||
secretNameAvailable: 'Name available',
|
||
secretNameTaken: 'Name already taken',
|
||
secretNameInvalid: 'Invalid name',
|
||
secretNameCheckError: 'Validation failed, please retry',
|
||
secretNameFixBeforeSave: 'Fix secret name validation errors before saving',
|
||
secretTypePlaceholder: 'Select type',
|
||
secretTypeInvalid: 'Type cannot be empty',
|
||
prevPage: 'Previous',
|
||
nextPage: 'Next',
|
||
rowView: 'View secrets',
|
||
viewTitle: 'View entry secrets',
|
||
viewNoSecrets: 'This entry has no associated secret fields.',
|
||
viewLockedMsg: 'Please go to the <a href="/dashboard">MCP config page</a> to unlock your passphrase first.',
|
||
viewDecryptError: 'Decryption failed. Please verify your passphrase matches the one used when encrypting.',
|
||
viewCopy: 'Copy',
|
||
viewCopied: 'Copied',
|
||
viewLoading: 'Decrypting…',
|
||
viewSaveChanges: 'Save changes',
|
||
viewChangesSaved: 'Saved',
|
||
viewUnlinkConfirm: 'Unlink secret "{name}"?',
|
||
parentSearchPlaceholder: 'Search entries by name',
|
||
parentSearchEmpty: 'No matching entries',
|
||
removeParent: 'Remove parent',
|
||
}
|
||
};
|
||
|
||
window.applyPageLang = function () {
|
||
var allTab = document.querySelector('.folder-tab[data-all-tab="1"]');
|
||
if (allTab) {
|
||
var count = allTab.getAttribute('data-count') || '0';
|
||
allTab.textContent = t('allTab') + ' (' + count + ')';
|
||
}
|
||
document.querySelectorAll('tr[data-entry-id]').forEach(function (tr) {
|
||
var map = {
|
||
'.col-name': 'mobileLabelName',
|
||
'.col-type': 'mobileLabelType',
|
||
'.col-notes': 'mobileLabelNotes',
|
||
'.col-tags': 'mobileLabelTags',
|
||
'.col-relations': 'mobileLabelRelations',
|
||
'.col-secrets': 'mobileLabelSecrets',
|
||
'.col-actions': 'mobileLabelActions'
|
||
};
|
||
Object.keys(map).forEach(function (sel) {
|
||
var td = tr.querySelector(sel);
|
||
if (td) td.setAttribute('data-label', t(map[sel]));
|
||
});
|
||
});
|
||
rebuildColPanel();
|
||
};
|
||
|
||
var COL_ORDER = ['name', 'type', 'notes', 'tags', 'relations', 'secrets', 'actions'];
|
||
var COL_ALWAYS_ON = { name: true, actions: true };
|
||
var COL_DEFAULTS = { name: true, type: true, notes: false, tags: true, relations: true, secrets: false, actions: true };
|
||
var COL_STORAGE_KEY = 'entries_col_vis';
|
||
var colPanel = document.getElementById('col-panel');
|
||
var colToggleBtn = document.getElementById('col-toggle-btn');
|
||
|
||
function getColVis() {
|
||
try {
|
||
var saved = localStorage.getItem(COL_STORAGE_KEY);
|
||
if (saved) { var parsed = JSON.parse(saved); if (parsed && typeof parsed === 'object') return parsed; }
|
||
} catch (e) {}
|
||
var defaults = {};
|
||
COL_ORDER.forEach(function (col) { defaults[col] = COL_DEFAULTS[col]; });
|
||
return defaults;
|
||
}
|
||
|
||
function saveColVis(vis) {
|
||
try { localStorage.setItem(COL_STORAGE_KEY, JSON.stringify(vis)); } catch (e) {}
|
||
}
|
||
|
||
function applyColVis(vis) {
|
||
COL_ORDER.forEach(function (col) {
|
||
var visible = vis[col] !== false;
|
||
document.querySelectorAll('[data-col="' + col + '"]').forEach(function (el) {
|
||
if (visible) {
|
||
el.classList.remove('col-hidden');
|
||
} else {
|
||
el.classList.add('col-hidden');
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
function rebuildColPanel() {
|
||
var vis = getColVis();
|
||
colPanel.innerHTML = '';
|
||
var fixedCols = ['name', 'actions'];
|
||
var optionalCols = COL_ORDER.filter(function (c) { return fixedCols.indexOf(c) === -1; });
|
||
|
||
function renderGroup(cols, groupKey) {
|
||
var groupLabel = document.createElement('div');
|
||
groupLabel.className = 'col-panel-group';
|
||
groupLabel.textContent = t(groupKey);
|
||
colPanel.appendChild(groupLabel);
|
||
cols.forEach(function (col) {
|
||
var item = document.createElement('label');
|
||
item.className = 'col-panel-item';
|
||
var cb = document.createElement('input');
|
||
cb.type = 'checkbox';
|
||
var i18nKey = 'col' + col.charAt(0).toUpperCase() + col.slice(1);
|
||
cb.checked = vis[col] !== false;
|
||
if (COL_ALWAYS_ON[col]) {
|
||
cb.disabled = true;
|
||
item.classList.add('disabled');
|
||
}
|
||
cb.addEventListener('change', function () {
|
||
vis[col] = cb.checked;
|
||
saveColVis(vis);
|
||
applyColVis(vis);
|
||
});
|
||
var span = document.createElement('span');
|
||
span.textContent = t(i18nKey) || col;
|
||
item.appendChild(cb);
|
||
item.appendChild(span);
|
||
colPanel.appendChild(item);
|
||
});
|
||
}
|
||
|
||
renderGroup(fixedCols, 'fixedColumns');
|
||
renderGroup(optionalCols, 'optionalColumns');
|
||
}
|
||
|
||
var colMenu = document.querySelector('.col-menu');
|
||
colToggleBtn.addEventListener('click', function (e) {
|
||
e.stopPropagation();
|
||
colPanel.classList.toggle('open');
|
||
});
|
||
document.addEventListener('click', function (e) {
|
||
if (!colMenu.contains(e.target)) {
|
||
colPanel.classList.remove('open');
|
||
}
|
||
});
|
||
|
||
applyColVis(getColVis());
|
||
rebuildColPanel();
|
||
|
||
var editOverlay = document.getElementById('edit-overlay');
|
||
var editError = document.getElementById('edit-error');
|
||
var editFolder = document.getElementById('edit-folder');
|
||
var editType = document.getElementById('edit-type');
|
||
var editName = document.getElementById('edit-name');
|
||
var editNotes = document.getElementById('edit-notes');
|
||
var editTags = document.getElementById('edit-tags');
|
||
var editMetadata = document.getElementById('edit-metadata');
|
||
var editUpdatedAt = document.getElementById('edit-updated-at');
|
||
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 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');
|
||
var viewEntryName = document.getElementById('view-entry-name');
|
||
var viewBody = document.getElementById('view-body');
|
||
|
||
function closeView() {
|
||
viewOverlay.hidden = true;
|
||
viewBody.innerHTML = '';
|
||
viewEntryName.textContent = '';
|
||
}
|
||
|
||
document.getElementById('view-close').addEventListener('click', closeView);
|
||
viewOverlay.addEventListener('click', function (e) {
|
||
if (e.target === viewOverlay) closeView();
|
||
});
|
||
|
||
function renderViewSecrets(secrets, secretSchema) {
|
||
viewBody.innerHTML = '';
|
||
var names = Object.keys(secrets);
|
||
if (names.length === 0) {
|
||
var msg = document.createElement('div');
|
||
msg.className = 'view-locked-msg';
|
||
msg.textContent = t('viewNoSecrets');
|
||
viewBody.appendChild(msg);
|
||
return;
|
||
}
|
||
|
||
var schemaMap = {};
|
||
(secretSchema || []).forEach(function (s) { schemaMap[s.name] = s; });
|
||
|
||
names.forEach(function (name) {
|
||
var raw = secrets[name];
|
||
var valueStr = (raw === null || raw === undefined) ? '' :
|
||
(typeof raw === 'object') ? JSON.stringify(raw, null, 2) : String(raw);
|
||
var schema = schemaMap[name] || {};
|
||
var secretId = schema.id || '';
|
||
var secretType = schema.secret_type || 'text';
|
||
var originalName = name;
|
||
var hasChanges = false;
|
||
|
||
var row = document.createElement('div');
|
||
row.className = 'view-secret-row';
|
||
row.setAttribute('data-secret-id', secretId);
|
||
row.setAttribute('data-original-name', originalName);
|
||
|
||
var header = document.createElement('div');
|
||
header.className = 'view-secret-header';
|
||
|
||
var nameWrap = document.createElement('div');
|
||
nameWrap.className = 'view-secret-name-wrap';
|
||
|
||
var nameSpan = document.createElement('span');
|
||
nameSpan.className = 'view-secret-name';
|
||
nameSpan.textContent = name;
|
||
|
||
var nameInput = document.createElement('input');
|
||
nameInput.type = 'text';
|
||
nameInput.className = 'view-secret-name-input';
|
||
nameInput.value = name;
|
||
nameInput.placeholder = t('renameSecretPlaceholder');
|
||
nameInput.setAttribute('data-original-name', originalName);
|
||
nameInput.hidden = true;
|
||
|
||
var typeBadge = document.createElement('span');
|
||
typeBadge.className = 'view-secret-type';
|
||
typeBadge.textContent = secretType;
|
||
|
||
var typeSelect = document.createElement('select');
|
||
typeSelect.className = 'view-secret-type-select';
|
||
typeSelect.hidden = true;
|
||
SECRET_TYPE_OPTIONS.forEach(function (opt) {
|
||
var option = document.createElement('option');
|
||
option.value = opt;
|
||
option.textContent = opt;
|
||
if (opt === secretType) option.selected = true;
|
||
typeSelect.appendChild(option);
|
||
});
|
||
if (SECRET_TYPE_OPTIONS.indexOf(secretType) === -1 && secretType) {
|
||
var fallback = document.createElement('option');
|
||
fallback.value = secretType;
|
||
fallback.textContent = secretType;
|
||
fallback.selected = true;
|
||
typeSelect.appendChild(fallback);
|
||
}
|
||
|
||
nameWrap.appendChild(nameSpan);
|
||
nameWrap.appendChild(nameInput);
|
||
nameWrap.appendChild(typeBadge);
|
||
nameWrap.appendChild(typeSelect);
|
||
header.appendChild(nameWrap);
|
||
|
||
var actions = document.createElement('div');
|
||
actions.className = 'view-secret-actions';
|
||
|
||
var editBtn = document.createElement('button');
|
||
editBtn.type = 'button';
|
||
editBtn.className = 'btn-icon btn-view-edit';
|
||
editBtn.textContent = '✎';
|
||
editBtn.title = t('renameSecretTitle');
|
||
|
||
var saveBtn = document.createElement('button');
|
||
saveBtn.type = 'button';
|
||
saveBtn.className = 'btn-icon btn-view-save';
|
||
saveBtn.textContent = t('viewSaveChanges');
|
||
saveBtn.hidden = true;
|
||
|
||
var cancelBtn = document.createElement('button');
|
||
cancelBtn.type = 'button';
|
||
cancelBtn.className = 'btn-icon btn-view-cancel';
|
||
cancelBtn.textContent = t('modalCancel');
|
||
cancelBtn.hidden = true;
|
||
|
||
var copyBtn = document.createElement('button');
|
||
copyBtn.type = 'button';
|
||
copyBtn.className = 'btn-icon';
|
||
copyBtn.textContent = t('viewCopy');
|
||
copyBtn.addEventListener('click', function () {
|
||
navigator.clipboard.writeText(valueStr).then(function () {
|
||
copyBtn.textContent = t('viewCopied');
|
||
setTimeout(function () { copyBtn.textContent = t('viewCopy'); }, 1800);
|
||
}).catch(function () {});
|
||
});
|
||
actions.appendChild(copyBtn);
|
||
|
||
var unlinkBtn = document.createElement('button');
|
||
unlinkBtn.type = 'button';
|
||
unlinkBtn.className = 'btn-icon btn-view-unlink';
|
||
unlinkBtn.textContent = '×';
|
||
unlinkBtn.title = t('unlinkTitle');
|
||
actions.appendChild(unlinkBtn);
|
||
|
||
actions.appendChild(editBtn);
|
||
actions.appendChild(saveBtn);
|
||
actions.appendChild(cancelBtn);
|
||
header.appendChild(actions);
|
||
row.appendChild(header);
|
||
|
||
var valueWrap = document.createElement('div');
|
||
valueWrap.className = 'view-secret-value-wrap';
|
||
var valueEl = document.createElement('div');
|
||
valueEl.className = 'view-secret-value';
|
||
valueEl.textContent = valueStr;
|
||
valueWrap.appendChild(valueEl);
|
||
row.appendChild(valueWrap);
|
||
|
||
var nameStatus = document.createElement('div');
|
||
nameStatus.className = 'secret-name-status';
|
||
nameStatus.setAttribute('data-status', 'idle');
|
||
row.appendChild(nameStatus);
|
||
|
||
viewBody.appendChild(row);
|
||
|
||
// ── Edit mode toggle ──
|
||
function enterEditMode() {
|
||
nameSpan.hidden = true;
|
||
typeBadge.hidden = true;
|
||
nameInput.hidden = false;
|
||
typeSelect.hidden = false;
|
||
saveBtn.hidden = false;
|
||
cancelBtn.hidden = false;
|
||
editBtn.hidden = true;
|
||
nameInput.focus();
|
||
nameInput.select();
|
||
}
|
||
|
||
function exitEditMode() {
|
||
nameSpan.hidden = false;
|
||
typeBadge.hidden = false;
|
||
nameInput.hidden = true;
|
||
typeSelect.hidden = true;
|
||
saveBtn.hidden = true;
|
||
cancelBtn.hidden = true;
|
||
editBtn.hidden = false;
|
||
nameStatus.textContent = '';
|
||
nameStatus.className = 'secret-name-status';
|
||
nameInput.value = nameSpan.textContent;
|
||
typeSelect.value = typeBadge.textContent;
|
||
hasChanges = false;
|
||
}
|
||
|
||
editBtn.addEventListener('click', enterEditMode);
|
||
cancelBtn.addEventListener('click', exitEditMode);
|
||
|
||
// ── Name validation ──
|
||
var debounceTimer = null;
|
||
var currentCheck = null;
|
||
nameInput.addEventListener('input', function () {
|
||
if (debounceTimer) clearTimeout(debounceTimer);
|
||
nameStatus.textContent = '';
|
||
nameStatus.className = 'secret-name-status';
|
||
debounceTimer = setTimeout(function () {
|
||
var newName = nameInput.value.trim();
|
||
if (!newName || newName === originalName) return;
|
||
nameStatus.textContent = t('checkingSecretName');
|
||
nameStatus.className = 'secret-name-status checking';
|
||
var checkId = Date.now();
|
||
currentCheck = checkId;
|
||
var params = new URLSearchParams();
|
||
params.set('name', newName);
|
||
params.set('exclude_secret_id', secretId);
|
||
fetch('/api/secrets/check-name?' + params.toString(), { credentials: 'same-origin' })
|
||
.then(function (r) { return r.json(); })
|
||
.then(function (data) {
|
||
if (currentCheck !== checkId) return;
|
||
if (data.ok && data.available) {
|
||
nameStatus.textContent = t('secretNameAvailable');
|
||
nameStatus.className = 'secret-name-status success';
|
||
hasChanges = true;
|
||
} else {
|
||
nameStatus.textContent = data.error || t('secretNameTaken');
|
||
nameStatus.className = 'secret-name-status error';
|
||
hasChanges = false;
|
||
}
|
||
})
|
||
.catch(function () {
|
||
if (currentCheck !== checkId) return;
|
||
nameStatus.textContent = t('secretNameCheckError');
|
||
nameStatus.className = 'secret-name-status error';
|
||
hasChanges = false;
|
||
});
|
||
}, 300);
|
||
});
|
||
|
||
nameInput.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Enter') { e.preventDefault(); saveBtn.click(); }
|
||
if (e.key === 'Escape') { cancelBtn.click(); }
|
||
});
|
||
|
||
// ── Save ──
|
||
saveBtn.addEventListener('click', function () {
|
||
var newName = nameInput.value.trim();
|
||
var newType = typeSelect.value;
|
||
if (!newName) { nameStatus.textContent = t('secretNameInvalid'); nameStatus.className = 'secret-name-status error'; return; }
|
||
if (!newType) { nameStatus.textContent = t('secretTypeInvalid'); nameStatus.className = 'secret-name-status error'; return; }
|
||
var patchBody = {};
|
||
if (newName !== originalName) patchBody.name = newName;
|
||
if (newType !== secretType) patchBody.type = newType;
|
||
if (Object.keys(patchBody).length === 0) { exitEditMode(); return; }
|
||
saveBtn.textContent = '...';
|
||
fetch('/api/secrets/' + encodeURIComponent(secretId), {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'same-origin',
|
||
body: JSON.stringify(patchBody)
|
||
}).then(function (r) {
|
||
return r.json().then(function (data) {
|
||
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
|
||
return data;
|
||
});
|
||
}).then(function () {
|
||
nameSpan.textContent = newName;
|
||
typeBadge.textContent = newType;
|
||
originalName = newName;
|
||
nameInput.setAttribute('data-original-name', newName);
|
||
saveBtn.textContent = t('viewChangesSaved');
|
||
setTimeout(function () { exitEditMode(); saveBtn.textContent = t('viewSaveChanges'); }, 1200);
|
||
// Update table row chip
|
||
var tableRow = document.querySelector('tr[data-entry-id="' + viewBody.getAttribute('data-entry-id') + '"]');
|
||
if (tableRow) {
|
||
var chip = tableRow.querySelector('.secret-chip .secret-name');
|
||
if (chip && chip.textContent === name) chip.textContent = newName;
|
||
}
|
||
}).catch(function (err) {
|
||
nameStatus.textContent = err.message || String(err);
|
||
nameStatus.className = 'secret-name-status error';
|
||
saveBtn.textContent = t('viewSaveChanges');
|
||
});
|
||
});
|
||
|
||
// ── Unlink ──
|
||
unlinkBtn.addEventListener('click', function () {
|
||
if (!confirm(tf('viewUnlinkConfirm', { name: nameSpan.textContent }))) return;
|
||
fetch('/api/entries/' + encodeURIComponent(viewBody.getAttribute('data-entry-id')) + '/secrets/' + encodeURIComponent(secretId), {
|
||
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 () {
|
||
row.remove();
|
||
if (!viewBody.querySelector('.view-secret-row')) {
|
||
viewBody.innerHTML = '';
|
||
var msg = document.createElement('div');
|
||
msg.className = 'view-locked-msg';
|
||
msg.textContent = t('viewNoSecrets');
|
||
viewBody.appendChild(msg);
|
||
}
|
||
// Update table row
|
||
var tableRow = document.querySelector('tr[data-entry-id="' + viewBody.getAttribute('data-entry-id') + '"]');
|
||
if (tableRow) {
|
||
var chip = tableRow.querySelector('.secret-chip');
|
||
if (chip) {
|
||
var chipName = chip.querySelector('.secret-name');
|
||
if (chipName && chipName.textContent === name) chip.remove();
|
||
}
|
||
}
|
||
}).catch(function (err) {
|
||
alert(err.message || String(err));
|
||
});
|
||
});
|
||
});
|
||
}
|
||
|
||
function openView(tr) {
|
||
var entryId = tr.getAttribute('data-entry-id');
|
||
var nameEl = tr.querySelector('.cell-name');
|
||
var entryName = nameEl ? nameEl.textContent.trim() : '';
|
||
var encKey = sessionStorage.getItem('enc_key');
|
||
|
||
viewEntryName.textContent = entryName;
|
||
viewBody.innerHTML = '';
|
||
viewBody.setAttribute('data-entry-id', entryId);
|
||
viewOverlay.hidden = false;
|
||
|
||
if (!encKey) {
|
||
var msg = document.createElement('div');
|
||
msg.className = 'view-locked-msg';
|
||
msg.innerHTML = t('viewLockedMsg');
|
||
viewBody.appendChild(msg);
|
||
return;
|
||
}
|
||
|
||
var loadingMsg = document.createElement('div');
|
||
loadingMsg.className = 'view-locked-msg';
|
||
loadingMsg.textContent = t('viewLoading');
|
||
viewBody.appendChild(loadingMsg);
|
||
|
||
var sj = tr.getAttribute('data-entry-secrets') || '[]';
|
||
var secretSchema;
|
||
try { secretSchema = JSON.parse(sj); } catch (e) { secretSchema = []; }
|
||
|
||
fetch('/api/entries/' + encodeURIComponent(entryId) + '/secrets/decrypt', {
|
||
credentials: 'same-origin',
|
||
headers: { 'X-Encryption-Key': encKey }
|
||
}).then(function (r) {
|
||
return r.json().then(function (data) {
|
||
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
|
||
return data;
|
||
});
|
||
}).then(function (data) {
|
||
renderViewSecrets(data.secrets || {}, secretSchema);
|
||
}).catch(function (e) {
|
||
viewBody.innerHTML = '';
|
||
var errMsg = document.createElement('div');
|
||
errMsg.className = 'view-locked-msg';
|
||
errMsg.style.color = '#f85149';
|
||
errMsg.textContent = e.message || t('viewDecryptError');
|
||
viewBody.appendChild(errMsg);
|
||
});
|
||
}
|
||
// ─────────────────────────────────────────────────────────────────────────
|
||
|
||
function showEditErr(msg) {
|
||
editError.textContent = msg || '';
|
||
editError.classList.toggle('visible', !!msg);
|
||
}
|
||
|
||
function formatLocalTime(raw) {
|
||
if (!raw) return '—';
|
||
var d = new Date(raw);
|
||
if (!d || isNaN(d.getTime())) return raw;
|
||
return d.toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'medium' });
|
||
}
|
||
|
||
function openEdit(tr) {
|
||
var id = tr.getAttribute('data-entry-id');
|
||
if (!id) return;
|
||
currentEntryId = id;
|
||
showEditErr('');
|
||
editFolder.value = tr.getAttribute('data-entry-folder') || '';
|
||
editType.value = tr.querySelector('.cell-type') ? tr.querySelector('.cell-type').textContent.trim() : '';
|
||
editName.value = tr.querySelector('.cell-name') ? tr.querySelector('.cell-name').textContent.trim() : '';
|
||
editNotes.value = tr.querySelector('.cell-notes-val') ? tr.querySelector('.cell-notes-val').textContent : '';
|
||
var tagsText = tr.querySelector('.cell-tags-val') ? tr.querySelector('.cell-tags-val').textContent.trim() : '';
|
||
editTags.value = tagsText;
|
||
var rawUpdated = tr.getAttribute('data-updated-at');
|
||
editUpdatedAt.textContent = formatLocalTime(rawUpdated);
|
||
editUpdatedAt.title = rawUpdated ? rawUpdated + ' (UTC)' : '';
|
||
var md = tr.getAttribute('data-entry-metadata') || '{}';
|
||
try {
|
||
var mobj = JSON.parse(md);
|
||
editMetadata.value = JSON.stringify(mobj, null, 2);
|
||
} 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;
|
||
}
|
||
|
||
function closeEdit() {
|
||
editOverlay.hidden = true;
|
||
currentEntryId = null;
|
||
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' && !viewOverlay.hidden) closeView();
|
||
});
|
||
|
||
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');
|
||
if (nameCell) nameCell.textContent = body.name;
|
||
var typeCell = tr.querySelector('.cell-type');
|
||
if (typeCell) typeCell.textContent = body.type;
|
||
var notesCell = tr.querySelector('.cell-notes-val');
|
||
if (notesCell) {
|
||
if (body.notes) { notesCell.textContent = body.notes; }
|
||
else { var notesWrap = tr.querySelector('.cell-notes'); if (notesWrap) notesWrap.innerHTML = ''; }
|
||
}
|
||
var tagsCell = tr.querySelector('.cell-tags-val');
|
||
if (tagsCell) tagsCell.textContent = body.tags.join(', ');
|
||
tr.setAttribute('data-entry-folder', body.folder);
|
||
tr.setAttribute('data-entry-metadata', JSON.stringify(body.metadata));
|
||
}
|
||
|
||
function refreshListAfterDelete(entryId) {
|
||
var tr = document.querySelector('tr[data-entry-id="' + entryId + '"]');
|
||
var folder = tr ? tr.getAttribute('data-entry-folder') : null;
|
||
if (tr) tr.remove();
|
||
var tbody = document.querySelector('table tbody');
|
||
if (tbody && !tbody.querySelector('tr[data-entry-id]')) {
|
||
var card = document.querySelector('.card');
|
||
if (card) {
|
||
var tableWrap = card.querySelector('.table-wrap');
|
||
if (tableWrap) tableWrap.remove();
|
||
var existingEmpty = card.querySelector('.empty');
|
||
if (!existingEmpty) {
|
||
var emptyDiv = document.createElement('div');
|
||
emptyDiv.className = 'empty';
|
||
emptyDiv.setAttribute('data-i18n', 'emptyEntries');
|
||
emptyDiv.textContent = t('emptyEntries');
|
||
var filterBar = card.querySelector('.filter-bar');
|
||
if (filterBar) { card.insertBefore(emptyDiv, filterBar.nextSibling); }
|
||
else { card.appendChild(emptyDiv); }
|
||
}
|
||
}
|
||
}
|
||
var allTab = document.querySelector('.folder-tab[data-all-tab="1"]');
|
||
if (allTab) {
|
||
var count = parseInt(allTab.getAttribute('data-count') || '0', 10);
|
||
if (count > 0) {
|
||
count -= 1;
|
||
allTab.setAttribute('data-count', String(count));
|
||
allTab.textContent = t('allTab') + ' (' + count + ')';
|
||
}
|
||
}
|
||
if (folder) {
|
||
document.querySelectorAll('.folder-tab:not([data-all-tab])').forEach(function (tab) {
|
||
if (tab.textContent.trim().indexOf(folder) === 0) {
|
||
var m = tab.textContent.match(/\((\d+)\)/);
|
||
if (m) {
|
||
var c = parseInt(m[1], 10);
|
||
if (c > 0) {
|
||
c -= 1;
|
||
tab.textContent = folder + ' (' + c + ')';
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
document.getElementById('edit-save').addEventListener('click', function () {
|
||
if (!currentEntryId) return;
|
||
var meta;
|
||
try {
|
||
meta = JSON.parse(editMetadata.value);
|
||
} catch (err) {
|
||
showEditErr(t('errInvalidJson'));
|
||
return;
|
||
}
|
||
if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {
|
||
showEditErr(t('errMetadataObject'));
|
||
return;
|
||
}
|
||
var tags = editTags.value.split(',').map(function (s) { return s.trim(); }).filter(Boolean);
|
||
var body = {
|
||
folder: editFolder.value,
|
||
type: editType.value,
|
||
name: editName.value.trim(),
|
||
notes: editNotes.value,
|
||
tags: tags,
|
||
metadata: meta,
|
||
parent_ids: selectedParents.map(function (parent) { return parent.id; })
|
||
};
|
||
showEditErr('');
|
||
fetch('/api/entries/' + encodeURIComponent(currentEntryId), {
|
||
method: 'PATCH',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
credentials: 'same-origin',
|
||
body: JSON.stringify(body)
|
||
}).then(function (r) {
|
||
return r.json().then(function (data) {
|
||
if (!r.ok) throw new Error(data.error || ('HTTP ' + r.status));
|
||
return data;
|
||
});
|
||
}).then(function () {
|
||
closeEdit();
|
||
refreshListAfterSave(currentEntryId, body);
|
||
}).catch(function (e) {
|
||
showEditErr(e.message || String(e));
|
||
});
|
||
});
|
||
|
||
document.querySelectorAll('tr[data-entry-id]').forEach(function (tr) {
|
||
var viewBtn = tr.querySelector('.btn-view-secrets');
|
||
if (viewBtn) {
|
||
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');
|
||
if (!id) return;
|
||
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)); });
|
||
});
|
||
});
|
||
|
||
applyLang();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|