Compare commits

...

1 Commits

Author SHA1 Message Date
agent
574c1c9967 release(secrets-mcp): 0.5.15 — 列设置面板锚定优化,移除查看密文隐藏功能
All checks were successful
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Successful in 5m53s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Successful in 1m35s
2026-04-10 11:54:20 +08:00
4 changed files with 192 additions and 48 deletions

2
Cargo.lock generated
View File

@@ -2065,7 +2065,7 @@ dependencies = [
[[package]]
name = "secrets-mcp"
version = "0.5.14"
version = "0.5.15"
dependencies = [
"anyhow",
"askama",

View File

@@ -1,6 +1,6 @@
[package]
name = "secrets-mcp"
version = "0.5.14"
version = "0.5.15"
edition.workspace = true
[[bin]]

View File

@@ -73,8 +73,7 @@
border-color: rgba(56,139,253,0.3);
color: #fff;
}
.filter-bar {
display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px 16px;
.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;
}
@@ -114,6 +113,29 @@
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;
@@ -123,10 +145,18 @@
}
table {
width: 100%;
min-width: 960px;
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;
@@ -142,13 +172,21 @@
}
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 { min-width: 108px; width: 1%; text-align: center; vertical-align: middle; }
.col-name { min-width: 180px; max-width: 260px; text-align: center; vertical-align: middle; }
.col-tags { min-width: 160px; max-width: 220px; }
.col-secrets { min-width: 220px; max-width: 420px; vertical-align: middle; }
.col-type { text-align: center; vertical-align: middle; }
.col-secrets { vertical-align: middle; }
.col-secrets .secret-list { max-height: 120px; overflow: auto; }
.col-actions { min-width: 132px; width: 1%; text-align: center; vertical-align: middle; }
.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;
@@ -199,6 +237,17 @@
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;
@@ -357,6 +406,9 @@
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; }
@@ -404,7 +456,7 @@
padding: 7px 10px; word-break: break-all; white-space: pre-wrap;
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: 6px 10px; border-radius: 8px; font-size: 12px; cursor: pointer;
border: 1px solid rgba(240,246,252,0.12); background: #161b22; color: #8b949e;
@@ -497,6 +549,10 @@
<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>
@@ -505,25 +561,34 @@
{% 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-i18n="colName">名称</th>
<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>
<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-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="关联">
<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 }}">
@@ -539,7 +604,7 @@
{% endfor %}
</div>
</td>
<td class="col-secrets" data-label="密文">
<td class="col-secrets" data-col="secrets" data-label="密文">
<div class="secret-list">
{% for s in entry.secrets %}
<span class="secret-chip">
@@ -549,7 +614,7 @@
{% endfor %}
</div>
</td>
<td class="col-actions" data-label="操作">
<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>
@@ -627,6 +692,9 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
I18N_PAGE = {
'zh-CN': {
pageTitle: 'Secrets — 条目',
columnSettings: '显示列',
fixedColumns: '固定列',
optionalColumns: '可选列',
navTrash: '回收站',
entriesTitle: '我的条目',
allTab: '全部',
@@ -694,8 +762,6 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
viewDecryptError: '解密失败,请确认密码短语与加密时一致。',
viewCopy: '复制',
viewCopied: '已复制',
viewShow: '显示',
viewHide: '隐藏',
viewLoading: '解密中…',
viewSaveChanges: '保存更改',
viewChangesSaved: '已保存',
@@ -706,6 +772,9 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
},
'zh-TW': {
pageTitle: 'Secrets — 條目',
columnSettings: '顯示列',
fixedColumns: '固定列',
optionalColumns: '可選列',
navTrash: '回收站',
entriesTitle: '我的條目',
allTab: '全部',
@@ -773,8 +842,6 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
viewDecryptError: '解密失敗,請確認密碼短語與加密時一致。',
viewCopy: '複製',
viewCopied: '已複製',
viewShow: '顯示',
viewHide: '隱藏',
viewLoading: '解密中…',
viewSaveChanges: '儲存變更',
viewChangesSaved: '已儲存',
@@ -785,6 +852,9 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
},
en: {
pageTitle: 'Secrets — Entries',
columnSettings: 'Columns',
fixedColumns: 'Fixed',
optionalColumns: 'Optional',
navTrash: 'Trash',
entriesTitle: 'My entries',
allTab: 'All',
@@ -852,8 +922,6 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
viewDecryptError: 'Decryption failed. Please verify your passphrase matches the one used when encrypting.',
viewCopy: 'Copy',
viewCopied: 'Copied',
viewShow: 'Show',
viewHide: 'Hide',
viewLoading: 'Decrypting…',
viewSaveChanges: 'Save changes',
viewChangesSaved: 'Saved',
@@ -885,8 +953,96 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
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');
@@ -1023,9 +1179,6 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
var raw = secrets[name];
var valueStr = (raw === null || raw === undefined) ? '' :
(typeof raw === 'object') ? JSON.stringify(raw, null, 2) : String(raw);
var isPassword = (name === 'password' || name === 'passwd' || name === 'secret');
var masked = isPassword;
var schema = schemaMap[name] || {};
var secretId = schema.id || '';
var secretType = schema.secret_type || 'text';
@@ -1104,19 +1257,6 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
cancelBtn.textContent = t('modalCancel');
cancelBtn.hidden = true;
if (isPassword) {
var toggleBtn = document.createElement('button');
toggleBtn.type = 'button';
toggleBtn.className = 'btn-icon btn-toggle-mask';
toggleBtn.textContent = t('viewShow');
toggleBtn.addEventListener('click', function () {
masked = !masked;
valueEl.classList.toggle('masked', masked);
toggleBtn.textContent = masked ? t('viewShow') : t('viewHide');
});
actions.appendChild(toggleBtn);
}
var copyBtn = document.createElement('button');
copyBtn.type = 'button';
copyBtn.className = 'btn-icon';
@@ -1145,7 +1285,7 @@ var SECRET_TYPE_OPTIONS = JSON.parse(document.getElementById('secret-type-option
var valueWrap = document.createElement('div');
valueWrap.className = 'view-secret-value-wrap';
var valueEl = document.createElement('div');
valueEl.className = 'view-secret-value' + (masked ? ' masked' : '');
valueEl.className = 'view-secret-value';
valueEl.textContent = valueStr;
valueWrap.appendChild(valueEl);
row.appendChild(valueWrap);

View File

@@ -65,6 +65,10 @@ function applyLang() {
var key = el.getAttribute('data-i18n-ph');
el.placeholder = t(key);
});
document.querySelectorAll('[data-i18n-title]').forEach(function (el) {
var key = el.getAttribute('data-i18n-title');
el.title = t(key);
});
document.querySelectorAll('.lang-btn').forEach(function (btn) {
var map = { 'zh-CN': '简', 'zh-TW': '繁', en: 'EN' };
btn.classList.toggle('active', btn.textContent === map[currentLang]);