* feat(home+news): state-aware /home + /news + admin-edited news section
Squash of the vr/home-page feature work for clean rebase onto main.
Original 18-commit history preserved in branch backup/vr-home-page-pre-rebase.
What's in this PR:
**State-aware /home page**
- New `/home` route with hero + auto-mode + connectors (Asana / GWS /
Atlassian) + lookarounds. Onboarded vs not-onboarded state-machine
branches a single template (`home_not_onboarded.html`); the install
steps, "Setup a new Claude Code" CTA (90-day PAT mint), and per-
connector setup prompts hide once `users.onboarded=TRUE`. A
completion badge replaces them.
- "Mark me as offboarded" button reverses the flag without an SQL UPDATE.
- `users.onboarded BOOLEAN` column added; default FALSE; flipped by the
CLI's `agnes init` post-success POST and the `/admin/users` API.
- Connector setup prompts pre-check whether the tool is already
installed/connected before re-running setup.
- GWS scope set widened to include Google Chat (`chat.spaces`,
`chat.messages`).
**Single template + design tokens**
- `dashboard.html` now extends `base.html` via the new
`{% block layout %}` opt-out (full-width pages skip the 800px
`.container`). Net: every page shares one shell.
- `style-custom.css` `:root` extended with `--space-{7,9,10,12}`,
`--radius-2xl`, `--shadow-{card,elevated}`, `--text-{muted,disabled}`,
`--focus-ring`, `--transition-*`, `--width-{narrow,app,wide}` so
inline page styles can migrate incrementally.
**Auth redirects honor AGNES_HOME_ROUTE**
- `safe_next_path` resolves the configured home route when no `default=`
is passed; OAuth callbacks, magic-link clicks, password form, and
LOCAL_DEV_MODE shortcuts now land on `/home` (or whatever the operator
picked) instead of always /dashboard.
**News section + /news permalink + /admin/news editor**
- Schema-bumped `news_template` table (single versioned entity, draft +
publish gate). `published BOOLEAN` distinguishes draft from public;
monotonically-increasing `version` per save; rows >30d pruned on
save except the currently-displayed published version.
- `/home` bottom-of-page renders the latest published intro with a
"Read more →" link to `/news` (which renders the full body).
- `/admin/news` editor with sandboxed live preview, versions table,
per-row Unpublish, Format-help cheatsheet.
- `agnes admin news show / draft / edit / publish / unpublish /
versions / export` (CLI). Talks to the live server via the
`/api/admin/news/*` endpoints (PAT-authed) — no direct DB access
so it coexists with a running uvicorn.
- **Optimistic-lock guard**: `agnes admin news publish --version N` and
PUT/PATCH endpoints accept `expected_version` and 409 with structured
`{error: "version_conflict", expected, actual, actual_by}` when a
concurrent admin replaced the draft. Edit refuses to overwrite a
draft authored by someone else without `--force` or
`--expect-version`.
- nh3 (Rust-backed ammonia) HTML sanitizer; iframe pre-pass strips
any iframe whose src is not on the YouTube/Vimeo/Loom allowlist;
javascript:/data: schemes blocked everywhere.
- Author CSS vocabulary: `.news-hero` (blue gradient hero block),
`.callout`/`.callout-{info,warn,success,danger}`,
`.video-embed`, `.news-section`, `.news-grid-{2,3}`, `.news-cta` —
all consolidated in `style-custom.css` under "News content
vocabulary (shared)" so /home perex, /news body, and /admin/news
preview share one source of styling.
- Code-inside-`<pre>` contrast fix (was unreadable amber-on-silver).
- `.news-content` table styling (border, header band, row-hover).
**`scripts/dev/run-local.sh`** — local uvicorn launcher. Pulls Google
OAuth client id/secret from GCP Secret Manager
(`AGNES_OAUTH_GCP_PROJECT`-driven, no vendor defaults), points
`AGNES_CLI_DIST_DIR` at `./dist` so the wheel endpoint resolves, and
`--dev` flips `LOCAL_DEV_MODE=1` + `AGNES_HOME_ROUTE=/home` for one-
command iteration. `LOCAL_DEV_MODE=1` also enables the FastAPI debug
toolbar.
**CLAUDE.md "Run tests before every push" section** codifies
`pytest tests/ -n auto -q` as non-negotiable before each push.
**Tests**: 51 + 14 + 8 = 73 new tests across news-template repo,
sanitizer, API, web, CLI; plus updated home/auth/template tests for
the new shared-shell architecture.
Origin docs (gitignored, customer-fork content):
docs/brainstorms/home-page-requirements.md,
docs/plans/2026-05-07-001-feat-home-page-plan.md.
* feat(cli): agnes onboarded {on,off,status} — self-scoped flag toggle
User-facing equivalent of the in-page "Mark me as (off)boarded" button
on /home. POSTs /api/me/onboarded with {onboarded, source}; --source
overrides the audit-log marker so flips made from the CLI vs the web
button vs agnes init automation stay distinguishable.
`status` reads via /api/me/profile (when present); falls back to a
quick body-marker scan of /home so the read path doesn't write an
audit_log row. PAT-authed via cli.client.api_post — same convention
as agnes admin news / agnes admin add-user etc.
Tests: 5 covering on/off/status round-trip, idempotency, and
audit-log source recording. Full suite holds at 12 pre-existing
failures (same set as before).
* ui(nav+home): primary nav reorg + green What's new band + /marketplace link fix
Primary nav (post-rebase audit + per-user feedback):
- Items: Home → Marketplace → Data Packages → Memory. Admin dropdown
for admins only. The "Dashboard" label was renamed Home — point still
resolves through `home_route` so customer instances on /dashboard
still land there.
- Activity Center moved into the Admin dropdown. Per-team adoption
analytics is admin-consumed in practice; the route still allows
any authed user for direct deep-links so existing /home tile +
bookmarks keep working.
- Memory link added (→ /corporate-memory) — was previously buried in
the /home "Look around" tiles.
- Setup local agent + My Stack dropped from main nav. Setup is the
/home install flow's home now; My Stack lives as a tab inside
/marketplace.
/home tweaks:
- Plugin marketplace tile now points at /marketplace (was /store —
legacy from before the marketplace rebrand landed in #230).
- "What's new" section header gets a green band (success-flavored
D1FAE5 background, A7F3D0 border, darker green title) so the
bottom-of-page news block visibly distinguishes from the blue
install-hero at the top. Header strip only — body stays white.
Test fix: test_home_route_resolution renamed `dashboard_link_uses_home_route`
→ `home_link_uses_home_route` and asserts `href="/home">Home` instead
of `href="/home">Dashboard` after the label change.
* fix(home): decouple Step 3 + Connect-tools collapse from server onboarded flag
The server-side `users.onboarded` flip happens through two paths:
1. Explicit user click on "Mark me as onboarded" or `agnes onboarded on`.
2. Implicit `agnes init` POST → /api/me/onboarded on success.
Path 2 produced a UX surprise: an analyst running `agnes init` mid-flow
reloaded /home and saw Step 3 (auto-mode) + Connect-your-tools auto-
collapse to summary bars. They were actively working through those
sections — the install POST never signalled "I'm done with the rest
of setup", just "Agnes itself is installed".
Decouple the section-collapse decision from the server flag:
- Step 1 + Step 2 install blocks: still hidden on `onboarded=TRUE`
(their completion is a hard server signal — Agnes IS installed).
- Step 3 + Connect-your-tools: render flat by default in BOTH states.
Wrapped in `<details class="setup-collapsible" open>` so the
browser's native disclosure handles per-section toggle without JS,
but the `<summary>` is CSS-hidden until the page-level
`data-setup-minimized="1"` attribute is set on `.home-mock`.
- New "Minimize setup view" toggle inside the blue install-hero,
rendered only when onboarded. Click flips the data-attr on
`.home-mock` AND removes the `open` attribute from each
`<details>`. State persists in `localStorage["agnes_home_setup_minimized"]`
so the choice survives reloads but is per-device.
- "Show full setup view" (the same button when minimized) re-opens
both `<details>` and clears localStorage.
When minimized, each `<details>` still has its own native expand/
collapse — click the gray summary bar to peek at one section without
toggling the page-level minimize off.
Tests:
- test_step3_and_connectors_render_flat_when_onboarded_by_default —
asserts `<details class="setup-collapsible" ... open>` for both
sections post-onboarding and the absence of any server-rendered
`data-setup-minimized` attribute on the `.home-mock` root.
- test_minimize_toggle_visible_only_when_onboarded — toggle button
rendered only when onboarded.
Full pytest holds at 12 pre-existing failures (same set).
2526 lines
82 KiB
HTML
2526 lines
82 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Dashboard - {{ config.INSTANCE_NAME or 'Data Analyst Portal' }}{% endblock %}
|
|
|
|
{% block head_extra %}
|
|
{% 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 %}
|
|
<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 {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
text-decoration: none;
|
|
color: inherit;
|
|
}
|
|
.header-logo svg {
|
|
display: block;
|
|
}
|
|
a.header-logo:focus-visible {
|
|
outline: 2px solid var(--primary);
|
|
outline-offset: 2px;
|
|
border-radius: 4px;
|
|
}
|
|
|
|
.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-meta {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
}
|
|
|
|
.env-setup-row .env-hint {
|
|
font-size: 12px;
|
|
color: rgba(255, 255, 255, 0.6);
|
|
}
|
|
|
|
.env-setup-link {
|
|
font-size: 13px;
|
|
color: rgba(255, 255, 255, 0.9);
|
|
text-decoration: underline;
|
|
text-underline-offset: 2px;
|
|
}
|
|
.env-setup-link:hover { color: #fff; }
|
|
|
|
.setup-link-banner {
|
|
margin-top: 12px;
|
|
padding: 10px 16px;
|
|
font-size: 13px;
|
|
color: var(--text-secondary, #6b7280);
|
|
text-align: center;
|
|
}
|
|
.setup-link-banner a {
|
|
color: var(--primary);
|
|
text-decoration: none;
|
|
font-weight: 500;
|
|
}
|
|
.setup-link-banner a:hover { text-decoration: underline; }
|
|
|
|
.env-setup-cta .btn-setup[disabled] {
|
|
opacity: 0.7;
|
|
cursor: wait;
|
|
}
|
|
/* .setup-error + .setup-fallback-* styles now live in
|
|
templates/_claude_setup_cta.jinja so /home reuses them. */
|
|
|
|
.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;
|
|
}
|
|
|
|
.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>
|
|
{% endblock %}
|
|
|
|
{% block layout %}
|
|
{% 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 a new Claude Code</h3>
|
|
<p class="env-subtitle">Generates a personal access token and copies a ready-to-paste setup script to your clipboard. Paste into Claude Code to finish.</p>
|
|
<div class="env-setup-row">
|
|
<button type="button" onclick="setupNewClaude(this)" class="btn-setup" id="setupClaudeBtn">
|
|
<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>
|
|
Setup a new Claude Code
|
|
</button>
|
|
<span class="env-meta">
|
|
<span class="env-hint">Valid 90 days · token stays in clipboard only</span>
|
|
<a href="/setup" class="env-setup-link">Open the full setup page →</a>
|
|
</span>
|
|
</div>
|
|
<div id="setupClaudeError" class="setup-error" role="alert" style="display:none;"></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{% if data_stats.last_updated %} status-dot--live{% endif %}"></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 %}{% if data_stats.remote_tables %} · {{ data_stats.local_tables }} local, {{ data_stats.remote_tables }} live{% endif %}{% else %}{{ data_stats.total_tables or 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{% if data_stats.last_updated %} · data from {{ data_stats.last_updated }}{% endif %}
|
|
</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 -->
|
|
|
|
{% if account_details and account_details.last_sync_display %}
|
|
<div class="setup-link-banner">
|
|
Need to set up another machine? <a href="/setup">Open the setup page →</a>
|
|
</div>
|
|
{% endif %}
|
|
|
|
</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, "ssh-keygen -t ed25519 -f {{ ssh_key }} -N ''")" 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>© {{ now().year if now is defined else 2024 }} {{ config.INSTANCE_COPYRIGHT or 'AI Data Analyst' }}</p>
|
|
</footer>
|
|
{# Setup-CTA JS (copyToClipboard, defaultTokenName, showSetupFallback,
|
|
setupNewClaude) + the SETUP_INSTRUCTIONS_TEMPLATE renderer all live
|
|
in the shared partial below. Both /dashboard and /home include it
|
|
so the clipboard payload, error handling, and fallback modal stay
|
|
in lockstep. The partial exposes copyToClipboard on window so the
|
|
copyCode / copyTermLine helpers below resolve it via global scope. #}
|
|
{% include "_claude_setup_cta.jinja" %}
|
|
|
|
<script>
|
|
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);
|
|
});
|
|
}
|
|
|
|
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/memory/stats');
|
|
if (resp.ok) {
|
|
const data = await resp.json();
|
|
document.getElementById('memoryKnowledgeCount').textContent = data.total || 0;
|
|
const approved = (data.by_status && data.by_status.approved) || 0;
|
|
const mandatory = (data.by_status && data.by_status.mandatory) || 0;
|
|
document.getElementById('memoryContributors').textContent = approved + mandatory;
|
|
document.getElementById('memoryYourRules').textContent = mandatory || 0;
|
|
}
|
|
} 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>
|
|
{% endblock %}
|