mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-19 14:56:27 +00:00
feat(app-store): category filter bar and custom registry settings
- TemplateService: static LSIO_CATEGORY_MAP covers ~80 well-known apps
across Automation, Downloaders, Media, Monitoring, Networking,
Security, Development, Productivity, Utilities, and Other. Category
lookup is O(1) and runs once at cache-fill time. Adds source field
('linuxserver' | 'custom') to Template for future per-source UX.
Portainer v2 registries pass their native categories through unchanged.
Adds clearCache() method wired to POST /api/templates/refresh-cache.
- AppStoreView: category pill bar (All + sorted dynamic categories)
rendered between search and grid; active pill fills on click; clicking
a category badge on a card also sets the active filter; app count
updates reactively; filter composed with existing search query.
- SettingsModal: new App Store section (local-node only) exposes a
custom Portainer v2 JSON URL input with Save & Refresh (saves setting
then busts the 24h template cache) and Reset to Default.
This commit is contained in:
@@ -1482,6 +1482,11 @@ app.get('/api/templates', async (req: Request, res: Response) => {
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/templates/refresh-cache', authMiddleware, (req: Request, res: Response) => {
|
||||
templateService.clearCache();
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
app.post('/api/templates/deploy', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { stackName, template, envVars } = req.body;
|
||||
|
||||
@@ -29,6 +29,7 @@ export interface Template {
|
||||
docs_url?: string;
|
||||
architectures?: string[];
|
||||
stars?: number;
|
||||
source?: string;
|
||||
repository?: {
|
||||
url: string;
|
||||
stackfile: string;
|
||||
@@ -40,11 +41,165 @@ export interface TemplatesResponse {
|
||||
templates: Template[];
|
||||
}
|
||||
|
||||
// Static category map for LSIO apps (the LSIO API does not expose category metadata).
|
||||
// Apps can belong to multiple categories. Unmapped apps fall back to ['Other'].
|
||||
const LSIO_CATEGORY_MAP: Record<string, string[]> = {
|
||||
// Media Servers
|
||||
'plex': ['Media'],
|
||||
'jellyfin': ['Media'],
|
||||
'emby': ['Media'],
|
||||
'navidrome': ['Media'],
|
||||
'airsonic-advanced': ['Media'],
|
||||
'airsonic': ['Media'],
|
||||
'beets': ['Media'],
|
||||
'calibre': ['Media', 'Books'],
|
||||
'calibre-web': ['Media', 'Books'],
|
||||
'kavita': ['Media', 'Books'],
|
||||
'komga': ['Media', 'Books'],
|
||||
'mylar3': ['Media', 'Books'],
|
||||
'ubooquity': ['Media', 'Books'],
|
||||
'lazylibrarian': ['Media', 'Books'],
|
||||
'cops': ['Media', 'Books'],
|
||||
'photoprism': ['Media', 'Productivity'],
|
||||
'immich': ['Media', 'Productivity'],
|
||||
'piwigo': ['Media'],
|
||||
'lychee': ['Media'],
|
||||
'davos': ['Media'],
|
||||
'mstream': ['Media'],
|
||||
'koel': ['Media'],
|
||||
'grocy': ['Productivity'],
|
||||
// *arr Automation suite
|
||||
'sonarr': ['Automation', 'Media'],
|
||||
'radarr': ['Automation', 'Media'],
|
||||
'lidarr': ['Automation', 'Media'],
|
||||
'readarr': ['Automation', 'Media'],
|
||||
'bazarr': ['Automation', 'Media'],
|
||||
'whisparr': ['Automation', 'Media'],
|
||||
'prowlarr': ['Automation'],
|
||||
'jackett': ['Automation'],
|
||||
'nzbhydra2': ['Automation'],
|
||||
'overseerr': ['Automation', 'Media'],
|
||||
'ombi': ['Automation', 'Media'],
|
||||
'requestrr': ['Automation'],
|
||||
'tautulli': ['Monitoring', 'Media'],
|
||||
'organizr': ['Automation'],
|
||||
'recyclarr': ['Automation'],
|
||||
'notifiarr': ['Automation'],
|
||||
'unpackerr': ['Automation'],
|
||||
// Dashboards / Homepages
|
||||
'heimdall': ['Utilities'],
|
||||
'homer': ['Utilities'],
|
||||
'dasherr': ['Utilities'],
|
||||
'flame': ['Utilities'],
|
||||
'homarr': ['Utilities'],
|
||||
'dashdot': ['Monitoring'],
|
||||
// Downloaders
|
||||
'qbittorrent': ['Downloaders'],
|
||||
'transmission': ['Downloaders'],
|
||||
'deluge': ['Downloaders'],
|
||||
'sabnzbd': ['Downloaders'],
|
||||
'nzbget': ['Downloaders'],
|
||||
'aria2': ['Downloaders'],
|
||||
'jdownloader-2': ['Downloaders'],
|
||||
'pyload-ng': ['Downloaders'],
|
||||
'rutorrent': ['Downloaders'],
|
||||
'flood': ['Downloaders'],
|
||||
'medusa': ['Automation', 'Downloaders'],
|
||||
'sickchill': ['Automation', 'Downloaders'],
|
||||
// Monitoring
|
||||
'grafana': ['Monitoring'],
|
||||
'netdata': ['Monitoring'],
|
||||
'uptime-kuma': ['Monitoring'],
|
||||
'statping-ng': ['Monitoring'],
|
||||
'healthchecks': ['Monitoring'],
|
||||
'smokeping': ['Monitoring'],
|
||||
'librespeed': ['Monitoring'],
|
||||
'speedtest-tracker': ['Monitoring'],
|
||||
'scrutiny': ['Monitoring'],
|
||||
'prometheus': ['Monitoring'],
|
||||
'loki': ['Monitoring'],
|
||||
'influxdb': ['Monitoring'],
|
||||
// Networking / Reverse Proxy
|
||||
'nginx': ['Networking'],
|
||||
'swag': ['Networking'],
|
||||
'letsencrypt': ['Networking'],
|
||||
'ddclient': ['Networking'],
|
||||
'duckdns': ['Networking'],
|
||||
'wireguard': ['Networking', 'Security'],
|
||||
'openvpn-as': ['Networking', 'Security'],
|
||||
'netbootxyz': ['Networking'],
|
||||
'pihole': ['Networking'],
|
||||
'unbound': ['Networking'],
|
||||
'adguardhome': ['Networking'],
|
||||
'cloudflared': ['Networking'],
|
||||
'haproxy': ['Networking'],
|
||||
'traefik': ['Networking'],
|
||||
'nginx-proxy-manager': ['Networking'],
|
||||
'fail2ban': ['Networking', 'Security'],
|
||||
// Security / Auth
|
||||
'vaultwarden': ['Security'],
|
||||
'authelia': ['Security'],
|
||||
'lldap': ['Security'],
|
||||
'endlessh': ['Security'],
|
||||
'sshwifty': ['Security'],
|
||||
// Development / CI
|
||||
'gitea': ['Development'],
|
||||
'code-server': ['Development'],
|
||||
'drone': ['Development'],
|
||||
'drone-runner-docker': ['Development'],
|
||||
'registry': ['Development'],
|
||||
'jenkins': ['Development'],
|
||||
'gogs': ['Development'],
|
||||
'woodpecker-ci': ['Development'],
|
||||
'gitlab': ['Development'],
|
||||
'fleet': ['Development'],
|
||||
// Productivity / Self-hosted SaaS
|
||||
'nextcloud': ['Productivity'],
|
||||
'bookstack': ['Productivity', 'Documentation'],
|
||||
'dokuwiki': ['Productivity', 'Documentation'],
|
||||
'wikijs': ['Productivity', 'Documentation'],
|
||||
'paperless-ngx': ['Productivity'],
|
||||
'mealie': ['Productivity'],
|
||||
'freshrss': ['Productivity'],
|
||||
'miniflux': ['Productivity'],
|
||||
'wallabag': ['Productivity'],
|
||||
'trilium': ['Productivity'],
|
||||
'hedgedoc': ['Productivity'],
|
||||
'etherpad': ['Productivity'],
|
||||
'monica': ['Productivity'],
|
||||
'firefly-iii': ['Productivity'],
|
||||
'shlink': ['Productivity'],
|
||||
'yourls': ['Productivity'],
|
||||
'stirling-pdf': ['Productivity'],
|
||||
'syncthing': ['Productivity'],
|
||||
'tandoor': ['Productivity'],
|
||||
'linkwarden': ['Productivity'],
|
||||
'vikunja': ['Productivity'],
|
||||
// Utilities / Backup
|
||||
'duplicati': ['Utilities'],
|
||||
'restic': ['Utilities'],
|
||||
'rsnapshot': ['Utilities'],
|
||||
'mysql-workbench': ['Utilities'],
|
||||
'sqlitebrowser': ['Utilities'],
|
||||
'filezilla': ['Utilities'],
|
||||
'rdesktop': ['Utilities'],
|
||||
'webtop': ['Utilities'],
|
||||
};
|
||||
|
||||
function getCategoriesForApp(name: string): string[] {
|
||||
return LSIO_CATEGORY_MAP[name.toLowerCase()] ?? ['Other'];
|
||||
}
|
||||
|
||||
export class TemplateService {
|
||||
private cachedTemplates: Template[] = [];
|
||||
private lastFetchTime: number = 0;
|
||||
private readonly CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // 24 hours
|
||||
|
||||
public clearCache(): void {
|
||||
this.cachedTemplates = [];
|
||||
this.lastFetchTime = 0;
|
||||
}
|
||||
|
||||
public async getTemplates(): Promise<Template[]> {
|
||||
const now = Date.now();
|
||||
if (this.cachedTemplates.length > 0 && now - this.lastFetchTime < this.CACHE_DURATION_MS) {
|
||||
@@ -73,6 +228,8 @@ export class TemplateService {
|
||||
docs_url: app.readme,
|
||||
architectures: app.arch,
|
||||
stars: app.stars,
|
||||
categories: getCategoriesForApp(app.name),
|
||||
source: 'linuxserver',
|
||||
// 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) => {
|
||||
@@ -91,7 +248,10 @@ export class TemplateService {
|
||||
});
|
||||
} else {
|
||||
// Legacy Portainer v2 Format (Fallback for custom registries)
|
||||
this.cachedTemplates = (response.data.templates || []).filter((t: Template) => !!t.image && t.type === 1);
|
||||
// The Portainer v2 spec includes a native `categories` field — pass it through.
|
||||
this.cachedTemplates = (response.data.templates || [])
|
||||
.filter((t: Template) => !!t.image && t.type === 1)
|
||||
.map((t: Template) => ({ ...t, source: 'custom' }));
|
||||
}
|
||||
|
||||
this.lastFetchTime = now;
|
||||
|
||||
Reference in New Issue
Block a user