From d55ff7b466b994ea6002aa24f608417f6016342d Mon Sep 17 00:00:00 2001 From: Jake Turner Date: Wed, 11 Feb 2026 21:47:38 -0800 Subject: [PATCH] feat: curated content update checking --- .../collection_updates_controller.ts | 20 +- admin/app/jobs/run_download_job.ts | 22 +- .../services/collection_manifest_service.ts | 6 + .../app/services/collection_update_service.ts | 239 ++++++++++-------- admin/app/services/ollama_service.ts | 10 +- admin/app/validators/common.ts | 18 ++ admin/constants/misc.ts | 2 + admin/docs/release-notes.md | 3 +- admin/inertia/lib/api.ts | 25 +- admin/inertia/pages/home.tsx | 2 +- admin/inertia/pages/settings/update.tsx | 207 +++++++++++++++ admin/start/env.ts | 7 + admin/start/routes.ts | 8 +- admin/types/collections.ts | 27 +- 14 files changed, 470 insertions(+), 126 deletions(-) create mode 100644 admin/constants/misc.ts diff --git a/admin/app/controllers/collection_updates_controller.ts b/admin/app/controllers/collection_updates_controller.ts index 182d1e2..a6cccdf 100644 --- a/admin/app/controllers/collection_updates_controller.ts +++ b/admin/app/controllers/collection_updates_controller.ts @@ -1,9 +1,25 @@ import { CollectionUpdateService } from '#services/collection_update_service' +import { + applyContentUpdateValidator, + applyAllContentUpdatesValidator, +} from '#validators/common' import type { HttpContext } from '@adonisjs/core/http' export default class CollectionUpdatesController { async checkForUpdates({}: HttpContext) { - const collectionUpdateService = new CollectionUpdateService() - return await collectionUpdateService.checkForUpdates() + const service = new CollectionUpdateService() + return await service.checkForUpdates() + } + + async applyUpdate({ request }: HttpContext) { + const update = await request.validateUsing(applyContentUpdateValidator) + const service = new CollectionUpdateService() + return await service.applyUpdate(update) + } + + async applyAllUpdates({ request }: HttpContext) { + const { updates } = await request.validateUsing(applyAllContentUpdatesValidator) + const service = new CollectionUpdateService() + return await service.applyAllUpdates(updates) } } diff --git a/admin/app/jobs/run_download_job.ts b/admin/app/jobs/run_download_job.ts index 24e4031..3cc09ad 100644 --- a/admin/app/jobs/run_download_job.ts +++ b/admin/app/jobs/run_download_job.ts @@ -41,9 +41,16 @@ export class RunDownloadJob { if (resourceMetadata) { const { default: InstalledResource } = await import('#models/installed_resource') const { DateTime } = await import('luxon') - const { getFileStatsIfExists } = await import('../utils/fs.js') + const { getFileStatsIfExists, deleteFileIfExists } = await import('../utils/fs.js') const stats = await getFileStatsIfExists(filepath) + // Look up the old entry so we can clean up the previous file after updating + const oldEntry = await InstalledResource.query() + .where('resource_id', resourceMetadata.resource_id) + .where('resource_type', filetype as 'zim' | 'map') + .first() + const oldFilePath = oldEntry?.file_path ?? null + await InstalledResource.updateOrCreate( { resource_id: resourceMetadata.resource_id, resource_type: filetype as 'zim' | 'map' }, { @@ -55,6 +62,19 @@ export class RunDownloadJob { installed_at: DateTime.now(), } ) + + // Delete the old file if it differs from the new one + if (oldFilePath && oldFilePath !== filepath) { + try { + await deleteFileIfExists(oldFilePath) + console.log(`[RunDownloadJob] Deleted old file: ${oldFilePath}`) + } catch (deleteError) { + console.warn( + `[RunDownloadJob] Failed to delete old file ${oldFilePath}:`, + deleteError + ) + } + } } if (filetype === 'zim') { diff --git a/admin/app/services/collection_manifest_service.ts b/admin/app/services/collection_manifest_service.ts index 9f43464..02924c4 100644 --- a/admin/app/services/collection_manifest_service.ts +++ b/admin/app/services/collection_manifest_service.ts @@ -192,6 +192,8 @@ export class CollectionManifestService { let zimCount = 0 let mapCount = 0 + console.log("RECONCILING FILESYSTEM MANIFESTS...") + // Reconcile ZIM files try { const zimDir = join(process.cwd(), ZIM_STORAGE_PATH) @@ -199,6 +201,8 @@ export class CollectionManifestService { const zimItems = await listDirectoryContents(zimDir) const zimFiles = zimItems.filter((f) => f.name.endsWith('.zim')) + console.log(`Found ${zimFiles.length} ZIM files on disk. Reconciling with database...`) + // Get spec for URL lookup const zimSpec = await this.getCachedSpec('zim_categories') const specResourceMap = new Map() @@ -215,10 +219,12 @@ export class CollectionManifestService { const seenZimIds = new Set() for (const file of zimFiles) { + console.log(`Processing ZIM file: ${file.name}`) // Skip Wikipedia files (managed by WikipediaSelection model) if (file.name.startsWith('wikipedia_en_')) continue const parsed = CollectionManifestService.parseZimFilename(file.name) + console.log(`Parsed ZIM filename:`, parsed) if (!parsed) continue seenZimIds.add(parsed.resource_id) diff --git a/admin/app/services/collection_update_service.ts b/admin/app/services/collection_update_service.ts index 8971e2e..b1e06d1 100644 --- a/admin/app/services/collection_update_service.ts +++ b/admin/app/services/collection_update_service.ts @@ -1,130 +1,157 @@ import logger from '@adonisjs/core/services/logger' +import env from '#start/env' +import axios from 'axios' import InstalledResource from '#models/installed_resource' -import { CollectionManifestService } from './collection_manifest_service.js' +import { RunDownloadJob } from '../jobs/run_download_job.js' +import { ZIM_STORAGE_PATH } from '../utils/fs.js' +import { join } from 'path' import type { - ZimCategoriesSpec, - MapsSpec, - CollectionResourceUpdateInfo, - CollectionUpdateCheckResult, - SpecResource, + ResourceUpdateCheckRequest, + ResourceUpdateInfo, + ContentUpdateCheckResult, } from '../../types/collections.js' +import { NOMAD_API_DEFAULT_BASE_URL } from '../../constants/misc.js' + +const MAP_STORAGE_PATH = '/storage/maps' + +const ZIM_MIME_TYPES = ['application/x-zim', 'application/x-openzim', 'application/octet-stream'] +const PMTILES_MIME_TYPES = ['application/vnd.pmtiles', 'application/octet-stream'] export class CollectionUpdateService { - private manifestService = new CollectionManifestService() - - async checkForUpdates(): Promise { - const resourceUpdates: CollectionResourceUpdateInfo[] = [] - let specChanged = false - - // Check if specs have changed - try { - const [zimChanged, mapsChanged] = await Promise.all([ - this.manifestService.fetchAndCacheSpec('zim_categories'), - this.manifestService.fetchAndCacheSpec('maps'), - ]) - specChanged = zimChanged || mapsChanged - } catch (error) { - logger.error('[CollectionUpdateService] Failed to fetch latest specs:', error) + async checkForUpdates(): Promise { + const nomadAPIURL = env.get('NOMAD_API_URL') || NOMAD_API_DEFAULT_BASE_URL + if (!nomadAPIURL) { + return { + updates: [], + checked_at: new Date().toISOString(), + error: 'Nomad API is not configured. Set the NOMAD_API_URL environment variable.', + } } - // Check for ZIM resource version updates - const zimUpdates = await this.checkZimUpdates() - resourceUpdates.push(...zimUpdates) + const installed = await InstalledResource.all() + if (installed.length === 0) { + return { + updates: [], + checked_at: new Date().toISOString(), + } + } - // Check for map resource version updates - const mapUpdates = await this.checkMapUpdates() - resourceUpdates.push(...mapUpdates) + const requestBody: ResourceUpdateCheckRequest = { + resources: installed.map((r) => ({ + resource_id: r.resource_id, + resource_type: r.resource_type, + installed_version: r.version, + })), + } + + try { + const response = await axios.post(`${nomadAPIURL}/api/v1/resources/check-updates`, requestBody, { + timeout: 15000, + }) + + logger.info( + `[CollectionUpdateService] Update check complete: ${response.data.length} update(s) available` + ) + + return { + updates: response.data, + checked_at: new Date().toISOString(), + } + } catch (error) { + if (axios.isAxiosError(error) && error.response) { + logger.error( + `[CollectionUpdateService] Nomad API returned ${error.response.status}: ${JSON.stringify(error.response.data)}` + ) + return { + updates: [], + checked_at: new Date().toISOString(), + error: `Nomad API returned status ${error.response.status}`, + } + } + const message = + error instanceof Error ? error.message : 'Unknown error contacting Nomad API' + logger.error(`[CollectionUpdateService] Failed to check for updates: ${message}`) + return { + updates: [], + checked_at: new Date().toISOString(), + error: `Failed to contact Nomad API: ${message}`, + } + } + } + + async applyUpdate( + update: ResourceUpdateInfo + ): Promise<{ success: boolean; jobId?: string; error?: string }> { + // Check if a download is already in progress for this URL + const existingJob = await RunDownloadJob.getByUrl(update.download_url) + if (existingJob) { + const state = await existingJob.getState() + if (state === 'active' || state === 'waiting' || state === 'delayed') { + return { + success: false, + error: `A download is already in progress for ${update.resource_id}`, + } + } + } + + const filename = this.buildFilename(update) + const filepath = this.buildFilepath(update, filename) + + const result = await RunDownloadJob.dispatch({ + url: update.download_url, + filepath, + timeout: 30000, + allowedMimeTypes: + update.resource_type === 'zim' ? ZIM_MIME_TYPES : PMTILES_MIME_TYPES, + forceNew: true, + filetype: update.resource_type, + resourceMetadata: { + resource_id: update.resource_id, + version: update.latest_version, + collection_ref: null, + }, + }) + + if (!result || !result.job) { + return { success: false, error: 'Failed to dispatch download job' } + } logger.info( - `[CollectionUpdateService] Update check complete: spec_changed=${specChanged}, resource_updates=${resourceUpdates.length}` + `[CollectionUpdateService] Dispatched update download for ${update.resource_id}: ${update.installed_version} → ${update.latest_version}` ) - return { spec_changed: specChanged, resource_updates: resourceUpdates } + return { success: true, jobId: result.job.id } } - private async checkZimUpdates(): Promise { - const updates: CollectionResourceUpdateInfo[] = [] + async applyAllUpdates( + updates: ResourceUpdateInfo[] + ): Promise<{ results: Array<{ resource_id: string; success: boolean; jobId?: string; error?: string }> }> { + const results: Array<{ + resource_id: string + success: boolean + jobId?: string + error?: string + }> = [] - try { - const spec = await this.manifestService.getCachedSpec('zim_categories') - if (!spec) return updates - - const installed = await InstalledResource.query().where('resource_type', 'zim') - if (installed.length === 0) return updates - - // Build a map of spec resources by ID for quick lookup - const specResourceMap = new Map() - for (const category of spec.categories) { - for (const tier of category.tiers) { - for (const resource of tier.resources) { - // Only keep the latest version if there are duplicates - const existing = specResourceMap.get(resource.id) - if (!existing || resource.version > existing.version) { - specResourceMap.set(resource.id, resource) - } - } - } - } - - // Compare installed versions against spec versions - for (const entry of installed) { - const specResource = specResourceMap.get(entry.resource_id) - if (!specResource) continue - - if (specResource.version > entry.version) { - updates.push({ - resource_id: entry.resource_id, - installed_version: entry.version, - latest_version: specResource.version, - latest_url: specResource.url, - latest_size_mb: specResource.size_mb, - }) - } - } - } catch (error) { - logger.error('[CollectionUpdateService] Error checking ZIM updates:', error) + for (const update of updates) { + const result = await this.applyUpdate(update) + results.push({ resource_id: update.resource_id, ...result }) } - return updates + return { results } } - private async checkMapUpdates(): Promise { - const updates: CollectionResourceUpdateInfo[] = [] - - try { - const spec = await this.manifestService.getCachedSpec('maps') - if (!spec) return updates - - const installed = await InstalledResource.query().where('resource_type', 'map') - if (installed.length === 0) return updates - - // Build a map of spec resources by ID - const specResourceMap = new Map() - for (const collection of spec.collections) { - for (const resource of collection.resources) { - specResourceMap.set(resource.id, resource) - } - } - - // Compare installed versions against spec versions - for (const entry of installed) { - const specResource = specResourceMap.get(entry.resource_id) - if (!specResource) continue - - if (specResource.version > entry.version) { - updates.push({ - resource_id: entry.resource_id, - installed_version: entry.version, - latest_version: specResource.version, - latest_url: specResource.url, - latest_size_mb: specResource.size_mb, - }) - } - } - } catch (error) { - logger.error('[CollectionUpdateService] Error checking map updates:', error) + private buildFilename(update: ResourceUpdateInfo): string { + if (update.resource_type === 'zim') { + return `${update.resource_id}_${update.latest_version}.zim` } + return `${update.resource_id}_${update.latest_version}.pmtiles` + } - return updates + private buildFilepath(update: ResourceUpdateInfo, filename: string): string { + if (update.resource_type === 'zim') { + return join(process.cwd(), ZIM_STORAGE_PATH, filename) + } + return join(process.cwd(), MAP_STORAGE_PATH, 'pmtiles', filename) } } diff --git a/admin/app/services/ollama_service.ts b/admin/app/services/ollama_service.ts index 9c4bc1b..2793152 100644 --- a/admin/app/services/ollama_service.ts +++ b/admin/app/services/ollama_service.ts @@ -11,8 +11,10 @@ import { SERVICE_NAMES } from '../../constants/service_names.js' import transmit from '@adonisjs/transmit/services/main' import Fuse, { IFuseOptions } from 'fuse.js' import { BROADCAST_CHANNELS } from '../../constants/broadcast.js' +import env from '#start/env' +import { NOMAD_API_DEFAULT_BASE_URL } from '../../constants/misc.js' -const NOMAD_MODELS_API_BASE_URL = 'https://api.projectnomad.us/api/v1/ollama/models' +const NOMAD_MODELS_API_PATH = '/api/v1/ollama/models' const MODELS_CACHE_FILE = path.join(process.cwd(), 'storage', 'ollama-models-cache.json') const CACHE_MAX_AGE_MS = 24 * 60 * 60 * 1000 // 24 hours @@ -214,7 +216,11 @@ export class OllamaService { } logger.info('[OllamaService] Fetching fresh available models from API') - const response = await axios.get(NOMAD_MODELS_API_BASE_URL) + + const baseUrl = env.get('NOMAD_API_URL') || NOMAD_API_DEFAULT_BASE_URL + const fullUrl = new URL(NOMAD_MODELS_API_PATH, baseUrl).toString() + + const response = await axios.get(fullUrl) if (!response.data || !Array.isArray(response.data.models)) { logger.warn( `[OllamaService] Invalid response format when fetching available models: ${JSON.stringify(response.data)}` diff --git a/admin/app/validators/common.ts b/admin/app/validators/common.ts index 183e40c..369e7a7 100644 --- a/admin/app/validators/common.ts +++ b/admin/app/validators/common.ts @@ -68,3 +68,21 @@ export const selectWikipediaValidator = vine.compile( optionId: vine.string().trim().minLength(1), }) ) + +const resourceUpdateInfoBase = vine.object({ + resource_id: vine.string().trim().minLength(1), + resource_type: vine.enum(['zim', 'map'] as const), + installed_version: vine.string().trim(), + latest_version: vine.string().trim().minLength(1), + download_url: vine.string().url().trim(), +}) + +export const applyContentUpdateValidator = vine.compile(resourceUpdateInfoBase) + +export const applyAllContentUpdatesValidator = vine.compile( + vine.object({ + updates: vine + .array(resourceUpdateInfoBase) + .minLength(1), + }) +) diff --git a/admin/constants/misc.ts b/admin/constants/misc.ts new file mode 100644 index 0000000..21f3db3 --- /dev/null +++ b/admin/constants/misc.ts @@ -0,0 +1,2 @@ + +export const NOMAD_API_DEFAULT_BASE_URL = 'https://api.projectnomad.us' \ No newline at end of file diff --git a/admin/docs/release-notes.md b/admin/docs/release-notes.md index 67f9293..6677f71 100644 --- a/admin/docs/release-notes.md +++ b/admin/docs/release-notes.md @@ -3,7 +3,8 @@ ## Unreleased ### Features - +- **Collections**: Complete overhaul of collection management with dynamic manifests, database tracking of installed resources, and improved UI for managing ZIM files and map assets +- **Collections**: Added support for checking if newer versions of installed resources are available based on manifest data ### Bug Fixes ### Improvements diff --git a/admin/inertia/lib/api.ts b/admin/inertia/lib/api.ts index e33b1bf..e6dfe89 100644 --- a/admin/inertia/lib/api.ts +++ b/admin/inertia/lib/api.ts @@ -4,7 +4,7 @@ import { ServiceSlim } from '../../types/services' import { FileEntry } from '../../types/files' import { CheckLatestVersionResult, SystemInformationResponse, SystemUpdateStatus } from '../../types/system' import { DownloadJobWithProgress, WikipediaState } from '../../types/downloads' -import type { CategoryWithStatus, CollectionWithStatus, CollectionUpdateCheckResult } from '../../types/collections' +import type { CategoryWithStatus, CollectionWithStatus, ContentUpdateCheckResult, ResourceUpdateInfo } from '../../types/collections' import { catchInternal } from './util' import { NomadOllamaModel, OllamaChatRequest } from '../../types/ollama' import { ChatResponse, ModelResponse } from 'ollama' @@ -127,9 +127,28 @@ class API { })() } - async checkForCollectionUpdates() { + async checkForContentUpdates() { return catchInternal(async () => { - const response = await this.client.post('/collection-updates/check') + const response = await this.client.post('/content-updates/check') + return response.data + })() + } + + async applyContentUpdate(update: ResourceUpdateInfo) { + return catchInternal(async () => { + const response = await this.client.post<{ success: boolean; jobId?: string; error?: string }>( + '/content-updates/apply', + update + ) + return response.data + })() + } + + async applyAllContentUpdates(updates: ResourceUpdateInfo[]) { + return catchInternal(async () => { + const response = await this.client.post<{ + results: Array<{ resource_id: string; success: boolean; jobId?: string; error?: string }> + }>('/content-updates/apply-all', { updates }) return response.data })() } diff --git a/admin/inertia/pages/home.tsx b/admin/inertia/pages/home.tsx index a8c099d..4724ce6 100644 --- a/admin/inertia/pages/home.tsx +++ b/admin/inertia/pages/home.tsx @@ -95,7 +95,7 @@ export default function Home(props: { const { data: easySetupVisited } = useSystemSetting({ key: 'ui.hasVisitedEasySetup' }) - const shouldHighlightEasySetup = easySetupVisited?.value !== 'true' + const shouldHighlightEasySetup = easySetupVisited?.value ? easySetupVisited?.value !== 'true' : false // Add installed services (non-dependency services only) props.system.services diff --git a/admin/inertia/pages/settings/update.tsx b/admin/inertia/pages/settings/update.tsx index 9a289f0..9d0ac5f 100644 --- a/admin/inertia/pages/settings/update.tsx +++ b/admin/inertia/pages/settings/update.tsx @@ -1,15 +1,220 @@ import { Head } from '@inertiajs/react' import SettingsLayout from '~/layouts/SettingsLayout' import StyledButton from '~/components/StyledButton' +import StyledTable from '~/components/StyledTable' +import StyledSectionHeader from '~/components/StyledSectionHeader' +import ActiveDownloads from '~/components/ActiveDownloads' import Alert from '~/components/Alert' import { useEffect, useState } from 'react' import { IconAlertCircle, IconArrowBigUpLines, IconCheck, IconCircleCheck, IconReload } from '@tabler/icons-react' import { SystemUpdateStatus } from '../../../types/system' +import type { ContentUpdateCheckResult, ResourceUpdateInfo } from '../../../types/collections' import api from '~/lib/api' import Input from '~/components/inputs/Input' import { useMutation } from '@tanstack/react-query' import { useNotifications } from '~/context/NotificationContext' +function ContentUpdatesSection() { + const { addNotification } = useNotifications() + const [checkResult, setCheckResult] = useState(null) + const [isChecking, setIsChecking] = useState(false) + const [applyingIds, setApplyingIds] = useState>(new Set()) + const [isApplyingAll, setIsApplyingAll] = useState(false) + + const handleCheck = async () => { + setIsChecking(true) + try { + const result = await api.checkForContentUpdates() + if (result) { + setCheckResult(result) + } + } catch { + setCheckResult({ + updates: [], + checked_at: new Date().toISOString(), + error: 'Failed to check for content updates', + }) + } finally { + setIsChecking(false) + } + } + + const handleApply = async (update: ResourceUpdateInfo) => { + setApplyingIds((prev) => new Set(prev).add(update.resource_id)) + try { + const result = await api.applyContentUpdate(update) + if (result?.success) { + addNotification({ type: 'success', message: `Update started for ${update.resource_id}` }) + // Remove from the updates list + setCheckResult((prev) => + prev + ? { ...prev, updates: prev.updates.filter((u) => u.resource_id !== update.resource_id) } + : prev + ) + } else { + addNotification({ type: 'error', message: result?.error || 'Failed to start update' }) + } + } catch { + addNotification({ type: 'error', message: `Failed to start update for ${update.resource_id}` }) + } finally { + setApplyingIds((prev) => { + const next = new Set(prev) + next.delete(update.resource_id) + return next + }) + } + } + + const handleApplyAll = async () => { + if (!checkResult?.updates.length) return + setIsApplyingAll(true) + try { + const result = await api.applyAllContentUpdates(checkResult.updates) + if (result?.results) { + const succeeded = result.results.filter((r) => r.success).length + const failed = result.results.filter((r) => !r.success).length + if (succeeded > 0) { + addNotification({ type: 'success', message: `Started ${succeeded} update(s)` }) + } + if (failed > 0) { + addNotification({ type: 'warning', message: `${failed} update(s) could not be started` }) + } + // Remove successful updates from the list + const successIds = new Set(result.results.filter((r) => r.success).map((r) => r.resource_id)) + setCheckResult((prev) => + prev + ? { ...prev, updates: prev.updates.filter((u) => !successIds.has(u.resource_id)) } + : prev + ) + } + } catch { + addNotification({ type: 'error', message: 'Failed to apply updates' }) + } finally { + setIsApplyingAll(false) + } + } + + return ( +
+ + +
+
+

+ Check if newer versions of your installed ZIM files and maps are available. +

+ + Check for Content Updates + +
+ + {checkResult?.error && ( + + )} + + {checkResult && !checkResult.error && checkResult.updates.length === 0 && ( + + )} + + {checkResult && checkResult.updates.length > 0 && ( +
+
+

+ {checkResult.updates.length} update(s) available +

+ + Update All ({checkResult.updates.length}) + +
+ ( + {record.resource_id} + ), + }, + { + accessor: 'resource_type', + title: 'Type', + render: (record) => ( + + {record.resource_type === 'zim' ? 'ZIM' : 'Map'} + + ), + }, + { + accessor: 'installed_version', + title: 'Version', + render: (record) => ( + + {record.installed_version} → {record.latest_version} + + ), + }, + { + accessor: 'resource_id', + title: '', + render: (record) => ( + handleApply(record)} + loading={applyingIds.has(record.resource_id)} + > + Update + + ), + }, + ]} + /> +
+ )} + + {checkResult?.checked_at && ( +

+ Last checked: {new Date(checkResult.checked_at).toLocaleString()} +

+ )} +
+ + +
+ ) +} + export default function SystemUpdatePage(props: { system: { updateAvailable: boolean @@ -380,6 +585,8 @@ export default function SystemUpdatePage(props: { variant="solid" /> + + {showLogs && (
diff --git a/admin/start/env.ts b/admin/start/env.ts index 2eb21c9..ddf9b5f 100644 --- a/admin/start/env.ts +++ b/admin/start/env.ts @@ -53,4 +53,11 @@ export default await Env.create(new URL('../', import.meta.url), { */ REDIS_HOST: Env.schema.string({ format: 'host' }), REDIS_PORT: Env.schema.number(), + + /* + |---------------------------------------------------------- + | Variables for configuring Project Nomad's external API URL + |---------------------------------------------------------- + */ + NOMAD_API_URL: Env.schema.string.optional(), }) diff --git a/admin/start/routes.ts b/admin/start/routes.ts index cff7d20..0a10285 100644 --- a/admin/start/routes.ts +++ b/admin/start/routes.ts @@ -34,7 +34,13 @@ router.get('/easy-setup', [EasySetupController, 'index']) router.get('/easy-setup/complete', [EasySetupController, 'complete']) router.get('/api/easy-setup/curated-categories', [EasySetupController, 'listCuratedCategories']) router.post('/api/manifests/refresh', [EasySetupController, 'refreshManifests']) -router.post('/api/collection-updates/check', [CollectionUpdatesController, 'checkForUpdates']) +router + .group(() => { + router.post('/check', [CollectionUpdatesController, 'checkForUpdates']) + router.post('/apply', [CollectionUpdatesController, 'applyUpdate']) + router.post('/apply-all', [CollectionUpdatesController, 'applyAllUpdates']) + }) + .prefix('/api/content-updates') router .group(() => { diff --git a/admin/types/collections.ts b/admin/types/collections.ts index da906f5..1ec6d5c 100644 --- a/admin/types/collections.ts +++ b/admin/types/collections.ts @@ -72,15 +72,24 @@ export type CollectionWithStatus = SpecCollection & { total_count: number } -export type CollectionResourceUpdateInfo = { - resource_id: string - installed_version: string - latest_version: string - latest_url: string - latest_size_mb?: number +export type ResourceUpdateCheckRequest = { + resources: Array<{ + resource_id: string + resource_type: 'zim' | 'map' + installed_version: string + }> } -export type CollectionUpdateCheckResult = { - spec_changed: boolean - resource_updates: CollectionResourceUpdateInfo[] +export type ResourceUpdateInfo = { + resource_id: string + resource_type: 'zim' | 'map' + installed_version: string + latest_version: string + download_url: string +} + +export type ContentUpdateCheckResult = { + updates: ResourceUpdateInfo[] + checked_at: string + error?: string }