Truth File strategy: never seed from latest, calibrate or detect
CheckModpackUpdates: - Reads .modpack-checker.json Truth File from server filesystem - Falls back to manifest.json, extracts fileID, writes Truth File - NEVER seeds current_version from latest API result - Unknown version → status: pending_calibration (not up_to_date) - Removed seedCurrentVersion heuristic — replaced with Truth File - writeTruthFile() helper writes .modpack-checker.json via Wings ModpackAPIController: - calibrate() now writes Truth File after DB update - Persists across server reinstalls and cron runs wrapper.tsx: - pending_calibration: shows "Version unknown" + "Identify Version" button - Ignored servers: muted card with "Resume" button (not hidden) - Extracted renderCalibrateDropdown() for reuse - Error state shows message instead of vanishing Migration: - Updates existing unknown+detected rows to pending_calibration Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f39b6d6c67
commit
27b2744786
@@ -0,0 +1,264 @@
|
||||
# MSG-2026-04-13-truth-file-version-detection
|
||||
|
||||
**From:** Chronicler #85
|
||||
**Date:** 2026-04-13
|
||||
**Priority:** HIGH — needed before BuiltByBit listings, ideally before April 15
|
||||
**Status:** OPEN
|
||||
|
||||
## Background
|
||||
|
||||
After 3 rounds of Gemini consultation and live Wings API testing, we have a
|
||||
complete architectural decision for version detection. Here's everything you
|
||||
need to implement it.
|
||||
|
||||
---
|
||||
|
||||
## What We Proved via Live Testing
|
||||
|
||||
- `DaemonFileRepository::getContent()` and `putContent()` both work from Panel
|
||||
PHP context (confirmed via tinker on live panel)
|
||||
- `manifest.json` does NOT survive Pterodactyl modpack installation on any of
|
||||
our 22 servers — installer discards it
|
||||
- `modpack_installations` table has no version info — only provider + modpack_id
|
||||
- Wings directory listing works — we can read the server filesystem
|
||||
|
||||
---
|
||||
|
||||
## The Fix: Truth File Strategy
|
||||
|
||||
### New Detection Order (replaces current fall-through logic)
|
||||
|
||||
```
|
||||
1. DB: current_file_id present? → skip to comparison
|
||||
2. Wings: read /.modpack-checker.json → parse file_id → store in DB → compare
|
||||
3. Wings: read /manifest.json → parse file_id → write Truth File → store in DB → compare
|
||||
4. Nothing found → set status = 'pending_calibration' → STOP (never seed from latest)
|
||||
```
|
||||
|
||||
**CRITICAL RULE: Never seed current_version from latest API result.**
|
||||
If we don't know the installed version, we say we don't know. Store `null` /
|
||||
`pending_calibration`, never assume latest = installed.
|
||||
|
||||
---
|
||||
|
||||
## The Truth File
|
||||
|
||||
Write `.modpack-checker.json` to server root via `DaemonFileRepository::putContent()`.
|
||||
|
||||
```json
|
||||
{
|
||||
"extension": "modpackchecker",
|
||||
"project_id": "490660",
|
||||
"file_id": "7097953",
|
||||
"version": "5.10.15",
|
||||
"calibrated_at": "2026-04-13T05:00:00+00:00"
|
||||
}
|
||||
```
|
||||
|
||||
**Write the Truth File in TWO scenarios:**
|
||||
1. When admin calibrates (picks version from dropdown)
|
||||
2. When ANY detection method successfully finds the version (manifest.json etc.)
|
||||
|
||||
This makes detection self-healing — once tracked by any method, always tracked
|
||||
even if the original source file disappears in a future update.
|
||||
|
||||
---
|
||||
|
||||
## New DB Status Value
|
||||
|
||||
Add `pending_calibration` to the status enum in `modpackchecker_servers`:
|
||||
```sql
|
||||
ALTER TABLE modpackchecker_servers
|
||||
MODIFY COLUMN status ENUM(
|
||||
'up_to_date','update_available','error','unknown','pending_calibration'
|
||||
) NOT NULL DEFAULT 'unknown';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Files to Change
|
||||
|
||||
### 1. `app/Console/Commands/CheckModpackUpdates.php`
|
||||
|
||||
In `checkVersion()` — replace the seeding fallback with Truth File logic:
|
||||
|
||||
```php
|
||||
// After existing detection chain (egg var, existing DB)...
|
||||
|
||||
// NEW: Check for Truth File on server filesystem
|
||||
if (empty($currentVersion)) {
|
||||
try {
|
||||
$repo = app(\Pterodactyl\Repositories\Wings\DaemonFileRepository::class)
|
||||
->setServer($server);
|
||||
$truthFile = json_decode($repo->getContent('/.modpack-checker.json'), true);
|
||||
if (!empty($truthFile['file_id'])) {
|
||||
$currentVersion = $truthFile['version'] ?? null;
|
||||
$currentFileId = $truthFile['file_id'];
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// File doesn't exist — continue to next check
|
||||
}
|
||||
}
|
||||
|
||||
// NEW: Check for legacy manifest.json
|
||||
if (empty($currentVersion)) {
|
||||
try {
|
||||
$repo = $repo ?? app(\Pterodactyl\Repositories\Wings\DaemonFileRepository::class)
|
||||
->setServer($server);
|
||||
$manifest = json_decode($repo->getContent('/manifest.json'), true);
|
||||
if (!empty($manifest['files'][0]['fileID'])) {
|
||||
$currentFileId = (string) $manifest['files'][0]['fileID'];
|
||||
// Write Truth File immediately so it persists
|
||||
$this->writeTruthFile($server, $modpackId, $currentFileId, null);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// File doesn't exist — continue
|
||||
}
|
||||
}
|
||||
|
||||
// REMOVE the seeding fallback entirely. Replace with:
|
||||
if (empty($currentVersion) && empty($currentFileId)) {
|
||||
$this->updateDatabase($server, [
|
||||
'platform' => $platform,
|
||||
'modpack_id' => $modpackId,
|
||||
'modpack_name' => $latestData['name'],
|
||||
'status' => 'pending_calibration',
|
||||
'detection_method' => $method,
|
||||
'last_checked' => now(),
|
||||
]);
|
||||
$this->info(" ⏳ PENDING: {$latestData['name']} — calibration required");
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
Add a `writeTruthFile()` helper method:
|
||||
```php
|
||||
private function writeTruthFile(Server $server, string $projectId,
|
||||
string $fileId, ?string $version): void
|
||||
{
|
||||
try {
|
||||
$repo = app(\Pterodactyl\Repositories\Wings\DaemonFileRepository::class)
|
||||
->setServer($server);
|
||||
$repo->putContent('/.modpack-checker.json', json_encode([
|
||||
'extension' => 'modpackchecker',
|
||||
'project_id' => $projectId,
|
||||
'file_id' => $fileId,
|
||||
'version' => $version,
|
||||
'calibrated_at' => now()->toIso8601String(),
|
||||
], JSON_PRETTY_PRINT));
|
||||
} catch (\Exception $e) {
|
||||
// Non-fatal — log and continue
|
||||
\Log::warning('[MVC] Could not write Truth File: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. `app/Http/Controllers/ModpackAPIController.php`
|
||||
|
||||
In the `calibrate()` method — after storing the file_id in DB, write the Truth File:
|
||||
|
||||
```php
|
||||
// After DB update in calibrate()...
|
||||
$this->writeTruthFileForServer($server, $modpackId, $fileId, $version);
|
||||
```
|
||||
|
||||
Add the same `writeTruthFile` logic (or extract to a shared service).
|
||||
|
||||
### 3. `views/server/wrapper.tsx`
|
||||
|
||||
Add `pending_calibration` handling. When `data.configured === false` and
|
||||
status is pending, show calibration prompt instead of empty/error state:
|
||||
|
||||
```tsx
|
||||
// After error state check, before main render...
|
||||
if (!data.configured && !data.update_available) {
|
||||
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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Ignore toggle — Muted Card (from previous Gemini consultation)
|
||||
|
||||
Replace `if (data.is_ignored) return null;` with:
|
||||
|
||||
```tsx
|
||||
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>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Needed
|
||||
|
||||
```sql
|
||||
ALTER TABLE modpackchecker_servers
|
||||
MODIFY COLUMN status ENUM(
|
||||
'up_to_date','update_available','error','unknown','pending_calibration'
|
||||
) NOT NULL DEFAULT 'unknown';
|
||||
```
|
||||
|
||||
Add to a new migration file:
|
||||
`database/migrations/2026_04_13_000002_add_pending_calibration_status.php`
|
||||
|
||||
---
|
||||
|
||||
## Post-Launch Enhancement (NOT for April 15)
|
||||
|
||||
Log parsing at server restart — possible future detection method. Every modpack
|
||||
formats startup logs differently, too brittle for now. Flag as Task for after
|
||||
launch.
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Server with no Truth File → shows "Identify Version" button
|
||||
- [ ] Admin selects version → Truth File written to server root → DB updated
|
||||
- [ ] Cron reads Truth File on next run → correct status shown
|
||||
- [ ] Server shows update available after Truth File written with old version
|
||||
- [ ] Ignore toggle → muted card with Resume button (not vanish)
|
||||
- [ ] Resume → normal card restored
|
||||
- [ ] Never seeds current_version from latest API result
|
||||
|
||||
---
|
||||
*— Chronicler #85*
|
||||
@@ -240,7 +240,7 @@ class CheckModpackUpdates extends Command
|
||||
$latestVersion = $latestData['version'] ?? 'Unknown';
|
||||
$latestFileId = $latestData['file_id'] ?? null;
|
||||
|
||||
// Get current_version: manifest > egg variable > existing DB > seed with latest
|
||||
// Get current_version: manifest arg > egg variable > existing DB
|
||||
$currentVersion = $installedVersion;
|
||||
$currentFileId = null;
|
||||
|
||||
@@ -257,11 +257,53 @@ class CheckModpackUpdates extends Command
|
||||
}
|
||||
$currentFileId = $existing->current_file_id ?? null;
|
||||
|
||||
// First time — try date-time seeding, fall back to latest
|
||||
if (empty($currentVersion)) {
|
||||
$seeded = $this->seedCurrentVersion($server, $platform, $modpackId, $latestVersion, $latestFileId);
|
||||
$currentVersion = $seeded['version'];
|
||||
$currentFileId = $seeded['file_id'];
|
||||
// Truth File: read .modpack-checker.json from server filesystem
|
||||
if (empty($currentFileId)) {
|
||||
try {
|
||||
$this->fileRepository->setServer($server);
|
||||
$truthRaw = $this->fileRepository->getContent('.modpack-checker.json');
|
||||
$truthFile = json_decode($truthRaw, true);
|
||||
if (!empty($truthFile['file_id'])) {
|
||||
$currentFileId = $truthFile['file_id'];
|
||||
$currentVersion = $currentVersion ?: ($truthFile['version'] ?? null);
|
||||
$this->line(" [truth] Read file_id {$currentFileId} from .modpack-checker.json");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Truth File doesn't exist yet
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy manifest.json: extract fileID, write Truth File
|
||||
if (empty($currentFileId)) {
|
||||
try {
|
||||
$this->fileRepository->setServer($server);
|
||||
$manifest = json_decode($this->fileRepository->getContent('manifest.json'), true);
|
||||
if (!empty($manifest['files'][0]['fileID'])) {
|
||||
$currentFileId = (string) $manifest['files'][0]['fileID'];
|
||||
$this->writeTruthFile($server, $modpackId, $currentFileId, $manifest['version'] ?? null);
|
||||
$currentVersion = $currentVersion ?: ($manifest['version'] ?? null);
|
||||
$this->line(" [manifest] Extracted file_id {$currentFileId}, wrote Truth File");
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// No manifest
|
||||
}
|
||||
}
|
||||
|
||||
// NEVER seed from latest — if unknown, mark pending_calibration
|
||||
if (empty($currentVersion) && empty($currentFileId)) {
|
||||
$this->updateDatabase($server, [
|
||||
'platform' => $platform,
|
||||
'modpack_id' => $modpackId,
|
||||
'modpack_name' => $latestData['name'],
|
||||
'latest_version' => $latestVersion,
|
||||
'latest_file_id' => $latestFileId,
|
||||
'status' => 'pending_calibration',
|
||||
'detection_method' => $method,
|
||||
'error_message' => null,
|
||||
'last_checked' => now(),
|
||||
]);
|
||||
$this->info(" ⏳ PENDING: {$latestData['name']} — calibration required");
|
||||
return;
|
||||
}
|
||||
|
||||
// Compare: prefer file ID, fall back to string
|
||||
@@ -328,40 +370,22 @@ class CheckModpackUpdates extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
* Seed current version using date-time heuristic.
|
||||
* Finds the release closest to (but not after) the server's created_at date.
|
||||
* Write .modpack-checker.json Truth File to server filesystem.
|
||||
*/
|
||||
private function seedCurrentVersion(Server $server, string $platform, string $modpackId, string $fallbackVersion, ?string $fallbackFileId): array
|
||||
private function writeTruthFile(Server $server, string $projectId, string $fileId, ?string $version): void
|
||||
{
|
||||
try {
|
||||
$installDate = $server->created_at?->toISOString();
|
||||
if (!$installDate) {
|
||||
return ['version' => $fallbackVersion, 'file_id' => $fallbackFileId];
|
||||
}
|
||||
|
||||
$history = $this->apiService->fetchFileHistory($platform, $modpackId);
|
||||
if (empty($history)) {
|
||||
return ['version' => $fallbackVersion, 'file_id' => $fallbackFileId];
|
||||
}
|
||||
|
||||
// Find the release closest to but not after install date
|
||||
$matched = collect($history)
|
||||
->filter(fn($f) => !empty($f['release_date']) && $f['release_date'] <= $installDate)
|
||||
->sortByDesc('release_date')
|
||||
->first();
|
||||
|
||||
if ($matched) {
|
||||
$this->line(" [seed] Matched version {$matched['version']} (released {$matched['release_date']}) to install date {$installDate}");
|
||||
return [
|
||||
'version' => $matched['version'],
|
||||
'file_id' => $matched['file_id'] ?? $fallbackFileId,
|
||||
];
|
||||
}
|
||||
$this->fileRepository->setServer($server);
|
||||
$this->fileRepository->putContent('.modpack-checker.json', json_encode([
|
||||
'extension' => 'modpackchecker',
|
||||
'project_id' => $projectId,
|
||||
'file_id' => $fileId,
|
||||
'version' => $version,
|
||||
'calibrated_at' => now()->toIso8601String(),
|
||||
], JSON_PRETTY_PRINT));
|
||||
} catch (\Exception $e) {
|
||||
$this->line(" [seed] Heuristic failed: " . $e->getMessage());
|
||||
\Log::warning('[MVC] Could not write Truth File: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return ['version' => $fallbackVersion, 'file_id' => $fallbackFileId];
|
||||
}
|
||||
|
||||
private function getVariable(Server $server, string $name): ?string
|
||||
|
||||
@@ -394,6 +394,21 @@ class ModpackAPIController extends Controller
|
||||
'status' => $updateAvailable ? 'update_available' : 'up_to_date',
|
||||
]);
|
||||
|
||||
// Write Truth File to server filesystem
|
||||
$modpackId = $cached->modpack_id ?? '';
|
||||
try {
|
||||
$this->fileRepository->setServer($server);
|
||||
$this->fileRepository->putContent('.modpack-checker.json', json_encode([
|
||||
'extension' => 'modpackchecker',
|
||||
'project_id' => $modpackId,
|
||||
'file_id' => $fileId,
|
||||
'version' => $version,
|
||||
'calibrated_at' => now()->toIso8601String(),
|
||||
], JSON_PRETTY_PRINT));
|
||||
} catch (\Exception $e) {
|
||||
\Log::warning('[MVC] Could not write Truth File on calibrate: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'current_version' => $version,
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Documents the new 'pending_calibration' status value.
|
||||
* The status column is VARCHAR so no schema change needed —
|
||||
* this migration updates any 'unknown' status rows that have
|
||||
* a detected platform but no current_version.
|
||||
*/
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
DB::table('modpackchecker_servers')
|
||||
->where('status', 'unknown')
|
||||
->whereNotNull('platform')
|
||||
->whereNull('current_version')
|
||||
->update(['status' => 'pending_calibration']);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('modpackchecker_servers')
|
||||
->where('status', 'pending_calibration')
|
||||
->update(['status' => 'unknown']);
|
||||
}
|
||||
};
|
||||
@@ -106,7 +106,30 @@ const ModpackVersionCard: React.FC = () => {
|
||||
};
|
||||
|
||||
if (loading) return null;
|
||||
if (data?.is_ignored) 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(
|
||||
@@ -118,6 +141,67 @@ const ModpackVersionCard: React.FC = () => {
|
||||
</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;
|
||||
|
||||
@@ -190,34 +274,7 @@ const ModpackVersionCard: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* Calibrate dropdown */}
|
||||
{showCalibrate && (
|
||||
<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>
|
||||
)}
|
||||
{showCalibrate && renderCalibrateDropdown()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user