design(iot-console): Nubes brand rebrand v0.1.57

- Палитра: #001C34 (Nubes navy) как фоновая карточек/navbar
- Логотип Nubes SVG в navbar и на экране входа (filter:invert → белый)
- Убраны эмодзи из brand-элементов
- Accent: #1a7fd4 (корпоративный синий на тёмном фоне)
- Badges: прямоугольные, UPPERCASE, строгие
- Кнопки/формы/таблицы: Nubes-спецификация
This commit is contained in:
Naeel
2026-04-04 20:34:23 +03:00
parent e54787177b
commit 0400f97eb6
2 changed files with 167 additions and 112 deletions
+1 -1
View File
@@ -75,7 +75,7 @@ spec:
- name: operator - name: operator
# При обновлении версии оператора — менять тег здесь (не latest!) # При обновлении версии оператора — менять тег здесь (не latest!)
# v0.1.50 — добавлены IoT controller, IoT REST API, MQTT auth endpoint # v0.1.50 — добавлены IoT controller, IoT REST API, MQTT auth endpoint
image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.56 image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.57
# Always — чтобы всегда тянуть по точному тегу (не кешировать старый) # Always — чтобы всегда тянуть по точному тегу (не кешировать старый)
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
+166 -111
View File
@@ -1,188 +1,238 @@
<!DOCTYPE html> <!DOCTYPE html>
<!-- Создано: 2026-04-04 <!-- Создано: 2026-04-04
Изменено: 2026-04-04 (design: Nubes brand, #001C34 palette, logo)
IoT Консоль — SPA для управления IoT устройствами. IoT Консоль — SPA для управления IoT устройствами.
Стек: ванильный HTML/CSS/JS + mqtt.js (CDN). Стек: ванильный HTML/CSS/JS + mqtt.js (CDN).
Функционал: вход, список устройств, credentials, эмулятор MQTT, заглушка телеметрии. Функционал: вход, список устройств, credentials, эмулятор MQTT, заглушка телеметрии.
Встраивается в оператор через go:embed, раздаётся по GET /console. Встраивается в оператор через go:embed, раздаётся по GET /console.
Доступна по: http://iot.kube5s.ru/console --> Доступна по: https://iot.kube5s.ru/console -->
<html lang="ru"> <html lang="ru">
<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>IoT Консоль</title> <title>Nubes IoT Console</title>
<style> <style>
/* ——— Nubes brand palette ———
#001C34 — Nubes navy (brand primary, navbar, logo)
#001120 — page background (deeper than brand)
#001929 — card surface
#0b2d50 — borders, dividers
#1a7fd4 — action accent (bright blue on navy)
#e2ecf6 — text primary
#6b8eaa — text secondary
#2d5070 — text muted / disabled */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
background: #0f172a; color: #e2e8f0; min-height: 100vh; background: #001120; color: #e2ecf6; min-height: 100vh;
} }
/* Navbar */ /* ——— Navbar ——— */
.navbar { .navbar {
background: #1e293b; border-bottom: 1px solid #334155; background: #001C34;
padding: 0 24px; height: 56px; border-bottom: 1px solid #0b2d50;
display: flex; align-items: center; gap: 12px; padding: 0 28px; height: 58px;
display: flex; align-items: center; gap: 14px;
} }
.navbar-brand { /* Логотип Nubes — SVG с инвертированием для белого цвета на тёмном фоне */
font-weight: 800; font-size: 17px; color: #3b82f6; cursor: pointer; .navbar-logo {
display: flex; align-items: center; gap: 6px; user-select: none; display: flex; align-items: center; gap: 10px;
cursor: pointer; user-select: none; text-decoration: none;
} }
.navbar-ns { .navbar-logo img {
background: #334155; border-radius: 4px; padding: 3px 10px; height: 18px;
font-size: 12px; color: #94a3b8; font-family: monospace; /* SVG залит #001C34, инвертируем до белого */
filter: brightness(0) invert(1);
}
.navbar-logo-sep {
width: 1px; height: 18px; background: #1a4a73; margin: 0 4px;
}
.navbar-logo-product {
font-size: 13px; font-weight: 600; color: #7aafd4;
letter-spacing: 0.04em; text-transform: uppercase;
} }
.navbar-spacer { flex: 1; } .navbar-spacer { flex: 1; }
/* Layout */ /* ——— Layout ——— */
.container { max-width: 920px; margin: 0 auto; padding: 32px 24px; } .container { max-width: 940px; margin: 0 auto; padding: 32px 24px; }
/* Card */ /* ——— Card ——— */
.card { .card {
background: #1e293b; border-radius: 10px; border: 1px solid #334155; background: #001929;
border-radius: 8px;
border: 1px solid #0b2d50;
padding: 24px; margin-bottom: 16px; padding: 24px; margin-bottom: 16px;
} }
.card-title { font-size: 16px; font-weight: 600; margin-bottom: 16px; } .card-title {
font-size: 15px; font-weight: 600;
/* Form */ color: #b8d4ec;
.form-group { margin-bottom: 16px; } margin-bottom: 16px;
label { display: block; font-size: 13px; color: #94a3b8; margin-bottom: 6px; } letter-spacing: 0.01em;
input, textarea, select {
width: 100%; background: #0f172a; border: 1px solid #334155;
border-radius: 6px; padding: 10px 12px; color: #e2e8f0;
font-size: 14px; font-family: inherit;
} }
input:focus, textarea:focus { outline: none; border-color: #3b82f6; }
/* ——— Form ——— */
.form-group { margin-bottom: 16px; }
label { display: block; font-size: 12px; font-weight: 500; color: #6b8eaa; margin-bottom: 6px; text-transform: uppercase; letter-spacing: 0.05em; }
input, textarea, select {
width: 100%;
background: #001120;
border: 1px solid #0b2d50;
border-radius: 5px; padding: 10px 12px;
color: #e2ecf6;
font-size: 14px; font-family: inherit;
transition: border-color 0.15s;
}
input:focus, textarea:focus { outline: none; border-color: #1a7fd4; box-shadow: 0 0 0 2px rgba(26,127,212,0.15); }
textarea { resize: vertical; font-family: 'Courier New', monospace; line-height: 1.5; } textarea { resize: vertical; font-family: 'Courier New', monospace; line-height: 1.5; }
input[type="number"] { width: auto; } input[type="number"] { width: auto; }
/* Buttons */ /* ——— Buttons ——— */
.btn { .btn {
border: none; border-radius: 6px; padding: 10px 18px; font-size: 14px; border: none; border-radius: 5px; padding: 10px 18px; font-size: 13px;
font-weight: 500; cursor: pointer; transition: opacity 0.15s; display: inline-flex; font-weight: 600; cursor: pointer; transition: opacity 0.15s, background 0.15s;
align-items: center; gap: 6px; white-space: nowrap; display: inline-flex; align-items: center; gap: 6px; white-space: nowrap;
letter-spacing: 0.02em;
} }
.btn:hover:not(:disabled) { opacity: 0.82; } .btn:hover:not(:disabled) { opacity: 0.85; }
.btn:disabled { opacity: 0.38; cursor: not-allowed; } .btn:disabled { opacity: 0.35; cursor: not-allowed; }
.btn-primary { background: #3b82f6; color: #fff; } .btn-primary { background: #1a7fd4; color: #fff; }
.btn-danger { background: #ef4444; color: #fff; } .btn-danger { background: #c0392b; color: #fff; }
.btn-success { background: #22c55e; color: #0f172a; font-weight: 600; } .btn-success { background: #1a9e5c; color: #fff; }
.btn-warning { background: #f59e0b; color: #0f172a; font-weight: 600; } .btn-warning { background: #d48f0a; color: #fff; }
.btn-ghost { background: transparent; color: #94a3b8; border: 1px solid #334155; } .btn-ghost { background: transparent; color: #6b8eaa; border: 1px solid #0b2d50; }
.btn-sm { padding: 5px 12px; font-size: 12px; } .btn-ghost:hover:not(:disabled) { color: #b8d4ec; border-color: #1a4a73; opacity: 1; }
.btn-sm { padding: 5px 11px; font-size: 12px; }
/* Table */ /* ——— Table ——— */
table { width: 100%; border-collapse: collapse; } table { width: 100%; border-collapse: collapse; }
th { th {
text-align: left; font-size: 11px; color: #64748b; text-align: left; font-size: 11px; font-weight: 600; color: #2d5070;
text-transform: uppercase; letter-spacing: 0.06em; text-transform: uppercase; letter-spacing: 0.07em;
padding: 0 14px 10px; border-bottom: 1px solid #334155; padding: 0 14px 10px; border-bottom: 1px solid #0b2d50;
} }
td { padding: 13px 14px; border-bottom: 1px solid rgba(51,65,85,0.5); vertical-align: middle; } td { padding: 13px 14px; border-bottom: 1px solid rgba(11,45,80,0.6); vertical-align: middle; }
tbody tr:last-child td { border-bottom: none; } tbody tr:last-child td { border-bottom: none; }
tbody tr:hover td { background: rgba(255,255,255,0.02); } tbody tr:hover td { background: rgba(26,127,212,0.04); }
/* Badges */ /* ——— Badges ——— */
.badge { .badge {
display: inline-flex; align-items: center; gap: 5px; display: inline-flex; align-items: center; gap: 5px;
font-size: 12px; font-weight: 500; border-radius: 9999px; padding: 2px 10px; font-size: 11px; font-weight: 600; border-radius: 4px;
padding: 3px 9px; letter-spacing: 0.04em; text-transform: uppercase;
} }
.badge::before { content: '●'; font-size: 7px; } .badge::before { content: '●'; font-size: 6px; }
.badge-active { background: rgba(34,197,94,0.12); color: #22c55e; } .badge-active { background: rgba(26,158,92,0.15); color: #2ecc8a; }
.badge-active::before { color: #22c55e; } .badge-active::before { color: #2ecc8a; }
.badge-disabled { background: rgba(100,116,139,0.12); color: #64748b; } .badge-disabled { background: rgba(45,80,112,0.3); color: #2d5070; }
.badge-disabled::before { color: #64748b; } .badge-disabled::before { color: #2d5070; }
.badge-error { background: rgba(239,68,68,0.12); color: #ef4444; } .badge-error { background: rgba(192,57,43,0.15); color: #e55042; }
.badge-error::before { color: #ef4444; } .badge-error::before { color: #e55042; }
.badge-pending { background: rgba(251,191,36,0.12); color: #fbbf24; } .badge-pending { background: rgba(212,143,10,0.15); color: #d4a017; }
.badge-pending::before { color: #fbbf24; } .badge-pending::before { color: #d4a017; }
/* Tabs */ /* ——— Tabs ——— */
.tabs { .tabs {
display: flex; border-bottom: 1px solid #334155; margin-bottom: 24px; display: flex; border-bottom: 1px solid #0b2d50; margin-bottom: 24px;
} }
.tab { .tab {
padding: 12px 20px; font-size: 14px; font-weight: 500; cursor: pointer; padding: 11px 20px; font-size: 13px; font-weight: 600;
color: #64748b; border-bottom: 2px solid transparent; transition: all 0.15s; cursor: pointer; color: #2d5070;
user-select: none; border-bottom: 2px solid transparent;
transition: color 0.15s, border-color 0.15s;
user-select: none; letter-spacing: 0.03em;
} }
.tab:hover { color: #cbd5e1; } .tab:hover { color: #7aafd4; }
.tab.active { color: #3b82f6; border-bottom-color: #3b82f6; } .tab.active { color: #1a7fd4; border-bottom-color: #1a7fd4; }
/* Credentials rows */ /* ——— Credentials ——— */
.cred-row { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; } .cred-row { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.cred-label { font-size: 12px; color: #64748b; min-width: 130px; } .cred-label { font-size: 11px; font-weight: 600; color: #2d5070; min-width: 130px; text-transform: uppercase; letter-spacing: 0.05em; }
.cred-value { .cred-value {
font-family: 'Courier New', monospace; font-size: 13px; font-family: 'Courier New', monospace; font-size: 13px;
background: #0a0f1a; padding: 8px 12px; border-radius: 4px; background: #000d18; padding: 8px 12px; border-radius: 4px;
border: 1px solid #334155; flex: 1; color: #cbd5e1; border: 1px solid #0b2d50; flex: 1; color: #9ecaee;
overflow-x: auto; word-break: break-all; overflow-x: auto; word-break: break-all;
} }
/* Connection status pill */ /* ——— Connection pill ——— */
.conn-pill { .conn-pill {
display: inline-flex; align-items: center; gap: 7px; display: inline-flex; align-items: center; gap: 7px;
font-size: 13px; font-weight: 500; padding: 5px 14px; border-radius: 9999px; font-size: 12px; font-weight: 600; padding: 4px 13px;
border-radius: 4px; letter-spacing: 0.04em; text-transform: uppercase;
} }
.conn-pill::before { content: '●'; font-size: 7px; } .conn-pill::before { content: '●'; font-size: 6px; }
.conn-pill.off { background: rgba(100,116,139,0.12); color: #64748b; } .conn-pill.off { background: rgba(45,80,112,0.25); color: #2d5070; }
.conn-pill.off::before { color: #64748b; } .conn-pill.off::before { color: #2d5070; }
.conn-pill.ing { background: rgba(251,191,36,0.12); color: #fbbf24; } .conn-pill.ing { background: rgba(212,143,10,0.15); color: #d4a017; }
.conn-pill.ing::before { color: #fbbf24; animation: blink 1s step-start infinite; } .conn-pill.ing::before { color: #d4a017; animation: blink 1s step-start infinite; }
.conn-pill.on { background: rgba(34,197,94,0.12); color: #22c55e; } .conn-pill.on { background: rgba(26,158,92,0.15); color: #2ecc8a; }
.conn-pill.on::before { color: #22c55e; } .conn-pill.on::before { color: #2ecc8a; }
@keyframes blink { 50% { opacity: 0; } } @keyframes blink { 50% { opacity: 0; } }
/* MQTT log */ /* ——— MQTT log ——— */
.mqtt-log { .mqtt-log {
background: #080d18; border: 1px solid #334155; border-radius: 6px; background: #000d18; border: 1px solid #0b2d50; border-radius: 5px;
padding: 10px 12px; max-height: 180px; overflow-y: auto; padding: 10px 12px; max-height: 180px; overflow-y: auto;
font-family: 'Courier New', monospace; font-size: 12px; font-family: 'Courier New', monospace; font-size: 12px;
line-height: 1.6; line-height: 1.6;
} }
.mqtt-log .ok { color: #22c55e; } .mqtt-log .ok { color: #2ecc8a; }
.mqtt-log .err { color: #ef4444; } .mqtt-log .err { color: #e55042; }
.mqtt-log .info { color: #64748b; } .mqtt-log .info { color: #2d5070; }
/* Modal */ /* ——— Modal ——— */
.modal-overlay { .modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.72); position: fixed; inset: 0; background: rgba(0,8,16,0.82);
display: flex; align-items: center; justify-content: center; z-index: 200; display: flex; align-items: center; justify-content: center; z-index: 200;
} }
.modal { .modal {
background: #1e293b; border: 1px solid #334155; border-radius: 12px; background: #001929; border: 1px solid #0b2d50; border-radius: 8px;
padding: 28px; width: 100%; max-width: 460px; padding: 28px; width: 100%; max-width: 460px;
} }
.modal-title { font-size: 16px; font-weight: 600; margin-bottom: 20px; } .modal-title { font-size: 15px; font-weight: 700; margin-bottom: 20px; color: #b8d4ec; }
/* Alerts */ /* ——— Alerts ——— */
.alert { padding: 11px 15px; border-radius: 6px; font-size: 13px; margin-bottom: 14px; } .alert { padding: 11px 15px; border-radius: 5px; font-size: 13px; margin-bottom: 14px; }
.alert-error { background: rgba(239,68,68,0.12); color: #f87171; border: 1px solid rgba(239,68,68,0.2); } .alert-error { background: rgba(192,57,43,0.1); color: #e55042; border: 1px solid rgba(192,57,43,0.25); }
.alert-success { background: rgba(34,197,94,0.12); color: #4ade80; border: 1px solid rgba(34,197,94,0.2); } .alert-success { background: rgba(26,158,92,0.1); color: #2ecc8a; border: 1px solid rgba(26,158,92,0.25); }
.alert-info { background: rgba(59,130,246,0.12); color: #60a5fa; border: 1px solid rgba(59,130,246,0.2); } .alert-info { background: rgba(26,127,212,0.1); color: #5aaae8; border: 1px solid rgba(26,127,212,0.25); }
/* Misc */ /* ——— Misc ——— */
.row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } .row { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.flex1 { flex: 1; } .flex1 { flex: 1; }
.mt8 { margin-top: 8px; } .mt8 { margin-top: 8px; }
.mt16 { margin-top: 16px; } .mt16 { margin-top: 16px; }
.mt24 { margin-top: 24px; } .mt24 { margin-top: 24px; }
.separator { border: none; border-top: 1px solid #334155; margin: 20px 0; } .separator { border: none; border-top: 1px solid #0b2d50; margin: 20px 0; }
.hint { font-size: 12px; color: #64748b; line-height: 1.6; } .hint { font-size: 12px; color: #2d5070; line-height: 1.6; }
.mono { font-family: 'Courier New', monospace; } .mono { font-family: 'Courier New', monospace; }
.coming-soon {
text-align: center; padding: 56px 24px; color: #475569; /* ——— Device list empty state ——— */
} .coming-soon { text-align: center; padding: 56px 24px; color: #2d5070; }
.coming-soon .icon { font-size: 40px; margin-bottom: 12px; } .coming-soon .icon { font-size: 36px; margin-bottom: 12px; opacity: 0.5; }
.coming-soon h3 { font-size: 18px; font-weight: 600; margin-bottom: 8px; color: #64748b; } .coming-soon h3 { font-size: 17px; font-weight: 600; margin-bottom: 8px; color: #1a4a73; }
a { color: #3b82f6; text-decoration: none; }
a { color: #1a7fd4; text-decoration: none; }
a:hover { text-decoration: underline; } a:hover { text-decoration: underline; }
.help-block { border: 1px solid #1e293b; border-radius: 10px; padding: 14px 18px; background: #0f172a; }
/* ——— Help block ——— */
.help-block {
border: 1px solid #0b2d50; border-radius: 8px;
padding: 16px 20px; background: #001929;
}
.help-block summary::-webkit-details-marker { display: none; } .help-block summary::-webkit-details-marker { display: none; }
.help-block[open] summary span { transform: rotate(90deg); display: inline-block; } .help-block[open] summary .help-arrow { transform: rotate(90deg); }
.help-step { display: flex; gap: 14px; align-items: flex-start; font-size: 13px; color: #94a3b8; line-height: 1.6; } .help-arrow { display: inline-block; transition: transform 0.15s; margin-right: 6px; font-size: 10px; }
.help-num { flex-shrink: 0; width: 24px; height: 24px; border-radius: 50%; background: #1e3a5f; color: #60a5fa; font-weight: 700; font-size: 12px; display: flex; align-items: center; justify-content: center; margin-top: 2px; } .help-step { display: flex; gap: 14px; align-items: flex-start; font-size: 13px; color: #6b8eaa; line-height: 1.6; }
.help-step strong { color: #e2e8f0; } .help-num {
.help-step em { color: #60a5fa; font-style: normal; } flex-shrink: 0; width: 22px; height: 22px; border-radius: 4px;
background: #001C34; border: 1px solid #1a4a73;
color: #1a7fd4; font-weight: 700; font-size: 11px;
display: flex; align-items: center; justify-content: center; margin-top: 2px;
}
.help-step strong { color: #b8d4ec; }
.help-step em { color: #1a7fd4; font-style: normal; }
</style> </style>
</head> </head>
<body> <body>
@@ -474,10 +524,11 @@ function renderLogin() {
app().innerHTML = ` app().innerHTML = `
<div style="display:flex;align-items:center;justify-content:center;min-height:100vh;padding:24px;"> <div style="display:flex;align-items:center;justify-content:center;min-height:100vh;padding:24px;">
<div class="card" style="width:100%;max-width:420px;"> <div class="card" style="width:100%;max-width:420px;">
<div style="text-align:center;margin-bottom:28px;"> <div style="text-align:center;margin-bottom:32px;">
<div style="font-size:32px;margin-bottom:8px;">⚡</div> <img src="https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/logo.svg"
<div style="font-size:22px;font-weight:800;color:#3b82f6;">IoT Консоль</div> alt="Nubes" style="height:24px;filter:brightness(0) invert(1);margin-bottom:16px;">
<div style="font-size:13px;color:#64748b;margin-top:4px;">Управление устройствами и телеметрией</div> <div style="font-size:11px;font-weight:700;color:#1a7fd4;letter-spacing:0.18em;text-transform:uppercase;margin-bottom:6px;">IoT Console</div>
<div style="font-size:12px;color:#2d5070;">Управление устройствами и телеметрией</div>
</div> </div>
<div id="login-err"></div> <div id="login-err"></div>
<div class="form-group"> <div class="form-group">
@@ -723,7 +774,7 @@ function deviceLayout(d) {
<details class="help-block" style="margin-top:24px;"> <details class="help-block" style="margin-top:24px;">
<summary style="cursor:pointer;font-size:13px;font-weight:600;color:#64748b;user-select:none;list-style:none;display:flex;align-items:center;gap:6px;"> <summary style="cursor:pointer;font-size:13px;font-weight:600;color:#64748b;user-select:none;list-style:none;display:flex;align-items:center;gap:6px;">
<span>▶</span> Как это работает — краткая инструкция <span class="help-arrow">▶</span> Как это работает — краткая инструкция
</summary> </summary>
<div style="margin-top:14px;display:grid;gap:12px;"> <div style="margin-top:14px;display:grid;gap:12px;">
@@ -1010,7 +1061,11 @@ function telemetryTab() {
function navbar() { function navbar() {
return ` return `
<nav class="navbar"> <nav class="navbar">
<div class="navbar-brand" onclick="nav('devices')">⚡ IoT Консоль</div> <div class="navbar-logo" onclick="nav('devices')">
<img src="https://terra.k8c.ru/docs/nubes/nubes/2.0.2/30_registry/assets/logo.svg" alt="Nubes">
<div class="navbar-logo-sep"></div>
<span class="navbar-logo-product">IoT Console</span>
</div>
<div class="navbar-spacer"></div> <div class="navbar-spacer"></div>
<button class="btn btn-ghost btn-sm" onclick="doLogout()">Выйти</button> <button class="btn btn-ghost btn-sm" onclick="doLogout()">Выйти</button>
</nav>`; </nav>`;