From 2c3fdf5544c55308918707eaa9c93c989e1f4327 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 24 Oct 2025 21:22:46 +0200 Subject: [PATCH 01/15] Fix script card click issues and missing Install button - Fix type signature mismatch in handleCardClick functions - Updated ScriptsGrid.tsx and DownloadedScriptsTab.tsx to accept ScriptCardType instead of { slug: string } - This resolves the modal not opening when clicking script cards - Fix server-side import issues - Updated autoSyncService.js to import from githubJsonService.ts instead of .js - Fixed path aliases in githubJsonService.ts to use relative imports - Updated scripts.ts to import from TypeScript service files directly - Fix missing Install button - Resolved scriptDownloaderService.checkScriptExists method not being available - Install button now appears when script files exist locally - Remove debug logging - Cleaned up temporary console.log statements and debug UI elements All script card interactions now work properly: - Cards open detail modal when clicked - Install button appears when appropriate - Server-side API calls work correctly --- src/app/_components/DownloadedScriptsTab.tsx | 2 +- src/app/_components/ScriptsGrid.tsx | 2 +- src/server/api/routers/scripts.ts | 19 ++++++++++++++++--- src/server/services/autoSyncService.js | 2 +- src/server/services/githubJsonService.ts | 4 ++-- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/app/_components/DownloadedScriptsTab.tsx b/src/app/_components/DownloadedScriptsTab.tsx index 1d2f64c..94552c0 100644 --- a/src/app/_components/DownloadedScriptsTab.tsx +++ b/src/app/_components/DownloadedScriptsTab.tsx @@ -356,7 +356,7 @@ export function DownloadedScriptsTab({ onInstallScript }: DownloadedScriptsTabPr } }, [selectedCategory]); - const handleCardClick = (scriptCard: { slug: string }) => { + const handleCardClick = (scriptCard: ScriptCardType) => { // All scripts are GitHub scripts, open modal setSelectedSlug(scriptCard.slug); setIsModalOpen(true); diff --git a/src/app/_components/ScriptsGrid.tsx b/src/app/_components/ScriptsGrid.tsx index 5d82e70..1dea71e 100644 --- a/src/app/_components/ScriptsGrid.tsx +++ b/src/app/_components/ScriptsGrid.tsx @@ -574,7 +574,7 @@ export function ScriptsGrid({ onInstallScript }: ScriptsGridProps) { }, []); - const handleCardClick = (scriptCard: { slug: string }) => { + const handleCardClick = (scriptCard: ScriptCardType) => { // All scripts are GitHub scripts, open modal setSelectedSlug(scriptCard.slug); setIsModalOpen(true); diff --git a/src/server/api/routers/scripts.ts b/src/server/api/routers/scripts.ts index 00dc243..61947d4 100644 --- a/src/server/api/routers/scripts.ts +++ b/src/server/api/routers/scripts.ts @@ -1,9 +1,9 @@ import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; import { scriptManager } from "~/server/lib/scripts"; -import { githubJsonService } from "~/server/services/githubJsonService"; -import { localScriptsService } from "~/server/services/localScripts"; -import { scriptDownloaderService } from "~/server/services/scriptDownloader"; +import { githubJsonService } from "~/server/services/githubJsonService.ts"; +import { localScriptsService } from "~/server/services/localScripts.ts"; +import { scriptDownloaderService } from "~/server/services/scriptDownloader.ts"; import { AutoSyncService } from "~/server/services/autoSyncService"; import type { ScriptCard } from "~/types/script"; @@ -115,6 +115,18 @@ export const scriptsRouter = createTRPCRouter({ .input(z.object({ slug: z.string() })) .query(async ({ input }) => { try { + console.log('getScriptBySlug called with slug:', input.slug); + console.log('githubJsonService methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(githubJsonService))); + console.log('githubJsonService.getScriptBySlug type:', typeof githubJsonService.getScriptBySlug); + + if (typeof githubJsonService.getScriptBySlug !== 'function') { + return { + success: false, + error: 'getScriptBySlug method is not available on githubJsonService', + script: null + }; + } + const script = await githubJsonService.getScriptBySlug(input.slug); if (!script) { return { @@ -125,6 +137,7 @@ export const scriptsRouter = createTRPCRouter({ } return { success: true, script }; } catch (error) { + console.error('Error in getScriptBySlug:', error); return { success: false, error: error instanceof Error ? error.message : 'Failed to fetch script', diff --git a/src/server/services/autoSyncService.js b/src/server/services/autoSyncService.js index e980bf3..c3bf74a 100644 --- a/src/server/services/autoSyncService.js +++ b/src/server/services/autoSyncService.js @@ -1,5 +1,5 @@ import cron from 'node-cron'; -import { githubJsonService } from './githubJsonService.js'; +import { githubJsonService } from './githubJsonService.ts'; import { scriptDownloaderService } from './scriptDownloader.js'; import { appriseService } from './appriseService.js'; import { readFile, writeFile, readFileSync, writeFileSync } from 'fs'; diff --git a/src/server/services/githubJsonService.ts b/src/server/services/githubJsonService.ts index 5a6848f..344d51e 100644 --- a/src/server/services/githubJsonService.ts +++ b/src/server/services/githubJsonService.ts @@ -1,7 +1,7 @@ import { writeFile, mkdir } from 'fs/promises'; import { join } from 'path'; -import { env } from '~/env.js'; -import type { Script, ScriptCard, GitHubFile } from '~/types/script'; +import { env } from '../../env.js'; +import type { Script, ScriptCard, GitHubFile } from '../../types/script'; export class GitHubJsonService { private baseUrl: string | null = null; From b40f5b788c16b4071f569173bc5748410313e680 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 24 Oct 2025 21:58:35 +0200 Subject: [PATCH 02/15] Fix script downloader placeholder files issue - Delete stub scriptDownloader.js that contained placeholder implementation - Implement real JavaScript script downloader with GitHub fetch functionality - Fix incremental JSON sync to only process newly synced files - Add proper error handling and file structure management - Support all script types (ct/, tools/, vm/, vw/) with directory preservation - Download install scripts for CT scripts - Re-enable auto-sync service to use real implementation Scripts now download real content from GitHub instead of placeholders. --- src/server/api/routers/scripts.ts | 6 +- src/server/lib/scripts.ts | 25 +- src/server/services/autoSyncService.js | 151 +++++++++--- src/server/services/githubJsonService.js | 278 +---------------------- src/server/services/githubJsonService.ts | 84 +++++-- src/server/services/localScripts.js | 6 + src/server/services/scriptDownloader.js | 183 +++------------ 7 files changed, 251 insertions(+), 482 deletions(-) create mode 100644 src/server/services/localScripts.js diff --git a/src/server/api/routers/scripts.ts b/src/server/api/routers/scripts.ts index 61947d4..452989c 100644 --- a/src/server/api/routers/scripts.ts +++ b/src/server/api/routers/scripts.ts @@ -1,9 +1,9 @@ import { z } from "zod"; import { createTRPCRouter, publicProcedure } from "~/server/api/trpc"; import { scriptManager } from "~/server/lib/scripts"; -import { githubJsonService } from "~/server/services/githubJsonService.ts"; -import { localScriptsService } from "~/server/services/localScripts.ts"; -import { scriptDownloaderService } from "~/server/services/scriptDownloader.ts"; +import { githubJsonService } from "~/server/services/githubJsonService"; +import { localScriptsService } from "~/server/services/localScripts"; +import { scriptDownloaderService } from "~/server/services/scriptDownloader"; import { AutoSyncService } from "~/server/services/autoSyncService"; import type { ScriptCard } from "~/types/script"; diff --git a/src/server/lib/scripts.ts b/src/server/lib/scripts.ts index 33038f1..6cc408c 100644 --- a/src/server/lib/scripts.ts +++ b/src/server/lib/scripts.ts @@ -25,6 +25,25 @@ export class ScriptManager { // Initialize lazily to avoid accessing env vars during module load } + /** + * Safely handle file modification time, providing fallback for invalid dates + * @param mtime - The file modification time from fs.stat + * @returns Date - Valid date or current date as fallback + */ + private safeMtime(mtime: Date): Date { + try { + // Check if the date is valid + if (!mtime || isNaN(mtime.getTime())) { + console.warn('Invalid mtime detected, using current time as fallback'); + return new Date(); + } + return mtime; + } catch (error) { + console.warn('Error processing mtime:', error); + return new Date(); + } + } + private initializeConfig() { if (this.scriptsDir === null) { // Handle both absolute and relative paths for testing @@ -63,7 +82,7 @@ export class ScriptManager { path: filePath, extension, size: stats.size, - lastModified: stats.mtime, + lastModified: this.safeMtime(stats.mtime), executable }); } @@ -125,7 +144,7 @@ export class ScriptManager { path: filePath, extension, size: stats.size, - lastModified: stats.mtime, + lastModified: this.safeMtime(stats.mtime), executable, logo, slug @@ -212,7 +231,7 @@ export class ScriptManager { path: filePath, extension, size: stats.size, - lastModified: stats.mtime, + lastModified: this.safeMtime(stats.mtime), executable, logo, slug diff --git a/src/server/services/autoSyncService.js b/src/server/services/autoSyncService.js index c3bf74a..f8372b8 100644 --- a/src/server/services/autoSyncService.js +++ b/src/server/services/autoSyncService.js @@ -1,5 +1,5 @@ import cron from 'node-cron'; -import { githubJsonService } from './githubJsonService.ts'; +import { githubJsonService } from './githubJsonService.js'; import { scriptDownloaderService } from './scriptDownloader.js'; import { appriseService } from './appriseService.js'; import { readFile, writeFile, readFileSync, writeFileSync } from 'fs'; @@ -12,6 +12,25 @@ export class AutoSyncService { this.isRunning = false; } + /** + * Safely convert a date to ISO string, handling invalid dates + * @param {Date} date - The date to convert + * @returns {string} - ISO string or fallback timestamp + */ + safeToISOString(date) { + try { + // Check if the date is valid + if (!date || isNaN(date.getTime())) { + console.warn('Invalid date provided to safeToISOString, using current time as fallback'); + return new Date().toISOString(); + } + return date.toISOString(); + } catch (error) { + console.warn('Error converting date to ISO string:', error instanceof Error ? error.message : String(error)); + return new Date().toISOString(); + } + } + /** * Load auto-sync settings from .env file */ @@ -251,56 +270,120 @@ export class AutoSyncService { const results = { jsonSync: syncResult, - newScripts: [], - updatedScripts: [], - errors: [] + newScripts: /** @type {string[]} */ ([]), + updatedScripts: /** @type {string[]} */ ([]), + errors: /** @type {string[]} */ ([]) }; // Step 2: Auto-download/update scripts if enabled const settings = this.loadSettings(); if (settings.autoDownloadNew || settings.autoUpdateExisting) { + console.log('Processing synced JSON files for script downloads...'); + // Only process scripts for files that were actually synced - // @ts-ignore - syncedFiles exists in the JavaScript version if (syncResult.syncedFiles && syncResult.syncedFiles.length > 0) { - // @ts-ignore - syncedFiles exists in the JavaScript version - console.log(`Processing ${syncResult.syncedFiles.length} synced JSON files for new scripts...`); + console.log(`Processing ${syncResult.syncedFiles.length} synced JSON files for script downloads...`); + + // Get scripts only for the synced files + const localScriptsService = await import('./localScripts.js'); + const syncedScripts = []; - // Get all scripts from synced files - // @ts-ignore - syncedFiles exists in the JavaScript version - const allSyncedScripts = await githubJsonService.getScriptsForFiles(syncResult.syncedFiles); + for (const filename of syncResult.syncedFiles) { + try { + // Extract slug from filename (remove .json extension) + const slug = filename.replace('.json', ''); + const script = await localScriptsService.localScriptsService.getScriptBySlug(slug); + if (script) { + syncedScripts.push(script); + } + } catch (error) { + console.warn(`Error loading script from ${filename}:`, error); + } + } - // Initialize script downloader service - // @ts-ignore - initializeConfig is public in the JS version - scriptDownloaderService.initializeConfig(); + console.log(`Found ${syncedScripts.length} scripts from synced JSON files`); // Filter to only truly NEW scripts (not previously downloaded) const newScripts = []; - for (const script of allSyncedScripts) { - const isDownloaded = await scriptDownloaderService.isScriptDownloaded(script); - if (!isDownloaded) { - newScripts.push(script); + const existingScripts = []; + + for (const script of syncedScripts) { + try { + // Validate script object + if (!script || !script.slug) { + console.warn('Invalid script object found, skipping:', script); + continue; + } + + const isDownloaded = await scriptDownloaderService.isScriptDownloaded(script); + if (!isDownloaded) { + newScripts.push(script); + } else { + existingScripts.push(script); + } + } catch (error) { + console.warn(`Error checking script ${script?.slug || 'unknown'}:`, error); + // Treat as new script if we can't check + if (script && script.slug) { + newScripts.push(script); + } } } - console.log(`Found ${newScripts.length} new scripts out of ${allSyncedScripts.length} total scripts`); + console.log(`Found ${newScripts.length} new scripts and ${existingScripts.length} existing scripts from synced files`); + // Download new scripts if (settings.autoDownloadNew && newScripts.length > 0) { console.log(`Auto-downloading ${newScripts.length} new scripts...`); - const downloadResult = await scriptDownloaderService.autoDownloadNewScripts(newScripts); - // @ts-ignore - Type assertion needed for dynamic assignment - results.newScripts = downloadResult.downloaded; - // @ts-ignore - Type assertion needed for dynamic assignment - results.errors.push(...downloadResult.errors); + const downloaded = []; + const errors = []; + + for (const script of newScripts) { + try { + const result = await scriptDownloaderService.loadScript(script); + if (result.success) { + downloaded.push(script.name || script.slug); + console.log(`Downloaded script: ${script.name || script.slug}`); + } else { + errors.push(`${script.name || script.slug}: ${result.message}`); + } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + errors.push(`${script.name || script.slug}: ${errorMsg}`); + console.error(`Failed to download script ${script.slug}:`, error); + } + } + + results.newScripts = downloaded; + results.errors.push(...errors); } - if (settings.autoUpdateExisting) { - console.log('Auto-updating existing scripts from synced files...'); - const updateResult = await scriptDownloaderService.autoUpdateExistingScripts(allSyncedScripts); - // @ts-ignore - Type assertion needed for dynamic assignment - results.updatedScripts = updateResult.updated; - // @ts-ignore - Type assertion needed for dynamic assignment - results.errors.push(...updateResult.errors); + // Update existing scripts + if (settings.autoUpdateExisting && existingScripts.length > 0) { + console.log(`Auto-updating ${existingScripts.length} existing scripts...`); + const updated = []; + const errors = []; + + for (const script of existingScripts) { + try { + // Always update existing scripts when auto-update is enabled + const result = await scriptDownloaderService.loadScript(script); + if (result.success) { + updated.push(script.name || script.slug); + console.log(`Updated script: ${script.name || script.slug}`); + } else { + errors.push(`${script.name || script.slug}: ${result.message}`); + } + } catch (error) { + const errorMsg = error instanceof Error ? error.message : String(error); + errors.push(`${script.name || script.slug}: ${errorMsg}`); + console.error(`Failed to update script ${script.slug}:`, error); + } + } + + results.updatedScripts = updated; + results.errors.push(...errors); } } else { console.log('No JSON files were synced, skipping script download/update'); @@ -316,7 +399,7 @@ export class AutoSyncService { } // Step 4: Update last sync time - const lastSyncTime = new Date().toISOString(); + const lastSyncTime = this.safeToISOString(new Date()); const updatedSettings = { ...settings, lastAutoSync: lastSyncTime }; this.saveSettings(updatedSettings); @@ -384,6 +467,12 @@ export class AutoSyncService { const grouped = new Map(); scripts.forEach(script => { + // Validate script object + if (!script || !script.name) { + console.warn('Invalid script object in groupScriptsByCategory, skipping:', script); + return; + } + const scriptCategories = script.categories || [0]; // Default to Miscellaneous (id: 0) scriptCategories.forEach((/** @type {number} */ catId) => { const categoryName = categoryMap.get(catId) || 'Miscellaneous'; diff --git a/src/server/services/githubJsonService.js b/src/server/services/githubJsonService.js index a5cdb6a..a3de782 100644 --- a/src/server/services/githubJsonService.js +++ b/src/server/services/githubJsonService.js @@ -1,276 +1,6 @@ -import { writeFile, mkdir } from 'fs/promises'; -import { readFileSync, readdirSync, statSync, utimesSync } from 'fs'; -import { join } from 'path'; -import { Buffer } from 'buffer'; +// JavaScript wrapper for githubJsonService.ts +// This allows the JavaScript autoSyncService.js to import the TypeScript service -export class GitHubJsonService { - constructor() { - this.baseUrl = null; - this.repoUrl = null; - this.branch = null; - this.jsonFolder = null; - this.localJsonDirectory = null; - this.scriptCache = new Map(); - } +import { githubJsonService } from './githubJsonService.ts'; - initializeConfig() { - if (this.repoUrl === null) { - // Get environment variables - this.repoUrl = process.env.REPO_URL || ""; - this.branch = process.env.REPO_BRANCH || "main"; - this.jsonFolder = process.env.JSON_FOLDER || "scripts"; - this.localJsonDirectory = join(process.cwd(), 'scripts', 'json'); - - // Only validate GitHub URL if it's provided - if (this.repoUrl) { - // Extract owner and repo from the URL - const urlMatch = /github\.com\/([^\/]+)\/([^\/]+)/.exec(this.repoUrl); - if (!urlMatch) { - throw new Error(`Invalid GitHub repository URL: ${this.repoUrl}`); - } - - const [, owner, repo] = urlMatch; - this.baseUrl = `https://api.github.com/repos/${owner}/${repo}`; - } else { - // Set a dummy base URL if no REPO_URL is provided - this.baseUrl = ""; - } - } - } - - async fetchFromGitHub(endpoint) { - this.initializeConfig(); - const response = await fetch(`${this.baseUrl}${endpoint}`, { - headers: { - 'Accept': 'application/vnd.github.v3+json', - 'User-Agent': 'PVEScripts-Local/1.0', - ...(process.env.GITHUB_TOKEN && { 'Authorization': `token ${process.env.GITHUB_TOKEN}` }) - }, - }); - - if (!response.ok) { - throw new Error(`GitHub API error: ${response.status} ${response.statusText}`); - } - - return response.json(); - } - - async syncJsonFiles() { - try { - this.initializeConfig(); - - if (!this.baseUrl) { - return { - success: false, - message: 'No GitHub repository configured' - }; - } - - console.log('Starting fast incremental JSON sync...'); - - // Ensure local directory exists - await mkdir(this.localJsonDirectory, { recursive: true }); - - // Step 1: Get file list from GitHub (single API call) - console.log('Fetching file list from GitHub...'); - const files = await this.fetchFromGitHub(`/contents/${this.jsonFolder}?ref=${this.branch}`); - - if (!Array.isArray(files)) { - throw new Error('Invalid response from GitHub API'); - } - - const jsonFiles = files.filter(file => file.name.endsWith('.json')); - console.log(`Found ${jsonFiles.length} JSON files in repository`); - - // Step 2: Get local file list (fast local operation) - const localFiles = new Map(); - try { - console.log(`Looking for local files in: ${this.localJsonDirectory}`); - const localFileList = readdirSync(this.localJsonDirectory); - console.log(`Found ${localFileList.length} files in local directory`); - for (const fileName of localFileList) { - if (fileName.endsWith('.json')) { - const filePath = join(this.localJsonDirectory, fileName); - const stats = statSync(filePath); - localFiles.set(fileName, { - mtime: stats.mtime, - size: stats.size - }); - } - } - } catch (error) { - console.log('Error reading local directory:', error.message); - console.log('Directory path:', this.localJsonDirectory); - console.log('No local files found, will download all'); - } - - console.log(`Found ${localFiles.size} local JSON files`); - - // Step 3: Compare and identify files that need syncing - const filesToSync = []; - let skippedCount = 0; - - for (const file of jsonFiles) { - const localFile = localFiles.get(file.name); - - if (!localFile) { - // File doesn't exist locally - filesToSync.push(file); - console.log(`Missing: ${file.name}`); - } else { - // Compare modification times and sizes - const localMtime = new Date(localFile.mtime); - const remoteMtime = new Date(file.updated_at); - const localSize = localFile.size; - const remoteSize = file.size; - - // Sync if remote is newer OR sizes are different (content changed) - if (localMtime < remoteMtime || localSize !== remoteSize) { - filesToSync.push(file); - console.log(`Changed: ${file.name} (${localMtime.toISOString()} -> ${remoteMtime.toISOString()})`); - } else { - skippedCount++; - console.log(`Up-to-date: ${file.name}`); - } - } - } - - console.log(`Files to sync: ${filesToSync.length}, Up-to-date: ${skippedCount}`); - - // Step 4: Download only the files that need syncing - let syncedCount = 0; - const errors = []; - const syncedFiles = []; - - // Process files in batches to avoid overwhelming the API - const batchSize = 10; - for (let i = 0; i < filesToSync.length; i += batchSize) { - const batch = filesToSync.slice(i, i + batchSize); - - // Process batch in parallel - const promises = batch.map(async (file) => { - try { - const content = await this.fetchFromGitHub(`/contents/${file.path}?ref=${this.branch}`); - - if (content.content) { - // Decode base64 content - const fileContent = Buffer.from(content.content, 'base64').toString('utf-8'); - - // Write to local file - const localPath = join(this.localJsonDirectory, file.name); - await writeFile(localPath, fileContent, 'utf-8'); - - // Update file modification time to match remote - const remoteMtime = new Date(file.updated_at); - utimesSync(localPath, remoteMtime, remoteMtime); - - syncedCount++; - syncedFiles.push(file.name); - console.log(`Synced: ${file.name}`); - } - } catch (error) { - console.error(`Failed to sync ${file.name}:`, error.message); - errors.push(`${file.name}: ${error.message}`); - } - }); - - await Promise.all(promises); - - // Small delay between batches to be nice to the API - if (i + batchSize < filesToSync.length) { - await new Promise(resolve => setTimeout(resolve, 100)); - } - } - - console.log(`JSON sync completed. Synced ${syncedCount} files, skipped ${skippedCount} files.`); - - return { - success: true, - message: `Successfully synced ${syncedCount} JSON files (${skippedCount} up-to-date)`, - syncedCount, - skippedCount, - syncedFiles, - errors - }; - - } catch (error) { - console.error('JSON sync failed:', error); - return { - success: false, - message: error.message, - error: error.message - }; - } - } - - async getAllScripts() { - try { - this.initializeConfig(); - - if (!this.localJsonDirectory) { - return []; - } - - const scripts = []; - - // Read all JSON files from local directory - const files = readdirSync(this.localJsonDirectory); - const jsonFiles = files.filter(file => file.endsWith('.json')); - - for (const file of jsonFiles) { - try { - const filePath = join(this.localJsonDirectory, file); - const content = readFileSync(filePath, 'utf-8'); - const script = JSON.parse(content); - - if (script && typeof script === 'object') { - scripts.push(script); - } - } catch (error) { - console.error(`Failed to parse ${file}:`, error.message); - } - } - - return scripts; - } catch (error) { - console.error('Failed to get all scripts:', error); - return []; - } - } - - /** - * Get scripts only for specific JSON files that were synced - */ - async getScriptsForFiles(syncedFiles) { - try { - this.initializeConfig(); - - if (!this.localJsonDirectory || !syncedFiles || syncedFiles.length === 0) { - return []; - } - - const scripts = []; - - for (const fileName of syncedFiles) { - try { - const filePath = join(this.localJsonDirectory, fileName); - const content = readFileSync(filePath, 'utf-8'); - const script = JSON.parse(content); - - if (script && typeof script === 'object') { - scripts.push(script); - } - } catch (error) { - console.error(`Failed to parse ${fileName}:`, error.message); - } - } - - return scripts; - } catch (error) { - console.error('Failed to get scripts for synced files:', error); - return []; - } - } -} - -export const githubJsonService = new GitHubJsonService(); +export { githubJsonService }; diff --git a/src/server/services/githubJsonService.ts b/src/server/services/githubJsonService.ts index 344d51e..30e0742 100644 --- a/src/server/services/githubJsonService.ts +++ b/src/server/services/githubJsonService.ts @@ -1,4 +1,4 @@ -import { writeFile, mkdir } from 'fs/promises'; +import { writeFile, mkdir, readdir } from 'fs/promises'; import { join } from 'path'; import { env } from '../../env.js'; import type { Script, ScriptCard, GitHubFile } from '../../types/script'; @@ -185,48 +185,90 @@ export class GitHubJsonService { } } - async syncJsonFiles(): Promise<{ success: boolean; message: string; count: number }> { + async syncJsonFiles(): Promise<{ success: boolean; message: string; count: number; syncedFiles: string[] }> { try { - // Get all scripts from GitHub (1 API call + raw downloads) - const scripts = await this.getAllScripts(); + console.log('Starting fast incremental JSON sync...'); + + // Get file list from GitHub + console.log('Fetching file list from GitHub...'); + const githubFiles = await this.getJsonFiles(); + console.log(`Found ${githubFiles.length} JSON files in repository`); + + // Get local files + const localFiles = await this.getLocalJsonFiles(); + console.log(`Found ${localFiles.length} files in local directory`); + console.log(`Found ${localFiles.filter(f => f.endsWith('.json')).length} local JSON files`); + + // Compare and find files that need syncing + const filesToSync = this.findFilesToSync(githubFiles, localFiles); + console.log(`Found ${filesToSync.length} files that need syncing`); + + if (filesToSync.length === 0) { + return { + success: true, + message: 'All JSON files are up to date', + count: 0, + syncedFiles: [] + }; + } - // Save scripts to local directory - await this.saveScriptsLocally(scripts); + // Download and save only the files that need syncing + const syncedFiles = await this.syncSpecificFiles(filesToSync); return { success: true, - message: `Successfully synced ${scripts.length} scripts from GitHub using 1 API call + raw downloads`, - count: scripts.length + message: `Successfully synced ${syncedFiles.length} JSON files from GitHub`, + count: syncedFiles.length, + syncedFiles }; } catch (error) { - console.error('Error syncing JSON files:', error); + console.error('JSON sync failed:', error); return { success: false, message: `Failed to sync JSON files: ${error instanceof Error ? error.message : 'Unknown error'}`, - count: 0 + count: 0, + syncedFiles: [] }; } } - private async saveScriptsLocally(scripts: Script[]): Promise { + private async getLocalJsonFiles(): Promise { this.initializeConfig(); try { - // Ensure the directory exists - await mkdir(this.localJsonDirectory!, { recursive: true }); + const files = await readdir(this.localJsonDirectory!); + return files.filter(f => f.endsWith('.json')); + } catch { + return []; + } + } - // Save each script as a JSON file - for (const script of scripts) { + private findFilesToSync(githubFiles: GitHubFile[], localFiles: string[]): GitHubFile[] { + const localFileSet = new Set(localFiles); + // Return only files that don't exist locally + return githubFiles.filter(ghFile => !localFileSet.has(ghFile.name)); + } + + private async syncSpecificFiles(filesToSync: GitHubFile[]): Promise { + this.initializeConfig(); + const syncedFiles: string[] = []; + + await mkdir(this.localJsonDirectory!, { recursive: true }); + + for (const file of filesToSync) { + try { + const script = await this.downloadJsonFile(file.path); const filename = `${script.slug}.json`; const filePath = join(this.localJsonDirectory!, filename); - const content = JSON.stringify(script, null, 2); - await writeFile(filePath, content, 'utf-8'); + await writeFile(filePath, JSON.stringify(script, null, 2), 'utf-8'); + syncedFiles.push(filename); + } catch (error) { + console.error(`Failed to sync ${file.name}:`, error); } - - } catch (error) { - console.error('Error saving scripts locally:', error); - throw new Error('Failed to save scripts locally'); } + + return syncedFiles; } + } // Singleton instance diff --git a/src/server/services/localScripts.js b/src/server/services/localScripts.js new file mode 100644 index 0000000..410ab48 --- /dev/null +++ b/src/server/services/localScripts.js @@ -0,0 +1,6 @@ +// JavaScript wrapper for localScripts.ts +// This allows the JavaScript autoSyncService.js to import the TypeScript service + +import { localScriptsService } from './localScripts.ts'; + +export { localScriptsService }; diff --git a/src/server/services/scriptDownloader.js b/src/server/services/scriptDownloader.js index fd06b8a..5bb69d2 100644 --- a/src/server/services/scriptDownloader.js +++ b/src/server/services/scriptDownloader.js @@ -1,14 +1,18 @@ -import { writeFile, readFile, mkdir } from 'fs/promises'; +// Real JavaScript implementation for script downloading import { join } from 'path'; +import { writeFile, mkdir } from 'fs/promises'; export class ScriptDownloaderService { constructor() { this.scriptsDirectory = null; + this.repoUrl = null; } initializeConfig() { if (this.scriptsDirectory === null) { this.scriptsDirectory = join(process.cwd(), 'scripts'); + // Get REPO_URL from environment or use default + this.repoUrl = process.env.REPO_URL || 'https://github.com/community-scripts/ProxmoxVE'; } } @@ -23,14 +27,27 @@ export class ScriptDownloaderService { } async downloadFileFromGitHub(filePath) { - // This is a simplified version - in a real implementation, - // you would fetch the file content from GitHub - // For now, we'll return a placeholder - return `#!/bin/bash -# Downloaded script: ${filePath} -# This is a placeholder - implement actual GitHub file download -echo "Script downloaded: ${filePath}" -`; + this.initializeConfig(); + if (!this.repoUrl) { + throw new Error('REPO_URL environment variable is not set'); + } + + // Extract repo path from URL + const match = /github\.com\/([^\/]+)\/([^\/]+)/.exec(this.repoUrl); + if (!match) { + throw new Error('Invalid GitHub repository URL'); + } + const [, owner, repo] = match; + + const url = `https://raw.githubusercontent.com/${owner}/${repo}/main/${filePath}`; + + console.log(`Downloading from GitHub: ${url}`); + const response = await fetch(url); + if (!response.ok) { + throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`); + } + + return response.text(); } modifyScriptContent(content) { @@ -57,6 +74,7 @@ echo "Script downloaded: ${filePath}" if (fileName) { // Download from GitHub + console.log(`Downloading script file: ${scriptPath}`); const content = await this.downloadFileFromGitHub(scriptPath); // Determine target directory based on script path @@ -111,6 +129,7 @@ echo "Script downloaded: ${filePath}" } files.push(`${finalTargetDir}/${fileName}`); + console.log(`Successfully downloaded: ${finalTargetDir}/${fileName}`); } } } @@ -121,12 +140,15 @@ echo "Script downloaded: ${filePath}" if (hasCtScript) { const installScriptName = `${script.slug}-install.sh`; try { + console.log(`Downloading install script: install/${installScriptName}`); const installContent = await this.downloadFileFromGitHub(`install/${installScriptName}`); const localInstallPath = join(this.scriptsDirectory, 'install', installScriptName); await writeFile(localInstallPath, installContent, 'utf-8'); files.push(`install/${installScriptName}`); - } catch { + console.log(`Successfully downloaded: install/${installScriptName}`); + } catch (error) { // Install script might not exist, that's okay + console.log(`Install script not found: install/${installScriptName}`); } } @@ -145,78 +167,6 @@ echo "Script downloaded: ${filePath}" } } - /** - * Auto-download new scripts that haven't been downloaded yet - */ - async autoDownloadNewScripts(allScripts) { - this.initializeConfig(); - const downloaded = []; - const errors = []; - - for (const script of allScripts) { - try { - // Check if script is already downloaded - const isDownloaded = await this.isScriptDownloaded(script); - - if (!isDownloaded) { - const result = await this.loadScript(script); - if (result.success) { - downloaded.push(script); // Return full script object instead of just name - console.log(`Auto-downloaded new script: ${script.name || script.slug}`); - } else { - errors.push(`${script.name || script.slug}: ${result.message}`); - } - } - } catch (error) { - const errorMsg = `${script.name || script.slug}: ${error instanceof Error ? error.message : 'Unknown error'}`; - errors.push(errorMsg); - console.error(`Failed to auto-download script ${script.slug}:`, error); - } - } - - return { downloaded, errors }; - } - - /** - * Auto-update existing scripts to newer versions - */ - async autoUpdateExistingScripts(allScripts) { - this.initializeConfig(); - const updated = []; - const errors = []; - - for (const script of allScripts) { - try { - // Check if script is downloaded - const isDownloaded = await this.isScriptDownloaded(script); - - if (isDownloaded) { - // Check if update is needed by comparing content - const needsUpdate = await this.scriptNeedsUpdate(script); - - if (needsUpdate) { - const result = await this.loadScript(script); - if (result.success) { - updated.push(script); // Return full script object instead of just name - console.log(`Auto-updated script: ${script.name || script.slug}`); - } else { - errors.push(`${script.name || script.slug}: ${result.message}`); - } - } - } - } catch (error) { - const errorMsg = `${script.name || script.slug}: ${error instanceof Error ? error.message : 'Unknown error'}`; - errors.push(errorMsg); - console.error(`Failed to auto-update script ${script.slug}:`, error); - } - } - - return { updated, errors }; - } - - /** - * Check if a script is already downloaded - */ async isScriptDownloaded(script) { if (!script.install_methods?.length) return false; @@ -261,7 +211,7 @@ echo "Script downloaded: ${filePath}" } try { - await readFile(filePath, 'utf8'); + await import('fs/promises').then(fs => fs.readFile(filePath, 'utf8')); // File exists, continue checking other methods } catch { // File doesn't exist, script is not fully downloaded @@ -274,73 +224,6 @@ echo "Script downloaded: ${filePath}" // All files exist, script is downloaded return true; } - - /** - * Check if a script needs updating by comparing local and remote content - */ - async scriptNeedsUpdate(script) { - if (!script.install_methods?.length) return false; - - for (const method of script.install_methods) { - if (method.script) { - const scriptPath = method.script; - const fileName = scriptPath.split('/').pop(); - - if (fileName) { - // Determine target directory based on script path - let targetDir; - let finalTargetDir; - let filePath; - - if (scriptPath.startsWith('ct/')) { - targetDir = 'ct'; - finalTargetDir = targetDir; - filePath = join(this.scriptsDirectory, targetDir, fileName); - } else if (scriptPath.startsWith('tools/')) { - targetDir = 'tools'; - const subPath = scriptPath.replace('tools/', ''); - const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : ''; - finalTargetDir = subDir ? join(targetDir, subDir) : targetDir; - filePath = join(this.scriptsDirectory, finalTargetDir, fileName); - } else if (scriptPath.startsWith('vm/')) { - targetDir = 'vm'; - const subPath = scriptPath.replace('vm/', ''); - const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : ''; - finalTargetDir = subDir ? join(targetDir, subDir) : targetDir; - filePath = join(this.scriptsDirectory, finalTargetDir, fileName); - } else if (scriptPath.startsWith('vw/')) { - targetDir = 'vw'; - const subPath = scriptPath.replace('vw/', ''); - const subDir = subPath.includes('/') ? subPath.substring(0, subPath.lastIndexOf('/')) : ''; - finalTargetDir = subDir ? join(targetDir, subDir) : targetDir; - filePath = join(this.scriptsDirectory, finalTargetDir, fileName); - } else { - targetDir = 'ct'; - finalTargetDir = targetDir; - filePath = join(this.scriptsDirectory, targetDir, fileName); - } - - try { - // Read local content - const localContent = await readFile(filePath, 'utf8'); - - // Download remote content - const remoteContent = await this.downloadFileFromGitHub(scriptPath); - - // Compare content (simple string comparison for now) - // In a more sophisticated implementation, you might want to compare - // file modification times or use content hashing - return localContent !== remoteContent; - } catch { - // If we can't read local or download remote, assume update needed - return true; - } - } - } - } - - return false; - } } export const scriptDownloaderService = new ScriptDownloaderService(); From 6c982050dadadaf4169ce19f9f81ea0faf8eb766 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 24 Oct 2025 22:02:56 +0200 Subject: [PATCH 03/15] Fix notification to show script names grouped by category - Store full script objects instead of just names in results.newScripts and results.updatedScripts - This allows groupScriptsByCategory to properly group scripts by their categories - Notification will now show actual script names grouped by category instead of 'undefined synced, undefined up to date' - Script objects contain name, slug, and categories fields needed for proper grouping --- src/server/services/autoSyncService.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/server/services/autoSyncService.js b/src/server/services/autoSyncService.js index f8372b8..99da6c8 100644 --- a/src/server/services/autoSyncService.js +++ b/src/server/services/autoSyncService.js @@ -343,7 +343,7 @@ export class AutoSyncService { try { const result = await scriptDownloaderService.loadScript(script); if (result.success) { - downloaded.push(script.name || script.slug); + downloaded.push(script); // Store full script object for category grouping console.log(`Downloaded script: ${script.name || script.slug}`); } else { errors.push(`${script.name || script.slug}: ${result.message}`); @@ -370,7 +370,7 @@ export class AutoSyncService { // Always update existing scripts when auto-update is enabled const result = await scriptDownloaderService.loadScript(script); if (result.success) { - updated.push(script.name || script.slug); + updated.push(script); // Store full script object for category grouping console.log(`Updated script: ${script.name || script.slug}`); } else { errors.push(`${script.name || script.slug}: ${result.message}`); @@ -504,7 +504,18 @@ export class AutoSyncService { // @ts-ignore - Dynamic property access if (results.jsonSync) { // @ts-ignore - Dynamic property access - body += `JSON Files: ${results.jsonSync.syncedCount} synced, ${results.jsonSync.skippedCount} up-to-date\n`; + const syncedCount = results.jsonSync.count || 0; + // @ts-ignore - Dynamic property access + const syncedFiles = results.jsonSync.syncedFiles || []; + + // Calculate up-to-date count (total files - synced files) + // We can't easily get total file count from the sync result, so just show synced count + if (syncedCount > 0) { + body += `JSON Files: ${syncedCount} synced\n`; + } else { + body += `JSON Files: All up-to-date\n`; + } + // @ts-ignore - Dynamic property access if (results.jsonSync.errors?.length > 0) { // @ts-ignore - Dynamic property access From e40bd1f6a39c1af4e82b1f2e08e3d5f40fa7e337 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 24 Oct 2025 22:04:23 +0200 Subject: [PATCH 04/15] Fix CT script source line replacement - Implement modifyScriptContent method to replace GitHub source line with local source - Replace 'source <(curl -fsSL https://raw.githubusercontent.com/community-scripts/ProxmoxVE/main/misc/build.func)' with 'SCRIPT_DIR="." \nsource "/../core/build.func"' - This ensures CT scripts use local build.func instead of downloading from GitHub - Applied to all CT scripts during download process - Tested with 2fauth script - replacement works correctly --- src/server/services/scriptDownloader.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/server/services/scriptDownloader.js b/src/server/services/scriptDownloader.js index 5bb69d2..9db3d60 100644 --- a/src/server/services/scriptDownloader.js +++ b/src/server/services/scriptDownloader.js @@ -51,8 +51,11 @@ export class ScriptDownloaderService { } modifyScriptContent(content) { - // Modify script content for CT scripts if needed - return content; + // Replace the build.func source line + const oldPattern = /source <\(curl -fsSL https:\/\/raw\.githubusercontent\.com\/community-scripts\/ProxmoxVE\/main\/misc\/build\.func\)/g; + const newPattern = 'SCRIPT_DIR="$(dirname "$0")" \nsource "$SCRIPT_DIR/../core/build.func"'; + + return content.replace(oldPattern, newPattern); } async loadScript(script) { From 9a8cff32273ce1492cb65ff099159a0d646847f8 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 24 Oct 2025 22:06:34 +0200 Subject: [PATCH 05/15] Add missing checkScriptExists method to scriptDownloader.js - Add checkScriptExists method that was missing from JavaScript implementation - This method is required by the checkScriptFiles API endpoint - Method checks if CT script and install script files exist locally - Returns ctExists, installExists, and files array - Fixes issue where UI shows 'Load Script' instead of 'Install' button for downloaded scripts --- src/server/services/scriptDownloader.js | 65 +++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/src/server/services/scriptDownloader.js b/src/server/services/scriptDownloader.js index 9db3d60..61e33e7 100644 --- a/src/server/services/scriptDownloader.js +++ b/src/server/services/scriptDownloader.js @@ -227,6 +227,71 @@ export class ScriptDownloaderService { // All files exist, script is downloaded return true; } + + async checkScriptExists(script) { + this.initializeConfig(); + const files = []; + let ctExists = false; + let installExists = false; + + try { + // Check scripts based on their install methods + if (script.install_methods?.length) { + for (const method of script.install_methods) { + if (method.script) { + const scriptPath = method.script; + const fileName = scriptPath.split('/').pop(); + + if (fileName) { + let targetDir; + if (scriptPath.startsWith('ct/')) { + targetDir = 'ct'; + } else if (scriptPath.startsWith('tools/')) { + targetDir = 'tools'; + } else if (scriptPath.startsWith('vm/')) { + targetDir = 'vm'; + } else { + targetDir = 'ct'; // Default fallback + } + + const filePath = join(this.scriptsDirectory, targetDir, fileName); + + try { + await access(filePath); + files.push(`${targetDir}/${fileName}`); + + if (scriptPath.startsWith('ct/')) { + ctExists = true; + } + } catch { + // File doesn't exist + } + } + } + } + } + + // Check for install script for CT scripts + const hasCtScript = script.install_methods?.some(method => method.script?.startsWith('ct/')); + if (hasCtScript) { + const installScriptName = `${script.slug}-install.sh`; + const installPath = join(this.scriptsDirectory, 'install', installScriptName); + + try { + await access(installPath); + files.push(`install/${installScriptName}`); + installExists = true; + } catch { + // Install script doesn't exist + } + } + + return { ctExists, installExists, files }; + } catch (error) { + console.error('Error checking script existence:', error); + return { ctExists: false, installExists: false, files: [] }; + } + } } export const scriptDownloaderService = new ScriptDownloaderService(); From 8fc9b27f559fbb354bc8c05b7da101d7947459ee Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 24 Oct 2025 22:07:46 +0200 Subject: [PATCH 06/15] Fix missing access import in scriptDownloader.js - Add missing 'access' import from 'fs/promises' - This was causing checkScriptExists method to fail with 'access is not defined' error - Now properly detects when script files exist locally - Fixes issue where UI shows 'Load Script' instead of 'Install' button for downloaded scripts --- src/server/services/scriptDownloader.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server/services/scriptDownloader.js b/src/server/services/scriptDownloader.js index 61e33e7..0a7ece0 100644 --- a/src/server/services/scriptDownloader.js +++ b/src/server/services/scriptDownloader.js @@ -1,6 +1,6 @@ // Real JavaScript implementation for script downloading import { join } from 'path'; -import { writeFile, mkdir } from 'fs/promises'; +import { writeFile, mkdir, access } from 'fs/promises'; export class ScriptDownloaderService { constructor() { From 926032e83b6334f4551d52d08a246f75da251c64 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 24 Oct 2025 22:09:16 +0200 Subject: [PATCH 07/15] Fix auto-sync service not stopping when disabled - Use global auto-sync service instance instead of creating new instances - This ensures stopAutoSync() is called on the actual running service - Fix .env file parsing to handle comments and empty lines properly - Update both TRPC and REST API endpoints to use global instance - Auto-sync service will now properly stop when disabled in settings --- src/app/api/settings/auto-sync/route.ts | 10 +++++++-- src/server/api/routers/scripts.ts | 11 +++++++++- src/server/services/autoSyncService.js | 29 +++++++++++++++++++------ 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/app/api/settings/auto-sync/route.ts b/src/app/api/settings/auto-sync/route.ts index 231e0d7..035c8d9 100644 --- a/src/app/api/settings/auto-sync/route.ts +++ b/src/app/api/settings/auto-sync/route.ts @@ -160,8 +160,14 @@ export async function POST(request: NextRequest) { // Reschedule auto-sync service with new settings try { - const { AutoSyncService } = await import('../../../../server/services/autoSyncService.js'); - const autoSyncService = new AutoSyncService(); + const { getAutoSyncService } = await import('../../../../server/lib/autoSyncInit.js'); + let autoSyncService = getAutoSyncService(); + + // If no global instance exists, create one + if (!autoSyncService) { + const { AutoSyncService } = await import('../../../../server/services/autoSyncService.js'); + autoSyncService = new AutoSyncService(); + } if (settings.autoSyncEnabled) { autoSyncService.scheduleAutoSync(); diff --git a/src/server/api/routers/scripts.ts b/src/server/api/routers/scripts.ts index 452989c..cc01602 100644 --- a/src/server/api/routers/scripts.ts +++ b/src/server/api/routers/scripts.ts @@ -503,7 +503,16 @@ export const scriptsRouter = createTRPCRouter({ })) .mutation(async ({ input }) => { try { - const autoSyncService = new AutoSyncService(); + // Use the global auto-sync service instance + const { getAutoSyncService } = await import('~/server/lib/autoSyncInit'); + let autoSyncService = getAutoSyncService(); + + // If no global instance exists, create one + if (!autoSyncService) { + const { AutoSyncService } = await import('~/server/services/autoSyncService'); + autoSyncService = new AutoSyncService(); + } + autoSyncService.saveSettings(input); // Reschedule auto-sync if enabled diff --git a/src/server/services/autoSyncService.js b/src/server/services/autoSyncService.js index 99da6c8..1d779b8 100644 --- a/src/server/services/autoSyncService.js +++ b/src/server/services/autoSyncService.js @@ -155,13 +155,28 @@ export class AutoSyncService { const existingKeys = new Set(); for (const line of lines) { - const [key] = line.split('='); - const trimmedKey = key?.trim(); - if (trimmedKey && trimmedKey in settingsMap) { - // @ts-ignore - Dynamic key access is safe here - newLines.push(`${trimmedKey}=${settingsMap[trimmedKey]}`); - existingKeys.add(trimmedKey); - } else if (trimmedKey && !(trimmedKey in settingsMap)) { + const trimmedLine = line.trim(); + + // Skip empty lines and comments + if (!trimmedLine || trimmedLine.startsWith('#')) { + newLines.push(line); + continue; + } + + const equalIndex = trimmedLine.indexOf('='); + if (equalIndex === -1) { + // Line doesn't contain '=', keep as is + newLines.push(line); + continue; + } + + const key = trimmedLine.substring(0, equalIndex).trim(); + if (key && key in settingsMap) { + // Replace existing setting + newLines.push(`${key}=${settingsMap[key]}`); + existingKeys.add(key); + } else { + // Keep other settings as is newLines.push(line); } } From 5acaf144fbb9e234d5991ed9f7d1b8f1c494d3e8 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 24 Oct 2025 22:15:24 +0200 Subject: [PATCH 08/15] Fix autosync toggle disable functionality - Fix service instance management to use global instance for stopping autosync - Add automatic saving when toggle is changed (no manual save required) - Fix validation issue where custom sync type without cron expression caused 400 error - Add comprehensive debugging and error handling - Ensure .env file is properly updated with AUTO_SYNC_ENABLED value - Improve service lifecycle management with proper state cleanup - Add fallback logic for invalid sync interval configurations Resolves issue where disabling autosync in GUI didn't update .env file or stop service --- .env.example | 9 + scripts/json/bunkerweb.json | 2 +- scripts/json/execute.json | 48 + scripts/json/glpi.json | 2 +- scripts/json/guardian.json | 2 +- scripts/json/jellyfin.json | 2 +- scripts/json/jotty.json | 40 + scripts/json/kavita.json | 2 +- scripts/json/metadata.json | 370 ++--- scripts/json/mysql.json | 2 +- scripts/json/nextcloudpi.json | 2 +- scripts/json/odoo.json | 2 +- scripts/json/omv.json | 2 +- scripts/json/open-archiver.json | 40 + scripts/json/openwebui.json | 2 +- scripts/json/paperless-ai.json | 4 +- scripts/json/tautulli.json | 2 +- scripts/json/technitiumdns.json | 2 +- scripts/json/undefined.json | 900 +++++------ scripts/json/versions.json | 1492 ++++++++++-------- scripts/json/zot-registry.json | 2 +- src/app/_components/GeneralSettingsModal.tsx | 81 +- src/app/api/settings/auto-sync/route.ts | 31 +- src/server/api/routers/scripts.ts | 6 +- src/server/lib/autoSyncInit.js | 11 + src/server/lib/autoSyncInit.ts | 7 + src/server/services/autoSyncService.js | 8 +- 27 files changed, 1690 insertions(+), 1383 deletions(-) create mode 100644 scripts/json/execute.json create mode 100644 scripts/json/jotty.json create mode 100644 scripts/json/open-archiver.json diff --git a/.env.example b/.env.example index bc8a796..f1963f7 100644 --- a/.env.example +++ b/.env.example @@ -27,3 +27,12 @@ AUTH_ENABLED=false AUTH_SETUP_COMPLETED=false JWT_SECRET= DATABASE_URL="file:/opt/ProxmoxVE-Local/data/settings.db" +AUTO_SYNC_ENABLED=false +SYNC_INTERVAL_TYPE= +SYNC_INTERVAL_PREDEFINED= +AUTO_DOWNLOAD_NEW= +AUTO_UPDATE_EXISTING= +NOTIFICATION_ENABLED= +APPRISE_URLS= +LAST_AUTO_SYNC= +SYNC_INTERVAL_CRON= \ No newline at end of file diff --git a/scripts/json/bunkerweb.json b/scripts/json/bunkerweb.json index 6c0a028..fc2a8f6 100644 --- a/scripts/json/bunkerweb.json +++ b/scripts/json/bunkerweb.json @@ -12,7 +12,7 @@ "documentation": "https://docs.bunkerweb.io/latest/", "website": "https://www.bunkerweb.io/", "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/bunkerweb.webp", - "config_path": "/opt/bunkerweb/variables.env", + "config_path": "/etc/bunkerweb/variables.env", "description": "BunkerWeb is a security-focused web server that enhances web application protection. It guards against common web vulnerabilities like SQL injection, XSS, and CSRF. It features simple setup and configuration using a YAML file, customizable security rules, and provides detailed logs for traffic monitoring and threat detection.", "install_methods": [ { diff --git a/scripts/json/execute.json b/scripts/json/execute.json new file mode 100644 index 0000000..31dac7a --- /dev/null +++ b/scripts/json/execute.json @@ -0,0 +1,48 @@ +{ + "name": "PVE LXC Execute Command", + "slug": "lxc-execute", + "categories": [ + 1 + ], + "date_created": "2025-09-18", + "type": "pve", + "updateable": false, + "privileged": false, + "interface_port": null, + "documentation": null, + "website": null, + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/proxmox.webp", + "config_path": "", + "description": "This script allows administrators to execute a custom command inside one or multiple LXC containers on a Proxmox VE node. Containers can be selectively excluded via an interactive checklist. If a container is stopped, the script will automatically start it, run the command, and then shut it down again. Only Debian and Ubuntu based containers are supported.", + "install_methods": [ + { + "type": "default", + "script": "tools/pve/execute.sh", + "resources": { + "cpu": null, + "ram": null, + "hdd": null, + "os": null, + "version": null + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "Execute within the Proxmox shell.", + "type": "info" + }, + { + "text": "Non-Debian/Ubuntu containers will be skipped automatically.", + "type": "info" + }, + { + "text": "Stopped containers will be started temporarily to run the command, then shut down again.", + "type": "warning" + } + ] +} diff --git a/scripts/json/glpi.json b/scripts/json/glpi.json index 12f1bfc..65b0df0 100644 --- a/scripts/json/glpi.json +++ b/scripts/json/glpi.json @@ -23,7 +23,7 @@ "ram": 2048, "hdd": 10, "os": "debian", - "version": "12" + "version": "13" } } ], diff --git a/scripts/json/guardian.json b/scripts/json/guardian.json index 3cffd31..a4f5751 100644 --- a/scripts/json/guardian.json +++ b/scripts/json/guardian.json @@ -12,7 +12,7 @@ "documentation": "https://github.com/HydroshieldMKII/Guardian/blob/main/README.md", "config_path": "/opt/guardian/.env", "website": "https://github.com/HydroshieldMKII/Guardian", - "logo": null, + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/guardian-plex.webp", "description": "Guardian is a lightweight companion app for Plex that lets you monitor, approve or block devices in real time. It helps you enforce per-user or global policies, stop unwanted sessions automatically and grant temporary access - all through a simple web interface.", "install_methods": [ { diff --git a/scripts/json/jellyfin.json b/scripts/json/jellyfin.json index 4be66ab..9acb705 100644 --- a/scripts/json/jellyfin.json +++ b/scripts/json/jellyfin.json @@ -21,7 +21,7 @@ "resources": { "cpu": 2, "ram": 2048, - "hdd": 8, + "hdd": 16, "os": "ubuntu", "version": "24.04" } diff --git a/scripts/json/jotty.json b/scripts/json/jotty.json new file mode 100644 index 0000000..aa84ed3 --- /dev/null +++ b/scripts/json/jotty.json @@ -0,0 +1,40 @@ +{ + "name": "jotty", + "slug": "jotty", + "categories": [ + 12 + ], + "date_created": "2025-10-21", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": 3000, + "documentation": "https://github.com/fccview/jotty/blob/main/README.md", + "website": "https://github.com/fccview/jotty", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/jotty.webp", + "config_path": "/opt/jotty/.env", + "description": "A simple, self-hosted app for your checklists and notes. Tired of bloated, cloud-based to-do apps? jotty is a lightweight alternative for managing your personal checklists and notes. It's built with Next.js 14, is easy to deploy, and keeps all your data on your own server.", + "install_methods": [ + { + "type": "default", + "script": "ct/jotty.sh", + "resources": { + "cpu": 2, + "ram": 3072, + "hdd": 6, + "os": "debian", + "version": "13" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "jotty was previously named rwMarkable", + "type": "info" + } + ] +} \ No newline at end of file diff --git a/scripts/json/kavita.json b/scripts/json/kavita.json index 587773e..e31251a 100644 --- a/scripts/json/kavita.json +++ b/scripts/json/kavita.json @@ -23,7 +23,7 @@ "ram": 2048, "hdd": 8, "os": "debian", - "version": "12" + "version": "13" } } ], diff --git a/scripts/json/metadata.json b/scripts/json/metadata.json index cc7bda6..4973b8d 100644 --- a/scripts/json/metadata.json +++ b/scripts/json/metadata.json @@ -1,186 +1,186 @@ { - "categories": [ - { - "name": "Proxmox & Virtualization", - "id": 1, - "sort_order": 1.0, - "description": "Tools and scripts to manage Proxmox VE and virtualization platforms effectively.", - "icon": "server" - }, - { - "name": "Operating Systems", - "id": 2, - "sort_order": 2.0, - "description": "Scripts for deploying and managing various operating systems.", - "icon": "monitor" - }, - { - "name": "Containers & Docker", - "id": 3, - "sort_order": 3.0, - "description": "Solutions for containerization using Docker and related technologies.", - "icon": "box" - }, - { - "name": "Network & Firewall", - "id": 4, - "sort_order": 4.0, - "description": "Enhance network security and configure firewalls with ease.", - "icon": "shield" - }, - { - "name": "Adblock & DNS", - "id": 5, - "sort_order": 5.0, - "description": "Optimize your network with DNS and ad-blocking solutions.", - "icon": "ban" - }, - { - "name": "Authentication & Security", - "id": 6, - "sort_order": 6.0, - "description": "Secure your infrastructure with authentication and security tools.", - "icon": "lock" - }, - { - "name": "Backup & Recovery", - "id": 7, - "sort_order": 7.0, - "description": "Reliable backup and recovery scripts to protect your data.", - "icon": "archive" - }, - { - "name": "Databases", - "id": 8, - "sort_order": 8.0, - "description": "Deploy and manage robust database systems with ease.", - "icon": "database" - }, - { - "name": "Monitoring & Analytics", - "id": 9, - "sort_order": 9.0, - "description": "Monitor system performance and analyze data seamlessly.", - "icon": "bar-chart" - }, - { - "name": "Dashboards & Frontends", - "id": 10, - "sort_order": 10.0, - "description": "Create interactive dashboards and user-friendly frontends.", - "icon": "layout" - }, - { - "name": "Files & Downloads", - "id": 11, - "sort_order": 11.0, - "description": "Manage file sharing and downloading solutions efficiently.", - "icon": "download" - }, - { - "name": "Documents & Notes", - "id": 12, - "sort_order": 12.0, - "description": "Organize and manage documents and note-taking tools.", - "icon": "file-text" - }, - { - "name": "Media & Streaming", - "id": 13, - "sort_order": 13.0, - "description": "Stream and manage media effortlessly across devices.", - "icon": "play" - }, - { - "name": "*Arr Suite", - "id": 14, - "sort_order": 14.0, - "description": "Automated media management with the popular *Arr suite tools.", - "icon": "tv" - }, - { - "name": "NVR & Cameras", - "id": 15, - "sort_order": 15.0, - "description": "Manage network video recorders and camera setups.", - "icon": "camera" - }, - { - "name": "IoT & Smart Home", - "id": 16, - "sort_order": 16.0, - "description": "Control and automate IoT devices and smart home systems.", - "icon": "home" - }, - { - "name": "ZigBee, Z-Wave & Matter", - "id": 17, - "sort_order": 17.0, - "description": "Solutions for ZigBee, Z-Wave, and Matter-based device management.", - "icon": "radio" - }, - { - "name": "MQTT & Messaging", - "id": 18, - "sort_order": 18.0, - "description": "Set up reliable messaging and MQTT-based communication systems.", - "icon": "message-circle" - }, - { - "name": "Automation & Scheduling", - "id": 19, - "sort_order": 19.0, - "description": "Automate tasks and manage scheduling with powerful tools.", - "icon": "clock" - }, - { - "name": "AI / Coding & Dev-Tools", - "id": 20, - "sort_order": 20.0, - "description": "Leverage AI and developer tools for smarter coding workflows.", - "icon": "code" - }, - { - "name": "Webservers & Proxies", - "id": 21, - "sort_order": 21.0, - "description": "Deploy and configure web servers and proxy solutions.", - "icon": "globe" - }, - { - "name": "Bots & ChatOps", - "id": 22, - "sort_order": 22.0, - "description": "Enhance collaboration with bots and ChatOps integrations.", - "icon": "bot" - }, - { - "name": "Finance & Budgeting", - "id": 23, - "sort_order": 23.0, - "description": "Track expenses and manage budgets efficiently.", - "icon": "dollar-sign" - }, - { - "name": "Gaming & Leisure", - "id": 24, - "sort_order": 24.0, - "description": "Scripts for gaming servers and leisure-related tools.", - "icon": "gamepad-2" - }, - { - "name": "Business & ERP", - "id": 25, - "sort_order": 25.0, - "description": "Streamline business operations with ERP and management tools.", - "icon": "building" - }, - { - "name": "Miscellaneous", - "id": 0, - "sort_order": 99.0, - "description": "General scripts and tools that don't fit into other categories.", - "icon": "more-horizontal" - } - ] -} \ No newline at end of file + "categories": [ + { + "name": "Proxmox & Virtualization", + "id": 1, + "sort_order": 1.0, + "description": "Tools and scripts to manage Proxmox VE and virtualization platforms effectively.", + "icon": "server" + }, + { + "name": "Operating Systems", + "id": 2, + "sort_order": 2.0, + "description": "Scripts for deploying and managing various operating systems.", + "icon": "monitor" + }, + { + "name": "Containers & Docker", + "id": 3, + "sort_order": 3.0, + "description": "Solutions for containerization using Docker and related technologies.", + "icon": "box" + }, + { + "name": "Network & Firewall", + "id": 4, + "sort_order": 4.0, + "description": "Enhance network security and configure firewalls with ease.", + "icon": "shield" + }, + { + "name": "Adblock & DNS", + "id": 5, + "sort_order": 5.0, + "description": "Optimize your network with DNS and ad-blocking solutions.", + "icon": "ban" + }, + { + "name": "Authentication & Security", + "id": 6, + "sort_order": 6.0, + "description": "Secure your infrastructure with authentication and security tools.", + "icon": "lock" + }, + { + "name": "Backup & Recovery", + "id": 7, + "sort_order": 7.0, + "description": "Reliable backup and recovery scripts to protect your data.", + "icon": "archive" + }, + { + "name": "Databases", + "id": 8, + "sort_order": 8.0, + "description": "Deploy and manage robust database systems with ease.", + "icon": "database" + }, + { + "name": "Monitoring & Analytics", + "id": 9, + "sort_order": 9.0, + "description": "Monitor system performance and analyze data seamlessly.", + "icon": "bar-chart" + }, + { + "name": "Dashboards & Frontends", + "id": 10, + "sort_order": 10.0, + "description": "Create interactive dashboards and user-friendly frontends.", + "icon": "layout" + }, + { + "name": "Files & Downloads", + "id": 11, + "sort_order": 11.0, + "description": "Manage file sharing and downloading solutions efficiently.", + "icon": "download" + }, + { + "name": "Documents & Notes", + "id": 12, + "sort_order": 12.0, + "description": "Organize and manage documents and note-taking tools.", + "icon": "file-text" + }, + { + "name": "Media & Streaming", + "id": 13, + "sort_order": 13.0, + "description": "Stream and manage media effortlessly across devices.", + "icon": "play" + }, + { + "name": "*Arr Suite", + "id": 14, + "sort_order": 14.0, + "description": "Automated media management with the popular *Arr suite tools.", + "icon": "tv" + }, + { + "name": "NVR & Cameras", + "id": 15, + "sort_order": 15.0, + "description": "Manage network video recorders and camera setups.", + "icon": "camera" + }, + { + "name": "IoT & Smart Home", + "id": 16, + "sort_order": 16.0, + "description": "Control and automate IoT devices and smart home systems.", + "icon": "home" + }, + { + "name": "ZigBee, Z-Wave & Matter", + "id": 17, + "sort_order": 17.0, + "description": "Solutions for ZigBee, Z-Wave, and Matter-based device management.", + "icon": "radio" + }, + { + "name": "MQTT & Messaging", + "id": 18, + "sort_order": 18.0, + "description": "Set up reliable messaging and MQTT-based communication systems.", + "icon": "message-circle" + }, + { + "name": "Automation & Scheduling", + "id": 19, + "sort_order": 19.0, + "description": "Automate tasks and manage scheduling with powerful tools.", + "icon": "clock" + }, + { + "name": "AI / Coding & Dev-Tools", + "id": 20, + "sort_order": 20.0, + "description": "Leverage AI and developer tools for smarter coding workflows.", + "icon": "code" + }, + { + "name": "Webservers & Proxies", + "id": 21, + "sort_order": 21.0, + "description": "Deploy and configure web servers and proxy solutions.", + "icon": "globe" + }, + { + "name": "Bots & ChatOps", + "id": 22, + "sort_order": 22.0, + "description": "Enhance collaboration with bots and ChatOps integrations.", + "icon": "bot" + }, + { + "name": "Finance & Budgeting", + "id": 23, + "sort_order": 23.0, + "description": "Track expenses and manage budgets efficiently.", + "icon": "dollar-sign" + }, + { + "name": "Gaming & Leisure", + "id": 24, + "sort_order": 24.0, + "description": "Scripts for gaming servers and leisure-related tools.", + "icon": "gamepad-2" + }, + { + "name": "Business & ERP", + "id": 25, + "sort_order": 25.0, + "description": "Streamline business operations with ERP and management tools.", + "icon": "building" + }, + { + "name": "Miscellaneous", + "id": 0, + "sort_order": 99.0, + "description": "General scripts and tools that don't fit into other categories.", + "icon": "more-horizontal" + } + ] +} diff --git a/scripts/json/mysql.json b/scripts/json/mysql.json index 0c0811c..9f15bab 100644 --- a/scripts/json/mysql.json +++ b/scripts/json/mysql.json @@ -23,7 +23,7 @@ "ram": 1024, "hdd": 4, "os": "debian", - "version": "13" + "version": "12" } } ], diff --git a/scripts/json/nextcloudpi.json b/scripts/json/nextcloudpi.json index 435a71f..cb55487 100644 --- a/scripts/json/nextcloudpi.json +++ b/scripts/json/nextcloudpi.json @@ -23,7 +23,7 @@ "ram": 2048, "hdd": 8, "os": "debian", - "version": "13" + "version": "12" } }, { diff --git a/scripts/json/odoo.json b/scripts/json/odoo.json index 4f9116c..f3ce5eb 100644 --- a/scripts/json/odoo.json +++ b/scripts/json/odoo.json @@ -23,7 +23,7 @@ "ram": 2048, "hdd": 6, "os": "debian", - "version": "13" + "version": "12" } } ], diff --git a/scripts/json/omv.json b/scripts/json/omv.json index 750ec98..5262bae 100644 --- a/scripts/json/omv.json +++ b/scripts/json/omv.json @@ -23,7 +23,7 @@ "ram": 1024, "hdd": 4, "os": "debian", - "version": "13" + "version": "12" } } ], diff --git a/scripts/json/open-archiver.json b/scripts/json/open-archiver.json new file mode 100644 index 0000000..b5a30e1 --- /dev/null +++ b/scripts/json/open-archiver.json @@ -0,0 +1,40 @@ +{ + "name": "Open-Archiver", + "slug": "open-archiver", + "categories": [ + 7 + ], + "date_created": "2025-10-18", + "type": "ct", + "updateable": true, + "privileged": false, + "interface_port": 3000, + "documentation": "https://docs.openarchiver.com/", + "config_path": "/opt/openarchiver/.env", + "website": "https://openarchiver.com/", + "logo": "https://cdn.jsdelivr.net/gh/selfhst/icons/webp/open-archiver.webp", + "description": "Open Archiver is a secure, self-hosted email archiving solution, and it's completely open source. Get an email archiver that enables full-text search across email and attachments. Create a permanent, searchable, and compliant mail archive from Google Workspace, Microsoft 35, and any IMAP server.", + "install_methods": [ + { + "type": "default", + "script": "ct/open-archiver.sh", + "resources": { + "cpu": 2, + "ram": 3072, + "hdd": 8, + "os": "debian", + "version": "13" + } + } + ], + "default_credentials": { + "username": null, + "password": null + }, + "notes": [ + { + "text": "Data directory is: `/opt/openarchiver-data`. If you have a lot of email, you might consider mounting external storage to this directory.", + "type": "info" + } + ] +} \ No newline at end of file diff --git a/scripts/json/openwebui.json b/scripts/json/openwebui.json index 5c0b76d..8035801 100644 --- a/scripts/json/openwebui.json +++ b/scripts/json/openwebui.json @@ -23,7 +23,7 @@ "ram": 8192, "hdd": 25, "os": "debian", - "version": "13" + "version": "12" } } ], diff --git a/scripts/json/paperless-ai.json b/scripts/json/paperless-ai.json index 53d7c01..4dcb356 100644 --- a/scripts/json/paperless-ai.json +++ b/scripts/json/paperless-ai.json @@ -19,8 +19,8 @@ "type": "default", "script": "ct/paperless-ai.sh", "resources": { - "cpu": 2, - "ram": 2048, + "cpu": 4, + "ram": 4096, "hdd": 20, "os": "debian", "version": "13" diff --git a/scripts/json/tautulli.json b/scripts/json/tautulli.json index 23c0136..4759af7 100644 --- a/scripts/json/tautulli.json +++ b/scripts/json/tautulli.json @@ -23,7 +23,7 @@ "ram": 1024, "hdd": 4, "os": "debian", - "version": "13" + "version": "12" } } ], diff --git a/scripts/json/technitiumdns.json b/scripts/json/technitiumdns.json index 4d890a6..9d481b7 100644 --- a/scripts/json/technitiumdns.json +++ b/scripts/json/technitiumdns.json @@ -23,7 +23,7 @@ "ram": 512, "hdd": 2, "os": "debian", - "version": "13" + "version": "12" } } ], diff --git a/scripts/json/undefined.json b/scripts/json/undefined.json index e282cb7..322dc62 100644 --- a/scripts/json/undefined.json +++ b/scripts/json/undefined.json @@ -1,53 +1,58 @@ [ { - "name": "wazuh/wazuh", - "version": "coverity-w42-4.14.0", - "date": "2025-10-16T12:13:42Z" + "name": "sabnzbd/sabnzbd", + "version": "4.5.5", + "date": "2025-10-24T11:12:22Z" }, { - "name": "prometheus/prometheus", - "version": "v0.307.1", - "date": "2025-10-17T08:41:04Z" + "name": "fuma-nama/fumadocs", + "version": "fumadocs-openapi@9.6.3", + "date": "2025-10-24T11:08:15Z" }, { - "name": "nzbgetcom/nzbget", - "version": "v25.4", - "date": "2025-10-09T10:27:01Z" + "name": "crowdsecurity/crowdsec", + "version": "v1.7.2", + "date": "2025-10-21T14:36:48Z" }, { - "name": "zwave-js/zwave-js-ui", - "version": "v11.5.2", - "date": "2025-10-17T08:20:13Z" + "name": "meilisearch/meilisearch", + "version": "prototype-v1.24.0.s3-snapshots-3", + "date": "2025-10-24T09:47:47Z" }, { - "name": "pocketbase/pocketbase", - "version": "v0.30.4", - "date": "2025-10-17T08:03:10Z" + "name": "Luligu/matterbridge", + "version": "3.3.4", + "date": "2025-10-24T06:43:49Z" }, { - "name": "juanfont/headscale", - "version": "v0.26.1", - "date": "2025-06-06T11:22:02Z" + "name": "Jackett/Jackett", + "version": "v0.24.179", + "date": "2025-10-24T05:54:44Z" }, { - "name": "Jackett/Jackett", - "version": "v0.24.145", - "date": "2025-10-17T05:55:22Z" + "name": "inventree/InvenTree", + "version": "1.0.8", + "date": "2025-10-24T05:32:09Z" }, { - "name": "booklore-app/booklore", - "version": "v1.7.0", - "date": "2025-10-17T05:51:41Z" + "name": "pocketbase/pocketbase", + "version": "v0.31.0", + "date": "2025-10-24T04:07:27Z" }, { - "name": "diced/zipline", - "version": "v4.3.2", - "date": "2025-10-17T04:13:23Z" + "name": "esphome/esphome", + "version": "2025.10.3", + "date": "2025-10-24T01:08:22Z" + }, + { + "name": "OliveTin/OliveTin", + "version": "3000.0.1", + "date": "2025-10-24T00:31:50Z" }, { "name": "jeedom/core", "version": "4.4.20", - "date": "2025-10-17T00:27:08Z" + "date": "2025-10-24T00:27:06Z" }, { "name": "steveiliop56/tinyauth", @@ -55,69 +60,159 @@ "date": "2025-10-15T16:53:55Z" }, { - "name": "chrisbenincasa/tunarr", - "version": "v0.22.8", - "date": "2025-10-16T23:57:39Z" + "name": "documenso/documenso", + "version": "v1.13.1", + "date": "2025-10-23T23:52:45Z" }, { - "name": "Stirling-Tools/Stirling-PDF", - "version": "v1.5.0", - "date": "2025-10-16T23:14:45Z" + "name": "mongodb/mongo", + "version": "r8.2.2-rc0", + "date": "2025-10-23T22:12:44Z" }, { - "name": "openhab/openhab-core", - "version": "5.0.2", - "date": "2025-10-16T21:27:35Z" + "name": "Cleanuparr/Cleanuparr", + "version": "v2.4.0", + "date": "2025-10-23T21:12:48Z" }, { - "name": "ollama/ollama", - "version": "v0.12.6-rc1", - "date": "2025-10-16T16:36:25Z" + "name": "wizarrrr/wizarr", + "version": "v2025.10.6", + "date": "2025-10-23T20:20:21Z" }, { - "name": "runtipi/runtipi", - "version": "v4.5.0", - "date": "2025-10-16T19:43:25Z" + "name": "tailscale/tailscale", + "version": "v1.90.1", + "date": "2025-10-23T19:12:48Z" }, { - "name": "minio/minio", - "version": "RELEASE.2025-10-15T17-29-55Z", - "date": "2025-10-16T19:33:51Z" + "name": "dgtlmoon/changedetection.io", + "version": "0.50.29", + "date": "2025-10-23T18:59:35Z" + }, + { + "name": "wazuh/wazuh", + "version": "v4.14.0", + "date": "2025-10-23T17:45:30Z" + }, + { + "name": "paperless-ngx/paperless-ngx", + "version": "v2.19.2", + "date": "2025-10-23T17:23:27Z" + }, + { + "name": "drakkan/sftpgo", + "version": "v2.7.0", + "date": "2025-10-23T17:15:07Z" + }, + { + "name": "zitadel/zitadel", + "version": "v4.4.0", + "date": "2025-10-16T07:20:34Z" + }, + { + "name": "msgbyte/tianji", + "version": "v1.30.1", + "date": "2025-10-23T16:20:50Z" + }, + { + "name": "TwiN/gatus", + "version": "v5.28.0", + "date": "2025-10-23T16:16:04Z" + }, + { + "name": "juanfont/headscale", + "version": "v0.26.1", + "date": "2025-06-06T11:22:02Z" + }, + { + "name": "HabitRPG/habitica", + "version": "v5.41.5", + "date": "2025-10-23T15:12:42Z" }, { "name": "keycloak/keycloak", - "version": "26.4.1", - "date": "2025-10-16T07:21:53Z" + "version": "26.4.2", + "date": "2025-10-23T06:59:32Z" }, { - "name": "SigNoz/signoz", - "version": "v0.98.0-rc.1", - "date": "2025-10-16T17:49:50Z" + "name": "AdguardTeam/AdGuardHome", + "version": "v0.107.68", + "date": "2025-10-23T14:26:29Z" }, { - "name": "open-webui/open-webui", - "version": "v0.6.34", - "date": "2025-10-16T16:55:58Z" + "name": "Kareadita/Kavita", + "version": "v0.8.8.3", + "date": "2025-10-23T12:31:49Z" }, { - "name": "dgtlmoon/changedetection.io", - "version": "0.50.25", - "date": "2025-10-16T15:00:16Z" + "name": "YunoHost/yunohost", + "version": "debian/12.1.32", + "date": "2025-10-23T12:30:33Z" }, { - "name": "tobychui/zoraxy", - "version": "v3.2.8", - "date": "2025-10-16T14:21:48Z" + "name": "YunoHost/yunohost", + "version": "debian/12.1.32", + "date": "2025-10-23T12:30:33Z" }, { - "name": "forgejo/forgejo", - "version": "v13.0.0", - "date": "2025-10-16T13:48:56Z" + "name": "duplicati/duplicati", + "version": "v2.2.0.0_stable_2025-10-23", + "date": "2025-10-23T11:48:25Z" }, { - "name": "esphome/esphome", - "version": "2025.10.1", - "date": "2025-10-16T10:00:10Z" + "name": "BerriAI/litellm", + "version": "v1.78.7-nightly", + "date": "2025-10-22T22:33:31Z" + }, + { + "name": "jhuckaby/Cronicle", + "version": "v0.9.99", + "date": "2025-10-22T22:20:33Z" + }, + { + "name": "moghtech/komodo", + "version": "v1.19.5", + "date": "2025-09-27T20:59:46Z" + }, + { + "name": "rcourtman/Pulse", + "version": "issue-596", + "date": "2025-10-22T19:48:56Z" + }, + { + "name": "gtsteffaniak/filebrowser", + "version": "v0.8.10-beta", + "date": "2025-10-22T18:41:54Z" + }, + { + "name": "docker/compose", + "version": "v2.40.2", + "date": "2025-10-22T17:38:12Z" + }, + { + "name": "louislam/uptime-kuma", + "version": "2.0.2", + "date": "2025-10-22T17:03:54Z" + }, + { + "name": "chrisvel/tududi", + "version": "v0.84.2", + "date": "2025-10-22T17:03:48Z" + }, + { + "name": "prometheus/prometheus", + "version": "v0.307.2", + "date": "2025-10-22T16:00:08Z" + }, + { + "name": "thecfu/scraparr", + "version": "v3.0.0-beta", + "date": "2025-10-22T13:12:10Z" + }, + { + "name": "NLnetLabs/unbound", + "version": "release-1.24.1", + "date": "2025-10-22T10:28:00Z" }, { "name": "mattermost/mattermost", @@ -125,45 +220,305 @@ "date": "2025-10-16T09:46:16Z" }, { - "name": "rcourtman/Pulse", - "version": "v4.24.0", - "date": "2025-10-16T09:08:47Z" + "name": "zabbix/zabbix", + "version": "7.4.4rc1", + "date": "2025-10-22T09:20:59Z" + }, + { + "name": "SigNoz/signoz", + "version": "v0.98.0", + "date": "2025-10-22T06:49:36Z" + }, + { + "name": "morpheus65535/bazarr", + "version": "v1.5.3", + "date": "2025-09-20T12:12:33Z" + }, + { + "name": "chrisbenincasa/tunarr", + "version": "v0.23.0-alpha.17", + "date": "2025-10-22T00:33:47Z" + }, + { + "name": "bluenviron/mediamtx", + "version": "v1.15.3", + "date": "2025-10-21T19:56:55Z" + }, + { + "name": "jenkinsci/jenkins", + "version": "jenkins-2.533", + "date": "2025-10-21T18:26:10Z" + }, + { + "name": "kyantech/Palmr", + "version": "v3.2.5-beta", + "date": "2025-10-21T16:49:14Z" + }, + { + "name": "element-hq/synapse", + "version": "v1.140.0", + "date": "2025-10-14T15:57:12Z" + }, + { + "name": "grafana/grafana", + "version": "v12.2.1", + "date": "2025-10-21T14:40:19Z" + }, + { + "name": "laurent22/joplin", + "version": "server-v3.4.4", + "date": "2025-09-25T13:19:26Z" + }, + { + "name": "goauthentik/authentik", + "version": "version/2025.10.0-rc2", + "date": "2025-10-21T00:19:36Z" + }, + { + "name": "AlexxIT/go2rtc", + "version": "v1.9.11", + "date": "2025-10-21T13:21:02Z" + }, + { + "name": "influxdata/telegraf", + "version": "v1.36.3", + "date": "2025-10-21T12:53:58Z" + }, + { + "name": "apache/cassandra", + "version": "5.0.6-tentative", + "date": "2025-10-21T11:42:35Z" + }, + { + "name": "evcc-io/evcc", + "version": "0.209.3", + "date": "2025-10-21T10:53:07Z" + }, + { + "name": "VictoriaMetrics/VictoriaMetrics", + "version": "pmm-6401-v1.128.0", + "date": "2025-10-21T08:30:52Z" + }, + { + "name": "openobserve/openobserve", + "version": "v0.16.0-rc1", + "date": "2025-10-21T00:37:47Z" + }, + { + "name": "henrygd/beszel", + "version": "v0.14.1", + "date": "2025-10-20T22:10:56Z" + }, + { + "name": "booklore-app/booklore", + "version": "v1.8.1", + "date": "2025-10-20T20:53:56Z" + }, + { + "name": "coder/code-server", + "version": "v4.105.1", + "date": "2025-10-20T20:19:23Z" + }, + { + "name": "pymedusa/Medusa", + "version": "v1.0.23", + "date": "2025-10-20T19:51:33Z" + }, + { + "name": "MediaBrowser/Emby.Releases", + "version": "4.9.1.80", + "date": "2025-09-30T20:25:16Z" + }, + { + "name": "rclone/rclone", + "version": "v1.71.2", + "date": "2025-10-20T15:25:52Z" + }, + { + "name": "Graylog2/graylog2-server", + "version": "7.0.0-rc.1", + "date": "2025-10-20T11:53:31Z" + }, + { + "name": "jupyter/notebook", + "version": "@jupyter-notebook/ui-components@7.5.0-beta.1", + "date": "2025-10-20T07:01:38Z" + }, + { + "name": "firefly-iii/firefly-iii", + "version": "v6.4.2", + "date": "2025-10-07T08:11:58Z" + }, + { + "name": "jellyfin/jellyfin", + "version": "v10.11.0", + "date": "2025-10-20T00:45:19Z" + }, + { + "name": "pelican-dev/panel", + "version": "v1.0.0-beta27", + "date": "2025-10-20T00:38:13Z" + }, + { + "name": "seriousm4x/UpSnap", + "version": "5.2.3", + "date": "2025-10-19T20:50:15Z" + }, + { + "name": "verdaccio/verdaccio", + "version": "generator-verdaccio-plugin@6.0.0-next-8.24", + "date": "2025-10-19T19:43:46Z" + }, + { + "name": "Part-DB/Part-DB-server", + "version": "v2.2.1", + "date": "2025-10-19T14:30:11Z" + }, + { + "name": "benzino77/tasmocompiler", + "version": "v13.0.0", + "date": "2025-10-19T10:03:18Z" + }, + { + "name": "Prowlarr/Prowlarr", + "version": "v2.0.5.5160", + "date": "2025-08-23T21:23:11Z" + }, + { + "name": "Lidarr/Lidarr", + "version": "v2.14.5.4836", + "date": "2025-10-08T15:30:50Z" + }, + { + "name": "ellite/Wallos", + "version": "v4.5.0", + "date": "2025-10-18T22:00:50Z" + }, + { + "name": "leiweibau/Pi.Alert", + "version": "v2025-10-18", + "date": "2025-10-18T20:35:54Z" + }, + { + "name": "project-zot/zot", + "version": "v2.1.10", + "date": "2025-10-18T18:46:36Z" + }, + { + "name": "homarr-labs/homarr", + "version": "v1.42.1", + "date": "2025-10-18T18:31:38Z" + }, + { + "name": "Notifiarr/notifiarr", + "version": "v0.9.0", + "date": "2025-10-18T17:03:56Z" + }, + { + "name": "TasmoAdmin/TasmoAdmin", + "version": "v4.3.2", + "date": "2025-10-18T12:11:00Z" + }, + { + "name": "readeck/readeck", + "version": "0.20.4", + "date": "2025-10-18T10:00:42Z" + }, + { + "name": "theonedev/onedev", + "version": "v13.0.9", + "date": "2025-10-18T09:59:25Z" + }, + { + "name": "runtipi/runtipi", + "version": "v4.5.1", + "date": "2025-10-18T08:12:19Z" + }, + { + "name": "nzbgetcom/nzbget", + "version": "v25.4", + "date": "2025-10-09T10:27:01Z" + }, + { + "name": "9001/copyparty", + "version": "v1.19.17", + "date": "2025-10-17T23:40:02Z" + }, + { + "name": "home-assistant/core", + "version": "2025.10.3", + "date": "2025-10-17T21:15:07Z" + }, + { + "name": "forgejo/forgejo", + "version": "v13.0.1", + "date": "2025-10-17T18:54:16Z" + }, + { + "name": "grokability/snipe-it", + "version": "v8.3.4", + "date": "2025-10-17T18:13:24Z" + }, + { + "name": "NodeBB/NodeBB", + "version": "v4.6.1", + "date": "2025-10-17T15:21:59Z" + }, + { + "name": "neo4j/neo4j", + "version": "5.26.14", + "date": "2025-10-17T12:38:22Z" + }, + { + "name": "zwave-js/zwave-js-ui", + "version": "v11.5.2", + "date": "2025-10-17T08:20:13Z" + }, + { + "name": "diced/zipline", + "version": "v4.3.2", + "date": "2025-10-17T04:13:23Z" + }, + { + "name": "Stirling-Tools/Stirling-PDF", + "version": "v1.5.0", + "date": "2025-10-16T23:14:45Z" + }, + { + "name": "openhab/openhab-core", + "version": "5.0.2", + "date": "2025-10-16T21:27:35Z" + }, + { + "name": "ollama/ollama", + "version": "v0.12.6-rc1", + "date": "2025-10-16T16:36:25Z" + }, + { + "name": "minio/minio", + "version": "RELEASE.2025-10-15T17-29-55Z", + "date": "2025-10-16T19:33:51Z" }, { - "name": "zitadel/zitadel", - "version": "v4.4.0", - "date": "2025-10-16T07:20:34Z" + "name": "open-webui/open-webui", + "version": "v0.6.34", + "date": "2025-10-16T16:55:58Z" }, { - "name": "morpheus65535/bazarr", - "version": "v1.5.3", - "date": "2025-09-20T12:12:33Z" + "name": "tobychui/zoraxy", + "version": "v3.2.8", + "date": "2025-10-16T14:21:48Z" }, { "name": "cloudreve/cloudreve", "version": "4.9.2", "date": "2025-10-16T03:24:44Z" }, - { - "name": "BerriAI/litellm", - "version": "v1.78.2-nightly", - "date": "2025-10-16T01:47:06Z" - }, { "name": "outline/outline", "version": "v1.0.0-test8", "date": "2025-10-16T01:32:14Z" }, - { - "name": "coder/code-server", - "version": "v4.104.3", - "date": "2025-10-07T17:42:11Z" - }, - { - "name": "inventree/InvenTree", - "version": "1.0.6", - "date": "2025-10-15T22:10:17Z" - }, { "name": "Ombi-app/Ombi", "version": "v4.47.1", @@ -179,46 +534,21 @@ "version": "v2.1.0", "date": "2025-10-15T19:24:14Z" }, - { - "name": "laurent22/joplin", - "version": "server-v3.4.4", - "date": "2025-09-25T13:19:26Z" - }, { "name": "karlomikus/bar-assistant", "version": "v5.9.0", "date": "2025-10-15T18:27:56Z" }, - { - "name": "msgbyte/tianji", - "version": "v1.28.0", - "date": "2025-10-15T16:18:36Z" - }, { "name": "linkwarden/linkwarden", "version": "v2.13.1", "date": "2025-10-15T13:29:37Z" }, - { - "name": "meilisearch/meilisearch", - "version": "prototype-docker-alpine-3-22-v8", - "date": "2025-10-15T13:20:20Z" - }, { "name": "TandoorRecipes/recipes", "version": "2.3.3", "date": "2025-10-15T13:18:27Z" }, - { - "name": "jenkinsci/jenkins", - "version": "jenkins-2.528.1", - "date": "2025-10-15T12:51:20Z" - }, - { - "name": "Graylog2/graylog2-server", - "version": "7.0.0-beta.5", - "date": "2025-10-15T11:43:16Z" - }, { "name": "cockpit-project/cockpit", "version": "349", @@ -229,56 +559,26 @@ "version": "v0.14.1", "date": "2024-08-29T22:32:51Z" }, - { - "name": "crowdsecurity/crowdsec", - "version": "v1.7.1", - "date": "2025-10-15T10:44:03Z" - }, - { - "name": "openobserve/openobserve", - "version": "v0.15.2", - "date": "2025-10-15T07:42:29Z" - }, { "name": "wavelog/wavelog", "version": "2.1.2", "date": "2025-10-15T06:51:32Z" }, - { - "name": "MediaBrowser/Emby.Releases", - "version": "4.9.1.80", - "date": "2025-09-30T20:25:16Z" - }, { "name": "seerr-team/seerr", "version": "preview-seerr", "date": "2025-10-14T22:21:33Z" }, - { - "name": "apache/cassandra", - "version": "5.0.6-tentative", - "date": "2025-10-14T20:36:34Z" - }, { "name": "cloudflare/cloudflared", "version": "2025.10.0", "date": "2025-10-14T19:07:37Z" }, - { - "name": "evcc-io/evcc", - "version": "0.209.2", - "date": "2025-10-14T18:55:44Z" - }, { "name": "crafty-controller/crafty-4", "version": "v4.5.5", "date": "2025-10-14T18:48:36Z" }, - { - "name": "tailscale/tailscale", - "version": "v1.88.4", - "date": "2025-10-14T17:57:52Z" - }, { "name": "plankanban/planka", "version": "planka-1.1.0", @@ -289,56 +589,16 @@ "version": "16.2", "date": "2025-09-08T14:03:25Z" }, - { - "name": "project-zot/zot", - "version": "v2.1.9", - "date": "2025-10-14T16:18:49Z" - }, - { - "name": "element-hq/synapse", - "version": "v1.140.0", - "date": "2025-10-14T15:57:12Z" - }, - { - "name": "n8n-io/n8n", - "version": "n8n@1.115.3", - "date": "2025-10-14T14:40:17Z" - }, - { - "name": "Prowlarr/Prowlarr", - "version": "v2.0.5.5160", - "date": "2025-08-23T21:23:11Z" - }, { "name": "rogerfar/rdt-client", "version": "v2.0.119", "date": "2025-10-13T23:15:11Z" }, - { - "name": "jellyfin/jellyfin", - "version": "v10.10.7", - "date": "2025-04-05T19:14:59Z" - }, { "name": "hargata/lubelog", "version": "v1.5.3", "date": "2025-10-13T19:59:30Z" }, - { - "name": "bluenviron/mediamtx", - "version": "v1.15.2", - "date": "2025-10-13T17:03:15Z" - }, - { - "name": "jhuckaby/Cronicle", - "version": "v0.9.97", - "date": "2025-10-13T16:13:50Z" - }, - { - "name": "sabnzbd/sabnzbd", - "version": "4.5.3", - "date": "2025-08-25T13:59:56Z" - }, { "name": "node-red/node-red", "version": "4.1.1", @@ -349,36 +609,11 @@ "version": "v0.29.0-rc.0", "date": "2025-10-10T01:13:27Z" }, - { - "name": "moghtech/komodo", - "version": "v1.19.5", - "date": "2025-09-27T20:59:46Z" - }, - { - "name": "firefly-iii/firefly-iii", - "version": "v6.4.2", - "date": "2025-10-07T08:11:58Z" - }, - { - "name": "Luligu/matterbridge", - "version": "3.3.2", - "date": "2025-10-12T21:30:43Z" - }, { "name": "globaleaks/globaleaks-whistleblowing-software", "version": "v5.0.85", "date": "2025-10-12T19:55:18Z" }, - { - "name": "ellite/Wallos", - "version": "v4.4.1", - "date": "2025-10-12T15:38:24Z" - }, - { - "name": "YunoHost/yunohost", - "version": "debian/12.1.28", - "date": "2025-10-12T14:55:27Z" - }, { "name": "PrivateBin/PrivateBin", "version": "2.0.1", @@ -394,26 +629,11 @@ "version": "v0.10.4", "date": "2025-10-11T19:53:39Z" }, - { - "name": "duplicati/duplicati", - "version": "v2.1.2.3-2.1.2.3_beta_2025-10-11", - "date": "2025-10-11T06:49:43Z" - }, - { - "name": "home-assistant/core", - "version": "2025.10.2", - "date": "2025-10-10T21:20:11Z" - }, { "name": "0xERR0R/blocky", "version": "v0.27.0", "date": "2025-10-10T20:11:48Z" }, - { - "name": "homarr-labs/homarr", - "version": "v1.41.0", - "date": "2025-10-10T19:15:38Z" - }, { "name": "getumbrel/umbrel", "version": "1.4.2", @@ -464,11 +684,6 @@ "version": "v3.4.2", "date": "2025-10-09T19:05:48Z" }, - { - "name": "henrygd/beszel", - "version": "v0.13.2", - "date": "2025-10-09T18:33:46Z" - }, { "name": "ErsatzTV/ErsatzTV", "version": "v25.7.1", @@ -484,16 +699,6 @@ "version": "11.0.1", "date": "2025-10-09T12:34:15Z" }, - { - "name": "theonedev/onedev", - "version": "v13.0.8", - "date": "2025-10-09T07:13:00Z" - }, - { - "name": "documenso/documenso", - "version": "v1.12.10", - "date": "2025-10-09T04:32:35Z" - }, { "name": "rabbitmq/rabbitmq-server", "version": "v4.1.4", @@ -504,21 +709,11 @@ "version": "v0.38.1", "date": "2025-10-08T21:34:07Z" }, - { - "name": "pelican-dev/panel", - "version": "v1.0.0-beta26", - "date": "2025-10-08T20:12:11Z" - }, { "name": "autobrr/autobrr", "version": "v1.68.0", "date": "2025-10-08T18:33:12Z" }, - { - "name": "AdguardTeam/AdGuardHome", - "version": "v0.107.67", - "date": "2025-09-29T14:45:57Z" - }, { "name": "advplyr/audiobookshelf", "version": "v2.30.0", @@ -529,16 +724,6 @@ "version": "v4.3.3", "date": "2025-10-08T15:45:21Z" }, - { - "name": "Lidarr/Lidarr", - "version": "v2.14.5.4836", - "date": "2025-10-08T15:30:50Z" - }, - { - "name": "wizarrrr/wizarr", - "version": "v2025.10.3", - "date": "2025-10-08T15:29:15Z" - }, { "name": "FlowiseAI/Flowise", "version": "flowise@3.0.8", @@ -559,21 +744,6 @@ "version": "v0.15.1", "date": "2025-10-07T20:30:56Z" }, - { - "name": "VictoriaMetrics/VictoriaMetrics", - "version": "pmm-6401-v1.127.0", - "date": "2025-10-07T14:31:32Z" - }, - { - "name": "chrisvel/tududi", - "version": "v0.83.2", - "date": "2025-10-07T14:30:15Z" - }, - { - "name": "thecfu/scraparr", - "version": "v2.2.5", - "date": "2025-10-07T12:34:31Z" - }, { "name": "pocket-id/pocket-id", "version": "v1.13.1", @@ -594,26 +764,16 @@ "version": "v2.2.2", "date": "2025-10-06T21:31:07Z" }, - { - "name": "grokability/snipe-it", - "version": "v8.3.3", - "date": "2025-10-06T19:57:17Z" - }, - { - "name": "TwiN/gatus", - "version": "v5.26.0", - "date": "2025-10-06T17:57:27Z" - }, - { - "name": "fuma-nama/fumadocs", - "version": "fumadocs-ui@15.8.4", - "date": "2025-10-06T15:41:49Z" - }, { "name": "bunkerity/bunkerweb", "version": "v1.6.5", "date": "2025-10-06T15:25:17Z" }, + { + "name": "mysql/mysql-server", + "version": "mysql-cluster-7.6.36", + "date": "2025-10-06T15:19:49Z" + }, { "name": "bastienwirtz/homer", "version": "v25.10.1", @@ -629,21 +789,11 @@ "version": "2.1.1", "date": "2025-06-14T17:45:06Z" }, - { - "name": "9001/copyparty", - "version": "v1.19.16", - "date": "2025-10-05T23:28:59Z" - }, { "name": "BookStackApp/BookStack", "version": "v25.07.3", "date": "2025-10-05T14:47:20Z" }, - { - "name": "seriousm4x/UpSnap", - "version": "5.2.2", - "date": "2025-10-05T09:12:17Z" - }, { "name": "pommee/goaway", "version": "v0.62.11", @@ -654,36 +804,16 @@ "version": "2.520", "date": "2025-10-05T00:51:34Z" }, - { - "name": "gtsteffaniak/filebrowser", - "version": "v0.8.8-beta", - "date": "2025-10-04T15:56:29Z" - }, - { - "name": "docker/compose", - "version": "v2.40.0", - "date": "2025-10-03T12:56:38Z" - }, { "name": "redis/redis", "version": "8.2.2", "date": "2025-10-03T06:22:38Z" }, - { - "name": "kyantech/Palmr", - "version": "v3.2.3-beta", - "date": "2025-10-02T13:48:14Z" - }, { "name": "actualbudget/actual", "version": "v25.10.0", "date": "2025-10-02T11:34:39Z" }, - { - "name": "NodeBB/NodeBB", - "version": "v4.6.0", - "date": "2025-10-01T18:12:07Z" - }, { "name": "Koenkk/zigbee2mqtt", "version": "2.6.2", @@ -694,21 +824,6 @@ "version": "v3.5.7.pypi", "date": "2025-10-01T05:32:27Z" }, - { - "name": "HabitRPG/habitica", - "version": "v5.41.4", - "date": "2025-09-30T22:26:11Z" - }, - { - "name": "zabbix/zabbix", - "version": "7.4.3", - "date": "2025-09-30T21:49:53Z" - }, - { - "name": "mongodb/mongo", - "version": "r8.2.1", - "date": "2025-09-30T21:46:28Z" - }, { "name": "WordPress/WordPress", "version": "4.7.31", @@ -724,36 +839,11 @@ "version": "v1.7.4", "date": "2025-09-30T13:34:30Z" }, - { - "name": "neo4j/neo4j", - "version": "4.4.46", - "date": "2025-09-30T13:21:24Z" - }, { "name": "thomiceli/opengist", "version": "v1.11.1", "date": "2025-09-30T00:24:16Z" }, - { - "name": "goauthentik/authentik", - "version": "version/2025.8.4", - "date": "2025-09-30T00:03:11Z" - }, - { - "name": "verdaccio/verdaccio", - "version": "v6.2.0", - "date": "2025-09-29T20:59:23Z" - }, - { - "name": "influxdata/telegraf", - "version": "v1.36.2", - "date": "2025-09-29T19:16:49Z" - }, - { - "name": "Cleanuparr/Cleanuparr", - "version": "v2.3.3", - "date": "2025-09-29T18:53:35Z" - }, { "name": "influxdata/influxdb", "version": "v2.7.12", @@ -764,11 +854,6 @@ "version": "1.0.0", "date": "2025-09-29T13:53:50Z" }, - { - "name": "jupyter/notebook", - "version": "@jupyter-notebook/ui-components@7.5.0-beta.0", - "date": "2025-09-29T09:16:42Z" - }, { "name": "lazy-media/Reactive-Resume", "version": "v1.2.6", @@ -819,36 +904,16 @@ "version": "1.2.39", "date": "2025-09-25T15:57:02Z" }, - { - "name": "rclone/rclone", - "version": "v1.71.1", - "date": "2025-09-24T16:32:16Z" - }, { "name": "alexta69/metube", "version": "2025.09.24", "date": "2025-09-24T13:51:23Z" }, - { - "name": "AlexxIT/go2rtc", - "version": "v1.9.10", - "date": "2025-09-24T13:49:53Z" - }, { "name": "syncthing/syncthing", "version": "v2.0.10", "date": "2025-09-24T08:33:37Z" }, - { - "name": "grafana/grafana", - "version": "v12.2.0", - "date": "2025-09-23T23:47:02Z" - }, - { - "name": "Part-DB/Part-DB-server", - "version": "v2.2.0", - "date": "2025-09-23T21:46:21Z" - }, { "name": "postgres/postgres", "version": "REL_18_0", @@ -894,16 +959,6 @@ "version": "v0.23.2", "date": "2025-09-18T17:18:59Z" }, - { - "name": "NLnetLabs/unbound", - "version": "release-1.24.0", - "date": "2025-09-18T08:36:55Z" - }, - { - "name": "TasmoAdmin/TasmoAdmin", - "version": "v4.3.1", - "date": "2025-07-22T20:10:08Z" - }, { "name": "eclipse-mosquitto/mosquitto", "version": "2.1.0-test1", @@ -929,11 +984,6 @@ "version": "v2.4.0p12", "date": "2025-09-16T12:53:03Z" }, - { - "name": "readeck/readeck", - "version": "0.20.3", - "date": "2025-09-16T07:29:49Z" - }, { "name": "Paymenter/Paymenter", "version": "v1.3.4", @@ -989,21 +1039,11 @@ "version": "v2.16.0", "date": "2025-09-09T01:05:45Z" }, - { - "name": "paperless-ngx/paperless-ngx", - "version": "v2.18.4", - "date": "2025-09-07T23:57:32Z" - }, { "name": "CrazyWolf13/streamlink-webui", "version": "0.6", "date": "2025-09-05T06:05:04Z" }, - { - "name": "louislam/uptime-kuma", - "version": "2.0.0-beta.2-temp", - "date": "2025-03-28T08:45:58Z" - }, { "name": "healthchecks/healthchecks", "version": "v3.11.2", @@ -1139,11 +1179,6 @@ "version": "v2.1.1867", "date": "2025-07-31T18:08:43Z" }, - { - "name": "leiweibau/Pi.Alert", - "version": "v2025-07-30", - "date": "2025-07-30T17:13:40Z" - }, { "name": "danielbrendel/hortusfox-web", "version": "v5.2", @@ -1154,11 +1189,6 @@ "version": "1.34.3", "date": "2025-07-30T09:10:59Z" }, - { - "name": "OliveTin/OliveTin", - "version": "2025.7.29", - "date": "2025-07-29T22:20:13Z" - }, { "name": "caddyserver/xcaddy", "version": "v0.4.5", @@ -1204,21 +1234,11 @@ "version": "v2.12.6", "date": "2025-07-09T21:52:15Z" }, - { - "name": "mysql/mysql-server", - "version": "mysql-cluster-9.4.0", - "date": "2025-07-09T08:35:30Z" - }, { "name": "photoprism/photoprism", "version": "250707-d28b3101e", "date": "2025-07-07T15:15:21Z" }, - { - "name": "Kareadita/Kavita", - "version": "v0.8.7", - "date": "2025-07-05T20:08:58Z" - }, { "name": "qbittorrent/qBittorrent", "version": "release-5.1.2", @@ -1254,11 +1274,6 @@ "version": "v4.0.15.2941", "date": "2025-06-20T17:20:54Z" }, - { - "name": "benzino77/tasmocompiler", - "version": "v12.7.0", - "date": "2025-06-20T08:31:16Z" - }, { "name": "prometheus-pve/prometheus-pve-exporter", "version": "v3.5.5", @@ -1434,11 +1449,6 @@ "version": "v1.11.2", "date": "2025-02-24T19:47:06Z" }, - { - "name": "drakkan/sftpgo", - "version": "v2.6.6", - "date": "2025-02-24T19:14:46Z" - }, { "name": "babybuddy/babybuddy", "version": "v2.7.1", @@ -1474,11 +1484,6 @@ "version": "v0.7.3", "date": "2024-12-15T10:18:06Z" }, - { - "name": "pymedusa/Medusa", - "version": "v1.0.22", - "date": "2024-12-13T12:22:19Z" - }, { "name": "phpipam/phpipam", "version": "v1.7.3", @@ -1499,11 +1504,6 @@ "version": "v2024.10.22-7ca5933", "date": "2024-10-22T09:58:03Z" }, - { - "name": "Notifiarr/notifiarr", - "version": "v0.8.3", - "date": "2024-10-04T23:49:07Z" - }, { "name": "FunkeyFlo/ps5-mqtt", "version": "v1.4.0", diff --git a/scripts/json/versions.json b/scripts/json/versions.json index 02db68c..ac0106f 100644 --- a/scripts/json/versions.json +++ b/scripts/json/versions.json @@ -1,273 +1,368 @@ [ { - "name": "YunoHost/yunohost", - "version": "debian/12.1.21", - "date": "2025-09-10T10:57:57Z" + "name": "mongodb/mongo", + "version": "r8.2.2-rc0", + "date": "2025-10-23T22:12:44Z" }, { - "name": "open-webui/open-webui", - "version": "v0.6.28", - "date": "2025-09-10T10:53:42Z" + "name": "Cleanuparr/Cleanuparr", + "version": "v2.4.0", + "date": "2025-10-23T21:12:48Z" }, { "name": "wizarrrr/wizarr", - "version": "v2025.9.1", - "date": "2025-09-10T10:47:26Z" + "version": "v2025.10.6", + "date": "2025-10-23T20:20:21Z" }, { - "name": "mattermost/mattermost", - "version": "v10.5.11", - "date": "2025-09-10T07:28:51Z" + "name": "tailscale/tailscale", + "version": "v1.90.1", + "date": "2025-10-23T19:12:48Z" }, { - "name": "cockpit-project/cockpit", - "version": "345.1", - "date": "2025-09-10T07:10:55Z" + "name": "dgtlmoon/changedetection.io", + "version": "0.50.29", + "date": "2025-10-23T18:59:35Z" + }, + { + "name": "wazuh/wazuh", + "version": "v4.14.0", + "date": "2025-10-23T17:45:30Z" + }, + { + "name": "paperless-ngx/paperless-ngx", + "version": "v2.19.2", + "date": "2025-10-23T17:23:27Z" + }, + { + "name": "drakkan/sftpgo", + "version": "v2.7.0", + "date": "2025-10-23T17:15:07Z" }, { "name": "zitadel/zitadel", - "version": "v4.1.3", - "date": "2025-09-08T13:36:08Z" + "version": "v4.4.0", + "date": "2025-10-16T07:20:34Z" }, { - "name": "Jackett/Jackett", - "version": "v0.22.2451", - "date": "2025-09-10T05:43:17Z" + "name": "msgbyte/tianji", + "version": "v1.30.1", + "date": "2025-10-23T16:20:50Z" }, { - "name": "esphome/esphome", - "version": "2025.8.4", - "date": "2025-09-10T05:03:47Z" + "name": "TwiN/gatus", + "version": "v5.28.0", + "date": "2025-10-23T16:16:04Z" }, { - "name": "firefly-iii/firefly-iii", - "version": "v6.3.2", - "date": "2025-08-19T04:08:36Z" + "name": "juanfont/headscale", + "version": "v0.26.1", + "date": "2025-06-06T11:22:02Z" + }, + { + "name": "HabitRPG/habitica", + "version": "v5.41.5", + "date": "2025-10-23T15:12:42Z" + }, + { + "name": "AdguardTeam/AdGuardHome", + "version": "v0.107.68", + "date": "2025-10-23T14:26:29Z" + }, + { + "name": "fuma-nama/fumadocs", + "version": "fumadocs-core@16.0.2", + "date": "2025-10-23T13:59:04Z" + }, + { + "name": "Kareadita/Kavita", + "version": "v0.8.8.3", + "date": "2025-10-23T12:31:49Z" + }, + { + "name": "YunoHost/yunohost", + "version": "debian/12.1.32", + "date": "2025-10-23T12:30:33Z" + }, + { + "name": "duplicati/duplicati", + "version": "v2.2.0.0_stable_2025-10-23", + "date": "2025-10-23T11:48:25Z" + }, + { + "name": "meilisearch/meilisearch", + "version": "prototype-v1.24.0.s3-snapshots-2", + "date": "2025-10-23T11:32:32Z" + }, + { + "name": "keycloak/keycloak", + "version": "26.4.2", + "date": "2025-10-23T06:59:32Z" + }, + { + "name": "Jackett/Jackett", + "version": "v0.24.175", + "date": "2025-10-23T05:50:23Z" }, { "name": "jeedom/core", "version": "4.4.20", - "date": "2025-09-10T00:27:10Z" + "date": "2025-10-23T00:27:05Z" }, { "name": "steveiliop56/tinyauth", - "version": "v3.6.2", - "date": "2025-07-17T12:08:03Z" + "version": "v4.0.1", + "date": "2025-10-15T16:53:55Z" }, { - "name": "authelia/authelia", - "version": "v4.39.9", - "date": "2025-09-09T22:48:24Z" + "name": "BerriAI/litellm", + "version": "v1.78.7-nightly", + "date": "2025-10-22T22:33:31Z" }, { - "name": "keycloak/keycloak", - "version": "26.0.15", - "date": "2025-08-27T12:12:03Z" + "name": "jhuckaby/Cronicle", + "version": "v0.9.99", + "date": "2025-10-22T22:20:33Z" }, { - "name": "MediaBrowser/Emby.Releases", - "version": "4.9.1.2", - "date": "2025-06-26T22:08:00Z" + "name": "sabnzbd/sabnzbd", + "version": "4.5.4", + "date": "2025-10-22T21:23:17Z" }, { - "name": "jenkinsci/jenkins", - "version": "jenkins-2.527", - "date": "2025-09-09T19:58:28Z" + "name": "moghtech/komodo", + "version": "v1.19.5", + "date": "2025-09-27T20:59:46Z" }, { - "name": "kyantech/Palmr", - "version": "v3.2.1-beta", - "date": "2025-09-09T19:47:13Z" + "name": "rcourtman/Pulse", + "version": "issue-596", + "date": "2025-10-22T19:48:56Z" }, { - "name": "Part-DB/Part-DB-server", - "version": "v2.1.2", - "date": "2025-09-09T19:34:11Z" + "name": "gtsteffaniak/filebrowser", + "version": "v0.8.10-beta", + "date": "2025-10-22T18:41:54Z" }, { - "name": "hargata/lubelog", - "version": "v1.5.1", - "date": "2025-09-09T16:56:49Z" + "name": "docker/compose", + "version": "v2.40.2", + "date": "2025-10-22T17:38:12Z" }, { - "name": "chrisvel/tududi", - "version": "v0.81", - "date": "2025-09-09T14:06:41Z" + "name": "louislam/uptime-kuma", + "version": "2.0.2", + "date": "2025-10-22T17:03:54Z" }, { - "name": "element-hq/synapse", - "version": "v1.138.0", - "date": "2025-09-09T11:25:50Z" + "name": "chrisvel/tududi", + "version": "v0.84.2", + "date": "2025-10-22T17:03:48Z" }, { - "name": "traefik/traefik", - "version": "v3.5.2", - "date": "2025-09-09T10:28:12Z" + "name": "prometheus/prometheus", + "version": "v0.307.2", + "date": "2025-10-22T16:00:08Z" }, { - "name": "docker/compose", - "version": "v2.39.3", - "date": "2025-09-09T08:27:27Z" + "name": "thecfu/scraparr", + "version": "v3.0.0-beta", + "date": "2025-10-22T13:12:10Z" }, { - "name": "OctoPrint/OctoPrint", - "version": "1.11.3", - "date": "2025-09-09T08:03:31Z" + "name": "NLnetLabs/unbound", + "version": "release-1.24.1", + "date": "2025-10-22T10:28:00Z" }, { - "name": "readeck/readeck", - "version": "0.20.2", - "date": "2025-09-09T06:09:25Z" + "name": "mattermost/mattermost", + "version": "server/public/v0.1.21", + "date": "2025-10-16T09:46:16Z" }, { - "name": "TandoorRecipes/recipes", - "version": "2.1.2", - "date": "2025-09-09T05:55:58Z" + "name": "zabbix/zabbix", + "version": "7.4.4rc1", + "date": "2025-10-22T09:20:59Z" }, { - "name": "gotson/komga", - "version": "1.23.4", - "date": "2025-09-09T02:47:05Z" + "name": "SigNoz/signoz", + "version": "v0.98.0", + "date": "2025-10-22T06:49:36Z" }, { - "name": "Tautulli/Tautulli", - "version": "v2.16.0", - "date": "2025-09-09T01:05:45Z" + "name": "morpheus65535/bazarr", + "version": "v1.5.3", + "date": "2025-09-20T12:12:33Z" }, { - "name": "mongodb/mongo", - "version": "r8.0.14-rc1", - "date": "2025-09-08T22:50:53Z" + "name": "chrisbenincasa/tunarr", + "version": "v0.23.0-alpha.17", + "date": "2025-10-22T00:33:47Z" }, { - "name": "diced/zipline", - "version": "v4.3.1", - "date": "2025-09-08T22:26:23Z" + "name": "bluenviron/mediamtx", + "version": "v1.15.3", + "date": "2025-10-21T19:56:55Z" }, { - "name": "cross-seed/cross-seed", - "version": "v6.13.3", - "date": "2025-09-08T21:45:15Z" + "name": "jenkinsci/jenkins", + "version": "jenkins-2.533", + "date": "2025-10-21T18:26:10Z" }, { - "name": "HabitRPG/habitica", - "version": "v5.40.2", - "date": "2025-09-08T20:59:44Z" + "name": "kyantech/Palmr", + "version": "v3.2.5-beta", + "date": "2025-10-21T16:49:14Z" }, { - "name": "booklore-app/booklore", - "version": "v1.2.1", - "date": "2025-09-08T19:31:07Z" + "name": "n8n-io/n8n", + "version": "n8n@1.116.2", + "date": "2025-10-21T11:39:58Z" }, { - "name": "fallenbagel/jellyseerr", - "version": "preview-OIDC", - "date": "2025-09-08T18:08:15Z" + "name": "element-hq/synapse", + "version": "v1.140.0", + "date": "2025-10-14T15:57:12Z" }, { - "name": "immich-app/immich", - "version": "v1.141.1", - "date": "2025-09-08T17:15:33Z" + "name": "grafana/grafana", + "version": "v12.2.1", + "date": "2025-10-21T14:40:19Z" }, { - "name": "msgbyte/tianji", - "version": "v1.24.27", - "date": "2025-09-08T16:23:37Z" + "name": "crowdsecurity/crowdsec", + "version": "v1.7.2", + "date": "2025-10-21T14:36:48Z" }, { - "name": "Paymenter/Paymenter", - "version": "v1.3.1", - "date": "2025-09-08T15:26:01Z" + "name": "laurent22/joplin", + "version": "server-v3.4.4", + "date": "2025-09-25T13:19:26Z" }, { - "name": "n8n-io/n8n", - "version": "n8n@1.109.2", - "date": "2025-09-03T07:50:21Z" + "name": "goauthentik/authentik", + "version": "version/2025.10.0-rc2", + "date": "2025-10-21T00:19:36Z" }, { - "name": "apache/tomcat", - "version": "10.1.46", - "date": "2025-09-08T14:29:54Z" + "name": "AlexxIT/go2rtc", + "version": "v1.9.11", + "date": "2025-10-21T13:21:02Z" }, { - "name": "home-assistant/operating-system", - "version": "16.2", - "date": "2025-09-08T14:03:25Z" + "name": "influxdata/telegraf", + "version": "v1.36.3", + "date": "2025-10-21T12:53:58Z" }, { - "name": "theonedev/onedev", - "version": "v12.0.10", - "date": "2025-09-08T13:20:16Z" + "name": "apache/cassandra", + "version": "5.0.6-tentative", + "date": "2025-10-21T11:42:35Z" }, { "name": "evcc-io/evcc", - "version": "0.207.6", - "date": "2025-09-08T11:52:00Z" + "version": "0.209.3", + "date": "2025-10-21T10:53:07Z" }, { - "name": "autobrr/autobrr", - "version": "v1.66.1", - "date": "2025-09-08T10:49:03Z" + "name": "VictoriaMetrics/VictoriaMetrics", + "version": "pmm-6401-v1.128.0", + "date": "2025-10-21T08:30:52Z" }, { - "name": "meilisearch/meilisearch", - "version": "latest", - "date": "2025-09-08T10:03:11Z" + "name": "openobserve/openobserve", + "version": "v0.16.0-rc1", + "date": "2025-10-21T00:37:47Z" }, { - "name": "syncthing/syncthing", - "version": "v2.0.8", - "date": "2025-09-08T08:07:18Z" + "name": "documenso/documenso", + "version": "v1.13.0", + "date": "2025-10-21T00:21:04Z" }, { - "name": "nzbgetcom/nzbget", - "version": "v25.3", - "date": "2025-09-01T09:47:06Z" + "name": "henrygd/beszel", + "version": "v0.14.1", + "date": "2025-10-20T22:10:56Z" }, { - "name": "webmin/webmin", - "version": "2.501", - "date": "2025-09-08T04:50:25Z" + "name": "booklore-app/booklore", + "version": "v1.8.1", + "date": "2025-10-20T20:53:56Z" }, { - "name": "paperless-ngx/paperless-ngx", - "version": "v2.18.4", - "date": "2025-09-07T23:57:32Z" + "name": "coder/code-server", + "version": "v4.105.1", + "date": "2025-10-20T20:19:23Z" }, { - "name": "9001/copyparty", - "version": "v1.19.8", - "date": "2025-09-07T23:36:42Z" + "name": "pymedusa/Medusa", + "version": "v1.0.23", + "date": "2025-10-20T19:51:33Z" }, { - "name": "minio/minio", - "version": "RELEASE.2025-09-07T16-13-09Z", - "date": "2025-09-07T18:53:04Z" + "name": "MediaBrowser/Emby.Releases", + "version": "4.9.1.80", + "date": "2025-09-30T20:25:16Z" }, { - "name": "karakeep-app/karakeep", - "version": "sdk/v0.27.0", - "date": "2025-09-07T17:49:04Z" + "name": "rclone/rclone", + "version": "v1.71.2", + "date": "2025-10-20T15:25:52Z" }, { - "name": "dgtlmoon/changedetection.io", - "version": "0.50.12", - "date": "2025-09-07T14:16:07Z" + "name": "Graylog2/graylog2-server", + "version": "7.0.0-rc.1", + "date": "2025-10-20T11:53:31Z" }, { - "name": "runtipi/runtipi", - "version": "v4.4.0", - "date": "2025-09-02T19:26:18Z" + "name": "jupyter/notebook", + "version": "@jupyter-notebook/ui-components@7.5.0-beta.1", + "date": "2025-10-20T07:01:38Z" }, { - "name": "semaphoreui/semaphore", - "version": "v2.17.0-beta1", - "date": "2025-09-07T08:56:50Z" + "name": "inventree/InvenTree", + "version": "1.0.7", + "date": "2025-10-20T05:23:10Z" }, { - "name": "Radarr/Radarr", - "version": "v5.27.5.10198", - "date": "2025-09-03T12:08:43Z" + "name": "firefly-iii/firefly-iii", + "version": "v6.4.2", + "date": "2025-10-07T08:11:58Z" + }, + { + "name": "jellyfin/jellyfin", + "version": "v10.11.0", + "date": "2025-10-20T00:45:19Z" + }, + { + "name": "esphome/esphome", + "version": "2025.10.2", + "date": "2025-10-20T00:42:57Z" + }, + { + "name": "pelican-dev/panel", + "version": "v1.0.0-beta27", + "date": "2025-10-20T00:38:13Z" + }, + { + "name": "seriousm4x/UpSnap", + "version": "5.2.3", + "date": "2025-10-19T20:50:15Z" + }, + { + "name": "verdaccio/verdaccio", + "version": "generator-verdaccio-plugin@6.0.0-next-8.24", + "date": "2025-10-19T19:43:46Z" + }, + { + "name": "Part-DB/Part-DB-server", + "version": "v2.2.1", + "date": "2025-10-19T14:30:11Z" + }, + { + "name": "benzino77/tasmocompiler", + "version": "v13.0.0", + "date": "2025-10-19T10:03:18Z" }, { "name": "Prowlarr/Prowlarr", @@ -275,134 +370,184 @@ "date": "2025-08-23T21:23:11Z" }, { - "name": "pocketbase/pocketbase", - "version": "v0.30.0", - "date": "2025-09-07T05:25:44Z" + "name": "Lidarr/Lidarr", + "version": "v2.14.5.4836", + "date": "2025-10-08T15:30:50Z" }, { - "name": "Lidarr/Lidarr", - "version": "v2.13.3.4711", - "date": "2025-08-28T20:06:24Z" + "name": "Luligu/matterbridge", + "version": "3.3.3", + "date": "2025-10-18T22:27:42Z" }, { - "name": "rcourtman/Pulse", - "version": "v4.14.0", - "date": "2025-09-05T18:28:28Z" + "name": "ellite/Wallos", + "version": "v4.5.0", + "date": "2025-10-18T22:00:50Z" }, { - "name": "bunkerity/bunkerweb", - "version": "v1.6.4", - "date": "2025-08-18T20:22:07Z" + "name": "leiweibau/Pi.Alert", + "version": "v2025-10-18", + "date": "2025-10-18T20:35:54Z" }, { - "name": "fuma-nama/fumadocs", - "version": "create-fumadocs-app@15.7.10", - "date": "2025-09-06T10:13:43Z" + "name": "project-zot/zot", + "version": "v2.1.10", + "date": "2025-10-18T18:46:36Z" }, { - "name": "Luligu/matterbridge", - "version": "3.2.6", - "date": "2025-09-06T07:50:15Z" + "name": "homarr-labs/homarr", + "version": "v1.42.1", + "date": "2025-10-18T18:31:38Z" }, { - "name": "forgejo/forgejo", - "version": "v12.0.3", - "date": "2025-09-06T07:01:44Z" + "name": "Notifiarr/notifiarr", + "version": "v0.9.0", + "date": "2025-10-18T17:03:56Z" }, { - "name": "moghtech/komodo", - "version": "v1.19.3", - "date": "2025-09-05T21:32:38Z" + "name": "TasmoAdmin/TasmoAdmin", + "version": "v4.3.2", + "date": "2025-10-18T12:11:00Z" }, { - "name": "homarr-labs/homarr", - "version": "v1.36.1", - "date": "2025-09-05T21:14:40Z" + "name": "readeck/readeck", + "version": "0.20.4", + "date": "2025-10-18T10:00:42Z" }, { - "name": "gtsteffaniak/filebrowser", - "version": "v0.8.4-beta", - "date": "2025-09-05T19:34:44Z" + "name": "theonedev/onedev", + "version": "v13.0.9", + "date": "2025-10-18T09:59:25Z" }, { - "name": "Stirling-Tools/Stirling-PDF", - "version": "v1.3.2", - "date": "2025-09-05T18:44:15Z" + "name": "runtipi/runtipi", + "version": "v4.5.1", + "date": "2025-10-18T08:12:19Z" }, { - "name": "henrygd/beszel", - "version": "v0.12.7", - "date": "2025-09-05T18:11:36Z" + "name": "nzbgetcom/nzbget", + "version": "v25.4", + "date": "2025-10-09T10:27:01Z" }, { - "name": "Brandawg93/PeaNUT", - "version": "v5.14.2", - "date": "2025-09-05T17:24:12Z" + "name": "9001/copyparty", + "version": "v1.19.17", + "date": "2025-10-17T23:40:02Z" }, { "name": "home-assistant/core", - "version": "2025.9.1", - "date": "2025-09-05T11:15:21Z" + "version": "2025.10.3", + "date": "2025-10-17T21:15:07Z" }, { - "name": "CrazyWolf13/streamlink-webui", - "version": "0.6", - "date": "2025-09-05T06:05:04Z" + "name": "forgejo/forgejo", + "version": "v13.0.1", + "date": "2025-10-17T18:54:16Z" }, { - "name": "louislam/uptime-kuma", - "version": "2.0.0-beta.2-temp", - "date": "2025-03-28T08:45:58Z" + "name": "grokability/snipe-it", + "version": "v8.3.4", + "date": "2025-10-17T18:13:24Z" }, { - "name": "wazuh/wazuh", - "version": "coverity-w36-4.13.0", - "date": "2025-09-01T11:40:11Z" + "name": "NodeBB/NodeBB", + "version": "v4.6.1", + "date": "2025-10-17T15:21:59Z" }, { - "name": "docmost/docmost", - "version": "v0.23.0", - "date": "2025-09-04T22:40:29Z" + "name": "neo4j/neo4j", + "version": "5.26.14", + "date": "2025-10-17T12:38:22Z" }, { - "name": "pelican-dev/panel", - "version": "v1.0.0-beta25", - "date": "2025-09-04T21:42:46Z" + "name": "zwave-js/zwave-js-ui", + "version": "v11.5.2", + "date": "2025-10-17T08:20:13Z" }, { - "name": "pelican-dev/wings", - "version": "v1.0.0-beta17", - "date": "2025-09-04T21:30:14Z" + "name": "pocketbase/pocketbase", + "version": "v0.30.4", + "date": "2025-10-17T08:03:10Z" }, { - "name": "Cleanuparr/Cleanuparr", - "version": "v2.2.3", - "date": "2025-09-04T19:24:39Z" + "name": "diced/zipline", + "version": "v4.3.2", + "date": "2025-10-17T04:13:23Z" }, { - "name": "AdguardTeam/AdGuardHome", - "version": "v0.107.65", - "date": "2025-08-20T14:02:28Z" + "name": "Stirling-Tools/Stirling-PDF", + "version": "v1.5.0", + "date": "2025-10-16T23:14:45Z" + }, + { + "name": "openhab/openhab-core", + "version": "5.0.2", + "date": "2025-10-16T21:27:35Z" }, { "name": "ollama/ollama", - "version": "v0.11.10", - "date": "2025-09-04T17:27:40Z" + "version": "v0.12.6", + "date": "2025-10-16T20:07:41Z" }, { - "name": "NodeBB/NodeBB", - "version": "v4.5.1", - "date": "2025-09-04T16:02:49Z" + "name": "minio/minio", + "version": "RELEASE.2025-10-15T17-29-55Z", + "date": "2025-10-16T19:33:51Z" }, { - "name": "raydak-labs/configarr", - "version": "v1.15.1", - "date": "2025-09-04T14:00:59Z" + "name": "open-webui/open-webui", + "version": "v0.6.34", + "date": "2025-10-16T16:55:58Z" }, { - "name": "plankanban/planka", - "version": "planka-1.0.4", - "date": "2025-09-04T13:49:40Z" + "name": "tobychui/zoraxy", + "version": "v3.2.8", + "date": "2025-10-16T14:21:48Z" + }, + { + "name": "cloudreve/cloudreve", + "version": "4.9.2", + "date": "2025-10-16T03:24:44Z" + }, + { + "name": "outline/outline", + "version": "v1.0.0-test8", + "date": "2025-10-16T01:32:14Z" + }, + { + "name": "Ombi-app/Ombi", + "version": "v4.47.1", + "date": "2025-01-05T21:14:23Z" + }, + { + "name": "netbox-community/netbox", + "version": "v4.4.4", + "date": "2025-10-15T19:27:01Z" + }, + { + "name": "immich-app/immich", + "version": "v2.1.0", + "date": "2025-10-15T19:24:14Z" + }, + { + "name": "karlomikus/bar-assistant", + "version": "v5.9.0", + "date": "2025-10-15T18:27:56Z" + }, + { + "name": "linkwarden/linkwarden", + "version": "v2.13.1", + "date": "2025-10-15T13:29:37Z" + }, + { + "name": "TandoorRecipes/recipes", + "version": "2.3.3", + "date": "2025-10-15T13:18:27Z" + }, + { + "name": "cockpit-project/cockpit", + "version": "349", + "date": "2025-10-15T11:43:12Z" }, { "name": "blakeblackshear/frigate", @@ -410,69 +555,144 @@ "date": "2024-08-29T22:32:51Z" }, { - "name": "openobserve/openobserve", - "version": "v0.15.1", - "date": "2025-09-04T10:37:23Z" + "name": "wavelog/wavelog", + "version": "2.1.2", + "date": "2025-10-15T06:51:32Z" }, { - "name": "emqx/emqx", - "version": "v5.8.8", - "date": "2025-09-04T08:35:36Z" + "name": "seerr-team/seerr", + "version": "preview-seerr", + "date": "2025-10-14T22:21:33Z" }, { - "name": "morpheus65535/bazarr", - "version": "v1.5.3-beta.10", - "date": "2025-07-15T06:07:03Z" + "name": "cloudflare/cloudflared", + "version": "2025.10.0", + "date": "2025-10-14T19:07:37Z" }, { - "name": "actualbudget/actual", - "version": "v25.9.0", - "date": "2025-09-04T01:12:37Z" + "name": "crafty-controller/crafty-4", + "version": "v4.5.5", + "date": "2025-10-14T18:48:36Z" }, { - "name": "hyperion-project/hyperion.ng", - "version": "2.1.1", - "date": "2025-06-14T17:45:06Z" + "name": "plankanban/planka", + "version": "planka-1.1.0", + "date": "2025-10-14T16:51:22Z" }, { - "name": "VictoriaMetrics/VictoriaMetrics", - "version": "pmm-6401-v1.125.1", - "date": "2025-09-03T20:17:18Z" + "name": "home-assistant/operating-system", + "version": "16.2", + "date": "2025-09-08T14:03:25Z" }, { - "name": "Graylog2/graylog2-server", - "version": "6.1.15", - "date": "2025-09-03T14:51:37Z" + "name": "rogerfar/rdt-client", + "version": "v2.0.119", + "date": "2025-10-13T23:15:11Z" }, { - "name": "glpi-project/glpi", - "version": "10.0.19", - "date": "2025-07-16T09:45:14Z" + "name": "hargata/lubelog", + "version": "v1.5.3", + "date": "2025-10-13T19:59:30Z" }, { - "name": "neo4j/neo4j", - "version": "5.26.12", - "date": "2025-09-03T12:03:22Z" + "name": "node-red/node-red", + "version": "4.1.1", + "date": "2025-10-13T14:23:53Z" }, { - "name": "Checkmk/checkmk", - "version": "v2.4.0p11", - "date": "2025-09-03T09:58:14Z" + "name": "prometheus/alertmanager", + "version": "v0.29.0-rc.0", + "date": "2025-10-10T01:13:27Z" }, { - "name": "apache/cassandra", - "version": "cassandra-4.1.10", - "date": "2025-09-03T08:46:02Z" + "name": "globaleaks/globaleaks-whistleblowing-software", + "version": "v5.0.85", + "date": "2025-10-12T19:55:18Z" }, { - "name": "crafty-controller/crafty-4", - "version": "v4.5.3", - "date": "2025-09-02T23:52:26Z" + "name": "PrivateBin/PrivateBin", + "version": "2.0.1", + "date": "2025-10-12T10:00:52Z" }, { - "name": "netbox-community/netbox", - "version": "v4.4.0", - "date": "2025-09-02T17:04:25Z" + "name": "authelia/authelia", + "version": "v4.39.13", + "date": "2025-10-12T05:45:48Z" + }, + { + "name": "gelbphoenix/autocaliweb", + "version": "v0.10.4", + "date": "2025-10-11T19:53:39Z" + }, + { + "name": "0xERR0R/blocky", + "version": "v0.27.0", + "date": "2025-10-10T20:11:48Z" + }, + { + "name": "getumbrel/umbrel", + "version": "1.4.2", + "date": "2025-05-09T08:54:49Z" + }, + { + "name": "Brandawg93/PeaNUT", + "version": "v5.16.0", + "date": "2025-10-10T16:17:02Z" + }, + { + "name": "raydak-labs/configarr", + "version": "v1.17.1", + "date": "2025-10-10T16:12:41Z" + }, + { + "name": "apache/tomcat", + "version": "10.1.48", + "date": "2025-10-10T14:46:53Z" + }, + { + "name": "semaphoreui/semaphore", + "version": "v2.16.34", + "date": "2025-10-10T11:57:38Z" + }, + { + "name": "azukaar/Cosmos-Server", + "version": "v0.18.4", + "date": "2025-04-05T19:12:57Z" + }, + { + "name": "emqx/emqx", + "version": "e6.0.1-alpha.1", + "date": "2025-10-10T06:57:48Z" + }, + { + "name": "mealie-recipes/mealie", + "version": "v3.3.2", + "date": "2025-10-10T03:45:06Z" + }, + { + "name": "projectsend/projectsend", + "version": "r1945", + "date": "2025-10-10T02:30:05Z" + }, + { + "name": "FlareSolverr/FlareSolverr", + "version": "v3.4.2", + "date": "2025-10-09T19:05:48Z" + }, + { + "name": "ErsatzTV/ErsatzTV", + "version": "v25.7.1", + "date": "2025-10-09T15:42:11Z" + }, + { + "name": "silverbulletmd/silverbullet", + "version": "2.1.9", + "date": "2025-10-09T13:57:14Z" + }, + { + "name": "glpi-project/glpi", + "version": "11.0.1", + "date": "2025-10-09T12:34:15Z" }, { "name": "rabbitmq/rabbitmq-server", @@ -480,264 +700,399 @@ "date": "2025-09-02T14:26:24Z" }, { - "name": "Dolibarr/dolibarr", - "version": "22.0.1", - "date": "2025-09-02T10:29:08Z" + "name": "ipfs/kubo", + "version": "v0.38.1", + "date": "2025-10-08T21:34:07Z" }, { - "name": "cloudreve/cloudreve", - "version": "4.7.0", - "date": "2025-09-02T06:02:43Z" + "name": "autobrr/autobrr", + "version": "v1.68.0", + "date": "2025-10-08T18:33:12Z" }, { - "name": "ErsatzTV/ErsatzTV", - "version": "v25.5.0", - "date": "2025-09-02T01:00:11Z" + "name": "advplyr/audiobookshelf", + "version": "v2.30.0", + "date": "2025-10-08T16:03:49Z" }, { - "name": "postgres/postgres", - "version": "REL_18_RC1", - "date": "2025-09-01T20:03:08Z" + "name": "nicolargo/glances", + "version": "v4.3.3", + "date": "2025-10-08T15:45:21Z" }, { - "name": "project-zot/zot", - "version": "v2.1.8", - "date": "2025-09-01T19:20:42Z" + "name": "FlowiseAI/Flowise", + "version": "flowise@3.0.8", + "date": "2025-10-08T12:19:18Z" }, { - "name": "Koenkk/zigbee2mqtt", - "version": "2.6.1", - "date": "2025-09-01T19:05:18Z" + "name": "gotson/komga", + "version": "1.23.5", + "date": "2025-10-08T07:31:37Z" }, { - "name": "outline/outline", - "version": "v0.87.3", - "date": "2025-09-01T16:25:43Z" + "name": "pelican-dev/wings", + "version": "v1.0.0-beta18", + "date": "2025-10-07T21:05:57Z" }, { - "name": "seanmorley15/AdventureLog", - "version": "v0.11.0", - "date": "2025-09-01T16:19:38Z" + "name": "C4illin/ConvertX", + "version": "v0.15.1", + "date": "2025-10-07T20:30:56Z" }, { - "name": "grafana/grafana", - "version": "rrc_steady_12.2.0-17245430286.patch1", - "date": "2025-09-01T14:19:14Z" + "name": "pocket-id/pocket-id", + "version": "v1.13.1", + "date": "2025-10-07T06:32:50Z" }, { - "name": "grokability/snipe-it", - "version": "v8.3.1", - "date": "2025-09-01T11:00:07Z" + "name": "sassanix/Warracker", + "version": "0.10.1.14", + "date": "2025-10-06T23:35:16Z" }, { - "name": "crowdsecurity/crowdsec", - "version": "v1.7.0", - "date": "2025-09-01T10:10:34Z" + "name": "Radarr/Radarr", + "version": "v5.28.0.10274", + "date": "2025-10-06T21:31:07Z" }, { - "name": "LibreTranslate/LibreTranslate", - "version": "v1.7.3", - "date": "2025-08-31T15:59:43Z" + "name": "Kometa-Team/Kometa", + "version": "v2.2.2", + "date": "2025-10-06T21:31:07Z" }, { - "name": "jhuckaby/Cronicle", - "version": "v0.9.91", - "date": "2025-08-30T21:49:57Z" + "name": "bunkerity/bunkerweb", + "version": "v1.6.5", + "date": "2025-10-06T15:25:17Z" }, { - "name": "silverbulletmd/silverbullet", - "version": "2.0.0", - "date": "2025-08-29T13:38:35Z" + "name": "mysql/mysql-server", + "version": "mysql-cluster-7.6.36", + "date": "2025-10-06T15:19:49Z" }, { - "name": "Forceu/Gokapi", - "version": "v2.1.0", - "date": "2025-08-29T12:56:13Z" + "name": "bastienwirtz/homer", + "version": "v25.10.1", + "date": "2025-10-06T14:23:20Z" }, { - "name": "saltstack/salt", - "version": "v3007.7", - "date": "2025-08-29T01:19:08Z" + "name": "jordan-dalby/ByteStash", + "version": "v1.5.9", + "date": "2025-10-06T08:34:01Z" }, { - "name": "linkwarden/linkwarden", - "version": "v2.12.2", - "date": "2025-08-28T20:34:30Z" + "name": "hyperion-project/hyperion.ng", + "version": "2.1.1", + "date": "2025-06-14T17:45:06Z" }, { - "name": "benjaminjonard/koillection", - "version": "1.7.0", - "date": "2025-08-28T18:10:59Z" + "name": "BookStackApp/BookStack", + "version": "v25.07.3", + "date": "2025-10-05T14:47:20Z" + }, + { + "name": "pommee/goaway", + "version": "v0.62.11", + "date": "2025-10-05T07:31:57Z" + }, + { + "name": "webmin/webmin", + "version": "2.520", + "date": "2025-10-05T00:51:34Z" + }, + { + "name": "redis/redis", + "version": "8.2.2", + "date": "2025-10-03T06:22:38Z" + }, + { + "name": "actualbudget/actual", + "version": "v25.10.0", + "date": "2025-10-02T11:34:39Z" + }, + { + "name": "Koenkk/zigbee2mqtt", + "version": "2.6.2", + "date": "2025-10-01T17:51:09Z" + }, + { + "name": "Kozea/Radicale", + "version": "v3.5.7.pypi", + "date": "2025-10-01T05:32:27Z" + }, + { + "name": "WordPress/WordPress", + "version": "4.7.31", + "date": "2025-09-30T18:00:06Z" + }, + { + "name": "MagicMirrorOrg/MagicMirror", + "version": "v2.33.0", + "date": "2025-09-30T16:18:10Z" }, { "name": "gristlabs/grist-core", - "version": "v1.7.3", - "date": "2025-08-28T16:50:02Z" + "version": "v1.7.4", + "date": "2025-09-30T13:34:30Z" }, { - "name": "BookStackApp/BookStack", - "version": "v25.07.2", - "date": "2025-08-28T16:46:05Z" + "name": "thomiceli/opengist", + "version": "v1.11.1", + "date": "2025-09-30T00:24:16Z" + }, + { + "name": "influxdata/influxdb", + "version": "v2.7.12", + "date": "2025-05-29T17:08:26Z" + }, + { + "name": "MDeLuise/plant-it", + "version": "1.0.0", + "date": "2025-09-29T13:53:50Z" + }, + { + "name": "lazy-media/Reactive-Resume", + "version": "v1.2.6", + "date": "2025-09-28T18:09:21Z" + }, + { + "name": "Pf2eToolsOrg/Pf2eTools", + "version": "v0.10.1", + "date": "2025-09-28T08:55:44Z" + }, + { + "name": "kimai/kimai", + "version": "2.40.0", + "date": "2025-09-27T16:19:26Z" + }, + { + "name": "FreshRSS/FreshRSS", + "version": "1.27.1", + "date": "2025-09-27T13:07:26Z" + }, + { + "name": "javedh-dev/tracktor", + "version": "0.3.18", + "date": "2025-09-27T10:32:09Z" + }, + { + "name": "Dolibarr/dolibarr", + "version": "22.0.2", + "date": "2025-09-27T01:43:20Z" + }, + { + "name": "cross-seed/cross-seed", + "version": "v6.13.5", + "date": "2025-09-27T01:10:59Z" + }, + { + "name": "traefik/traefik", + "version": "v3.5.3", + "date": "2025-09-26T09:31:01Z" + }, + { + "name": "go-gitea/gitea", + "version": "v1.26.0-dev", + "date": "2025-09-24T16:45:38Z" }, { "name": "Threadfin/Threadfin", - "version": "1.2.37", - "date": "2025-08-28T16:25:55Z" + "version": "1.2.39", + "date": "2025-09-25T15:57:02Z" }, { - "name": "influxdata/influxdb", - "version": "v3.4.1", - "date": "2025-08-28T13:56:00Z" + "name": "alexta69/metube", + "version": "2025.09.24", + "date": "2025-09-24T13:51:23Z" }, { - "name": "garethgeorge/backrest", - "version": "v1.9.2", - "date": "2025-08-28T07:06:14Z" + "name": "syncthing/syncthing", + "version": "v2.0.10", + "date": "2025-09-24T08:33:37Z" }, { - "name": "pocket-id/pocket-id", - "version": "v1.10.0", - "date": "2025-08-27T20:35:47Z" + "name": "postgres/postgres", + "version": "REL_18_0", + "date": "2025-09-22T20:11:33Z" }, { - "name": "ipfs/kubo", - "version": "v0.37.0", - "date": "2025-08-27T20:03:52Z" + "name": "gethomepage/homepage", + "version": "v1.5.0", + "date": "2025-09-22T15:28:49Z" }, { - "name": "zwave-js/zwave-js-ui", - "version": "v11.2.1", - "date": "2025-08-27T15:19:02Z" + "name": "itsmng/itsm-ng", + "version": "v2.1.0", + "date": "2025-09-22T09:23:37Z" }, { - "name": "documenso/documenso", - "version": "v1.12.2-rc.6", - "date": "2025-08-26T01:17:43Z" + "name": "Athou/commafeed", + "version": "5.11.1", + "date": "2025-09-22T02:21:27Z" }, { - "name": "coder/code-server", - "version": "v4.103.2", - "date": "2025-08-25T23:30:54Z" + "name": "gotify/server", + "version": "v2.7.3", + "date": "2025-09-21T12:07:19Z" }, { - "name": "advplyr/audiobookshelf", - "version": "v2.29.0", - "date": "2025-08-25T22:43:20Z" + "name": "traccar/traccar", + "version": "v6.10.0", + "date": "2025-09-20T15:40:36Z" }, { - "name": "mealie-recipes/mealie", - "version": "v3.1.2", - "date": "2025-08-25T18:00:52Z" + "name": "mmastrac/stylus", + "version": "v0.17.0", + "date": "2025-09-19T22:23:28Z" }, { - "name": "sabnzbd/sabnzbd", - "version": "4.5.3", - "date": "2025-08-25T13:59:56Z" + "name": "saltstack/salt", + "version": "v3007.8", + "date": "2025-09-18T18:19:04Z" + }, + { + "name": "docmost/docmost", + "version": "v0.23.2", + "date": "2025-09-18T17:18:59Z" + }, + { + "name": "eclipse-mosquitto/mosquitto", + "version": "2.1.0-test1", + "date": "2025-09-17T18:21:45Z" + }, + { + "name": "heiher/hev-socks5-server", + "version": "2.10.0", + "date": "2025-09-17T14:47:00Z" + }, + { + "name": "icereed/paperless-gpt", + "version": "v0.23.0", + "date": "2025-09-17T10:15:51Z" + }, + { + "name": "WGDashboard/WGDashboard", + "version": "v4.3.0.1", + "date": "2025-09-17T08:50:39Z" + }, + { + "name": "Checkmk/checkmk", + "version": "v2.4.0p12", + "date": "2025-09-16T12:53:03Z" + }, + { + "name": "Paymenter/Paymenter", + "version": "v1.3.4", + "date": "2025-09-15T20:48:02Z" + }, + { + "name": "apache/tika", + "version": "3.2.0", + "date": "2025-09-15T18:03:08Z" }, { - "name": "zabbix/zabbix", - "version": "7.4.2", - "date": "2025-08-25T12:38:14Z" + "name": "linuxserver/Heimdall", + "version": "v2.7.6", + "date": "2025-09-15T15:50:44Z" }, { - "name": "FlareSolverr/FlareSolverr", - "version": "v3.4.0", - "date": "2025-08-25T03:22:00Z" + "name": "usememos/memos", + "version": "v0.25.1", + "date": "2025-09-15T14:57:30Z" }, { - "name": "plexguide/Huntarr.io", - "version": "8.2.10", - "date": "2025-08-25T01:26:55Z" + "name": "karakeep-app/karakeep", + "version": "cli/v0.27.1", + "date": "2025-09-14T14:48:48Z" }, { - "name": "Ombi-app/Ombi", - "version": "v4.47.1", - "date": "2025-01-05T21:14:23Z" + "name": "intri-in/manage-my-damn-life-nextjs", + "version": "v0.8.1", + "date": "2025-09-14T06:45:23Z" }, { - "name": "wavelog/wavelog", - "version": "2.1", - "date": "2025-08-24T15:42:19Z" + "name": "go-vikunja/vikunja", + "version": "v1.0.0-rc0", + "date": "2025-08-17T18:47:15Z" }, { - "name": "janeczku/calibre-web", - "version": "0.6.25", - "date": "2025-08-24T08:51:55Z" + "name": "zerotier/ZeroTierOne", + "version": "1.16.0", + "date": "2025-09-11T18:01:57Z" }, { - "name": "sysadminsmedia/homebox", - "version": "v0.21.0", - "date": "2025-08-23T18:33:53Z" + "name": "aceberg/WatchYourLAN", + "version": "2.1.4", + "date": "2025-09-10T12:08:09Z" }, { - "name": "prometheus/prometheus", - "version": "v0.306.0-rc.0", - "date": "2025-08-21T13:31:03Z" + "name": "OctoPrint/OctoPrint", + "version": "1.11.3", + "date": "2025-09-09T08:03:31Z" }, { - "name": "caddyserver/caddy", - "version": "v2.10.2", - "date": "2025-08-23T03:10:31Z" + "name": "Tautulli/Tautulli", + "version": "v2.16.0", + "date": "2025-09-09T01:05:45Z" }, { - "name": "tailscale/tailscale", - "version": "v1.86.5", - "date": "2025-08-22T17:13:13Z" + "name": "CrazyWolf13/streamlink-webui", + "version": "0.6", + "date": "2025-09-05T06:05:04Z" }, { - "name": "rclone/rclone", - "version": "v1.71.0", - "date": "2025-08-22T16:41:23Z" + "name": "healthchecks/healthchecks", + "version": "v3.11.2", + "date": "2025-09-02T08:36:57Z" }, { - "name": "goauthentik/authentik", - "version": "version/2025.8.1", - "date": "2025-08-22T14:55:30Z" + "name": "seanmorley15/AdventureLog", + "version": "v0.11.0", + "date": "2025-09-01T16:19:38Z" }, { - "name": "lazy-media/Reactive-Resume", - "version": "v1.2.4", - "date": "2025-08-22T07:40:01Z" + "name": "LibreTranslate/LibreTranslate", + "version": "v1.7.3", + "date": "2025-08-31T15:59:43Z" }, { - "name": "Kozea/Radicale", - "version": "v3.5.5", - "date": "2025-08-22T06:57:33Z" + "name": "Forceu/Gokapi", + "version": "v2.1.0", + "date": "2025-08-29T12:56:13Z" }, { - "name": "traccar/traccar", - "version": "v6.9.1", - "date": "2025-08-22T04:04:12Z" + "name": "benjaminjonard/koillection", + "version": "1.7.0", + "date": "2025-08-28T18:10:59Z" }, { - "name": "cloudflare/cloudflared", - "version": "2025.8.1", - "date": "2025-08-21T15:39:34Z" + "name": "garethgeorge/backrest", + "version": "v1.9.2", + "date": "2025-08-28T07:06:14Z" }, { - "name": "gethomepage/homepage", - "version": "v1.4.6", - "date": "2025-08-21T14:05:58Z" + "name": "plexguide/Huntarr.io", + "version": "8.2.10", + "date": "2025-08-25T01:26:55Z" }, { - "name": "openhab/openhab-core", - "version": "4.3.7", - "date": "2025-08-20T10:26:21Z" + "name": "janeczku/calibre-web", + "version": "0.6.25", + "date": "2025-08-24T08:51:55Z" }, { - "name": "duplicati/duplicati", - "version": "v2.1.2.0-2.1.2.0_beta_2025-08-20", - "date": "2025-08-20T08:15:46Z" + "name": "sysadminsmedia/homebox", + "version": "v0.21.0", + "date": "2025-08-23T18:33:53Z" }, { - "name": "TwiN/gatus", - "version": "v5.23.2", - "date": "2025-08-19T21:24:45Z" + "name": "maxdorninger/MediaManager", + "version": "1.8.0", + "date": "2025-08-23T16:22:30Z" }, { - "name": "karlomikus/bar-assistant", - "version": "v5.8.0", - "date": "2025-08-19T16:46:00Z" + "name": "caddyserver/caddy", + "version": "v2.10.2", + "date": "2025-08-23T03:10:31Z" }, { "name": "oauth2-proxy/oauth2-proxy", @@ -749,21 +1104,6 @@ "version": "v1.1.07", "date": "2025-08-18T16:13:54Z" }, - { - "name": "FreshRSS/FreshRSS", - "version": "1.27.0", - "date": "2025-08-18T16:03:26Z" - }, - { - "name": "redis/redis", - "version": "8.2.1", - "date": "2025-08-18T15:42:48Z" - }, - { - "name": "jupyter/notebook", - "version": "@jupyter-notebook/ui-components@7.5.0-alpha.2", - "date": "2025-08-18T07:39:41Z" - }, { "name": "lldap/lldap", "version": "v0.6.2", @@ -774,11 +1114,6 @@ "version": "deluge-2.2.1.dev0", "date": "2025-08-17T20:22:28Z" }, - { - "name": "go-vikunja/vikunja", - "version": "v1.0.0-rc0", - "date": "2025-08-17T18:47:15Z" - }, { "name": "matze/wastebin", "version": "3.3.0", @@ -790,45 +1125,20 @@ "date": "2025-08-17T06:24:54Z" }, { - "name": "intri-in/manage-my-damn-life-nextjs", - "version": "v0.8.0-release", - "date": "2025-08-15T06:19:12Z" - }, - { - "name": "jellyfin/jellyfin", - "version": "v10.10.7", - "date": "2025-04-05T19:14:59Z" - }, - { - "name": "Kometa-Team/Kometa", - "version": "v2.2.1", - "date": "2025-08-13T19:49:01Z" - }, - { - "name": "go-gitea/gitea", - "version": "v1.24.5", - "date": "2025-08-13T16:35:52Z" + "name": "Leantime/leantime", + "version": "latest", + "date": "2025-08-15T15:33:51Z" }, { - "name": "ellite/Wallos", - "version": "v4.1.1", - "date": "2025-08-13T11:58:04Z" + "name": "swapplications/uhf-server-dist", + "version": "1.5.1", + "date": "2025-08-13T15:43:57Z" }, { "name": "requarks/wiki", "version": "v2.5.308", "date": "2025-08-13T07:09:29Z" }, - { - "name": "bluenviron/mediamtx", - "version": "v1.14.0", - "date": "2025-08-12T13:58:46Z" - }, - { - "name": "FlowiseAI/Flowise", - "version": "flowise@3.0.5", - "date": "2025-08-11T13:01:45Z" - }, { "name": "slskd/slskd", "version": "0.23.2", @@ -839,81 +1149,31 @@ "version": "1012-08-09", "date": "2025-08-10T13:50:58Z" }, - { - "name": "kimai/kimai", - "version": "2.38.0", - "date": "2025-08-08T21:47:19Z" - }, - { - "name": "apache/tika", - "version": "3.2.2", - "date": "2025-08-08T03:40:40Z" - }, { "name": "MariaDB/server", "version": "mariadb-12.0.2", "date": "2025-08-07T21:23:15Z" }, - { - "name": "Athou/commafeed", - "version": "5.11.0", - "date": "2025-08-06T21:14:18Z" - }, - { - "name": "bastienwirtz/homer", - "version": "v25.08.1", - "date": "2025-08-06T21:04:07Z" - }, { "name": "TryGhost/Ghost-CLI", "version": "v1.28.3", "date": "2025-08-06T12:32:02Z" }, - { - "name": "WordPress/WordPress", - "version": "4.7.30", - "date": "2025-08-05T17:23:06Z" - }, { "name": "binwiederhier/ntfy", "version": "v2.14.0", "date": "2025-08-05T08:31:35Z" }, - { - "name": "rogerfar/rdt-client", - "version": "v2.0.116", - "date": "2025-08-05T04:45:21Z" - }, - { - "name": "linuxserver/Heimdall", - "version": "v2.7.4", - "date": "2025-08-02T16:51:19Z" - }, { "name": "inspircd/inspircd", "version": "v4.8.0", "date": "2025-08-02T09:12:10Z" }, - { - "name": "donaldzou/WGDashboard", - "version": "v4.2.5", - "date": "2025-08-02T08:58:21Z" - }, - { - "name": "alexta69/metube", - "version": "2025.07.31", - "date": "2025-08-01T14:44:48Z" - }, { "name": "Suwayomi/Suwayomi-Server", "version": "v2.1.1867", "date": "2025-07-31T18:08:43Z" }, - { - "name": "leiweibau/Pi.Alert", - "version": "v2025-07-30", - "date": "2025-07-30T17:13:40Z" - }, { "name": "danielbrendel/hortusfox-web", "version": "v5.2", @@ -934,66 +1194,21 @@ "version": "v0.4.5", "date": "2025-07-29T16:39:18Z" }, - { - "name": "node-red/node-red", - "version": "4.1.0", - "date": "2025-07-29T15:15:26Z" - }, { "name": "navidrome/navidrome", "version": "v0.58.0", "date": "2025-07-28T18:59:50Z" }, - { - "name": "PrivateBin/PrivateBin", - "version": "2.0.0", - "date": "2025-07-28T07:48:40Z" - }, { "name": "umami-software/umami", "version": "v2.19.0", "date": "2025-07-27T22:25:00Z" }, - { - "name": "aceberg/WatchYourLAN", - "version": "2.1.3", - "date": "2025-07-26T14:19:00Z" - }, - { - "name": "heiher/hev-socks5-server", - "version": "2.9.0", - "date": "2025-07-25T14:20:25Z" - }, - { - "name": "TasmoAdmin/TasmoAdmin", - "version": "v4.3.1", - "date": "2025-07-22T20:10:08Z" - }, { "name": "PCJones/UmlautAdaptarr", "version": "v0.7.3", "date": "2025-07-22T14:39:54Z" }, - { - "name": "tobychui/zoraxy", - "version": "v3.2.5r2", - "date": "2025-07-21T12:52:26Z" - }, - { - "name": "icereed/paperless-gpt", - "version": "v0.22.0", - "date": "2025-07-17T06:35:43Z" - }, - { - "name": "usememos/memos", - "version": "v0.25.0", - "date": "2025-07-16T14:57:02Z" - }, - { - "name": "NLnetLabs/unbound", - "version": "release-1.23.1", - "date": "2025-07-16T09:20:27Z" - }, { "name": "sbondCo/Watcharr", "version": "v2.1.1", @@ -1014,36 +1229,16 @@ "version": "1.3.11", "date": "2025-07-13T13:33:48Z" }, - { - "name": "eclipse-mosquitto/mosquitto", - "version": "v2.0.22", - "date": "2025-07-11T21:34:20Z" - }, { "name": "NginxProxyManager/nginx-proxy-manager", "version": "v2.12.6", "date": "2025-07-09T21:52:15Z" }, - { - "name": "nicolargo/glances", - "version": "v4.3.3", - "date": "2025-07-09T15:35:44Z" - }, - { - "name": "mysql/mysql-server", - "version": "mysql-cluster-9.4.0", - "date": "2025-07-09T08:35:30Z" - }, { "name": "photoprism/photoprism", "version": "250707-d28b3101e", "date": "2025-07-07T15:15:21Z" }, - { - "name": "Kareadita/Kavita", - "version": "v0.8.7", - "date": "2025-07-05T20:08:58Z" - }, { "name": "qbittorrent/qBittorrent", "version": "release-5.1.2", @@ -1054,11 +1249,6 @@ "version": "2025.4", "date": "2025-07-01T18:01:37Z" }, - { - "name": "MagicMirrorOrg/MagicMirror", - "version": "v2.32.0", - "date": "2025-06-30T22:12:48Z" - }, { "name": "typesense/typesense", "version": "v29.0", @@ -1074,31 +1264,16 @@ "version": "v2.18.0", "date": "2025-06-24T08:29:55Z" }, - { - "name": "itsmng/itsm-ng", - "version": "v2.0.7", - "date": "2025-06-23T14:35:40Z" - }, { "name": "clusterzx/paperless-ai", "version": "v3.0.7", "date": "2025-06-22T17:49:29Z" }, - { - "name": "inventree/InvenTree", - "version": "0.17.14", - "date": "2025-06-21T23:43:04Z" - }, { "name": "Sonarr/Sonarr", "version": "v4.0.15.2941", "date": "2025-06-20T17:20:54Z" }, - { - "name": "benzino77/tasmocompiler", - "version": "v12.7.0", - "date": "2025-06-20T08:31:16Z" - }, { "name": "prometheus-pve/prometheus-pve-exporter", "version": "v3.5.5", @@ -1129,26 +1304,6 @@ "version": "v0.8.4", "date": "2025-06-10T07:57:14Z" }, - { - "name": "jordan-dalby/ByteStash", - "version": "v1.5.8", - "date": "2025-06-07T11:39:10Z" - }, - { - "name": "juanfont/headscale", - "version": "v0.26.1", - "date": "2025-06-06T11:22:02Z" - }, - { - "name": "C4illin/ConvertX", - "version": "v0.14.1", - "date": "2025-06-04T08:57:15Z" - }, - { - "name": "Pf2eToolsOrg/Pf2eTools", - "version": "v0.9.0", - "date": "2025-06-03T11:49:40Z" - }, { "name": "release-argus/Argus", "version": "0.26.3", @@ -1159,11 +1314,6 @@ "version": "v1.13.0", "date": "2025-05-25T20:21:13Z" }, - { - "name": "0xERR0R/blocky", - "version": "v0.26.2", - "date": "2025-05-22T05:24:42Z" - }, { "name": "hansmi/prometheus-paperless-exporter", "version": "v0.0.8", @@ -1184,11 +1334,6 @@ "version": "v0.2.3", "date": "2025-05-10T21:14:45Z" }, - { - "name": "getumbrel/umbrel", - "version": "1.4.2", - "date": "2025-05-09T08:54:49Z" - }, { "name": "ZoeyVid/NPMplus", "version": "2025-05-07-r1", @@ -1199,11 +1344,6 @@ "version": "3.5.0", "date": "2025-05-05T16:28:24Z" }, - { - "name": "gotify/server", - "version": "v2.6.3", - "date": "2025-04-27T09:05:42Z" - }, { "name": "TechnitiumSoftware/DnsServer", "version": "v13.6.0", @@ -1234,16 +1374,6 @@ "version": "v0.2.11", "date": "2025-04-12T21:13:08Z" }, - { - "name": "thomiceli/opengist", - "version": "v1.10.0", - "date": "2025-04-07T14:32:15Z" - }, - { - "name": "azukaar/Cosmos-Server", - "version": "v0.18.4", - "date": "2025-04-05T19:12:57Z" - }, { "name": "wger-project/wger", "version": "2.3", @@ -1274,6 +1404,11 @@ "version": "v1.55.4", "date": "2025-03-24T11:31:02Z" }, + { + "name": "redlib-org/redlib", + "version": "v0.36.0", + "date": "2025-03-20T03:06:11Z" + }, { "name": "Donkie/Spoolman", "version": "v0.22.1", @@ -1289,11 +1424,6 @@ "version": "v0.18.0", "date": "2025-03-11T12:47:22Z" }, - { - "name": "AlexxIT/go2rtc", - "version": "v1.9.9", - "date": "2025-03-10T03:22:11Z" - }, { "name": "awawa-dev/HyperHDR", "version": "v21.0.0.0", @@ -1304,11 +1434,6 @@ "version": "v2.4.2", "date": "2025-03-08T10:49:04Z" }, - { - "name": "prometheus/alertmanager", - "version": "v0.28.1", - "date": "2025-03-07T15:41:35Z" - }, { "name": "toniebox-reverse-engineering/teddycloud", "version": "tc_v0.6.4", @@ -1324,11 +1449,6 @@ "version": "v1.11.2", "date": "2025-02-24T19:47:06Z" }, - { - "name": "drakkan/sftpgo", - "version": "v2.6.6", - "date": "2025-02-24T19:14:46Z" - }, { "name": "babybuddy/babybuddy", "version": "v2.7.1", @@ -1364,16 +1484,6 @@ "version": "v0.7.3", "date": "2024-12-15T10:18:06Z" }, - { - "name": "pymedusa/Medusa", - "version": "v1.0.22", - "date": "2024-12-13T12:22:19Z" - }, - { - "name": "MDeLuise/plant-it", - "version": "0.10.0", - "date": "2024-12-10T09:35:26Z" - }, { "name": "phpipam/phpipam", "version": "v1.7.3", @@ -1389,31 +1499,16 @@ "version": "0.10.1", "date": "2024-11-10T10:25:45Z" }, - { - "name": "zerotier/ZeroTierOne", - "version": "1.14.2", - "date": "2024-10-29T16:17:48Z" - }, { "name": "CorentinTh/it-tools", "version": "v2024.10.22-7ca5933", "date": "2024-10-22T09:58:03Z" }, - { - "name": "Notifiarr/notifiarr", - "version": "v0.8.3", - "date": "2024-10-04T23:49:07Z" - }, { "name": "FunkeyFlo/ps5-mqtt", "version": "v1.4.0", "date": "2024-08-06T19:57:33Z" }, - { - "name": "projectsend/projectsend", - "version": "r1720", - "date": "2024-08-03T04:07:20Z" - }, { "name": "hywax/mafl", "version": "v0.15.4", @@ -1448,5 +1543,10 @@ "name": "thelounge/thelounge-deb", "version": "v4.4.3", "date": "2024-04-06T12:24:35Z" + }, + { + "name": "deepch/RTSPtoWeb", + "version": "v2.4.3", + "date": "2023-03-29T12:05:02Z" } ] diff --git a/scripts/json/zot-registry.json b/scripts/json/zot-registry.json index 4cfb98b..8ef7baf 100644 --- a/scripts/json/zot-registry.json +++ b/scripts/json/zot-registry.json @@ -23,7 +23,7 @@ "ram": 2048, "hdd": 5, "os": "Debian", - "version": "12" + "version": "13" } } ], diff --git a/src/app/_components/GeneralSettingsModal.tsx b/src/app/_components/GeneralSettingsModal.tsx index 94c4589..7cf3b9f 100644 --- a/src/app/_components/GeneralSettingsModal.tsx +++ b/src/app/_components/GeneralSettingsModal.tsx @@ -322,31 +322,18 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr setIsSaving(true); setMessage(null); + console.log('Saving auto-sync settings:', { + autoSyncEnabled, + syncIntervalType, + syncIntervalPredefined, + syncIntervalCron, + autoDownloadNew, + autoUpdateExisting, + notificationEnabled, + appriseUrls + }); + try { - // Validate cron expression if custom - if (syncIntervalType === 'custom' && syncIntervalCron) { - const response = await fetch('/api/settings/auto-sync', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - autoSyncEnabled, - syncIntervalType, - syncIntervalPredefined, - syncIntervalCron, - autoDownloadNew, - autoUpdateExisting, - notificationEnabled, - appriseUrls: appriseUrls - }) - }); - - if (!response.ok) { - const errorData = await response.json(); - setMessage({ type: 'error', text: errorData.error ?? 'Failed to save auto-sync settings' }); - return; - } - } - const response = await fetch('/api/settings/auto-sync', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -362,11 +349,16 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr }) }); + console.log('API response status:', response.status); + if (response.ok) { + const result = await response.json(); + console.log('API response data:', result); setMessage({ type: 'success', text: 'Auto-sync settings saved successfully!' }); setTimeout(() => setMessage(null), 3000); } else { const errorData = await response.json(); + console.error('API error:', errorData); setMessage({ type: 'error', text: errorData.error ?? 'Failed to save auto-sync settings' }); } } catch (error) { @@ -824,7 +816,46 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr { + console.log('Toggle changed to:', checked); + setAutoSyncEnabled(checked); + + // Auto-save when toggle changes + try { + // If syncIntervalType is custom but no cron expression, fallback to predefined + const effectiveSyncIntervalType = (syncIntervalType === 'custom' && !syncIntervalCron) + ? 'predefined' + : syncIntervalType; + + const response = await fetch('/api/settings/auto-sync', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + autoSyncEnabled: checked, + syncIntervalType: effectiveSyncIntervalType, + syncIntervalPredefined: effectiveSyncIntervalType === 'predefined' ? syncIntervalPredefined : undefined, + syncIntervalCron: effectiveSyncIntervalType === 'custom' ? syncIntervalCron : undefined, + autoDownloadNew, + autoUpdateExisting, + notificationEnabled, + appriseUrls: appriseUrls + }) + }); + + if (response.ok) { + console.log('Auto-sync toggle saved successfully'); + // Update local state to reflect the effective sync interval type + if (effectiveSyncIntervalType !== syncIntervalType) { + setSyncIntervalType(effectiveSyncIntervalType); + } + } else { + const errorData = await response.json(); + console.error('Failed to save auto-sync toggle:', errorData); + } + } catch (error) { + console.error('Error saving auto-sync toggle:', error); + } + }} disabled={isSaving} /> diff --git a/src/app/api/settings/auto-sync/route.ts b/src/app/api/settings/auto-sync/route.ts index 035c8d9..5002df1 100644 --- a/src/app/api/settings/auto-sync/route.ts +++ b/src/app/api/settings/auto-sync/route.ts @@ -7,6 +7,7 @@ import { isValidCron } from 'cron-validator'; export async function POST(request: NextRequest) { try { const settings = await request.json(); + console.log('Received auto-sync settings:', settings); if (!settings || typeof settings !== 'object') { return NextResponse.json( @@ -45,11 +46,14 @@ export async function POST(request: NextRequest) { // Validate sync interval type if (!['predefined', 'custom'].includes(settings.syncIntervalType)) { + console.log('Invalid syncIntervalType:', settings.syncIntervalType); return NextResponse.json( { error: 'syncIntervalType must be "predefined" or "custom"' }, { status: 400 } ); } + + console.log('Sync interval validation - type:', settings.syncIntervalType, 'cron:', settings.syncIntervalCron); // Validate predefined interval if (settings.syncIntervalType === 'predefined') { @@ -64,14 +68,13 @@ export async function POST(request: NextRequest) { // Validate custom cron expression if (settings.syncIntervalType === 'custom') { - if (!settings.syncIntervalCron || typeof settings.syncIntervalCron !== 'string') { - return NextResponse.json( - { error: 'Custom cron expression is required when syncIntervalType is "custom"' }, - { status: 400 } - ); - } - - if (!isValidCron(settings.syncIntervalCron, { seconds: false })) { + if (!settings.syncIntervalCron || typeof settings.syncIntervalCron !== 'string' || settings.syncIntervalCron.trim() === '') { + console.log('Custom sync interval type but no cron expression provided, falling back to predefined'); + // Fallback to predefined if custom is selected but no cron expression + settings.syncIntervalType = 'predefined'; + settings.syncIntervalPredefined = settings.syncIntervalPredefined || '1hour'; + settings.syncIntervalCron = ''; + } else if (!isValidCron(settings.syncIntervalCron, { seconds: false })) { return NextResponse.json( { error: 'Invalid cron expression' }, { status: 400 } @@ -156,23 +159,33 @@ export async function POST(request: NextRequest) { } // Write back to .env file + console.log('Writing to .env file:', envPath); + console.log('New .env content:', envContent); fs.writeFileSync(envPath, envContent); + console.log('Successfully wrote to .env file'); // Reschedule auto-sync service with new settings try { - const { getAutoSyncService } = await import('../../../../server/lib/autoSyncInit.js'); + const { getAutoSyncService, setAutoSyncService } = await import('../../../../server/lib/autoSyncInit.js'); let autoSyncService = getAutoSyncService(); // If no global instance exists, create one if (!autoSyncService) { const { AutoSyncService } = await import('../../../../server/services/autoSyncService.js'); autoSyncService = new AutoSyncService(); + setAutoSyncService(autoSyncService); } + // Update the global service instance with new settings + console.log('Updating global service instance with settings:', settings); + autoSyncService.saveSettings(settings); + if (settings.autoSyncEnabled) { + console.log('Enabling auto-sync...'); autoSyncService.scheduleAutoSync(); console.log('Auto-sync rescheduled with new settings'); } else { + console.log('Disabling auto-sync...'); autoSyncService.stopAutoSync(); console.log('Auto-sync stopped'); } diff --git a/src/server/api/routers/scripts.ts b/src/server/api/routers/scripts.ts index cc01602..2b5b61e 100644 --- a/src/server/api/routers/scripts.ts +++ b/src/server/api/routers/scripts.ts @@ -504,22 +504,26 @@ export const scriptsRouter = createTRPCRouter({ .mutation(async ({ input }) => { try { // Use the global auto-sync service instance - const { getAutoSyncService } = await import('~/server/lib/autoSyncInit'); + const { getAutoSyncService, setAutoSyncService } = await import('~/server/lib/autoSyncInit'); let autoSyncService = getAutoSyncService(); // If no global instance exists, create one if (!autoSyncService) { const { AutoSyncService } = await import('~/server/services/autoSyncService'); autoSyncService = new AutoSyncService(); + setAutoSyncService(autoSyncService); } + // Save settings to both .env file and service instance autoSyncService.saveSettings(input); // Reschedule auto-sync if enabled if (input.autoSyncEnabled) { autoSyncService.scheduleAutoSync(); + console.log('Auto-sync rescheduled with new settings'); } else { autoSyncService.stopAutoSync(); + console.log('Auto-sync stopped'); } return { success: true, message: 'Auto-sync settings saved successfully' }; diff --git a/src/server/lib/autoSyncInit.js b/src/server/lib/autoSyncInit.js index 5950854..2ecf2fb 100644 --- a/src/server/lib/autoSyncInit.js +++ b/src/server/lib/autoSyncInit.js @@ -9,13 +9,16 @@ export function initializeAutoSync() { try { console.log('Initializing auto-sync service...'); autoSyncService = new AutoSyncService(); + console.log('AutoSyncService instance created'); // Load settings and schedule if enabled const settings = autoSyncService.loadSettings(); + console.log('Settings loaded:', settings); if (settings.autoSyncEnabled) { console.log('Auto-sync is enabled, scheduling cron job...'); autoSyncService.scheduleAutoSync(); + console.log('Cron job scheduled'); } else { console.log('Auto-sync is disabled'); } @@ -23,6 +26,7 @@ export function initializeAutoSync() { console.log('Auto-sync service initialized successfully'); } catch (error) { console.error('Failed to initialize auto-sync service:', error); + console.error('Error stack:', error.stack); } } @@ -49,6 +53,13 @@ export function getAutoSyncService() { return autoSyncService; } +/** + * Set the auto-sync service instance (for external management) + */ +export function setAutoSyncService(service) { + autoSyncService = service; +} + /** * Graceful shutdown handler */ diff --git a/src/server/lib/autoSyncInit.ts b/src/server/lib/autoSyncInit.ts index 862db2c..76189fe 100644 --- a/src/server/lib/autoSyncInit.ts +++ b/src/server/lib/autoSyncInit.ts @@ -49,6 +49,13 @@ export function getAutoSyncService(): AutoSyncService | null { return autoSyncService; } +/** + * Set the auto-sync service instance (for external management) + */ +export function setAutoSyncService(service: AutoSyncService | null): void { + autoSyncService = service; +} + /** * Graceful shutdown handler */ diff --git a/src/server/services/autoSyncService.js b/src/server/services/autoSyncService.js index 1d779b8..e7c9b2b 100644 --- a/src/server/services/autoSyncService.js +++ b/src/server/services/autoSyncService.js @@ -173,6 +173,7 @@ export class AutoSyncService { const key = trimmedLine.substring(0, equalIndex).trim(); if (key && key in settingsMap) { // Replace existing setting + // @ts-ignore - Dynamic property access is safe here newLines.push(`${key}=${settingsMap[key]}`); existingKeys.add(key); } else { @@ -256,7 +257,10 @@ export class AutoSyncService { if (this.cronJob) { this.cronJob.stop(); this.cronJob = null; + this.isRunning = false; console.log('Auto-sync cron job stopped'); + } else { + console.log('No active cron job to stop'); } } @@ -285,8 +289,8 @@ export class AutoSyncService { const results = { jsonSync: syncResult, - newScripts: /** @type {string[]} */ ([]), - updatedScripts: /** @type {string[]} */ ([]), + newScripts: /** @type {any[]} */ ([]), + updatedScripts: /** @type {any[]} */ ([]), errors: /** @type {string[]} */ ([]) }; From fdeda6c77a14199c63ea9d8159aec21b7008f816 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 24 Oct 2025 22:16:31 +0200 Subject: [PATCH 09/15] Add GitHub token authentication to sync services - Add GitHub token authentication to GitHubJsonService for API calls - Add GitHub token authentication to GitHubService for API calls - Update fetchFromGitHub methods to use GITHUB_TOKEN from .env - Update downloadJsonFile methods to use GitHub token for raw file downloads - Add proper error handling for rate limit exceeded (403) errors - Add console logging to show when token is/isn't being used - Improve error messages to suggest setting GITHUB_TOKEN for higher rate limits This ensures that when a GitHub token is specified in .env, it will be used for all GitHub API calls during sync operations, providing higher rate limits and better reliability. --- src/server/services/github.ts | 23 ++++++++++---- src/server/services/githubJsonService.ts | 38 +++++++++++++++++++----- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/src/server/services/github.ts b/src/server/services/github.ts index c5e4a77..2545a6e 100644 --- a/src/server/services/github.ts +++ b/src/server/services/github.ts @@ -29,14 +29,25 @@ export class GitHubService { } private async fetchFromGitHub(endpoint: string): Promise { - const response = await fetch(`${this.baseUrl}${endpoint}`, { - headers: { - 'Accept': 'application/vnd.github.v3+json', - 'User-Agent': 'PVEScripts-Local/1.0', - }, - }); + const headers: HeadersInit = { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'PVEScripts-Local/1.0', + }; + + // Add GitHub token authentication if available + if (env.GITHUB_TOKEN) { + headers.Authorization = `token ${env.GITHUB_TOKEN}`; + console.log('Using GitHub token for API authentication'); + } else { + console.log('No GitHub token found, using unauthenticated requests (lower rate limits)'); + } + + const response = await fetch(`${this.baseUrl}${endpoint}`, { headers }); if (!response.ok) { + if (response.status === 403) { + throw new Error(`GitHub API rate limit exceeded. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`); + } throw new Error(`GitHub API error: ${response.status} ${response.statusText}`); } diff --git a/src/server/services/githubJsonService.ts b/src/server/services/githubJsonService.ts index 30e0742..01a6f82 100644 --- a/src/server/services/githubJsonService.ts +++ b/src/server/services/githubJsonService.ts @@ -41,14 +41,26 @@ export class GitHubJsonService { private async fetchFromGitHub(endpoint: string): Promise { this.initializeConfig(); - const response = await fetch(`${this.baseUrl!}${endpoint}`, { - headers: { - 'Accept': 'application/vnd.github.v3+json', - 'User-Agent': 'PVEScripts-Local/1.0', - }, - }); + + const headers: HeadersInit = { + 'Accept': 'application/vnd.github.v3+json', + 'User-Agent': 'PVEScripts-Local/1.0', + }; + + // Add GitHub token authentication if available + if (env.GITHUB_TOKEN) { + headers.Authorization = `token ${env.GITHUB_TOKEN}`; + console.log('Using GitHub token for API authentication'); + } else { + console.log('No GitHub token found, using unauthenticated requests (lower rate limits)'); + } + + const response = await fetch(`${this.baseUrl!}${endpoint}`, { headers }); if (!response.ok) { + if (response.status === 403) { + throw new Error(`GitHub API rate limit exceeded. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`); + } throw new Error(`GitHub API error: ${response.status} ${response.statusText}`); } @@ -59,8 +71,20 @@ export class GitHubJsonService { this.initializeConfig(); const rawUrl = `https://raw.githubusercontent.com/${this.extractRepoPath()}/${this.branch!}/${filePath}`; - const response = await fetch(rawUrl); + const headers: HeadersInit = { + 'User-Agent': 'PVEScripts-Local/1.0', + }; + + // Add GitHub token authentication if available (for raw files, use token in URL or header) + if (env.GITHUB_TOKEN) { + headers.Authorization = `token ${env.GITHUB_TOKEN}`; + } + + const response = await fetch(rawUrl, { headers }); if (!response.ok) { + if (response.status === 403) { + throw new Error(`GitHub rate limit exceeded while downloading ${filePath}. Consider setting GITHUB_TOKEN for higher limits. Status: ${response.status} ${response.statusText}`); + } throw new Error(`Failed to download ${filePath}: ${response.status} ${response.statusText}`); } From 7b4daf87544b2d91cfdbbbcf9cf6ddf261b6ded4 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 24 Oct 2025 22:20:13 +0200 Subject: [PATCH 10/15] Fix autosync continuing to run after being disabled - Add defensive check in cron job execution to stop if autosync is disabled - Ensure isRunning flag is set to false when stopping autosync - Add logging to show when autosync is disabled and not scheduling - Fix both API route and TRPC router to properly stop service - Prevent multiple cron jobs from running simultaneously This fixes the issue where autosync would continue running even after being disabled in the GUI, causing rate limit errors and unwanted syncs. --- src/app/api/settings/auto-sync/route.ts | 15 ++------------- src/server/api/routers/scripts.ts | 2 ++ src/server/services/autoSyncService.js | 9 +++++++++ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/app/api/settings/auto-sync/route.ts b/src/app/api/settings/auto-sync/route.ts index 5002df1..f912a27 100644 --- a/src/app/api/settings/auto-sync/route.ts +++ b/src/app/api/settings/auto-sync/route.ts @@ -7,7 +7,6 @@ import { isValidCron } from 'cron-validator'; export async function POST(request: NextRequest) { try { const settings = await request.json(); - console.log('Received auto-sync settings:', settings); if (!settings || typeof settings !== 'object') { return NextResponse.json( @@ -46,14 +45,11 @@ export async function POST(request: NextRequest) { // Validate sync interval type if (!['predefined', 'custom'].includes(settings.syncIntervalType)) { - console.log('Invalid syncIntervalType:', settings.syncIntervalType); return NextResponse.json( { error: 'syncIntervalType must be "predefined" or "custom"' }, { status: 400 } ); } - - console.log('Sync interval validation - type:', settings.syncIntervalType, 'cron:', settings.syncIntervalCron); // Validate predefined interval if (settings.syncIntervalType === 'predefined') { @@ -69,7 +65,6 @@ export async function POST(request: NextRequest) { // Validate custom cron expression if (settings.syncIntervalType === 'custom') { if (!settings.syncIntervalCron || typeof settings.syncIntervalCron !== 'string' || settings.syncIntervalCron.trim() === '') { - console.log('Custom sync interval type but no cron expression provided, falling back to predefined'); // Fallback to predefined if custom is selected but no cron expression settings.syncIntervalType = 'predefined'; settings.syncIntervalPredefined = settings.syncIntervalPredefined || '1hour'; @@ -159,10 +154,7 @@ export async function POST(request: NextRequest) { } // Write back to .env file - console.log('Writing to .env file:', envPath); - console.log('New .env content:', envContent); fs.writeFileSync(envPath, envContent); - console.log('Successfully wrote to .env file'); // Reschedule auto-sync service with new settings try { @@ -177,17 +169,14 @@ export async function POST(request: NextRequest) { } // Update the global service instance with new settings - console.log('Updating global service instance with settings:', settings); autoSyncService.saveSettings(settings); if (settings.autoSyncEnabled) { - console.log('Enabling auto-sync...'); autoSyncService.scheduleAutoSync(); - console.log('Auto-sync rescheduled with new settings'); } else { - console.log('Disabling auto-sync...'); autoSyncService.stopAutoSync(); - console.log('Auto-sync stopped'); + // Ensure the service is completely stopped and won't restart + autoSyncService.isRunning = false; } } catch (error) { console.error('Error rescheduling auto-sync service:', error); diff --git a/src/server/api/routers/scripts.ts b/src/server/api/routers/scripts.ts index 2b5b61e..0c394b2 100644 --- a/src/server/api/routers/scripts.ts +++ b/src/server/api/routers/scripts.ts @@ -523,6 +523,8 @@ export const scriptsRouter = createTRPCRouter({ console.log('Auto-sync rescheduled with new settings'); } else { autoSyncService.stopAutoSync(); + // Ensure the service is completely stopped and won't restart + autoSyncService.isRunning = false; console.log('Auto-sync stopped'); } diff --git a/src/server/services/autoSyncService.js b/src/server/services/autoSyncService.js index e7c9b2b..cade526 100644 --- a/src/server/services/autoSyncService.js +++ b/src/server/services/autoSyncService.js @@ -205,6 +205,7 @@ export class AutoSyncService { const settings = this.loadSettings(); if (!settings.autoSyncEnabled) { + console.log('Auto-sync is disabled, not scheduling cron job'); return; } @@ -240,6 +241,14 @@ export class AutoSyncService { return; } + // Double-check that autosync is still enabled before executing + const currentSettings = this.loadSettings(); + if (!currentSettings.autoSyncEnabled) { + console.log('Auto-sync has been disabled, stopping cron job'); + this.stopAutoSync(); + return; + } + console.log('Starting scheduled auto-sync...'); await this.executeAutoSync(); }, { From ffef6313d431592d39cc718175b527fc5ae16e29 Mon Sep 17 00:00:00 2001 From: Michel Roegl-Brunner Date: Fri, 24 Oct 2025 22:23:59 +0200 Subject: [PATCH 11/15] Add error reporting for GitHub rate limit errors - Add specific error handling for GitHub API rate limit (403) errors - Create RateLimitError with proper error name for identification - Store last sync error and error time in settings for UI display - Add error status display in autosync settings modal - Show user-friendly error messages with GitHub token suggestion - Clear error status on successful sync - Update both GitHubJsonService and GitHubService with rate limit error handling This provides better user feedback when GitHub API rate limits are exceeded and guides users to set up a GitHub token for higher rate limits. --- src/app/_components/GeneralSettingsModal.tsx | 44 +++++++-------- src/app/api/settings/auto-sync/route.ts | 15 ++++-- src/server/services/autoSyncService.js | 57 ++++++++++++++++---- src/server/services/github.ts | 7 ++- src/server/services/githubJsonService.ts | 11 ++-- 5 files changed, 92 insertions(+), 42 deletions(-) diff --git a/src/app/_components/GeneralSettingsModal.tsx b/src/app/_components/GeneralSettingsModal.tsx index 7cf3b9f..9a5debb 100644 --- a/src/app/_components/GeneralSettingsModal.tsx +++ b/src/app/_components/GeneralSettingsModal.tsx @@ -46,6 +46,8 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr const [appriseUrls, setAppriseUrls] = useState([]); const [appriseUrlsText, setAppriseUrlsText] = useState(''); const [lastAutoSync, setLastAutoSync] = useState(''); + const [lastAutoSyncError, setLastAutoSyncError] = useState(null); + const [lastAutoSyncErrorTime, setLastAutoSyncErrorTime] = useState(null); const [cronValidationError, setCronValidationError] = useState(''); // Load existing settings when modal opens @@ -311,6 +313,8 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr setAppriseUrls(settings.appriseUrls ?? []); setAppriseUrlsText((settings.appriseUrls ?? []).join('\n')); setLastAutoSync(settings.lastAutoSync ?? ''); + setLastAutoSyncError(settings.lastAutoSyncError ?? null); + setLastAutoSyncErrorTime(settings.lastAutoSyncErrorTime ?? null); } } } catch (error) { @@ -322,17 +326,6 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr setIsSaving(true); setMessage(null); - console.log('Saving auto-sync settings:', { - autoSyncEnabled, - syncIntervalType, - syncIntervalPredefined, - syncIntervalCron, - autoDownloadNew, - autoUpdateExisting, - notificationEnabled, - appriseUrls - }); - try { const response = await fetch('/api/settings/auto-sync', { method: 'POST', @@ -348,17 +341,12 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr appriseUrls: appriseUrls }) }); - - console.log('API response status:', response.status); if (response.ok) { - const result = await response.json(); - console.log('API response data:', result); setMessage({ type: 'success', text: 'Auto-sync settings saved successfully!' }); setTimeout(() => setMessage(null), 3000); } else { const errorData = await response.json(); - console.error('API error:', errorData); setMessage({ type: 'error', text: errorData.error ?? 'Failed to save auto-sync settings' }); } } catch (error) { @@ -817,7 +805,6 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr { - console.log('Toggle changed to:', checked); setAutoSyncEnabled(checked); // Auto-save when toggle changes @@ -843,14 +830,10 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr }); if (response.ok) { - console.log('Auto-sync toggle saved successfully'); // Update local state to reflect the effective sync interval type if (effectiveSyncIntervalType !== syncIntervalType) { setSyncIntervalType(effectiveSyncIntervalType); } - } else { - const errorData = await response.json(); - console.error('Failed to save auto-sync toggle:', errorData); } } catch (error) { console.error('Error saving auto-sync toggle:', error); @@ -1047,6 +1030,25 @@ export function GeneralSettingsModal({ isOpen, onClose }: GeneralSettingsModalPr )} + {lastAutoSyncError && ( +
+
+ + + +
+

Last sync error:

+

{lastAutoSyncError}

+ {lastAutoSyncErrorTime && ( +

+ {new Date(lastAutoSyncErrorTime).toLocaleString()} +

+ )} +
+
+
+ )} +