mirror of
https://github.com/house-of-vanity/k8s-secrets.git
synced 2026-11-08 15:28:50 +00:00
Compact secret cards and use English UI text
This commit is contained in:
1 parent
603e263959
commit
61b0ebff0c
1 file changed
+41
-52
+41
-52
@@ -1,56 +1,47 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="ru">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Секреты</title>
|
<title>Secrets</title>
|
||||||
<style>
|
<style>
|
||||||
:root { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; color: #17233b; background: #f4f6fa; }
|
:root { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; color: #17233b; background: #f4f6fa; }
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
body { min-width: 320px; margin: 0; }
|
body { min-width: 320px; margin: 0; }
|
||||||
button { font: inherit; }
|
button { font: inherit; }
|
||||||
.page { width: min(100% - 48px, 1480px); margin: 0 auto; padding: 52px 0 88px; }
|
.page { width: min(100% - 32px, 1160px); margin: 0 auto; padding: 28px 0 48px; }
|
||||||
.page-header { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; margin-bottom: 32px; }
|
.page-header { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 18px; }
|
||||||
.eyebrow { margin: 0 0 10px; color: #5c6c88; font-size: 11px; font-weight: 750; letter-spacing: .14em; text-transform: uppercase; }
|
h1 { margin: 0; font-size: 24px; font-weight: 720; letter-spacing: -.035em; line-height: 1.2; }
|
||||||
h1 { margin: 0; font-size: clamp(28px, 4vw, 38px); font-weight: 720; letter-spacing: -.04em; line-height: 1.1; }
|
.count { flex: none; padding: 6px 10px; border: 1px solid #dce3ed; border-radius: 999px; background: #fff; color: #52617c; font-size: 11px; font-weight: 650; white-space: nowrap; }
|
||||||
.subtitle { margin: 10px 0 0; color: #687791; font-size: 14px; }
|
.secrets-grid { display: flex; flex-wrap: wrap; gap: 12px; align-items: flex-start; }
|
||||||
.count { flex: none; padding: 9px 13px; border: 1px solid #dce3ed; border-radius: 999px; background: #fff; color: #52617c; font-size: 12px; font-weight: 650; white-space: nowrap; }
|
.secret-card { flex: 0 1 340px; min-width: min(100%, 290px); overflow: hidden; border: 1px solid #dfe5ef; border-radius: 12px; background: #fff; box-shadow: 0 4px 16px rgba(31, 48, 82, .04); }
|
||||||
.secrets-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 380px), 1fr)); gap: 20px; align-items: start; }
|
|
||||||
.secret-card { min-width: 0; overflow: hidden; border: 1px solid #dfe5ef; border-radius: 18px; background: #fff; box-shadow: 0 8px 28px rgba(31, 48, 82, .045); }
|
|
||||||
.secret-card.expired { border-color: #efc8c3; }
|
.secret-card.expired { border-color: #efc8c3; }
|
||||||
.card-header { padding: 22px 22px 18px; border-bottom: 1px solid #edf0f5; }
|
.card-header { padding: 14px 14px 12px; border-bottom: 1px solid #edf0f5; }
|
||||||
.card-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
|
.card-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
|
||||||
.secret-name { min-width: 0; margin: 0; overflow-wrap: anywhere; font-size: 17px; font-weight: 700; letter-spacing: -.02em; line-height: 1.35; }
|
.secret-name { min-width: 0; margin: 0; overflow-wrap: anywhere; font-size: 14px; font-weight: 700; letter-spacing: -.01em; line-height: 1.35; }
|
||||||
.source-badge, .expired-badge { display: inline-flex; align-items: center; gap: 6px; width: fit-content; border-radius: 999px; font-size: 11px; font-weight: 700; line-height: 1; white-space: nowrap; }
|
.source-badge, .expired-badge { display: inline-flex; align-items: center; gap: 6px; width: fit-content; border-radius: 999px; font-size: 11px; font-weight: 700; line-height: 1; white-space: nowrap; }
|
||||||
.source-badge { flex: none; padding: 7px 9px; }
|
.source-badge { flex: none; padding: 5px 7px; }
|
||||||
.source-badge.k8s { background: #eaf0ff; color: #315cb3; }
|
.source-badge.k8s { background: #eaf0ff; color: #315cb3; }
|
||||||
.source-badge.webhook { background: #f0ebff; color: #6c47b6; }
|
.source-badge.webhook { background: #f0ebff; color: #6c47b6; }
|
||||||
.expired-badge { margin-top: 12px; padding: 7px 9px; background: #fff0ed; color: #ad453b; }
|
.expired-badge { margin-top: 8px; padding: 5px 7px; background: #fff0ed; color: #ad453b; }
|
||||||
.fields { display: grid; gap: 10px; padding: 16px; }
|
.fields { display: grid; gap: 7px; padding: 10px; }
|
||||||
.field { min-width: 0; padding: 13px; border: 1px solid #edf0f5; border-radius: 12px; background: #f8fafd; }
|
.field { min-width: 0; padding: 9px 10px; border: 1px solid #edf0f5; border-radius: 8px; background: #f8fafd; }
|
||||||
.field-key { display: block; margin-bottom: 10px; color: #62718b; font-size: 11px; font-weight: 700; letter-spacing: .035em; overflow-wrap: anywhere; }
|
.field-key { display: block; margin-bottom: 5px; color: #62718b; font-size: 10px; font-weight: 700; letter-spacing: .025em; overflow-wrap: anywhere; }
|
||||||
.field-content { display: flex; align-items: center; gap: 10px; min-width: 0; }
|
.field-content { display: flex; align-items: center; gap: 7px; min-width: 0; }
|
||||||
.field-value { display: block; flex: 1; min-width: 0; overflow-wrap: anywhere; color: #263751; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.55; white-space: pre-wrap; }
|
.field-value { display: block; flex: 1; min-width: 0; overflow-wrap: anywhere; color: #263751; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; line-height: 1.45; white-space: pre-wrap; }
|
||||||
.totp-code { color: #325dc0; font-size: 25px; font-weight: 750; letter-spacing: .08em; line-height: 1.15; white-space: nowrap; }
|
.totp-code { color: #325dc0; font-size: 21px; font-weight: 750; letter-spacing: .06em; line-height: 1.1; white-space: nowrap; }
|
||||||
.totp-timer { flex: none; min-width: 30px; color: #6a7890; font-size: 11px; font-variant-numeric: tabular-nums; text-align: right; }
|
.totp-timer { flex: none; min-width: 24px; color: #6a7890; font-size: 10px; font-variant-numeric: tabular-nums; text-align: right; }
|
||||||
.copy-button { display: inline-flex; flex: none; align-items: center; justify-content: center; gap: 6px; min-height: 32px; padding: 0 10px; border: 1px solid #d8e0ec; border-radius: 8px; background: #fff; color: #445674; cursor: pointer; font-size: 11px; font-weight: 700; transition: background .15s, border-color .15s, color .15s; }
|
.copy-button { display: inline-flex; flex: none; align-items: center; justify-content: center; gap: 5px; min-height: 27px; padding: 0 7px; border: 1px solid #d8e0ec; border-radius: 6px; background: #fff; color: #445674; cursor: pointer; font-size: 10px; font-weight: 700; transition: background .15s, border-color .15s, color .15s; }
|
||||||
.copy-button:hover { border-color: #a9bce2; background: #eef3ff; color: #315cb3; }
|
.copy-button:hover { border-color: #a9bce2; background: #eef3ff; color: #315cb3; }
|
||||||
.copy-button:focus-visible { outline: 3px solid #a9bce2; outline-offset: 2px; }
|
.copy-button:focus-visible { outline: 3px solid #a9bce2; outline-offset: 2px; }
|
||||||
.copy-button.copied { border-color: #b9ddcc; background: #eaf7ef; color: #237a4c; }
|
.copy-button.copied { border-color: #b9ddcc; background: #eaf7ef; color: #237a4c; }
|
||||||
.copy-button.failed { border-color: #efc8c3; background: #fff0ed; color: #ad453b; }
|
.copy-button.failed { border-color: #efc8c3; background: #fff0ed; color: #ad453b; }
|
||||||
.copy-button svg { width: 14px; height: 14px; }
|
.copy-button svg { width: 12px; height: 12px; }
|
||||||
.card-footer { display: grid; gap: 5px; padding: 0 22px 20px; color: #73819a; font-size: 11px; line-height: 1.4; }
|
.card-footer { display: grid; gap: 3px; padding: 0 14px 12px; color: #73819a; font-size: 10px; line-height: 1.35; }
|
||||||
.card-footer strong { color: #596b86; font-weight: 600; }
|
.card-footer strong { color: #596b86; font-weight: 600; }
|
||||||
.notice { padding: 25px; border: 1px solid #dfe5ef; border-radius: 16px; background: #fff; color: #60708b; font-size: 14px; }
|
.notice { padding: 18px; border: 1px solid #dfe5ef; border-radius: 12px; background: #fff; color: #60708b; font-size: 13px; }
|
||||||
.notice.error { border-color: #efc8c3; background: #fff7f5; color: #a43c34; }
|
.notice.error { border-color: #efc8c3; background: #fff7f5; color: #a43c34; }
|
||||||
@media (max-width: 560px) {
|
@media (max-width: 560px) { .page { width: min(100% - 24px, 1160px); padding-top: 20px; } }
|
||||||
.page { width: min(100% - 28px, 1480px); padding-top: 32px; }
|
|
||||||
.page-header { align-items: flex-start; margin-bottom: 24px; }
|
|
||||||
.count { margin-top: 4px; }
|
|
||||||
.card-header { padding: 19px 18px 16px; }
|
|
||||||
.fields { padding: 12px; }
|
|
||||||
.card-footer { padding: 0 18px 18px; }
|
|
||||||
}
|
|
||||||
@media (max-width: 390px) {
|
@media (max-width: 390px) {
|
||||||
.page-header { flex-wrap: wrap; }
|
.page-header { flex-wrap: wrap; }
|
||||||
.field-content { flex-wrap: wrap; }
|
.field-content { flex-wrap: wrap; }
|
||||||
@@ -62,18 +53,16 @@
|
|||||||
<main class="page">
|
<main class="page">
|
||||||
<header class="page-header">
|
<header class="page-header">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Secret Reader</p>
|
<h1>Secrets</h1>
|
||||||
<h1>Секреты</h1>
|
|
||||||
<p class="subtitle">Актуальные значения из Kubernetes и webhook</p>
|
|
||||||
</div>
|
</div>
|
||||||
<span class="count">Всего: {{ secrets.len() }}</span>
|
<span class="count">{{ secrets.len() }} total</span>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{% if let Some(err) = error %}
|
{% if let Some(err) = error %}
|
||||||
<div class="notice error" role="alert">Ошибка загрузки: {{ err }}</div>
|
<div class="notice error" role="alert">Failed to load secrets: {{ err }}</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if secrets.len() == 0 %}
|
{% if secrets.len() == 0 %}
|
||||||
<div class="notice">Пока нет секретов для отображения.</div>
|
<div class="notice">No secrets to display.</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="secrets-grid">
|
<div class="secrets-grid">
|
||||||
{% for secret in secrets %}
|
{% for secret in secrets %}
|
||||||
@@ -87,7 +76,7 @@
|
|||||||
{% endmatch %}
|
{% endmatch %}
|
||||||
</div>
|
</div>
|
||||||
{% if secret.expired %}
|
{% if secret.expired %}
|
||||||
{% if let Some(expires) = secret.expires_at %}<span class="expired-badge">Истёк: {{ expires }}</span>{% endif %}
|
{% if let Some(expires) = secret.expires_at %}<span class="expired-badge">Expired: {{ expires }}</span>{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -97,14 +86,14 @@
|
|||||||
<span class="field-key">{{ key }}</span>
|
<span class="field-key">{{ key }}</span>
|
||||||
<div class="field-content">
|
<div class="field-content">
|
||||||
{% if value.starts_with("otpauth://totp/") %}
|
{% if value.starts_with("otpauth://totp/") %}
|
||||||
<span class="field-value totp-code" data-totp-url="{{ value }}" aria-label="Одноразовый код">••• •••</span>
|
<span class="field-value totp-code" data-totp-url="{{ value }}" aria-label="One-time code">••• •••</span>
|
||||||
<span class="totp-timer" aria-label="Осталось секунд"></span>
|
<span class="totp-timer" aria-label="Seconds remaining"></span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<code class="field-value">{{ value }}</code>
|
<code class="field-value">{{ value }}</code>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button class="copy-button" type="button" aria-label="Копировать {{ key }}" title="Копировать {{ key }}">
|
<button class="copy-button" type="button" aria-label="Copy {{ key }}" title="Copy {{ key }}">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/></svg>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="12" height="12" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/></svg>
|
||||||
<span>Копировать</span>
|
<span>Copy</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -113,9 +102,9 @@
|
|||||||
|
|
||||||
{% if let Some(received) = secret.received_at %}
|
{% if let Some(received) = secret.received_at %}
|
||||||
<footer class="card-footer">
|
<footer class="card-footer">
|
||||||
<span>Получен: <strong>{{ received }}</strong></span>
|
<span>Received: <strong>{{ received }}</strong></span>
|
||||||
{% if let Some(expires) = secret.expires_at %}
|
{% if let Some(expires) = secret.expires_at %}
|
||||||
{% if !secret.expired %}<span>Истекает: <strong>{{ expires }}</strong></span>{% endif %}
|
{% if !secret.expired %}<span>Expires: <strong>{{ expires }}</strong></span>{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</footer>
|
</footer>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -139,9 +128,9 @@
|
|||||||
const code = totp.generate();
|
const code = totp.generate();
|
||||||
codeElement.dataset.copyValue = code;
|
codeElement.dataset.copyValue = code;
|
||||||
codeElement.textContent = code.replace(/(.{3})(?=.)/, '$1 ');
|
codeElement.textContent = code.replace(/(.{3})(?=.)/, '$1 ');
|
||||||
timerElement.textContent = (totp.period - (now % totp.period)) + 'с';
|
timerElement.textContent = (totp.period - (now % totp.period)) + 's';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
codeElement.textContent = 'Ошибка кода';
|
codeElement.textContent = 'Code error';
|
||||||
timerElement.textContent = '';
|
timerElement.textContent = '';
|
||||||
console.error('Failed to generate TOTP:', error);
|
console.error('Failed to generate TOTP:', error);
|
||||||
}
|
}
|
||||||
@@ -178,15 +167,15 @@
|
|||||||
try {
|
try {
|
||||||
await writeClipboard(value);
|
await writeClipboard(value);
|
||||||
button.classList.add('copied');
|
button.classList.add('copied');
|
||||||
label.textContent = 'Скопировано';
|
label.textContent = 'Copied';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
button.classList.add('failed');
|
button.classList.add('failed');
|
||||||
label.textContent = 'Ошибка';
|
label.textContent = 'Error';
|
||||||
console.error('Failed to copy value:', error);
|
console.error('Failed to copy value:', error);
|
||||||
}
|
}
|
||||||
button.resetTimeout = setTimeout(() => {
|
button.resetTimeout = setTimeout(() => {
|
||||||
button.classList.remove('copied', 'failed');
|
button.classList.remove('copied', 'failed');
|
||||||
label.textContent = 'Копировать';
|
label.textContent = 'Copy';
|
||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user