Merge claude/2
This commit is contained in:
commit
58541b1fe3
@ -310,6 +310,19 @@ class AppsManager {
|
||||
return !(app.config || {})[`CFG_${slug.toUpperCase()}_INSTANCE_OF`];
|
||||
});
|
||||
|
||||
// Developer-mode-only apps (CFG_<APP>_DEV_ONLY=true — operator-grade
|
||||
// things like the marketplace server) stay out of the grid unless dev
|
||||
// mode is on. An installed one always shows: never hide something
|
||||
// that's running.
|
||||
const devModeOn = String(window.systemConfigs?.CFG_DEV_MODE) === 'true';
|
||||
if (!devModeOn) {
|
||||
filteredApps = filteredApps.filter(app => {
|
||||
const slug = (app.command || '').split(' ').pop();
|
||||
const devOnly = String((app.config || {})[`CFG_${slug.toUpperCase()}_DEV_ONLY`] || '').toLowerCase() === 'true';
|
||||
return !devOnly || app.installed;
|
||||
});
|
||||
}
|
||||
|
||||
if (category === 'installed') {
|
||||
filteredApps = filteredApps.filter(app => app.installed);
|
||||
} else if (category !== 'all') {
|
||||
|
||||
41
containers/marketplace/docker-compose.yml
Normal file
41
containers/marketplace/docker-compose.yml
Normal file
@ -0,0 +1,41 @@
|
||||
networks:
|
||||
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
|
||||
external: true
|
||||
|
||||
services:
|
||||
marketplace-service: #LIBREPORTAL|SERVICE_TAG_1|marketplace-service
|
||||
container_name: marketplace-service
|
||||
image: nginx:alpine
|
||||
restart: unless-stopped
|
||||
hostname: marketplace
|
||||
# GLUETUN_OFF_BEGIN
|
||||
ports:
|
||||
- "PORTS_DATA_1" #LIBREPORTAL|PORTS_TAG_1|PORTS_DATA_1
|
||||
# GLUETUN_OFF_END
|
||||
labels:
|
||||
libreportal.category: "CATEGORY_DATA" #LIBREPORTAL|CATEGORY_TAG|CATEGORY_DATA
|
||||
libreportal.title: "TITLE_DATA" #LIBREPORTAL|TITLE_TAG|TITLE_DATA
|
||||
libreportal.backup.files: "data"
|
||||
traefik.enable: TRAEFIK_ENABLE_DATA #LIBREPORTAL|TRAEFIK_ENABLE_TAG|TRAEFIK_ENABLE_DATA
|
||||
# TRAEFIK_PORT_1_BEGIN
|
||||
traefik.http.routers.marketplace-service.entrypoints: web,websecure
|
||||
traefik.http.routers.marketplace-service.rule: Host(`DOMAINSUBNAME_DATA_1`) #LIBREPORTAL|DOMAINSUBNAME_TAG_1|DOMAINSUBNAME_DATA_1
|
||||
traefik.http.routers.marketplace-service.tls: true
|
||||
traefik.http.routers.marketplace-service.tls.certresolver: production
|
||||
traefik.http.services.marketplace-service.loadbalancer.server.port: PORT_INTERNAL_DATA_1 #LIBREPORTAL|PORT_INTERNAL_TAG_1|PORT_INTERNAL_DATA_1
|
||||
traefik.http.routers.marketplace-service.middlewares: MIDDLEWARE_DATA_1 #LIBREPORTAL|MIDDLEWARE_TAG_1|MIDDLEWARE_DATA_1
|
||||
# TRAEFIK_PORT_1_END
|
||||
healthcheck:
|
||||
disable: HEALTHCHECK_DATA #LIBREPORTAL|HEALTHCHECK_TAG|HEALTHCHECK_DATA
|
||||
volumes:
|
||||
- SOCKET_DATA #LIBREPORTAL|SOCKET_TAG|SOCKET_DATA
|
||||
- ./data:/usr/share/nginx/html:ro
|
||||
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
# GLUETUN_OFF_BEGIN
|
||||
networks:
|
||||
DOCKER_NETWORK_DATA: #LIBREPORTAL|DOCKER_NETWORK_TAG|DOCKER_NETWORK_DATA
|
||||
ipv4_address: IP_DATA_1 #LIBREPORTAL|IP_TAG_1|IP_DATA_1
|
||||
# GLUETUN_OFF_END
|
||||
# GLUETUN_ON_BEGIN
|
||||
# network_mode: "container:gluetun-service"
|
||||
# GLUETUN_ON_END
|
||||
63
containers/marketplace/marketplace.config
Normal file
63
containers/marketplace/marketplace.config
Normal file
@ -0,0 +1,63 @@
|
||||
#
|
||||
# =============================================================================
|
||||
# GENERAL CONFIGURATION
|
||||
# =============================================================================
|
||||
# APP_NAME = name of application for use in scripts
|
||||
# COMPOSE_FILE = default for no app_name in docker-compose file name, app if there is
|
||||
# BACKUP = if true, include this application in backup operations
|
||||
# HEALTHCHECK = if true, default docker health checks for that container will be enabled
|
||||
# AUTHELIA = if true, use Authelia authentication, if false turned off.
|
||||
# HEADSCALE = options : false, local, remote (see general config). e.g false or local,remote
|
||||
# DEV_ONLY = if true, hidden from the App Center unless Developer Mode is on
|
||||
#
|
||||
CFG_MARKETPLACE_APP_NAME=marketplace
|
||||
CFG_MARKETPLACE_BACKUP=true
|
||||
CFG_MARKETPLACE_BACKUP_STRATEGY=auto
|
||||
CFG_MARKETPLACE_COMPOSE_FILE=default
|
||||
CFG_MARKETPLACE_HEALTHCHECK=false
|
||||
CFG_MARKETPLACE_AUTHELIA=false
|
||||
CFG_MARKETPLACE_HEADSCALE=false
|
||||
CFG_MARKETPLACE_DEV_ONLY=true
|
||||
#
|
||||
# =============================================================================
|
||||
# METADATA
|
||||
# =============================================================================
|
||||
# CATEGORY = application category for grouping
|
||||
# TITLE = display name for the application
|
||||
# DESCRIPTION = short description of the application
|
||||
# LONG_DESCRIPTION = detailed description of the application
|
||||
# URL = source repository or documentation URL
|
||||
# ACTIONS = available actions for this application
|
||||
#
|
||||
CFG_MARKETPLACE_CATEGORY="system"
|
||||
CFG_MARKETPLACE_TITLE="LibrePortal Marketplace"
|
||||
CFG_MARKETPLACE_DESCRIPTION="Host your own app catalog and registry"
|
||||
CFG_MARKETPLACE_LONG_DESCRIPTION="Serves a signed LibrePortal artifact channel (apps, hotfixes, releases) plus a browsable marketplace website over it. The official marketplace runs this exact app; point any box's CFG_RELEASE_BASE_URL at your instance to use it"
|
||||
CFG_MARKETPLACE_URL="https://github.com/Webstar/LibrePortal"
|
||||
CFG_MARKETPLACE_ACTIONS="configure|install|restart|shutdown|uninstall"
|
||||
#
|
||||
# =============================================================================
|
||||
# NETWORK CONFIGURATION
|
||||
# =============================================================================
|
||||
# DOMAIN = number of domain from the general config, useful when using multiple domains
|
||||
# WHITELIST = if true only allow whitelisted ips on traefik, if false allow all
|
||||
#
|
||||
CFG_MARKETPLACE_DOMAIN=1
|
||||
CFG_MARKETPLACE_WHITELIST=false
|
||||
CFG_MARKETPLACE_NETWORK=default
|
||||
#
|
||||
# =============================================================================
|
||||
# PORT CONFIGURATION
|
||||
# =============================================================================
|
||||
# PORT_ = port configuration: app|name|external:internal|access|protocol|login|traefik|webui|description
|
||||
# - app: application name
|
||||
# - name: service identifier (webui, api, ssh, etc.)
|
||||
# - external:internal: port mapping (external can be 'random' for auto-allocation)
|
||||
# - access: 'public' (internet accessible), 'private' (local network only), 'disabled' (not running)
|
||||
# - protocol: 'tcp' or 'udp'
|
||||
# - login: if true, this port requires basic-auth via Traefik (only meaningful when traefik=true)
|
||||
# - traefik: if true, Traefik handles this port (reverse proxy)
|
||||
# - webui: if true, this port serves the main web interface
|
||||
# - description: human-readable description of the service
|
||||
#
|
||||
CFG_MARKETPLACE_PORT_1="marketplace-service|webui|random:80|public|tcp|false|true|true|Marketplace|/|marketplace"
|
||||
22
containers/marketplace/marketplace.svg
Normal file
22
containers/marketplace/marketplace.svg
Normal file
@ -0,0 +1,22 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<defs>
|
||||
<linearGradient id="mkbg" x1="0" y1="0" x2="1" y2="1">
|
||||
<stop offset="0" stop-color="#6366f1"/>
|
||||
<stop offset="1" stop-color="#22d3ee"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect x="4" y="4" width="56" height="56" rx="14" fill="url(#mkbg)"/>
|
||||
<!-- storefront awning -->
|
||||
<path d="M14 22 L18 13 h28 l4 9 z" fill="#ffffff" opacity="0.95"/>
|
||||
<path d="M14 22 h9 v4a4.5 4.5 0 0 1 -9 0 z" fill="#e0e7ff"/>
|
||||
<path d="M23 22 h9 v4a4.5 4.5 0 0 1 -9 0 z" fill="#ffffff"/>
|
||||
<path d="M32 22 h9 v4a4.5 4.5 0 0 1 -9 0 z" fill="#e0e7ff"/>
|
||||
<path d="M41 22 h9 v4a4.5 4.5 0 0 1 -9 0 z" fill="#ffffff"/>
|
||||
<!-- shop body -->
|
||||
<rect x="17" y="30" width="30" height="21" rx="2" fill="#ffffff" opacity="0.95"/>
|
||||
<!-- door -->
|
||||
<rect x="22" y="36" width="8" height="15" rx="1" fill="#4f46e5"/>
|
||||
<!-- window with check (signed) -->
|
||||
<rect x="34" y="36" width="9" height="8" rx="1" fill="#a5f3fc"/>
|
||||
<path d="M35.8 40 l1.7 1.7 l3 -3.4" stroke="#0e7490" stroke-width="1.6" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
38
containers/marketplace/nginx.conf
Normal file
38
containers/marketplace/nginx.conf
Normal file
@ -0,0 +1,38 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# The bootstrap installer: served as a shell script, never cached, so
|
||||
# `curl … | sudo bash` always gets the current one.
|
||||
location = /install.sh {
|
||||
default_type text/x-shellscript;
|
||||
add_header Cache-Control "no-cache, must-revalidate";
|
||||
}
|
||||
|
||||
# The signed catalog + channel manifests move on every publish — no cache.
|
||||
location ~ /index\.json(\.minisig)?$ {
|
||||
default_type application/json;
|
||||
add_header Cache-Control "no-cache, must-revalidate";
|
||||
}
|
||||
location ~ /latest\.json$ {
|
||||
default_type application/json;
|
||||
add_header Cache-Control "no-cache, must-revalidate";
|
||||
}
|
||||
|
||||
# Payloads keep their name across re-publishes (same-id upsert), so they
|
||||
# are NOT immutable — short revalidating cache; boxes verify the sha256
|
||||
# pinned in the fresh index anyway.
|
||||
location ~ /payloads/ {
|
||||
add_header Cache-Control "public, max-age=300, must-revalidate";
|
||||
}
|
||||
|
||||
# Version-pinned release artifacts never change — cache hard.
|
||||
location ~ \.tar\.gz$ { default_type application/gzip; add_header Cache-Control "public, max-age=31536000, immutable"; }
|
||||
location ~ \.sha256$ { default_type text/plain; add_header Cache-Control "public, max-age=31536000, immutable"; }
|
||||
location ~ \.minisig$ { default_type text/plain; add_header Cache-Control "public, max-age=31536000, immutable"; }
|
||||
|
||||
# The browse UI.
|
||||
location / { try_files $uri $uri/ =404; }
|
||||
}
|
||||
212
containers/marketplace/resources/site/index.html
Normal file
212
containers/marketplace/resources/site/index.html
Normal file
@ -0,0 +1,212 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>LibrePortal Marketplace</title>
|
||||
<link rel="icon" type="image/svg+xml" href="marketplace.svg">
|
||||
<style>
|
||||
/* Self-contained by design: no external fonts, scripts or images — the same
|
||||
no-third-party rule as the LibrePortal WebUI. */
|
||||
:root {
|
||||
--bg1:#0b1e3a; --bg2:#0e3a5c; --card:rgba(255,255,255,.06);
|
||||
--border:rgba(255,255,255,.14); --text:#e8f0fb; --dim:#9db4cc;
|
||||
--indigo:#6366f1; --indigo-soft:rgba(99,102,241,.28);
|
||||
--teal:#22d3ee; --teal-soft:rgba(34,211,238,.20);
|
||||
}
|
||||
* { box-sizing:border-box; margin:0; }
|
||||
body {
|
||||
font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
||||
color:var(--text); min-height:100vh;
|
||||
background: radial-gradient(1200px 700px at 15% -10%, #1c4d7a 0%, transparent 60%),
|
||||
linear-gradient(150deg, var(--bg1), var(--bg2));
|
||||
}
|
||||
.wrap { max-width:1080px; margin:0 auto; padding:32px 20px 64px; }
|
||||
header { display:flex; align-items:center; gap:14px; margin-bottom:6px; }
|
||||
header img { width:44px; height:44px; }
|
||||
h1 { font-size:1.7rem; letter-spacing:.3px; }
|
||||
.tagline { color:var(--dim); margin:4px 0 26px; }
|
||||
.bar { display:flex; gap:10px; flex-wrap:wrap; margin-bottom:18px; }
|
||||
.bar input {
|
||||
flex:1; min-width:220px; padding:10px 14px; border-radius:10px;
|
||||
border:1px solid var(--border); background:var(--card); color:var(--text);
|
||||
font-size:.95rem; outline:none;
|
||||
}
|
||||
.bar input:focus { border-color:var(--indigo); }
|
||||
.chips { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:22px; }
|
||||
.chip {
|
||||
padding:5px 12px; border-radius:999px; font-size:.8rem; cursor:pointer;
|
||||
border:1px solid var(--border); background:var(--card); color:var(--dim);
|
||||
}
|
||||
.chip.on { background:var(--indigo-soft); border-color:var(--indigo); color:#c7d2fe; }
|
||||
.grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(300px,1fr)); gap:16px; }
|
||||
.card {
|
||||
background:var(--card); border:1px solid var(--border); border-radius:14px;
|
||||
padding:18px; display:flex; flex-direction:column; gap:10px;
|
||||
}
|
||||
.card .top { display:flex; gap:12px; align-items:center; }
|
||||
.card .icon {
|
||||
width:44px; height:44px; border-radius:10px; background:rgba(255,255,255,.09);
|
||||
display:flex; align-items:center; justify-content:center; overflow:hidden;
|
||||
font-weight:700; font-size:1.2rem; color:#c7d2fe; flex-shrink:0;
|
||||
}
|
||||
.card .icon img { width:100%; height:100%; object-fit:contain; }
|
||||
.card h2 { font-size:1.05rem; }
|
||||
.tags { display:flex; gap:6px; flex-wrap:wrap; }
|
||||
.tag { font-size:.72rem; padding:2px 9px; border-radius:999px; border:1px solid var(--border); color:var(--dim); }
|
||||
.tag.official { background:var(--teal-soft); border-color:rgba(34,211,238,.5); color:#a5f3fc; }
|
||||
.tag.official::before { content:"✓ "; }
|
||||
.tag.community { background:rgba(245,158,11,.2); border-color:rgba(245,158,11,.5); color:#fcd34d; }
|
||||
.desc { color:var(--dim); font-size:.88rem; line-height:1.45; flex:1; }
|
||||
.addline { display:flex; gap:8px; align-items:center; }
|
||||
.addline code {
|
||||
flex:1; font-size:.78rem; padding:8px 10px; border-radius:8px; overflow-x:auto;
|
||||
background:rgba(0,0,0,.30); border:1px solid var(--border); white-space:nowrap;
|
||||
}
|
||||
.addline button {
|
||||
padding:8px 14px; border-radius:8px; border:none; cursor:pointer;
|
||||
background:var(--indigo); color:#fff; font-weight:600; font-size:.82rem;
|
||||
}
|
||||
.addline button:hover { background:#4f46e5; }
|
||||
.empty, .meta { color:var(--dim); font-size:.9rem; }
|
||||
.meta { margin:14px 2px 0; }
|
||||
footer { margin-top:44px; padding-top:18px; border-top:1px solid var(--border); color:var(--dim); font-size:.85rem; line-height:1.6; }
|
||||
footer code { background:rgba(0,0,0,.3); padding:1px 6px; border-radius:6px; }
|
||||
a { color:#93c5fd; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<header>
|
||||
<img src="marketplace.svg" alt="">
|
||||
<h1>LibrePortal Marketplace</h1>
|
||||
</header>
|
||||
<p class="tagline">A signed, self-hostable app catalog — browse here, install from your own LibrePortal.</p>
|
||||
|
||||
<div class="bar"><input id="q" type="search" placeholder="Search apps…" autocomplete="off"></div>
|
||||
<div class="chips" id="chips"></div>
|
||||
<div class="grid" id="grid"></div>
|
||||
<p class="empty" id="empty" hidden>Nothing published on this channel yet.</p>
|
||||
<p class="meta" id="meta"></p>
|
||||
|
||||
<footer>
|
||||
<p><strong>Add an app:</strong> copy its command into your LibrePortal box's terminal, or find it
|
||||
on your App Center's grid as an “Available” card once your box scans this catalog.</p>
|
||||
<p><strong>Use this marketplace from your box:</strong> set <code>CFG_RELEASE_BASE_URL</code> to this
|
||||
site's address. <strong>Run your own:</strong> this whole site is the open-source
|
||||
<code>marketplace</code> app that ships with LibrePortal (Developer Mode) — publish with
|
||||
<code>make_app.sh</code> and serve the same signed files. Boxes only ever trust the minisign
|
||||
signature on the catalog, never this website.</p>
|
||||
</footer>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
'use strict';
|
||||
var CHANNELS = ['stable', 'edge'];
|
||||
var state = { apps: [], cat: 'all', q: '' };
|
||||
var grid = document.getElementById('grid');
|
||||
var chips = document.getElementById('chips');
|
||||
var empty = document.getElementById('empty');
|
||||
var meta = document.getElementById('meta');
|
||||
|
||||
function esc(s) {
|
||||
return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||
});
|
||||
}
|
||||
|
||||
function render() {
|
||||
var q = state.q.toLowerCase();
|
||||
var shown = state.apps.filter(function (a) {
|
||||
if (state.cat !== 'all' && a.category !== state.cat) return false;
|
||||
if (q && (a.title + ' ' + a.description + ' ' + a.long_description + ' ' + a.slug).toLowerCase().indexOf(q) < 0) return false;
|
||||
return true;
|
||||
});
|
||||
grid.innerHTML = shown.map(function (a) {
|
||||
var iconInner = a.icon
|
||||
? '<img src="' + esc(a.icon) + '" alt="" onerror="this.parentNode.textContent=' + "'" + esc((a.title || '?')[0].toUpperCase()) + "'" + '">'
|
||||
: esc((a.title || '?')[0].toUpperCase());
|
||||
var trust = a.trust === 'official'
|
||||
? '<span class="tag official">Official</span>'
|
||||
: '<span class="tag community">' + esc(a.trust) + '</span>';
|
||||
return '<div class="card">' +
|
||||
'<div class="top"><div class="icon">' + iconInner + '</div><div>' +
|
||||
'<h2>' + esc(a.title) + '</h2>' +
|
||||
'<div class="tags"><span class="tag">' + esc(a.category || 'app') + '</span>' + trust +
|
||||
'<span class="tag">v' + esc(a.version) + '</span></div>' +
|
||||
'</div></div>' +
|
||||
'<p class="desc">' + esc(a.long_description || a.description) + '</p>' +
|
||||
'<div class="addline"><code>libreportal app add ' + esc(a.slug) + '</code>' +
|
||||
'<button data-slug="' + esc(a.slug) + '">Copy</button></div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
empty.hidden = shown.length > 0;
|
||||
}
|
||||
|
||||
function renderChips(cats) {
|
||||
var all = ['all'].concat(cats);
|
||||
chips.innerHTML = all.map(function (c) {
|
||||
return '<span class="chip' + (c === state.cat ? ' on' : '') + '" data-cat="' + esc(c) + '">' + esc(c) + '</span>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function load(i) {
|
||||
i = i || 0;
|
||||
if (i >= CHANNELS.length) { empty.hidden = false; return; }
|
||||
var ch = CHANNELS[i];
|
||||
fetch(ch + '/index.json', { cache: 'no-store' })
|
||||
.then(function (r) { if (!r.ok) throw 0; return r.json(); })
|
||||
.then(function (idx) {
|
||||
state.apps = (idx.artifacts || []).filter(function (a) {
|
||||
return a && a.type === 'app' && a.payload && a.payload.kind === 'bundle' &&
|
||||
a.applies_when && a.applies_when.app;
|
||||
}).map(function (a) {
|
||||
var m = a.meta || {};
|
||||
return {
|
||||
slug: a.applies_when.app, title: a.title || a.applies_when.app,
|
||||
description: m.description || a.why || '',
|
||||
long_description: m.long_description || '',
|
||||
category: (m.category || '').toLowerCase(),
|
||||
trust: a.trust || 'official', version: a.version || 1,
|
||||
icon: m.icon ? ch + '/payloads/icons/' + a.applies_when.app + '.' + m.icon.split('.').pop() : null
|
||||
};
|
||||
});
|
||||
var cats = [];
|
||||
state.apps.forEach(function (a) { if (a.category && cats.indexOf(a.category) < 0) cats.push(a.category); });
|
||||
renderChips(cats.sort());
|
||||
meta.textContent = state.apps.length + ' app(s) · channel: ' + ch + ' · catalog serial ' + (idx.index_serial || '?') +
|
||||
' · published ' + (idx.generated_at || '?');
|
||||
render();
|
||||
})
|
||||
.catch(function () { load(i + 1); });
|
||||
}
|
||||
|
||||
chips.addEventListener('click', function (e) {
|
||||
var c = e.target.getAttribute && e.target.getAttribute('data-cat');
|
||||
if (!c) return;
|
||||
state.cat = c;
|
||||
renderChips(state.apps.reduce(function (acc, a) {
|
||||
if (a.category && acc.indexOf(a.category) < 0) acc.push(a.category);
|
||||
return acc;
|
||||
}, []).sort());
|
||||
render();
|
||||
});
|
||||
grid.addEventListener('click', function (e) {
|
||||
var slug = e.target.getAttribute && e.target.getAttribute('data-slug');
|
||||
if (!slug) return;
|
||||
var txt = 'libreportal app add ' + slug;
|
||||
(navigator.clipboard ? navigator.clipboard.writeText(txt) : Promise.reject())
|
||||
.then(function () { e.target.textContent = 'Copied!'; })
|
||||
.catch(function () { e.target.textContent = txt; })
|
||||
.finally ? null : null;
|
||||
setTimeout(function () { e.target.textContent = 'Copy'; }, 1600);
|
||||
});
|
||||
document.getElementById('q').addEventListener('input', function (e) {
|
||||
state.q = e.target.value; render();
|
||||
});
|
||||
|
||||
load();
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
18
containers/marketplace/scripts/marketplace_install_hooks.sh
Normal file
18
containers/marketplace/scripts/marketplace_install_hooks.sh
Normal file
@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Marketplace install hooks — seed the served docroot. The generic installApp
|
||||
# driver handles compose/start; this converges the browse-UI files on every
|
||||
# (re)install (always refreshed from the definition), while published channel
|
||||
# data (stable/, edge/ — the operator's signed catalog) is never touched.
|
||||
|
||||
marketplace_install_post_setup()
|
||||
{
|
||||
local app_name="$1"
|
||||
local src="${install_containers_dir%/}/$app_name/resources/site"
|
||||
local dest="$containers_dir$app_name/data"
|
||||
|
||||
runFileOp mkdir -p "$dest" || return 1
|
||||
runFileOp cp -f "$src/index.html" "$dest/index.html" || return 1
|
||||
runFileOp cp -f "${install_containers_dir%/}/$app_name/$app_name.svg" "$dest/marketplace.svg" || return 1
|
||||
isSuccessful "Marketplace site seeded. Publish a catalog into it: rsync dist/<channel>/ $dest/<channel>/"
|
||||
}
|
||||
@ -632,6 +632,7 @@ declare -gA LP_FN_MAP=(
|
||||
[manifestReadFromSnapshot]="backup/manifest/manifest_read.sh"
|
||||
[manifestRemove]="backup/manifest/manifest_write.sh"
|
||||
[manifestWrite]="backup/manifest/manifest_write.sh"
|
||||
[marketplace_install_post_setup]="marketplace/scripts/marketplace_install_hooks.sh"
|
||||
[mattermostToolsMenu]="menu/tools/manage_mattermost.sh"
|
||||
[maybeRegenPoll]="task/crontab_task_processor.sh"
|
||||
[menuContinue]="menu/message/continue.sh"
|
||||
@ -1598,6 +1599,7 @@ declare -gA LP_FN_ROOT=(
|
||||
[manifestReadFromSnapshot]="scripts"
|
||||
[manifestRemove]="scripts"
|
||||
[manifestWrite]="scripts"
|
||||
[marketplace_install_post_setup]="containers"
|
||||
[mattermostToolsMenu]="scripts"
|
||||
[maybeRegenPoll]="scripts"
|
||||
[menuContinue]="scripts"
|
||||
@ -2585,6 +2587,7 @@ manifestReadField() { source "${install_scripts_dir}backup/manifest/manifest_rea
|
||||
manifestReadFromSnapshot() { source "${install_scripts_dir}backup/manifest/manifest_read.sh"; manifestReadFromSnapshot "$@"; }
|
||||
manifestRemove() { source "${install_scripts_dir}backup/manifest/manifest_write.sh"; manifestRemove "$@"; }
|
||||
manifestWrite() { source "${install_scripts_dir}backup/manifest/manifest_write.sh"; manifestWrite "$@"; }
|
||||
marketplace_install_post_setup() { source "${install_containers_dir}marketplace/scripts/marketplace_install_hooks.sh"; marketplace_install_post_setup "$@"; }
|
||||
mattermostToolsMenu() { source "${install_scripts_dir}menu/tools/manage_mattermost.sh"; mattermostToolsMenu "$@"; }
|
||||
maybeRegenPoll() { source "${install_scripts_dir}task/crontab_task_processor.sh"; maybeRegenPoll "$@"; }
|
||||
menuContinue() { source "${install_scripts_dir}menu/message/continue.sh"; menuContinue "$@"; }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user