Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 62 additions & 20 deletions brand.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,25 @@ const version=document.getElementById('manifest-version');
const brandCount=document.getElementById('brand-count');
const progress=document.getElementById('progress');
const rawPath=document.getElementById('raw-path');
const MANIFEST_TIMEOUT_MS=15000;
let manifest={brands:[],canonicalRawBase:'https://raw.githubusercontent.com/puchadave/assets/main/'};

for(const [id,el] of Object.entries({'brand-grid':brandGrid,'asset-list':assetList,'asset-filter':filter,'manifest-version':version,'brand-count':brandCount,'progress':progress,'raw-path':rawPath})){
if(!el)console.error(`brand.js: missing element #${id}; related portal features are disabled.`);
}

function esc(v=''){return String(v).replace(/[&<>"']/g,m=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[m]));}
function initials(name=''){return name.split(/[\s._-]+/).filter(Boolean).slice(0,2).map(x=>x[0]).join('').toUpperCase()||'•';}
function repoPath(path=''){return `https://github.com/puchadave/assets/tree/main/${path}`;}
function rawAsset(path=''){return `${manifest.canonicalRawBase||'https://raw.githubusercontent.com/puchadave/assets/main/'}${path}`;}
function fallbackMark(b){return `<span class="mark-fallback"><i>${esc(initials(b.name))}</i>${esc(b.name)}</span>`;}

function brands(){return Array.isArray(manifest.brands)?manifest.brands:[];}

function renderBrands(){
brandCount.textContent=manifest.brands.length;
brandGrid.innerHTML=manifest.brands.map(b=>{
if(!brandGrid)return;
if(brandCount)brandCount.textContent=brands().length;
brandGrid.innerHTML=brands().map(b=>{
const primary=(b.assets||[]).find(a=>a.role==='primary'&&a.path);
const mark=primary
? `<img class="brand-logo" src="${esc(rawAsset(primary.path))}" alt="${esc(b.name)} Logo" data-brand-id="${esc(b.id)}">`
Expand All @@ -31,22 +39,25 @@ function renderBrands(){

document.querySelectorAll('.brand-logo').forEach(img=>{
img.addEventListener('error',()=>{
const b=manifest.brands.find(x=>x.id===img.dataset.brandId);
if(b)img.closest('.mark').innerHTML=fallbackMark(b);
const b=brands().find(x=>x.id===img.dataset.brandId);
const mark=img.closest('.mark');
console.warn(`brand.js: logo for "${img.dataset.brandId}" could not be loaded (${img.src}).`);
if(b&&mark)mark.innerHTML=fallbackMark(b);
},{once:true});
});

document.querySelectorAll('[data-brand]').forEach(a=>a.addEventListener('click',()=>{
filter.value=a.dataset.brand;
if(filter)filter.value=a.dataset.brand;
renderAssets(a.dataset.brand);
}));
}

function flattenAssets(){
return manifest.brands.flatMap(b=>(b.assets||[]).map(a=>({...a,brand:b.name,brandId:b.id,base:b.path})));
return brands().flatMap(b=>(b.assets||[]).map(a=>({...a,brand:b.name,brandId:b.id,base:b.path})));
}

function renderAssets(q=''){
if(!assetList)return;
const needle=q.trim().toLowerCase();
const rows=flattenAssets().filter(a=>!needle||[a.brand,a.brandId,a.role,a.format,a.path,a.label].join(' ').toLowerCase().includes(needle));
if(!rows.length){
Expand All @@ -60,33 +71,64 @@ function renderAssets(q=''){
}).join('');
}

async function fetchManifest(){
const r=await fetch(`${MANIFEST_URL}?v=${Date.now()}`,{cache:'no-store',signal:AbortSignal.timeout?.(MANIFEST_TIMEOUT_MS)});
if(!r.ok)throw new Error(`HTTP ${r.status} ${r.statusText} für ${MANIFEST_URL}`);
let data;
try{
data=await r.json();
}catch(e){
throw new Error(`Manifest ist kein gültiges JSON: ${e.message}`,{cause:e});
}
if(!data||!Array.isArray(data.brands))throw new Error('Manifest enthält kein "brands"-Array.');
return data;
}

function showManifestError(e){
const reason=e?.name==='TimeoutError'?`Zeitüberschreitung nach ${MANIFEST_TIMEOUT_MS/1000}s`:e?.message||String(e);
console.error('brand.js: Brand-Manifest konnte nicht geladen werden.',e);
if(version)version.textContent='manifest nicht verfügbar';
if(brandGrid)brandGrid.innerHTML=`<article class="brand-card"><div class="mark"><span class="mark-fallback"><i>!</i>Manifest</span></div><h3>Brand-Manifest nicht verfügbar</h3><p>Die Portalstruktur ist online, aber das kanonische Brand-Manifest konnte nicht geladen werden.</p><p><code>${esc(reason)}</code></p></article>`;
if(assetList)assetList.innerHTML=`<div class="asset-row"><div><strong>Manifest konnte nicht geladen werden.</strong><small>${esc(reason)}</small></div></div>`;
}

async function loadManifest(){
try{
const r=await fetch(`${MANIFEST_URL}?v=${Date.now()}`,{cache:'no-store'});
if(!r.ok)throw new Error(`HTTP ${r.status}`);
manifest=await r.json();
version.textContent=`manifest ${manifest.version||'–'}`;
manifest=await fetchManifest();
}catch(e){
showManifestError(e);
return;
}
try{
if(version)version.textContent=`manifest ${manifest.version||'–'}`;
renderBrands();
renderAssets();
}catch(e){
brandGrid.innerHTML='<article class="brand-card"><div class="mark"><span class="mark-fallback"><i>!</i>Manifest</span></div><h3>Brand-Manifest nicht verfügbar</h3><p>Die Portalstruktur ist online, aber das kanonische Brand-Manifest konnte nicht geladen werden.</p></article>';
assetList.innerHTML='<div class="asset-row"><strong>Manifest konnte nicht geladen werden.</strong></div>';
console.error(e);
showManifestError(new Error(`Manifest konnte nicht gerendert werden: ${e.message}`,{cause:e}));
}
}

filter?.addEventListener('input',e=>renderAssets(e.target.value));
document.getElementById('copy-path')?.addEventListener('click',async e=>{
const button=e.currentTarget;
const reset=()=>setTimeout(()=>{button.textContent='Kopieren';},1600);
try{
if(!rawPath)throw new Error('Element #raw-path fehlt.');
if(!navigator.clipboard)throw new Error('Clipboard-API nicht verfügbar (benötigt HTTPS).');
await navigator.clipboard.writeText(rawPath.textContent);
e.currentTarget.textContent='Kopiert';
setTimeout(()=>e.currentTarget.textContent='Kopieren',1200);
}catch{}
button.textContent='Kopiert';
}catch(err){
console.error('brand.js: Pfad konnte nicht kopiert werden.',err);
button.textContent='Kopieren fehlgeschlagen';
}
reset();
});
window.addEventListener('scroll',()=>{
const h=document.documentElement.scrollHeight-innerHeight;
progress.style.width=`${h>0?(scrollY/h)*100:0}%`;
},{passive:true});
if(progress){
window.addEventListener('scroll',()=>{
const h=document.documentElement.scrollHeight-innerHeight;
progress.style.width=`${h>0?(scrollY/h)*100:0}%`;
},{passive:true});
}
window.addEventListener('pointermove',e=>{
document.documentElement.style.setProperty('--mx',`${e.clientX}px`);
document.documentElement.style.setProperty('--my',`${e.clientY}px`);
Expand Down
94 changes: 69 additions & 25 deletions proxmox/install-webowie-proxmox-branding.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,46 +7,90 @@ PVE_IMAGES="/usr/share/pve-manager/images"
WTK_IMAGES="/usr/share/javascript/proxmox-widget-toolkit/images"
PVE_INDEX="/usr/share/pve-manager/index.html.tpl"
INSTALL="/usr/local/share/webowie-branding"
LOG_FILE="/var/log/webowie-pve-branding.log"
ASSETS=(proxmox_logo.png logo-128.png dd_logo.png favicon.ico proxmox_logo.svg pve-background-1920x1080.png)
log(){ printf '\033[38;2;93;217;232m[webOwie]\033[0m %s\n' "$*"; }
warn(){ echo "WARN: $*" >&2; }
die(){ echo "ERROR: $*" >&2; exit 1; }
trap 'echo "ERROR: ${BASH_SOURCE[0]}: aborted with status $? at line $LINENO: ${BASH_COMMAND}" >&2' ERR
[[ $EUID -eq 0 ]] || die "Run as root."
command -v pveversion >/dev/null || die "Proxmox VE not detected."
command -v curl >/dev/null || die "curl is required."

fetch_assets(){
local stage="$1" f
for f in "${ASSETS[@]}"; do
curl -fsSL --retry 3 --connect-timeout 20 "$RAW/$f" -o "$stage/$f" \
|| die "Could not download $RAW/$f (branch '$BRANCH'). Existing branding was left untouched."
[[ -s "$stage/$f" ]] || die "Downloaded asset is empty: $RAW/$f"
done
}

apply_assets(){
local f
for f in "${ASSETS[@]}"; do
[[ -s "$INSTALL/$f" ]] || die "Brand asset missing or empty: $INSTALL/$f. Run '$0 --install' again."
done
install -m0644 "$INSTALL/proxmox_logo.png" "$PVE_IMAGES/proxmox_logo.png"
install -m0644 "$INSTALL/logo-128.png" "$PVE_IMAGES/logo-128.png"
install -m0644 "$INSTALL/favicon.ico" "$PVE_IMAGES/favicon.ico"
if [[ -e "$PVE_IMAGES/dd_logo.png" ]]; then install -m0644 "$INSTALL/dd_logo.png" "$PVE_IMAGES/dd_logo.png"; fi
if [[ -e "$WTK_IMAGES/proxmox_logo.svg" ]]; then
install -m0644 "$INSTALL/proxmox_logo.svg" "$WTK_IMAGES/proxmox_logo.svg"
else
warn "Widget-toolkit logo not found at $WTK_IMAGES/proxmox_logo.svg; skipping that override."
fi
}

brand_index(){
[[ -f "$PVE_INDEX" ]] || { warn "$PVE_INDEX not found; skipping HTML branding."; return 0; }
grep -q '</head>' "$PVE_INDEX" || die "No </head> found in $PVE_INDEX; HTML branding cannot be injected."
sed -i '/<!-- WEBOWIE-BRANDING-BEGIN -->/,/<!-- WEBOWIE-BRANDING-END -->/d' "$PVE_INDEX"
sed -i -E 's#<title>[^<]*</title>#<title>webOwie Infrastructure Control Plane · puchalla.it.com</title>#' "$PVE_INDEX"
sed -i '/<\/head>/i\
<!-- WEBOWIE-BRANDING-BEGIN -->\
<meta name="theme-color" content="#000000">\
<style id="webowie-branding">body:after{content:"webOwie · puchalla.it.com · Powered by Proxmox VE";position:fixed;right:14px;bottom:8px;z-index:2147483647;pointer-events:none;font:11px Arial,sans-serif;letter-spacing:.7px;color:#5DD9E8;opacity:.48}</style>\
<!-- WEBOWIE-BRANDING-END -->' "$PVE_INDEX"
grep -q 'WEBOWIE-BRANDING-BEGIN' "$PVE_INDEX" || die "HTML branding block was not written to $PVE_INDEX."
}

case "${1:---install}" in
--install)
[[ -f "$0" && -r "$0" ]] || die "Cannot self-install: run the script from a file, not from a pipe (curl -fsSL ... -o install.sh && bash install.sh)."
stamp="$(date +%Y%m%d-%H%M%S)"; backup="$BACKUP_ROOT/$stamp"; mkdir -p "$backup" "$INSTALL"
for f in "$PVE_IMAGES/proxmox_logo.png" "$PVE_IMAGES/logo-128.png" "$PVE_IMAGES/favicon.ico" "$PVE_IMAGES/dd_logo.png" "$WTK_IMAGES/proxmox_logo.svg" "$PVE_INDEX"; do [[ -f "$f" ]] || continue; mkdir -p "$backup$(dirname "$f")"; cp -a "$f" "$backup$f"; done
printf '%s\n' "$backup" >"$BACKUP_ROOT/.last"
for f in proxmox_logo.png logo-128.png dd_logo.png favicon.ico proxmox_logo.svg pve-background-1920x1080.png; do curl -fsSL "$RAW/$f" -o "$INSTALL/$f"; done
install -m0644 "$INSTALL/proxmox_logo.png" "$PVE_IMAGES/proxmox_logo.png"
install -m0644 "$INSTALL/logo-128.png" "$PVE_IMAGES/logo-128.png"
install -m0644 "$INSTALL/favicon.ico" "$PVE_IMAGES/favicon.ico"
[[ -e "$PVE_IMAGES/dd_logo.png" ]] && install -m0644 "$INSTALL/dd_logo.png" "$PVE_IMAGES/dd_logo.png"
[[ -e "$WTK_IMAGES/proxmox_logo.svg" ]] && install -m0644 "$INSTALL/proxmox_logo.svg" "$WTK_IMAGES/proxmox_logo.svg"
stage="$(mktemp -d)"; trap 'rm -rf "$stage"' EXIT
fetch_assets "$stage"
for f in "${ASSETS[@]}"; do install -m0644 "$stage/$f" "$INSTALL/$f"; done
apply_assets
install -m0644 "$INSTALL/pve-background-1920x1080.png" "$PVE_IMAGES/webowie-background.png"
if [[ -f "$PVE_INDEX" ]]; then
sed -i '/<!-- WEBOWIE-BRANDING-BEGIN -->/,/<!-- WEBOWIE-BRANDING-END -->/d' "$PVE_INDEX"
sed -i -E 's#<title>[^<]*</title>#<title>webOwie Infrastructure Control Plane · puchalla.it.com</title>#' "$PVE_INDEX"
sed -i '/<\/head>/i\
<!-- WEBOWIE-BRANDING-BEGIN -->\
<meta name="theme-color" content="#000000">\
<style id="webowie-branding">body:after{content:"webOwie · puchalla.it.com · Powered by Proxmox VE";position:fixed;right:14px;bottom:8px;z-index:2147483647;pointer-events:none;font:11px Arial,sans-serif;letter-spacing:.7px;color:#5DD9E8;opacity:.48}</style>\
<!-- WEBOWIE-BRANDING-END -->' "$PVE_INDEX"
fi
brand_index
install -m0755 "$0" /usr/local/sbin/webowie-pve-branding
cat >/etc/apt/apt.conf.d/99-webowie-branding <<'EOF'
DPkg::Post-Invoke { "if [ -x /usr/local/sbin/webowie-pve-branding ]; then /usr/local/sbin/webowie-pve-branding --reapply >/dev/null 2>&1 || true; fi"; };
cat >/etc/apt/apt.conf.d/99-webowie-branding <<EOF
DPkg::Post-Invoke { "if [ -x /usr/local/sbin/webowie-pve-branding ]; then /usr/local/sbin/webowie-pve-branding --reapply >>${LOG_FILE} 2>&1 || echo \"\$(date -Is) webowie-pve-branding --reapply failed\" >>${LOG_FILE}; fi"; };
EOF
systemctl restart pveproxy.service; log "Installed. Backup: $backup" ;;
systemctl restart pveproxy.service || die "Branding installed, but restarting pveproxy.service failed. Check 'systemctl status pveproxy.service'."
log "Installed. Backup: $backup" ;;
--reapply)
[[ -d "$INSTALL" ]] || die "Brand assets are not installed."
install -m0644 "$INSTALL/proxmox_logo.png" "$PVE_IMAGES/proxmox_logo.png"; install -m0644 "$INSTALL/logo-128.png" "$PVE_IMAGES/logo-128.png"; install -m0644 "$INSTALL/favicon.ico" "$PVE_IMAGES/favicon.ico"
[[ -e "$PVE_IMAGES/dd_logo.png" ]] && install -m0644 "$INSTALL/dd_logo.png" "$PVE_IMAGES/dd_logo.png"
[[ -e "$WTK_IMAGES/proxmox_logo.svg" ]] && install -m0644 "$INSTALL/proxmox_logo.svg" "$WTK_IMAGES/proxmox_logo.svg" ;;
apply_assets ;;
--restore)
backup="$(cat "$BACKUP_ROOT/.last" 2>/dev/null || true)"; [[ -d "$backup" ]] || die "No backup found."
while IFS= read -r -d '' src; do dst="${src#$backup}"; mkdir -p "$(dirname "$dst")"; cp -a "$src" "$dst"; done < <(find "$backup" -type f -print0)
rm -f /etc/apt/apt.conf.d/99-webowie-branding; systemctl restart pveproxy.service; log "Restored $backup" ;;
--status) echo "webOwie PVE Branding"; echo "Asset branch: $BRANCH"; echo "Source: $RAW"; pveversion | head -1 ;;
backup="$(cat "$BACKUP_ROOT/.last" 2>/dev/null || true)"
[[ -n "$backup" ]] || die "No backup marker at $BACKUP_ROOT/.last."
[[ -d "$backup" ]] || die "Recorded backup directory is missing: $backup"
restored=0
while IFS= read -r -d '' src; do dst="${src#"$backup"}"; mkdir -p "$(dirname "$dst")"; cp -a "$src" "$dst"; restored=$((restored+1)); done < <(find "$backup" -type f -print0)
[[ $restored -gt 0 ]] || die "Backup $backup contains no files; nothing was restored."
rm -f /etc/apt/apt.conf.d/99-webowie-branding
systemctl restart pveproxy.service || die "Files restored from $backup, but restarting pveproxy.service failed."
log "Restored $backup ($restored files)" ;;
--status)
echo "webOwie PVE Branding"; echo "Asset branch: $BRANCH"; echo "Source: $RAW"
if [[ -d "$INSTALL" ]]; then echo "Local assets: $INSTALL"; else echo "Local assets: not installed"; fi
version="$(pveversion || true)"
[[ -n "$version" ]] || die "pveversion returned no output."
printf '%s\n' "$version" | head -n1 ;;
*) die "Usage: $0 [--install|--reapply|--restore|--status]" ;;
esac
Loading