Extend theming from 3 CSS variables (primary colors only) to 14 configurable properties covering colors, fonts, borders, and shape. All values are optional with sensible defaults. - New _theme.html include replaces duplicated inline injection - Wire theme include into all 7 templates (base, login, dashboard, catalog, admin_tables, activity_center, corporate_memory) - Conditional font loading: skip default Inter when custom font_url set - Config.theme_overrides() classmethod generates CSS variable dict - Visual theme-reference.html guide for instance configurators - Document all theme keys in instance.yaml.example
2619 lines
87 KiB
HTML
2619 lines
87 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Dashboard - Data Analyst Portal</title>
|
|
{% if not config.THEME_FONT_URL %}
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
{% endif %}
|
|
<link rel="stylesheet" href="{{ url_for('static', filename='style-custom.css') }}">
|
|
<style>
|
|
/* ── Header ── */
|
|
.header {
|
|
background: var(--surface);
|
|
border-bottom: 1px solid var(--border);
|
|
padding: 0 32px;
|
|
height: 72px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
position: sticky;
|
|
top: 0;
|
|
z-index: 100;
|
|
}
|
|
|
|
.header-left {
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
gap: 2px;
|
|
}
|
|
|
|
.header-logo svg {
|
|
display: block;
|
|
}
|
|
|
|
.header-subtitle {
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
letter-spacing: 0.4px;
|
|
text-transform: uppercase;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.header-right {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
}
|
|
|
|
.header-email {
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.avatar {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
background: var(--primary-light);
|
|
color: var(--primary);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
letter-spacing: 0.3px;
|
|
}
|
|
|
|
.avatar-img {
|
|
width: 36px;
|
|
height: 36px;
|
|
border-radius: 50%;
|
|
border: 2px solid var(--border);
|
|
}
|
|
|
|
.btn-logout {
|
|
font-family: var(--font-primary);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
background: none;
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 6px 14px;
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.btn-logout:hover {
|
|
color: var(--text-primary);
|
|
border-color: #D1D5DB;
|
|
background: var(--border-light);
|
|
}
|
|
|
|
/* ── Main Container ── */
|
|
.main {
|
|
max-width: 1280px;
|
|
margin: 0 auto;
|
|
padding: 28px 32px 48px;
|
|
}
|
|
|
|
/* ── Stats Row ── */
|
|
.stats-row {
|
|
display: grid;
|
|
grid-template-columns: repeat(5, 1fr);
|
|
gap: 16px;
|
|
margin-bottom: 28px;
|
|
}
|
|
|
|
.stat-card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
padding: 20px 22px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.stat-label {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.stat-value {
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
letter-spacing: -0.5px;
|
|
line-height: 1.2;
|
|
}
|
|
|
|
/* ── Credit Line ── */
|
|
.credit-line {
|
|
text-align: center;
|
|
font-size: 12px;
|
|
color: #9CA3AF;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.credit-line .heart {
|
|
color: #EF4444;
|
|
}
|
|
|
|
.credit-line strong {
|
|
color: #6B7280;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* ── Two-Column Layout ── */
|
|
.dashboard-grid {
|
|
display: grid;
|
|
grid-template-columns: 1fr 400px;
|
|
grid-template-rows: auto auto;
|
|
gap: 24px;
|
|
}
|
|
|
|
.left-column {
|
|
grid-row: 1 / 3;
|
|
display: grid;
|
|
grid-template-rows: subgrid;
|
|
gap: 24px;
|
|
}
|
|
|
|
.right-column {
|
|
grid-row: 1 / 3;
|
|
display: grid;
|
|
grid-template-rows: subgrid;
|
|
gap: 24px;
|
|
position: sticky;
|
|
top: 24px;
|
|
}
|
|
|
|
/* ── Card Base ── */
|
|
.card {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 12px;
|
|
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.card-header {
|
|
padding: 22px 24px 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.card-header-left {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
}
|
|
|
|
.card-title {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.card-body {
|
|
padding: 20px 24px 24px;
|
|
}
|
|
|
|
/* ── Section Title ── */
|
|
.section-title {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.6px;
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
/* ── Data Source Cards ── */
|
|
.data-source {
|
|
padding: 22px 24px;
|
|
border-bottom: 1px solid var(--border-light);
|
|
}
|
|
|
|
.data-source:last-of-type {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.data-source-header {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
justify-content: space-between;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.data-source-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
}
|
|
|
|
.data-source-icon {
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 10px;
|
|
background: var(--border-light);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.data-source-icon svg {
|
|
color: #6B7280;
|
|
}
|
|
|
|
.data-source-icon.realtime {
|
|
background: rgba(16, 183, 127, 0.1);
|
|
}
|
|
|
|
.data-source-icon.realtime svg {
|
|
color: var(--success);
|
|
}
|
|
|
|
.data-source-icon.disabled {
|
|
background: #F3F4F6;
|
|
}
|
|
|
|
.data-source-icon.disabled svg {
|
|
color: #9CA3AF;
|
|
}
|
|
|
|
.data-source-name {
|
|
font-size: 15px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.data-source-status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.status-dot {
|
|
width: 7px;
|
|
height: 7px;
|
|
border-radius: 50%;
|
|
background: var(--success);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.status-dot--live {
|
|
animation: pulse-dot 2s ease-in-out infinite;
|
|
}
|
|
|
|
@keyframes pulse-dot {
|
|
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(16, 183, 127, 0.4); }
|
|
50% { opacity: 0.8; box-shadow: 0 0 0 4px rgba(16, 183, 127, 0); }
|
|
}
|
|
|
|
.data-source-details {
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
line-height: 1.6;
|
|
margin-left: 58px;
|
|
}
|
|
|
|
.badge-included {
|
|
flex-shrink: 0;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: #059669;
|
|
background: rgba(16, 183, 127, 0.1);
|
|
border-radius: 6px;
|
|
padding: 4px 10px;
|
|
white-space: nowrap;
|
|
align-self: center;
|
|
}
|
|
|
|
.badge-subscribed {
|
|
flex-shrink: 0;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: var(--primary);
|
|
background: var(--primary-light);
|
|
border-radius: 6px;
|
|
padding: 4px 10px;
|
|
white-space: nowrap;
|
|
align-self: center;
|
|
}
|
|
|
|
.badge-disabled {
|
|
flex-shrink: 0;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: #9CA3AF;
|
|
background: #F3F4F6;
|
|
border-radius: 6px;
|
|
padding: 4px 10px;
|
|
white-space: nowrap;
|
|
align-self: center;
|
|
}
|
|
|
|
/* ── Toggles inside data source ── */
|
|
.data-source-toggles {
|
|
display: flex;
|
|
gap: 16px;
|
|
margin-left: 58px;
|
|
margin-top: 12px;
|
|
padding-top: 12px;
|
|
border-top: 1px solid var(--border-light);
|
|
}
|
|
|
|
.toggle-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.toggle-switch {
|
|
position: relative;
|
|
width: 36px;
|
|
height: 20px;
|
|
}
|
|
|
|
.toggle-switch input {
|
|
opacity: 0;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
|
|
.toggle-slider {
|
|
position: absolute;
|
|
cursor: pointer;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: #E5E7EB;
|
|
transition: .2s;
|
|
border-radius: 20px;
|
|
}
|
|
|
|
.toggle-slider:before {
|
|
position: absolute;
|
|
content: "";
|
|
height: 14px;
|
|
width: 14px;
|
|
left: 3px;
|
|
bottom: 3px;
|
|
background-color: white;
|
|
transition: .2s;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
input:checked + .toggle-slider {
|
|
background-color: var(--primary);
|
|
}
|
|
|
|
input:checked + .toggle-slider:before {
|
|
transform: translateX(16px);
|
|
}
|
|
|
|
/* Disabled/locked toggle */
|
|
.toggle-switch.locked .toggle-slider {
|
|
cursor: not-allowed;
|
|
background-color: #9CA3AF;
|
|
}
|
|
|
|
.toggle-switch.locked .toggle-slider:before {
|
|
transform: translateX(16px);
|
|
}
|
|
|
|
input:disabled + .toggle-slider {
|
|
cursor: not-allowed;
|
|
opacity: 0.6;
|
|
}
|
|
|
|
.toggle-label {
|
|
font-size: 12px;
|
|
color: var(--text-primary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.toggle-label.locked {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* ── Catalog CTA ── */
|
|
.catalog-cta {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 18px 24px;
|
|
border-top: 1px solid var(--border-light);
|
|
background: var(--primary-light);
|
|
border-radius: 0 0 12px 12px;
|
|
}
|
|
|
|
.catalog-cta-text {
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.catalog-cta-link {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
white-space: nowrap;
|
|
transition: gap 0.15s ease;
|
|
}
|
|
|
|
.catalog-cta-link:hover {
|
|
gap: 10px;
|
|
}
|
|
|
|
/* ── Corporate Memory Widget ── */
|
|
.memory-card {
|
|
border-left: 3px solid var(--warning);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.memory-card .card-header {
|
|
background: rgba(245, 159, 10, 0.08);
|
|
padding: 22px 24px;
|
|
}
|
|
|
|
.memory-icon {
|
|
width: 28px;
|
|
height: 28px;
|
|
background: var(--warning);
|
|
border-radius: 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.badge-beta {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
background: var(--border-light);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 2px 7px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.memory-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 12px;
|
|
padding: 20px 24px;
|
|
}
|
|
|
|
.memory-stat {
|
|
text-align: center;
|
|
padding: 14px 8px;
|
|
border-radius: 8px;
|
|
background: var(--border-light);
|
|
}
|
|
|
|
.memory-stat--highlight {
|
|
background: rgba(245, 159, 10, 0.1);
|
|
}
|
|
|
|
.memory-stat-value {
|
|
font-size: 22px;
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.memory-stat--highlight .memory-stat-value {
|
|
color: #D97706;
|
|
}
|
|
|
|
.memory-stat-label {
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.memory-description {
|
|
margin: 0 24px 20px;
|
|
padding: 14px 16px;
|
|
border-left: 3px solid rgba(245, 159, 10, 0.4);
|
|
background: rgba(245, 159, 10, 0.04);
|
|
border-radius: 0 8px 8px 0;
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
line-height: 1.6;
|
|
flex: 1;
|
|
}
|
|
|
|
.memory-description strong {
|
|
color: var(--text-primary);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.memory-description code {
|
|
background: rgba(0, 0, 0, 0.06);
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
}
|
|
|
|
.memory-footer {
|
|
padding: 16px 24px;
|
|
border-top: 1px solid var(--border-light);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-top: auto;
|
|
}
|
|
|
|
.memory-sync {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.memory-sync .sync-icon {
|
|
color: var(--success);
|
|
}
|
|
|
|
.btn-memory {
|
|
font-family: var(--font-primary);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #92400E;
|
|
background: rgba(245, 159, 10, 0.15);
|
|
border: none;
|
|
border-radius: 8px;
|
|
padding: 8px 16px;
|
|
cursor: pointer;
|
|
transition: background 0.15s ease;
|
|
text-decoration: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.btn-memory:hover {
|
|
background: rgba(245, 159, 10, 0.25);
|
|
}
|
|
|
|
/* ── Activity Center Widget ── */
|
|
.activity-card {
|
|
border-left: 3px solid #10B981;
|
|
}
|
|
|
|
.activity-card .card-header {
|
|
background: rgba(16, 185, 129, 0.08);
|
|
padding: 22px 24px;
|
|
}
|
|
|
|
.activity-icon {
|
|
width: 28px;
|
|
height: 28px;
|
|
background: #10B981;
|
|
border-radius: 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.badge-demo {
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
color: #92400E;
|
|
background: rgba(245, 159, 10, 0.15);
|
|
border: 1px solid rgba(245, 159, 10, 0.3);
|
|
border-radius: 4px;
|
|
padding: 2px 7px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
.activity-stats {
|
|
display: grid;
|
|
grid-template-columns: repeat(3, 1fr);
|
|
gap: 12px;
|
|
padding: 20px 24px;
|
|
}
|
|
|
|
.activity-stat {
|
|
text-align: center;
|
|
padding: 14px 8px;
|
|
border-radius: 8px;
|
|
background: var(--border-light);
|
|
}
|
|
|
|
.activity-stat--highlight {
|
|
background: rgba(16, 185, 129, 0.1);
|
|
}
|
|
|
|
.activity-stat-value {
|
|
font-size: 22px;
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
line-height: 1.2;
|
|
}
|
|
|
|
.activity-stat--highlight .activity-stat-value {
|
|
color: #059669;
|
|
}
|
|
|
|
.activity-stat-label {
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.activity-maturity-bar {
|
|
margin: 0 24px 16px;
|
|
}
|
|
|
|
.activity-maturity-label {
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.maturity-bar {
|
|
display: flex;
|
|
height: 8px;
|
|
border-radius: 4px;
|
|
overflow: hidden;
|
|
gap: 2px;
|
|
}
|
|
|
|
.maturity-segment {
|
|
transition: flex 0.3s ease;
|
|
}
|
|
|
|
.maturity-segment--optimized { background: #10B981; }
|
|
.maturity-segment--mature { background: #3B82F6; }
|
|
.maturity-segment--developing { background: #F59E0B; }
|
|
.maturity-segment--early { background: #9CA3AF; }
|
|
|
|
.maturity-legend {
|
|
display: flex;
|
|
gap: 12px;
|
|
margin-top: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.maturity-legend-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
font-size: 10px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.maturity-legend-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.activity-description {
|
|
margin: 0 24px 20px;
|
|
padding: 14px 16px;
|
|
border-left: 3px solid rgba(16, 185, 129, 0.4);
|
|
background: rgba(16, 185, 129, 0.04);
|
|
border-radius: 0 8px 8px 0;
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.activity-description strong {
|
|
color: var(--text-primary);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.activity-footer {
|
|
padding: 16px 24px;
|
|
border-top: 1px solid var(--border-light);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.activity-trend {
|
|
font-size: 12px;
|
|
color: #059669;
|
|
font-weight: 600;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
|
|
.btn-activity {
|
|
font-family: var(--font-primary);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #065F46;
|
|
background: rgba(16, 185, 129, 0.15);
|
|
border: none;
|
|
border-radius: 8px;
|
|
padding: 8px 16px;
|
|
cursor: pointer;
|
|
transition: background 0.15s ease;
|
|
text-decoration: none;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.btn-activity:hover {
|
|
background: rgba(16, 185, 129, 0.25);
|
|
}
|
|
|
|
/* ── Bottom Cards Row ── */
|
|
.bottom-row {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
gap: 24px;
|
|
}
|
|
|
|
/* ── Notifications Card ── */
|
|
.notif-channels {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.notif-channel {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
padding: 16px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 10px;
|
|
background: var(--surface);
|
|
}
|
|
|
|
.notif-channel-icon {
|
|
width: 38px;
|
|
height: 38px;
|
|
border-radius: 8px;
|
|
background: var(--border-light);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.notif-channel-icon.telegram {
|
|
background: rgba(25, 118, 210, 0.1);
|
|
color: #1976D2;
|
|
}
|
|
|
|
.notif-channel-icon.desktop {
|
|
background: rgba(124, 58, 237, 0.1);
|
|
color: #7C3AED;
|
|
}
|
|
|
|
.notif-channel-icon svg {
|
|
color: inherit;
|
|
}
|
|
|
|
.notif-channel-name {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin-bottom: 2px;
|
|
}
|
|
|
|
.notif-status {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.notif-status--active {
|
|
color: var(--success);
|
|
}
|
|
|
|
.notif-status--inactive {
|
|
color: #9CA3AF;
|
|
}
|
|
|
|
.link-manage {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.link-manage:hover {
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.link-manage.active {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.notif-badge {
|
|
margin-left: auto;
|
|
flex-shrink: 0;
|
|
padding: 4px 10px;
|
|
border-radius: 6px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.notif-badge.active {
|
|
background: rgba(16, 183, 127, 0.1);
|
|
color: #059669;
|
|
}
|
|
|
|
.notif-unlink {
|
|
display: none;
|
|
margin-left: auto;
|
|
flex-shrink: 0;
|
|
padding: 4px 10px;
|
|
border-radius: 6px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
background: rgba(239, 68, 68, 0.1);
|
|
color: #EF4444;
|
|
border: 1px solid rgba(239, 68, 68, 0.2);
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.notif-unlink:hover {
|
|
background: rgba(239, 68, 68, 0.2);
|
|
}
|
|
|
|
.notif-unlink.visible,
|
|
.notif-managing .notif-unlink {
|
|
display: inline-block;
|
|
}
|
|
|
|
.notif-managing .notif-badge {
|
|
display: none;
|
|
}
|
|
|
|
.notif-link {
|
|
display: none;
|
|
margin-left: auto;
|
|
flex-shrink: 0;
|
|
padding: 4px 10px;
|
|
border-radius: 6px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
background: rgba(37, 99, 235, 0.1);
|
|
color: #2563EB;
|
|
border: 1px solid rgba(37, 99, 235, 0.2);
|
|
cursor: pointer;
|
|
transition: background 0.15s;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.notif-link:hover {
|
|
background: rgba(37, 99, 235, 0.2);
|
|
}
|
|
|
|
.notif-managing .notif-link {
|
|
display: inline-block;
|
|
}
|
|
|
|
.telegram-verify {
|
|
display: none;
|
|
margin-top: 12px;
|
|
padding: 12px;
|
|
background: var(--background);
|
|
border-radius: 8px;
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.telegram-verify.visible {
|
|
display: block;
|
|
}
|
|
|
|
.telegram-verify ol {
|
|
margin: 8px 0 12px 18px;
|
|
line-height: 1.8;
|
|
}
|
|
|
|
.telegram-verify-row {
|
|
display: flex;
|
|
gap: 8px;
|
|
align-items: center;
|
|
}
|
|
|
|
.telegram-verify-row input {
|
|
width: 120px;
|
|
padding: 6px 10px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
font-family: var(--font-mono);
|
|
font-size: 13px;
|
|
text-align: center;
|
|
}
|
|
|
|
.telegram-verify-row button {
|
|
padding: 6px 14px;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
background: var(--primary);
|
|
color: white;
|
|
border: none;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.telegram-verify-row button:hover {
|
|
opacity: 0.9;
|
|
}
|
|
|
|
.telegram-verify-error {
|
|
color: var(--error);
|
|
font-size: 11px;
|
|
margin-top: 6px;
|
|
display: none;
|
|
}
|
|
|
|
/* ── Account Card ── */
|
|
.account-grid {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
}
|
|
|
|
.account-row {
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
}
|
|
|
|
.account-label {
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.4px;
|
|
width: 90px;
|
|
flex-shrink: 0;
|
|
padding-top: 2px;
|
|
}
|
|
|
|
.account-value {
|
|
font-size: 14px;
|
|
color: var(--text-primary);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.account-value--mono {
|
|
font-family: var(--font-mono);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.badge-role {
|
|
display: inline-block;
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
border-radius: 4px;
|
|
padding: 3px 10px;
|
|
letter-spacing: 0.3px;
|
|
}
|
|
|
|
.badge-role.admin {
|
|
color: #EA580C;
|
|
background: rgba(234, 88, 12, 0.1);
|
|
}
|
|
|
|
.badge-role.privileged {
|
|
color: #7C3AED;
|
|
background: rgba(124, 58, 237, 0.1);
|
|
}
|
|
|
|
.badge-role.analyst {
|
|
color: var(--primary);
|
|
background: var(--primary-light);
|
|
}
|
|
|
|
.badge-role.default {
|
|
color: var(--text-secondary);
|
|
background: var(--border-light);
|
|
}
|
|
|
|
.badge-group {
|
|
display: inline-block;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
font-family: var(--font-mono);
|
|
color: var(--text-secondary);
|
|
background: var(--border-light);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
padding: 2px 8px;
|
|
margin-right: 6px;
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.account-scripts {
|
|
list-style: none;
|
|
padding: 0;
|
|
flex: 1;
|
|
}
|
|
|
|
.account-scripts li {
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
padding: 6px 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.account-scripts li + li {
|
|
border-top: 1px solid var(--border-light);
|
|
}
|
|
|
|
.script-name {
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.script-time {
|
|
font-size: 11px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.cron-line {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
margin-top: 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.cron-line svg {
|
|
width: 12px;
|
|
height: 12px;
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.account-empty {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
font-style: italic;
|
|
}
|
|
|
|
.account-hint {
|
|
font-size: 11px;
|
|
color: var(--text-secondary);
|
|
opacity: 0.7;
|
|
margin-top: 2px;
|
|
}
|
|
|
|
.account-hint code {
|
|
background: var(--background);
|
|
padding: 1px 5px;
|
|
border-radius: 3px;
|
|
font-family: var(--font-mono);
|
|
font-size: 10px;
|
|
}
|
|
|
|
.sync-datasets {
|
|
display: flex;
|
|
gap: 4px;
|
|
margin-top: 6px;
|
|
}
|
|
|
|
.dataset-badge {
|
|
display: inline-block;
|
|
padding: 2px 8px;
|
|
background: rgba(16, 183, 127, 0.1);
|
|
color: var(--success);
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
}
|
|
|
|
/* ── Setup Banner ── */
|
|
/* ── Environment Setup CTA (shown after account creation) ── */
|
|
.env-setup-cta {
|
|
background: linear-gradient(135deg, #0073D1 0%, #0056A3 100%);
|
|
border-radius: 12px;
|
|
padding: 24px 32px;
|
|
margin-bottom: 24px;
|
|
box-shadow: 0 4px 16px rgba(0, 115, 209, 0.2);
|
|
color: white;
|
|
}
|
|
|
|
.env-setup-cta h3 {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
margin: 0 0 4px 0;
|
|
color: white;
|
|
}
|
|
|
|
.env-setup-cta .env-subtitle {
|
|
font-size: 13px;
|
|
color: rgba(255, 255, 255, 0.7);
|
|
margin-bottom: 16px;
|
|
}
|
|
|
|
.env-setup-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.env-setup-row .code-pill {
|
|
background: rgba(255, 255, 255, 0.15);
|
|
padding: 8px 16px;
|
|
border-radius: 6px;
|
|
font-family: var(--font-mono);
|
|
font-size: 14px;
|
|
color: white;
|
|
}
|
|
|
|
.env-setup-row .btn-setup {
|
|
font-family: var(--font-primary);
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--primary);
|
|
background: #FFFFFF;
|
|
border: none;
|
|
border-radius: 6px;
|
|
padding: 8px 20px;
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
white-space: nowrap;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.env-setup-row .btn-setup:hover {
|
|
background: #F0F7FF;
|
|
}
|
|
|
|
.env-setup-row .btn-setup.copied {
|
|
background: var(--success);
|
|
color: white;
|
|
}
|
|
|
|
.env-setup-row .env-hint {
|
|
font-size: 12px;
|
|
color: rgba(255, 255, 255, 0.6);
|
|
margin-left: auto;
|
|
}
|
|
|
|
/* ── Setup Banner (bottom, for returning users) ── */
|
|
.setup-banner {
|
|
background: var(--background);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 16px 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-top: 24px;
|
|
}
|
|
|
|
.setup-banner-text {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.setup-banner-title {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.setup-banner-desc {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.btn-setup-secondary {
|
|
font-family: var(--font-primary);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--primary);
|
|
background: transparent;
|
|
border: 1px solid var(--primary);
|
|
border-radius: 6px;
|
|
padding: 6px 16px;
|
|
cursor: pointer;
|
|
transition: all 0.15s ease;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.btn-setup-secondary:hover {
|
|
background: rgba(0, 115, 209, 0.05);
|
|
}
|
|
|
|
.btn-setup-secondary.copied {
|
|
background: var(--success);
|
|
color: white;
|
|
border-color: var(--success);
|
|
}
|
|
|
|
/* ── Support Banner ── */
|
|
.support-banner {
|
|
text-align: center;
|
|
padding: 16px;
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
margin-top: 24px;
|
|
}
|
|
|
|
.support-banner .heart {
|
|
color: #EF4444;
|
|
}
|
|
|
|
.slack-badge {
|
|
display: inline-block;
|
|
margin-left: 8px;
|
|
padding: 2px 8px;
|
|
background: var(--background);
|
|
border-radius: 4px;
|
|
color: var(--text-primary);
|
|
text-decoration: none;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.slack-badge:hover {
|
|
background: var(--border);
|
|
}
|
|
|
|
/* ── Footer ── */
|
|
.footer {
|
|
text-align: center;
|
|
padding: 32px;
|
|
font-size: 12px;
|
|
color: #9CA3AF;
|
|
}
|
|
|
|
/* ── Flash Messages ── */
|
|
.flash-messages {
|
|
max-width: 1280px;
|
|
margin: 0 auto;
|
|
padding: 16px 32px 0;
|
|
}
|
|
|
|
.flash {
|
|
padding: 12px 16px;
|
|
border-radius: 8px;
|
|
margin-bottom: 12px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
.flash-success {
|
|
background: rgba(16, 183, 127, 0.1);
|
|
color: var(--success);
|
|
border-left: 3px solid var(--success);
|
|
}
|
|
|
|
.flash-error {
|
|
background: rgba(234, 88, 12, 0.1);
|
|
color: var(--error);
|
|
border-left: 3px solid var(--error);
|
|
}
|
|
|
|
/* ── New User Layout ── */
|
|
.new-user-grid {
|
|
max-width: 960px;
|
|
margin: 0 auto;
|
|
}
|
|
|
|
.setup-card {
|
|
padding: 32px 36px;
|
|
}
|
|
|
|
.setup-card h3 {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
color: var(--text-primary);
|
|
margin: 0;
|
|
}
|
|
|
|
.setup-header {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 12px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.setup-header .setup-subtitle {
|
|
color: var(--text-secondary);
|
|
font-size: 13px;
|
|
}
|
|
|
|
/* ── Onboarding sections ── */
|
|
.onboard-section {
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.onboard-section:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
.onboard-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.onboard-label .step-num {
|
|
width: 22px;
|
|
height: 22px;
|
|
background: var(--primary);
|
|
color: white;
|
|
border-radius: 50%;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.onboard-label strong {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.onboard-label .step-hint {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
margin-left: auto;
|
|
}
|
|
|
|
/* ── Terminal block (steps 1-3 combined) ── */
|
|
.terminal-block {
|
|
background: #1e1e2e;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
font-family: var(--font-mono);
|
|
font-size: 13px;
|
|
}
|
|
|
|
.terminal-bar {
|
|
background: #313244;
|
|
padding: 6px 12px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
.terminal-dot {
|
|
width: 10px;
|
|
height: 10px;
|
|
border-radius: 50%;
|
|
}
|
|
|
|
.terminal-dot.r { background: #f38ba8; }
|
|
.terminal-dot.y { background: #f9e2af; }
|
|
.terminal-dot.g { background: #a6e3a1; }
|
|
|
|
.terminal-lines {
|
|
padding: 12px 16px;
|
|
}
|
|
|
|
.terminal-line {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 4px 0;
|
|
color: #cdd6f4;
|
|
line-height: 1.6;
|
|
}
|
|
|
|
.terminal-line .prompt {
|
|
color: #a6e3a1;
|
|
margin-right: 8px;
|
|
user-select: none;
|
|
}
|
|
|
|
.terminal-line .cmd {
|
|
flex: 1;
|
|
}
|
|
|
|
.terminal-line .comment {
|
|
color: #6c7086;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.terminal-line .btn-copy-term {
|
|
padding: 2px 6px;
|
|
background: transparent;
|
|
border: 1px solid #45475a;
|
|
color: #6c7086;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
font-size: 11px;
|
|
font-family: var(--font-mono);
|
|
margin-left: 8px;
|
|
transition: all 0.15s;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.terminal-line .btn-copy-term:hover {
|
|
border-color: #89b4fa;
|
|
color: #89b4fa;
|
|
}
|
|
|
|
.terminal-line .btn-copy-term.copied {
|
|
border-color: #a6e3a1;
|
|
color: #a6e3a1;
|
|
}
|
|
|
|
/* ── Registration inline (step 4) ── */
|
|
.reg-inline {
|
|
background: var(--background);
|
|
border-radius: 8px;
|
|
padding: 16px 20px;
|
|
}
|
|
|
|
.reg-inline .reg-row {
|
|
display: flex;
|
|
gap: 12px;
|
|
align-items: flex-end;
|
|
}
|
|
|
|
.reg-inline .reg-field {
|
|
flex: 1;
|
|
}
|
|
|
|
.reg-inline .reg-field label {
|
|
display: block;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--text-secondary);
|
|
margin-bottom: 4px;
|
|
}
|
|
|
|
.reg-inline .reg-field textarea {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
font-family: var(--font-mono);
|
|
font-size: 12px;
|
|
resize: none;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.reg-inline .reg-field textarea:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 2px rgba(0, 115, 209, 0.1);
|
|
}
|
|
|
|
.reg-inline .reg-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-top: 8px;
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.reg-inline .username-tag {
|
|
font-family: var(--font-mono);
|
|
font-weight: 600;
|
|
color: var(--primary);
|
|
background: rgba(0, 115, 209, 0.08);
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.reg-inline .btn-register {
|
|
padding: 8px 20px;
|
|
background: var(--primary);
|
|
color: white;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
white-space: nowrap;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.reg-inline .btn-register:hover {
|
|
background: #005BA3;
|
|
}
|
|
|
|
/* ── Claude Code section (step 5) ── */
|
|
.claude-section {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.claude-section .code-inline {
|
|
background: var(--background);
|
|
padding: 8px 14px;
|
|
border-radius: 6px;
|
|
font-family: var(--font-mono);
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.btn-copy-v2 {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 8px 18px;
|
|
background: var(--primary);
|
|
color: white;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
border: none;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
|
|
.btn-copy-v2:hover {
|
|
background: #005BA3;
|
|
}
|
|
|
|
.btn-copy-v2.copied {
|
|
background: var(--success);
|
|
}
|
|
|
|
.claude-hint {
|
|
font-size: 12px;
|
|
color: var(--text-secondary);
|
|
margin-left: auto;
|
|
}
|
|
|
|
.helper-text {
|
|
margin-top: 24px;
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* ── Registration Card ── */
|
|
.registration-card h3 {
|
|
font-size: 18px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin: 0 0 16px 0;
|
|
}
|
|
|
|
.info-box-v2 {
|
|
background: var(--background);
|
|
padding: 12px 16px;
|
|
border-radius: 6px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.username-preview {
|
|
font-family: var(--font-mono);
|
|
font-weight: 600;
|
|
color: var(--primary);
|
|
margin-left: 8px;
|
|
}
|
|
|
|
.form-v2 {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
}
|
|
|
|
.form-group-v2 label {
|
|
display: block;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
margin-bottom: 6px;
|
|
}
|
|
|
|
.form-group-v2 textarea {
|
|
width: 100%;
|
|
padding: 12px;
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
font-family: var(--font-mono);
|
|
font-size: 13px;
|
|
resize: vertical;
|
|
}
|
|
|
|
.form-group-v2 textarea:focus {
|
|
outline: none;
|
|
border-color: var(--primary);
|
|
box-shadow: 0 0 0 2px rgba(0, 115, 209, 0.1);
|
|
}
|
|
|
|
.form-row-v2 {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.form-info-v2 {
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
.form-info-v2 code {
|
|
background: var(--background);
|
|
padding: 2px 8px;
|
|
border-radius: 4px;
|
|
font-family: var(--font-mono);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.btn-v2 {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 8px 16px;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
text-decoration: none;
|
|
border: none;
|
|
}
|
|
|
|
.btn-primary-v2 {
|
|
background: var(--primary);
|
|
color: white;
|
|
}
|
|
|
|
.btn-primary-v2:hover {
|
|
background: #005BA3;
|
|
}
|
|
|
|
.help-text-v2 {
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* ── Alert ── */
|
|
.alert-v2 {
|
|
padding: 20px;
|
|
border-radius: 8px;
|
|
display: flex;
|
|
gap: 16px;
|
|
}
|
|
|
|
.alert-error-v2 {
|
|
background: rgba(234, 88, 12, 0.1);
|
|
border-left: 3px solid var(--error);
|
|
}
|
|
|
|
.alert-icon {
|
|
width: 24px;
|
|
height: 24px;
|
|
background: var(--error);
|
|
color: white;
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-weight: 600;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.alert-v2 h4 {
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
margin: 0 0 8px 0;
|
|
}
|
|
|
|
.alert-v2 p {
|
|
font-size: 13px;
|
|
color: var(--text-secondary);
|
|
margin: 4px 0;
|
|
}
|
|
|
|
.beta-badge {
|
|
display: inline-block;
|
|
padding: 2px 7px;
|
|
border-radius: 4px;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
background: #F3F4F6;
|
|
color: #9CA3AF;
|
|
border: 1px solid #E5E7EB;
|
|
vertical-align: middle;
|
|
margin-left: 6px;
|
|
}
|
|
|
|
/* ── Responsive ── */
|
|
@media (max-width: 1100px) {
|
|
.dashboard-grid {
|
|
grid-template-columns: 1fr;
|
|
grid-template-rows: auto;
|
|
}
|
|
|
|
.left-column,
|
|
.right-column {
|
|
grid-row: auto;
|
|
grid-template-rows: auto;
|
|
position: static;
|
|
}
|
|
|
|
.stats-row {
|
|
grid-template-columns: repeat(3, 1fr);
|
|
}
|
|
}
|
|
|
|
@media (max-width: 700px) {
|
|
.header {
|
|
padding: 0 16px;
|
|
}
|
|
|
|
.main {
|
|
padding: 20px 16px 40px;
|
|
}
|
|
|
|
.stats-row {
|
|
grid-template-columns: repeat(2, 1fr);
|
|
}
|
|
|
|
.bottom-row {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
|
|
.setup-banner {
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
text-align: center;
|
|
padding: 28px 24px;
|
|
}
|
|
|
|
.onboard-label {
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.onboard-label .step-hint {
|
|
width: 100%;
|
|
margin-left: 30px;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.claude-section {
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.claude-hint {
|
|
width: 100%;
|
|
margin-left: 0;
|
|
}
|
|
}
|
|
</style>
|
|
{% include '_theme.html' %}
|
|
</head>
|
|
<body>
|
|
|
|
<!-- ═══════════════ HEADER ═══════════════ -->
|
|
<header class="header">
|
|
<div class="header-left">
|
|
<div class="header-logo">
|
|
{{ config.LOGO_SVG | safe }}
|
|
</div>
|
|
<span class="header-subtitle">Data Analyst Portal</span>
|
|
</div>
|
|
{% if session.user %}
|
|
<div class="header-right">
|
|
<span class="header-email">{{ session.user.email }}</span>
|
|
{% if session.user.picture %}
|
|
<img src="{{ session.user.picture }}" alt="Profile" class="avatar-img">
|
|
{% else %}
|
|
<div class="avatar">{{ (user.name or user.email)[:2] | upper }}</div>
|
|
{% endif %}
|
|
<a href="{{ url_for('auth.logout') }}" class="btn-logout">Logout</a>
|
|
</div>
|
|
{% endif %}
|
|
</header>
|
|
|
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
|
{% if messages %}
|
|
<div class="flash-messages">
|
|
{% for category, message in messages %}
|
|
<div class="flash flash-{{ category }}">
|
|
{{ message }}
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
{% endif %}
|
|
{% endwith %}
|
|
|
|
{% if user_info.exists %}
|
|
{# ── EXISTING USER ── #}
|
|
|
|
<main class="main">
|
|
|
|
{% if not account_details or not account_details.last_sync_display %}
|
|
<!-- ═══════════════ ENVIRONMENT SETUP CTA ═══════════════ -->
|
|
<div class="env-setup-cta">
|
|
<h3>Set up your local environment</h3>
|
|
<p class="env-subtitle">Run Claude Code in your project folder and paste the setup instructions to configure SSH, sync data, and initialize DuckDB.</p>
|
|
<div class="env-setup-row">
|
|
<span class="code-pill">cd data-analyst && claude</span>
|
|
<button onclick="copyBootstrapInstructions(this)" class="btn-setup" id="bootstrapCopyBtn">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
</svg>
|
|
Copy Setup Instructions
|
|
</button>
|
|
<span class="env-hint">Paste into Claude Code to complete setup</span>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<!-- ═══════════════ STATS ROW ═══════════════ -->
|
|
<div class="stats-row">
|
|
<div class="stat-card">
|
|
<span class="stat-label">Tables</span>
|
|
<span class="stat-value">{{ data_stats.tables }}</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-label">Columns</span>
|
|
<span class="stat-value">{{ data_stats.columns }}</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-label">Rows</span>
|
|
<span class="stat-value">{{ data_stats.rows_display }}</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-label">Data Size</span>
|
|
<span class="stat-value">{{ data_stats.size_display }}</span>
|
|
</div>
|
|
<div class="stat-card">
|
|
<span class="stat-label">Unstructured</span>
|
|
<span class="stat-value">{{ data_stats.unstructured_display }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Credit line -->
|
|
<div class="credit-line">
|
|
{{ config.INSTANCE_NAME }}
|
|
</div>
|
|
|
|
<!-- ═══════════════ DASHBOARD GRID ═══════════════ -->
|
|
<div class="dashboard-grid">
|
|
|
|
<!-- ── Left Column ── -->
|
|
<div class="left-column">
|
|
|
|
<!-- Your Data Card -->
|
|
<div class="card">
|
|
<div class="card-header">
|
|
<span class="section-title" style="margin-bottom: 0;">Your Data</span>
|
|
</div>
|
|
|
|
<!-- Core Business Data -->
|
|
<div class="data-source">
|
|
<div class="data-source-header">
|
|
<div class="data-source-info">
|
|
<div class="data-source-icon">
|
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
<ellipse cx="12" cy="5" rx="9" ry="3"/>
|
|
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
|
|
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<div class="data-source-name">Core Business Data</div>
|
|
<div class="data-source-status">
|
|
<span class="status-dot"></span>
|
|
{% if data_stats.last_updated %}Synced {{ data_stats.last_updated }}{% else %}Not yet synced{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<span class="badge-included">Always included</span>
|
|
</div>
|
|
<div class="data-source-details">
|
|
{% if catalog_data %}{% for cat in catalog_data %}{{ cat.name }} ({{ cat.count }} tables){% if not loop.last %}, {% endif %}{% endfor %} -- {{ data_stats.tables }} tables total{% else %}Finance, HR, Sales, KBC Telemetry -- {{ data_stats.tables }} tables total{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if metrics_data %}
|
|
{% set metrics_total = namespace(n=0) %}
|
|
{% for c in metrics_data %}{% set metrics_total.n = metrics_total.n + c.metrics|length %}{% endfor %}
|
|
<div class="data-source">
|
|
<div class="data-source-header">
|
|
<div class="data-source-info">
|
|
<div class="data-source-icon">
|
|
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M3 3v18h18"/>
|
|
<path d="M18.7 8l-5.1 5.2-2.8-2.7L7 14.3"/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<div class="data-source-name">Business Metrics</div>
|
|
<div class="data-source-status">
|
|
{{ metrics_total.n }} metrics across {{ metrics_data|length }} categories
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<span class="badge-included">Always included</span>
|
|
</div>
|
|
<div class="data-source-details">
|
|
{% for c in metrics_data %}{{ c.label }} ({{ c.metrics|length }}){% if not loop.last %}, {% endif %}{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="catalog-cta">
|
|
<div class="catalog-cta-text">Manage your data subscriptions or discover new data sources</div>
|
|
<a href="{{ url_for('catalog') }}" class="catalog-cta-link">
|
|
Open Data Catalog
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<polyline points="9 18 15 12 9 6"/>
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bottom Row: Notifications + Account -->
|
|
<div class="bottom-row">
|
|
|
|
<!-- Notifications Card -->
|
|
<div class="card">
|
|
<div class="card-header" style="padding-bottom: 0;">
|
|
<span class="card-title">Notifications</span>
|
|
<a class="link-manage" onclick="toggleNotifManage(this)">Manage</a>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="notif-channels">
|
|
<div class="notif-channel">
|
|
<div class="notif-channel-icon telegram">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M22 2L11 13"/>
|
|
<path d="M22 2L15 22L11 13L2 9L22 2Z"/>
|
|
</svg>
|
|
</div>
|
|
<div>
|
|
<div class="notif-channel-name">Telegram</div>
|
|
{% if not telegram_status.linked %}
|
|
<span class="notif-status notif-status--inactive">Not linked</span>
|
|
{% endif %}
|
|
</div>
|
|
{% if telegram_status.linked %}
|
|
<span class="notif-badge active">Active</span>
|
|
<button class="notif-unlink" onclick="unlinkChannel('telegram')">Unlink</button>
|
|
{% else %}
|
|
<button class="notif-link" onclick="showTelegramVerify()">Link</button>
|
|
{% endif %}
|
|
</div>
|
|
<div class="notif-channel">
|
|
<div class="notif-channel-icon desktop">
|
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
|
|
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/>
|
|
<line x1="8" y1="21" x2="16" y2="21"/>
|
|
<line x1="12" y1="17" x2="12" y2="21"/>
|
|
</svg>
|
|
</div>
|
|
<div style="min-width: 0;">
|
|
<div class="notif-channel-name">macOS App</div>
|
|
<span class="beta-badge" style="margin-left: 0;">private beta</span>
|
|
{% if not desktop_status.linked %}
|
|
<span class="notif-status notif-status--inactive">Not linked</span>
|
|
{% endif %}
|
|
</div>
|
|
{% if desktop_status.linked %}
|
|
<span class="notif-badge active">Active</span>
|
|
<button class="notif-unlink" onclick="unlinkChannel('desktop')">Unlink</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div id="telegramVerify" class="telegram-verify">
|
|
<ol>
|
|
<li>Message <code>/start</code> to <strong>@{{ config.TELEGRAM_BOT_USERNAME or 'your-bot' }}</strong> on Telegram</li>
|
|
<li>Enter the verification code below</li>
|
|
</ol>
|
|
<div class="telegram-verify-row">
|
|
<input type="text" id="verifyCode" placeholder="6-digit code" maxlength="6">
|
|
<button onclick="verifyTelegram()" id="verifyBtn">Verify</button>
|
|
</div>
|
|
<div id="telegramError" class="telegram-verify-error"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Account Card -->
|
|
<div class="card">
|
|
<div class="card-header" style="padding-bottom: 0;">
|
|
<span class="card-title">Account</span>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="account-grid">
|
|
<div class="account-row">
|
|
<span class="account-label">Username</span>
|
|
<span class="account-value account-value--mono">{{ username }}</span>
|
|
</div>
|
|
<div class="account-row">
|
|
<span class="account-label">Role</span>
|
|
{% if user_info.is_admin %}
|
|
<span class="badge-role admin">Administrator</span>
|
|
{% elif user_info.is_privileged %}
|
|
<span class="badge-role privileged">Privileged Analyst</span>
|
|
{% elif user_info.is_analyst %}
|
|
<span class="badge-role analyst">Standard Analyst</span>
|
|
{% else %}
|
|
<span class="badge-role default">User</span>
|
|
{% endif %}
|
|
</div>
|
|
<div class="account-row">
|
|
<span class="account-label">Groups</span>
|
|
<div>
|
|
{% for group in user_info.groups %}
|
|
<span class="badge-group">{{ group }}</span>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
|
|
{% if account_details %}
|
|
<div class="account-row">
|
|
<span class="account-label">Scripts</span>
|
|
<div style="flex: 1;">
|
|
{% if account_details.notification_scripts %}
|
|
<ul class="account-scripts">
|
|
{% for script in account_details.notification_scripts %}
|
|
<li>
|
|
<span class="script-name">{{ script.stem }}</span>
|
|
<span class="script-time">{{ script.last_run or 'never' }}</span>
|
|
</li>
|
|
{% endfor %}
|
|
</ul>
|
|
{% if account_details.cron_schedule %}
|
|
<div class="cron-line">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
|
{{ account_details.cron_schedule }}
|
|
</div>
|
|
{% endif %}
|
|
{% else %}
|
|
<div class="account-empty">No scripts configured</div>
|
|
<div class="account-hint">Add <code>.py</code> scripts to <code>~/user/notifications/</code></div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
<div class="account-row">
|
|
<span class="account-label">Last Sync</span>
|
|
<div>
|
|
{% if account_details.last_sync_display %}
|
|
<span class="account-value" style="font-size: 13px; color: var(--text-secondary);">{{ account_details.last_sync_display }}</span>
|
|
{% else %}
|
|
<div class="account-empty">Not yet synced</div>
|
|
<div class="account-hint">Run <code>bash server/scripts/sync_data.sh</code></div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /bottom-row -->
|
|
</div><!-- /left-column -->
|
|
|
|
<!-- ── Right Column ── -->
|
|
<div class="right-column">
|
|
|
|
<!-- Corporate Memory Widget -->
|
|
<div class="card memory-card">
|
|
<div class="card-header">
|
|
<div class="card-header-left">
|
|
<div class="memory-icon">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<path d="M12 2L2 7l10 5 10-5-10-5z"/>
|
|
<path d="M2 17l10 5 10-5"/>
|
|
<path d="M2 12l10 5 10-5"/>
|
|
</svg>
|
|
</div>
|
|
<span class="card-title">Corporate Memory</span>
|
|
</div>
|
|
<span class="badge-beta">private beta</span>
|
|
</div>
|
|
|
|
<div class="memory-stats">
|
|
<div class="memory-stat">
|
|
<div class="memory-stat-value" id="memoryContributors">0</div>
|
|
<div class="memory-stat-label">Contributors</div>
|
|
</div>
|
|
<div class="memory-stat">
|
|
<div class="memory-stat-value" id="memoryKnowledgeCount">0</div>
|
|
<div class="memory-stat-label">Knowledge Items</div>
|
|
</div>
|
|
<div class="memory-stat memory-stat--highlight">
|
|
<div class="memory-stat-value" id="memoryYourRules">0</div>
|
|
<div class="memory-stat-label">Your Rules</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="memory-description">
|
|
<strong>Shared knowledge</strong> from your team's Claude Code sessions.
|
|
Upvote useful insights and they'll sync to your local <code>.claude/rules/</code>.
|
|
</div>
|
|
|
|
<div class="memory-footer">
|
|
<div class="memory-sync">
|
|
<span class="status-dot"></span>
|
|
<span id="memorySyncStatus">Synced</span>
|
|
</div>
|
|
<a href="{{ url_for('corporate_memory') }}" class="btn-memory">
|
|
Browse Knowledge
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M9 18l6-6-6-6"/>
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Activity Center Widget -->
|
|
<div class="card activity-card">
|
|
<div class="card-header">
|
|
<div class="card-header-left">
|
|
<div class="activity-icon">
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#FFFFFF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
<line x1="18" y1="20" x2="18" y2="10"/>
|
|
<line x1="12" y1="20" x2="12" y2="4"/>
|
|
<line x1="6" y1="20" x2="6" y2="14"/>
|
|
</svg>
|
|
</div>
|
|
<span class="card-title">Activity Center</span>
|
|
</div>
|
|
<span class="badge-demo">demo</span>
|
|
</div>
|
|
|
|
<div class="activity-stats">
|
|
<div class="activity-stat">
|
|
<div class="activity-stat-value">{{ activity_summary.get('teams_active', 0) }}/{{ activity_summary.get('teams_total', 0) }}</div>
|
|
<div class="activity-stat-label">Teams Active</div>
|
|
</div>
|
|
<div class="activity-stat">
|
|
<div class="activity-stat-value">{{ activity_summary.get('business_processes_identified', 0) }}</div>
|
|
<div class="activity-stat-label">Processes</div>
|
|
</div>
|
|
<div class="activity-stat activity-stat--highlight">
|
|
<div class="activity-stat-value">{{ activity_summary.get('avg_success_rate', 0) }}%</div>
|
|
<div class="activity-stat-label">Success Rate</div>
|
|
</div>
|
|
</div>
|
|
|
|
{% set maturity = activity_summary.get('maturity_distribution', {}) %}
|
|
{% set maturity_total = maturity.get('optimized', 0) + maturity.get('mature', 0) + maturity.get('developing', 0) + maturity.get('early', 0) %}
|
|
<div class="activity-maturity-bar">
|
|
<div class="activity-maturity-label">Process Maturity Distribution</div>
|
|
{% if maturity_total > 0 %}
|
|
<div class="maturity-bar">
|
|
<div class="maturity-segment maturity-segment--optimized" style="flex: {{ maturity.get('optimized', 0) }}" title="Optimized: {{ maturity.get('optimized', 0) }}"></div>
|
|
<div class="maturity-segment maturity-segment--mature" style="flex: {{ maturity.get('mature', 0) }}" title="Mature: {{ maturity.get('mature', 0) }}"></div>
|
|
<div class="maturity-segment maturity-segment--developing" style="flex: {{ maturity.get('developing', 0) }}" title="Developing: {{ maturity.get('developing', 0) }}"></div>
|
|
<div class="maturity-segment maturity-segment--early" style="flex: {{ maturity.get('early', 0) }}" title="Early: {{ maturity.get('early', 0) }}"></div>
|
|
</div>
|
|
<div class="maturity-legend">
|
|
<span class="maturity-legend-item"><span class="maturity-legend-dot" style="background: #10B981"></span> Optimized ({{ maturity.get('optimized', 0) }})</span>
|
|
<span class="maturity-legend-item"><span class="maturity-legend-dot" style="background: #3B82F6"></span> Mature ({{ maturity.get('mature', 0) }})</span>
|
|
<span class="maturity-legend-item"><span class="maturity-legend-dot" style="background: #F59E0B"></span> Developing ({{ maturity.get('developing', 0) }})</span>
|
|
<span class="maturity-legend-item"><span class="maturity-legend-dot" style="background: #9CA3AF"></span> Early ({{ maturity.get('early', 0) }})</span>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="activity-description">
|
|
<strong>Strategic overview</strong> of how data powers business processes across {{ activity_summary.get('teams_total', 0) }} teams.
|
|
</div>
|
|
|
|
<div class="activity-footer">
|
|
<span class="activity-trend">
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/>
|
|
<polyline points="17 6 23 6 23 12"/>
|
|
</svg>
|
|
{{ activity_summary.get('adoption_trend', '') }} adoption
|
|
</span>
|
|
<a href="{{ url_for('activity_center') }}" class="btn-activity">
|
|
View Activity Center
|
|
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<path d="M9 18l6-6-6-6"/>
|
|
</svg>
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
</div><!-- /right-column -->
|
|
</div><!-- /dashboard-grid -->
|
|
|
|
<!-- ═══════════════ SETUP BANNER ═══════════════ -->
|
|
<div class="setup-banner">
|
|
<div class="setup-banner-text">
|
|
<div class="setup-banner-title">Set up a new machine</div>
|
|
<div class="setup-banner-desc">Copy instructions and paste into Claude Code to configure another local environment.</div>
|
|
</div>
|
|
<button onclick="copyBootstrapInstructions(this)" class="btn-setup-secondary" id="bootstrapCopyBtnBottom">
|
|
Copy Setup Instructions
|
|
</button>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
{% else %}
|
|
{# ── NEW USER ── #}
|
|
|
|
<main class="main">
|
|
<div class="new-user-grid">
|
|
{% if not username_available %}
|
|
<div class="alert-v2 alert-error-v2" style="margin-bottom: 16px;">
|
|
<span class="alert-icon">!</span>
|
|
<div>
|
|
<h4>Username Not Available</h4>
|
|
<p>{{ username_error }}</p>
|
|
<p>Your email generates username <code>{{ username }}</code>, which cannot be used. Contact an administrator.</p>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
<div class="card setup-card">
|
|
<div class="setup-header">
|
|
<h3>Get Started</h3>
|
|
<span class="setup-subtitle">Set up your workspace in 4 steps</span>
|
|
</div>
|
|
|
|
{# ── Steps 1-3: Terminal commands ── #}
|
|
<div class="onboard-section">
|
|
<div class="onboard-label">
|
|
<span class="step-num">1</span>
|
|
<strong>Create folder</strong>
|
|
<span class="step-num" style="margin-left: 12px;">2</span>
|
|
<strong>Generate SSH key</strong>
|
|
<span class="step-num" style="margin-left: 12px;">3</span>
|
|
<strong>Copy public key</strong>
|
|
<span class="step-hint">Run these in your terminal</span>
|
|
</div>
|
|
<div class="terminal-block">
|
|
<div class="terminal-bar">
|
|
<span class="terminal-dot r"></span>
|
|
<span class="terminal-dot y"></span>
|
|
<span class="terminal-dot g"></span>
|
|
</div>
|
|
<div class="terminal-lines">
|
|
<div class="terminal-line">
|
|
<span class="prompt">$</span>
|
|
<span class="cmd">mkdir -p data-analyst && cd data-analyst</span>
|
|
<button onclick="copyTermLine(this, 'mkdir -p data-analyst && cd data-analyst')" class="btn-copy-term">copy</button>
|
|
</div>
|
|
<div class="terminal-line">
|
|
<span class="prompt">$</span>
|
|
<span class="cmd">ssh-keygen -t ed25519 -f ~/.ssh/data_analyst_server -N ''</span>
|
|
<button onclick="copyTermLine(this, "ssh-keygen -t ed25519 -f ~/.ssh/data_analyst_server -N ''")" class="btn-copy-term">copy</button>
|
|
</div>
|
|
<div class="terminal-line">
|
|
<span class="prompt">$</span>
|
|
<span class="cmd">cat ~/.ssh/data_analyst_server.pub</span>
|
|
<button onclick="copyTermLine(this, 'cat ~/.ssh/data_analyst_server.pub')" class="btn-copy-term">copy</button>
|
|
</div>
|
|
<div class="terminal-line">
|
|
<span class="comment"># Copy the output above and paste it below</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{# ── Step 4: Register ── #}
|
|
{% if username_available %}
|
|
<div class="onboard-section">
|
|
<div class="onboard-label">
|
|
<span class="step-num">4</span>
|
|
<strong>Create your account</strong>
|
|
<span class="step-hint">
|
|
Username: <span class="username-tag" style="font-family: var(--font-mono); font-weight: 600; color: var(--primary);">{{ username }}</span>
|
|
</span>
|
|
</div>
|
|
<div class="reg-inline">
|
|
<form action="{{ url_for('register') }}" method="post">
|
|
<div class="reg-row">
|
|
<div class="reg-field">
|
|
<label for="ssh_key">Paste your SSH public key</label>
|
|
<textarea name="ssh_key" id="ssh_key" rows="1"
|
|
placeholder="ssh-ed25519 AAAA... or ssh-rsa AAAA..." required></textarea>
|
|
</div>
|
|
<button type="submit" class="btn-register">Create Account</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{# Step 5 (Claude Code setup) appears on dashboard after account creation #}
|
|
</div>
|
|
</div>
|
|
|
|
<div class="support-banner" style="margin-top: 16px;">
|
|
{{ config.INSTANCE_NAME }} - need help? Contact your platform team.
|
|
</div>
|
|
</main>
|
|
{% endif %}
|
|
|
|
<footer class="footer">
|
|
<p>© {{ now().year if now is defined else 2024 }} {{ config.INSTANCE_COPYRIGHT or 'AI Data Analyst' }}</p>
|
|
</footer>
|
|
|
|
<script>
|
|
function copyToClipboard(text) {
|
|
if (navigator.clipboard && window.isSecureContext) {
|
|
return navigator.clipboard.writeText(text);
|
|
}
|
|
var ta = document.createElement('textarea');
|
|
ta.value = text;
|
|
ta.style.position = 'fixed';
|
|
ta.style.left = '-9999px';
|
|
ta.style.top = '-9999px';
|
|
document.body.appendChild(ta);
|
|
ta.focus();
|
|
ta.select();
|
|
return new Promise(function(resolve, reject) {
|
|
document.execCommand('copy') ? resolve() : reject();
|
|
document.body.removeChild(ta);
|
|
});
|
|
}
|
|
|
|
function copyCode(button, text) {
|
|
var originalHTML = button.innerHTML;
|
|
copyToClipboard(text).then(function() {
|
|
button.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="20 6 9 17 4 12"></polyline></svg>';
|
|
button.classList.add('copied');
|
|
setTimeout(function() {
|
|
button.innerHTML = originalHTML;
|
|
button.classList.remove('copied');
|
|
}, 2000);
|
|
});
|
|
}
|
|
|
|
function copyTermLine(button, text) {
|
|
copyToClipboard(text).then(function() {
|
|
button.textContent = 'done';
|
|
button.classList.add('copied');
|
|
setTimeout(function() {
|
|
button.textContent = 'copy';
|
|
button.classList.remove('copied');
|
|
}, 2000);
|
|
});
|
|
}
|
|
|
|
function copyBootstrapInstructions(btn) {
|
|
var username = {{ username | tojson }};
|
|
var serverHost = {{ server_host | tojson }};
|
|
var serverHostname = {{ server_hostname | tojson }};
|
|
var webappUrl = serverHostname ? 'http://' + serverHostname : '';
|
|
|
|
var sshHost = 'data-analyst';
|
|
var instructions = 'Set up my AI Data Analyst local environment.\n\n'
|
|
+ 'Connection details:\n'
|
|
+ ' Server IP: ' + serverHost + '\n'
|
|
+ ' Webapp: ' + webappUrl + '\n'
|
|
+ ' My username: ' + username + '\n'
|
|
+ ' SSH key: ~/.ssh/data_analyst_server (already generated)\n\n'
|
|
+ 'Steps:\n\n'
|
|
+ '1. SSH config\n'
|
|
+ ' Check ~/.ssh/config - if a Host entry named "' + sshHost + '" already exists\n'
|
|
+ ' with a DIFFERENT server, ask me what name to use instead.\n'
|
|
+ ' Otherwise add:\n'
|
|
+ ' Host ' + sshHost + '\n'
|
|
+ ' HostName ' + serverHost + '\n'
|
|
+ ' User ' + username + '\n'
|
|
+ ' IdentityFile ~/.ssh/data_analyst_server\n'
|
|
+ ' StrictHostKeyChecking accept-new\n'
|
|
+ ' Then test: ssh ' + sshHost + ' echo ok\n\n'
|
|
+ '2. Create project folders (use explicit mkdir, not brace expansion):\n'
|
|
+ ' mkdir -p server/docs server/scripts server/parquet server/metadata server/examples\n'
|
|
+ ' mkdir -p user/duckdb user/notifications user/artifacts user/scripts user/parquet user/sessions\n\n'
|
|
+ '3. Download from server via rsync (use --no-perms --no-group to avoid macOS permission errors).\n'
|
|
+ ' Skip directories that don\'t exist on the server (rsync exit code 23 = missing source).\n'
|
|
+ ' rsync -avz --no-perms --no-group ' + sshHost + ':server/scripts/ ./server/scripts/\n'
|
|
+ ' rsync -avz --no-perms --no-group ' + sshHost + ':server/docs/ ./server/docs/\n'
|
|
+ ' rsync -avz --no-perms --no-group ' + sshHost + ':server/examples/ ./server/examples/\n'
|
|
+ ' rsync -avz --no-perms --no-group ' + sshHost + ':server/metadata/ ./server/metadata/\n'
|
|
+ ' rsync -avz --no-perms --no-group --progress ' + sshHost + ':server/parquet/ ./server/parquet/\n'
|
|
+ ' Note: some folders may be empty if data sync hasn\'t run on the server yet. That\'s OK.\n\n'
|
|
+ '4. Set up Python venv:\n'
|
|
+ ' python3 -m venv .venv\n'
|
|
+ ' source .venv/bin/activate\n'
|
|
+ ' pip install pandas pyarrow duckdb pyyaml python-dotenv\n\n'
|
|
+ '5. Initialize DuckDB (only if server/scripts/setup_views.sh exists):\n'
|
|
+ ' bash server/scripts/setup_views.sh\n\n'
|
|
+ '6. Create CLAUDE.md (only if server/docs/setup/claude_md_template.txt exists):\n'
|
|
+ ' Copy the template, replace {username} with ' + username + '\n';
|
|
|
|
var button = btn || document.getElementById('bootstrapCopyBtn');
|
|
var origText = button.textContent;
|
|
copyToClipboard(instructions).then(function() {
|
|
button.textContent = 'Copied!';
|
|
button.classList.add('copied');
|
|
setTimeout(function() {
|
|
button.textContent = origText;
|
|
button.classList.remove('copied');
|
|
}, 2000);
|
|
});
|
|
}
|
|
|
|
async function updateSyncSettings() {
|
|
const jiraToggle = document.getElementById('toggle-jira');
|
|
const attachmentsToggle = document.getElementById('toggle-jira_attachments');
|
|
|
|
// Handle dependency: jira_attachments requires jira
|
|
if (!jiraToggle.checked) {
|
|
attachmentsToggle.checked = false;
|
|
attachmentsToggle.disabled = true;
|
|
} else {
|
|
attachmentsToggle.disabled = false;
|
|
}
|
|
|
|
const datasets = {
|
|
jira: jiraToggle.checked,
|
|
jira_attachments: attachmentsToggle.checked
|
|
};
|
|
|
|
try {
|
|
const resp = await fetch('/api/sync-settings', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({datasets: datasets})
|
|
});
|
|
if (resp.ok) {
|
|
// Reload to reflect real-time section change
|
|
location.reload();
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to update settings:', e);
|
|
}
|
|
}
|
|
|
|
// Load Corporate Memory stats
|
|
async function loadMemoryStats() {
|
|
try {
|
|
const resp = await fetch('/api/corporate-memory/stats');
|
|
if (resp.ok) {
|
|
const data = await resp.json();
|
|
document.getElementById('memoryKnowledgeCount').textContent = data.knowledge_count || 0;
|
|
document.getElementById('memoryContributors').textContent = data.contributors || 0;
|
|
document.getElementById('memoryYourRules').textContent = data.your_rules || 0;
|
|
// Show relative collection time
|
|
const el = document.getElementById('memorySyncStatus');
|
|
if (data.last_collection) {
|
|
const ago = Date.now() - new Date(data.last_collection).getTime();
|
|
const mins = Math.floor(ago / 60000);
|
|
if (mins < 1) el.textContent = 'Compiled just now';
|
|
else if (mins < 60) el.textContent = 'Compiled ' + mins + ' min ago';
|
|
else if (mins < 1440) el.textContent = 'Compiled ' + Math.floor(mins / 60) + 'h ago';
|
|
else el.textContent = 'Compiled ' + Math.floor(mins / 1440) + 'd ago';
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Silently fail - widget will show defaults
|
|
console.log('Could not load memory stats:', e);
|
|
}
|
|
}
|
|
|
|
// Load memory stats on page load (only for existing users)
|
|
{% if user_info.exists %}
|
|
document.addEventListener('DOMContentLoaded', loadMemoryStats);
|
|
{% endif %}
|
|
|
|
// Notification management
|
|
function toggleNotifManage(link) {
|
|
const channels = link.closest('.card').querySelector('.notif-channels');
|
|
const managing = channels.classList.toggle('notif-managing');
|
|
link.textContent = managing ? 'Done' : 'Manage';
|
|
link.classList.toggle('active', managing);
|
|
// Hide verify panel when exiting manage mode
|
|
if (!managing) {
|
|
document.getElementById('telegramVerify').classList.remove('visible');
|
|
}
|
|
}
|
|
|
|
function showTelegramVerify() {
|
|
document.getElementById('telegramVerify').classList.toggle('visible');
|
|
}
|
|
|
|
async function verifyTelegram() {
|
|
const code = document.getElementById('verifyCode').value.trim();
|
|
const errorEl = document.getElementById('telegramError');
|
|
const btn = document.getElementById('verifyBtn');
|
|
errorEl.style.display = 'none';
|
|
|
|
if (!code || code.length !== 6) {
|
|
errorEl.textContent = 'Please enter a 6-digit code.';
|
|
errorEl.style.display = 'block';
|
|
return;
|
|
}
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = 'Verifying...';
|
|
|
|
try {
|
|
const resp = await fetch('/api/telegram/verify', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({code: code})
|
|
});
|
|
const data = await resp.json();
|
|
|
|
if (resp.ok) {
|
|
location.reload();
|
|
} else {
|
|
errorEl.textContent = data.error || 'Verification failed.';
|
|
errorEl.style.display = 'block';
|
|
}
|
|
} catch (e) {
|
|
errorEl.textContent = 'Network error. Please try again.';
|
|
errorEl.style.display = 'block';
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.textContent = 'Verify';
|
|
}
|
|
|
|
async function unlinkChannel(channel) {
|
|
const label = channel === 'telegram' ? 'Telegram' : 'macOS App';
|
|
if (!confirm(`Unlink ${label}? You will stop receiving notifications.`)) return;
|
|
try {
|
|
const resp = await fetch(`/api/${channel}/unlink`, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'}
|
|
});
|
|
if (resp.ok) {
|
|
location.reload();
|
|
} else {
|
|
const data = await resp.json();
|
|
alert(data.error || 'Failed to unlink.');
|
|
}
|
|
} catch (e) {
|
|
console.error('Unlink failed:', e);
|
|
alert('Failed to unlink. Please try again.');
|
|
}
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|