File size: 2,525 Bytes
0163c2d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import { useCallback, useEffect, useState } from "react";
import { useApi } from "@/contexts/ApiContext";
import { isHostedSpace } from "@/lib/isHostedSpace";

export interface UpdateStatus {
  update_available: boolean;
  current_commit: string | null;
  latest_commit: string | null;
  commits_behind: number | null;
  compare_url: string | null;
  update_command: string | null;
  can_auto_update: boolean;
}

// Stores the latest SHA the user chose to ignore via "don't ask again". A newer
// release has a different SHA, so the popup naturally returns — which clears the
// previous opt-out, exactly as intended.
const DISMISS_KEY = "lelab:update-dismissed-sha";

interface UseUpdateCheckResult {
  status: UpdateStatus | null;
  open: boolean;
  /** Close the popup. `dontAskAgain` persists the current SHA so it stays hidden. */
  dismiss: (dontAskAgain: boolean) => void;
}

/**
 * Checks GitHub (via the backend) once on load for a newer LeLab and decides
 * whether to surface the update popup. Skipped on the hosted HF Space (a
 * different runtime that can't be updated this way) and silent on any failure.
 */
export function useUpdateCheck(): UseUpdateCheckResult {
  const { baseUrl, fetchWithHeaders } = useApi();
  const [status, setStatus] = useState<UpdateStatus | null>(null);
  const [open, setOpen] = useState(false);

  useEffect(() => {
    if (isHostedSpace()) return;
    let cancelled = false;
    fetchWithHeaders(`${baseUrl}/system/update-check`)
      .then((r) => (r.ok ? r.json() : null))
      .then((data: UpdateStatus | null) => {
        if (cancelled || !data || !data.update_available) return;
        let dismissed: string | null = null;
        try {
          dismissed = localStorage.getItem(DISMISS_KEY);
        } catch {
          /* localStorage unavailable — show the popup anyway */
        }
        if (dismissed && dismissed === data.latest_commit) return;
        setStatus(data);
        setOpen(true);
      })
      .catch(() => {
        /* backend/GitHub unreachable — stay silent */
      });
    return () => {
      cancelled = true;
    };
  }, [baseUrl, fetchWithHeaders]);

  const dismiss = useCallback(
    (dontAskAgain: boolean) => {
      if (dontAskAgain && status?.latest_commit) {
        try {
          localStorage.setItem(DISMISS_KEY, status.latest_commit);
        } catch {
          /* localStorage unavailable — nothing to persist */
        }
      }
      setOpen(false);
    },
    [status]
  );

  return { status, open, dismiss };
}