mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-06-21 14:21:46 +00:00
fix: address prerelease workflow review bugs
- Type checkVersion() with VersionInfo interface; fixes TS errors in checkAndNotifyVersion() where object type blocked property access - Don't cache fallback on !resp.ok or fetch throw; prevents a transient GitHub outage from poisoning the 5-min version cache - Guard parseInt result with Number.isFinite() in compareVersions; malformed -pre.abc tags no longer silently compare as equal via NaN - Pre-compute stripped versions before sort in checkVersion(); avoids mutating input array and redundant replace() calls in comparator - Bump GitHub releases fetch from per_page=20 to per_page=100 - Store appVersion in authStore; populate from App.tsx getAppConfig call and remove redundant getAppConfig fetch in Navbar useEffect - Type GitHubPanel error/expanded state as string|null and Record<number,boolean>
This commit is contained in:
+2
-1
@@ -88,7 +88,7 @@ function RootRedirect() {
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const { loadUser, isAuthenticated, demoMode, setDemoMode, setDevMode, setIsPrerelease, setHasMapsKey, setServerTimezone, setAppRequireMfa, setTripRemindersEnabled } = useAuthStore()
|
||||
const { loadUser, isAuthenticated, demoMode, setDemoMode, setDevMode, setIsPrerelease, setAppVersion, setHasMapsKey, setServerTimezone, setAppRequireMfa, setTripRemindersEnabled } = useAuthStore()
|
||||
const { loadSettings } = useSettingsStore()
|
||||
|
||||
useEffect(() => {
|
||||
@@ -99,6 +99,7 @@ export default function App() {
|
||||
if (config?.demo_mode) setDemoMode(true)
|
||||
if (config?.dev_mode) setDevMode(true)
|
||||
if (config?.is_prerelease !== undefined) setIsPrerelease(config.is_prerelease)
|
||||
if (config?.version) setAppVersion(config.version)
|
||||
if (config?.has_maps_key !== undefined) setHasMapsKey(config.has_maps_key)
|
||||
if (config?.timezone) setServerTimezone(config.timezone)
|
||||
if (config?.require_mfa !== undefined) setAppRequireMfa(!!config.require_mfa)
|
||||
|
||||
@@ -16,8 +16,8 @@ export default function GitHubPanel({ isPrerelease = false }: { isPrerelease?: b
|
||||
const { t, language } = useTranslation()
|
||||
const [releases, setReleases] = useState<GithubRelease[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState(null)
|
||||
const [expanded, setExpanded] = useState({})
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [expanded, setExpanded] = useState<Record<number, boolean>>({})
|
||||
const [page, setPage] = useState(1)
|
||||
const [hasMore, setHasMore] = useState(true)
|
||||
const [loadingMore, setLoadingMore] = useState(false)
|
||||
|
||||
@@ -27,14 +27,13 @@ interface Addon {
|
||||
}
|
||||
|
||||
export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }: NavbarProps): React.ReactElement {
|
||||
const { user, logout, isPrerelease } = useAuthStore()
|
||||
const { user, logout, isPrerelease, appVersion } = useAuthStore()
|
||||
const { settings, updateSetting } = useSettingsStore()
|
||||
const { addons: allAddons, loadAddons } = useAddonStore()
|
||||
const { t, locale } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const [userMenuOpen, setUserMenuOpen] = useState<boolean>(false)
|
||||
const [appVersion, setAppVersion] = useState<string | null>(null)
|
||||
const darkMode = settings.dark_mode
|
||||
const dark = darkMode === true || darkMode === 'dark' || (darkMode === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||
|
||||
@@ -45,12 +44,6 @@ export default function Navbar({ tripTitle, tripId, onBack, showBack, onShare }:
|
||||
if (user) loadAddons()
|
||||
}, [user, location.pathname])
|
||||
|
||||
useEffect(() => {
|
||||
import('../../api/client').then(({ authApi }) => {
|
||||
authApi.getAppConfig?.().then(c => setAppVersion(c?.version)).catch(() => {})
|
||||
})
|
||||
}, [])
|
||||
|
||||
const handleLogout = () => {
|
||||
logout()
|
||||
navigate('/login', { state: { noRedirect: true } })
|
||||
|
||||
@@ -23,6 +23,7 @@ interface AuthState {
|
||||
demoMode: boolean
|
||||
devMode: boolean
|
||||
isPrerelease: boolean
|
||||
appVersion: string
|
||||
hasMapsKey: boolean
|
||||
serverTimezone: string
|
||||
/** Server policy: all users must enable MFA */
|
||||
@@ -43,6 +44,7 @@ interface AuthState {
|
||||
setDemoMode: (val: boolean) => void
|
||||
setDevMode: (val: boolean) => void
|
||||
setIsPrerelease: (val: boolean) => void
|
||||
setAppVersion: (val: string) => void
|
||||
setHasMapsKey: (val: boolean) => void
|
||||
setServerTimezone: (tz: string) => void
|
||||
setAppRequireMfa: (val: boolean) => void
|
||||
@@ -61,6 +63,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
demoMode: localStorage.getItem('demo_mode') === 'true',
|
||||
devMode: false,
|
||||
isPrerelease: false,
|
||||
appVersion: '',
|
||||
hasMapsKey: false,
|
||||
serverTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
appRequireMfa: false,
|
||||
@@ -226,6 +229,7 @@ export const useAuthStore = create<AuthState>((set, get) => ({
|
||||
|
||||
setDevMode: (val: boolean) => set({ devMode: val }),
|
||||
setIsPrerelease: (val: boolean) => set({ isPrerelease: val }),
|
||||
setAppVersion: (val: string) => set({ appVersion: val }),
|
||||
setHasMapsKey: (val: boolean) => set({ hasMapsKey: val }),
|
||||
setServerTimezone: (tz: string) => set({ serverTimezone: tz }),
|
||||
setAppRequireMfa: (val: boolean) => set({ appRequireMfa: val }),
|
||||
|
||||
@@ -24,7 +24,8 @@ export function compareVersions(a: string, b: string): number {
|
||||
const parse = (v: string) => {
|
||||
const [base, pre] = v.split('-pre.');
|
||||
const parts = base.split('.').map(Number);
|
||||
const preN = pre !== undefined ? parseInt(pre, 10) : null;
|
||||
const n = pre !== undefined ? parseInt(pre, 10) : null;
|
||||
const preN = n !== null && Number.isFinite(n) ? n : null;
|
||||
return { parts, preN };
|
||||
};
|
||||
const pa = parse(a), pb = parse(b);
|
||||
@@ -315,59 +316,63 @@ export async function getGithubReleases(perPage: string = '10', page: string = '
|
||||
}
|
||||
}
|
||||
|
||||
const VERSION_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
let _versionCache: { data: object; expiresAt: number } | null = null;
|
||||
interface VersionInfo {
|
||||
current: string;
|
||||
latest: string;
|
||||
update_available: boolean;
|
||||
release_url?: string;
|
||||
is_docker: boolean;
|
||||
is_prerelease: boolean;
|
||||
}
|
||||
|
||||
export async function checkVersion() {
|
||||
const VERSION_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
let _versionCache: { data: VersionInfo; expiresAt: number } | null = null;
|
||||
|
||||
export async function checkVersion(): Promise<VersionInfo> {
|
||||
if (_versionCache && Date.now() < _versionCache.expiresAt) {
|
||||
return _versionCache.data;
|
||||
}
|
||||
|
||||
const currentVersion: string = process.env.APP_VERSION || require('../../package.json').version;
|
||||
const isPrerelease = currentVersion.includes('-pre.');
|
||||
const fallback = { current: currentVersion, latest: currentVersion, update_available: false, is_docker: isDocker, is_prerelease: isPrerelease };
|
||||
let result: object = fallback;
|
||||
const fallback: VersionInfo = { current: currentVersion, latest: currentVersion, update_available: false, is_docker: isDocker, is_prerelease: isPrerelease };
|
||||
let result: VersionInfo;
|
||||
try {
|
||||
if (isPrerelease) {
|
||||
// Fetch release list and find the newest prerelease
|
||||
const resp = await fetch(
|
||||
'https://api.github.com/repos/mauriceboe/TREK/releases?per_page=20',
|
||||
'https://api.github.com/repos/mauriceboe/TREK/releases?per_page=100',
|
||||
{ headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'TREK-Server' } }
|
||||
);
|
||||
if (!resp.ok) {
|
||||
result = fallback;
|
||||
} else {
|
||||
const data = await resp.json() as Array<{ tag_name?: string; html_url?: string; prerelease?: boolean }>;
|
||||
const prereleases = Array.isArray(data) ? data.filter(r => r.prerelease) : [];
|
||||
if (!prereleases.length) {
|
||||
result = fallback;
|
||||
} else {
|
||||
// Sort by version descending and pick highest
|
||||
const sorted = prereleases.sort((a, b) => compareVersions(
|
||||
(b.tag_name || '').replace(/^v/, ''),
|
||||
(a.tag_name || '').replace(/^v/, '')
|
||||
));
|
||||
const latest = (sorted[0].tag_name || '').replace(/^v/, '');
|
||||
const update_available = !!latest && latest !== currentVersion && compareVersions(latest, currentVersion) > 0;
|
||||
result = { current: currentVersion, latest, update_available, release_url: sorted[0].html_url || '', is_docker: isDocker, is_prerelease: true };
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
const data = await resp.json() as Array<{ tag_name?: string; html_url?: string; prerelease?: boolean }>;
|
||||
const prereleases = Array.isArray(data) ? data.filter(r => r.prerelease) : [];
|
||||
if (!prereleases.length) {
|
||||
return fallback;
|
||||
}
|
||||
// Pre-compute stripped versions, then sort descending
|
||||
const tagged = prereleases.map(r => ({ r, v: (r.tag_name || '').replace(/^v/, '') }));
|
||||
tagged.sort((a, b) => compareVersions(b.v, a.v));
|
||||
const latest = tagged[0].v;
|
||||
const update_available = !!latest && latest !== currentVersion && compareVersions(latest, currentVersion) > 0;
|
||||
result = { current: currentVersion, latest, update_available, release_url: tagged[0].r.html_url || '', is_docker: isDocker, is_prerelease: true };
|
||||
} else {
|
||||
const resp = await fetch(
|
||||
'https://api.github.com/repos/mauriceboe/TREK/releases/latest',
|
||||
{ headers: { 'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'TREK-Server' } }
|
||||
);
|
||||
if (!resp.ok) {
|
||||
result = fallback;
|
||||
} else {
|
||||
const data = await resp.json() as { tag_name?: string; html_url?: string };
|
||||
const latest = (data.tag_name || '').replace(/^v/, '');
|
||||
const update_available = !!latest && latest !== currentVersion && compareVersions(latest, currentVersion) > 0;
|
||||
result = { current: currentVersion, latest, update_available, release_url: data.html_url || '', is_docker: isDocker, is_prerelease: false };
|
||||
return fallback;
|
||||
}
|
||||
const data = await resp.json() as { tag_name?: string; html_url?: string };
|
||||
const latest = (data.tag_name || '').replace(/^v/, '');
|
||||
const update_available = !!latest && latest !== currentVersion && compareVersions(latest, currentVersion) > 0;
|
||||
result = { current: currentVersion, latest, update_available, release_url: data.html_url || '', is_docker: isDocker, is_prerelease: false };
|
||||
}
|
||||
} catch {
|
||||
result = fallback;
|
||||
return fallback;
|
||||
}
|
||||
|
||||
_versionCache = { data: result, expiresAt: Date.now() + VERSION_CACHE_TTL };
|
||||
@@ -389,7 +394,7 @@ export async function checkAndNotifyVersion(): Promise<void> {
|
||||
actorId: null,
|
||||
scope: 'admin',
|
||||
targetId: 0,
|
||||
params: { version: result.latest as string },
|
||||
params: { version: result.latest },
|
||||
});
|
||||
} catch {
|
||||
// Silently ignore — version check is non-critical
|
||||
|
||||
Reference in New Issue
Block a user