Compare commits

...

28 Commits

Author SHA1 Message Date
MayaChat
c36c2c08f4 Enhance galaxy background with terrain generation and moon lighting effects 2025-11-24 01:05:55 -05:00
MayaChat
0cfe29bea0 Refactor galaxy background animation and enhance terrain generation with smoother mountains and improved lighting 2025-11-24 01:03:57 -05:00
MayaChat
d054a2396b Enhance Services Homepage with galaxy background animation and canvas integration 2025-11-24 00:55:02 -05:00
MayaChat
9e034fcfa2 Remove Services Homepage entry from Management group in services.xml 2025-11-24 00:50:35 -05:00
MayaChat
14055cf4a2 Update service configurations: add maintenance status for Zoraxy, set host for Wizarr and Picoshare 2025-11-24 00:49:20 -05:00
MayaChat
99d04dbc3e Add SVG logos for OpenAI, Scrutiny, Wizarr, YouTube, and Traefik Proxy 2025-11-24 00:46:42 -05:00
MayaChat
4b21cf630b Add additional services to Management, Media, and AI & Tools groups in services.xml 2025-11-24 00:46:29 -05:00
MayaChat
7f192f0add Add Bazarr SVG logo 2025-11-24 00:14:10 -05:00
MayaChat
18262dcfc7 Add Prowlarr SVG logo 2025-11-24 00:13:55 -05:00
MayaChat
3b0d51e1c8 Add SVG logos for Lidarr, Radarr, Readarr, and Sonarr 2025-11-24 00:13:46 -05:00
MayaChat
e8c13c998c Enhance XML parsing in index.html and add media management services to services.xml 2025-11-24 00:12:19 -05:00
MayaChat
67cb864342 Add service grouping feature to README and implement dynamic rendering in index.html 2025-11-24 00:09:47 -05:00
MayaChat
5ef15cc692 Refactor services.xml to organize services into categorized groups for improved readability 2025-11-24 00:07:47 -05:00
MayaChat
7fe048ab23 Update service names for clarity and remove duplicate entry 2025-11-24 00:07:39 -05:00
MayaChat
5c798c075d Add automatic health checks and status indicators for services 2025-11-24 00:03:14 -05:00
MayaChat
f14955e534 Set Portainer (HTTP) service status to offline 2025-11-24 00:00:45 -05:00
MayaChat
c7ca3995e8 Add status attribute to services for improved service monitoring 2025-11-23 23:58:45 -05:00
MayaChat
3cc18bcd9e Update services.xml documentation to clarify field descriptions 2025-11-23 23:58:38 -05:00
MayaChat
874535103a Update contributing section to highlight completed features and future improvements 2025-11-23 23:58:08 -05:00
MayaChat
93bde02e73 Enhance README.md with keyboard shortcuts, info button details, and status indicator examples 2025-11-23 23:57:58 -05:00
MayaChat
69deb4ebc7 Add service status attribute description to configuration guide 2025-11-23 23:57:48 -05:00
MayaChat
b1b8cf01b6 Update README.md to include additional features: search & filter, connection details, keyboard navigation, and status indicators 2025-11-23 23:57:37 -05:00
MayaChat
cb74c580c8 Add selected state styles for service cards 2025-11-23 23:57:32 -05:00
MayaChat
eaa2126c7e Add keyboard navigation for service cards 2025-11-23 23:57:03 -05:00
MayaChat
5e4a51ad9c Add status dot indicators for service cards 2025-11-23 23:56:54 -05:00
MayaChat
642b332c15 Add service status indicator to service cards 2025-11-23 23:56:37 -05:00
MayaChat
e2ca853042 Add search input styles and info button to card component 2025-11-23 23:56:05 -05:00
MayaChat
75b7922517 Implement search functionality for services and add info button for connection details 2025-11-23 23:56:00 -05:00
16 changed files with 627 additions and 45 deletions

160
README.md
View File

@@ -14,6 +14,12 @@ A lightweight, self-hosted dashboard for quick access to your Docker services wi
- 📱 **Responsive Layout** - Works seamlessly on desktop and mobile
- 🐳 **Docker-Ready** - Single-container deployment with nginx
-**Lightweight** - Minimal resources, fast loading
- 🔎 **Search & Filter** - Instant search with keyboard shortcut (press `/`)
- **Connection Details** - Info button shows hostname/port for each service
- 🎮 **Keyboard Navigation** - Arrow keys to navigate, Enter to open, Esc to clear
- 🟢 **Status Indicators** - Optional visual status (online/offline/maintenance)
- 🏥 **Automatic Health Checks** - Real-time ping tests to detect service availability
- 📂 **Service Groups** - Organize services into categorized sections
## Quick Start
@@ -27,23 +33,39 @@ The homepage will be available at `http://localhost:8088`
### 2. Configure Your Services
Edit `services.xml` to add your services:
Edit `services.xml` to add your services organized into groups:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<services>
<service
name="Portainer"
proto="https"
host="portainer.example.com"
logo="portainer.svg"
/>
<service
name="Jellyfin"
proto="http"
port="8096"
logo="jellyfin.svg"
/>
<group name="Management">
<service
name="Portainer"
proto="https"
port="9443"
logo="portainer.svg"
/>
</group>
<group name="Media">
<service
name="Jellyfin"
proto="http"
port="8096"
host="jellyfin.example.com"
logo="jellyfin.svg"
/>
</group>
</services>
```
Or use services without groups (they'll appear in a single grid):
```xml
<?xml version="1.0" encoding="UTF-8"?>
<services>
<service name="Portainer" proto="https" port="9443" logo="portainer.svg" />
<service name="Jellyfin" proto="http" port="8096" logo="jellyfin.svg" />
</services>
```
@@ -64,6 +86,8 @@ docker-compose restart
| `port` | No | Port number | `"8080"` |
| `host` | No | Custom hostname or full URL | `"nextcloud.example.com"` |
| `logo` | No | Icon filename in `/logos/` | `"nextcloud.svg"` |
| `status` | No | Set to `"maintenance"` to skip health check | `"maintenance"` |
| `check-health` | No | Enable/disable auto health check | `"true"` (default) or `"false"` |
### URL Resolution Logic
@@ -88,8 +112,95 @@ The service URL is built using this priority:
<!-- Local service (uses page hostname) -->
<service name="Portainer" proto="https" port="9443" logo="portainer.svg" />
<!-- Service with status indicator -->
<service name="Home Assistant" proto="http" port="8123" logo="homeassistant.svg" status="online" />
```
## Keyboard Shortcuts
- **`/`** - Focus the search bar (press from anywhere)
- **Arrow Keys** - Navigate between service cards (Up/Down/Left/Right)
- **Enter** - Open the selected service in a new tab
- **Esc** - Clear the current selection
## Info Button
When a service has both a hostname and port configured, a small info button (ⓘ) appears in the bottom-right corner of the card. Click it to view connection details including:
- Service name
- Hostname
- Port number
- Protocol
## Status Indicators
Services are automatically checked for availability when the page loads. Status indicators appear in the top-right corner of each card:
```xml
<!-- Automatic health check (default behavior) -->
<service name="Jellyfin" proto="http" port="8096" logo="jellyfin.svg" />
<!-- Manual maintenance mode (skips health check) -->
<service name="Nextcloud" status="maintenance" host="cloud.example.com" logo="nextcloud.svg" />
<!-- Disable health check for a service -->
<service name="Legacy App" check-health="false" proto="http" port="8080" logo="app.svg" />
```
Status colors:
- **Gray spinning** (checking) - Currently testing service availability
- **Green pulsing** (online) - Service responded successfully to health check
- **Red** (offline) - Service failed to respond or timed out (5 seconds)
- **Orange** (maintenance) - Manual maintenance mode, health check skipped
### How Health Checks Work
- Each service is automatically pinged when the page loads
- Uses a 5-second timeout per service
- Checks run in parallel for all services
- Services marked `status="maintenance"` skip the health check
- Set `check-health="false"` to disable checking for specific services
- No server-side component needed - runs entirely in the browser
## Service Groups
Organize your services into categorized sections for better organization:
```xml
<services>
<group name="Management">
<service name="Portainer" proto="https" port="9443" logo="portainer.svg" />
<service name="Uptime Kuma" proto="http" port="3001" logo="uptime-kuma.svg" />
</group>
<group name="Media">
<service name="Jellyfin" proto="http" port="8096" logo="jellyfin.svg" />
<service name="Transmission" proto="http" port="9091" logo="transmission.svg" />
</group>
<group name="Storage">
<service name="Nextcloud" host="cloud.example.com" logo="nextcloud.svg" />
<service name="FileBrowser" proto="http" port="8986" logo="filebrowser.svg" />
</group>
</services>
```
### Group Features
- **Categorization**: Group related services together (Media, Management, Development, etc.)
- **Visual Separation**: Each group has a styled header with an accent underline
- **Smart Search**: Searching filters services and hides empty groups automatically
- **Backward Compatible**: Services without groups still work (displayed in a single grid)
- **Flexible**: Use as many or as few groups as needed
### Group Tips
- Use clear, descriptive group names (e.g., "Media Services" instead of "Group 1")
- Keep related services together for easier navigation
- Groups appear in the order defined in the XML
- Empty groups (no services) are automatically hidden
## Icon Management
### Using Included Icons
@@ -309,12 +420,23 @@ Then update via script or use a template processor.
## Contributing
Contributions welcome! Areas for improvement:
- Search/filter functionality
- Dark/light mode toggle
- Service health checks
- Drag-and-drop reordering
- Categorized sections
Contributions welcome! Completed features:
- ✅ Search/filter functionality with keyboard shortcut
- ✅ Keyboard navigation (arrow keys, Enter, Esc)
- ✅ Service status indicators (online/offline/maintenance)
- ✅ Automatic health checks with ping tests
- ✅ Info button showing connection details
- ✅ Service groups/categories
Areas for future improvement:
- Service health checks via custom endpoints
- Drag-and-drop reordering within groups
- Collapsible group sections
- Custom themes/color schemes
- Export/import service configurations
- Dashboard widgets (time, weather, etc.)
## License

View File

@@ -5,16 +5,22 @@
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Services Homepage</title>
<link rel="stylesheet" href="/styles.css" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplex-noise/2.4.0/simplex-noise.min.js"></script>
</head>
<body>
<canvas id="galaxy-canvas"></canvas>
<header>
<h1>My Services</h1>
<p class="subtitle">Quick links to the commonly used containers on this host</p>
<div class="search-container">
<input type="text" id="search-input" placeholder="🔍 Search services..." />
</div>
</header>
<main>
<section class="grid" id="services-grid">
<!-- Services will be populated dynamically from /services.xml -->
<section id="services-container">
<!-- Service groups will be populated dynamically from /services.xml -->
</section>
<section class="notes">
@@ -29,25 +35,243 @@
<footer>
<small>Generated: static homepage — served by nginx in a container. Edit and rebuild to update.</small>
</footer>
<script>
// Galaxy background - exact CodePen recreation with moon and terrain
(function initGalaxy(){
console.clear();
var ww = window.innerWidth,
wh = window.innerHeight;
var renderer = new THREE.WebGLRenderer({
antialias: true,
canvas: document.querySelector('#galaxy-canvas')
});
renderer.setSize(ww, wh);
renderer.setClearColor(0x001a2d);
var scene = new THREE.Scene();
scene.fog = new THREE.Fog(0x001a2d, 80, 140);
var camera = new THREE.PerspectiveCamera(45, ww/wh, 0.1, 200);
camera.position.x = 70;
camera.position.y = 30;
camera.position.z = 5;
camera.lookAt(new THREE.Vector3());
/* LIGHTS */
var moonLight = new THREE.PointLight(0xffffff, 2, 150);
scene.add(moonLight);
var moon;
function createMoon() {
var geometry = new THREE.SphereGeometry(8, 32, 32);
var material = new THREE.MeshPhongMaterial({
color: 0x26fdd9,
shininess: 15,
emissive: 0x2bb2e6,
emissiveIntensity: 0.8
});
moon = new THREE.Mesh(geometry, material);
moon.position.x = -9;
moon.position.z = -6.5;
moon.position.y = 1;
moon.rotation.y = -1;
scene.add(moon);
moonLight.position.copy(moon.position);
moonLight.position.y += 4;
var moonLight2 = new THREE.PointLight(0xffffff, 0.6, 150);
scene.add(moonLight2);
moonLight2.position.x += 20;
moonLight2.position.y -= 20;
moonLight2.position.z -= 25;
}
// Initialize simplex noise
var noise = new SimplexNoise();
function createTerrain() {
var geometry = new THREE.PlaneGeometry(150, 150, 120, 120);
var m = new THREE.Matrix4();
m.makeRotationX(Math.PI * -0.5);
geometry.applyMatrix4(m);
var positions = geometry.attributes.position;
for(var i = 0; i < positions.count; i++) {
var x = positions.getX(i);
var z = positions.getZ(i);
var ratio = noise.noise3D(x * 0.03, z * 0.03, 0);
positions.setY(i, ratio * 10);
}
positions.needsUpdate = true;
geometry.computeVertexNormals();
var material = new THREE.MeshPhongMaterial({
color: 0x198257,
emissive: 0x032f50
});
var plane = new THREE.Mesh(geometry, material);
scene.add(plane);
}
var stars = new THREE.Group();
scene.add(stars);
var starsLights = new THREE.Group();
scene.add(starsLights);
var starsAmount = 20;
function createStars() {
var geometry = new THREE.SphereGeometry(0.3, 16, 16);
var material = new THREE.MeshBasicMaterial({color: 0xffffff});
for(var i = 0; i < starsAmount; i++) {
var star = new THREE.Mesh(geometry, material);
star.position.x = (Math.random() - 0.5) * 150;
star.position.z = (Math.random() - 0.5) * 150;
var ratio = noise.noise3D(star.position.x * 0.03, star.position.z * 0.03, 0);
star.position.y = ratio * 10 + 0.3;
stars.add(star);
var velX = (Math.random() + 0.1) * 0.1 * (Math.random() < 0.5 ? -1 : 1);
var velY = (Math.random() + 0.1) * 0.1 * (Math.random() < 0.5 ? -1 : 1);
star.vel = new THREE.Vector2(velX, velY);
var starLight = new THREE.PointLight(0xffffff, 0.8, 3);
starLight.position.copy(star.position);
starLight.position.y += 0.5;
starsLights.add(starLight);
}
}
function updateStar(star, index) {
if(star.position.x < -75) {
star.position.x = 75;
}
if(star.position.x > 75) {
star.position.x = -75;
}
if(star.position.z < -75) {
star.position.z = 75;
}
if(star.position.z > 75) {
star.position.z = -75;
}
star.position.x += star.vel.x;
star.position.z += star.vel.y;
var ratio = noise.noise3D(star.position.x * 0.03, star.position.z * 0.03, 0);
star.position.y = ratio * 10 + 0.3;
starsLights.children[index].position.copy(star.position);
starsLights.children[index].position.y += 0.5;
}
function render(a) {
requestAnimationFrame(render);
for(var i = 0; i < starsAmount; i++) {
updateStar(stars.children[i], i);
}
renderer.render(scene, camera);
}
function onResize() {
ww = window.innerWidth;
wh = window.innerHeight;
camera.aspect = ww / wh;
camera.updateProjectionMatrix();
renderer.setSize(ww, wh);
}
createMoon();
createTerrain();
createStars();
requestAnimationFrame(render);
window.addEventListener('resize', onResize);
})();
</script>
<script>
// Fetch services.xml and render the service cards with logos.
(async function(){
const grid = document.getElementById('services-grid');
const container = document.getElementById('services-container');
const searchInput = document.getElementById('search-input');
const host = window.location.hostname;
let allServices = []; // Store all service elements for filtering
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=>{
// Check for XML parsing errors
const parseError = doc.querySelector('parsererror');
if(parseError){
throw new Error('XML parsing error: ' + parseError.textContent);
}
// Check if we have groups or just services
const groups = Array.from(doc.getElementsByTagName('group'));
const hasGroups = groups.length > 0;
console.log('Found', groups.length, 'groups');
if(hasGroups){
// Render grouped services
groups.forEach(group => {
const groupName = group.getAttribute('name') || 'Services';
const services = Array.from(group.getElementsByTagName('service'));
console.log('Group:', groupName, 'has', services.length, 'services');
if(services.length === 0) return;
// Create group section
const groupSection = document.createElement('section');
groupSection.className = 'service-group';
const groupHeader = document.createElement('h2');
groupHeader.className = 'group-header';
groupHeader.textContent = groupName;
groupSection.appendChild(groupHeader);
const grid = document.createElement('div');
grid.className = 'grid';
services.forEach(s => {
const card = createServiceCard(s, host, allServices);
grid.appendChild(card);
});
groupSection.appendChild(grid);
container.appendChild(groupSection);
});
} else {
// Fallback: render ungrouped services
const services = Array.from(doc.getElementsByTagName('service'));
if(services.length === 0){
container.innerHTML = '<p class="notes">No services found in services.xml</p>';
return;
}
const grid = document.createElement('div');
grid.className = 'grid';
grid.id = 'services-grid';
services.forEach(s => {
const card = createServiceCard(s, host, allServices);
grid.appendChild(card);
});
container.appendChild(grid);
}
// Function to create a service card
function createServiceCard(s, host, allServices) {
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
const manualStatus = s.getAttribute('status'); // optional: 'online', 'offline', 'maintenance'
const checkHealth = s.getAttribute('check-health') !== 'false'; // default true, set to false to disable
// Build href: prefer explicit host attribute when present.
// Rules:
@@ -81,6 +305,7 @@
a.href = href;
a.target = '_blank';
a.rel = 'noreferrer';
a.dataset.serviceName = name.toLowerCase(); // For search filtering
const img = document.createElement('img');
img.className = 'logo';
@@ -93,11 +318,163 @@
a.appendChild(img);
a.appendChild(span);
grid.appendChild(a);
// Add status indicator - will be updated by health check
const statusDot = document.createElement('span');
statusDot.className = 'status-dot status-checking';
statusDot.title = 'Checking status...';
let currentStatus = 'checking';
// If maintenance mode is set manually, use that and skip health check
if(manualStatus === 'maintenance'){
statusDot.className = 'status-dot status-maintenance';
statusDot.title = 'Status: maintenance';
currentStatus = 'maintenance';
a.appendChild(statusDot);
} else if(checkHealth && href){
// Show checking indicator initially
a.appendChild(statusDot);
// Perform health check
(async function performHealthCheck(){
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout
const response = await fetch(href, {
method: 'HEAD',
mode: 'no-cors', // Allow cross-origin requests
cache: 'no-cache',
signal: controller.signal
});
clearTimeout(timeoutId);
// In no-cors mode, opaque responses mean the server responded
// We consider this as "online"
currentStatus = 'online';
statusDot.className = 'status-dot status-online';
statusDot.title = 'Status: online';
} catch(err) {
// Connection failed or timed out
currentStatus = 'offline';
statusDot.className = 'status-dot status-offline';
statusDot.title = 'Status: offline';
}
})();
} else if(manualStatus){
// Use manual status if health check is disabled
statusDot.className = `status-dot status-${manualStatus}`;
statusDot.title = `Status: ${manualStatus}`;
currentStatus = manualStatus;
a.appendChild(statusDot);
}
// Add info button if both hostname and port are available
if((hostAttr || host) && port){
const infoBtn = document.createElement('button');
infoBtn.className = 'info-btn';
infoBtn.title = 'Connection details';
infoBtn.innerHTML = 'ⓘ';
infoBtn.onclick = (e) => {
e.preventDefault();
e.stopPropagation();
const displayHost = hostAttr || host;
const details = `Service: ${name}\nHost: ${displayHost}\nPort: ${port}\nProtocol: ${proto}`;
alert(details);
};
a.appendChild(infoBtn);
}
allServices.push(a);
return a;
}
// Search functionality
searchInput.addEventListener('input', (e) => {
const searchTerm = e.target.value.toLowerCase().trim();
let visibleCount = 0;
// Also hide/show group headers
const groupSections = container.querySelectorAll('.service-group');
allServices.forEach(card => {
const serviceName = card.dataset.serviceName;
if(serviceName.includes(searchTerm)){
card.style.display = '';
visibleCount++;
} else {
card.style.display = 'none';
}
});
// Hide empty groups
groupSections.forEach(section => {
const visibleCards = section.querySelectorAll('.card:not([style*="display: none"])');
section.style.display = visibleCards.length > 0 ? '' : 'none';
});
// Show message if no results
const existingMsg = container.querySelector('.no-results');
if(existingMsg) existingMsg.remove();
if(visibleCount === 0 && searchTerm !== ''){
const msg = document.createElement('p');
msg.className = 'notes no-results';
msg.textContent = `No services found matching "${e.target.value}"`;
container.appendChild(msg);
}
});
// Focus search on '/' key
document.addEventListener('keydown', (e) => {
if(e.key === '/' && document.activeElement !== searchInput){
e.preventDefault();
searchInput.focus();
}
});
// Keyboard navigation for service cards
let selectedIndex = -1;
const selectCard = (index) => {
const visibleCards = allServices.filter(card => card.style.display !== 'none');
if(visibleCards.length === 0) return;
// Remove previous selection
visibleCards.forEach(card => card.classList.remove('selected'));
// Update index
if(index < 0) index = visibleCards.length - 1;
if(index >= visibleCards.length) index = 0;
selectedIndex = index;
// Add selection
visibleCards[selectedIndex].classList.add('selected');
visibleCards[selectedIndex].scrollIntoView({behavior: 'smooth', block: 'nearest'});
};
document.addEventListener('keydown', (e) => {
if(document.activeElement === searchInput) return;
const visibleCards = allServices.filter(card => card.style.display !== 'none');
if(e.key === 'ArrowRight' || e.key === 'ArrowDown'){
e.preventDefault();
selectCard(selectedIndex + 1);
} else if(e.key === 'ArrowLeft' || e.key === 'ArrowUp'){
e.preventDefault();
selectCard(selectedIndex - 1);
} else if(e.key === 'Enter' && selectedIndex >= 0){
e.preventDefault();
visibleCards[selectedIndex].click();
} else if(e.key === 'Escape'){
visibleCards.forEach(card => card.classList.remove('selected'));
selectedIndex = -1;
}
});
}catch(err){
console.error(err);
grid.innerHTML = '<p class="notes">Error loading services.xml — see console for details.</p>';
container.innerHTML = '<p class="notes">Error loading services.xml — see console for details.</p>';
}
})();
</script>

1
logos/bazarr.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Subtitle Edit</title><path d="M3.858.02C1.744.23.16 1.91.02 4.09c-.038.598-.02 15.896.02 16.156.3 1.996 1.752 3.455 3.7 3.719.418.057 16.38.04 16.674-.018 1.433-.28 2.614-1.164 3.156-2.363.2-.443.304-.776.377-1.208.047-.282.075-16.036.029-16.509A4.266 4.266 0 0 0 20.348.048C20.065.008 4.261-.02 3.858.02m7.237 6.15c.707.707 1.285 1.299 1.285 1.315 0 .024-.57.03-2.79.03-3.106 0-2.95-.008-3.286.16-1.145.58-1.175 2.2-.052 2.8.34.18.265.174 1.725.192 1.404.018 1.475.023 1.976.153 1.495.388 2.688 1.64 3.015 3.164a4.2 4.2 0 0 1-3.547 5.057c-.347.046-6.605.05-6.605.004 0-.016.573-.602 1.273-1.302L5.36 16.47l1.87-.01c2.07-.009 1.97-.002 2.326-.172a1.566 1.566 0 0 0 .421-2.532c-.431-.43-.571-.461-2.05-.462-1.802 0-2.364-.125-3.253-.721-3.078-2.066-2.152-6.837 1.475-7.597.38-.08.522-.086 2.11-.089l1.551-.003 1.284 1.285m10.067-1.256c0 .017-.578.608-1.284 1.315l-1.284 1.286h-4.427l-1.296-1.298a68.614 68.608 0 0 1-1.296-1.315c0-.01 2.157-.018 4.793-.018 3.813 0 4.794.006 4.794.03m-2.562 7.06-.006 1.308h-4.449l-.033-.094c-.336-.942-.695-1.527-1.346-2.194a4.325 4.325 0 0 1-.292-.313c0-.01 1.38-.016 3.066-.016h3.066l-.006 1.309m1.278 5.78a67.498 67.492 0 0 1 1.284 1.302c0 .01-1.955.018-4.344.018-2.389 0-4.344-.008-4.344-.018 0-.01.103-.12.228-.243a5.453 5.453 0 0 0 1.38-2.185l.053-.16h4.458l1.285 1.285"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

1
logos/lidarr.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>MusicBrainz</title><path d="M11.582 0L1.418 5.832v12.336L11.582 24V10.01L7.1 12.668v3.664c.01.111.01.225 0 .336-.103.435-.54.804-1 1.111-.802.537-1.752.509-2.166-.111-.413-.62-.141-1.631.666-2.168.384-.28.863-.399 1.334-.332V6.619c0-.154.134-.252.226-.308L11.582 3zm.836 0v6.162c.574.03 1.14.16 1.668.387a2.225 2.225 0 0 0 1.656-.717 1.02 1.02 0 1 1 1.832-.803l.004.006a1.022 1.022 0 0 1-1.295 1.197c-.34.403-.792.698-1.297.85.34.263.641.576.891.928a1.04 1.04 0 0 1 .777.125c.768.486.568 1.657-.318 1.857-.886.2-1.574-.77-1.09-1.539.02-.03.042-.06.065-.09a3.598 3.598 0 0 0-1.436-1.166 4.142 4.142 0 0 0-1.457-.369v4.01c.855.06 1.256.493 1.555.834.227.256.356.39.578.402.323.018.568.008.806 0a5.44 5.44 0 0 1 .895.022c.94-.017 1.272-.226 1.605-.446a2.533 2.533 0 0 1 1.131-.463 1.027 1.027 0 0 1 .12-.263 1.04 1.04 0 0 1 .105-.137c.023-.025.047-.044.07-.066a4.775 4.775 0 0 1 0-2.405l-.012-.01a1.02 1.02 0 1 1 .692.272h-.057a4.288 4.288 0 0 0 0 1.877h.063a1.02 1.02 0 1 1-.545 1.883l-.047-.033a1 1 0 0 1-.352-.442 1.885 1.885 0 0 0-.814.354 3.03 3.03 0 0 1-.703.365c.757.555 1.772 1.6 2.199 2.299a1.03 1.03 0 0 1 .256-.033 1.02 1.02 0 1 1-.545 1.88l-.047-.03a1.017 1.017 0 0 1-.27-1.376.72.72 0 0 1 .051-.072c-.445-.775-2.026-2.28-2.46-2.387a4.037 4.037 0 0 0-1.31-.117c-.24.008-.513.018-.866 0-.515-.027-.783-.333-1.043-.629-.26-.296-.51-.56-1.055-.611V18.5a1.877 1.877 0 0 0 .426-.135.333.333 0 0 1 .058-.027c.56-.267 1.421-.91 2.096-2.447a1.02 1.02 0 0 1-.27-1.344 1.02 1.02 0 1 1 .915 1.54 6.273 6.273 0 0 1-1.432 2.136 1.785 1.785 0 0 1 .691.306.667.667 0 0 0 .37.168 3.31 3.31 0 0 0 .888-.222 1.02 1.02 0 0 1 1.787-.79v-.005a1.02 1.02 0 0 1-.773 1.683 1.022 1.022 0 0 1-.719-.287 3.935 3.935 0 0 1-1.168.287h-.05a1.313 1.313 0 0 1-.71-.275c-.262-.177-.51-.345-1.402-.12a2.098 2.098 0 0 1-.707.2V24l10.164-5.832V5.832zm4.154 4.904a.352.352 0 0 0-.197.639l.018.01c.163.1.378.053.484-.108v-.002a.352.352 0 0 0-.303-.539zm-4.99 1.928L7.082 9.5v2l4.5-2.668zm8.385.38a.352.352 0 0 0-.295.165v.002a.35.35 0 0 0 .096.473l.013.01a.357.357 0 0 0 .487-.108.352.352 0 0 0-.301-.541zM16.09 8.647a.352.352 0 0 0-.277.163.355.355 0 0 0 .296.54c.482 0 .463-.73-.02-.703zm3.877 2.477a.352.352 0 0 0-.295.164.35.35 0 0 0 .094.475l.015.01a.357.357 0 0 0 .485-.11.352.352 0 0 0-.3-.539zm-4.375 3.594a.352.352 0 0 0-.291.172.35.35 0 0 0-.04.265.352.352 0 1 0 .33-.437zm4.375.789a.352.352 0 0 0-.295.164v.002a.352.352 0 0 0 .094.473l.015.01a.357.357 0 0 0 .485-.108.352.352 0 0 0-.3-.54zm-2.803 2.488v.002a.347.347 0 0 0-.223.084.352.352 0 0 0 .23.62.347.347 0 0 0 .23-.085.348.348 0 0 0 .12-.24.353.353 0 0 0-.35-.38.347.347 0 0 0-.007 0Z"/></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

1
logos/openwebui.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

1
logos/prowlarr.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>OpenSearch</title><path d="M23.1515 8.8125a.8484.8484 0 0 0-.8484.8485c0 6.982-5.6601 12.6421-12.6421 12.6421a.8485.8485 0 0 0 0 1.6969C17.5802 24 24 17.5802 24 9.661a.8485.8485 0 0 0-.8485-.8485Zm-5.121 5.4375c.816-1.3311 1.6051-3.1058 1.4498-5.5905-.3216-5.1468-4.9832-9.0512-9.3851-8.6281C8.372.1971 6.6025 1.6017 6.7598 4.1177c.0683 1.0934.6034 1.7386 1.473 2.2348.8279.4722 1.8914.7713 3.097 1.1104 1.4563.4096 3.1455.8697 4.4438 1.8265 1.5561 1.1467 2.6198 2.4759 2.2569 4.9606Zm-16.561-9C.6535 6.581-.1355 8.3558.0197 10.8405c.3216 5.1468 4.9832 9.0512 9.385 8.6281 1.7233-.1657 3.4927-1.5703 3.3355-4.0863-.0683-1.0934-.6034-1.7386-1.4731-2.2348-.8278-.4722-1.8913-.7713-3.0969-1.1104-1.4563-.4096-3.1455-.8697-4.4438-1.8265-1.5561-1.1467-2.6198-2.476-2.257-4.9606Z"/></svg>

After

Width:  |  Height:  |  Size: 860 B

1
logos/radarr.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>radarr</title><path d="M5.274 0C3.189.039 1.19 1.547 1.19 4.705l.184 14.518c0 1.47 1.103 2.205 2.573 2.021L3.764 3.786c0-1.654.919-1.838 2.022-1.103l14.7 8.27c1.103.734 1.655 1.47 1.838 2.756.92-1.654.552-4.043-1.286-5.33L7.991.846A4.559 4.559 0 0 0 5.274.001zm1.982 6.91-.184 10.107 9.004-5.146Zm13.598 6.064-15.068 8.82c-.92.552-2.022.736-3.124.368.918 1.47 3.307 2.389 5.145 1.47l12.68-7.35c1.102-.736 1.286-2.022.367-3.308z"/></svg>

After

Width:  |  Height:  |  Size: 514 B

1
logos/readarr.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>BookStack</title><path d="M.3013 17.6146c-.1299-.3387-.5228-1.5119-.1337-2.4314l9.8273 5.6738a.329.329 0 0 0 .3299 0L24 12.9616v2.3542l-13.8401 7.9906-9.8586-5.6918zM.1911 8.9628c-.2882.8769.0149 2.0581.1236 2.4261l9.8452 5.6841L24 9.0823V6.7275L10.3248 14.623a.329.329 0 0 1-.3299 0L.1911 8.9628zm13.1698-1.9361c-.1819.1113-.4394.0015-.4852-.2064l-.2805-1.1336-2.1254-.1752a.33.33 0 0 1-.1378-.6145l5.5782-3.2207-1.7021-.9826L.6979 8.4935l9.462 5.463 13.5104-7.8004-4.401-2.5407-5.9084 3.4113zm-.1821-1.7286.2321.938 5.1984-3.0014-2.0395-1.1775-4.994 2.8834 1.3099.108a.3302.3302 0 0 1 .2931.2495zM24 9.845l-13.6752 7.8954a.329.329 0 0 1-.3299 0L.1678 12.0667c-.3891.919.003 2.0914.1332 2.4311l9.8589 5.692L24 12.1993V9.845z"/></svg>

After

Width:  |  Height:  |  Size: 812 B

1
logos/scrutiny.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>smart</title><path d="M10.85.846A11.138 11.138 0 0 0 0 11.979v.04a11.136 11.136 0 0 0 10.844 11.135h.283a10.983 10.983 0 0 0 4.041-.758.395.395 0 0 0 .256-.369v-5.564a.21.21 0 0 0-.274-.195c-1.202.489-2.215.957-3.96.957a5.222 5.222 0 0 1-5.22-5.22 5.22 5.22 0 0 1 5.22-5.22c1.745 0 2.758.467 3.96.955a.21.21 0 0 0 .274-.193V1.979a.395.395 0 0 0-.256-.37 10.983 10.983 0 0 0-4.037-.763Zm5.863 1.82v18.67a.238.238 0 0 0 .377.19c3.413-2.122 6.91-8.16 6.91-9.52 0-1.36-3.497-7.396-6.91-9.522a.238.238 0 0 0-.377.182Z"/></svg>

After

Width:  |  Height:  |  Size: 599 B

Submodule logos/simple-icons.manual-backup-20251123-234134 added at 3dd8ab9a29

1
logos/sonarr.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>sonarr</title><path d="M21.212 4.282c1.851 2.204 2.777 4.776 2.777 7.718 0 2.848-.867 5.344-2.602 7.489a934.355 934.355 0 0 1-2.101-2.095c-1.477-1.477-1.792-3.293-1.792-5.278 0-2.224.127-3.486 1.577-4.935l2.478-2.478a13.209 13.209 0 0 0-.337-.421Zm-17.7 16.193C1.708 18.678.6 16.59.188 14.213A11.84 11.84 0 0 1 .011 12c0-.28.006-.548.017-.802 0-.026.007-.052.022-.078.153-2.601 1.076-4.889 2.767-6.865-.108.127-.214.256-.316.387 0 0 1.351 1.346 2.329 2.323 1.408 1.409 1.726 3.215 1.726 5.151 0 1.985-.249 3.762-1.781 5.295-1.035 1.035-2.119 2.124-2.119 2.124.112.136.229.271.349.404.029-.027 1.297-1.348 2.123-2.175 1.638-1.637 1.928-3.528 1.928-5.648 0-2.072-.365-3.997-1.873-5.504a620.045 620.045 0 0 0-2.366-2.357c.168-.196.342-.388.523-.576l3.117 3.106-.194.195 1.903 1.898.547-.549L6.81 6.432l-.196.196L3.495 3.52c.01-.009.436-.416.643-.597.009.011 2.28 2.283 2.28 2.283 1.538 1.537 3.5 1.955 5.621 1.955 2.18 0 4.134-.442 5.731-2.038.907-.908 2.153-2.149 2.162-2.16.17.151.491.461.56.528l.013.013-3.111 3.028-.001.002-.197-.194-1.876 1.903.552.543 1.875-1.903-.197-.194 3.109-3.026c.193.203.377.41.553.619-.03.025-2.495 2.546-2.495 2.546-1.556 1.556-1.723 2.9-1.723 5.288 0 2.121.361 4.054 1.939 5.632a576.91 576.91 0 0 0 2.133 2.124c-.183.208-.599.645-.613.66l-3.066-3.174.195-.196-1.995-1.986-.546.549 1.995 1.986.195-.196 3.065 3.172c-.021.019-.385.362-.552.506-.01-.013-1.974-1.978-1.974-1.978-1.842-1.842-3.299-2.039-5.731-2.039-2.338 0-3.92.239-5.632 1.95-.944.944-2.078 2.085-2.089 2.099-.275-.23-.649-.594-.649-.594l3.019-3.024.199.192 1.854-1.925-.558-.538-1.854 1.926.199.191-3.016 3.022ZM12 8.672A3.33 3.33 0 0 0 8.672 12 3.33 3.33 0 0 0 12 15.328 3.33 3.33 0 0 0 15.328 12 3.33 3.33 0 0 0 12 8.672ZM4.52 2.6C6.665.867 9.162 0 12.011 0c2.88 0 5.394.88 7.541 2.639 0 0-1.215 1.209-2.136 2.13-1.496 1.496-3.334 1.892-5.377 1.892-1.985 0-3.829-.37-5.267-1.809L4.52 2.6Zm14.837 18.909a9.507 9.507 0 0 1-.342.256C16.994 23.255 14.659 24 12.011 24c-2.652 0-4.983-.745-6.993-2.235-.104-.074-.208-.15-.31-.227 0 0 1.096-1.101 2.053-2.058 1.602-1.602 3.09-1.804 5.278-1.804 2.28 0 3.651.166 5.377 1.892l1.941 1.941Z"/></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

1
logos/wizarr.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>CSS Wizardry</title><path d="M0 16.5V1.127C0 .502.506 0 1.127 0h21.748C23.498 0 24 .505 24 1.126V15.95c-.676-.413-1.467-.62-2.372-.62-1.258 0-2.212.296-2.862.886-.65.591-.974 1.333-.974 2.226 0 .979.336 1.698 1.008 2.158.397.276 1.114.53 2.151.765l1.056.237c.618.135 1.07.29 1.36.466.288.18.432.436.432.765 0 .564-.29.95-.872 1.157l-.024.008H20.68a1.528 1.528 0 0 1-.688-.462c-.185-.225-.31-.565-.372-1.021h-1.99c0 .56.109 1.053.325 1.483h-1.681c.196-.396.294-.837.294-1.32 0-.889-.297-1.568-.892-2.037-.384-.302-.952-.543-1.705-.724l-1.719-.412c-.663-.158-1.096-.296-1.299-.413a.858.858 0 0 1-.473-.799c0-.387.16-.69.48-.906.32-.217.75-.325 1.286-.325.482 0 .886.084 1.21.25.488.253.75.68.785 1.28h2.003c-.036-1.06-.425-1.869-1.167-2.426-.742-.557-1.639-.836-2.69-.836-1.258 0-2.212.296-2.861.886-.65.591-.975 1.333-.975 2.226 0 .979.336 1.698 1.008 2.158.397.276 1.114.53 2.152.765l1.055.237c.618.135 1.071.29 1.36.466.288.18.433.436.433.765 0 .564-.291.95-.873 1.157l-.025.008h-2.223a1.528 1.528 0 0 1-.688-.462c-.185-.225-.31-.565-.372-1.021h-1.99c0 .56.108 1.053.324 1.483H6.611a4.75 4.75 0 0 0 .667-1.801H5.215c-.14.514-.316.9-.528 1.157-.261.326-.603.54-1.026.644H2.42c-.45-.115-.839-.37-1.165-.762C.792 22.68.56 21.842.56 20.724c0-1.119.218-1.984.656-2.595.437-.611 1.035-.917 1.793-.917.744 0 1.305.217 1.684.65.212.243.386.604.52 1.082H7.3c-.032-.622-.262-1.242-.69-1.86-.776-1.1-2.003-1.65-3.68-1.65-1.168 0-2.145.355-2.929 1.067zm24 3.654v-1.562h-.518c-.036-.6-.298-1.026-.785-1.279-.325-.166-.728-.25-1.21-.25-.537 0-.966.108-1.286.325-.32.216-.48.518-.48.906 0 .357.157.623.473.799.203.117.636.255 1.299.413l1.718.412c.29.07.554.149.789.236z"/></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

1
logos/youtubedl.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>YouTube</title><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>

After

Width:  |  Height:  |  Size: 459 B

1
logos/zoraxy.svg Normal file
View File

@@ -0,0 +1 @@
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Traefik Proxy</title><path d="M12 1.19c1.088 0 2.056.768 2.056 1.714 0 .947-.921 1.715-2.056 1.715-.13 0-.3-.022-.509-.064a.685.685 0 0 0-.475.076l-7.37 4.195a.344.344 0 0 0 .001.597l7.99 4.49c.208.116.461.116.669 0l8.034-4.468a.343.343 0 0 0 .003-.598l-2.507-1.424a.683.683 0 0 0-.67-.003l-2.647 1.468a.234.234 0 0 0-.119.18l-.001.025c0 .946-.921 1.714-2.056 1.714s-2.056-.768-2.056-1.714c0-.947.921-1.715 2.056-1.715.042 0 .09.002.145.007l.087.008.096.013a.685.685 0 0 0 .425-.08l3.913-2.173c.3-.166.662-.171.965-.017l.04.023 5.465 3.104c.686.39.693 1.368.03 1.773l-.037.021-3.656 2.033a.343.343 0 0 0 .007.604l3.62 1.906c.72.378.736 1.402.03 1.804l-10.995 6.272a1.03 1.03 0 0 1-1.019 0L.526 16.43a1.03 1.03 0 0 1 .034-1.806l3.66-1.911a.343.343 0 0 0 .01-.603L.524 10.029a1.03 1.03 0 0 1-.041-1.77l.036-.021L9.618 3.06a.688.688 0 0 0 .308-.369l.011-.036c.32-.952 1.046-1.466 2.063-1.466Zm5.076 12.63-4.492 2.586-.041.022c-.306.158-.671.152-.973-.018l-4.478-2.527a.682.682 0 0 0-.65-.01L3.86 15.224a.343.343 0 0 0-.012.602l7.887 4.515c.21.12.467.121.677 0l7.956-4.547a.343.343 0 0 0-.01-.602l-2.623-1.384a.683.683 0 0 0-.659.012z"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,21 +1,73 @@
<?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)
Structure:
- Use <group> elements to organize services into categories
- Each group has a 'name' attribute for the category title
- Services without a group will appear in "Other Services"
Service Fields:
- id: unique identifier (optional)
- name: display name (required)
- proto: protocol - http or https (optional, default: http)
- port: port number (optional, shows info button if present with host)
- host: custom hostname or full URL (optional)
- logo: filename in /logos/ (optional, default: default.svg)
- status: maintenance only (optional) - will override health check
- check-health: true/false (optional, default: true) - disable auto health check
Status Behavior:
- If status="maintenance": Shows orange dot, skips health check
- Otherwise: Automatically pings service URL and shows:
* Gray spinning dot while checking
* Green pulsing dot if online
* Red dot if offline/unreachable
- Set check-health="false" to disable automatic checking
-->
<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" host="git.spatulaa.com"/>
<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" />
<group name="Management">
<service id="portainer" name="Portainer" proto="https" port="9443" logo="portainer.svg" />
<service id="uptime-kuma" name="Uptime Kuma" proto="http" port="3001" logo="uptime-kuma.svg" />
<service id="scrutiny" name="Scrutiny" proto="http" port="8090" logo="scrutiny.svg" />
<service id="zoraxy" name="Zoraxy" proto="http" port="8888" logo="zoraxy.svg" status="maintenance"/>
</group>
<group name="Media">
<service id="jellyfin" name="Jellyfin" proto="http" port="8096" logo="jellyfin.svg" host="jellyfin.spatulaa.com" />
<service id="jellyseer" name="Jellyseer" proto="http" port="5055" logo="jellyfin.svg" host="jellyseer.spatulaa.com" />
<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="tubearchivist" name="Tube Archivist" proto="http" port="8888" logo="youtubedl.svg" />
<service id="wizarr" name="Wizarr" proto="http" port="5690" logo="wizarr.svg" host="wiz.spatulaa.com" />
</group>
<group name="Media Management (*arr Stack)">
<service id="prowlarr" name="Prowlarr" proto="http" port="9696" logo="prowlarr.svg" />
<service id="sonarr" name="Sonarr" proto="http" port="8989" logo="sonarr.svg" />
<service id="radarr" name="Radarr" proto="http" port="7878" logo="radarr.svg" />
<service id="lidarr" name="Lidarr" proto="http" port="8686" logo="lidarr.svg" />
<service id="readarr" name="Readarr" proto="http" port="8787" logo="readarr.svg" />
<service id="bazarr" name="Bazarr" proto="http" port="6767" logo="bazarr.svg" />
</group>
<group name="Storage &amp; Files">
<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="picoshare" name="Picoshare" proto="http" port="4001" logo="picoshare.svg" host="cdn.spatulaa.com" />
</group>
<group name="Development">
<service id="gitea" name="Gitea" proto="http" port="3000" logo="gitea.svg" host="git.spatulaa.com" />
</group>
<group name="AI &amp; Tools">
<service id="open-webui" name="Open WebUI" proto="http" port="3333" logo="openwebui.svg" />
</group>
<group name="Other Services">
<service id="homeassistant" name="Home Assistant" proto="http" port="8123" logo="homeassistant.svg" />
<service id="kiwix" name="Kiwix" proto="http" port="666" logo="kiwix.svg" />
<service id="aurcache-repo" name="Aurcache" proto="http" port="888" logo="aurcache.svg" />
</group>
</services>

View File

@@ -3,21 +3,40 @@
--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}
html,body{height:100%;margin:0;font-family:Inter,Segoe UI,Roboto,Arial,sans-serif;background:#001a2d;color:#e6eef8;overflow-x:hidden}
#galaxy-canvas{position:fixed;top:0;left:0;width:100%;height:100%;z-index:0}
header,main,footer{position:relative;z-index:10}
header{padding:24px 20px;text-align:center;background:rgba(0,26,45,0.7);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
header h1{margin:0;font-size:28px}
.subtitle{color:var(--muted);margin-top:6px}
.search-container{margin-top:16px;max-width:400px;margin-left:auto;margin-right:auto}
#search-input{width:100%;padding:10px 16px;border-radius:8px;border:1px solid rgba(255,255,255,0.15);background:rgba(0,15,30,0.8);color:#e6eef8;font-size:14px;outline:none;transition:all .3s ease;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
#search-input::placeholder{color:var(--muted)}
#search-input:focus{border-color:rgba(79,70,229,0.5);box-shadow:0 0 0 3px rgba(79,70,229,0.2);background:rgba(0,15,30,0.9)}
main{max-width:1100px;margin:18px auto;padding:12px}
.service-group{margin-bottom:32px}
.group-header{font-size:18px;font-weight:600;color:#e6eef8;margin:0 0 12px 0;padding-bottom:8px;border-bottom:1px solid rgba(255,255,255,0.1);position:relative}
.group-header::before{content:'';position:absolute;bottom:-1px;left:0;width:60px;height:2px;background:linear-gradient(90deg,rgba(79,70,229,0.8),rgba(139,92,246,0.5));border-radius:2px}
.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(135deg, rgba(79,70,229,0.1), rgba(139,92,246,0.05), rgba(236,72,153,0.1));text-decoration:none;color:inherit;border:1px solid rgba(255,255,255,0.15);font-weight:600;transition:transform .3s ease,box-shadow .3s ease,border-color .3s ease;position:relative;overflow:hidden;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px)}
.card{display:flex;align-items:center;justify-content:center;padding:18px;border-radius:10px;background:linear-gradient(135deg, rgba(10,37,64,0.85), rgba(15,50,85,0.85), rgba(20,45,75,0.85));text-decoration:none;color:inherit;border:1px solid rgba(255,255,255,0.2);font-weight:600;transition:transform .3s ease,box-shadow .3s ease,border-color .3s ease;position:relative;overflow:hidden;backdrop-filter:blur(15px);-webkit-backdrop-filter:blur(15px)}
.card::before{content:'';position:absolute;top:-50%;left:-50%;width:200%;height:200%;background:linear-gradient(45deg,transparent 30%,rgba(255,255,255,0.08) 50%,transparent 70%);transform:rotate(45deg);animation:shimmer 8s infinite linear;pointer-events:none}
@keyframes shimmer{0%{left:-100%}100%{left:100%}}
.card:hover::before{animation-duration:3s}
.card::after{content:'';position:absolute;inset:0;border-radius:10px;padding:1px;background:linear-gradient(135deg,rgba(79,70,229,0.5),rgba(139,92,246,0.3),rgba(236,72,153,0.5));-webkit-mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);-webkit-mask-composite:xor;mask:linear-gradient(#fff 0 0) content-box,linear-gradient(#fff 0 0);mask-composite:exclude;pointer-events:none;opacity:0;transition:opacity .3s ease}
.card:hover{transform:translateY(-6px);box-shadow:0 10px 40px rgba(79,70,229,0.4),0 0 20px rgba(139,92,246,0.3);border-color:rgba(139,92,246,0.5)}
.card:hover::after{opacity:1}
.card.selected{transform:translateY(-4px);box-shadow:0 8px 30px rgba(79,70,229,0.5),0 0 15px rgba(139,92,246,0.4);border-color:rgba(139,92,246,0.7);outline:2px solid rgba(79,70,229,0.6);outline-offset:2px}
.card .logo{width:36px;height:36px;margin-right:12px;flex:0 0 36px;filter:brightness(0) invert(1)}
.card .label{flex:1;text-align:left}
.status-dot{position:absolute;top:6px;right:6px;width:10px;height:10px;border-radius:50%;border:2px solid rgba(255,255,255,0.3);animation:pulse 2s infinite}
.status-dot.status-online{background:#10b981;box-shadow:0 0 8px rgba(16,185,129,0.6)}
.status-dot.status-offline{background:#ef4444;box-shadow:0 0 8px rgba(239,68,68,0.6);animation:none}
.status-dot.status-maintenance{background:#f59e0b;box-shadow:0 0 8px rgba(245,158,11,0.6)}
.status-dot.status-checking{background:#6b7280;box-shadow:0 0 8px rgba(107,114,128,0.6);animation:spin 1s linear infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.6}}
@keyframes spin{0%{transform:rotate(0deg)}100%{transform:rotate(360deg)}}
.info-btn{position:absolute;bottom:6px;right:6px;width:20px;height:20px;border-radius:50%;background:rgba(79,70,229,0.3);border:1px solid rgba(255,255,255,0.3);color:#fff;font-size:12px;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:all .2s ease;padding:0;line-height:1;backdrop-filter:blur(5px);-webkit-backdrop-filter:blur(5px)}
.info-btn:hover{background:rgba(79,70,229,0.6);border-color:rgba(255,255,255,0.6);transform:scale(1.1)}
.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}