Add service grouping feature to README and implement dynamic rendering in index.html

This commit is contained in:
MayaChat
2025-11-24 00:09:47 -05:00
parent 5ef15cc692
commit 67cb864342
4 changed files with 151 additions and 26 deletions

View File

@@ -19,6 +19,7 @@ A lightweight, self-hosted dashboard for quick access to your Docker services wi
- 🎮 **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
@@ -32,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>
```
@@ -146,6 +163,44 @@ Status colors:
- 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
@@ -370,15 +425,18 @@ 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 (automatic ping/availability detection)
- Drag-and-drop reordering
- Categorized sections/groups
- 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

@@ -16,8 +16,8 @@
</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">
@@ -35,7 +35,7 @@
<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
@@ -46,9 +46,61 @@
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 if we have groups or just services
const groups = Array.from(doc.getElementsByTagName('group'));
const hasGroups = groups.length > 0;
if(hasGroups){
// Render grouped services
groups.forEach(group => {
const groupName = group.getAttribute('name') || 'Services';
const services = Array.from(group.getElementsByTagName('service'));
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') || '';
@@ -170,14 +222,18 @@
a.appendChild(infoBtn);
}
grid.appendChild(a);
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)){
@@ -187,14 +243,21 @@
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 = grid.querySelector('.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}"`;
grid.appendChild(msg);
container.appendChild(msg);
}
});

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

View File

@@ -12,6 +12,9 @@ header h1{margin:0;font-size:28px}
#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(255,255,255,0.08)}
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::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}