From f9e8874f6c6ef454912ca69dac12802e59000df5 Mon Sep 17 00:00:00 2001 From: SaelixCode Date: Thu, 5 Mar 2026 12:37:59 -0500 Subject: [PATCH] feat: integrate official lsio api and rich template metadata --- CHANGELOG.md | 2 + backend/src/services/TemplateService.ts | 47 +++++++++++++++++++++-- frontend/src/components/TemplatesView.tsx | 41 +++++++++++++++++++- 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fe9d99b..5ba8e231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- **Added:** Official LinuxServer.io API integration as the default Template Registry. +- **Added:** Rich metadata display in the App Store (Architectures, Documentation links, GitHub repository links). - **Added:** Dynamic Template Registry URL support via global settings, defaulting to LinuxServer.io templates. - **Fixed:** Smart Volume Sanitizer to automatically rewrite messy Portainer bind mounts into clean, relative paths (Sencho 1:1 path rule). - Git Flow branching strategy and branch protection. diff --git a/backend/src/services/TemplateService.ts b/backend/src/services/TemplateService.ts index a03a2eed..d8feac36 100644 --- a/backend/src/services/TemplateService.ts +++ b/backend/src/services/TemplateService.ts @@ -25,6 +25,10 @@ export interface Template { env?: TemplateEnv[]; categories?: string[]; platform?: string; + github_url?: string; + docs_url?: string; + architectures?: string[]; + stars?: number; repository?: { url: string; stackfile: string; @@ -50,11 +54,46 @@ export class TemplateService { try { const settings = DatabaseService.getInstance().getGlobalSettings(); // Default to a reliable LSIO Portainer v2 template registry if not set - const registryUrl = settings.template_registry_url || 'https://raw.githubusercontent.com/technorabilia/portainer-templates/main/lsio/templates/templates.json'; + const registryUrl = settings.template_registry_url || 'https://api.linuxserver.io/api/v1/images?include_config=true'; + + const response = await axios.get(registryUrl); + + if (registryUrl.includes('api.linuxserver.io')) { + // Official LSIO API Schema Mapping + const lsioApps = response.data?.data?.repositories?.linuxserver || []; + + this.cachedTemplates = Object.values(lsioApps).map((app: any) => { + return { + type: 1, + title: app.name, + description: app.description || '', + logo: app.logo || `https://raw.githubusercontent.com/linuxserver/docker-templates/master/linuxserver.io/img/${app.name}-logo.png`, + image: `lscr.io/linuxserver/${app.name}:latest`, + github_url: app.github, + docs_url: app.readme, + architectures: app.arch, + stars: app.stars, + // Map configs if available, otherwise default to empty arrays + ports: (app.config?.ports || []).map((p: any) => `${p.external || p.internal}:${p.internal}/${p.protocol || 'tcp'}`), + volumes: (app.config?.volumes || []).map((v: any) => { + const folderName = v.path.split('/').filter(Boolean).pop() || 'data'; + return { + container: v.path, + bind: `./${folderName}` // Proactively create a clean relative path + }; + }), + env: (app.config?.environment || []).map((e: any) => ({ + name: e.name, + label: e.desc || e.name, + default: e.default || '' + })) + }; + }); + } else { + // Legacy Portainer v2 Format (Fallback for custom registries) + this.cachedTemplates = (response.data.templates || []).filter((t: Template) => !!t.image && t.type === 1); + } - const response = await axios.get(registryUrl); - // Filter out templates without images as we are generating compose files from image, ports, etc. - this.cachedTemplates = (response.data.templates || []).filter((t: Template) => !!t.image && t.type === 1); this.lastFetchTime = now; return this.cachedTemplates; } catch (error) { diff --git a/frontend/src/components/TemplatesView.tsx b/frontend/src/components/TemplatesView.tsx index ebea14f7..1f26f43f 100644 --- a/frontend/src/components/TemplatesView.tsx +++ b/frontend/src/components/TemplatesView.tsx @@ -5,7 +5,7 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter } from "@/components/ui/sheet"; import { Button } from "@/components/ui/button"; -import { Search, Rocket, Loader2, Info } from "lucide-react"; +import { Search, Rocket, Loader2, Info, ExternalLink, Github, Star } from "lucide-react"; import { toast } from "sonner"; export interface TemplateEnv { @@ -24,6 +24,10 @@ export interface Template { volumes?: any[]; env?: TemplateEnv[]; categories?: string[]; + github_url?: string; + docs_url?: string; + architectures?: string[]; + stars?: number; } interface TemplatesViewProps { @@ -198,6 +202,41 @@ export function TemplatesView({ onDeploySuccess }: TemplatesViewProps) { {selectedTemplate.description} + + {(selectedTemplate.architectures || selectedTemplate.stars !== undefined || selectedTemplate.github_url || selectedTemplate.docs_url) && ( +
+ {selectedTemplate.architectures && selectedTemplate.architectures.length > 0 && ( +
+ {selectedTemplate.architectures.map(arch => ( + + {arch} + + ))} +
+ )} + +
+ {selectedTemplate.stars !== undefined && ( +
+ + {selectedTemplate.stars} +
+ )} + {selectedTemplate.github_url && ( + + + Source + + )} + {selectedTemplate.docs_url && ( + + + Docs + + )} +
+
+ )}