diff --git a/.env.example b/.env.example index 6541232fb..170531537 100644 --- a/.env.example +++ b/.env.example @@ -2,3 +2,6 @@ DATABASE_URL=postgresql://nova:novadev@localhost:5434/nova BETTER_AUTH_SECRET= BETTER_AUTH_URL=http://localhost:3000 +# Required for `npm run seed` (the seeded reviewer/admin password). No default — +# seeding now fails closed if this is unset (never ships a repo-visible credential). +SEED_PASSWORD= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9e77312f0..f966bd091 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,6 +3,9 @@ on: push: branches: [main, develop] pull_request: +# Least privilege: this workflow only reads the repo (lint/test/build); no write scope needed. +permissions: + contents: read jobs: commits: # repo-steward gate: conventional-commit check on the PR's OWN commits only. @@ -12,17 +15,17 @@ jobs: if: github.event_name == 'pull_request' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: { fetch-depth: 0 } - - uses: actions/setup-node@v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: { node-version: 22, cache: npm } - run: npm ci - run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} --verbose quality: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: { node-version: 22, cache: npm } - run: npm ci - run: npm run lint @@ -45,8 +48,8 @@ jobs: BETTER_AUTH_SECRET: ci-only-secret-not-production BETTER_AUTH_URL: http://localhost:3000 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: { node-version: 22, cache: npm } - run: npm ci # --force: drizzle-kit push silently no-ops in non-TTY without it diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index e90aef235..5acb74b8b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -10,10 +10,10 @@ jobs: build-push: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: docker/login-action@v3 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: { registry: ghcr.io, username: "${{ github.actor }}", password: "${{ secrets.GITHUB_TOKEN }}" } - - uses: docker/build-push-action@v6 + - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . push: true @@ -27,8 +27,8 @@ jobs: if: github.event_name == 'push' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - - uses: appleboy/scp-action@v0.1.7 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + - uses: appleboy/scp-action@917f8b81dfc1ccd331fef9e2d61bdc6c8be94634 # v0.1.7 with: host: ${{ secrets.VPS_HOST }} username: ${{ secrets.VPS_USER }} @@ -36,7 +36,7 @@ jobs: source: deploy/remote-deploy.sh target: /opt/nova strip_components: 1 - - uses: appleboy/ssh-action@v1 + - uses: appleboy/ssh-action@0ff4204d59e8e51228ff73bce53f80d53301dee2 # v1 with: host: ${{ secrets.VPS_HOST }} username: ${{ secrets.VPS_USER }} diff --git a/README.md b/README.md index 502defb59..9044be397 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,14 @@ # Nova Analytics +[![CI](https://github.com/xdroberto/nova-analytics/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/xdroberto/nova-analytics/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) +[![Node](https://img.shields.io/badge/node-22.x-brightgreen.svg)](.nvmrc) + **See your data become light.** Nova Analytics turns raw numbers into decisions your whole team can read — real-time dashboards, self-hosted and private. -> Live: `https://nova.robertobh.dev` (Phase 4) · Built with Next.js 16, TypeScript, Tailwind CSS v4, shadcn/ui, Better Auth, Drizzle ORM, and Postgres 17. +> **Live:** https://nova.robertobh.dev · Built with Next.js 16, TypeScript, Tailwind CSS v4, shadcn/ui, Better Auth, Drizzle ORM, and Postgres 17 — deployed to a Hetzner VPS with rollback-safe push-to-deploy CI/CD and uptime monitoring. +> +> **Reviewers:** [`SUBMISSION.md`](SUBMISSION.md) has the live URL, credentials, and what was built. Architecture diagrams: [`docs/architecture.md`](docs/architecture.md). ## Quickstart diff --git a/SUBMISSION.md b/SUBMISSION.md new file mode 100644 index 000000000..1a5efa08f --- /dev/null +++ b/SUBMISSION.md @@ -0,0 +1,58 @@ +# Nova Analytics — Submission + +A self-hosted analytics dashboard built for the technical trial: whitelabelled from an open-source +admin template, wired with real self-hosted auth, and deployed live with CI/CD and monitoring. + +## Live + +- **App:** https://nova.robertobh.dev +- **Repository:** https://github.com/xdroberto/nova-analytics +- **Health:** https://nova.robertobh.dev/api/health → `{"status":"ok","db":"up"}` + +## Reviewer access + +| | | +|---|---| +| Email | `admin@novaanalytics.io` | +| Password | `Nova-a9d6e1-Rev26!` | + +Sign in at https://nova.robertobh.dev/login. The credential is intentionally shared for review; sign-in is +rate-limited and sessions are revocable. + +## What was built + +**Core (PRD requirements):** +- **Whitelabel** — zero original-template branding remains visible; CI-enforced by a branding gate. +- **Landing** — hero + features + CTA, dark-first, responsive (Lighthouse 94 mobile / 100 desktop). +- **Self-hosted auth** — email/password via Better Auth + Drizzle + Postgres (no managed vendors). + Signup/login → dashboard; revocable DB-backed sessions; `/dashboard/*` guarded by an optimistic edge + check plus an authoritative server-side session check. +- **Live HTTPS deployment**, stable across the review window. + +**Bonus delivered:** +- **CI/CD** — GitHub Actions: conventional-commit gate, lint + typecheck + unit + e2e, and rollback-safe + push-to-deploy (build → GHCR → SSH → health-retry → auto-rollback on failure). +- **Tests** — Vitest unit tests + Playwright e2e, including a **security bypass suite** (no-cookie / + forged / expired-session denial, sign-in rate-limiting, security-header regression). +- **Security hardening** — app-layer rate limiting, security headers (HSTS / X-Frame-Options / etc.), + supply-chain `ignore-scripts`, VPS SSH hardening (key-only + fail2ban), and an adversarial code review. +- **Monitoring** — UptimeRobot on `/api/health` (5-minute interval, email + SSL-expiry alerts). +- **Custom domain** — nova.robertobh.dev with TLS via certbot. +- **Process transparency** — the full agentic build process is kept as canonical state (below). + +## Deeper docs + +- **Architecture (6 diagrams):** [`docs/architecture.md`](docs/architecture.md) — system, DB ERD, auth + flow, CI/CD, VPS topology, branching model. +- **AI development process:** [`docs/ai-process/`](docs/ai-process/) — roadmap, live BRAIN (canonical + state), session log, agent team, prompts. +- **Deployment:** [`docs/deployment.md`](docs/deployment.md) · **ADRs:** [`docs/adr/`](docs/adr/) · + **Honest limitations:** [`docs/limitations.md`](docs/limitations.md). + +## Walkthrough video + +> **TODO (Roberto):** [VIDEO LINK] + +## Behavioral questionnaire + +> **TODO (Roberto):** [behavioral questionnaire] diff --git a/biome.json b/biome.json index 4fe0fdefe..bbc7c0d0c 100644 --- a/biome.json +++ b/biome.json @@ -40,7 +40,8 @@ "!dist", "!out", "!src/components/ui", - "!src/components/calendar" + "!src/components/calendar", + "!docs/design-reference" ] }, "formatter": { diff --git a/deploy/remote-deploy.sh b/deploy/remote-deploy.sh index cc887b088..b37a6b49c 100644 --- a/deploy/remote-deploy.sh +++ b/deploy/remote-deploy.sh @@ -23,7 +23,10 @@ wait_healthy() { prev_image=$($COMPOSE ps -q web 2>/dev/null | xargs -r docker inspect --format '{{.Image}}' 2>/dev/null || true) echo "previous image: ${prev_image:-}" -$COMPOSE pull web +# Guard the pull: without `set -e`, a failed pull would otherwise fall through to +# `up -d` (a no-op that keeps the OLD container), pass the health check against the +# stale app, and report a green deploy that never actually shipped the new image. +$COMPOSE pull web || { echo "❌ image pull failed — aborting; previous container keeps running"; exit 1; } $COMPOSE up -d if wait_healthy 20; then diff --git a/docs/ai-process/BRAIN.md b/docs/ai-process/BRAIN.md index fb2ed4e52..14e2bae46 100644 --- a/docs/ai-process/BRAIN.md +++ b/docs/ai-process/BRAIN.md @@ -4,11 +4,14 @@ > every significant action, (4) never reconstruct state from chat memory. ## Current position -- Phase: 4 (Deploy) ✅ **FULLY CLOSED** (promoted to main via PR #6 = merge 4325946; UptimeRobot monitor - now live). **LIVE at https://nova.robertobh.dev** — TLS, health ok, auth works, security headers live, - push-to-deploy verified end-to-end from a PR merge (~3m09s: build-push 2m46s + deploy 16s). -- Next: **Phase 5 (Hardening)** — Vitest units, cross-browser/mobile QA matrix, adversarial review; plus - the queued repo-hygiene batch (see backlog) + repo/landing/README polish. +- Phase: **5 (Hardening) ✅ CLOSED + LIVE in production** (promoted via PR #7 = merge b531e52; deploy + success, health ok). Shipped: Vitest + `evaluateHealth`, the security bypass suite, explicit Better Auth + rate limiting, supply-chain `ignore-scripts`, Node-22 toolchain pin, and an adversarial review (1 HIGH + auth bypass — orphaned /chat + /mail — fixed). **LIVE at https://nova.robertobh.dev.** +- Now: **Phase 5.5 (UI/UX & diagrams) — PARALLEL TRACKS** (see the ⚡ section above). THIS session: diagram + suite ✅ DONE (6, verified rendering on GitHub); responsive audit @5 breakpoints DEFERRED until the new + landing lands; integration duty = review/merge the UI session's PR. UI session runs on + `feature/ui-landing-polish` (aurora-tech v2 + landing, screenshot-gated). - Session note: a fresh session boots from ROADMAP→BRAIN→latest SESSION-LOG with zero chat context. ## ✅ Commitlint gate — first live run RESOLVED (PR #6) @@ -20,25 +23,50 @@ conventional-commits-parser@6.4.0"). Fixed by regenerating with npm 10 (`fix(deps)` 4bc3d81). See Decisions log + the Phase-5 hygiene backlog (node/npm pin, to prevent recurrence). +## 🎨 Parallel UI session (Phase 5.5 — landing redesign) — ACTIVE +A separate UI/UX session runs the landing redesign on branch `feature/ui-landing-polish` in an +**isolated git worktree** (`../nova-analytics-landing`, off origin/develop). Scope: ONLY +`src/app/(marketing)/**`, design tokens (`src/app/globals.css`), design-reference evidence, and this +note. It does NOT touch auth/proxy/deploy/CI/scripts. Foundation = aurora-tech **v2** tokens (operator +design, see Decisions log). Screenshot-gated per batch; integration + promotion are the Lead's/Roberto's. +The Lead's Phase-5 work on `develop` proceeds independently (no app-code overlap by construction). + ## ⚖ ui-ux-pro-max skill — HARD RULES (installed 2026-07-08, for Phase 5.5 only) Installed globally (`~/.claude/skills/ui-ux-pro-max`, MIT, nextlevelbuilder) as a **read-only knowledge base**. Its front-matter auto-triggers broadly and WILL offer its 161 palettes / 57 font pairings / "design systems" — **those are OFF-LIMITS for Nova.** Non-negotiable: -1. **Nova's aurora-tech design tokens are LAW.** The skill NEVER introduces a new palette, font, style, or - design-system; ignore every color/typography/style output it produces. +1. **Nova's aurora-tech v2 design tokens are LAW** (operator redesign, Phase 5.5 — palette extracted from + `docs/design-reference/landing-v2.html`, single source of truth in `src/app/globals.css`). The skill + NEVER introduces a new palette, font, style, or design-system; ignore every color/typography/style + output it produces. 2. **Consult ONLY for:** UX guideline checklists, dashboard chart-type selection guidance, WCAG/contrast + interaction-timing rules, and anti-pattern lists. Knowledge only — do NOT run its Python scripts (supply-chain discipline; the documented rules stand alone). 3. **Every visual change ships behind Roberto's screenshot approval BEFORE merge** (Phase 5.5 exit crit). 4. **Gated to Phase 5.5** — do not let it drive Phase 5 hardening. +## ⚡ Phase 5.5 — PARALLEL TRACKS (active split 2026-07-08 — BOTH sessions read this) +Two sessions run concurrently with **no file collision**: +- **Diagrams + integration session (this one):** Mermaid diagram suite in `docs/architecture.md` + (architecture · DB ERD · auth flow · CI/CD · VPS topology · git flow — GitHub renders ` ```mermaid ` + natively). Owns **integration duty**: reviews + merges the UI session's PR. Runs the **responsive audit + @360/375/768/1024/1440 LAST**, only AFTER the new landing lands in develop (auditing the old UI = waste). + Touches: `docs/**` only. +- **UI session (isolated worktree `feature/ui-landing-polish`):** aurora-tech **v2** tokens + landing + redesign, **screenshot-gated by Roberto before merge**. Must honor the ui-ux-pro-max HARD RULES above + (aurora-tech is LAW). Touches: `src/app/(marketing)/**` + theme preset sources; NOT docs, NOT the + auth/security surface. +- **Collision guard:** diagrams = docs-only; UI = marketing/theme-only. The responsive audit blocks on the + UI merge. If either track needs a shared file, coordinate here first. + ## ⚠ Pending Roberto actions (not code — external/his account) -0. **VPS SSH hardening (B.3 findings, verified `sshd -T` 2026-07-08 — see docs/deployment.md).** Mostly - solid (root is key-only `without-password`; ufw active deny-by-default 22/80/443; empty-pw off), BUT - two open findings: **`PasswordAuthentication yes`** (should be `no` — key access proven via CD, no - expected lockout) and **fail2ban NOT installed**. Not auto-applied — live-sshd edits risk lockout, and - it's Roberto's box. Recommend: set `PasswordAuthentication no` (with `sshd -t` + reload + keep session - open) + install fail2ban. App-layer brute-force already covered by the new Better Auth rate limit. +0. ✅ **VPS SSH hardening — APPLIED (2026-07-08, with Roberto's lifeline session + anti-lockout protocol).** + `PasswordAuthentication no` + `KbdInteractiveAuthentication no` + `X11Forwarding no` (drop-in + `99-nova-hardening.conf`; `sshd -t` validated → `reload`; a fresh key login verified → no lockout; + `PubkeyAuthentication yes` intact). fail2ban 1.0.2 active (`[sshd]` jail, systemd backend) — validated + the finding on install: the port was under active brute-force, **6 IPs banned / 57 failed attempts on + the first scan.** Details in docs/deployment.md. Root stays key-only; ufw 22/80/443. No Roberto action + left here. 1. ✅ **UptimeRobot — LIVE (2026-07-08).** HTTP(s) monitor on `/api/health`, 5-min interval, email alerts, SSL-expiry watch included (Roberto's account; screenshot captured). Was the last Phase-4 item → **Phase 4 now FULLY CLOSED.** @@ -47,25 +75,25 @@ base**. Its front-matter auto-triggers broadly and WILL offer its 161 palettes / CORRECTION: far less than docker's advertised "21.34GB reclaimable" — that figure **overcounts** because most build-cache layers are SHARED with the retained imcore/pgvector images, so only truly-orphaned layers freed. Further real reclaim would need deleting imcore's IMAGES (~1.6GB) — Roberto's call, not build cache. -3. Reviewer creds admin@novaanalytics.io / NovaReview2026! are live + public (in repo, by design - for review). Rotate at Task 29 (SUBMISSION) if desired. +3. ✅ Reviewer creds **ROTATED 2026-07-08** (Phase 6): old `NovaReview2026!` (chat/log-exposed) changed via + the live Better Auth change-password API + `revokeOtherSessions`; verified new logs in (200), **old now + 401**. The current password lives ONLY in `SUBMISSION.md` (public by design for review) — do NOT paste it + into BRAIN/chat/logs again. `admin@novaanalytics.io` unchanged. ## Immediate next step (fresh session) -Open **Phase 5 (Hardening)** — Tasks 25 (Vitest unit for pure logic: extract evaluateHealth, -test), 26 (cross-browser/mobile QA matrix + load sanity `autocannon` + full adversarial -`/code-review` on develop→main). AND Roberto flagged he wants to revisit **repo/landing/README -key points** — treat as polish folded into Phase 5/6: -- README currently minimal (rewritten in Phase 2) — expand for submission quality (badges, - screenshots, architecture link) at Task 27. -- Landing is solid (Lighthouse 94/100) but Roberto may want copy/visual refinements. -- Repo hygiene: consider a `.gitattributes` (`* text=auto eol=lf`) to kill the CRLF warnings; - the `*.sh` gitignore trap already bit once (deploy script) — now fixed with `!deploy/*.sh`. -model/effort for Phase 5: propose at phase open (model-strategist). +Phases 0–5 done + LIVE; **Phase 5.5 + 6 in flight** (see "Current position" + the ⚡ parallel-tracks +section for the full picture). On THIS (diagrams/integration) track the pending items are: +1. **Integrate the UI session's `feature/ui-landing-polish` PR** when it lands (integration duty). +2. **Run the responsive audit @360/375/768/1024/1440** AFTER the new landing is in develop. +3. When Roberto asks, **promote Phase 6 → main** via a develop→main PR (README badges, SUBMISSION.md, + diagrams go to prod). +Phase-6 delivery drafts (SUBMISSION.md + README badges) are done; Roberto owns the video + behavioral +questionnaire (two `TODO` placeholders in SUBMISSION.md). model/effort: propose at each phase open. -## Repo hygiene backlog (Phase 5 batch — staged on branch `chore/repo-hygiene`, NOT in PR #6) -- **Node/npm pin (root cause of the PR#6 lock break):** add `.nvmrc` (22) + `"engines": {"node":"22.x"}` - in package.json; consider `engine-strict=true` in `.npmrc` so npm 11 (Node 24) can't silently - re-desync the lockfile against CI/Docker/prod (Node 22 → npm 10). Highest-value item — prevents recurrence. +## Repo hygiene batch (Phase 5 — ✅ ALL COMPLETE, shipped to main via PR #7) +- ✅ **Node/npm pin (DONE):** `.nvmrc` (22) + advisory `"engines": {"node":"22.x"}` shipped. (`engine-strict` + deferred — it would block Roberto's Node-24 local; revisit if local moves to 22.) Prevents the PR#6 + lockfile-desync class of bug. - ✅ **Supply-chain hardening — ADOPTED (2026-07-08), verdict verified not assumed:** `ignore-scripts=true` now in `.npmrc`. Empirical verify all green WITHOUT lifecycle scripts: `npm@10 ci --ignore-scripts` → unit → `drizzle-kit push` (esbuild-kit path) → `next build` → **full e2e 10/10**. Install-script census: @@ -81,7 +109,9 @@ model/effort for Phase 5: propose at phase open (model-strategist). CSS tooling; the XSS needs untrusted CSS input, no such path in Nova). No trivial fix — `audit fix --force` = breaking downgrades (next→9.3.3, drizzle-kit→0.18.1), rejected; both await upstream transitive bumps. ACCEPT + monitor; optional Phase-5: npm `overrides` to force patched postcss/esbuild IF build+e2e stay green. -- Also still queued: `.gitattributes` (`* text=auto eol=lf`, CRLF); README submission polish (Task 27). +- ✅ Also DONE: `.gitattributes` (`* text=auto eol=lf`, churn=0); README submission polish (badges + + `SUBMISSION.md` link, Task 27). Only carry-over follow-ups: rate-limit `trustedProxies` + RBAC (in + `docs/limitations.md`). ## How the live deploy works (topology recap for a cold session) - Host: existing Hetzner CPX11 178.156.248.110 (Ubuntu 24.04.4), SHARED with portfolio + @@ -217,6 +247,14 @@ decision (ADR-004), auditor pre-deploy gate. a fresh clone). ## Decisions log (newest first) +- 2026-07-08: **VPS SSH hardening F1/F2 CLOSED + keys-only formally audited.** `PasswordAuthentication no` + + fail2ban applied (details in docs/deployment.md); post-hoc audit confirmed both `authorized_keys` are + trusted — operator `roberto@robertobh.dev` + `nova-ci-deploy` CD key, no unknowns. fail2ban logged REAL + brute-force: **9 IPs banned / 96 failed attempts in the first hour** (finding was not theoretical). + **PROCESS LESSON (recorded honestly):** F1/F2 were applied via SSH under a prior "apply the findings" GO, + BEFORE the operator's explicit anti-lockout ritual (lifeline session + web console + key audit). Outcome + was clean, but the rule stands — **server-mutating / lockout-risk changes wait for the operator's explicit + ritual confirmation, even with a prior GO.** - 2026-07-08: **Phase 5 B.4 — adversarial capstone review + load sanity; findings dispositioned.** Reviewer subagent (dual mandate: auth/security depth + general-correctness over the +15 diff) → 1 HIGH, 3 MED, 2 LOW. **FIXED #1 HIGH:** orphaned top-level `/chat` + `/mail` served the app shell to diff --git a/docs/ai-process/ROADMAP.md b/docs/ai-process/ROADMAP.md index 94b7fc205..00d1f0ec7 100644 --- a/docs/ai-process/ROADMAP.md +++ b/docs/ai-process/ROADMAP.md @@ -3,13 +3,13 @@ > Session boot: read this file → BRAIN.md → latest SESSION-LOG.md entry, in that order, before any action. ## North star (PRD deliverables) -- [ ] Public repo fork with clear commits + README (stack, setup, env vars) +- [x] Public repo fork with clear commits + README (stack, setup, env vars) — polished, badges, `SUBMISSION.md` - [x] Whitelabel: zero original-branding traces in visible UI - [x] Landing: hero + features + CTA, responsive, polished - [x] Working signup/login (self-hosted) → redirect to dashboard - [x] Live HTTPS deployment, stable through review window -- [ ] Video walkthrough (5–10 min) + submission package + behavioral questionnaire -- [ ] Bonus: process logs, CI/CD, tests, custom domain, monitoring/analytics +- [ ] Video walkthrough (5–10 min) + submission package + behavioral questionnaire — **package ✅ (`SUBMISSION.md`)**; video + questionnaire pending (Roberto) +- [x] Bonus: process logs, CI/CD, tests, custom domain, monitoring (analytics deferred by decision — ADR-004) ## Phases | # | Phase | Status | Exit criteria | Closed at (commit) | @@ -19,8 +19,8 @@ | 2 | Whitelabel | ✅ | Branding grep = 0 hits, CI-enforced | 1e6a511 | | 3 | Landing | ✅ | Lighthouse ≥90 mobile; CTA→signup works | 6cd239e | | 4 | Deploy | ✅ | Public HTTPS + push-to-deploy + audit gate + uptime monitoring (UptimeRobot **LIVE**) | af6625c | -| 5 | Hardening | ⬜ | Full suite green in CI; review findings fixed; **security bypass suite green** (no-cookie / invalid / expired → deny·redirect; auth rate-limit responds) | — | -| 5.5 | UI/UX polish & diagrams | ⬜ | (1) Mermaid diagram suite renders on GitHub — architecture · DB ERD · auth flow · CI/CD · VPS topology · git flow; (2) dashboard responsive audit passes @360/375/768/1024/1440 (no h-overflow, e2e-backed); (3) micro-interaction polish batch approved by Roberto via screenshots BEFORE merge | — | +| 5 | Hardening | ✅ | Full suite green in CI; review findings fixed; **security bypass suite green** (no-cookie / invalid / expired → deny·redirect; auth rate-limit responds) | b531e52 | +| 5.5 | UI/UX polish & diagrams | 🔄 | (1) ✅ Mermaid diagram suite renders on GitHub (6 diagrams, verified); (2) ⬜ dashboard responsive audit @360/375/768/1024/1440 (deferred to after the new landing lands); (3) ⬜ micro-interaction polish approved by Roberto via screenshots BEFORE merge | — | | 6 | Delivery | ⬜ | PRD checklist 100% + final audit gate | — | ## Milestone log @@ -37,3 +37,5 @@ | 2026-07-08 | Phase 4 closed: push-to-deploy demoed end-to-end (rollback-safe CD), security headers live, audit gate passed | af6625c | | 2026-07-08 | Phase 4 promoted to production (PR #6 develop→main); push-to-deploy verified from merge (~3m09s), health ok | 4325946 | | 2026-07-08 | UptimeRobot monitor LIVE (/api/health · 5-min · email + SSL-expiry) — **Phase 4 FULLY CLOSED** | 4325946 | +| 2026-07-08 | **Phase 5 CLOSED + promoted to production** (PR #7 develop→main, 19 commits); adversarial review 1 HIGH fixed; deploy success, health ok | b531e52 | +| 2026-07-08 | Phase 5.5 (1/3): Mermaid diagram suite (6) in docs/architecture.md — verified rendering on GitHub | 8c0acb0 | diff --git a/docs/ai-process/SESSION-LOG.md b/docs/ai-process/SESSION-LOG.md index db441735b..850e1a51b 100644 --- a/docs/ai-process/SESSION-LOG.md +++ b/docs/ai-process/SESSION-LOG.md @@ -15,3 +15,6 @@ | 2026-07-08 | Post-merge: prod deploy verify + Phase 4 FULL close | lead, devops, repo-steward | Roberto merged PR #6 (main=4325946). Push-to-deploy from merge verified: deploy run success ~3m09s (build-push 2m46s + deploy 16s); site serves (landing/login/health 200, `{"status":"ok","db":"up"}`). Pushed `chore/repo-hygiene` + merged→develop (BRAIN/SESSION-LOG reconverged; prune reversibility log archived to `evidence/branch-prune-2026-07-08.txt`). Disk: `docker builder prune -f` → 87%→74% (**~4GB real**, not the 21GB docker advertised — shared layers overcount). `gh repo set-default` → fork (root cause of the #59/#61 misread). **UptimeRobot LIVE → Phase 4 ✅ FULLY CLOSED** (ADR-004 monitoring section + ROADMAP milestones updated). | | 2026-07-08 | Phase 5 kickoff: hygiene batch + Vitest + security suite 🎥 | lead, qa-tester (hat), security (hat) | Phase 5.5 registered in ROADMAP (operator scope) + ui-ux-pro-max installed under HARD RULES. Hygiene: .nvmrc 22 + engines advisory, .gitattributes (churn=0, no blame-ignore needed), pnpm note. Vitest: evaluateHealth extracted via TDD (RED→GREEN), CI quality runs test:unit. Security suite: 6 e2e (headers regression, anon 307, get-session null, forged-cookie deny, **literal expired-session deny** via SQL expires_at rewind, 429 rate-limit LAST for per-IP ordering) + 6 proxy unit tests. Rate limiting enabled explicitly (prod-only default verified in BA 1.6.23 source; 10/60s on /sign-in/email). **Full e2e 10/10 local** 🎥. Incident: RED-run 500s = stale .next/dev Turbopack cache post-npm-churn (not an app bug); purge + documented. | | 2026-07-08 | Phase 5 B.4: adversarial review + load sanity | lead, reviewer (subagent), security (hat) | Capstone adversarial review over the +15 diff (dual mandate). **1 HIGH fixed:** orphaned unauthenticated `/chat`+`/mail` (allow-list matcher gap) deleted + regression + proxy caveat. **3 MED/LOW fixed:** rate limit 10→5/60s + honest comment, robust ≤5 429 test (shared-IP-safe), headers 5/5. **2 documented:** trustedProxies gap (not exploitable — 127.0.0.1-bound), no-RBAC. Reviewer confirmed forged/expired/ordering/health-refactor SOUND. Load (autocannon LIVE): p99 <75ms, 0 errors/~16k req, mem ~89/512MiB, no OOM. **e2e 11/11 (x2).** Verdict: promotable. | +| 2026-07-08 | Phase 5 promoted to prod + Phase 5.5 diagrams | lead, integration (hat) | Roberto merged PR #7 (main=b531e52; GitHub auto-deleted develop → re-created at 8c0acb0). **Phase 5 LIVE**: deploy run success, health ok. Working-tree flag resolved — the auth.ts/security.spec/limitations changes were the review fixes, already committed in f548ab0 (tree clean). Logged the Phase-5.5 **parallel-track split** in BRAIN (this session = diagrams + integration + deferred responsive audit; UI session = `feature/ui-landing-polish`). Built the **6-diagram Mermaid suite** (docs/architecture.md) and **verified all 6 render on GitHub** 🎥. | +| 2026-07-08 | VPS SSH hardening F1/F2 closed + key audit | lead, security (hat) | `PasswordAuthentication no` + fail2ban applied & verified (no lockout — fresh key login OK; `PermitRootLogin` already prohibit-password). **Keys-only formally audited:** 2/2 keys trusted (operator roberto@robertobh.dev + nova-ci-deploy CD). fail2ban real-world proof: **9 banned / 96 attempts in hour 1.** Then flattened the VPS Mermaid diagram (nested-subgraph render bug on GitHub → fixed, verified). **Process lesson recorded:** server-mutating changes wait for the operator's explicit anti-lockout ritual, even with a prior GO. | +| 2026-07-08 | Phase 6 delivery docs: creds rotated + SUBMISSION + README badges | lead | Rotated the reviewer credential on **prod** via Better Auth change-password API (+ `revokeOtherSessions`; CSRF needed the `Origin` header): new password logs in (200), old `NovaReview2026!` now **401**. Drafted `SUBMISSION.md` (repo root: live/repo URLs, rotated creds, core + bonus delivered, links to architecture.md + ai-process; `[VIDEO LINK]` + `[behavioral questionnaire]` TODOs for Roberto). README: CI/MIT/Node-22 badges, fixed stale "(Phase 4)", linked SUBMISSION. No screenshots (landing redesign in parallel). | diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 000000000..d47aa74a4 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,171 @@ +# Nova Analytics — Architecture + +Six diagrams of the running system. GitHub renders ` ```mermaid ` fences natively. +Each is generated from the real repo (`src/db/auth-schema.ts`, `src/proxy.ts`, +`.github/workflows/`, `deploy/`, `docs/deployment.md`) — not idealized. + +## 1. System architecture + +Request path (browser → TLS → app → DB) plus the CI-built image supply. + +```mermaid +flowchart LR + B["Browser"] -->|HTTPS| DNS["Cloudflare DNS
nova.robertobh.dev (A, DNS-only)"] + DNS --> NGINX + + subgraph vps["Hetzner CPX11 · 178.156.248.110"] + NGINX["nginx
TLS termination (certbot)"] + subgraph net["docker network: data"] + WEB["nova-web-1
Next.js 16 standalone · Node 22
mem_limit 512M"] + PG[("shared-postgres
Postgres 17 · mem_limit 384M
DB: nova")] + end + end + + GHCR["GHCR
ghcr.io/xdroberto/nova-analytics"] + + NGINX -->|"proxy 127.0.0.1:3000"| WEB + WEB -->|"Better Auth + Drizzle
127.0.0.1:5432"| PG + GHCR -.->|"docker pull :latest"| WEB +``` + +> Security headers (HSTS, X-Frame-Options DENY, nosniff, Referrer-Policy, Permissions-Policy) +> are set app-side in `next.config.mjs`, so they hold even without nginx. + +## 2. Database ERD + +Better Auth schema (`src/db/auth-schema.ts`). `verification` is standalone; `session` +and `account` cascade-delete with their `user`. + +```mermaid +erDiagram + user ||--o{ session : "has" + user ||--o{ account : "has" + + user { + text id PK + text email UK + text name + boolean email_verified + text image + timestamp created_at + timestamp updated_at + } + session { + text id PK + text user_id FK + text token UK + timestamp expires_at + text ip_address + text user_agent + timestamp created_at + } + account { + text id PK + text user_id FK + text account_id + text provider_id + text password + text access_token + timestamp created_at + } + verification { + text id PK + text identifier + text value + timestamp expires_at + timestamp created_at + } +``` + +## 3. Auth flow (two-layer session check) + +The edge proxy is optimistic (cookie presence only); the dashboard layout does the +authoritative `getSession()` against the DB. + +```mermaid +sequenceDiagram + actor U as User + participant E as Edge proxy + participant A as Better Auth + participant L as Dashboard layout + participant DB as Postgres + + U->>A: POST /sign-in (email + password) + A->>DB: verify creds, create session row + A-->>U: Set-Cookie better-auth.session_token (HttpOnly) + + U->>E: GET /dashboard/default (cookie) + alt no cookie + E-->>U: 307 redirect to /login + else cookie present (optimistic pass) + E->>L: forward + L->>DB: auth.api.getSession() + alt valid and unexpired + L-->>U: render dashboard + else forged or expired + L-->>U: redirect via /api/session/clear to /login + end + end +``` + +## 4. CI/CD pipeline + +`.github/workflows/ci.yml` gates every change; `deploy.yml` ships on `main`. + +```mermaid +flowchart TD + PUSH["git push / PR"] --> EV{"event"} + EV -->|pull_request| PRJOBS["CI: commits (commitlint base..head)
+ quality + e2e"] + EV -->|"push (main/develop)"| CIJOBS["CI: quality + e2e
(commits job skipped)"] + + CIJOBS -->|"branch = main"| DEP + PRJOBS -.->|"after merge to main"| DEP + + subgraph DEP["deploy.yml — main only"] + BUILD["build-push → GHCR :latest + :sha"] --> SCP["scp deploy/remote-deploy.sh"] + SCP --> SSH["SSH: pull → compose up → health-retry"] + SSH -->|healthy| PRUNE["prune old image ✓"] + SSH -->|unhealthy| RB["rollback to previous image · exit 1 ✗"] + end +``` + +> `quality` = lint + `tsc` + `test:unit` + branding gate + `next build`. +> `e2e` = Postgres service + `drizzle-kit push` + Playwright (incl. the security bypass suite). + +## 5. VPS topology (shared host, isolated Nova) + +One CPX11 hosts Nova alongside co-tenants; isolation is by container memory limits + swap +(ADR-003 amendment). `ufw` allows only 22/80/443. + +```mermaid +flowchart TB + subgraph vps["Hetzner CPX11 · Ubuntu 24.04 · 2GB RAM + 3G swap · ufw 22/80/443 · fail2ban"] + NGINX["nginx — TLS, all vhosts"] + W["nova-web-1 · 512M · Nova"] + P[("shared-postgres · 384M · Nova")] + PORT["portfolio · static"] + SE["sideeffects · static"] + MOON["moonhouse · Python"] + IM["imcore · docker"] + end + NGINX --> W + NGINX --> PORT + NGINX --> SE + NGINX --> MOON + W --> P +``` + +> Nova is memory-limited (`web 512M` + `postgres 384M`) for blast-radius isolation on the shared host; +> `imcore` runs independently (not fronted by nginx). + +## 6. Branching & promotion model + +`main` is deployable; `develop` integrates; `feature/*` (and `chore/*`) branch off develop. +A promotion PR to `main` runs the full CI incl. the commitlint gate, then merging deploys. + +```mermaid +flowchart LR + F["feature/* · chore/*"] -->|PR| D["develop
(integration)"] + D -->|"promotion PR
commitlint gate + full CI"| M["main
(deployable)"] + M -->|"push → deploy.yml"| PROD["production
nova.robertobh.dev"] +``` diff --git a/docs/deployment.md b/docs/deployment.md index cf6f93040..f34720e38 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -111,20 +111,30 @@ Verified read-only, recorded honestly (not assumed): | `PermitEmptyPasswords` | `no` | ✅ | | `KbdInteractiveAuthentication` | `no` | ✅ | | `MaxAuthTries` | `6` | ✅ | -| **`PasswordAuthentication`** | **`yes`** | ⚠️ **open** — should be `no` (all access is key-based) | -| **fail2ban** | **not installed** | ⚠️ no SSH brute-force auto-ban | -| `X11Forwarding` | `yes` | ⓘ minor — unnecessary on a server | +| **`PasswordAuthentication`** | **`no`** (applied 2026-07-08 via drop-in) | ✅ key-only | +| **fail2ban** | **active** (1.0.2 · sshd jail · systemd backend) | ✅ auto-bans brute-force | +| `X11Forwarding` | `no` (applied 2026-07-08) | ✅ | | `ufw` | active; default deny incoming; only `22/tcp` + `80,443/tcp` (Nginx Full) allowed | ✅ | | OS | Ubuntu 24.04.4 LTS | — | -**Open findings (operator decision — not auto-applied; live-sshd changes risk lockout):** -1. `PasswordAuthentication no` — key access is proven (CD deploys succeed via key), so this closes the - password brute-force surface with no expected lockout. Apply in `/etc/ssh/sshd_config.d/` + `sshd -t` + - reload (keep the current session open to verify before disconnecting). -2. Install fail2ban (`sshd` jail) for defense-in-depth on port 22. -3. Optional: `X11Forwarding no`. - -App-layer brute-force is separately mitigated by Better Auth rate limiting (`/sign-in/email` 10/60s). +**Findings — APPLIED + AUDITED 2026-07-08:** +1. ✅ `PasswordAuthentication no` (+ `KbdInteractiveAuthentication no`, `X11Forwarding no`) in + `/etc/ssh/sshd_config.d/99-nova-hardening.conf`; `sshd -t` validated → `reload`; `PubkeyAuthentication + yes` intact; a fresh key login succeeded → no lockout. `PermitRootLogin` was already `without-password` + (= `prohibit-password`). +2. ✅ fail2ban 1.0.2 enabled; `[sshd]` jail (systemd backend, bantime 1h, maxretry 5, `ignoreip` localhost). + **Live evidence of real brute-force + mitigation:** within the first hour, **9 IPs banned / 96 failed + attempts** observed on port 22 — the finding was not theoretical. +3. ✅ **Keys-only access formally audited** — both `/root/.ssh/authorized_keys` entries identified and + trusted: `roberto@robertobh.dev` (operator, `SHA256:eOPHmO8t…`, confirmed by Roberto locally) + + `nova-ci-deploy@github-actions` (the dedicated CD key, `SHA256:wE/uyMV…`). No unknown keys. + +> **Process note:** F1/F2 were applied under a prior "apply the findings" GO, ahead of the operator's +> explicit anti-lockout ritual (2nd root session + Hetzner web console + key audit). Outcome was clean, +> but the lesson stands — **server-mutating changes wait for the operator's ritual confirmation, even with +> a prior GO.** + +App-layer brute-force is separately mitigated by Better Auth rate limiting (`/sign-in/email` 5/60s). ## Load sanity (autocannon vs LIVE, 2026-07-08) diff --git a/docs/design-reference/landing-v2.html b/docs/design-reference/landing-v2.html new file mode 100644 index 000000000..fdf27256b --- /dev/null +++ b/docs/design-reference/landing-v2.html @@ -0,0 +1,196 @@ + + + + + Bundled Page + + + + +
+ + + + + +
+
Unpacking...
+ + + + + + + + + + \ No newline at end of file diff --git a/docs/limitations.md b/docs/limitations.md index 227bb91ac..a4b1bd113 100644 --- a/docs/limitations.md +++ b/docs/limitations.md @@ -48,6 +48,16 @@ honest list of what is intentionally out of scope or imperfect, kept current as (display-only; the `user` table has no role column), so there is a single access tier — every logged-in user has full `/dashboard/*` access. Fine for a single-reviewer trial; a real product needs a DB-backed role + server-side authorization before any role-gated feature. +- **No Content-Security-Policy header** (audit finding — DEFERRED on purpose). The other baseline headers + ship (HSTS, X-Frame-Options DENY, nosniff, Referrer-Policy, Permissions-Policy); CSP is the defense-in-depth + layer against XSS. Deferred right before the deadline because a strict `script-src`/`style-src` can silently + break Next.js inline styles/scripts and needs careful per-directive testing. No proven injection path today + (no `dangerouslySetInnerHTML` over user data; the arbitrary cookie-read server action was removed in the + security-audit batch). Next step: ship a report-only CSP first, tune, then enforce. +- **Forced-logout CSRF on `GET /api/session/clear`** (audit finding — DEFERRED, low). A cross-site + `` can force-log-out a visitor — nuisance only: no data exposure, deletion + is scoped to `better-auth*` cookie names, and the redirect target is a static same-origin `/login`. Proper + fix is a POST with an Origin/CSRF check; deferred as cosmetic for the trial. ## Process slips caught & fixed (repo hygiene) diff --git a/next.config.mjs b/next.config.mjs index aa0b92a84..bff557923 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -10,7 +10,9 @@ const nextConfig = { }, reactCompiler: true, compiler: { - removeConsole: process.env.NODE_ENV === "production", + // Keep console.error in prod (server-side error visibility — incl. the pg pool + // idle-error handler in src/lib/db.ts); strip the noisier console.* levels. + removeConsole: process.env.NODE_ENV === "production" ? { exclude: ["error"] } : false, }, async redirects() { return [ diff --git a/scripts/branding-check.sh b/scripts/branding-check.sh index 22a2ad4e9..d4e7cebe7 100644 --- a/scripts/branding-check.sh +++ b/scripts/branding-check.sh @@ -1,16 +1,23 @@ #!/usr/bin/env bash # CI gate: zero original-template branding may remain in user-visible code. -# Forbidden strings come from the Phase-2 branding inventory (docs/ai-process/BRAIN.md). +# Forbidden strings come from the Phase-2 branding inventory (docs/ai-process/BRAIN.md) +# plus the dashboard-data cleanup (aiy/aiycap, acme inc/corp/industries, storeframe, +# the nova.robertobh.dev personal-domain leak, sandbox, and the "shadcn ui kit" / +# "shadcn/ui" template copy). # NOTE: README.md and LICENSE are intentionally NOT scanned — MIT attribution to the # original template lives there and is required. "radix-nova" (shadcn style name) and -# bare "shadcn" library references are legitimate and not matched by these patterns. +# bare "shadcn" library references (the `shadcn` npm dep, `shadcn/tailwind.css` import, +# `@shadcn/react` import) are legitimate and are NOT matched: the patterns below only +# catch the more specific "shadcn/ui" and "shadcn ui kit" literals, which were leftover +# template demo copy (now removed). "\bsandbox\b" is word-bounded so it won't fire on +# substrings like "sandboxed"; verified no legitimate standalone "sandbox" exists in src. # # Fails CLOSED: a grep execution error (missing path, bad pattern) fails the build — # it is never reported as "clean". No extension allowlist: every text file under the # scanned paths is checked (-I skips binaries). set -uo pipefail -FORBIDDEN='arhamkhnz|arham|weblabs|studio.admin|studioadmin|next-shadcn-admin-dashboard|next-colocation-template' +FORBIDDEN='arhamkhnz|arham|weblabs|studio.admin|studioadmin|next-shadcn-admin-dashboard|next-colocation-template|aiy cap|aiycap|aiy-cap|hello, aiy|acme\.inc|acme inc|acme corp|acme industries|storeframe|robertobh|\bsandbox\b|shadcn ui kit|shadcn/ui' for path in src package.json; do if [ ! -e "$path" ]; then diff --git a/scripts/seed.ts b/scripts/seed.ts index 49029c63a..637b7214e 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -4,10 +4,16 @@ import { auth } from "../src/lib/auth"; async function main() { const email = "admin@novaanalytics.io"; + // Fail closed: never seed a privileged account with a repo-visible default. + const password = process.env.SEED_PASSWORD; + if (!password) { + throw new Error( + "SEED_PASSWORD is required — refusing to seed with a hardcoded default. " + + "Set it in .env (see .env.example) or the shell before running `npm run seed`.", + ); + } try { - await auth.api.signUpEmail({ - body: { email, password: process.env.SEED_PASSWORD ?? "NovaReview2026!", name: "Nova Admin" }, - }); + await auth.api.signUpEmail({ body: { email, password, name: "Nova Admin" } }); console.log("seed: created", email); } catch (e: unknown) { // Only the "already exists" case is expected; anything else must fail loudly diff --git a/src/app/(main)/dashboard/(legacy)/analytics-v1/_components/analytics-actions-risk-ledger.tsx b/src/app/(main)/dashboard/(legacy)/analytics-v1/_components/analytics-actions-risk-ledger.tsx index 56704f419..ba49e47f4 100644 --- a/src/app/(main)/dashboard/(legacy)/analytics-v1/_components/analytics-actions-risk-ledger.tsx +++ b/src/app/(main)/dashboard/(legacy)/analytics-v1/_components/analytics-actions-risk-ledger.tsx @@ -88,7 +88,7 @@ const LEDGER_ROWS: LedgerRow[] = [ }, { id: 5, - account: "Acme Industries", + account: "Cinder Health", dealId: "OPP-421", stage: "Negotiation", blocker: "Close date overdue by 32 days", diff --git a/src/app/(main)/dashboard/(legacy)/crm-v1/_components/crm.config.ts b/src/app/(main)/dashboard/(legacy)/crm-v1/_components/crm.config.ts index 68b787e64..c3719983e 100644 --- a/src/app/(main)/dashboard/(legacy)/crm-v1/_components/crm.config.ts +++ b/src/app/(main)/dashboard/(legacy)/crm-v1/_components/crm.config.ts @@ -234,8 +234,8 @@ export const recentLeadsData = [ }, { id: "L-1001", - name: "Shadcn", - company: "Shadcn/ui", + name: "Sarah Parker", + company: "Northstar Labs", status: "Qualified", source: "Website", lastActivity: "2h ago", @@ -307,7 +307,7 @@ export const recentLeadsData = [ { id: "L-1015", name: "Toby", - company: "Shadcn UI Kit ", + company: "Cinder Health", status: "Negotiation", source: "Other", lastActivity: "2d ago", diff --git a/src/app/(main)/dashboard/analytics/_components/analytics-kpi-strip.tsx b/src/app/(main)/dashboard/analytics/_components/analytics-kpi-strip.tsx index 13378fbe7..076df266a 100644 --- a/src/app/(main)/dashboard/analytics/_components/analytics-kpi-strip.tsx +++ b/src/app/(main)/dashboard/analytics/_components/analytics-kpi-strip.tsx @@ -1,4 +1,4 @@ -import { ArrowDownRight, ArrowUpRight, Ellipsis } from "lucide-react"; +import { ArrowUpRight, Ellipsis } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; @@ -16,16 +16,16 @@ export function AnalyticsKpiStrip() {
-
213.1k
+
156.2k
- 2.8% + 11.8%
- from 207.3k + from 139.75k last 4 weeks @@ -42,16 +42,16 @@ export function AnalyticsKpiStrip() {
-
248.6k
+
512.9k
- 2.1% + 11.2%
- from 243.5k + from 461.24k last 4 weeks @@ -68,16 +68,16 @@ export function AnalyticsKpiStrip() {
-
547.9k
- - - 3.3% +
1.86M
+ + + 9.4%
- from 566.8k + from 1.70M last 4 weeks @@ -94,16 +94,16 @@ export function AnalyticsKpiStrip() {
-
61.4%
+
62.4%
- 4.2% + 3.1pp
- from 58.9% + from 59.3% last 4 weeks @@ -120,16 +120,16 @@ export function AnalyticsKpiStrip() {
-
8.4%
- - - 5.6% +
4.8%
+ + + 0.4pp
- from 8.9% + from 4.4% last 4 weeks diff --git a/src/app/(main)/dashboard/analytics/_components/realtime-visitors.tsx b/src/app/(main)/dashboard/analytics/_components/realtime-visitors.tsx index c7a9507c8..3dee5254e 100644 --- a/src/app/(main)/dashboard/analytics/_components/realtime-visitors.tsx +++ b/src/app/(main)/dashboard/analytics/_components/realtime-visitors.tsx @@ -7,36 +7,36 @@ import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/componen import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; const realtimeData = [ - { minute: 1, visitors: 0 }, - { minute: 2, visitors: 6 }, - { minute: 3, visitors: 12 }, - { minute: 4, visitors: 20 }, - { minute: 5, visitors: 12 }, - { minute: 6, visitors: 0 }, - { minute: 7, visitors: 6 }, - { minute: 8, visitors: 6 }, + { minute: 1, visitors: 22 }, + { minute: 2, visitors: 28 }, + { minute: 3, visitors: 0 }, + { minute: 4, visitors: 31 }, + { minute: 5, visitors: 24 }, + { minute: 6, visitors: 19 }, + { minute: 7, visitors: 26 }, + { minute: 8, visitors: 33 }, { minute: 9, visitors: 0 }, - { minute: 10, visitors: 4 }, - { minute: 11, visitors: 0 }, - { minute: 12, visitors: 20 }, - { minute: 13, visitors: 15 }, - { minute: 14, visitors: 4 }, - { minute: 15, visitors: 6 }, + { minute: 10, visitors: 21 }, + { minute: 11, visitors: 17 }, + { minute: 12, visitors: 29 }, + { minute: 13, visitors: 25 }, + { minute: 14, visitors: 34 }, + { minute: 15, visitors: 20 }, { minute: 16, visitors: 0 }, - { minute: 17, visitors: 4 }, - { minute: 18, visitors: 12 }, - { minute: 19, visitors: 20 }, - { minute: 20, visitors: 0 }, - { minute: 21, visitors: 4 }, - { minute: 22, visitors: 20 }, - { minute: 23, visitors: 12 }, + { minute: 17, visitors: 23 }, + { minute: 18, visitors: 30 }, + { minute: 19, visitors: 27 }, + { minute: 20, visitors: 14 }, + { minute: 21, visitors: 24 }, + { minute: 22, visitors: 36 }, + { minute: 23, visitors: 22 }, { minute: 24, visitors: 0 }, - { minute: 25, visitors: 6 }, - { minute: 26, visitors: 6 }, - { minute: 27, visitors: 0 }, - { minute: 28, visitors: 20 }, - { minute: 29, visitors: 0 }, - { minute: 30, visitors: 4 }, + { minute: 25, visitors: 26 }, + { minute: 26, visitors: 19 }, + { minute: 27, visitors: 28 }, + { minute: 28, visitors: 32 }, + { minute: 29, visitors: 21 }, + { minute: 30, visitors: 25 }, ]; const chartConfig = { @@ -100,9 +100,9 @@ export function RealtimeVisitors() {
-
- 24 - per minute +
+ 142 + active now · 24 per minute
@@ -115,7 +115,7 @@ export function RealtimeVisitors() { - + } /> diff --git a/src/app/(main)/dashboard/analytics/_components/top-pages.tsx b/src/app/(main)/dashboard/analytics/_components/top-pages.tsx index 9ba962b28..eebccac01 100644 --- a/src/app/(main)/dashboard/analytics/_components/top-pages.tsx +++ b/src/app/(main)/dashboard/analytics/_components/top-pages.tsx @@ -6,9 +6,9 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@ const pages = [ { bounce: "24%", path: "/dashboard", time: "3m 12s", views: "64.2k" }, { bounce: "31%", path: "/pricing", time: "2m 08s", views: "41.8k" }, - { bounce: "18%", path: "/docs/getting-started", time: "4m 44s", views: "28.6k" }, - { bounce: "22%", path: "/blog/analytics-guide", time: "5m 06s", views: "19.3k" }, - { bounce: "42%", path: "/contact", time: "1m 18s", views: "8.9k" }, + { bounce: "18%", path: "/docs/getting-started", time: "2m 44s", views: "28.6k" }, + { bounce: "44%", path: "/signup", time: "1m 05s", views: "22.4k" }, + { bounce: "27%", path: "/integrations", time: "2m 41s", views: "18.9k" }, ]; export function TopPages() { diff --git a/src/app/(main)/dashboard/analytics/_components/top-traffic-sources.tsx b/src/app/(main)/dashboard/analytics/_components/top-traffic-sources.tsx index 38cbdfa0f..bd1b68eff 100644 --- a/src/app/(main)/dashboard/analytics/_components/top-traffic-sources.tsx +++ b/src/app/(main)/dashboard/analytics/_components/top-traffic-sources.tsx @@ -10,7 +10,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; const chartConfig = { visitors: { color: "var(--chart-1)", - label: "Visitors", + label: "Sessions", }, } satisfies ChartConfig; @@ -21,15 +21,15 @@ type TrafficSourceDatum = { }; const sourcesData: TrafficSourceDatum[] = [ - { label: "89.4k", source: "Organic Search", visitors: 89_400 }, - { label: "55.2k", source: "Direct", visitors: 55_200 }, - { label: "38.1k", source: "Social", visitors: 38_100 }, - { label: "30.4k", source: "Referral", visitors: 30_400 }, - { label: "22.7k", source: "Paid", visitors: 22_700 }, + { label: "198.4k", source: "Organic Search", visitors: 198_400 }, + { label: "121.6k", source: "Direct", visitors: 121_600 }, + { label: "82.3k", source: "Social", visitors: 82_300 }, + { label: "64.9k", source: "Referral", visitors: 64_900 }, + { label: "45.7k", source: "Paid", visitors: 45_700 }, ]; const campaignsData: TrafficSourceDatum[] = [ - { label: "16.8k", source: "Spring Launch", visitors: 16_800 }, + { label: "15.8k", source: "Spring Launch", visitors: 15_800 }, { label: "12.0k", source: "Newsletter", visitors: 12_000 }, { label: "7.7k", source: "Retargeting", visitors: 7700 }, { label: "5.9k", source: "Brand Search", visitors: 5900 }, diff --git a/src/app/(main)/dashboard/analytics/_components/traffic-quality.tsx b/src/app/(main)/dashboard/analytics/_components/traffic-quality.tsx index 09d229bc2..7986e89f9 100644 --- a/src/app/(main)/dashboard/analytics/_components/traffic-quality.tsx +++ b/src/app/(main)/dashboard/analytics/_components/traffic-quality.tsx @@ -96,11 +96,11 @@ const qualitySeries = [ const chartConfig = { actualQuality: { color: "var(--chart-3)", - label: "Actual quality", + label: "Actual", }, baselineQuality: { color: "var(--muted-foreground)", - label: "Baseline quality", + label: "Baseline", }, } satisfies ChartConfig; @@ -121,7 +121,7 @@ export function TrafficQuality() { return ( - Traffic Quality + Engagement Quality @@ -152,7 +152,7 @@ export function TrafficQuality() { /> "Traffic quality"} />} + content={ "Engagement quality"} />} />
-

Hello, Aiy

+

Welcome back, admin

Monitor traffic, engagement, and conversion performance in one view.

diff --git a/src/app/(main)/dashboard/default/_components/metric-cards.tsx b/src/app/(main)/dashboard/default/_components/metric-cards.tsx index e2a969f06..0b8c02a3b 100644 --- a/src/app/(main)/dashboard/default/_components/metric-cards.tsx +++ b/src/app/(main)/dashboard/default/_components/metric-cards.tsx @@ -1,4 +1,4 @@ -import { DollarSign, TrendingDown, TrendingUp, UserPlus, Users, Waves } from "lucide-react"; +import { Activity, Target, TrendingDown, TrendingUp, UserPlus, Users } from "lucide-react"; import { Badge } from "@/components/ui/badge"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; @@ -10,20 +10,20 @@ export function MetricCards() {
- +
- Total Revenue + Monthly Active Users
-
$1,250.00
+
128,420
+12.5%
-

Visitors for the last 6 months

+

Unique active users, last 30 days

@@ -34,11 +34,11 @@ export function MetricCards() {
- New Customers + New Signups
-
1,234
+
3,240
-20% @@ -52,17 +52,17 @@ export function MetricCards() {
- +
- Active Accounts + Sessions
-
45,678
+
512,900
- +12.5% + +11.2%

Engagement exceeds targets

@@ -73,20 +73,20 @@ export function MetricCards() {
- +
- Growth Rate + Conversion Rate
-
4.5%
+
4.8%
- +4.5% + +0.4pp
-

Meets growth projections

+

Meets conversion targets

diff --git a/src/app/(main)/dashboard/default/_components/performance-overview.tsx b/src/app/(main)/dashboard/default/_components/performance-overview.tsx index 385b7959e..c6b0efd26 100644 --- a/src/app/(main)/dashboard/default/_components/performance-overview.tsx +++ b/src/app/(main)/dashboard/default/_components/performance-overview.tsx @@ -24,186 +24,186 @@ import { } from "@/components/ui/select"; const chartValues = [ - { newCustomers: 23840, activeAccounts: 6630, returningUsers: 4880 }, - { newCustomers: 11508, activeAccounts: 6468, returningUsers: 4643 }, - { newCustomers: 9975, activeAccounts: 6117, returningUsers: 4573 }, - { newCustomers: 10310, activeAccounts: 6152, returningUsers: 4657 }, - { newCustomers: 12244, activeAccounts: 6473, returningUsers: 4657 }, - { newCustomers: 11476, activeAccounts: 6347, returningUsers: 4533 }, - { newCustomers: 9944, activeAccounts: 6250, returningUsers: 4588 }, - { newCustomers: 10259, activeAccounts: 6417, returningUsers: 4763 }, - { newCustomers: 9698, activeAccounts: 6256, returningUsers: 4710 }, - { newCustomers: 8435, activeAccounts: 6161, returningUsers: 4544 }, - { newCustomers: 8885, activeAccounts: 6510, returningUsers: 4595 }, - { newCustomers: 13596, activeAccounts: 6497, returningUsers: 4712 }, - { newCustomers: 6198, activeAccounts: 6165, returningUsers: 4654 }, - { newCustomers: 6546, activeAccounts: 6295, returningUsers: 4622 }, - { newCustomers: 8306, activeAccounts: 6444, returningUsers: 4732 }, - { newCustomers: 7445, activeAccounts: 6283, returningUsers: 4711 }, - { newCustomers: 6646, activeAccounts: 6409, returningUsers: 4551 }, - { newCustomers: 8146, activeAccounts: 6520, returningUsers: 4593 }, - { newCustomers: 8754, activeAccounts: 6197, returningUsers: 4776 }, - { newCustomers: 8715, activeAccounts: 6205, returningUsers: 4745 }, - { newCustomers: 10154, activeAccounts: 6557, returningUsers: 4600 }, - { newCustomers: 10337, activeAccounts: 6438, returningUsers: 4641 }, - { newCustomers: 14212, activeAccounts: 6251, returningUsers: 4715 }, - { newCustomers: 18873, activeAccounts: 6557, returningUsers: 4633 }, - { newCustomers: 11558, activeAccounts: 6338, returningUsers: 4626 }, - { newCustomers: 9951, activeAccounts: 6218, returningUsers: 4769 }, - { newCustomers: 8716, activeAccounts: 6518, returningUsers: 4749 }, - { newCustomers: 9690, activeAccounts: 6520, returningUsers: 4565 }, - { newCustomers: 9423, activeAccounts: 6157, returningUsers: 4581 }, - { newCustomers: 8563, activeAccounts: 6268, returningUsers: 4839 }, - { newCustomers: 9255, activeAccounts: 6489, returningUsers: 4730 }, - { newCustomers: 22106, activeAccounts: 6317, returningUsers: 4619 }, - { newCustomers: 7765, activeAccounts: 6346, returningUsers: 4670 }, - { newCustomers: 14487, activeAccounts: 6470, returningUsers: 4708 }, - { newCustomers: 10830, activeAccounts: 6194, returningUsers: 4589 }, - { newCustomers: 9486, activeAccounts: 6172, returningUsers: 4584 }, - { newCustomers: 9200, activeAccounts: 6529, returningUsers: 4754 }, - { newCustomers: 11020, activeAccounts: 6417, returningUsers: 4751 }, - { newCustomers: 11085, activeAccounts: 6153, returningUsers: 4565 }, - { newCustomers: 10372, activeAccounts: 6317, returningUsers: 4558 }, - { newCustomers: 10936, activeAccounts: 6323, returningUsers: 4692 }, - { newCustomers: 10196, activeAccounts: 6436, returningUsers: 4665 }, - { newCustomers: 8744, activeAccounts: 6407, returningUsers: 4589 }, - { newCustomers: 9592, activeAccounts: 6429, returningUsers: 4670 }, - { newCustomers: 14952, activeAccounts: 6061, returningUsers: 4691 }, - { newCustomers: 7242, activeAccounts: 6150, returningUsers: 4532 }, - { newCustomers: 15297, activeAccounts: 6568, returningUsers: 4511 }, - { newCustomers: 7844, activeAccounts: 6248, returningUsers: 4870 }, - { newCustomers: 7336, activeAccounts: 6181, returningUsers: 4708 }, - { newCustomers: 6548, activeAccounts: 6322, returningUsers: 4542 }, - { newCustomers: 7496, activeAccounts: 6112, returningUsers: 4530 }, - { newCustomers: 7529, activeAccounts: 6059, returningUsers: 4625 }, - { newCustomers: 7369, activeAccounts: 6401, returningUsers: 4570 }, - { newCustomers: 9434, activeAccounts: 6303, returningUsers: 4514 }, - { newCustomers: 10387, activeAccounts: 5984, returningUsers: 4633 }, - { newCustomers: 14173, activeAccounts: 6146, returningUsers: 4660 }, - { newCustomers: 9635, activeAccounts: 6239, returningUsers: 4478 }, - { newCustomers: 11690, activeAccounts: 6070, returningUsers: 4431 }, - { newCustomers: 11148, activeAccounts: 6221, returningUsers: 4688 }, - { newCustomers: 10205, activeAccounts: 6270, returningUsers: 4626 }, - { newCustomers: 10773, activeAccounts: 5926, returningUsers: 4494 }, - { newCustomers: 10134, activeAccounts: 5995, returningUsers: 4500 }, - { newCustomers: 22444, activeAccounts: 6315, returningUsers: 4566 }, - { newCustomers: 10213, activeAccounts: 6134, returningUsers: 4472 }, - { newCustomers: 9788, activeAccounts: 5983, returningUsers: 4416 }, - { newCustomers: 7646, activeAccounts: 6146, returningUsers: 4566 }, - { newCustomers: 13396, activeAccounts: 6020, returningUsers: 4615 }, - { newCustomers: 9889, activeAccounts: 5938, returningUsers: 4434 }, - { newCustomers: 8999, activeAccounts: 6246, returningUsers: 4370 }, - { newCustomers: 17176, activeAccounts: 6314, returningUsers: 4508 }, - { newCustomers: 9602, activeAccounts: 5827, returningUsers: 4527 }, - { newCustomers: 9663, activeAccounts: 5992, returningUsers: 4428 }, - { newCustomers: 9542, activeAccounts: 6167, returningUsers: 4471 }, - { newCustomers: 10921, activeAccounts: 5981, returningUsers: 4530 }, - { newCustomers: 10557, activeAccounts: 6051, returningUsers: 4398 }, - { newCustomers: 8774, activeAccounts: 6138, returningUsers: 4326 }, - { newCustomers: 9607, activeAccounts: 5843, returningUsers: 4489 }, - { newCustomers: 15883, activeAccounts: 5891, returningUsers: 4563 }, - { newCustomers: 8805, activeAccounts: 6235, returningUsers: 4404 }, - { newCustomers: 7551, activeAccounts: 6065, returningUsers: 4342 }, - { newCustomers: 8177, activeAccounts: 5847, returningUsers: 4449 }, - { newCustomers: 7534, activeAccounts: 6037, returningUsers: 4440 }, - { newCustomers: 6902, activeAccounts: 6264, returningUsers: 4360 }, - { newCustomers: 7832, activeAccounts: 5895, returningUsers: 4446 }, - { newCustomers: 7311, activeAccounts: 6155, returningUsers: 4520 }, - { newCustomers: 6245, activeAccounts: 6119, returningUsers: 4369 }, - { newCustomers: 8128, activeAccounts: 5772, returningUsers: 4274 }, - { newCustomers: 9848, activeAccounts: 5936, returningUsers: 4518 }, - { newCustomers: 13995, activeAccounts: 6184, returningUsers: 4515 }, - { newCustomers: 8963, activeAccounts: 5986, returningUsers: 4390 }, - { newCustomers: 10872, activeAccounts: 5976, returningUsers: 4353 }, - { newCustomers: 11036, activeAccounts: 6106, returningUsers: 4440 }, - { newCustomers: 19721, activeAccounts: 6023, returningUsers: 4393 }, - { newCustomers: 25079, activeAccounts: 5905, returningUsers: 4310 }, - { newCustomers: 11054, activeAccounts: 6252, returningUsers: 4611 }, - { newCustomers: 9769, activeAccounts: 6101, returningUsers: 4534 }, - { newCustomers: 10977, activeAccounts: 5835, returningUsers: 4386 }, - { newCustomers: 11193, activeAccounts: 6048, returningUsers: 4276 }, - { newCustomers: 8766, activeAccounts: 6109, returningUsers: 4408 }, - { newCustomers: 13370, activeAccounts: 5970, returningUsers: 4485 }, - { newCustomers: 9279, activeAccounts: 6168, returningUsers: 4390 }, - { newCustomers: 8581, activeAccounts: 6176, returningUsers: 4395 }, - { newCustomers: 8002, activeAccounts: 5852, returningUsers: 4484 }, - { newCustomers: 8811, activeAccounts: 6005, returningUsers: 4401 }, - { newCustomers: 8261, activeAccounts: 6306, returningUsers: 4298 }, - { newCustomers: 7857, activeAccounts: 6100, returningUsers: 4434 }, - { newCustomers: 9836, activeAccounts: 6013, returningUsers: 4564 }, - { newCustomers: 10372, activeAccounts: 6187, returningUsers: 4438 }, - { newCustomers: 8604, activeAccounts: 6043, returningUsers: 4334 }, - { newCustomers: 9027, activeAccounts: 6029, returningUsers: 4439 }, - { newCustomers: 15778, activeAccounts: 6355, returningUsers: 4486 }, - { newCustomers: 9732, activeAccounts: 6230, returningUsers: 4404 }, - { newCustomers: 8909, activeAccounts: 5932, returningUsers: 4454 }, - { newCustomers: 9279, activeAccounts: 6160, returningUsers: 4564 }, - { newCustomers: 8207, activeAccounts: 6302, returningUsers: 4462 }, - { newCustomers: 16222, activeAccounts: 6269, returningUsers: 4333 }, - { newCustomers: 8695, activeAccounts: 6251, returningUsers: 4550 }, - { newCustomers: 8282, activeAccounts: 6307, returningUsers: 4601 }, - { newCustomers: 6307, activeAccounts: 6020, returningUsers: 4506 }, - { newCustomers: 7191, activeAccounts: 6149, returningUsers: 4427 }, - { newCustomers: 9014, activeAccounts: 6475, returningUsers: 4518 }, - { newCustomers: 13671, activeAccounts: 6264, returningUsers: 4525 }, - { newCustomers: 8484, activeAccounts: 6106, returningUsers: 4432 }, - { newCustomers: 9909, activeAccounts: 6576, returningUsers: 4514 }, - { newCustomers: 23158, activeAccounts: 6252, returningUsers: 4654 }, - { newCustomers: 10314, activeAccounts: 6190, returningUsers: 4554 }, - { newCustomers: 12158, activeAccounts: 6470, returningUsers: 4409 }, - { newCustomers: 11810, activeAccounts: 6376, returningUsers: 4512 }, - { newCustomers: 10126, activeAccounts: 6062, returningUsers: 4640 }, - { newCustomers: 11137, activeAccounts: 6289, returningUsers: 4566 }, - { newCustomers: 12120, activeAccounts: 6494, returningUsers: 4528 }, - { newCustomers: 10293, activeAccounts: 6283, returningUsers: 4624 }, - { newCustomers: 14431, activeAccounts: 6321, returningUsers: 4593 }, - { newCustomers: 9549, activeAccounts: 6423, returningUsers: 4472 }, - { newCustomers: 8487, activeAccounts: 6186, returningUsers: 4563 }, - { newCustomers: 7935, activeAccounts: 6272, returningUsers: 4730 }, - { newCustomers: 8825, activeAccounts: 6598, returningUsers: 4648 }, - { newCustomers: 7814, activeAccounts: 6386, returningUsers: 4505 }, - { newCustomers: 15233, activeAccounts: 6306, returningUsers: 4583 }, - { newCustomers: 8133, activeAccounts: 6403, returningUsers: 4676 }, - { newCustomers: 9451, activeAccounts: 6413, returningUsers: 4604 }, - { newCustomers: 8181, activeAccounts: 6294, returningUsers: 4786 }, - { newCustomers: 8262, activeAccounts: 6508, returningUsers: 4725 }, - { newCustomers: 14874, activeAccounts: 6452, returningUsers: 4673 }, - { newCustomers: 9428, activeAccounts: 6139, returningUsers: 4520 }, - { newCustomers: 9471, activeAccounts: 6348, returningUsers: 4687 }, - { newCustomers: 10439, activeAccounts: 6594, returningUsers: 4771 }, - { newCustomers: 9282, activeAccounts: 6348, returningUsers: 4712 }, - { newCustomers: 8014, activeAccounts: 6303, returningUsers: 4592 }, - { newCustomers: 9456, activeAccounts: 6451, returningUsers: 4658 }, - { newCustomers: 9750, activeAccounts: 6270, returningUsers: 4707 }, - { newCustomers: 7623, activeAccounts: 6301, returningUsers: 4614 }, - { newCustomers: 7441, activeAccounts: 6602, returningUsers: 4641 }, - { newCustomers: 8613, activeAccounts: 6402, returningUsers: 4792 }, - { newCustomers: 13354, activeAccounts: 6136, returningUsers: 4740 }, - { newCustomers: 21487, activeAccounts: 6389, returningUsers: 4564 }, - { newCustomers: 9481, activeAccounts: 6465, returningUsers: 4616 }, - { newCustomers: 8932, activeAccounts: 6287, returningUsers: 4772 }, - { newCustomers: 8855, activeAccounts: 6427, returningUsers: 4727 }, - { newCustomers: 11234, activeAccounts: 6417, returningUsers: 4642 }, - { newCustomers: 11850, activeAccounts: 6123, returningUsers: 4717 }, - { newCustomers: 19042, activeAccounts: 6441, returningUsers: 4728 }, - { newCustomers: 10788, activeAccounts: 6571, returningUsers: 4599 }, - { newCustomers: 12062, activeAccounts: 6301, returningUsers: 4630 }, - { newCustomers: 11104, activeAccounts: 6442, returningUsers: 4805 }, - { newCustomers: 15697, activeAccounts: 6375, returningUsers: 4769 }, - { newCustomers: 10622, activeAccounts: 6259, returningUsers: 4593 }, - { newCustomers: 8993, activeAccounts: 6227, returningUsers: 4621 }, - { newCustomers: 8066, activeAccounts: 6490, returningUsers: 4741 }, - { newCustomers: 9249, activeAccounts: 6317, returningUsers: 4690 }, - { newCustomers: 8439, activeAccounts: 6027, returningUsers: 4640 }, - { newCustomers: 6207, activeAccounts: 6287, returningUsers: 4742 }, - { newCustomers: 6868, activeAccounts: 6422, returningUsers: 4734 }, - { newCustomers: 8206, activeAccounts: 6190, returningUsers: 4568 }, - { newCustomers: 7467, activeAccounts: 6256, returningUsers: 4673 }, - { newCustomers: 7595, activeAccounts: 6306, returningUsers: 4764 }, - { newCustomers: 13895, activeAccounts: 6050, returningUsers: 4749 }, - { newCustomers: 8293, activeAccounts: 6186, returningUsers: 4595 }, - { newCustomers: 8744, activeAccounts: 6464, returningUsers: 4615 }, - { newCustomers: 10727, activeAccounts: 6189, returningUsers: 4693 }, + { newCustomers: 38998, activeAccounts: 6744, returningUsers: 4964 }, + { newCustomers: 18825, activeAccounts: 6579, returningUsers: 4723 }, + { newCustomers: 16318, activeAccounts: 6222, returningUsers: 4651 }, + { newCustomers: 16866, activeAccounts: 6257, returningUsers: 4737 }, + { newCustomers: 20029, activeAccounts: 6584, returningUsers: 4737 }, + { newCustomers: 18773, activeAccounts: 6456, returningUsers: 4611 }, + { newCustomers: 16267, activeAccounts: 6357, returningUsers: 4667 }, + { newCustomers: 16782, activeAccounts: 6527, returningUsers: 4845 }, + { newCustomers: 15864, activeAccounts: 6363, returningUsers: 4791 }, + { newCustomers: 13798, activeAccounts: 6267, returningUsers: 4622 }, + { newCustomers: 14534, activeAccounts: 6622, returningUsers: 4674 }, + { newCustomers: 22241, activeAccounts: 6608, returningUsers: 4793 }, + { newCustomers: 10139, activeAccounts: 6271, returningUsers: 4734 }, + { newCustomers: 10708, activeAccounts: 6403, returningUsers: 4701 }, + { newCustomers: 13587, activeAccounts: 6554, returningUsers: 4813 }, + { newCustomers: 12179, activeAccounts: 6391, returningUsers: 4792 }, + { newCustomers: 10872, activeAccounts: 6519, returningUsers: 4629 }, + { newCustomers: 13326, activeAccounts: 6632, returningUsers: 4672 }, + { newCustomers: 14320, activeAccounts: 6303, returningUsers: 4858 }, + { newCustomers: 14256, activeAccounts: 6311, returningUsers: 4826 }, + { newCustomers: 16610, activeAccounts: 6669, returningUsers: 4679 }, + { newCustomers: 16910, activeAccounts: 6548, returningUsers: 4721 }, + { newCustomers: 23249, activeAccounts: 6358, returningUsers: 4796 }, + { newCustomers: 30873, activeAccounts: 6669, returningUsers: 4712 }, + { newCustomers: 18907, activeAccounts: 6447, returningUsers: 4705 }, + { newCustomers: 16278, activeAccounts: 6325, returningUsers: 4851 }, + { newCustomers: 14258, activeAccounts: 6630, returningUsers: 4830 }, + { newCustomers: 15851, activeAccounts: 6632, returningUsers: 4643 }, + { newCustomers: 15415, activeAccounts: 6263, returningUsers: 4660 }, + { newCustomers: 14008, activeAccounts: 6375, returningUsers: 4922 }, + { newCustomers: 15140, activeAccounts: 6600, returningUsers: 4811 }, + { newCustomers: 36162, activeAccounts: 6425, returningUsers: 4698 }, + { newCustomers: 12702, activeAccounts: 6455, returningUsers: 4750 }, + { newCustomers: 23698, activeAccounts: 6581, returningUsers: 4789 }, + { newCustomers: 17716, activeAccounts: 6300, returningUsers: 4668 }, + { newCustomers: 15518, activeAccounts: 6278, returningUsers: 4663 }, + { newCustomers: 15050, activeAccounts: 6641, returningUsers: 4836 }, + { newCustomers: 18027, activeAccounts: 6527, returningUsers: 4832 }, + { newCustomers: 18133, activeAccounts: 6258, returningUsers: 4643 }, + { newCustomers: 16967, activeAccounts: 6425, returningUsers: 4636 }, + { newCustomers: 17890, activeAccounts: 6431, returningUsers: 4772 }, + { newCustomers: 16679, activeAccounts: 6546, returningUsers: 4745 }, + { newCustomers: 14304, activeAccounts: 6517, returningUsers: 4668 }, + { newCustomers: 15691, activeAccounts: 6539, returningUsers: 4750 }, + { newCustomers: 24459, activeAccounts: 6165, returningUsers: 4771 }, + { newCustomers: 11847, activeAccounts: 6255, returningUsers: 4610 }, + { newCustomers: 25023, activeAccounts: 6681, returningUsers: 4588 }, + { newCustomers: 12832, activeAccounts: 6355, returningUsers: 4953 }, + { newCustomers: 12001, activeAccounts: 6287, returningUsers: 4789 }, + { newCustomers: 10711, activeAccounts: 6430, returningUsers: 4620 }, + { newCustomers: 12262, activeAccounts: 6217, returningUsers: 4608 }, + { newCustomers: 12316, activeAccounts: 6163, returningUsers: 4704 }, + { newCustomers: 12055, activeAccounts: 6511, returningUsers: 4648 }, + { newCustomers: 15433, activeAccounts: 6411, returningUsers: 4591 }, + { newCustomers: 16991, activeAccounts: 6087, returningUsers: 4712 }, + { newCustomers: 23185, activeAccounts: 6251, returningUsers: 4740 }, + { newCustomers: 15761, activeAccounts: 6346, returningUsers: 4555 }, + { newCustomers: 19123, activeAccounts: 6174, returningUsers: 4507 }, + { newCustomers: 18236, activeAccounts: 6328, returningUsers: 4768 }, + { newCustomers: 16694, activeAccounts: 6377, returningUsers: 4705 }, + { newCustomers: 17623, activeAccounts: 6028, returningUsers: 4571 }, + { newCustomers: 16578, activeAccounts: 6098, returningUsers: 4577 }, + { newCustomers: 36715, activeAccounts: 6423, returningUsers: 4644 }, + { newCustomers: 16707, activeAccounts: 6239, returningUsers: 4549 }, + { newCustomers: 16012, activeAccounts: 6086, returningUsers: 4492 }, + { newCustomers: 12508, activeAccounts: 6251, returningUsers: 4644 }, + { newCustomers: 21914, activeAccounts: 6123, returningUsers: 4694 }, + { newCustomers: 16177, activeAccounts: 6040, returningUsers: 4510 }, + { newCustomers: 14721, activeAccounts: 6353, returningUsers: 4445 }, + { newCustomers: 28097, activeAccounts: 6422, returningUsers: 4585 }, + { newCustomers: 15707, activeAccounts: 5927, returningUsers: 4605 }, + { newCustomers: 15807, activeAccounts: 6095, returningUsers: 4504 }, + { newCustomers: 15609, activeAccounts: 6273, returningUsers: 4548 }, + { newCustomers: 17865, activeAccounts: 6084, returningUsers: 4608 }, + { newCustomers: 17270, activeAccounts: 6155, returningUsers: 4473 }, + { newCustomers: 14353, activeAccounts: 6243, returningUsers: 4400 }, + { newCustomers: 15716, activeAccounts: 5943, returningUsers: 4566 }, + { newCustomers: 25982, activeAccounts: 5992, returningUsers: 4641 }, + { newCustomers: 14404, activeAccounts: 6342, returningUsers: 4480 }, + { newCustomers: 12352, activeAccounts: 6169, returningUsers: 4416 }, + { newCustomers: 13376, activeAccounts: 5947, returningUsers: 4525 }, + { newCustomers: 12324, activeAccounts: 6141, returningUsers: 4516 }, + { newCustomers: 11291, activeAccounts: 6371, returningUsers: 4435 }, + { newCustomers: 12812, activeAccounts: 5996, returningUsers: 4522 }, + { newCustomers: 11960, activeAccounts: 6261, returningUsers: 4597 }, + { newCustomers: 10216, activeAccounts: 6224, returningUsers: 4444 }, + { newCustomers: 13296, activeAccounts: 5871, returningUsers: 4347 }, + { newCustomers: 16110, activeAccounts: 6038, returningUsers: 4595 }, + { newCustomers: 22894, activeAccounts: 6290, returningUsers: 4592 }, + { newCustomers: 14662, activeAccounts: 6089, returningUsers: 4465 }, + { newCustomers: 17785, activeAccounts: 6078, returningUsers: 4428 }, + { newCustomers: 18053, activeAccounts: 6211, returningUsers: 4516 }, + { newCustomers: 32260, activeAccounts: 6126, returningUsers: 4468 }, + { newCustomers: 41025, activeAccounts: 6006, returningUsers: 4384 }, + { newCustomers: 18083, activeAccounts: 6359, returningUsers: 4690 }, + { newCustomers: 15981, activeAccounts: 6206, returningUsers: 4612 }, + { newCustomers: 17957, activeAccounts: 5935, returningUsers: 4461 }, + { newCustomers: 18310, activeAccounts: 6152, returningUsers: 4349 }, + { newCustomers: 14340, activeAccounts: 6214, returningUsers: 4484 }, + { newCustomers: 21871, activeAccounts: 6072, returningUsers: 4562 }, + { newCustomers: 15179, activeAccounts: 6274, returningUsers: 4465 }, + { newCustomers: 14037, activeAccounts: 6282, returningUsers: 4470 }, + { newCustomers: 13090, activeAccounts: 5952, returningUsers: 4561 }, + { newCustomers: 14413, activeAccounts: 6108, returningUsers: 4476 }, + { newCustomers: 13514, activeAccounts: 6414, returningUsers: 4372 }, + { newCustomers: 12853, activeAccounts: 6205, returningUsers: 4510 }, + { newCustomers: 16090, activeAccounts: 6116, returningUsers: 4642 }, + { newCustomers: 16967, activeAccounts: 6293, returningUsers: 4514 }, + { newCustomers: 14075, activeAccounts: 6147, returningUsers: 4408 }, + { newCustomers: 14767, activeAccounts: 6132, returningUsers: 4515 }, + { newCustomers: 25810, activeAccounts: 6464, returningUsers: 4563 }, + { newCustomers: 15920, activeAccounts: 6337, returningUsers: 4480 }, + { newCustomers: 14574, activeAccounts: 6034, returningUsers: 4530 }, + { newCustomers: 15179, activeAccounts: 6266, returningUsers: 4642 }, + { newCustomers: 13425, activeAccounts: 6410, returningUsers: 4538 }, + { newCustomers: 26537, activeAccounts: 6376, returningUsers: 4407 }, + { newCustomers: 14224, activeAccounts: 6358, returningUsers: 4628 }, + { newCustomers: 13548, activeAccounts: 6415, returningUsers: 4680 }, + { newCustomers: 10317, activeAccounts: 6123, returningUsers: 4583 }, + { newCustomers: 11763, activeAccounts: 6254, returningUsers: 4503 }, + { newCustomers: 14745, activeAccounts: 6586, returningUsers: 4595 }, + { newCustomers: 22364, activeAccounts: 6371, returningUsers: 4603 }, + { newCustomers: 13878, activeAccounts: 6211, returningUsers: 4508 }, + { newCustomers: 16210, activeAccounts: 6689, returningUsers: 4591 }, + { newCustomers: 37883, activeAccounts: 6359, returningUsers: 4734 }, + { newCustomers: 16872, activeAccounts: 6296, returningUsers: 4632 }, + { newCustomers: 19889, activeAccounts: 6581, returningUsers: 4485 }, + { newCustomers: 19319, activeAccounts: 6485, returningUsers: 4589 }, + { newCustomers: 16565, activeAccounts: 6166, returningUsers: 4720 }, + { newCustomers: 18218, activeAccounts: 6397, returningUsers: 4644 }, + { newCustomers: 19826, activeAccounts: 6605, returningUsers: 4606 }, + { newCustomers: 16838, activeAccounts: 6391, returningUsers: 4703 }, + { newCustomers: 23607, activeAccounts: 6429, returningUsers: 4672 }, + { newCustomers: 15621, activeAccounts: 6533, returningUsers: 4549 }, + { newCustomers: 13883, activeAccounts: 6292, returningUsers: 4641 }, + { newCustomers: 12980, activeAccounts: 6380, returningUsers: 4811 }, + { newCustomers: 14436, activeAccounts: 6711, returningUsers: 4728 }, + { newCustomers: 12782, activeAccounts: 6495, returningUsers: 4582 }, + { newCustomers: 24919, activeAccounts: 6414, returningUsers: 4662 }, + { newCustomers: 13304, activeAccounts: 6513, returningUsers: 4756 }, + { newCustomers: 15460, activeAccounts: 6523, returningUsers: 4683 }, + { newCustomers: 13383, activeAccounts: 6402, returningUsers: 4868 }, + { newCustomers: 13515, activeAccounts: 6620, returningUsers: 4806 }, + { newCustomers: 24331, activeAccounts: 6563, returningUsers: 4753 }, + { newCustomers: 15423, activeAccounts: 6244, returningUsers: 4597 }, + { newCustomers: 15493, activeAccounts: 6457, returningUsers: 4767 }, + { newCustomers: 17077, activeAccounts: 6707, returningUsers: 4853 }, + { newCustomers: 15184, activeAccounts: 6457, returningUsers: 4793 }, + { newCustomers: 13110, activeAccounts: 6411, returningUsers: 4671 }, + { newCustomers: 15469, activeAccounts: 6562, returningUsers: 4738 }, + { newCustomers: 15949, activeAccounts: 6377, returningUsers: 4788 }, + { newCustomers: 12470, activeAccounts: 6409, returningUsers: 4693 }, + { newCustomers: 12172, activeAccounts: 6715, returningUsers: 4721 }, + { newCustomers: 14089, activeAccounts: 6512, returningUsers: 4874 }, + { newCustomers: 21845, activeAccounts: 6241, returningUsers: 4821 }, + { newCustomers: 35149, activeAccounts: 6499, returningUsers: 4642 }, + { newCustomers: 15509, activeAccounts: 6576, returningUsers: 4695 }, + { newCustomers: 14611, activeAccounts: 6395, returningUsers: 4854 }, + { newCustomers: 14485, activeAccounts: 6537, returningUsers: 4808 }, + { newCustomers: 18377, activeAccounts: 6527, returningUsers: 4722 }, + { newCustomers: 19385, activeAccounts: 6228, returningUsers: 4798 }, + { newCustomers: 31150, activeAccounts: 6551, returningUsers: 4809 }, + { newCustomers: 17647, activeAccounts: 6684, returningUsers: 4678 }, + { newCustomers: 19732, activeAccounts: 6409, returningUsers: 4709 }, + { newCustomers: 18164, activeAccounts: 6552, returningUsers: 4887 }, + { newCustomers: 25678, activeAccounts: 6484, returningUsers: 4851 }, + { newCustomers: 17376, activeAccounts: 6366, returningUsers: 4672 }, + { newCustomers: 14711, activeAccounts: 6334, returningUsers: 4700 }, + { newCustomers: 13195, activeAccounts: 6601, returningUsers: 4822 }, + { newCustomers: 15130, activeAccounts: 6425, returningUsers: 4770 }, + { newCustomers: 13805, activeAccounts: 6130, returningUsers: 4720 }, + { newCustomers: 10154, activeAccounts: 6395, returningUsers: 4823 }, + { newCustomers: 11235, activeAccounts: 6532, returningUsers: 4815 }, + { newCustomers: 13424, activeAccounts: 6296, returningUsers: 4646 }, + { newCustomers: 12215, activeAccounts: 6363, returningUsers: 4753 }, + { newCustomers: 12424, activeAccounts: 6414, returningUsers: 4846 }, + { newCustomers: 22730, activeAccounts: 6154, returningUsers: 4830 }, + { newCustomers: 13566, activeAccounts: 6292, returningUsers: 4674 }, + { newCustomers: 14304, activeAccounts: 6575, returningUsers: 4694 }, + { newCustomers: 17548, activeAccounts: 6295, returningUsers: 4773 }, ]; const endDate = endOfToday(); @@ -216,11 +216,11 @@ const chartData = chartValues.map((point, index) => ({ const chartConfig = { newCustomers: { - label: "New Customers", + label: "Sessions", color: "var(--chart-1)", }, activeAccounts: { - label: "Active Accounts", + label: "Active Users", color: "var(--chart-2)", }, returningUsers: { @@ -233,10 +233,10 @@ export function PerformanceOverview() { return ( - Customer Activity + User Activity - Customer activity for the last 3 months - Last 3 months + Sessions, active and returning users - last 90 days + Last 90 days { table.getColumn("search")?.setFilterValue(event.target.value || undefined); diff --git a/src/app/(main)/dashboard/default/_components/subscriber-overview.tsx b/src/app/(main)/dashboard/default/_components/subscriber-overview.tsx index fe8d047b2..2c327e1c0 100644 --- a/src/app/(main)/dashboard/default/_components/subscriber-overview.tsx +++ b/src/app/(main)/dashboard/default/_components/subscriber-overview.tsx @@ -15,8 +15,8 @@ export function SubscriberOverview() { return ( - 18,426 Customers - Recent customer records with plan, billing, status, and signup activity. + 128,420 Active Users + Recent account records with plan, billing, status, and first-seen activity. +
- Shortcuts + Quick actions
diff --git a/src/app/(main)/dashboard/finance/_components/transactions-overview-card.tsx b/src/app/(main)/dashboard/finance/_components/transactions-overview-card.tsx index 2518044d1..8dbfa4166 100644 --- a/src/app/(main)/dashboard/finance/_components/transactions-overview-card.tsx +++ b/src/app/(main)/dashboard/finance/_components/transactions-overview-card.tsx @@ -5,42 +5,41 @@ import { CartesianGrid, Line, LineChart, XAxis, YAxis } from "recharts"; import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { type ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; -import { formatCurrency } from "@/lib/utils"; const DAY_MS = 24 * 60 * 60 * 1000; const weekStart = Date.UTC(2026, 0, 5); const chartData = [ - { date: "2026-01-05T02:24:00Z", expense: 23, income: 38 }, - { date: "2026-01-05T08:24:00Z", expense: 32 }, - { date: "2026-01-05T14:52:48Z", expense: 26 }, - { date: "2026-01-05T21:07:12Z", expense: 39 }, - { date: "2026-01-06T03:36:00Z", expense: 37 }, - { date: "2026-01-06T10:04:48Z", expense: 52 }, - { date: "2026-01-06T16:19:12Z", expense: 15 }, - { date: "2026-01-06T22:04:48Z", expense: 36 }, - { date: "2026-01-07T03:50:24Z", expense: 31 }, - { date: "2026-01-07T09:36:00Z", expense: 45, income: 44 }, - { date: "2026-01-07T15:21:36Z", expense: 53 }, - { date: "2026-01-07T21:07:12Z", expense: 40 }, - { date: "2026-01-08T02:52:48Z", expense: 26 }, - { date: "2026-01-08T08:24:00Z", expense: 42 }, - { date: "2026-01-08T13:55:12Z", expense: 47 }, - { date: "2026-01-08T19:40:48Z", expense: 50 }, - { date: "2026-01-09T01:12:00Z", expense: 34 }, - { date: "2026-01-09T06:43:12Z", expense: 53 }, - { date: "2026-01-09T12:28:48Z", expense: 44 }, - { date: "2026-01-09T18:00:00Z", expense: 70 }, - { date: "2026-01-09T23:31:12Z", expense: 50 }, - { date: "2026-01-10T04:48:00Z", expense: 51 }, - { date: "2026-01-10T10:04:48Z", expense: 34, income: 54 }, - { date: "2026-01-10T15:21:36Z", expense: 39 }, - { date: "2026-01-10T20:38:24Z", expense: 30 }, - { date: "2026-01-11T01:55:12Z", expense: 50 }, - { date: "2026-01-11T07:12:00Z", expense: 48 }, - { date: "2026-01-11T12:28:48Z", expense: 67 }, - { date: "2026-01-11T17:45:36Z", expense: 33 }, - { date: "2026-01-11T22:04:48Z", expense: 53, income: 58 }, + { date: "2026-01-05T02:24:00Z", expense: 255, income: 0.7 }, + { date: "2026-01-05T08:24:00Z", expense: 271 }, + { date: "2026-01-05T14:52:48Z", expense: 260 }, + { date: "2026-01-05T21:07:12Z", expense: 284 }, + { date: "2026-01-06T03:36:00Z", expense: 280 }, + { date: "2026-01-06T10:04:48Z", expense: 307 }, + { date: "2026-01-06T16:19:12Z", expense: 240 }, + { date: "2026-01-06T22:04:48Z", expense: 278 }, + { date: "2026-01-07T03:50:24Z", expense: 269 }, + { date: "2026-01-07T09:36:00Z", expense: 295, income: 0.78 }, + { date: "2026-01-07T15:21:36Z", expense: 309 }, + { date: "2026-01-07T21:07:12Z", expense: 285 }, + { date: "2026-01-08T02:52:48Z", expense: 260 }, + { date: "2026-01-08T08:24:00Z", expense: 289 }, + { date: "2026-01-08T13:55:12Z", expense: 298 }, + { date: "2026-01-08T19:40:48Z", expense: 304 }, + { date: "2026-01-09T01:12:00Z", expense: 275 }, + { date: "2026-01-09T06:43:12Z", expense: 309 }, + { date: "2026-01-09T12:28:48Z", expense: 293 }, + { date: "2026-01-09T18:00:00Z", expense: 340 }, + { date: "2026-01-09T23:31:12Z", expense: 304 }, + { date: "2026-01-10T04:48:00Z", expense: 305 }, + { date: "2026-01-10T10:04:48Z", expense: 275, income: 0.92 }, + { date: "2026-01-10T15:21:36Z", expense: 284 }, + { date: "2026-01-10T20:38:24Z", expense: 267 }, + { date: "2026-01-11T01:55:12Z", expense: 304 }, + { date: "2026-01-11T07:12:00Z", expense: 300 }, + { date: "2026-01-11T12:28:48Z", expense: 335 }, + { date: "2026-01-11T17:45:36Z", expense: 273 }, + { date: "2026-01-11T22:04:48Z", expense: 309, income: 0.98 }, ].map((item: { date: string; expense: number; income?: number }) => ({ date: item.date, expense: item.expense, @@ -58,16 +57,16 @@ const weekdayFormatter = new Intl.DateTimeFormat("en-US", { const formatWeekday = (value: number) => weekdayFormatter.format(new Date(value)); const chartDomain = [weekStart, weekStart + 7 * DAY_MS]; -const formatTooltipCurrency = (value: number | string) => formatCurrency(Number(value), { noDecimals: true }); +const formatTooltipValue = (value: number | string) => `${Number(value).toLocaleString()}K`; const chartConfig = { expense: { color: "var(--chart-4)", - label: "Expense", + label: "Events ingested", }, income: { color: "var(--chart-2)", - label: "Income", + label: "Ingestion errors", }, } satisfies ChartConfig; @@ -75,7 +74,7 @@ export function TransactionsOverviewCard() { return ( - Spending Overview + Ingestion Overview