chore: add update guide

This commit is contained in:
Abhishek Kumar 2026-04-21 08:49:48 +05:30
parent 6606a7f901
commit 330b81d908
4 changed files with 275 additions and 10 deletions

View file

@ -2,6 +2,7 @@
import type { Team } from "@stackframe/stack";
import {
ArrowUpCircle,
AudioLines,
Brain,
ChevronLeft,
@ -54,6 +55,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useAppConfig } from "@/context/AppConfigContext";
import { useLatestReleaseVersion } from "@/hooks/useLatestReleaseVersion";
import type { LocalUser } from "@/lib/auth";
import { useAuth } from "@/lib/auth";
import { cn } from "@/lib/utils";
@ -89,6 +91,12 @@ export function AppSidebar() {
// Version info from app config context
const versionInfo = config ? { ui: config.uiVersion, api: config.apiVersion } : null;
// Check for updates only on self-hosted (OSS) deployments — cloud is managed for the user.
const { latest: latestRelease, isBehind, isLatest } = useLatestReleaseVersion(
versionInfo?.ui,
{ enabled: config?.deploymentMode === "oss" },
);
const isActive = (path: string) => {
return pathname.startsWith(path);
};
@ -232,17 +240,53 @@ export function AppSidebar() {
<div className="flex items-center justify-between">
{/* Logo - only show when expanded */}
{effectiveState === "expanded" && (
<Link
href="/"
className="flex items-center gap-2 px-2 text-xl font-bold"
>
Dograh
{versionInfo && (
<span className="text-xs font-normal text-muted-foreground">
v{versionInfo.ui}
</span>
<div className="flex items-center gap-2">
<Link
href="/"
className="flex items-center gap-2 px-2 text-xl font-bold"
>
Dograh
{versionInfo && (
<span className="text-xs font-normal text-muted-foreground">
v{versionInfo.ui}
</span>
)}
</Link>
{isBehind && latestRelease && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<a
href="https://docs.dograh.com/deployment/update"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1 rounded-md border bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium leading-none text-amber-900 transition-opacity hover:opacity-80 dark:bg-amber-950 dark:text-amber-200"
>
<ArrowUpCircle className="h-3 w-3" />
Update
</a>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>Latest: {latestRelease} click to see the update guide</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</Link>
{isLatest && (
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<span className="inline-flex items-center rounded-md border bg-emerald-50 px-1.5 py-0.5 text-[10px] font-medium leading-none text-emerald-900 dark:bg-emerald-950 dark:text-emerald-200">
Latest
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
<p>You&apos;re running the latest release</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
)}
{/* Toggle button - center it when collapsed */}
<SidebarTrigger className={cn(

View file

@ -0,0 +1,107 @@
"use client";
import { useEffect, useState } from "react";
interface Options {
enabled: boolean;
}
interface Result {
latest: string | null;
isBehind: boolean;
isLatest: boolean;
}
const CACHE_KEY = "dograh-latest-release";
const CACHE_TTL_MS = 6 * 60 * 60 * 1000;
const SEMVER_RE = /^(?:[a-z][a-z0-9-]*-)?v?(\d+)\.(\d+)\.(\d+)$/i;
function parseSemver(tag: string): [number, number, number] | null {
const m = tag.match(SEMVER_RE);
if (!m) return null;
return [Number(m[1]), Number(m[2]), Number(m[3])];
}
function isOlder(current: string, latest: string): boolean {
const c = parseSemver(current);
const l = parseSemver(latest);
if (!c || !l) return false;
for (let i = 0; i < 3; i++) {
if (c[i] < l[i]) return true;
if (c[i] > l[i]) return false;
}
return false;
}
export function useLatestReleaseVersion(
currentVersion: string | undefined,
{ enabled }: Options,
): Result {
const [latest, setLatest] = useState<string | null>(null);
useEffect(() => {
if (!enabled || !currentVersion) return;
try {
const raw = localStorage.getItem(CACHE_KEY);
if (raw) {
const parsed = JSON.parse(raw) as { tag: string; fetchedAt: number };
if (Date.now() - parsed.fetchedAt < CACHE_TTL_MS) {
setLatest(parsed.tag);
return;
}
}
} catch {
// ignore malformed cache
}
let cancelled = false;
fetch("https://api.github.com/repos/dograh-hq/dograh/releases/latest")
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (cancelled || !data?.tag_name) return;
const tag = data.tag_name as string;
try {
localStorage.setItem(
CACHE_KEY,
JSON.stringify({ tag, fetchedAt: Date.now() }),
);
} catch {
// storage may be full or disabled
}
setLatest(tag);
})
.catch(() => {
// silent — don't break the sidebar if GitHub is unreachable
});
return () => {
cancelled = true;
};
}, [enabled, currentVersion]);
const normalizedCurrent = currentVersion
? currentVersion.startsWith("v")
? currentVersion
: `v${currentVersion}`
: null;
const currentParsed = normalizedCurrent ? parseSemver(normalizedCurrent) : null;
const latestParsed = latest ? parseSemver(latest) : null;
const isBehind = !!(
normalizedCurrent &&
latest &&
isOlder(normalizedCurrent, latest)
);
const isLatest = !!(
currentParsed &&
latestParsed &&
currentParsed[0] === latestParsed[0] &&
currentParsed[1] === latestParsed[1] &&
currentParsed[2] === latestParsed[2]
);
return { latest, isBehind, isLatest };
}