feat(modpackchecker): Phase 5 complete - Dashboard badge and cron job
Phase 5 Components (completing Pyrrhus's work): NEW FILES: - views/dashboard/UpdateBadge.tsx: Dashboard badge component - Shows 🟢 (up to date) or 🟠 (update available) next to server names - Global cache prevents multiple API calls on page load - Reads from local database, never calls external APIs directly - Fire (#FF6B35) and Frost (#4ECDC4) brand colors - console/CheckModpackUpdates.php: Laravel cron command - Run with: php artisan modpackchecker:check - Loops through servers with MODPACK_PLATFORM variable - Checks CurseForge, Modrinth, FTB, Technic APIs - Rate limited (2s sleep between checks) - Stores results in modpackchecker_servers table UPDATED FILES: - Controllers/ModpackAPIController.php: - Added getStatus() method for dashboard badge endpoint - Returns all user's servers' update status in single query - Added DB facade import - routes/client.php: - Added GET /extensions/modpackchecker/status route - build.sh: - Complete rewrite for Phase 5 - Handles both console widget AND dashboard badge - Auto-detects extension directory (dev vs extensions) - Copies CheckModpackUpdates.php to app/Console/Commands/ - Injects UpdateBadge into ServerRow.tsx - Clear status output and next-steps guide Architecture (Gemini-approved): CRON (hourly) → Database cache → Single API endpoint → React badge Dashboard badge is 'dumb' - only reads from cache, never external APIs Completing work started by Chronicler #62 (Pyrrhus). UpdateBadge.tsx was lost in Blueprint corruption - reconstructed from handoff notes and architecture documentation. Signed-off-by: Claude (Chronicler #63) <claude@firefrostgaming.com>
This commit is contained in:
@@ -9,6 +9,7 @@ use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Admin\BlueprintAdm
|
|||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Support\Facades\Http;
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
class ModpackAPIController extends Controller
|
class ModpackAPIController extends Controller
|
||||||
{
|
{
|
||||||
@@ -245,4 +246,34 @@ class ModpackAPIController extends Controller
|
|||||||
'version' => $data['version'] ?? 'Unknown',
|
'version' => $data['version'] ?? 'Unknown',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cached update status for all servers (dashboard badge view)
|
||||||
|
* Called once on page load, returns status for all user's servers
|
||||||
|
*/
|
||||||
|
public function getStatus(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$user = $request->user();
|
||||||
|
|
||||||
|
// Get all servers the user has access to
|
||||||
|
$serverIds = $user->accessibleServers()->pluck('id')->toArray();
|
||||||
|
|
||||||
|
// Query our cache table for these servers
|
||||||
|
$statuses = DB::table('modpackchecker_servers')
|
||||||
|
->whereIn('server_id', $serverIds)
|
||||||
|
->get()
|
||||||
|
->keyBy('server_uuid');
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($statuses as $uuid => $status) {
|
||||||
|
$result[$uuid] = [
|
||||||
|
'update_available' => (bool) $status->update_available,
|
||||||
|
'modpack_name' => $status->modpack_name,
|
||||||
|
'current_version' => $status->current_version,
|
||||||
|
'latest_version' => $status->latest_version,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,95 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# build.sh - Executes automatically during blueprint -build
|
# build.sh - Executes automatically during blueprint -build
|
||||||
|
# Phase 5: Console widget + Dashboard badge injection
|
||||||
|
|
||||||
echo "Injecting ModpackChecker React Components..."
|
echo "=========================================="
|
||||||
|
echo "ModpackChecker Build Script - Phase 5"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
# 1. Copy your component into the Pterodactyl source tree
|
# Determine the extension source directory
|
||||||
cp .blueprint/dev/views/server/wrapper.tsx resources/scripts/components/server/ModpackVersionCard.tsx
|
# Blueprint may run from .blueprint/dev/ or .blueprint/extensions/modpackchecker/
|
||||||
|
if [ -d ".blueprint/extensions/modpackchecker/views" ]; then
|
||||||
# 2. Patch ServerConsoleContainer.tsx safely
|
EXT_DIR=".blueprint/extensions/modpackchecker"
|
||||||
if ! grep -q "ModpackVersionCard" resources/scripts/components/server/console/ServerConsoleContainer.tsx; then
|
elif [ -d ".blueprint/dev/views" ]; then
|
||||||
# Inject the import at the top of the file
|
EXT_DIR=".blueprint/dev"
|
||||||
sed -i '1i import ModpackVersionCard from "@/components/server/ModpackVersionCard";' resources/scripts/components/server/console/ServerConsoleContainer.tsx
|
|
||||||
|
|
||||||
# Inject the component directly below the ServerDetailsBlock
|
|
||||||
sed -i '/<ServerDetailsBlock className/a \ <ModpackVersionCard />' resources/scripts/components/server/console/ServerConsoleContainer.tsx
|
|
||||||
|
|
||||||
echo "ModpackVersionCard injected into ServerConsoleContainer.tsx"
|
|
||||||
else
|
else
|
||||||
echo "ModpackVersionCard already present, skipping injection"
|
echo "ERROR: Cannot find extension views directory"
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
echo "Using extension directory: $EXT_DIR"
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 1. CONSOLE WIDGET (wrapper.tsx → ModpackVersionCard)
|
||||||
|
# ===========================================
|
||||||
|
echo ""
|
||||||
|
echo "--- Console Widget ---"
|
||||||
|
|
||||||
|
if [ -f "$EXT_DIR/views/server/wrapper.tsx" ]; then
|
||||||
|
cp "$EXT_DIR/views/server/wrapper.tsx" resources/scripts/components/server/ModpackVersionCard.tsx
|
||||||
|
echo "✓ Copied ModpackVersionCard.tsx"
|
||||||
|
else
|
||||||
|
echo "⚠ wrapper.tsx not found, skipping console widget"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Inject into ServerConsoleContainer.tsx
|
||||||
|
if ! grep -q "ModpackVersionCard" resources/scripts/components/server/console/ServerConsoleContainer.tsx 2>/dev/null; then
|
||||||
|
sed -i '1i import ModpackVersionCard from "@/components/server/ModpackVersionCard";' resources/scripts/components/server/console/ServerConsoleContainer.tsx
|
||||||
|
sed -i '/<ServerDetailsBlock className/a \ <ModpackVersionCard />' resources/scripts/components/server/console/ServerConsoleContainer.tsx
|
||||||
|
echo "✓ Injected ModpackVersionCard into ServerConsoleContainer.tsx"
|
||||||
|
else
|
||||||
|
echo "○ ModpackVersionCard already present in ServerConsoleContainer.tsx"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 2. DASHBOARD BADGE (UpdateBadge.tsx)
|
||||||
|
# ===========================================
|
||||||
|
echo ""
|
||||||
|
echo "--- Dashboard Badge ---"
|
||||||
|
|
||||||
|
if [ -f "$EXT_DIR/views/dashboard/UpdateBadge.tsx" ]; then
|
||||||
|
# Ensure target directory exists
|
||||||
|
mkdir -p resources/scripts/components/dashboard
|
||||||
|
cp "$EXT_DIR/views/dashboard/UpdateBadge.tsx" resources/scripts/components/dashboard/UpdateBadge.tsx
|
||||||
|
echo "✓ Copied UpdateBadge.tsx"
|
||||||
|
else
|
||||||
|
echo "⚠ UpdateBadge.tsx not found, skipping dashboard badge"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Inject into ServerRow.tsx (dashboard server list)
|
||||||
|
if ! grep -q "UpdateBadge" resources/scripts/components/dashboard/ServerRow.tsx 2>/dev/null; then
|
||||||
|
# Add import at top
|
||||||
|
sed -i '1i import UpdateBadge from "@/components/dashboard/UpdateBadge";' resources/scripts/components/dashboard/ServerRow.tsx
|
||||||
|
|
||||||
|
# Inject badge right after the server name
|
||||||
|
# The pattern looks for the server name paragraph and adds our badge inside it
|
||||||
|
sed -i 's|<p css={tw`text-lg break-words`}>{server.name}</p>|<p css={tw`text-lg break-words`}>{server.name}<UpdateBadge serverUuid={server.uuid} /></p>|' resources/scripts/components/dashboard/ServerRow.tsx
|
||||||
|
|
||||||
|
echo "✓ Injected UpdateBadge into ServerRow.tsx"
|
||||||
|
else
|
||||||
|
echo "○ UpdateBadge already present in ServerRow.tsx"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 3. CONSOLE COMMAND (CheckModpackUpdates.php)
|
||||||
|
# ===========================================
|
||||||
|
echo ""
|
||||||
|
echo "--- Console Command ---"
|
||||||
|
|
||||||
|
if [ -f "$EXT_DIR/console/CheckModpackUpdates.php" ]; then
|
||||||
|
cp "$EXT_DIR/console/CheckModpackUpdates.php" app/Console/Commands/CheckModpackUpdates.php
|
||||||
|
echo "✓ Copied CheckModpackUpdates.php to app/Console/Commands/"
|
||||||
|
else
|
||||||
|
echo "⚠ CheckModpackUpdates.php not found, skipping cron command"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "ModpackChecker injection complete!"
|
||||||
|
echo "=========================================="
|
||||||
|
echo ""
|
||||||
|
echo "Next steps:"
|
||||||
|
echo " 1. Run: yarn build:production"
|
||||||
|
echo " 2. Restart: systemctl restart php8.3-fpm"
|
||||||
|
echo " 3. Test cron: php artisan modpackchecker:check"
|
||||||
|
echo ""
|
||||||
|
|||||||
@@ -0,0 +1,188 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Pterodactyl\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Pterodactyl\Models\Server;
|
||||||
|
|
||||||
|
class CheckModpackUpdates extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'modpackchecker:check';
|
||||||
|
protected $description = 'Check all servers for modpack updates';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$this->info('Starting modpack update check...');
|
||||||
|
|
||||||
|
// Get all servers that have modpack variables set
|
||||||
|
$servers = Server::whereHas('variables', function ($q) {
|
||||||
|
$q->where('env_variable', 'MODPACK_PLATFORM');
|
||||||
|
})->get();
|
||||||
|
|
||||||
|
$this->info("Found {$servers->count()} servers with modpack configuration");
|
||||||
|
|
||||||
|
foreach ($servers as $server) {
|
||||||
|
$this->checkServer($server);
|
||||||
|
// Rate limiting - sleep between checks
|
||||||
|
sleep(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Modpack update check complete!');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkServer(Server $server): void
|
||||||
|
{
|
||||||
|
$this->line("Checking: {$server->name} ({$server->uuid})");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get platform and modpack ID from variables
|
||||||
|
$platform = $this->getVariable($server, 'MODPACK_PLATFORM');
|
||||||
|
$modpackId = $this->getVariable($server, 'MODPACK_ID');
|
||||||
|
|
||||||
|
if (!$platform || !$modpackId) {
|
||||||
|
$this->warn(" Skipping - missing platform or modpack ID");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get latest version from API
|
||||||
|
$latestData = $this->fetchLatestVersion($platform, $modpackId);
|
||||||
|
|
||||||
|
if (!$latestData) {
|
||||||
|
$this->error(" Failed to fetch version data");
|
||||||
|
$this->updateDatabase($server, [
|
||||||
|
'platform' => $platform,
|
||||||
|
'modpack_id' => $modpackId,
|
||||||
|
'error_message' => 'Failed to fetch version data',
|
||||||
|
'update_available' => false,
|
||||||
|
]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current version (from variable or file - for now just use variable)
|
||||||
|
$currentVersion = $this->getVariable($server, 'MODPACK_CURRENT_VERSION');
|
||||||
|
|
||||||
|
// Determine if update is available
|
||||||
|
$updateAvailable = $currentVersion && $currentVersion !== $latestData['version'];
|
||||||
|
|
||||||
|
$this->updateDatabase($server, [
|
||||||
|
'platform' => $platform,
|
||||||
|
'modpack_id' => $modpackId,
|
||||||
|
'modpack_name' => $latestData['name'],
|
||||||
|
'current_version' => $currentVersion,
|
||||||
|
'latest_version' => $latestData['version'],
|
||||||
|
'update_available' => $updateAvailable,
|
||||||
|
'error_message' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$status = $updateAvailable ? '🟠 UPDATE AVAILABLE' : '🟢 Up to date';
|
||||||
|
$this->info(" {$status}: {$latestData['name']} - {$latestData['version']}");
|
||||||
|
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->error(" Error: {$e->getMessage()}");
|
||||||
|
$this->updateDatabase($server, [
|
||||||
|
'error_message' => $e->getMessage(),
|
||||||
|
'update_available' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getVariable(Server $server, string $name): ?string
|
||||||
|
{
|
||||||
|
$variable = $server->variables()
|
||||||
|
->where('env_variable', $name)
|
||||||
|
->first();
|
||||||
|
return $variable?->server_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function fetchLatestVersion(string $platform, string $modpackId): ?array
|
||||||
|
{
|
||||||
|
return match($platform) {
|
||||||
|
'modrinth' => $this->checkModrinth($modpackId),
|
||||||
|
'curseforge' => $this->checkCurseForge($modpackId),
|
||||||
|
'ftb' => $this->checkFTB($modpackId),
|
||||||
|
'technic' => $this->checkTechnic($modpackId),
|
||||||
|
default => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkModrinth(string $projectId): ?array
|
||||||
|
{
|
||||||
|
$response = Http::withHeaders([
|
||||||
|
'User-Agent' => 'FirefrostGaming/ModpackChecker/1.0',
|
||||||
|
])->get("https://api.modrinth.com/v2/project/{$projectId}");
|
||||||
|
|
||||||
|
if (!$response->successful()) return null;
|
||||||
|
$project = $response->json();
|
||||||
|
|
||||||
|
$versionResponse = Http::withHeaders([
|
||||||
|
'User-Agent' => 'FirefrostGaming/ModpackChecker/1.0',
|
||||||
|
])->get("https://api.modrinth.com/v2/project/{$projectId}/version");
|
||||||
|
|
||||||
|
$versions = $versionResponse->json();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $project['title'] ?? 'Unknown',
|
||||||
|
'version' => $versions[0]['version_number'] ?? 'Unknown',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkCurseForge(string $modpackId): ?array
|
||||||
|
{
|
||||||
|
$apiKey = DB::table('settings')
|
||||||
|
->where('key', 'like', '%modpackchecker::curseforge_api_key%')
|
||||||
|
->value('value');
|
||||||
|
|
||||||
|
if (!$apiKey) return null;
|
||||||
|
|
||||||
|
$response = Http::withHeaders([
|
||||||
|
'x-api-key' => $apiKey,
|
||||||
|
])->get("https://api.curseforge.com/v1/mods/{$modpackId}");
|
||||||
|
|
||||||
|
if (!$response->successful()) return null;
|
||||||
|
$data = $response->json()['data'] ?? [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $data['name'] ?? 'Unknown',
|
||||||
|
'version' => $data['latestFiles'][0]['displayName'] ?? 'Unknown',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkFTB(string $modpackId): ?array
|
||||||
|
{
|
||||||
|
$response = Http::get("https://api.modpacks.ch/public/modpack/{$modpackId}");
|
||||||
|
if (!$response->successful()) return null;
|
||||||
|
$data = $response->json();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $data['name'] ?? 'Unknown',
|
||||||
|
'version' => $data['versions'][0]['name'] ?? 'Unknown',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function checkTechnic(string $slug): ?array
|
||||||
|
{
|
||||||
|
$response = Http::get("https://api.technicpack.net/modpack/{$slug}?build=1");
|
||||||
|
if (!$response->successful()) return null;
|
||||||
|
$data = $response->json();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $data['displayName'] ?? $data['name'] ?? 'Unknown',
|
||||||
|
'version' => $data['version'] ?? 'Unknown',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateDatabase(Server $server, array $data): void
|
||||||
|
{
|
||||||
|
DB::table('modpackchecker_servers')->updateOrInsert(
|
||||||
|
['server_id' => $server->id],
|
||||||
|
array_merge($data, [
|
||||||
|
'server_uuid' => $server->uuid,
|
||||||
|
'last_checked' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,3 +14,6 @@ use Pterodactyl\BlueprintFramework\Extensions\modpackchecker\Controllers\Modpack
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
Route::post('/servers/{server}/ext/modpackchecker/check', [ModpackAPIController::class, 'manualCheck']);
|
Route::post('/servers/{server}/ext/modpackchecker/check', [ModpackAPIController::class, 'manualCheck']);
|
||||||
|
|
||||||
|
// Dashboard badge status endpoint - returns all servers' update status in one call
|
||||||
|
Route::get('/extensions/modpackchecker/status', [ModpackAPIController::class, 'getStatus']);
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import http from '@/api/http';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UpdateBadge - Shows a colored dot next to server name on dashboard
|
||||||
|
*
|
||||||
|
* Architecture (per Gemini/Pyrrhus):
|
||||||
|
* - NEVER calls external APIs directly from dashboard
|
||||||
|
* - Reads from local database cache populated by cron job
|
||||||
|
* - Single API call on page load, cached globally
|
||||||
|
* - Shows 🟢 (up to date) or 🟠 (update available)
|
||||||
|
*/
|
||||||
|
|
||||||
|
interface ServerStatus {
|
||||||
|
update_available: boolean;
|
||||||
|
modpack_name?: string;
|
||||||
|
current_version?: string;
|
||||||
|
latest_version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusCache {
|
||||||
|
[serverUuid: string]: ServerStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global cache - shared across all UpdateBadge instances
|
||||||
|
// Prevents multiple API calls when rendering server list
|
||||||
|
let globalCache: StatusCache | null = null;
|
||||||
|
let fetchPromise: Promise<StatusCache> | null = null;
|
||||||
|
|
||||||
|
const fetchAllStatuses = async (): Promise<StatusCache> => {
|
||||||
|
// Return cached data if available
|
||||||
|
if (globalCache !== null) {
|
||||||
|
return globalCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If already fetching, wait for that promise
|
||||||
|
if (fetchPromise !== null) {
|
||||||
|
return fetchPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start new fetch
|
||||||
|
fetchPromise = http.get('/api/client/extensions/modpackchecker/status')
|
||||||
|
.then((response) => {
|
||||||
|
globalCache = response.data || {};
|
||||||
|
return globalCache;
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error('ModpackChecker: Failed to fetch status', error);
|
||||||
|
globalCache = {};
|
||||||
|
return globalCache;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
fetchPromise = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
return fetchPromise;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UpdateBadgeProps {
|
||||||
|
serverUuid: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UpdateBadge: React.FC<UpdateBadgeProps> = ({ serverUuid }) => {
|
||||||
|
const [status, setStatus] = useState<ServerStatus | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchAllStatuses()
|
||||||
|
.then((cache) => {
|
||||||
|
setStatus(cache[serverUuid] || null);
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, [serverUuid]);
|
||||||
|
|
||||||
|
// Don't render anything while loading or if no status
|
||||||
|
if (loading || !status) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only show badge if we have modpack data
|
||||||
|
if (!status.modpack_name) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dotStyle: React.CSSProperties = {
|
||||||
|
display: 'inline-block',
|
||||||
|
width: '8px',
|
||||||
|
height: '8px',
|
||||||
|
borderRadius: '50%',
|
||||||
|
marginLeft: '8px',
|
||||||
|
backgroundColor: status.update_available ? '#FF6B35' : '#4ECDC4', // Fire : Frost
|
||||||
|
boxShadow: status.update_available
|
||||||
|
? '0 0 4px rgba(255, 107, 53, 0.5)'
|
||||||
|
: '0 0 4px rgba(78, 205, 196, 0.5)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const tooltipText = status.update_available
|
||||||
|
? `Update available: ${status.latest_version}`
|
||||||
|
: `Up to date: ${status.latest_version}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
style={dotStyle}
|
||||||
|
title={tooltipText}
|
||||||
|
aria-label={tooltipText}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UpdateBadge;
|
||||||
Reference in New Issue
Block a user