diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index 117907d..ac07158 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -3,11 +3,9 @@
import {
GnBadge,
GnButton,
- GnCard,
GnCheckbox,
GnInput,
GnModal,
- GnPageHeader,
GnTabs,
GnTextarea
} from "gnexus-ui-kit/vue";
@@ -51,6 +49,8 @@
});
const visibleFields = computed(() => selected.value?.fields || []);
+const activeSecrets = computed(() => secrets.value.filter((secret) => !secret.archived).length);
+const mcpSecrets = computed(() => secrets.value.filter((secret) => secret.allow_mcp).length);
function parseFields() {
return form.fieldsText
@@ -175,26 +175,55 @@
-
+
-
+
-
-
+
+ Select a secret
+ Metadata is shown by default. Values are available through reveal.
+
+
-
- Create secret
-
-
-
-
-
-
-
-
-
-
-
+
-
+
Version history
{{ selected?.title || "Select a secret first" }}
Version {{ version.version_number }}
{{ version.created_at }}
-
+
-
+
Audit
Refresh
@@ -271,11 +336,11 @@
{{ event.action }}
{{ event.channel }} · {{ event.created_at }}
-
+
-
+
API tokens
Refresh
@@ -286,17 +351,17 @@
{{ token.name }}
{{ token.scopes.join(", ") }}
-
+
-
+
Settings
Export decrypted JSON
Delete all data
-
+
diff --git a/frontend/src/styles.css b/frontend/src/styles.css
index fb0900f..a815b70 100644
--- a/frontend/src/styles.css
+++ b/frontend/src/styles.css
@@ -1,30 +1,121 @@
+:root {
+ color-scheme: dark;
+}
+
+* {
+ box-sizing: border-box;
+}
+
body {
margin: 0;
+ min-width: 320px;
+ background: #101018;
+ color: #d6ddff;
+ font-family:
+ "IBM Plex Mono",
+ ui-monospace,
+ SFMono-Regular,
+ Menlo,
+ Consolas,
+ monospace;
+}
+
+button,
+input,
+textarea,
+select {
+ font: inherit;
}
.creds-app {
min-height: 100vh;
- padding: 24px;
+ width: min(1480px, calc(100vw - 48px));
+ margin: 0 auto;
+ padding: 24px 0 40px;
}
-.creds-grid {
+.app-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 24px;
+ padding: 18px 20px;
+ border: 1px solid #3e4665;
+ background: #171824;
+}
+
+.app-header h1,
+.panel h2 {
+ margin: 0;
+ line-height: 1.15;
+ letter-spacing: 0;
+}
+
+.app-header h1 {
+ font-size: 24px;
+}
+
+.eyebrow {
+ margin: 0 0 6px;
+ color: #8ea2e8;
+ font-size: 12px;
+ text-transform: uppercase;
+}
+
+.session-box {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ min-width: 0;
+ color: #aeb8df;
+}
+
+.session-box span {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.app-tabs {
+ margin: 14px 0;
+}
+
+.metrics-row {
display: grid;
- grid-template-columns: minmax(280px, 1fr) minmax(320px, 1.2fr) minmax(320px, 1fr);
- gap: 16px;
- align-items: start;
+ grid-template-columns: repeat(3, minmax(120px, 160px)) minmax(420px, 1fr);
+ gap: 12px;
+ margin-bottom: 14px;
}
-.panel-stack {
- display: grid;
- gap: 16px;
- max-width: 980px;
-}
-
+.metric,
.panel {
- display: grid;
- gap: 14px;
+ border: 1px solid #3e4665;
+ background: #1a1b28;
}
+.metric {
+ display: grid;
+ gap: 6px;
+ min-height: 76px;
+ padding: 14px 16px;
+}
+
+.metric span,
+.muted,
+small {
+ color: #9ba7cb;
+}
+
+.metric strong {
+ font-size: 24px;
+ line-height: 1;
+}
+
+.metric.wide {
+ min-width: 0;
+}
+
+.search-line,
.toolbar {
display: flex;
gap: 10px;
@@ -32,52 +123,212 @@
flex-wrap: wrap;
}
-.secret-row,
-.field-row {
- width: 100%;
+.search-line > *:first-child {
+ flex: 1 1 280px;
+ min-width: 220px;
+}
+
+.workspace-grid {
+ display: grid;
+ grid-template-columns: 340px minmax(420px, 1fr) 380px;
+ gap: 14px;
+ align-items: start;
+}
+
+.panel-stack {
+ display: grid;
+ gap: 14px;
+ max-width: 960px;
+}
+
+.panel {
+ display: grid;
+ gap: 14px;
+ min-width: 0;
+ padding: 16px;
+}
+
+.panel-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
- padding: 10px 0;
- border: 0;
- border-bottom: 1px solid var(--gn-border-subtle, #273142);
+ min-width: 0;
+}
+
+.panel-title h2 {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 18px;
+}
+
+.panel-title span {
+ color: #9ba7cb;
+ font-size: 12px;
+ white-space: nowrap;
+}
+
+.list-panel {
+ max-height: calc(100vh - 220px);
+ overflow: auto;
+}
+
+.secret-row,
+.field-row {
+ width: 100%;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) auto;
+ gap: 12px;
+ align-items: center;
+ padding: 11px 10px;
+ border: 1px solid transparent;
+ border-bottom-color: #30384f;
background: transparent;
color: inherit;
text-align: left;
}
+button.secret-row {
+ cursor: pointer;
+}
+
+button.secret-row:hover,
+button.secret-row.selected {
+ border-color: #6f7eb3;
+ background: #222438;
+}
+
.secret-row span,
-.field-row span {
+.field-row span,
+.row-meta {
+ min-width: 0;
display: grid;
gap: 4px;
}
+.secret-row strong,
+.field-row strong {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.row-meta {
+ justify-items: end;
+}
+
+.description-grid {
+ display: grid;
+ grid-template-columns: 110px minmax(0, 1fr);
+ gap: 9px 14px;
+ padding: 12px;
+ border: 1px solid #30384f;
+ background: #151622;
+}
+
+.description-grid span {
+ color: #9ba7cb;
+}
+
+.description-grid strong {
+ min-width: 0;
+ overflow-wrap: anywhere;
+}
+
.field-list {
display: grid;
+ border-top: 1px solid #30384f;
+}
+
+.field-row {
+ grid-template-columns: minmax(120px, 180px) minmax(0, 1fr) auto;
+}
+
+.field-row code {
+ min-width: 0;
+ overflow: hidden;
+ padding: 6px 8px;
+ background: #11121c;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
.revealed {
- margin-top: 8px;
+ margin-top: 4px;
+ border: 1px solid #536184;
+}
+
+.form-grid {
+ display: grid;
+ gap: 12px;
}
.checks {
display: flex;
gap: 14px;
flex-wrap: wrap;
+ align-items: center;
}
-.muted,
-small {
- opacity: 0.72;
+.empty-state {
+ display: grid;
+ gap: 6px;
+ padding: 18px;
+ border: 1px dashed #465173;
+ color: #9ba7cb;
+}
+
+.empty-state strong {
+ color: #d6ddff;
+}
+
+.empty-state.large {
+ min-height: 240px;
+ align-content: center;
+ justify-items: center;
+ text-align: center;
}
.error {
- color: var(--gn-danger, #ff5c7a);
+ margin: 0;
+ color: #ff7b8d;
}
-@media (max-width: 1100px) {
- .creds-grid {
+@media (max-width: 1260px) {
+ .workspace-grid,
+ .metrics-row {
+ grid-template-columns: 1fr;
+ }
+
+ .list-panel {
+ max-height: none;
+ }
+}
+
+@media (max-width: 720px) {
+ .creds-app {
+ width: min(100vw - 24px, 1480px);
+ padding-top: 12px;
+ }
+
+ .app-header,
+ .session-box {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .field-row,
+ .secret-row {
+ grid-template-columns: 1fr;
+ }
+
+ .row-meta {
+ justify-items: start;
+ }
+
+ .description-grid {
grid-template-columns: 1fr;
}
}
diff --git a/gnexus_creds/main.py b/gnexus_creds/main.py
index 08e6f77..17057d9 100644
--- a/gnexus_creds/main.py
+++ b/gnexus_creds/main.py
@@ -20,6 +20,7 @@
PROJECT_ROOT = Path(__file__).resolve().parent.parent
FRONTEND_DIST = PROJECT_ROOT / "frontend" / "dist"
+UI_KIT_ASSETS = PROJECT_ROOT / "frontend" / "node_modules" / "gnexus-ui-kit" / "dist" / "assets"
def create_app() -> FastAPI:
@@ -45,6 +46,10 @@
return {"status": "ok"}
if FRONTEND_DIST.exists():
+ ui_kit_fonts_dir = UI_KIT_ASSETS / "fonts"
+ if ui_kit_fonts_dir.exists():
+ app.mount("/assets/fonts", StaticFiles(directory=ui_kit_fonts_dir), name="ui-kit-fonts")
+
assets_dir = FRONTEND_DIST / "assets"
if assets_dir.exists():
app.mount("/assets", StaticFiles(directory=assets_dir), name="assets")