feat: Implement comprehensive help system with contextual icons (#122)

* feat: implement comprehensive help system with contextual icons

- Add HelpModal component with navigation sidebar and 7 help sections
- Add HelpButton component for main header controls
- Add ContextualHelpIcon component for contextual help throughout UI
- Add help icons to all major UI sections:
  - Settings modals (Server Settings, General Settings)
  - Sync button with update system help
  - Tab headers (Available, Downloaded, Installed Scripts)
  - FilterBar and CategorySidebar
- Add comprehensive help content covering:
  - Server Settings: PVE server management, auth types, color coding
  - General Settings: Save filters, GitHub integration, authentication
  - Sync Button: Script metadata syncing explanation
  - Available Scripts: Browsing, filtering, downloading
  - Downloaded Scripts: Local script management and updates
  - Installed Scripts: Auto-detection feature (primary focus), manual management
  - Update System: Automatic/manual update process, release notes
- Improve VersionDisplay: remove 'Update Available' text, add 'Release Notes:' label
- Make help icons more noticeable with increased size
- Fix dark theme compatibility issues in help modal

* fix: resolve linting errors in HelpModal component

- Remove unused Filter import
- Fix unescaped entities by replacing apostrophes and quotes with HTML entities
- All linting errors resolved

* feat: implement release notes modal system

- Add getAllReleases API endpoint to fetch GitHub releases with notes
- Create ReleaseNotesModal component with localStorage version tracking
- Add sticky Footer component with release notes link
- Make version badge clickable to open release notes
- Auto-show modal after updates when version changes
- Track last seen version in localStorage to prevent repeated shows
- Highlight new version in modal when opened after update
- Add manual access via footer and version badge clicks

* fix: use nullish coalescing operator in ReleaseNotesModal

- Replace logical OR (||) with nullish coalescing (??) operator
- Fixes ESLint prefer-nullish-coalescing rule violation
- Ensures build passes successfully
This commit is contained in:
Michel Roegl-Brunner
2025-10-13 15:05:23 +02:00
committed by GitHub
parent 24afce49a3
commit aaa09b4745
13 changed files with 1050 additions and 97 deletions

View File

@@ -1,6 +1,7 @@
'use client';
import { useState } from 'react';
import { ContextualHelpIcon } from './ContextualHelpIcon';
interface CategorySidebarProps {
categories: string[];
@@ -201,9 +202,12 @@ export function CategorySidebar({
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-border">
{!isCollapsed && (
<div>
<h3 className="text-lg font-semibold text-foreground">Categories</h3>
<p className="text-sm text-muted-foreground">{totalScripts} Total scripts</p>
<div className="flex items-center justify-between w-full">
<div>
<h3 className="text-lg font-semibold text-foreground">Categories</h3>
<p className="text-sm text-muted-foreground">{totalScripts} Total scripts</p>
</div>
<ContextualHelpIcon section="available-scripts" tooltip="Help with categories" />
</div>
)}
<button

View File

@@ -0,0 +1,46 @@
'use client';
import { useState } from 'react';
import { HelpModal } from './HelpModal';
import { Button } from './ui/button';
import { HelpCircle } from 'lucide-react';
interface ContextualHelpIconProps {
section: string;
className?: string;
size?: 'sm' | 'default';
tooltip?: string;
}
export function ContextualHelpIcon({
section,
className = '',
size = 'sm',
tooltip = 'Help'
}: ContextualHelpIconProps) {
const [isOpen, setIsOpen] = useState(false);
const sizeClasses = size === 'sm'
? 'h-7 w-7 p-1.5'
: 'h-9 w-9 p-2';
return (
<>
<Button
onClick={() => setIsOpen(true)}
variant="ghost"
size="icon"
className={`${sizeClasses} text-muted-foreground hover:text-foreground hover:bg-muted ${className}`}
title={tooltip}
>
<HelpCircle className="w-4 h-4" />
</Button>
<HelpModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
initialSection={section}
/>
</>
);
}

View File

@@ -2,6 +2,7 @@
import React, { useState } from "react";
import { Button } from "./ui/button";
import { ContextualHelpIcon } from "./ContextualHelpIcon";
import { Package, Monitor, Wrench, Server, FileText, Calendar, RefreshCw, Filter } from "lucide-react";
export interface FilterState {
@@ -104,6 +105,14 @@ export function FilterBar({
</div>
)}
{/* Filter Header */}
{!isLoadingFilters && (
<div className="mb-4 flex items-center justify-between">
<h3 className="text-lg font-medium text-foreground">Filter Scripts</h3>
<ContextualHelpIcon section="available-scripts" tooltip="Help with filtering and searching" />
</div>
)}
{/* Search Bar */}
<div className="mb-4">
<div className="relative max-w-md w-full">

View File

@@ -0,0 +1,64 @@
'use client';
import { api } from '~/trpc/react';
import { Button } from './ui/button';
import { ExternalLink, FileText } from 'lucide-react';
interface FooterProps {
onOpenReleaseNotes: () => void;
}
export function Footer({ onOpenReleaseNotes }: FooterProps) {
const { data: versionData } = api.version.getCurrentVersion.useQuery();
return (
<footer className="sticky bottom-0 mt-auto border-t border-border bg-muted/30 py-6 backdrop-blur-sm">
<div className="container mx-auto px-4">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-4">
<span>© 2024 PVE Scripts Local</span>
{versionData?.success && versionData.version && (
<Button
variant="ghost"
size="sm"
onClick={onOpenReleaseNotes}
className="h-auto p-1 text-xs hover:text-foreground"
>
v{versionData.version}
</Button>
)}
</div>
<div className="flex items-center gap-4">
<Button
variant="ghost"
size="sm"
onClick={onOpenReleaseNotes}
className="h-auto p-2 text-xs hover:text-foreground"
>
<FileText className="h-3 w-3 mr-1" />
Release Notes
</Button>
<Button
variant="ghost"
size="sm"
asChild
className="h-auto p-2 text-xs hover:text-foreground"
>
<a
href="https://github.com/community-scripts/ProxmoxVE-Local"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1"
>
<ExternalLink className="h-3 w-3" />
GitHub
</a>
</Button>
</div>
</div>
</div>
</footer>
);
}

View File

@@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
import { Button } from './ui/button';
import { Input } from './ui/input';
import { Toggle } from './ui/toggle';
import { ContextualHelpIcon } from './ContextualHelpIcon';
interface GeneralSettingsModalProps {
isOpen: boolean;
@@ -280,7 +281,10 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr
<div className="bg-card rounded-lg shadow-xl max-w-4xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-4 sm:p-6 border-b border-border">
<h2 className="text-xl sm:text-2xl font-bold text-card-foreground">Settings</h2>
<div className="flex items-center gap-2">
<h2 className="text-xl sm:text-2xl font-bold text-card-foreground">Settings</h2>
<ContextualHelpIcon section="general-settings" tooltip="Help with General Settings" />
</div>
<Button
onClick={onClose}
variant="ghost"

View File

@@ -0,0 +1,40 @@
'use client';
import { useState } from 'react';
import { HelpModal } from './HelpModal';
import { Button } from './ui/button';
import { HelpCircle } from 'lucide-react';
interface HelpButtonProps {
initialSection?: string;
}
export function HelpButton({ initialSection }: HelpButtonProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
<div className="text-sm text-muted-foreground font-medium">
Need help?
</div>
<Button
onClick={() => setIsOpen(true)}
variant="outline"
size="default"
className="inline-flex items-center"
title="Open Help"
>
<HelpCircle className="w-5 h-5 mr-2" />
Help
</Button>
</div>
<HelpModal
isOpen={isOpen}
onClose={() => setIsOpen(false)}
initialSection={initialSection}
/>
</>
);
}

View File

@@ -0,0 +1,492 @@
'use client';
import { useState } from 'react';
import { Button } from './ui/button';
import { HelpCircle, Server, Settings, RefreshCw, Package, HardDrive, FolderOpen, Search, Download } from 'lucide-react';
interface HelpModalProps {
isOpen: boolean;
onClose: () => void;
initialSection?: string;
}
type HelpSection = 'server-settings' | 'general-settings' | 'sync-button' | 'available-scripts' | 'downloaded-scripts' | 'installed-scripts' | 'update-system';
export function HelpModal({ isOpen, onClose, initialSection = 'server-settings' }: HelpModalProps) {
const [activeSection, setActiveSection] = useState<HelpSection>(initialSection as HelpSection);
if (!isOpen) return null;
const sections = [
{ id: 'server-settings' as HelpSection, label: 'Server Settings', icon: Server },
{ id: 'general-settings' as HelpSection, label: 'General Settings', icon: Settings },
{ id: 'sync-button' as HelpSection, label: 'Sync Button', icon: RefreshCw },
{ id: 'available-scripts' as HelpSection, label: 'Available Scripts', icon: Package },
{ id: 'downloaded-scripts' as HelpSection, label: 'Downloaded Scripts', icon: HardDrive },
{ id: 'installed-scripts' as HelpSection, label: 'Installed Scripts', icon: FolderOpen },
{ id: 'update-system' as HelpSection, label: 'Update System', icon: Download },
];
const renderContent = () => {
switch (activeSection) {
case 'server-settings':
return (
<div className="space-y-6">
<div>
<h3 className="text-xl font-semibold text-foreground mb-4">Server Settings</h3>
<p className="text-muted-foreground mb-6">
Manage your Proxmox VE servers and configure connection settings.
</p>
</div>
<div className="space-y-4">
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Adding PVE Servers</h4>
<ul className="text-sm text-muted-foreground space-y-2">
<li> <strong>Server Name:</strong> A friendly name to identify your server</li>
<li> <strong>IP Address:</strong> The IP address or hostname of your PVE server</li>
<li> <strong>Username:</strong> PVE user account (usually root or a dedicated user)</li>
<li> <strong>SSH Port:</strong> Default is 22, change if your server uses a different port</li>
</ul>
</div>
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Authentication Types</h4>
<ul className="text-sm text-muted-foreground space-y-2">
<li> <strong>Password:</strong> Use username and password authentication</li>
<li> <strong>SSH Key:</strong> Use SSH key pair for secure authentication</li>
<li> <strong>Both:</strong> Try SSH key first, fallback to password if needed</li>
</ul>
</div>
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Server Color Coding</h4>
<p className="text-sm text-muted-foreground">
Assign colors to servers for visual distinction throughout the application.
This helps identify which server you&apos;re working with when managing scripts.
This needs to be enabled in the General Settings.
</p>
</div>
</div>
</div>
);
case 'general-settings':
return (
<div className="space-y-6">
<div>
<h3 className="text-xl font-semibold text-foreground mb-4">General Settings</h3>
<p className="text-muted-foreground mb-6">
Configure application preferences and behavior.
</p>
</div>
<div className="space-y-4">
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Save Filters</h4>
<p className="text-sm text-muted-foreground mb-2">
When enabled, your script filter preferences (search terms, categories, sorting)
will be automatically saved and restored when you return to the application.
</p>
<ul className="text-sm text-muted-foreground space-y-1">
<li> Search queries are preserved</li>
<li> Selected script types are remembered</li>
<li> Sort preferences are maintained</li>
<li> Category selections are saved</li>
</ul>
</div>
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Server Color Coding</h4>
<p className="text-sm text-muted-foreground">
Enable visual color coding for servers throughout the application.
This makes it easier to identify which server you&apos;re working with.
</p>
</div>
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">GitHub Integration</h4>
<p className="text-sm text-muted-foreground mb-2">
Add a GitHub Personal Access Token to increase API rate limits and improve performance.
</p>
<ul className="text-sm text-muted-foreground space-y-1">
<li> Bypasses GitHub&apos;s rate limiting for unauthenticated requests</li>
<li> Improves script loading and syncing performance</li>
<li> Token is stored securely and only used for API calls</li>
</ul>
</div>
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Authentication</h4>
<p className="text-sm text-muted-foreground mb-2">
Secure your application with username and password authentication.
</p>
<ul className="text-sm text-muted-foreground space-y-1">
<li> Set up username and password for app access</li>
<li> Enable/disable authentication as needed</li>
<li> Credentials are stored securely</li>
</ul>
</div>
</div>
</div>
);
case 'sync-button':
return (
<div className="space-y-6">
<div>
<h3 className="text-xl font-semibold text-foreground mb-4">Sync Button</h3>
<p className="text-muted-foreground mb-6">
Synchronize script metadata from the ProxmoxVE GitHub repository.
</p>
</div>
<div className="space-y-4">
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">What Does Syncing Do?</h4>
<ul className="text-sm text-muted-foreground space-y-2">
<li> <strong>Updates Script Metadata:</strong> Downloads the latest script information (JSON files)</li>
<li> <strong>Refreshes Available Scripts:</strong> Updates the list of scripts you can download</li>
<li> <strong>Updates Categories:</strong> Refreshes script categories and organization</li>
<li> <strong>Checks for Updates:</strong> Identifies which downloaded scripts have newer versions</li>
</ul>
</div>
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Important Notes</h4>
<ul className="text-sm text-muted-foreground space-y-2">
<li> <strong>Metadata Only:</strong> Syncing only updates script information, not the actual script files</li>
<li> <strong>No Downloads:</strong> Script files are downloaded separately when you choose to install them</li>
<li> <strong>Last Sync Time:</strong> Shows when the last successful sync occurred</li>
<li> <strong>Rate Limits:</strong> GitHub API limits may apply without a personal access token</li>
</ul>
</div>
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">When to Sync</h4>
<ul className="text-sm text-muted-foreground space-y-1">
<li> When you want to see the latest available scripts</li>
<li> To check for updates to your downloaded scripts</li>
<li> If you notice scripts are missing or outdated</li>
<li> After the ProxmoxVE repository has been updated</li>
</ul>
</div>
</div>
</div>
);
case 'available-scripts':
return (
<div className="space-y-6">
<div>
<h3 className="text-xl font-semibold text-foreground mb-4">Available Scripts</h3>
<p className="text-muted-foreground mb-6">
Browse and discover scripts from the ProxmoxVE repository.
</p>
</div>
<div className="space-y-4">
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Browsing Scripts</h4>
<ul className="text-sm text-muted-foreground space-y-2">
<li> <strong>Category Sidebar:</strong> Filter scripts by category (Storage, Network, Security, etc.)</li>
<li> <strong>Search:</strong> Find scripts by name or description</li>
<li> <strong>View Modes:</strong> Switch between card and list view</li>
<li> <strong>Sorting:</strong> Sort by name or creation date</li>
</ul>
</div>
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Filtering Options</h4>
<ul className="text-sm text-muted-foreground space-y-2">
<li> <strong>Script Types:</strong> Filter by CT (Container) or other script types</li>
<li> <strong>Update Status:</strong> Show only scripts with available updates</li>
<li> <strong>Search Query:</strong> Search within script names and descriptions</li>
<li> <strong>Categories:</strong> Filter by specific script categories</li>
</ul>
</div>
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Script Actions</h4>
<ul className="text-sm text-muted-foreground space-y-2">
<li> <strong>View Details:</strong> Click on a script to see full information and documentation</li>
<li> <strong>Download:</strong> Download script files to your local system</li>
<li> <strong>Install:</strong> Run scripts directly on your PVE servers</li>
<li> <strong>Preview:</strong> View script content before downloading</li>
</ul>
</div>
</div>
</div>
);
case 'downloaded-scripts':
return (
<div className="space-y-6">
<div>
<h3 className="text-xl font-semibold text-foreground mb-4">Downloaded Scripts</h3>
<p className="text-muted-foreground mb-6">
Manage scripts that have been downloaded to your local system.
</p>
</div>
<div className="space-y-4">
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">What Are Downloaded Scripts?</h4>
<p className="text-sm text-muted-foreground mb-2">
These are scripts that you&apos;ve downloaded from the repository and are stored locally on your system.
</p>
<ul className="text-sm text-muted-foreground space-y-1">
<li> Script files are stored in your local scripts directory</li>
<li> You can run these scripts on your PVE servers</li>
<li> Scripts can be updated when newer versions are available</li>
</ul>
</div>
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Update Detection</h4>
<p className="text-sm text-muted-foreground mb-2">
The system automatically checks if newer versions of your downloaded scripts are available.
</p>
<ul className="text-sm text-muted-foreground space-y-1">
<li> Scripts with updates available are marked with an update indicator</li>
<li> You can filter to show only scripts with available updates</li>
<li> Update detection happens when you sync with the repository</li>
</ul>
</div>
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Managing Downloaded Scripts</h4>
<ul className="text-sm text-muted-foreground space-y-2">
<li> <strong>Update Scripts:</strong> Download the latest version of a script</li>
<li> <strong>View Details:</strong> See script information and documentation</li>
<li> <strong>Install/Run:</strong> Execute scripts on your PVE servers</li>
<li> <strong>Filter & Search:</strong> Use the same filtering options as Available Scripts</li>
</ul>
</div>
</div>
</div>
);
case 'installed-scripts':
return (
<div className="space-y-6">
<div>
<h3 className="text-xl font-semibold text-foreground mb-4">Installed Scripts</h3>
<p className="text-muted-foreground mb-6">
Track and manage scripts that are installed on your PVE servers.
</p>
</div>
<div className="space-y-4">
<div className="p-4 border border-border rounded-lg bg-muted/50 border-primary/20">
<h4 className="font-medium text-foreground mb-2 flex items-center gap-2">
<Search className="w-4 h-4" />
Auto-Detection (Primary Feature)
</h4>
<p className="text-sm text-muted-foreground mb-3">
The system can automatically detect LXC containers that have community-script tags on your PVE servers.
</p>
<ul className="text-sm text-muted-foreground space-y-2">
<li> <strong>Automatic Discovery:</strong> Scans your PVE servers for containers with community-script tags</li>
<li> <strong>Container Detection:</strong> Identifies LXC containers running Proxmox helper scripts</li>
<li> <strong>Server Association:</strong> Links detected scripts to the specific PVE server</li>
<li> <strong>Bulk Import:</strong> Automatically creates records for all detected scripts</li>
</ul>
<div className="mt-3 p-3 bg-primary/10 rounded-lg border border-primary/20">
<p className="text-sm font-medium text-primary">How Auto-Detection Works:</p>
<ol className="text-sm text-muted-foreground mt-1 space-y-1">
<li>1. Connects to your configured PVE servers</li>
<li>2. Scans LXC container configurations</li>
<li>3. Looks for containers with community-script tags</li>
<li>4. Creates installed script records automatically</li>
</ol>
</div>
</div>
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Manual Script Management</h4>
<ul className="text-sm text-muted-foreground space-y-2">
<li> <strong>Add Scripts Manually:</strong> Create records for scripts not auto-detected</li>
<li> <strong>Edit Script Details:</strong> Update script names and container IDs</li>
<li> <strong>Delete Scripts:</strong> Remove scripts from tracking</li>
<li> <strong>Bulk Operations:</strong> Clean up old or invalid script records</li>
</ul>
</div>
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Script Tracking Features</h4>
<ul className="text-sm text-muted-foreground space-y-2">
<li> <strong>Installation Status:</strong> Track success, failure, or in-progress installations</li>
<li> <strong>Server Association:</strong> Know which server each script is installed on</li>
<li> <strong>Container ID:</strong> Link scripts to specific LXC containers</li>
<li> <strong>Execution Logs:</strong> View output and logs from script installations</li>
<li> <strong>Filtering:</strong> Filter by server, status, or search terms</li>
</ul>
</div>
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Managing Installed Scripts</h4>
<ul className="text-sm text-muted-foreground space-y-2">
<li> <strong>View All Scripts:</strong> See all tracked scripts across all servers</li>
<li> <strong>Filter by Server:</strong> Show scripts for a specific PVE server</li>
<li> <strong>Filter by Status:</strong> Show successful, failed, or in-progress installations</li>
<li> <strong>Sort Options:</strong> Sort by name, container ID, server, status, or date</li>
<li> <strong>Update Scripts:</strong> Re-run or update existing script installations</li>
</ul>
</div>
</div>
</div>
);
case 'update-system':
return (
<div className="space-y-6">
<div>
<h3 className="text-xl font-semibold text-foreground mb-4">Update System</h3>
<p className="text-muted-foreground mb-6">
Keep your PVE Scripts Management application up to date with the latest features and improvements.
</p>
</div>
<div className="space-y-4">
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">What Does Updating Do?</h4>
<ul className="text-sm text-muted-foreground space-y-2">
<li> <strong>Downloads Latest Version:</strong> Fetches the newest release from the GitHub repository</li>
<li> <strong>Updates Application Files:</strong> Replaces current files with the latest version</li>
<li> <strong>Installs Dependencies:</strong> Updates Node.js packages and dependencies</li>
<li> <strong>Rebuilds Application:</strong> Compiles the application with latest changes</li>
<li> <strong>Restarts Server:</strong> Automatically restarts the application server</li>
</ul>
</div>
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">How to Update</h4>
<div className="space-y-3">
<div>
<h5 className="font-medium text-foreground mb-2">Automatic Update (Recommended)</h5>
<ul className="text-sm text-muted-foreground space-y-1">
<li> Click the &quot;Update Now&quot; button when an update is available</li>
<li> The system will handle everything automatically</li>
<li> You&apos;ll see a progress overlay with update logs</li>
<li> The page will reload automatically when complete</li>
</ul>
</div>
<div>
<h5 className="font-medium text-foreground mb-2">Manual Update (Advanced)</h5>
<p className="text-sm text-muted-foreground mb-2">If automatic update fails, you can update manually:</p>
<div className="bg-muted p-3 rounded-lg font-mono text-sm">
<div className="text-muted-foreground"># Navigate to the application directory</div>
<div>cd $PVESCRIPTLOCAL_DIR</div>
<div className="text-muted-foreground"># Pull latest changes</div>
<div>git pull</div>
<div className="text-muted-foreground"># Install dependencies</div>
<div>npm install</div>
<div className="text-muted-foreground"># Build the application</div>
<div>npm run build</div>
<div className="text-muted-foreground"># Start the application</div>
<div>npm start</div>
</div>
</div>
</div>
</div>
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Update Process</h4>
<ol className="text-sm text-muted-foreground space-y-2">
<li><strong>1. Check for Updates:</strong> System automatically checks GitHub for new releases</li>
<li><strong>2. Download Update:</strong> Downloads the latest release files</li>
<li><strong>3. Backup Current Version:</strong> Creates backup of current installation</li>
<li><strong>4. Install New Version:</strong> Replaces files and updates dependencies</li>
<li><strong>5. Build Application:</strong> Compiles the updated code</li>
<li><strong>6. Restart Server:</strong> Stops old server and starts new version</li>
<li><strong>7. Reload Page:</strong> Automatically refreshes the browser</li>
</ol>
</div>
<div className="p-4 border border-border rounded-lg">
<h4 className="font-medium text-foreground mb-2">Release Notes</h4>
<p className="text-sm text-muted-foreground mb-2">
Click the external link icon next to the update button to view detailed release notes on GitHub.
</p>
<ul className="text-sm text-muted-foreground space-y-1">
<li> See what&apos;s new in each version</li>
<li> Read about bug fixes and improvements</li>
<li> Check for any breaking changes</li>
<li> View installation requirements</li>
</ul>
</div>
<div className="p-4 border border-border rounded-lg bg-muted/50">
<h4 className="font-medium text-foreground mb-2">Important Notes</h4>
<ul className="text-sm text-muted-foreground space-y-2">
<li> <strong>Backup:</strong> Your data and settings are preserved during updates</li>
<li> <strong>Downtime:</strong> Brief downtime occurs during the update process</li>
<li> <strong>Compatibility:</strong> Updates maintain backward compatibility with your data</li>
<li> <strong>Rollback:</strong> If issues occur, you can manually revert to previous version</li>
</ul>
</div>
</div>
</div>
);
default:
return null;
}
};
return (
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-2 sm:p-4">
<div className="bg-card rounded-lg shadow-xl max-w-6xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-4 sm:p-6 border-b border-border">
<h2 className="text-xl sm:text-2xl font-bold text-card-foreground flex items-center gap-2">
<HelpCircle className="w-6 h-6" />
Help & Documentation
</h2>
<Button
onClick={onClose}
variant="ghost"
size="icon"
className="text-muted-foreground hover:text-foreground"
>
<svg className="w-5 h-5 sm:w-6 sm:h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</Button>
</div>
<div className="flex h-[calc(95vh-120px)] sm:h-[calc(90vh-140px)]">
{/* Sidebar Navigation */}
<div className="w-64 border-r border-border bg-muted/30 overflow-y-auto">
<nav className="p-4 space-y-2">
{sections.map((section) => {
const Icon = section.icon;
return (
<Button
key={section.id}
onClick={() => setActiveSection(section.id)}
variant={activeSection === section.id ? "default" : "ghost"}
size="sm"
className="w-full justify-start gap-2 text-left"
>
<Icon className="w-4 h-4" />
{section.label}
</Button>
);
})}
</nav>
</div>
{/* Content Area */}
<div className="flex-1 overflow-y-auto">
<div className="p-4 sm:p-6">
{renderContent()}
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,202 @@
'use client';
import { useState, useEffect } from 'react';
import { api } from '~/trpc/react';
import { Button } from './ui/button';
import { Badge } from './ui/badge';
import { X, ExternalLink, Calendar, Tag, Loader2 } from 'lucide-react';
interface ReleaseNotesModalProps {
isOpen: boolean;
onClose: () => void;
highlightVersion?: string;
}
interface Release {
tagName: string;
name: string;
publishedAt: string;
htmlUrl: string;
body: string;
}
// Helper functions for localStorage
const getLastSeenVersion = (): string | null => {
if (typeof window === 'undefined') return null;
return localStorage.getItem('LAST_SEEN_RELEASE_VERSION');
};
const markVersionAsSeen = (version: string): void => {
if (typeof window === 'undefined') return;
localStorage.setItem('LAST_SEEN_RELEASE_VERSION', version);
};
export function ReleaseNotesModal({ isOpen, onClose, highlightVersion }: ReleaseNotesModalProps) {
const [currentVersion, setCurrentVersion] = useState<string | null>(null);
const { data: releasesData, isLoading, error } = api.version.getAllReleases.useQuery(undefined, {
enabled: isOpen
});
const { data: versionData } = api.version.getCurrentVersion.useQuery(undefined, {
enabled: isOpen
});
// Get current version when modal opens
useEffect(() => {
if (isOpen && versionData?.success && versionData.version) {
setCurrentVersion(versionData.version);
}
}, [isOpen, versionData]);
// Mark version as seen when modal closes
const handleClose = () => {
if (currentVersion) {
markVersionAsSeen(currentVersion);
}
onClose();
};
if (!isOpen) return null;
const releases: Release[] = releasesData?.success ? releasesData.releases ?? [] : [];
return (
<div className="fixed inset-0 backdrop-blur-sm bg-black/50 flex items-center justify-center z-50 p-4">
<div className="bg-card rounded-lg shadow-xl max-w-4xl w-full max-h-[90vh] flex flex-col border border-border">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-border">
<div className="flex items-center gap-3">
<Tag className="h-6 w-6 text-blue-600" />
<h2 className="text-2xl font-bold text-card-foreground">Release Notes</h2>
</div>
<Button
variant="ghost"
size="sm"
onClick={handleClose}
className="h-8 w-8 p-0"
>
<X className="h-4 w-4" />
</Button>
</div>
{/* Content */}
<div className="flex-1 overflow-hidden flex flex-col">
{isLoading ? (
<div className="flex items-center justify-center p-8">
<div className="flex items-center gap-3">
<Loader2 className="h-6 w-6 animate-spin text-primary" />
<span className="text-muted-foreground">Loading release notes...</span>
</div>
</div>
) : error || !releasesData?.success ? (
<div className="flex items-center justify-center p-8">
<div className="text-center">
<p className="text-destructive mb-2">Failed to load release notes</p>
<p className="text-sm text-muted-foreground">
{releasesData?.error ?? 'Please try again later'}
</p>
</div>
</div>
) : releases.length === 0 ? (
<div className="flex items-center justify-center p-8">
<p className="text-muted-foreground">No releases found</p>
</div>
) : (
<div className="flex-1 overflow-y-auto p-6 space-y-6">
{releases.map((release, index) => {
const isHighlighted = highlightVersion && release.tagName.replace('v', '') === highlightVersion;
const isLatest = index === 0;
return (
<div
key={release.tagName}
className={`border rounded-lg p-6 ${
isHighlighted
? 'border-blue-500 bg-blue-50/10 dark:bg-blue-950/10'
: 'border-border bg-card'
} ${isLatest ? 'ring-2 ring-primary/20' : ''}`}
>
{/* Release Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center gap-3 mb-2">
<h3 className="text-xl font-semibold text-card-foreground">
{release.name || release.tagName}
</h3>
{isLatest && (
<Badge variant="default" className="text-xs">
Latest
</Badge>
)}
{isHighlighted && (
<Badge variant="secondary" className="text-xs bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200">
New
</Badge>
)}
</div>
<div className="flex items-center gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<Tag className="h-4 w-4" />
<span>{release.tagName}</span>
</div>
<div className="flex items-center gap-1">
<Calendar className="h-4 w-4" />
<span>
{new Date(release.publishedAt).toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
})}
</span>
</div>
</div>
</div>
<Button
variant="ghost"
size="sm"
asChild
className="h-8 w-8 p-0"
>
<a
href={release.htmlUrl}
target="_blank"
rel="noopener noreferrer"
title="View on GitHub"
>
<ExternalLink className="h-4 w-4" />
</a>
</Button>
</div>
{/* Release Body */}
{release.body && (
<div className="prose prose-sm max-w-none dark:prose-invert">
<div className="whitespace-pre-wrap text-sm text-card-foreground leading-relaxed">
{release.body}
</div>
</div>
)}
</div>
);
})}
</div>
)}
</div>
{/* Footer */}
<div className="flex items-center justify-between p-6 border-t border-border bg-muted/30">
<div className="text-sm text-muted-foreground">
{currentVersion && (
<span>Current version: <span className="font-medium text-card-foreground">v{currentVersion}</span></span>
)}
</div>
<Button onClick={handleClose} variant="default">
Close
</Button>
</div>
</div>
</div>
);
}
// Export helper functions for use in other components
export { getLastSeenVersion, markVersionAsSeen };

View File

@@ -3,6 +3,7 @@
import { useState } from 'react';
import { api } from '~/trpc/react';
import { Button } from './ui/button';
import { ContextualHelpIcon } from './ContextualHelpIcon';
export function ResyncButton() {
const [isResyncing, setIsResyncing] = useState(false);
@@ -44,27 +45,30 @@ export function ResyncButton() {
Sync scripts with ProxmoxVE repo
</div>
<div className="flex flex-col sm:flex-row sm:items-center gap-3">
<Button
onClick={handleResync}
disabled={isResyncing}
variant="outline"
size="default"
className="inline-flex items-center"
>
{isResyncing ? (
<>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
<span>Syncing...</span>
</>
) : (
<>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>Sync Json Files</span>
</>
)}
</Button>
<div className="flex items-center gap-2">
<Button
onClick={handleResync}
disabled={isResyncing}
variant="outline"
size="default"
className="inline-flex items-center"
>
{isResyncing ? (
<>
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white mr-2"></div>
<span>Syncing...</span>
</>
) : (
<>
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
<span>Sync Json Files</span>
</>
)}
</Button>
<ContextualHelpIcon section="sync-button" tooltip="Help with Sync Button" />
</div>
{lastSync && (
<div className="text-xs text-muted-foreground">

View File

@@ -5,6 +5,7 @@ import type { Server, CreateServerData } from '../../types/server';
import { ServerForm } from './ServerForm';
import { ServerList } from './ServerList';
import { Button } from './ui/button';
import { ContextualHelpIcon } from './ContextualHelpIcon';
interface SettingsModalProps {
isOpen: boolean;
@@ -106,7 +107,10 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) {
<div className="bg-card rounded-lg shadow-xl max-w-4xl w-full max-h-[95vh] sm:max-h-[90vh] overflow-hidden">
{/* Header */}
<div className="flex items-center justify-between p-4 sm:p-6 border-b border-border">
<h2 className="text-xl sm:text-2xl font-bold text-card-foreground">Settings</h2>
<div className="flex items-center gap-2">
<h2 className="text-xl sm:text-2xl font-bold text-card-foreground">Settings</h2>
<ContextualHelpIcon section="server-settings" tooltip="Help with Server Settings" />
</div>
<Button
onClick={onClose}
variant="ghost"

View File

@@ -3,10 +3,15 @@
import { api } from "~/trpc/react";
import { Badge } from "./ui/badge";
import { Button } from "./ui/button";
import { ContextualHelpIcon } from "./ContextualHelpIcon";
import { ExternalLink, Download, RefreshCw, Loader2 } from "lucide-react";
import { useState, useEffect, useRef } from "react";
interface VersionDisplayProps {
onOpenReleaseNotes?: () => void;
}
// Loading overlay component with log streaming
function LoadingOverlay({
isNetworkError = false,
@@ -72,7 +77,7 @@ function LoadingOverlay({
);
}
export function VersionDisplay() {
export function VersionDisplay({ onOpenReleaseNotes }: VersionDisplayProps = {}) {
const { data: versionStatus, isLoading, error } = api.version.getVersionStatus.useQuery();
const [isUpdating, setIsUpdating] = useState(false);
const [updateResult, setUpdateResult] = useState<{ success: boolean; message: string } | null>(null);
@@ -230,31 +235,16 @@ export function VersionDisplay() {
{isUpdating && <LoadingOverlay isNetworkError={isNetworkError} logs={updateLogs} />}
<div className="flex flex-col sm:flex-row items-center gap-2 sm:gap-2">
<Badge variant={isUpToDate ? "default" : "secondary"} className="text-xs">
<Badge
variant={isUpToDate ? "default" : "secondary"}
className={`text-xs ${onOpenReleaseNotes ? 'cursor-pointer hover:opacity-80 transition-opacity' : ''}`}
onClick={onOpenReleaseNotes}
>
v{currentVersion}
</Badge>
{updateAvailable && releaseInfo && (
<div className="flex flex-col sm:flex-row items-center gap-2 sm:gap-3">
<div className="relative group">
<Badge variant="destructive" className="animate-pulse cursor-help text-xs">
Update Available
</Badge>
<div className="absolute top-full left-1/2 transform -translate-x-1/2 mt-2 px-3 py-2 bg-gray-900 dark:bg-gray-700 text-white text-xs rounded-lg shadow-lg opacity-0 group-hover:opacity-100 transition-opacity duration-200 pointer-events-none whitespace-nowrap z-10 hidden sm:block">
<div className="text-center">
<div className="font-semibold mb-1">How to update:</div>
<div>Click the button to update, when installed via the helper script</div>
<div>or update manually:</div>
<div>cd $PVESCRIPTLOCAL_DIR</div>
<div>git pull</div>
<div>npm install</div>
<div>npm run build</div>
<div>npm start</div>
</div>
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 border-4 border-transparent border-b-gray-900 dark:border-b-gray-700"></div>
</div>
</div>
<div className="flex items-center gap-2">
<Button
onClick={handleUpdate}
@@ -278,6 +268,11 @@ export function VersionDisplay() {
)}
</Button>
<ContextualHelpIcon section="update-system" tooltip="Help with updates" />
</div>
<div className="flex items-center gap-1">
<span className="text-xs text-muted-foreground">Release Notes:</span>
<a
href={releaseInfo.htmlUrl}
target="_blank"

View File

@@ -1,7 +1,7 @@
'use client';
import { useState, useRef } from 'react';
import { useState, useRef, useEffect } from 'react';
import { ScriptsGrid } from './_components/ScriptsGrid';
import { DownloadedScriptsTab } from './_components/DownloadedScriptsTab';
import { InstalledScriptsTab } from './_components/InstalledScriptsTab';
@@ -9,20 +9,51 @@ import { ResyncButton } from './_components/ResyncButton';
import { Terminal } from './_components/Terminal';
import { ServerSettingsButton } from './_components/ServerSettingsButton';
import { SettingsButton } from './_components/SettingsButton';
import { HelpButton } from './_components/HelpButton';
import { VersionDisplay } from './_components/VersionDisplay';
import { Button } from './_components/ui/button';
import { ContextualHelpIcon } from './_components/ContextualHelpIcon';
import { ReleaseNotesModal, getLastSeenVersion } from './_components/ReleaseNotesModal';
import { Footer } from './_components/Footer';
import { Rocket, Package, HardDrive, FolderOpen } from 'lucide-react';
import { api } from '~/trpc/react';
export default function Home() {
const [runningScript, setRunningScript] = useState<{ path: string; name: string; mode?: 'local' | 'ssh'; server?: any } | null>(null);
const [activeTab, setActiveTab] = useState<'scripts' | 'downloaded' | 'installed'>('scripts');
const [releaseNotesOpen, setReleaseNotesOpen] = useState(false);
const [highlightVersion, setHighlightVersion] = useState<string | undefined>(undefined);
const terminalRef = useRef<HTMLDivElement>(null);
// Fetch data for script counts
const { data: scriptCardsData } = api.scripts.getScriptCardsWithCategories.useQuery();
const { data: localScriptsData } = api.scripts.getAllDownloadedScripts.useQuery();
const { data: installedScriptsData } = api.installedScripts.getAllInstalledScripts.useQuery();
const { data: versionData } = api.version.getCurrentVersion.useQuery();
// Auto-show release notes modal after update
useEffect(() => {
if (versionData?.success && versionData.version) {
const currentVersion = versionData.version;
const lastSeenVersion = getLastSeenVersion();
// If we have a current version and either no last seen version or versions don't match
if (currentVersion && (!lastSeenVersion || currentVersion !== lastSeenVersion)) {
setHighlightVersion(currentVersion);
setReleaseNotesOpen(true);
}
}
}, [versionData]);
const handleOpenReleaseNotes = () => {
setHighlightVersion(undefined);
setReleaseNotesOpen(true);
};
const handleCloseReleaseNotes = () => {
setReleaseNotesOpen(false);
setHighlightVersion(undefined);
};
// Calculate script counts
const scriptCounts = {
@@ -83,7 +114,7 @@ export default function Home() {
Manage and execute Proxmox helper scripts locally with live output streaming
</p>
<div className="flex justify-center px-2">
<VersionDisplay />
<VersionDisplay onOpenReleaseNotes={handleOpenReleaseNotes} />
</div>
</div>
@@ -93,6 +124,7 @@ export default function Home() {
<ServerSettingsButton />
<SettingsButton />
<ResyncButton />
<HelpButton />
</div>
</div>
@@ -100,54 +132,63 @@ export default function Home() {
<div className="mb-6 sm:mb-8">
<div className="border-b border-border">
<nav className="-mb-px flex flex-col sm:flex-row space-y-2 sm:space-y-0 sm:space-x-2 lg:space-x-8">
<Button
variant="ghost"
size="null"
onClick={() => setActiveTab('scripts')}
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
activeTab === 'scripts'
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
}`}>
<Package className="h-4 w-4" />
<span className="hidden sm:inline">Available Scripts</span>
<span className="sm:hidden">Available</span>
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
{scriptCounts.available}
</span>
</Button>
<Button
variant="ghost"
size="null"
onClick={() => setActiveTab('downloaded')}
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
activeTab === 'downloaded'
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
}`}>
<HardDrive className="h-4 w-4" />
<span className="hidden sm:inline">Downloaded Scripts</span>
<span className="sm:hidden">Downloaded</span>
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
{scriptCounts.downloaded}
</span>
</Button>
<Button
variant="ghost"
size="null"
onClick={() => setActiveTab('installed')}
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
activeTab === 'installed'
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
}`}>
<FolderOpen className="h-4 w-4" />
<span className="hidden sm:inline">Installed Scripts</span>
<span className="sm:hidden">Installed</span>
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
{scriptCounts.installed}
</span>
</Button>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="null"
onClick={() => setActiveTab('scripts')}
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
activeTab === 'scripts'
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
}`}>
<Package className="h-4 w-4" />
<span className="hidden sm:inline">Available Scripts</span>
<span className="sm:hidden">Available</span>
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
{scriptCounts.available}
</span>
</Button>
<ContextualHelpIcon section="available-scripts" tooltip="Help with Available Scripts" />
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="null"
onClick={() => setActiveTab('downloaded')}
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
activeTab === 'downloaded'
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
}`}>
<HardDrive className="h-4 w-4" />
<span className="hidden sm:inline">Downloaded Scripts</span>
<span className="sm:hidden">Downloaded</span>
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
{scriptCounts.downloaded}
</span>
</Button>
<ContextualHelpIcon section="downloaded-scripts" tooltip="Help with Downloaded Scripts" />
</div>
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="null"
onClick={() => setActiveTab('installed')}
className={`px-3 py-2 text-sm flex items-center justify-center sm:justify-start gap-2 w-full sm:w-auto ${
activeTab === 'installed'
? 'bg-accent text-accent-foreground rounded-t-md rounded-b-none'
: 'hover:bg-accent hover:text-accent-foreground hover:rounded-t-md hover:rounded-b-none'
}`}>
<FolderOpen className="h-4 w-4" />
<span className="hidden sm:inline">Installed Scripts</span>
<span className="sm:hidden">Installed</span>
<span className="ml-1 px-2 py-0.5 text-xs bg-muted text-muted-foreground rounded-full">
{scriptCounts.installed}
</span>
</Button>
<ContextualHelpIcon section="installed-scripts" tooltip="Help with Installed Scripts" />
</div>
</nav>
</div>
</div>
@@ -179,6 +220,16 @@ export default function Home() {
<InstalledScriptsTab />
)}
</div>
{/* Footer */}
<Footer onOpenReleaseNotes={handleOpenReleaseNotes} />
{/* Release Notes Modal */}
<ReleaseNotesModal
isOpen={releaseNotesOpen}
onClose={handleCloseReleaseNotes}
highlightVersion={highlightVersion}
/>
</main>
);
}

View File

@@ -11,6 +11,7 @@ interface GitHubRelease {
name: string;
published_at: string;
html_url: string;
body: string;
}
// Helper function to fetch from GitHub API with optional authentication
@@ -127,6 +128,43 @@ export const versionRouter = createTRPCRouter({
}
}),
// Get all releases for release notes
getAllReleases: publicProcedure
.query(async () => {
try {
const response = await fetchGitHubAPI('https://api.github.com/repos/community-scripts/ProxmoxVE-Local/releases');
if (!response.ok) {
throw new Error(`GitHub API error: ${response.status}`);
}
const releases: GitHubRelease[] = await response.json();
// Sort by published date (newest first)
const sortedReleases = releases
.filter(release => !release.tag_name.includes('beta') && !release.tag_name.includes('alpha'))
.sort((a, b) => new Date(b.published_at).getTime() - new Date(a.published_at).getTime());
return {
success: true,
releases: sortedReleases.map(release => ({
tagName: release.tag_name,
name: release.name,
publishedAt: release.published_at,
htmlUrl: release.html_url,
body: release.body
}))
};
} catch (error) {
console.error('Error fetching all releases:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Failed to fetch releases',
releases: []
};
}
}),
// Get update logs from the log file
getUpdateLogs: publicProcedure
.query(async () => {