Initial commit: Services homepage
5
Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
FROM nginx:alpine
|
||||||
|
LABEL maintainer="services-homepage"
|
||||||
|
COPY . /usr/share/nginx/html
|
||||||
|
EXPOSE 80
|
||||||
|
CMD ["nginx", "-g", "daemon off;"]
|
||||||
13
docker-compose.yml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
services-homepage:
|
||||||
|
build: .
|
||||||
|
container_name: services-homepage
|
||||||
|
ports:
|
||||||
|
- "8088:80"
|
||||||
|
restart: unless-stopped
|
||||||
|
logging:
|
||||||
|
driver: json-file
|
||||||
|
options:
|
||||||
|
max-size: "5m"
|
||||||
|
max-file: "2"
|
||||||
105
index.html
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
|
<title>Services Homepage</title>
|
||||||
|
<link rel="stylesheet" href="/styles.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>My Services</h1>
|
||||||
|
<p class="subtitle">Quick links to the commonly used containers on this host</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<section class="grid" id="services-grid">
|
||||||
|
<!-- Services will be populated dynamically from /services.xml -->
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="notes">
|
||||||
|
<h2>Notes</h2>
|
||||||
|
<ul>
|
||||||
|
<li>If any service is behind a reverse proxy or uses host networking, the path/host may differ.</li>
|
||||||
|
<li>Edit <code>services.xml</code> in this repo to add/remove links.</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<small>Generated: static homepage — served by nginx in a container. Edit and rebuild to update.</small>
|
||||||
|
</footer>
|
||||||
|
<script>
|
||||||
|
// Fetch services.xml and render the service cards with logos.
|
||||||
|
(async function(){
|
||||||
|
const grid = document.getElementById('services-grid');
|
||||||
|
const host = window.location.hostname;
|
||||||
|
try{
|
||||||
|
const res = await fetch('/services.xml', {cache: 'no-cache'});
|
||||||
|
if(!res.ok){ throw new Error('Failed to load services.xml'); }
|
||||||
|
const text = await res.text();
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString(text, 'application/xml');
|
||||||
|
const services = Array.from(doc.getElementsByTagName('service'));
|
||||||
|
if(services.length===0){ grid.innerHTML = '<p class="notes">No services found in services.xml</p>'; return; }
|
||||||
|
services.forEach(s=>{
|
||||||
|
const name = s.getAttribute('name') || s.getAttribute('id') || 'unknown';
|
||||||
|
const proto = s.getAttribute('proto') || 'http';
|
||||||
|
const port = s.getAttribute('port') || '';
|
||||||
|
const logo = s.getAttribute('logo') || '';
|
||||||
|
const hostAttr = s.getAttribute('host'); // optional public hostname or full URL
|
||||||
|
|
||||||
|
// Build href: prefer explicit host attribute when present.
|
||||||
|
// Rules:
|
||||||
|
// - If hostAttr starts with http(s)://, use it as-is.
|
||||||
|
// - If hostAttr is a hostname (no protocol) and does NOT include a port, prefer HTTPS and DO NOT append the service port.
|
||||||
|
// - If hostAttr includes a port (example.com:8096), use it as provided (no extra port appended).
|
||||||
|
// - If no hostAttr, fall back to current page hostname and use the service port.
|
||||||
|
let href = '';
|
||||||
|
if(hostAttr){
|
||||||
|
if(/^https?:\/\//i.test(hostAttr)){
|
||||||
|
href = hostAttr;
|
||||||
|
} else {
|
||||||
|
const hasPortInHost = /:\d+$/.test(hostAttr);
|
||||||
|
if(hasPortInHost){
|
||||||
|
// host includes port, use the host as provided with the proto
|
||||||
|
href = `${proto}://${hostAttr}`;
|
||||||
|
} else {
|
||||||
|
// Hostname without port: prefer HTTPS and do NOT append numeric port.
|
||||||
|
href = `https://${hostAttr}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback to current page hostname and include port if non-standard
|
||||||
|
let portPart = '';
|
||||||
|
if(port && !((proto==='http'&&port==='80')||(proto==='https'&&port==='443'))){ portPart = ':'+port; }
|
||||||
|
href = `${proto}://${host}${portPart}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.className = 'card';
|
||||||
|
a.href = href;
|
||||||
|
a.target = '_blank';
|
||||||
|
a.rel = 'noreferrer';
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.className = 'logo';
|
||||||
|
img.src = logo ? ('/logos/'+logo) : '/logos/default.svg';
|
||||||
|
img.alt = name + ' logo';
|
||||||
|
|
||||||
|
const span = document.createElement('div');
|
||||||
|
span.className = 'label';
|
||||||
|
span.textContent = `${name}${port?(' ('+port+')'):''}`;
|
||||||
|
|
||||||
|
a.appendChild(img);
|
||||||
|
a.appendChild(span);
|
||||||
|
grid.appendChild(a);
|
||||||
|
});
|
||||||
|
}catch(err){
|
||||||
|
console.error(err);
|
||||||
|
grid.innerHTML = '<p class="notes">Error loading services.xml — see console for details.</p>';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
5
logos/aurcache.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect width="24" height="24" rx="4" fill="#071020"/>
|
||||||
|
<path d="M6 12h12" stroke="#60a5fa" stroke-width="1.6" stroke-linecap="round"/>
|
||||||
|
<path d="M8 8v8" stroke="#34d399" stroke-width="1.6" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 297 B |
4
logos/default.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect width="24" height="24" rx="4" fill="#0b1220"/>
|
||||||
|
<circle cx="12" cy="12" r="5" stroke="#94a3b8" stroke-width="1.2" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 217 B |
4
logos/filebrowser.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect width="24" height="24" rx="4" fill="#071020"/>
|
||||||
|
<path d="M7 8h10v8H7z" stroke="#34d399" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 245 B |
4
logos/gitea.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect width="24" height="24" rx="4" fill="#071021"/>
|
||||||
|
<path d="M4 16c3-4 9-4 13 0" stroke="#fb7185" stroke-width="1.6" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 227 B |
4
logos/homeassistant.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect width="24" height="24" rx="4" fill="#0f1720"/>
|
||||||
|
<circle cx="12" cy="12" r="6" stroke="#22c55e" stroke-width="1.6" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 217 B |
4
logos/jellyfin.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect width="24" height="24" rx="4" fill="#071024"/>
|
||||||
|
<path d="M6 17v-6h12v6" stroke="#f59e0b" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 246 B |
4
logos/kiwix.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect width="24" height="24" rx="4" fill="#071020"/>
|
||||||
|
<path d="M6 6h12v12H6z" stroke="#f87171" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 246 B |
5
logos/nextcloud.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect width="24" height="24" rx="4" fill="#071020"/>
|
||||||
|
<circle cx="8.5" cy="12" r="2" fill="#60a5fa" />
|
||||||
|
<circle cx="15.5" cy="12" r="2" fill="#60a5fa" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 238 B |
6
logos/picoshare.svg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect width="24" height="24" rx="4" fill="#071020"/>
|
||||||
|
<path d="M8 12h8" stroke="#a78bfa" stroke-width="1.6" stroke-linecap="round"/>
|
||||||
|
<circle cx="8" cy="12" r="1.6" fill="#a78bfa"/>
|
||||||
|
<circle cx="16" cy="12" r="1.6" fill="#a78bfa"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 317 B |
4
logos/portainer.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect width="24" height="24" rx="4" fill="#111827"/>
|
||||||
|
<path d="M6 8h12M6 12h12M6 16h12" stroke="#60a5fa" stroke-width="1.6" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 232 B |
4
logos/tdarr.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect width="24" height="24" rx="4" fill="#071020"/>
|
||||||
|
<path d="M7 7l10 10M17 7L7 17" stroke="#fb923c" stroke-width="1.6" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 229 B |
4
logos/transmission.svg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect width="24" height="24" rx="4" fill="#071020"/>
|
||||||
|
<path d="M12 6v12M6 12h12" stroke="#60a5fa" stroke-width="1.6" stroke-linecap="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 225 B |
5
logos/uptime-kuma.svg
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none">
|
||||||
|
<rect width="24" height="24" rx="4" fill="#071024"/>
|
||||||
|
<path d="M6 12h12" stroke="#60a5fa" stroke-width="1.6" stroke-linecap="round"/>
|
||||||
|
<circle cx="12" cy="12" r="4" stroke="#34d399" stroke-width="1.6" fill="none"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 299 B |
21
services.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
services.xml - simple list of services for the homepage
|
||||||
|
Fields: id, name, proto, port, logo (filename in /logos/), note (optional)
|
||||||
|
-->
|
||||||
|
<services>
|
||||||
|
<service id="portainer" name="Portainer" proto="https" port="9443" logo="portainer.svg" />
|
||||||
|
<service id="portainer-http" name="Portainer (HTTP)" proto="http" port="8000" logo="portainer.svg" />
|
||||||
|
<service id="homeassistant" name="Home Assistant" proto="http" port="8123" logo="homeassistant.svg" />
|
||||||
|
<service id="jellyfin" name="Jellyfin" proto="http" port="8096" logo="jellyfin.svg" host="jellyfin.spatulaa.com" />
|
||||||
|
<service id="nextcloud" name="Nextcloud" proto="http" port="8080" logo="nextcloud.svg" host="cloud.spatulaa.com"/>
|
||||||
|
<service id="filebrowser" name="FileBrowser" proto="http" port="8986" logo="filebrowser.svg" />
|
||||||
|
<service id="gitea" name="Gitea" proto="http" port="3000" logo="gitea.svg" />
|
||||||
|
<service id="uptime-kuma" name="Uptime Kuma" proto="http" port="3001" logo="uptime-kuma.svg" />
|
||||||
|
<service id="picoshare" name="Picoshare" proto="http" port="4001" logo="picoshare.svg" />
|
||||||
|
<service id="transmission" name="Transmission" proto="http" port="9091" logo="transmission.svg" />
|
||||||
|
<service id="tdarr" name="Tdarr" proto="http" port="8265" logo="tdarr.svg" />
|
||||||
|
<service id="kiwix" name="Kiwix" proto="http" port="666" logo="kiwix.svg" />
|
||||||
|
<service id="aurcache-repo" name="Aurcache Repo" proto="http" port="888" logo="aurcache.svg" />
|
||||||
|
<service id="aurcache-frontend" name="Aurcache Frontend" proto="http" port="881" logo="aurcache.svg" />
|
||||||
|
</services>
|
||||||
19
styles.css
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
/* Minimal, responsive grid layout */
|
||||||
|
:root{
|
||||||
|
--bg:#0f1720;--card:#0b1220;--accent:#4f46e5;--muted:#94a3b8;color-scheme: dark;
|
||||||
|
}
|
||||||
|
*{box-sizing:border-box}
|
||||||
|
html,body{height:100%;margin:0;font-family:Inter,Segoe UI,Roboto,Arial,sans-serif;background:linear-gradient(180deg,#071020 0%,#0b1220 100%);color:#e6eef8}
|
||||||
|
header{padding:24px 20px;text-align:center}
|
||||||
|
header h1{margin:0;font-size:28px}
|
||||||
|
.subtitle{color:var(--muted);margin-top:6px}
|
||||||
|
main{max-width:1100px;margin:18px auto;padding:12px}
|
||||||
|
.grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:12px}
|
||||||
|
.card{display:flex;align-items:center;justify-content:center;padding:18px;border-radius:10px;background:linear-gradient(180deg, rgba(255,255,255,0.02), rgba(255,255,255,0.01));text-decoration:none;color:inherit;border:1px solid rgba(255,255,255,0.03);font-weight:600;transition:transform .12s ease,box-shadow .12s ease}
|
||||||
|
.card:hover{transform:translateY(-6px);box-shadow:0 10px 30px rgba(15,20,32,0.6)}
|
||||||
|
.card .logo{width:36px;height:36px;margin-right:12px;flex:0 0 36px}
|
||||||
|
.card .label{flex:1;text-align:left}
|
||||||
|
.notes{margin-top:18px;background:rgba(255,255,255,0.02);padding:12px;border-radius:8px;color:var(--muted)}
|
||||||
|
footer{padding:12px 20px;text-align:center;color:var(--muted);font-size:12px}
|
||||||
|
|
||||||
|
@media (max-width:420px){.card{padding:14px;font-size:14px}}
|
||||||