Add service grouping feature to README and implement dynamic rendering in index.html
This commit is contained in:
90
README.md
90
README.md
@@ -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
|
||||
|
||||
|
||||
83
index.html
83
index.html
@@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
1
logos/simple-icons.manual-backup-20251123-234134
Submodule
1
logos/simple-icons.manual-backup-20251123-234134
Submodule
Submodule logos/simple-icons.manual-backup-20251123-234134 added at 3dd8ab9a29
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user