Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
8b62012
init changelog route
madster456 Dec 21, 2025
903ea2c
init changelog parse
madster456 Dec 21, 2025
50bd17e
Update for new changelog
madster456 Dec 21, 2025
3b10389
Merge branch 'dev' into dashboard/changelog
madster456 Jan 8, 2026
5aad4e6
changelog in stack-companion
madster456 Jan 8, 2026
13f023a
Merge branch 'dev' into dashboard/changelog
madster456 Jan 8, 2026
ec8d86e
Merge branch 'dev' into dashboard/changelog
madster456 Jan 10, 2026
7d112df
Merge branch 'dev' into dashboard/changelog
madster456 Jan 14, 2026
3971834
cookie parsing and string comparing
madster456 Jan 14, 2026
74d3dfb
update properly typed props
madster456 Jan 14, 2026
0a30feb
Removed unused code
madster456 Jan 14, 2026
096fa26
abort controller, empty array handling, error propagation from parent
madster456 Jan 14, 2026
31e7e76
knosti's notes
madster456 Jan 22, 2026
3c250fc
Merge branch 'dev' into dashboard/changelog
madster456 Jan 27, 2026
5dfed8c
Move changelog fetching to backend
madster456 Jan 27, 2026
c6773ee
Ignore unreleased in changelog
madster456 Jan 27, 2026
030b8c5
Merge branch 'dev' into dashboard/changelog
madster456 Jan 27, 2026
872c3ba
Merge branch 'dev' into dashboard/changelog
madster456 Jan 27, 2026
da92216
Fixed type mistmatch
madster456 Jan 27, 2026
d1e4bd2
Merge branch 'dev' into dashboard/changelog
madster456 Jan 28, 2026
63cca4b
Merge branch 'dev' into dashboard/changelog
madster456 Jan 28, 2026
2cf1d0d
Merge branch 'dev' into dashboard/changelog
madster456 Jan 28, 2026
49f674a
Merge branch 'dev' into dashboard/changelog
madster456 Jan 28, 2026
d69f72f
Merge branch 'dev' into dashboard/changelog
madster456 Jan 28, 2026
c7cf181
Merge branch 'dev' into dashboard/changelog
madster456 Jan 29, 2026
1542480
Merge branch 'dev' into dashboard/changelog
madster456 Jan 29, 2026
ae6b94b
Merge branch 'dev' into dashboard/changelog
madster456 Jan 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/dashboard/.env
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ STACK_DEVELOPMENT_TRANSLATION_LOCALE=# enter the locale to use for the translati
NEXT_PUBLIC_STACK_ENABLE_DEVELOPMENT_FEATURES_PROJECT_IDS='["internal"]'
NEXT_PUBLIC_STACK_DEBUGGER_ON_ASSERTION_ERROR=# set to true to open the debugger on assertion errors (set to true in .env.development)
STACK_FEATUREBASE_JWT_SECRET=# used for Featurebase SSO, you probably won't have to set this
STACK_CHANGELOG_URL=# Used for raw github link to root changelog.md file.
42 changes: 42 additions & 0 deletions apps/dashboard/src/app/api/changelog/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { parseRootChangelog } from "@/lib/changelog";
import { NextResponse } from "next/server";

const REVALIDATE_SECONDS = 60 * 60;

export async function GET() {
const changelogUrl = process.env.STACK_CHANGELOG_URL;

if (!changelogUrl) {
return NextResponse.json({ entries: [] });
}

try {
const response = await fetch(changelogUrl, {
headers: {
"Accept": "text/plain",
"User-Agent": "stack-auth-dashboard-changelog-widget",
},
next: {
revalidate: REVALIDATE_SECONDS,
},
});

if (!response.ok) {
return NextResponse.json(
{ error: "Failed to download changelog" },
{ status: 502 },
);
}

const content = await response.text();
const entries = parseRootChangelog(content).slice(0, 8);

return NextResponse.json({ entries });
} catch (error) {
console.error("Failed to fetch remote changelog", error);
return NextResponse.json(
{ error: "Failed to fetch changelog" },
{ status: 500 },
);
}
}
Comment thread
madster456 marked this conversation as resolved.
Outdated
86 changes: 83 additions & 3 deletions apps/dashboard/src/components/stack-companion.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
'use client';

import { Button, Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui';
import { ChangelogEntry } from '@/lib/changelog';
Comment thread
madster456 marked this conversation as resolved.
import { cn } from '@/lib/utils';
import { checkVersion, VersionCheckResult } from '@/lib/version-check';
import { BookOpenIcon, CircleNotchIcon, ClockClockwiseIcon, LightbulbIcon, XIcon } from '@phosphor-icons/react';
import { runAsynchronously } from '@stackframe/stack-shared/dist/utils/promises';
import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react';
import packageJson from '../../package.json';
import { FeedbackForm } from './feedback-form';
Expand Down Expand Up @@ -73,6 +75,7 @@ export function useStackCompanion() {
return useContext(StackCompanionContext);
}


export function StackCompanion({ className }: { className?: string }) {
const [activeItem, setActiveItem] = useState<string | null>(null);
const [mounted, setMounted] = useState(false);
Expand All @@ -82,6 +85,9 @@ export function StackCompanion({ className }: { className?: string }) {
const [isAnimating, setIsAnimating] = useState(false);
const [isDragging, setIsDragging] = useState(false);
const [isSplitScreenMode, setIsSplitScreenMode] = useState(false);
const [changelogData, setChangelogData] = useState<ChangelogEntry[]>([]);
const [hasNewVersions, setHasNewVersions] = useState(false);
const [lastSeenVersion, setLastSeenVersion] = useState('');

const startXRef = useRef(0);
const startWidthRef = useRef(0);
Expand Down Expand Up @@ -125,6 +131,72 @@ export function StackCompanion({ className }: { className?: string }) {
return cleanup;
}, []);

// Fetch changelog data on mount and check for new versions
useEffect(() => {
const fetchChangelogData = async () => {
try {
const response = await fetch('/api/changelog');
if (response.ok) {
const payload = await response.json();
const entries = payload.entries || [];
setChangelogData(entries);

// Check for new versions
const lastSeen = document.cookie
.split('; ')
.find(row => row.startsWith('stack-last-seen-changelog-version='))
?.split('=')[1] || '';

setLastSeenVersion(lastSeen);

if (entries.length > 0) {
// If no lastSeen cookie, user hasn't seen any changelog yet - show bell
if (!lastSeen) {
setHasNewVersions(true);
} else {
const hasNewer = entries.some((entry: ChangelogEntry) => {
if (entry.isUnreleased) return false;
return entry.version > lastSeen;
Comment thread
madster456 marked this conversation as resolved.
Outdated
});
setHasNewVersions(hasNewer);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}
}
} catch (error) {
console.error('Failed to fetch changelog data:', error);
}
Comment thread
madster456 marked this conversation as resolved.
};

runAsynchronously(fetchChangelogData());
Comment thread
madster456 marked this conversation as resolved.
Outdated
}, []);
Comment thread
madster456 marked this conversation as resolved.

// Re-check for new versions when changelog is opened/closed
useEffect(() => {
if (activeItem === 'changelog' || activeItem === null) {
// Re-check versions when opening or closing changelog
const lastSeen = document.cookie
.split('; ')
.find(row => row.startsWith('stack-last-seen-changelog-version='))
?.split('=')[1] || '';

if (changelogData.length > 0) {
// If no lastSeen cookie, user hasn't seen any changelog yet - show bell
if (!lastSeen) {
setHasNewVersions(true);
} else {
const hasNewer = changelogData.some((entry: ChangelogEntry) => {
if (entry.isUnreleased) return false;
return entry.version > lastSeen;
});
setHasNewVersions(hasNewer);
Comment thread
madster456 marked this conversation as resolved.
}
} else {
setHasNewVersions(false);
}
}
}, [activeItem, changelogData]);


const openDrawer = useCallback((itemId: string) => {
setActiveItem(itemId);
setIsAnimating(true);
Expand Down Expand Up @@ -304,7 +376,7 @@ export function StackCompanion({ className }: { className?: string }) {
<div className="flex-1 overflow-y-auto p-5 overflow-x-hidden no-drag cursor-auto">
{activeItem === 'docs' && <UnifiedDocsWidget isActive={true} />}
{activeItem === 'feedback' && <FeatureRequestBoard isActive={true} />}
{activeItem === 'changelog' && <ChangelogWidget isActive={true} />}
{activeItem === 'changelog' && <ChangelogWidget isActive={true} initialData={changelogData} />}
{activeItem === 'support' && <FeedbackForm />}
</div>
</div>
Expand Down Expand Up @@ -338,18 +410,26 @@ export function StackCompanion({ className }: { className?: string }) {
className={cn(
"h-10 w-10 p-0 text-muted-foreground transition-all duration-[50ms] rounded-xl relative group",
item.hoverBg,
activeItem === item.id && "bg-foreground/10 text-foreground shadow-sm ring-1 ring-foreground/5"
activeItem === item.id && "bg-foreground/10 text-foreground shadow-sm ring-1 ring-foreground/5",
// Glow effect for changelog with new updates
item.id === 'changelog' && hasNewVersions && "ring-2 ring-green-500/30 bg-green-500/10"
)}
onClick={(e) => {
e.stopPropagation();
handleItemClick(item.id);
}}
>
<item.icon className={cn("h-5 w-5 transition-transform duration-[50ms] group-hover:scale-110", item.color)} />
{item.id === 'changelog' && hasNewVersions && (
<span className="absolute -top-1 -right-1 flex h-3 w-3">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75" />
<span className="relative inline-flex rounded-full h-3 w-3 bg-green-500" />
</span>
)}
</Button>
</TooltipTrigger>
<TooltipContent side="left" className="z-[60] mr-2">
{item.label}
{item.id === 'changelog' && hasNewVersions ? `${item.label} (New updates available!)` : item.label}
</TooltipContent>
</Tooltip>
))}
Expand Down
Loading