Files
firefrost-services/services/modpack-version-checker/blueprint-extension/views/server/wrapper.tsx
Claude c0b6bc5a22 Fix: pending_calibration shows Identify Version button instead of checkmark
Chronicler #85 direct fix (Code unavailable on mobile):
- ModpackAPIController: add pending_calibration flag to serverStatus response
- wrapper.tsx: add pending_calibration to StatusData interface
- wrapper.tsx: render Identify Version button when pending_calibration is true
Replaces false green checkmark for servers needing calibration.
2026-04-13 11:57:52 +00:00

308 lines
13 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { ServerContext } from '@/state/server';
import http from '@/api/http';
import { faCube, faSync, faEyeSlash, faCog } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import classNames from 'classnames';
interface StatusData {
configured: boolean;
platform?: string;
modpack_name?: string;
current_version?: string;
latest_version?: string;
update_available?: boolean;
last_checked?: string;
detection_method?: string;
is_ignored?: boolean;
pending_calibration?: boolean;
}
interface Release {
file_id: string;
version: string;
display_name: string;
release_date?: string;
}
const platformIcons: Record<string, string> = {
curseforge: '🔥',
modrinth: '🍃',
technic: '⚙️',
ftb: '📦',
};
const ModpackVersionCard: React.FC = () => {
const uuid = ServerContext.useStoreState((state) => state.server.data?.uuid);
const [data, setData] = useState<StatusData | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [checking, setChecking] = useState(false);
const [showCalibrate, setShowCalibrate] = useState(false);
const [releases, setReleases] = useState<Release[]>([]);
const [loadingReleases, setLoadingReleases] = useState(false);
// Zero-click: load cached status on mount
useEffect(() => {
if (!uuid) return;
http.get(`/api/client/extensions/modpackchecker/servers/${uuid}/status`)
.then((res) => { setData(res.data); setError(null); })
.catch(() => setError('Unable to load modpack status.'))
.finally(() => setLoading(false));
}, [uuid]);
// Manual refresh
const refresh = async () => {
if (!uuid || checking) return;
setChecking(true);
setError(null);
try {
await http.post(`/api/client/extensions/modpackchecker/servers/${uuid}/check`);
const res = await http.get(`/api/client/extensions/modpackchecker/servers/${uuid}/status`);
setData(res.data);
} catch {
setError('Check failed. Try again.');
}
setChecking(false);
};
// Open calibrate dropdown
const openCalibrate = async () => {
if (!uuid) return;
setShowCalibrate(true);
setLoadingReleases(true);
try {
const res = await http.get(`/api/client/extensions/modpackchecker/servers/${uuid}/releases`);
setReleases(res.data.releases || []);
} catch {
setReleases([]);
}
setLoadingReleases(false);
};
// Select a release to calibrate
const selectRelease = async (release: Release) => {
if (!uuid) return;
try {
const res = await http.post(`/api/client/extensions/modpackchecker/servers/${uuid}/calibrate`, {
file_id: release.file_id,
version: release.version,
});
setData((prev) => prev ? {
...prev,
current_version: release.version,
update_available: res.data.update_available,
} : prev);
} catch {}
setShowCalibrate(false);
};
// Ignore server
const toggleIgnore = async () => {
if (!uuid) return;
try {
const res = await http.post(`/api/client/extensions/modpackchecker/servers/${uuid}/ignore`);
setData((prev) => prev ? { ...prev, is_ignored: res.data.is_ignored } : prev);
} catch {}
};
if (loading) return null;
// Muted card for ignored servers (with Resume button)
if (data?.is_ignored) {
return (
<div className={classNames(
'rounded shadow-lg bg-gray-600 opacity-50',
'col-span-3 md:col-span-2 lg:col-span-6',
'px-3 py-2 md:p-3 lg:p-4 mt-2'
)}>
<div className={'flex items-center justify-between'}>
<div className={'flex items-center'}>
<FontAwesomeIcon icon={faCube} className={'w-4 h-4 mr-2 text-gray-400'} />
<span className={'text-gray-400 text-sm'}>
{data.modpack_name || 'Modpack'} Updates ignored
</span>
</div>
<button onClick={toggleIgnore}
className={'text-xs px-3 py-1 bg-gray-700 hover:bg-gray-600 text-gray-200 rounded transition-colors'}>
Resume
</button>
</div>
</div>
);
}
if (error && !data) return (
<div className={classNames(
'rounded shadow-lg bg-gray-600 text-gray-400 text-xs',
'col-span-3 md:col-span-2 lg:col-span-6',
'px-3 py-2 mt-2'
)}>
<FontAwesomeIcon icon={faCube} className={'w-3 h-3 mr-2'} />{error}
</div>
);
// Pending calibration — version unknown, show Identify Version button
if (data?.pending_calibration) {
return (
<div className={classNames(
'rounded shadow-lg bg-gray-600',
'col-span-3 md:col-span-2 lg:col-span-6',
'px-3 py-2 md:p-3 lg:p-4 mt-2'
)}>
<div className={'flex items-center justify-between'}>
<div className={'flex items-center'}>
<FontAwesomeIcon icon={faCube} className={'w-4 h-4 mr-2 text-gray-400'} />
<span className={'text-gray-400 text-sm'}>
{data.modpack_name || 'Modpack'} Version unknown
</span>
</div>
<button onClick={openCalibrate}
className={'text-xs px-3 py-1 bg-cyan-600 hover:bg-cyan-500 text-white rounded transition-colors'}>
Identify Version
</button>
</div>
</div>
);
}
// Calibrate dropdown (reusable)
const renderCalibrateDropdown = () => (
<div className={'mt-2 bg-gray-800 rounded p-2 max-h-48 overflow-y-auto'}>
<p className={'text-xs text-gray-400 mb-1'}>Select your installed version:</p>
{loadingReleases && <p className={'text-xs text-gray-500'}>Loading releases...</p>}
{!loadingReleases && releases.length === 0 && (
<p className={'text-xs text-gray-500'}>No releases found</p>
)}
{releases.map((r) => (
<button key={r.file_id} onClick={() => selectRelease(r)}
className={classNames(
'block w-full text-left px-2 py-1 text-sm rounded',
'hover:bg-gray-700 text-gray-200',
data?.current_version === r.version ? 'bg-gray-700 text-cyan-300' : ''
)}>
{r.display_name}
{r.release_date && (
<span className={'text-gray-500 text-xs ml-2'}>
{new Date(r.release_date).toLocaleDateString()}
</span>
)}
</button>
))}
<button onClick={() => setShowCalibrate(false)}
className={'mt-1 text-xs text-gray-500 hover:text-gray-300'}>
Cancel
</button>
</div>
);
// Pending calibration — show "Identify Version" prompt
if (data && !data.configured && data.modpack_name) {
return (
<div className={classNames(
'rounded shadow-lg bg-gray-600',
'col-span-3 md:col-span-2 lg:col-span-6',
'px-3 py-2 md:p-3 lg:p-4 mt-2'
)}>
<div className={'flex items-center justify-between'}>
<div className={'flex items-center'}>
<FontAwesomeIcon icon={faCube} className={'w-4 h-4 mr-2 text-gray-400'} />
<span className={'text-gray-400 text-sm'}>
{data.modpack_name} Version unknown
</span>
</div>
<div className={'flex gap-1'}>
<button onClick={openCalibrate}
className={'text-xs px-3 py-1 bg-cyan-600 hover:bg-cyan-500 text-white rounded transition-colors'}>
Identify Version
</button>
<button onClick={toggleIgnore}
className={'text-gray-400 hover:text-gray-200 p-1'} title={'Hide'}>
<FontAwesomeIcon icon={faEyeSlash} className={'w-3 h-3'} />
</button>
</div>
</div>
{showCalibrate && renderCalibrateDropdown()}
</div>
);
}
const hasUpdate = data.update_available;
const configured = data.configured;
// Short name: "All the Mods 9 - ATM9" → "ATM9"
const shortName = (data.modpack_name?.includes(' - ')
? data.modpack_name.split(' - ').pop()
: data.modpack_name) || 'Modpack';
// Extract semver from version strings (e.g. "All the Mods 9-0.1.0" → "0.1.0")
const extractVersion = (v?: string) => {
if (!v) return '?';
const match = v.match(/[\d]+\.[\d]+[\d.]*$/);
return match ? match[0] : v;
};
const bgColor = hasUpdate ? 'bg-orange-500' : configured ? 'bg-cyan-500' : 'bg-gray-700';
const icon = data.platform ? (platformIcons[data.platform] || '📦') : '📦';
return (
<div className={classNames(
'rounded shadow-lg relative bg-gray-600',
'col-span-3 md:col-span-2 lg:col-span-6',
'px-3 py-2 md:p-3 lg:p-4 mt-2'
)}>
<div className={classNames('w-1 h-full absolute left-0 top-0 rounded-l sm:hidden', bgColor)} />
<div className={'flex items-center'}>
<div className={classNames(
'hidden flex-shrink-0 items-center justify-center rounded-lg shadow-md w-12 h-12',
'sm:flex sm:mr-4', bgColor
)}>
<FontAwesomeIcon icon={faCube} className={'w-6 h-6 text-gray-50'} />
</div>
<div className={'flex flex-col justify-center overflow-hidden w-full'}>
<div className={'flex items-center justify-between'}>
<p className={'font-header font-medium leading-tight text-xs md:text-sm text-gray-200'}>
{configured ? `${icon} ${data.modpack_name || 'Modpack'}` : 'Modpack Version'}
</p>
<div className={'flex gap-1'}>
<button onClick={refresh} disabled={checking}
className={'text-gray-400 hover:text-gray-200 p-1'} title={'Refresh'}>
<FontAwesomeIcon icon={faSync} spin={checking} className={'w-3 h-3'} />
</button>
{configured && (
<button onClick={openCalibrate}
className={'text-gray-400 hover:text-gray-200 p-1'} title={'Calibrate version'}>
<FontAwesomeIcon icon={faCog} className={'w-3 h-3'} />
</button>
)}
<button onClick={toggleIgnore}
className={'text-gray-400 hover:text-gray-200 p-1'} title={'Hide (not a modpack)'}>
<FontAwesomeIcon icon={faEyeSlash} className={'w-3 h-3'} />
</button>
</div>
</div>
<div className={'font-semibold text-gray-50 truncate text-sm'}>
{!configured && <span className={'text-gray-400'}>Not detected waiting for cron</span>}
{configured && !hasUpdate && (
<span className={'text-cyan-300'}> {shortName} {extractVersion(data.latest_version)}</span>
)}
{configured && hasUpdate && (
<span className={'text-orange-300'}>
{shortName} {extractVersion(data.current_version)} {extractVersion(data.latest_version)}
</span>
)}
</div>
</div>
</div>
{/* Calibrate dropdown */}
{showCalibrate && renderCalibrateDropdown()}
</div>
);
};
export default ModpackVersionCard;