Commit Graph

19 Commits

Author SHA1 Message Date
Semih
b61d955256 feat(insights): semantic dedup gate + dismissed-theme suppression in analyze
The fingerprint hash (even after a7fe80f) still over-splits: the same root
problem produces different tag-sets / first-error / first-failed-endpoint
across sessions, so it hashes to a new fingerprint and creates a duplicate
insight. And generic ux_friction/frustrated_session sessions (category-tree
vs parts-panel vs schema-image) carry no structured signal to separate
sub-problems — only the LLM's reading of the timeline does. Dismissed insights
were only suppressed on an EXACT fingerprint repeat; a near-variant got a fresh
fingerprint and re-opened the theme. Dismissed rows were also excluded from the
fast-path cache, so every recurring session re-ran the LLM for no reason.

Two-layer dedup so (1) duplicates can't be created and (2) nothing similar to a
dismissed/duplicate theme is re-opened:

Layer 1 — fingerprint (or aliased fingerprint) fast-path, no LLM:
  - dismissed/duplicate  → SUPPRESS (bump occ/lastSeen, keep status)
  - validated/shipped    → flag REGRESSION (cheap, no re-analysis)
  - active & fresh        → attach occurrence
  - active & stale        → fall through to refresh re-analysis (unchanged)

Layer 2 — semantic dedup gate before creating a NEW insight (flash, no new infra):
  - build a bounded catalog: all active insights + recently-touched
    dismissed/duplicate/validated/shipped (lookback-windowed, capped)
  - ask the model whether the session is the SAME underlying problem as a
    catalog entry (anti-hallucination: only accept catalog ids; conf >= 0.72)
  - on match: alias this fingerprint onto that insight + attach/suppress, so the
    next identical session fast-paths in Layer 1 (no LLM, no duplicate row)
  - no match: create a new insight as before

Schema: Insight.aliasFingerprints String[] (additive; applied via prisma db push
on deploy). Dedup-gate cost is logged to costLedger as promptTag=dedup_gate.

Tunables: INSIGHT_DEDUP_GATE (default on), INSIGHT_DEDUP_THRESHOLD (0.72),
INSIGHT_DEDUP_SUPPRESS_DAYS (90), INSIGHT_DEDUP_CATALOG_MAX (120).

Pure decision helpers (classifyExistingAction, shouldAcceptMatch) extracted and
covered by a smoke test (apps/worker `pnpm test`, 18/18). tsc --noEmit clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 20:41:18 +03:00
Semih
8a81cb77bf feat(insights): multi-project Sentry archive
The sentry-archive job was hardcoded to a single SENTRY_PROJECT slug.
Now that sase has a second Sentry project (sase-web, browser SDK), the
worker has to pull both — otherwise frontend events live only in
Sentry's UI and never reach the panel's sentry_events table for the
behavioral-insight pipeline to pick up.

Changes:

- SENTRY_PROJECT becomes a CSV (e.g. "python,sase-web") parsed once at
  module load into PROJECTS[]. Backward-compatible: a single value
  keeps working exactly as before.
- Issues are fetched org-wide in a single loop (unchanged endpoint)
  but each issue's Sentry project slug is now persisted via the new
  `sentrySourceProject` column on sentry_issues.
- Events are project-scoped on Sentry's side, so the job iterates
  projects and calls listEventsPage(slug, cursor) for each. Per-project
  pagination + dedup is preserved. A project-level failure is recorded
  in `error` but doesn't abort the other projects.
- New result shape: `perProject: { [slug]: { fetched, inserted, duplicate } }`
  Pipeline log gains a `[python=N/M sase-web=N/M]` breakdown so it's
  obvious at a glance which project produced what.
- Schema: nullable `sentrySourceProject` on both SentryIssue and
  SentryEvent (+ composite index per projectKey for queries that want
  to filter "panel project = sase AND sentry project = sase-web").
  Migration is a pure nullable add — `prisma db push` on container
  start is safe. Existing rows stay null until next sync touches them.

After merge, set `SENTRY_PROJECT=python,sase-web` on the panel-worker
Coolify app and redeploy.
2026-06-01 18:37:10 +03:00
Semih
928728dc66 feat(insights): Phase C — Sentry issues + events archive
Mirrors Sentry into our DB before the free tier prunes events (~30d).

- `SentryIssue` (aggregate state, upserted to latest) + `SentryEvent`
  (raw occurrences, lossless full payload JSONB, dedup by eventId, cold
  dump to MinIO `sentry-archive/{project}/YYYY/MM/DD.jsonl.gz`).
- `lib/sentry.ts`: read-only client, Link-header cursor pagination,
  listIssuesPage / listEventsPage. EU-region aware (SENTRY_API_BASE).
- `sentry-archive` job @hourly: issues upsert + events newest-first with
  skipDuplicates, stops once a page is all-duplicates (caught up).
- Config via env: SENTRY_AUTH_TOKEN / SENTRY_ORG / SENTRY_PROJECT /
  SENTRY_API_BASE.

Verified live against otolog/python (EU): a test event archived on run 1,
0 inserts / 1 duplicate on run 2 (dedup), issue upserted idempotently.

Note: Sase API currently sends to an inaccessible org's DSN (hardcoded
fallback); repointing SENTRY_DSN to otolog/python is a separate deploy step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 21:32:50 +03:00
Semih
ab86dbdf59 feat(insights): Phase B — PostHog person + cohort snapshots
Append-only history of person properties and cohort definitions, since
the free tier overwrites the live person / prunes data. A new row is
written only when the payload changes (hash compare), so the tables stay
a compact change-timeline rather than a daily full copy.

- `PosthogPersonSnapshot` (personId join key to PosthogEvent.personId,
  all distinct_ids, properties JSONB, propertiesHash).
- `PosthogCohortSnapshot` (cohortId, name, count, filters, stateHash).
- `listPersons` / `listCohorts` REST helpers (next-pagination) in posthog.ts.
- `stableHash` (sorted-key JSON hash) in hash.ts for change detection.
- `posthog-identity-archive` job wired @03:00 daily. Latest-hash lookup
  via one DISTINCT ON query, inserts only changed rows via createMany.

Verified against prod: 242 persons → 242 snapshots on first run, 0 on
immediate re-run (change detection). 0 cohorts currently → graceful no-op.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:47:20 +03:00
Semih
f663c0aa09 feat(insights): permanent archive of PostHog events + raw recordings
Phase A of the external observability archive. PostHog Cloud free tier
deletes data on a rolling window (events ~1yr, session recordings ~30d);
this mirrors both into our own infra permanently (hot Postgres + cold
MinIO gzip JSONL).

PostHog raw event archive:
- New `PosthogEvent` model (uuid PK → dedup, full properties JSONB,
  hot columns promoted + indexed).
- `posthog-event-archive` job: watermark-paged HogQL pull (cursor in the
  existing IngestionWatermark via a "sase:events" stream key), createMany
  + skipDuplicates for idempotency, 365d backfill. Daily closed-day cold
  dump to MinIO `posthog-archive/{project}/YYYY/MM/DD.jsonl.gz`.
- `hogqlQuery` helper added to posthog.ts. Timestamp cursor uses
  parseDateTimeBestEffort() — ClickHouse 500s on a raw ISO8601 literal
  (verified live against the project).

Raw rrweb recording preservation (separate from compress, which only
covers scored sessions and caps blobs):
- `SessionMeta.rrwebArchivedAt` / `rrwebArchiveKey`.
- `archive-recordings` job: every recording within 25d (margin before
  30d deletion), full blob range (no 12-cap), gzip → MinIO
  `rrweb-archive/{project}/YYYY/MM/DD/{sessionId}.jsonl.gz`.

- `putBuffer` gzip helper in minio.ts.
- Both jobs wired into pipeline.ts (event-archive@*/15min, recordings@*/6h).
- retention.ts untouched → new table + buckets persist forever (the goal).

Schema applies via the existing `prisma db push` on web start (additive:
new table + nullable columns).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 20:15:29 +03:00
Semih
fdce3f6bd0 feat(content): Phase 8 content generation (Faz A+B) for Sase.tr
Hybrid content automation pilot: generation + review + drafts live in the
panel (reusing the insight pipeline's DeepSeek client, prompt_templates
versioning, cost_ledger and budget_settings); publishing/distribution will
go through n8n (Faz C, not built). Channels: blog, LinkedIn, X, Instagram.
Topic sourcing is automatic (LLM-generated ideas). Approval model: drafts
sit in the panel for manual review/edit/publish.

Faz A (worker):
- ContentTopic / ContentDraft Prisma models (content_topics, content_drafts)
- content-prompts.ts: 5 seed prompts (topic ideas[pro] + blog[pro] +
  linkedin/x/instagram[flash]), Turkish B2B automotive tone, per-channel
  JSON schemas
- content-budget.ts: separate budget envelope (sums only content_* spend)
- content-topics job (auto idea gen, backlog-capped, title dedupe) +
  content-generate job (queued topic -> one draft per channel)
- content-pipeline scheduler (separate BullMQ queue, topics@*/8h,
  generate@*/10min), wired into index.ts; seeded via seed-runtime
- content budget settings (caps + content_paused kill switch); seed default
  content_paused=true for a safe first deploy

Faz B (web):
- /content (queue + auto/manual triggers + manual topic form),
  /content/t/[id] (per-channel draft cards: preview, JSON edits,
  approve/reject), /content/costs (content-only spend)
- server actions (audit-logged), manual trigger API routes, contentQueue(),
  nav + Cmd+K entries
- content caps surfaced on /insights/settings/budgets + whitelisted

Both packages typecheck. Schema applies on deploy (web start runs
prisma db push).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 00:11:05 +03:00
Semih
b1ec7a5277 feat(sase): user lifecycle controls — Phase B
Suspend / Reactivate / Ban buttons on the user detail page; wired to
spoke endpoints (sase.tr#27).

Admin SDK
- suspendUser/reactivateUser/banUser on SaseAdmin + LifecycleResult.
- SASE_ADMIN_ENDPOINTS updated.

Route
- POST /api/sase/users/[id]/lifecycle (action + reason). Auth + spoke
  wired + min reason length checks. Audit on both success and failure.

RO model
- Sase Prisma schema adds status/statusReason/statusChangedAt/
  statusChangedBy. getUser() returns lifecycleStatus + statusReason.

UI
- LifecycleStatusBadge in header next to email reveal.
- LifecycleButtons renders the right actions for the current state.
- Modals with reason textarea; ban requires a double-confirm checkbox.
- Impersonate hidden when user is suspended/banned (AuthGuard rejects).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:38:07 +03:00
Semih
5b26d10485 feat(sase): founder notes — Phase A of user mutations
Panel-side only — first writable feature on the Sase user detail page.
Notes are stored in the panel database (sase_user_notes); the spoke is
never touched (KVKK minimize).

Model
- SaseUserNote { id, saseUserId, authorUserId, body, pinned, createdAt }
- Index on (saseUserId, pinned, createdAt) for the default render order

API
- GET  /api/sase/users/[id]/notes — list (pinned first, then newest)
- POST /api/sase/users/[id]/notes — { body, pinned } create (audit'li)
- PATCH/DELETE /api/sase/notes/[noteId] — toggle pin / hard delete

UI
- New "Notlar" tab on user detail. Textarea + pinned checkbox + submit;
  list shows TR-localized timestamps, per-row pin/unpin and delete.

Schema sync
- package.json `start` now runs `prisma db push --skip-generate` before
  `next start`. Panel uses db-push style (no migrations dir); this lets
  the new table land on next deploy without a separate manual step.
  Future destructive changes will require a smarter migration approach.

teknikborc.md updated: super_panel_reader role still needs to be created
on the new sase-postgres (current panel uses sase superuser).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 09:32:48 +03:00
Semih
0b31685e95 feat(sase): user management Phase 7a — listing + detail (RO)
Adım 1 — User listesi sayfası
- Extend Sase RO Prisma schema: Plan, Brand, UserBrand, BankAccount, Payment, QueryLog
- listUsers() + listPlans() repo with filter/sort/page in apps/web/src/lib/sase/users.ts
- /projects/sase/users — TanStack-free server-rendered table with URL-param filters
  (plan multi-select, status, activity preset, search) + pager
- Sase landing "Users →" link

Adım 2 — User detay 360°
- /projects/sase/users/[id] — header (masked email + audit-logged reveal button)
  + 5 tabs: Özet · Subscription & Billing · Kullanım · Aktivite · Audit
- Usage stats: 30d/90d/lifetime queries, success rate, avg response, daily sparkline,
  brand/source distribution, top VINs
- Timeline: merged signup + subscriptions + payments + recent queries
- User-scoped audit trail (AuditLog endpoint LIKE filter)
- POST /api/sase/users/[id]/reveal-email — audit-logged full email reveal

teknikborc.md — Phase 7 PRD vs Sase.tr şeması gap'leri kaydedildi
(EFT yok, B2B yok, API keys/webhooks yok, vb.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 00:42:59 +03:00
Semih
b635b529e2 feat(phase6e): eval framework + prompt editor + daily brief + retention + bundle mode
Schema:
- eval_sets (promptTag, cases JSON: [{id, timeline, expected: {...}}])
- eval_runs (per-execution scoring: passedSchema/Severity/Rubric, cost, tokens, results)

Prompts:
- 2 new seed templates: upgrade_hesitation (flash), pattern_bundle (pro)
- pickPromptTag() routes 'upgrade_hesitation' tag to upgrade_hesitation prompt
- Editor UI at /insights/settings/prompts/[id]: edit system/user/schema/tier/temp,
  publishPromptVersion() creates new version + deactivates old (CRUD with auto bump)
- setPromptActive() to toggle versions

Daily Brief (/insights/brief):
- Last 24h: sessions processed, insights produced (with severity breakdown),
  cost, cache hit rate; today's top 5 priorities; week stats (shipped/validated/regressed)

Retention cron (04:15 UTC daily):
- Delete sessions_meta + session_custom_events older than 90d (unless referenced by
  active insight)
- Delete compressed_sessions rows + MinIO timeline blobs older than 180d
- raw_metadata 30d (currently no-op; we don't persist raw metadata to MinIO)

Eval framework:
- apps/worker/src/jobs/eval-run.ts: runs cases against current/specified prompt version,
  scores schema_pass + severity_match + rubric_substring; stores EvalRun
- apps/worker BullMQ queue handler for 'eval-run' job name
- apps/web installed bullmq; /lib/queue.ts thin Queue accessor
- Web actions: createEvalSet, triggerEvalRun (enqueues job to insight-pipeline queue)
- UI: /insights/settings/eval-sets list, /new create form (paste JSON cases),
  /[id] detail with Run button + recent runs + per-case JSON

Bundle mode (analyze job):
- Pull 3x batch, group by fingerprint
- Groups ≥ INSIGHT_BUNDLE_THRESHOLD (default 3) → use 'pattern_bundle' prompt
- Timeline = primary rep + PATTERN BUNDLE summary block (occurrences, unique users, deltas)
- Insight stores ALL group session IDs as relatedSessionIds; all marked analyzed in one tx
- Cost amortized: 1 LLM call per group

Nav/Cmd+K:
- Inbox header links: Brief, Patterns, Eval sets
- Palette: Daily brief, Eval sets entries

Deferred to backlog: embedding-similarity cross-fingerprint clustering, Telegram brief delivery,
Sase.tr-side data-private audit (separate repo).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 06:11:48 +00:00
Semih
c5acdfc8ec feat(phase6d): GitHub action loop — issue creation, webhook, validation cron, patterns view
Schema:
- Insight.+githubIssueNumber (user-visible #N, separate from id BigInt)

GitHub integration (apps/web/src/lib/github.ts):
- repoFor(projectKey): env-based GITHUB_REPO_<KEY>=owner/repo mapping
- createIssue / getIssue REST wrappers
- verifyWebhookSignature (HMAC-SHA256 timing-safe)
- buildIssueBody: renders structured markdown from insight + LLM body
  (hypothesis, reproduce steps, affected route/provider, quick/long fixes,
  suggested investigation, evidence links to panel, DoD checklist)

Server action createGithubIssueForInsight:
- Auth-gated, audited; idempotent (refuses if issue already exists)
- Labels: insight-driven, severity-<P>, type-<T>, <project>-pilot
- Sets status=in_backlog, stores githubIssueUrl/Id/Number/State

Webhook /api/webhooks/github:
- Signature verify with GITHUB_WEBHOOK_SECRET
- issues.closed → status=shipped + shippedAt + validationStartedAt
- issues.reopened → status=in_progress + clear validation state
- issues.opened → status=in_backlog
- PR linking placeholder (passthrough only for now)

Validation cron (worker, daily 5:00 UTC):
- For each insight in 'shipped' state:
  - Count sessions with same fingerprint after shippedAt
  - >= INSIGHT_REGRESSION_THRESHOLD (default 3) → status=regressed + regressionDetected=true
  - validationPeriodDays elapsed with no regression → status=validated + validatedAt

UI:
- Insight detail: GithubActions card — Create button (when no issue),
  external link + issue # + state (when present)
- New /insights/patterns page: clusters insights by type + affected_route/provider,
  shows ≥2-insight or ≥5-occurrence groups sorted by max severity
- Inbox header link to Patterns
- Cmd+K palette: Patterns entry

Env needed: GITHUB_TOKEN, GITHUB_REPO_SASE, GITHUB_WEBHOOK_SECRET.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-14 05:45:48 +00:00
Semih
95c7e8e321 feat(phase6b): LLM analysis layer — DeepSeek + insights + cost ledger + budget guard
Schema:
- insights (fingerprint dedup, occurrence_count, related_session_ids[], priority_score)
- cost_ledger (per-call, cache_hit/miss/output token split + USD)
- prompt_templates (versioned, per-tag, with JSON schema + tier + temperature)
- budget_settings (global key/value: monthly_hard_cap_usd, daily_soft/hard, per_call_max, analysis_paused)

Worker:
- lib/deepseek.ts: Anthropic-compat endpoint client (deepseek-v4-flash | deepseek-v4-pro),
  usage→USD with V4 promo pricing, cache_read_input_tokens awareness, extractJson() helper
- lib/budget.ts: checkBudget() returns active|soft_throttled|hard_paused|monthly_paused,
  forces flash tier on soft cap, halts on hard/monthly cap or analysis_paused
- lib/prompts.ts: 5 seed templates (bug_triage P, ux_friction F, payment_issue P,
  onboarding_stuck F, provider_quality F) with embedded JSON schemas + Sase.tr context;
  pickPromptTag() maps session tags → prompt
- lib/json-validate.ts: lightweight schema validator (no ajv dep)
- lib/seed-runtime.ts: idempotent upsert of prompts + default budget settings on worker boot
- lib/minio.ts: +getText() for compressed timeline fetch
- jobs/analyze.ts: budget guard → 6h fingerprint cache (attach session to existing insight) →
  template lookup → severity-based tier override → DeepSeek call → JSON parse + validate →
  insert insight (or aggregate occurrence) → write cost_ledger
- scheduler: analyze@*/4min on insight-pipeline queue

UI:
- /insights (was pipeline view) → now Insight Inbox: priority-sorted list w/ KPI strip
  (new/in_backlog/shipped/today $/month $), severity badges, link to detail
- /insights/i/[id]: insight detail with structured body render, related sessions,
  per-session cost breakdown, raw JSON collapsible
- /insights/costs: KPI cards (today, month, avg, cache hit), daily 30d bar table,
  by-model + by-prompt breakdowns, top 10 expensive, recent errors
- /insights/pipeline: moved old session-pipeline view here
- /insights/sessions/[id]: unchanged session timeline viewer

Defaults:
- monthly cap $30, daily soft $1.50 / hard $3, per-call $0.20, analysis_paused=false
- Severity→Tier: P0/P1=pro, P2/P3/INFO=flash; budget soft-cap forces flash

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 23:01:55 +00:00
Semih
fc6a3f7a39 feat(phase6a-v1.2): dual-mode custom-event enrichment (v1.0 + v2.0 aliases)
- Prisma: posthog_person_cache (24h TTL), posthog_group_cache, session_custom_events
- SessionMeta: +posthogDistinctId, +groupKey, +customEventCount
- PostHog client: listSessionEvents, getPerson, getGroup
- posthog-cache.ts: cache-with-stale-refresh for person + group properties
- event-taxonomy.ts: dual-mode alias map (vin_decoded↔vin_decode_initiated,
  vin_decode_success↔vin_decode_succeeded, vin_decode_error↔vin_decode_failed,
  payment_success↔payment_succeeded). 56 tracked event names total.
- enrich.ts: per-session custom events fetch, persist to session_custom_events,
  attach user_properties + group_properties via cache.
- tagger.ts: 14 new custom-event tags
  (vin_decode_fail_pattern, provider_reliability_issue, payment_friction,
   payment_ui_silent_failure (P0), payment_failed_session, checkout_abandonment,
   downgrade_pending, compatibility_quality_gap, parts_export_abandoned,
   search_validation_friction, onboarding_stuck, upgrade_hesitation,
   power_user_path, at_risk_active_session, provider_mismatch,
   api_key_friction, webhook_setup_struggle); group-context priority bump.
- heuristic.ts: customEventPromoteReasons() — 10 custom-event promote signals.
- compress.ts: merge_rrweb_and_custom_events interleaving with ⤷ EVENT: prefix,
  ±1.5s grouping, standalone flush for events with no nearby rrweb. Adds
  company_context header line; key_events lists top-5 custom event counts.
- /insights page: +Events column. /insights/sessions/[id]: collapsible custom
  events table + group key display.

Graceful: when Sase.tr emits 0 custom events the pipeline falls back to
rrweb-only tagging from Phase 6a v1.1.

PRD: behavioral-insight-pipeline-prd-1.md (v1.2),
     sase-posthog-events-prd.md (Sase.tr-side, separate codebase).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 22:17:18 +00:00
Semih
1a7a1b5787 feat(phase6a): behavioral insight pipeline ingestion + tagging + compression
- prisma: sessions_meta, compressed_sessions, ingestion_watermarks
- worker: PostHog client (eu.i.posthog.com), heuristic filter
- worker: BullMQ insight-pipeline queue (ingest 5min / tag 2min / compress 3min)
- worker: tagger (bug_suspected, ux_friction, upgrade_hesitation, etc.) + severity scoring
- worker: rrweb -> semantic timeline transform + 8-pattern PII sanitization
- worker: fingerprint hash + MinIO upload (bucket: insight-compressed)
- web: /insights pipeline dashboard + session timeline viewer
- sidebar: Insights nav entry

LLM (Phase 6b), insight inbox, cost dashboard, GitHub loop deferred to later phases.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-13 20:58:36 +00:00
Semih
b9709ed07e feat(phase3): worker container + event bus + scheduled jobs
apps/worker:
- BullMQ nightly scheduler (cron 0 3 * * *)
- Redis Streams consumer-group per wired/active project
- Persists events to Event model

schema:
- Event model (streamId unique, project + type indexed)
2026-05-13 11:03:58 +00:00
Semih
e455ebbfce feat(phase2): planned badge, Cmd+K palette, per-project dashboard cards
- Project.status (planned/wired/active) + seed updates
- ProjectBadge component
- CommandPalette (⌘K) wired in PanelShell with project + nav + sign-out
- Dashboard '/' shows per-project cards + KPI strip
- Added doner312 to seeded projects
2026-05-13 10:54:07 +00:00
Semih
f72dcc05ba feat(phase1b): sase.tr read-only connection
- prisma/sase/schema.prisma with User + UserSubscription (partial view)
- lib/db-sase.ts exports typed RO client
- /projects/sase shows live metrics (users count, new users 7d, active subs)
- seed marks sase as active

DATABASE_URL_SASE_RO wired to super_panel_reader (SELECT-only)
2026-05-13 10:38:03 +00:00
Semih
d189206f06 feat(phase1a): admin-sdk base, audit helper, project seed, panel pages
- src/lib/audit.ts + src/lib/admin-sdk/base.ts (typed HTTP + auto audit)
- prisma/seed.ts seeds 5 projects (all inactive)
- pages: /projects, /projects/[key], /audit, /operations, /events, /settings
- components/panel-shell.tsx shared layout
- entrypoint runs seed after db push
2026-05-13 10:33:18 +00:00
Semih
67a7c5b887 feat: phase 0 skeleton — next.js 16 + better-auth + prisma 2026-05-13 09:17:50 +00:00