agnes-the-ai-analyst/webapp/templates/dashboard.html
Petr 508d92771f Generate setup instructions from bootstrap.yaml (single source of truth)
- Rewrite bootstrap.yaml as clean structured YAML with steps, commands,
  descriptions, conditions, and notes
- Add _generate_setup_instructions() in app.py that reads YAML, substitutes
  placeholders, and produces clipboard-ready plain text
- Replace 50-line hardcoded JS string builder with single tojson variable
- All setup instructions now editable in one YAML file
2026-03-15 00:37:19 +01:00

2578 lines
84 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 {{ project_dir }} && 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 {{ project_dir }} && cd {{ project_dir }}</span>
<button onclick="copyTermLine(this, 'mkdir -p {{ project_dir }} && cd {{ project_dir }}')" class="btn-copy-term">copy</button>
</div>
<div class="terminal-line">
<span class="prompt">$</span>
<span class="cmd">ssh-keygen -t ed25519 -f {{ ssh_key }} -N ''</span>
<button onclick="copyTermLine(this, &quot;ssh-keygen -t ed25519 -f {{ ssh_key }} -N ''&quot;)" class="btn-copy-term">copy</button>
</div>
<div class="terminal-line">
<span class="prompt">$</span>
<span class="cmd">cat {{ ssh_key }}.pub</span>
<button onclick="copyTermLine(this, 'cat {{ ssh_key }}.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>&copy; {{ 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 instructions = {{ setup_instructions | tojson }};
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>