@lightupai/polaris 0.0.77 → 0.0.79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/web/app.ts CHANGED
@@ -21,7 +21,20 @@ import {
21
21
  type Sql,
22
22
  } from "../service/db";
23
23
  import { layout, nav } from "./layout";
24
- import { renderSetupView, renderActiveView, renderProfileView, renderErrorView } from "./views";
24
+ import {
25
+ renderSetupView,
26
+ renderActiveView,
27
+ renderProfileView,
28
+ renderErrorView,
29
+ renderTranscriptView,
30
+ renderSearchView,
31
+ renderDecisionsView,
32
+ renderProjectSettingsView,
33
+ type TranscriptEvent,
34
+ type SearchResult,
35
+ type ProjectMemberView,
36
+ } from "./views";
37
+ import { AnnotationKind, type Annotation } from "../types";
25
38
  import { renderLandingPage } from "./pages";
26
39
  import { createSystemChannel, postSystemEvent } from "../slack/system";
27
40
  import {
@@ -137,6 +150,9 @@ setInterval(() => {
137
150
  export function createApp(sql: Sql) {
138
151
  const app = new Hono();
139
152
 
153
+ // Polaris API service (same convention as slack/bridge.ts)
154
+ const apiUrl = process.env.POLARIS_API_URL ?? "http://localhost:4321";
155
+
140
156
  // Start hourly signup rollup
141
157
  startSignupRollup(sql);
142
158
 
@@ -191,8 +207,8 @@ export function createApp(sql: Sql) {
191
207
  // --- Landing page ---
192
208
 
193
209
  app.get("/", (c) => {
194
- return layout(renderLandingPage(), "Polaris — It's like Gong for Claude Code sessions", {
195
- title: "Polaris — It's like Gong for Claude Code sessions",
210
+ return layout(renderLandingPage(), "Polaris by Lightup — It's like Gong for Claude Code sessions", {
211
+ title: "Polaris by Lightup — It's like Gong for Claude Code sessions",
196
212
  description: "Capture every AI coding session. Stream prompts, responses, and tool calls to Slack in real time. Collaborate across agents. Nothing is lost.",
197
213
  canonical: "https://app.withpolaris.ai",
198
214
  });
@@ -343,13 +359,14 @@ export function createApp(sql: Sql) {
343
359
  const projects = (await listProjects(sql, payload.org_id)).filter((p) => p.name !== "_system");
344
360
  const allSessions = (await listSessions(sql, payload.org_id)).filter((s) => s.project !== "_system");
345
361
  const promptCounts = await getSessionPromptCounts(sql, payload.org_id);
362
+ const participantId = payload.participant_id;
346
363
 
347
364
  function buildSessionFixture(s: typeof allSessions[0]): import("./fixtures").SessionFixture {
348
365
  return {
349
366
  name: s.name,
350
367
  project: s.project,
351
368
  driver: s.driver ?? "",
352
- role: (s.driver === payload.participant_id ? "driver" : "advisor") as "driver" | "advisor",
369
+ role: (s.driver === participantId ? "driver" : "advisor") as "driver" | "advisor",
353
370
  description: "",
354
371
  participants: s.driver ? [{ id: s.driver, role: "driver" as const }] : [],
355
372
  eventCount: promptCounts.get(`${s.project}/${s.name}`) ?? 0,
@@ -383,9 +400,9 @@ export function createApp(sql: Sql) {
383
400
  };
384
401
 
385
402
  if (hasConnectedSession) {
386
- return layout(renderActiveView(ctx, sessionFixtures, projectFixtures, devices), "Dashboard — Polaris");
403
+ return layout(renderActiveView(ctx, sessionFixtures, projectFixtures, devices), "Dashboard — Polaris by Lightup");
387
404
  }
388
- return layout(renderSetupView(ctx, devices), "Setup — Polaris");
405
+ return layout(renderSetupView(ctx, devices), "Setup — Polaris by Lightup");
389
406
  });
390
407
 
391
408
  // --- Profile ---
@@ -409,9 +426,363 @@ export function createApp(sql: Sql) {
409
426
  slackConnected: !!org.slack_team_id,
410
427
  cliInstalled: false,
411
428
  hasConnectedSession: false,
429
+ totalPrompts: 0,
430
+ };
431
+
432
+ return layout(renderProfileView(ctx, payload.participant_id), "Polaris by Lightup - Profile");
433
+ });
434
+
435
+ // --- Session transcript ---
436
+
437
+ app.get("/sessions/:proj/:sess", async (c) => {
438
+ const token = c.req.query("token");
439
+ if (!token) return c.redirect("/login");
440
+
441
+ const payload = await verifyToken(token);
442
+ if (!payload) return c.redirect("/login");
443
+
444
+ const org = await getOrg(sql, payload.org_id);
445
+ if (!org) return c.redirect("/login");
446
+
447
+ const proj = c.req.param("proj");
448
+ const sess = c.req.param("sess");
449
+ const before = c.req.query("before");
450
+
451
+ const eventsUrl = new URL(`${apiUrl}/projects/${encodeURIComponent(proj)}/sessions/${encodeURIComponent(sess)}/events`);
452
+ eventsUrl.searchParams.set("limit", "200");
453
+ if (before) eventsUrl.searchParams.set("before", before);
454
+
455
+ let events: TranscriptEvent[];
456
+ let nextCursor: string | null;
457
+ try {
458
+ const res = await fetch(eventsUrl, { headers: { Authorization: `Bearer ${token}` } });
459
+ if (!res.ok) {
460
+ const message = res.status === 404 ? "Session not found." : `Failed to load transcript (${res.status}).`;
461
+ return layout(renderErrorView(message, "Back to dashboard", `/dashboard?token=${token}`));
462
+ }
463
+ const data = (await res.json()) as { events: TranscriptEvent[]; nextCursor: string | null };
464
+ events = data.events ?? [];
465
+ nextCursor = data.nextCursor ?? null;
466
+ } catch {
467
+ return layout(renderErrorView("Could not reach the Polaris API.", "Back to dashboard", `/dashboard?token=${token}`));
468
+ }
469
+
470
+ // Annotations are progressive enhancement — render the transcript even if they fail to load.
471
+ let annotations: Annotation[] = [];
472
+ try {
473
+ const annRes = await fetch(
474
+ `${apiUrl}/projects/${encodeURIComponent(proj)}/sessions/${encodeURIComponent(sess)}/annotations`,
475
+ { headers: { Authorization: `Bearer ${token}` } }
476
+ );
477
+ if (annRes.ok) {
478
+ annotations = ((await annRes.json()) as { annotations: Annotation[] }).annotations ?? [];
479
+ }
480
+ } catch { /* transcript still renders without annotations */ }
481
+
482
+ const ctx = { token, userName: payload.name, orgName: org.name, email: payload.email };
483
+ const title = `Polaris - ${proj}/${sess}`.replace(/[<>&"]/g, "");
484
+ return layout(renderTranscriptView(ctx, proj, sess, events, nextCursor, before, annotations), title);
485
+ });
486
+
487
+ // Proxies to the API inject endpoint; the API derives the sender from the bearer token.
488
+ app.post("/sessions/:proj/:sess/inject", async (c) => {
489
+ const token = c.req.query("token");
490
+ if (!token) return c.redirect("/login");
491
+
492
+ const payload = await verifyToken(token);
493
+ if (!payload) return c.redirect("/login");
494
+
495
+ const proj = c.req.param("proj");
496
+ const sess = c.req.param("sess");
497
+ const transcriptUrl = `/sessions/${encodeURIComponent(proj)}/${encodeURIComponent(sess)}?token=${token}`;
498
+
499
+ const body = await c.req.parseBody();
500
+ const content = typeof body.content === "string" ? body.content.trim() : "";
501
+ if (!content) return c.redirect(transcriptUrl);
502
+
503
+ try {
504
+ const res = await fetch(`${apiUrl}/projects/${encodeURIComponent(proj)}/sessions/${encodeURIComponent(sess)}/inject`, {
505
+ method: "POST",
506
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
507
+ body: JSON.stringify({ content }),
508
+ });
509
+ if (!res.ok) {
510
+ return layout(renderErrorView(`Inject failed (${res.status}).`, "Back to transcript", transcriptUrl));
511
+ }
512
+ } catch {
513
+ return layout(renderErrorView("Could not reach the Polaris API.", "Back to transcript", transcriptUrl));
514
+ }
515
+
516
+ return c.redirect(transcriptUrl);
517
+ });
518
+
519
+ // --- Annotations (star / tag / decision curation) ---
520
+ // Proxies to the API annotations endpoints; the API derives the participant from the bearer token.
521
+
522
+ app.post("/sessions/:proj/:sess/annotations", async (c) => {
523
+ const token = c.req.query("token");
524
+ if (!token) return c.redirect("/login");
525
+
526
+ const payload = await verifyToken(token);
527
+ if (!payload) return c.redirect("/login");
528
+
529
+ const proj = c.req.param("proj");
530
+ const sess = c.req.param("sess");
531
+ const transcriptUrl = `/sessions/${encodeURIComponent(proj)}/${encodeURIComponent(sess)}?token=${token}`;
532
+
533
+ const body = await c.req.parseBody();
534
+ const kind = typeof body.kind === "string" ? body.kind : "";
535
+ if (!AnnotationKind.safeParse(kind).success) return c.redirect(transcriptUrl);
536
+ const eventId = typeof body.event_id === "string" && body.event_id ? body.event_id : undefined;
537
+ const value = typeof body.value === "string" && body.value.trim() ? body.value.trim() : undefined;
538
+
539
+ try {
540
+ const res = await fetch(`${apiUrl}/projects/${encodeURIComponent(proj)}/sessions/${encodeURIComponent(sess)}/annotations`, {
541
+ method: "POST",
542
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
543
+ body: JSON.stringify({ event_id: eventId, kind, value }),
544
+ });
545
+ if (!res.ok) {
546
+ return layout(renderErrorView(`Annotation failed (${res.status}).`, "Back to transcript", transcriptUrl));
547
+ }
548
+ } catch {
549
+ return layout(renderErrorView("Could not reach the Polaris API.", "Back to transcript", transcriptUrl));
550
+ }
551
+
552
+ return c.redirect(transcriptUrl);
553
+ });
554
+
555
+ app.post("/sessions/:proj/:sess/annotations/:id/delete", async (c) => {
556
+ const token = c.req.query("token");
557
+ if (!token) return c.redirect("/login");
558
+
559
+ const payload = await verifyToken(token);
560
+ if (!payload) return c.redirect("/login");
561
+
562
+ const proj = c.req.param("proj");
563
+ const sess = c.req.param("sess");
564
+ const id = c.req.param("id");
565
+ const transcriptUrl = `/sessions/${encodeURIComponent(proj)}/${encodeURIComponent(sess)}?token=${token}`;
566
+
567
+ try {
568
+ const res = await fetch(`${apiUrl}/annotations/${encodeURIComponent(id)}`, {
569
+ method: "DELETE",
570
+ headers: { Authorization: `Bearer ${token}` },
571
+ });
572
+ if (!res.ok) {
573
+ return layout(renderErrorView(`Failed to remove annotation (${res.status}).`, "Back to transcript", transcriptUrl));
574
+ }
575
+ } catch {
576
+ return layout(renderErrorView("Could not reach the Polaris API.", "Back to transcript", transcriptUrl));
577
+ }
578
+
579
+ return c.redirect(transcriptUrl);
580
+ });
581
+
582
+ // --- Decisions (org-wide curation feed) ---
583
+
584
+ app.get("/decisions", async (c) => {
585
+ const token = c.req.query("token");
586
+ if (!token) return c.redirect("/login");
587
+
588
+ const payload = await verifyToken(token);
589
+ if (!payload) return c.redirect("/login");
590
+
591
+ const org = await getOrg(sql, payload.org_id);
592
+ if (!org) return c.redirect("/login");
593
+
594
+ const project = (c.req.query("project") ?? "").trim();
595
+
596
+ let decisions: Annotation[] | null = null;
597
+ let decisionsError: string | undefined;
598
+ const decisionsUrl = new URL(`${apiUrl}/decisions`);
599
+ if (project) decisionsUrl.searchParams.set("project", project);
600
+ decisionsUrl.searchParams.set("limit", "100");
601
+ try {
602
+ const res = await fetch(decisionsUrl, { headers: { Authorization: `Bearer ${token}` } });
603
+ if (res.ok) {
604
+ decisions = ((await res.json()) as { decisions: Annotation[] }).decisions ?? [];
605
+ } else {
606
+ decisionsError = `Failed to load decisions (${res.status}).`;
607
+ }
608
+ } catch {
609
+ decisionsError = "Could not reach the Polaris API.";
610
+ }
611
+
612
+ const ctx = { token, userName: payload.name, orgName: org.name, email: payload.email };
613
+ return layout(renderDecisionsView(ctx, project, decisions, decisionsError), "Polaris by Lightup - Decisions");
614
+ });
615
+
616
+ // --- Project settings (visibility + members) ---
617
+
618
+ app.get("/projects/:proj/settings", async (c) => {
619
+ const token = c.req.query("token");
620
+ if (!token) return c.redirect("/login");
621
+
622
+ const payload = await verifyToken(token);
623
+ if (!payload) return c.redirect("/login");
624
+
625
+ const org = await getOrg(sql, payload.org_id);
626
+ if (!org) return c.redirect("/login");
627
+
628
+ const proj = c.req.param("proj");
629
+ const projects = await listProjects(sql, payload.org_id);
630
+ const project = projects.find((p) => p.name === proj);
631
+ if (!project) {
632
+ return layout(renderErrorView("Project not found.", "Back to dashboard", `/dashboard?token=${token}`));
633
+ }
634
+
635
+ let members: ProjectMemberView[] = [];
636
+ let membersError: string | undefined;
637
+ try {
638
+ const res = await fetch(`${apiUrl}/projects/${encodeURIComponent(proj)}/members`, {
639
+ headers: { Authorization: `Bearer ${token}` },
640
+ });
641
+ if (res.ok) {
642
+ const data = (await res.json()) as ProjectMemberView[] | { members?: ProjectMemberView[] };
643
+ members = Array.isArray(data) ? data : data.members ?? [];
644
+ } else {
645
+ membersError = `Failed to load members (${res.status}).`;
646
+ }
647
+ } catch {
648
+ membersError = "Could not reach the Polaris API.";
649
+ }
650
+
651
+ const ctx = { token, userName: payload.name, orgName: org.name, email: payload.email };
652
+ const title = `Polaris - ${proj} settings`.replace(/[<>&"]/g, "");
653
+ return layout(renderProjectSettingsView(ctx, proj, project.visibility ?? "org", members, membersError), title);
654
+ });
655
+
656
+ app.post("/projects/:proj/visibility", async (c) => {
657
+ const token = c.req.query("token");
658
+ if (!token) return c.redirect("/login");
659
+
660
+ const payload = await verifyToken(token);
661
+ if (!payload) return c.redirect("/login");
662
+
663
+ const proj = c.req.param("proj");
664
+ const settingsUrl = `/projects/${encodeURIComponent(proj)}/settings?token=${token}`;
665
+
666
+ const body = await c.req.parseBody();
667
+ const visibility = typeof body.visibility === "string" ? body.visibility : "";
668
+ if (visibility !== "org" && visibility !== "members") return c.redirect(settingsUrl);
669
+
670
+ try {
671
+ const res = await fetch(`${apiUrl}/projects/${encodeURIComponent(proj)}/visibility`, {
672
+ method: "POST",
673
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
674
+ body: JSON.stringify({ visibility }),
675
+ });
676
+ if (!res.ok) {
677
+ return layout(renderErrorView(`Failed to update visibility (${res.status}).`, "Back to settings", settingsUrl));
678
+ }
679
+ } catch {
680
+ return layout(renderErrorView("Could not reach the Polaris API.", "Back to settings", settingsUrl));
681
+ }
682
+
683
+ return c.redirect(settingsUrl);
684
+ });
685
+
686
+ app.post("/projects/:proj/members", async (c) => {
687
+ const token = c.req.query("token");
688
+ if (!token) return c.redirect("/login");
689
+
690
+ const payload = await verifyToken(token);
691
+ if (!payload) return c.redirect("/login");
692
+
693
+ const proj = c.req.param("proj");
694
+ const settingsUrl = `/projects/${encodeURIComponent(proj)}/settings?token=${token}`;
695
+
696
+ const body = await c.req.parseBody();
697
+ const participantId = typeof body.participant_id === "string" ? body.participant_id.trim() : "";
698
+ if (!participantId) return c.redirect(settingsUrl);
699
+ const role = typeof body.role === "string" && body.role.trim() ? body.role.trim() : undefined;
700
+
701
+ try {
702
+ const res = await fetch(`${apiUrl}/projects/${encodeURIComponent(proj)}/members`, {
703
+ method: "POST",
704
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
705
+ body: JSON.stringify({ participant_id: participantId, role }),
706
+ });
707
+ if (!res.ok) {
708
+ return layout(renderErrorView(`Failed to add member (${res.status}).`, "Back to settings", settingsUrl));
709
+ }
710
+ } catch {
711
+ return layout(renderErrorView("Could not reach the Polaris API.", "Back to settings", settingsUrl));
712
+ }
713
+
714
+ return c.redirect(settingsUrl);
715
+ });
716
+
717
+ app.post("/projects/:proj/members/:pid/delete", async (c) => {
718
+ const token = c.req.query("token");
719
+ if (!token) return c.redirect("/login");
720
+
721
+ const payload = await verifyToken(token);
722
+ if (!payload) return c.redirect("/login");
723
+
724
+ const proj = c.req.param("proj");
725
+ const pid = c.req.param("pid");
726
+ const settingsUrl = `/projects/${encodeURIComponent(proj)}/settings?token=${token}`;
727
+
728
+ try {
729
+ const res = await fetch(`${apiUrl}/projects/${encodeURIComponent(proj)}/members/${encodeURIComponent(pid)}`, {
730
+ method: "DELETE",
731
+ headers: { Authorization: `Bearer ${token}` },
732
+ });
733
+ if (!res.ok) {
734
+ return layout(renderErrorView(`Failed to remove member (${res.status}).`, "Back to settings", settingsUrl));
735
+ }
736
+ } catch {
737
+ return layout(renderErrorView("Could not reach the Polaris API.", "Back to settings", settingsUrl));
738
+ }
739
+
740
+ return c.redirect(settingsUrl);
741
+ });
742
+
743
+ // --- Search ---
744
+
745
+ app.get("/search", async (c) => {
746
+ const token = c.req.query("token");
747
+ if (!token) return c.redirect("/login");
748
+
749
+ const payload = await verifyToken(token);
750
+ if (!payload) return c.redirect("/login");
751
+
752
+ const org = await getOrg(sql, payload.org_id);
753
+ if (!org) return c.redirect("/login");
754
+
755
+ const query = {
756
+ q: (c.req.query("q") ?? "").trim(),
757
+ project: c.req.query("project") ?? "",
758
+ sender: c.req.query("sender") ?? "",
759
+ source: c.req.query("source") ?? "",
760
+ tag: (c.req.query("tag") ?? "").trim(),
412
761
  };
413
762
 
414
- return layout(renderProfileView(ctx, payload.participant_id), "Polaris - Profile");
763
+ let results: SearchResult[] | null = null;
764
+ let searchError: string | undefined;
765
+ if (query.q) {
766
+ const searchUrl = new URL(`${apiUrl}/search`);
767
+ searchUrl.searchParams.set("q", query.q);
768
+ if (query.project) searchUrl.searchParams.set("project", query.project);
769
+ if (query.sender) searchUrl.searchParams.set("sender", query.sender);
770
+ if (query.source) searchUrl.searchParams.set("source", query.source);
771
+ if (query.tag) searchUrl.searchParams.set("tag", query.tag);
772
+ try {
773
+ const res = await fetch(searchUrl, { headers: { Authorization: `Bearer ${token}` } });
774
+ if (res.ok) {
775
+ results = ((await res.json()) as { results: SearchResult[] }).results ?? [];
776
+ } else {
777
+ searchError = `Search failed (${res.status}).`;
778
+ }
779
+ } catch {
780
+ searchError = "Could not reach the Polaris API.";
781
+ }
782
+ }
783
+
784
+ const ctx = { token, userName: payload.name, orgName: org.name, email: payload.email };
785
+ return layout(renderSearchView(ctx, query, results, searchError), "Polaris by Lightup - Search");
415
786
  });
416
787
 
417
788
  // --- Preview (dev only — all view states on one page) ---
@@ -528,7 +899,7 @@ export function createApp(sql: Sql) {
528
899
  </section>
529
900
  </div>
530
901
  </div>
531
- `, "Polaris - Preview");
902
+ `, "Polaris by Lightup - Preview");
532
903
  });
533
904
 
534
905
  // --- Slack OAuth ---
package/src/web/layout.ts CHANGED
@@ -14,7 +14,7 @@ export interface SeoOpts {
14
14
  canonical?: string;
15
15
  }
16
16
 
17
- export function layout(body: string, title = "Polaris", seo?: SeoOpts): Response {
17
+ export function layout(body: string, title = "Polaris by Lightup", seo?: SeoOpts): Response {
18
18
  const pageTitle = seo?.title ?? title;
19
19
  const description = seo?.description ?? "It's like Gong for Claude Code sessions. Capture every prompt, response, and tool call. Stream to Slack. Collaborate in real time.";
20
20
  const canonical = seo?.canonical ?? "https://app.withpolaris.ai";
@@ -86,7 +86,9 @@ document.addEventListener('click', function(e) {
86
86
 
87
87
  export function nav(token?: string, opts?: NavOpts): string {
88
88
  const right = token
89
- ? `<div class="relative group">
89
+ ? `<a href="/decisions?token=${token}" class="text-sm font-medium text-gray-600 hover:text-gray-900">Decisions</a>
90
+ <a href="/search?token=${token}" class="text-sm font-medium text-gray-600 hover:text-gray-900">Search</a>
91
+ <div class="relative group">
90
92
  <button class="flex items-center gap-2 px-2 py-1.5 rounded-lg hover:bg-gray-100 transition">
91
93
  <div class="w-7 h-7 rounded-full bg-polaris-600 flex items-center justify-center text-white text-xs font-bold">${(opts?.userName ?? "U").charAt(0).toUpperCase()}</div>
92
94
  <div class="text-left">
@@ -102,6 +104,8 @@ export function nav(token?: string, opts?: NavOpts): string {
102
104
  <p class="text-[10px] text-gray-400">${opts?.orgName ?? ""}</p>
103
105
  </div>
104
106
  <a href="/dashboard?token=${token}" class="block px-3 py-2 text-sm text-gray-700 hover:bg-gray-50">Dashboard</a>
107
+ <a href="/decisions?token=${token}" class="block px-3 py-2 text-sm text-gray-700 hover:bg-gray-50">Decisions</a>
108
+ <a href="/search?token=${token}" class="block px-3 py-2 text-sm text-gray-700 hover:bg-gray-50">Search</a>
105
109
  <a href="/profile?token=${token}" class="block px-3 py-2 text-sm text-gray-700 hover:bg-gray-50">Profile</a>
106
110
  <div class="border-t border-gray-100"></div>
107
111
  <a href="/" class="block px-3 py-2 text-sm text-gray-700 hover:bg-gray-50">Log out</a>
@@ -127,7 +131,10 @@ export function nav(token?: string, opts?: NavOpts): string {
127
131
  return `
128
132
  <nav class="border-b border-gray-200 bg-white">
129
133
  <div class="max-w-5xl mx-auto px-6 h-14 flex items-center justify-between">
130
- <a href="${token ? `/dashboard?token=${token}` : "/"}" class="text-lg font-bold tracking-tight text-gray-900">Polaris</a>
134
+ <div class="flex items-center gap-1.5">
135
+ <a href="${token ? `/dashboard?token=${token}` : "/"}" class="text-lg font-bold tracking-tight text-gray-900">Polaris</a>
136
+ <span class="text-sm text-gray-400">by <a href="https://lightup.ai" class="hover:text-gray-600 transition">Lightup</a></span>
137
+ </div>
131
138
  <div class="flex items-center gap-4">${right}</div>
132
139
  </div>
133
140
  </nav>${bannerHtml}`;
package/src/web/pages.ts CHANGED
@@ -488,7 +488,7 @@ export function renderLandingPage(): string {
488
488
 
489
489
  <footer class="border-t border-gray-200 mt-8">
490
490
  <div class="max-w-3xl mx-auto px-6 py-8 flex items-center justify-between text-sm text-gray-400">
491
- <span>Polaris</span>
491
+ <span>© ${new Date().getFullYear()} <a href="https://lightup.ai" class="hover:text-gray-600 transition">Lightup</a>. All rights reserved.</span>
492
492
  <div class="flex items-center gap-6">
493
493
  <a href="https://github.com/lightup-data/polaris/discussions" class="inline-flex items-center gap-1.5 hover:text-gray-600 transition"><svg class="w-4 h-4" viewBox="0 0 16 16" fill="currentColor"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/></svg>Community</a>
494
494
  <a href="/login" class="hover:text-gray-600 transition">Sign in</a>
@@ -505,7 +505,7 @@ export function renderWelcomePage(token: string, name: string, orgName: string,
505
505
  <div class="w-12 h-12 rounded-full bg-green-100 flex items-center justify-center mx-auto mb-4">
506
506
  <svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
507
507
  </div>
508
- <h1 class="text-2xl font-bold text-gray-900">Welcome to Polaris</h1>
508
+ <h1 class="text-2xl font-bold text-gray-900">Welcome to Polaris <span class="font-normal text-lg text-gray-400">by <a href="https://lightup.ai" class="hover:text-gray-600 transition">Lightup</a></span></h1>
509
509
  <p class="mt-2 text-gray-600">${orgName} is ready to go.</p>
510
510
  </div>
511
511
  <div class="mt-8">