Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
74214ca
UI polish: add formatBytes template func and TFTP breadcrumb data
chadleeshaw Sep 29, 2026
64defaa
UI polish: add formatBytes template func and TFTP breadcrumb data
chadleeshaw Sep 29, 2026
62bef42
UI polish: nav active state, mobile menu behavior, footer, home cards…
chadleeshaw Sep 29, 2026
35f7964
UI polish: nav active state, mobile menu behavior, footer, home cards…
chadleeshaw Sep 29, 2026
db563b1
UI polish: nav active state, mobile menu behavior, footer, home cards…
chadleeshaw Sep 29, 2026
116b116
UI polish: DHCP page - status text, contextual start/stop, toasts, ic…
chadleeshaw Sep 29, 2026
b1bf240
UI polish: TFTP breadcrumbs, status page fixes, provision toasts and …
chadleeshaw Sep 29, 2026
dce3997
UI polish: TFTP breadcrumbs, status page fixes, provision toasts and …
chadleeshaw Sep 29, 2026
b281159
UI polish: TFTP breadcrumbs, status page fixes, provision toasts and …
chadleeshaw Sep 29, 2026
afeeab6
UI polish: TFTP breadcrumbs, status page fixes, provision toasts and …
chadleeshaw Sep 29, 2026
74efec0
UI polish: OS images/syslinux download auto-refresh, progress bars, b…
chadleeshaw Sep 29, 2026
58c507f
UI polish: OS images/syslinux download auto-refresh, progress bars, b…
chadleeshaw Sep 29, 2026
ef498f2
probe: binary upload test
chadleeshaw Sep 29, 2026
b953258
remove binary upload probe
chadleeshaw Sep 29, 2026
f40e311
dhcp: use bg-base-200 for server card to match other pages
chadleeshaw Sep 29, 2026
0801744
login: use bg-base-200 for card to match other pages
chadleeshaw Sep 29, 2026
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
46 changes: 45 additions & 1 deletion handlers/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net"
"net/http"
"os"
"path/filepath"
"strings"
"sync"

Expand Down Expand Up @@ -46,6 +47,49 @@ func LoadTemplates() map[string]*template.Template {
return templates
}

// templateFuncs exposes formatting helpers to every parsed template.
var templateFuncs = template.FuncMap{
"formatBytes": formatBytes,
}

// formatBytes renders a byte count as a human-readable string (e.g. "117.7 MB").
// Non-numeric or negative inputs render as an empty string.
func formatBytes(v any) string {
var b int64
switch n := v.(type) {
case int:
b = int64(n)
case int8:
b = int64(n)
case int16:
b = int64(n)
case int32:
b = int64(n)
case int64:
b = n
case uint:
b = int64(n)
case uint8:
b = int64(n)
case uint16:
b = int64(n)
case uint32:
b = int64(n)
case uint64:
b = int64(n)
case float32:
b = int64(n)
case float64:
b = int64(n)
default:
return ""
}
if b < 0 {
return ""
}
return humanReadableSize(b)
}

// parseTemplates parses every page/modal template exactly once.
// A template that fails to parse is logged and left out of the map —
// callers treat a missing entry as "template not available" instead of
Expand Down Expand Up @@ -75,7 +119,7 @@ func parseTemplates() map[string]*template.Template {

parsed := make(map[string]*template.Template, len(files))
for name, paths := range files {
tmpl, err := template.ParseFiles(paths...)
tmpl, err := template.New(filepath.Base(paths[0])).Funcs(templateFuncs).ParseFiles(paths...)
if err != nil {
log.Printf("Error parsing template %s: %v", name, err)
continue
Expand Down
27 changes: 27 additions & 0 deletions handlers/tftp.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,12 +294,19 @@ type FileInfo struct {
IsDir bool
}

// Breadcrumb is one segment of the TFTP directory trail shown in the UI.
type Breadcrumb struct {
Name string
Dir string
}

// TFTPData holds information for rendering the TFTP management page
type TFTPData struct {
Title string
ServerRunning bool
ServerDirectory string
PrevDirectory string
Breadcrumbs []Breadcrumb
Files []FileInfo
}

Expand Down Expand Up @@ -360,10 +367,30 @@ func (h *TFTPHandlers) getFileInfo(dir string) (*TFTPData, error) {
ServerRunning: isTFTPRunning(),
ServerDirectory: relDir,
PrevDirectory: removeLastDir(relDir),
Breadcrumbs: buildBreadcrumbs(relDir),
Files: fileInfos,
}, nil
}

// buildBreadcrumbs turns a TFTP-relative directory ("", "a", "a/b") into a
// trail of clickable segments for the file browser UI.
func buildBreadcrumbs(relDir string) []Breadcrumb {
if relDir == "" {
return nil
}
parts := strings.Split(relDir, string(filepath.Separator))
crumbs := make([]Breadcrumb, 0, len(parts))
accum := ""
for _, part := range parts {
if accum != "" {
accum += string(filepath.Separator)
}
accum += part
crumbs = append(crumbs, Breadcrumb{Name: part, Dir: accum})
}
return crumbs
}

// mustAbs returns the absolute form of p, falling back to p itself on error.
func mustAbs(p string) string {
abs, err := filepath.Abs(p)
Expand Down
72 changes: 52 additions & 20 deletions templates/base.templ
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,18 @@
<header class="sticky top-0 z-50 shadow-lg bg-neutral text-white">
<div class="container mx-auto px-4 py-2">
<nav class="flex justify-between items-center">
<div class="flex items-center space-x-4">
<img src="/public/http/img/Ignite_small.png" alt="Ignite Logo" class="h-10 -mr-4">
<div class="flex items-center space-x-2">
<img src="/public/http/img/Ignite_small.png" alt="Ignite Logo" class="h-10">
<div class="text-2xl font-bold text-primary">ignite</div>
</div>
<div class="hidden md:flex items-center space-x-4">
<a href="/" class="hover:text-primary transition-colors duration-300"><i class="fas fa-home mr-2"></i>Home</a>
<a href="/dhcp" class="hover:text-primary transition-colors duration-300"><i class="fas fa-network-wired mr-2"></i>DHCP</a>
<a href="/provision" class="hover:text-primary transition-colors duration-300"><i class="fas fa-cogs mr-2"></i>Provision</a>
<a href="/tftp" class="hover:text-primary transition-colors duration-300"><i class="fas fa-server mr-2"></i>TFTP</a>
<a href="/osimages" class="hover:text-primary transition-colors duration-300"><i class="fas fa-compact-disc mr-2"></i>OS Images</a>
<a href="/syslinux" class="hover:text-primary transition-colors duration-300"><i class="fas fa-microchip mr-2"></i>Syslinux</a>
<a href="/status" class="hover:text-primary transition-colors duration-300"><i class="fas fa-info-circle mr-2"></i>Status</a>
<a data-nav href="/" class="hover:text-primary transition-colors duration-300"><i class="fas fa-home mr-2"></i>Home</a>
<a data-nav href="/dhcp" class="hover:text-primary transition-colors duration-300"><i class="fas fa-network-wired mr-2"></i>DHCP</a>
<a data-nav href="/provision" class="hover:text-primary transition-colors duration-300"><i class="fas fa-cogs mr-2"></i>Provision</a>
<a data-nav href="/tftp" class="hover:text-primary transition-colors duration-300"><i class="fas fa-server mr-2"></i>TFTP</a>
<a data-nav href="/osimages" class="hover:text-primary transition-colors duration-300"><i class="fas fa-compact-disc mr-2"></i>OS Images</a>
<a data-nav href="/syslinux" class="hover:text-primary transition-colors duration-300"><i class="fas fa-microchip mr-2"></i>Syslinux</a>
<a data-nav href="/status" class="hover:text-primary transition-colors duration-300"><i class="fas fa-info-circle mr-2"></i>Status</a>

<!-- User Menu -->
<div class="dropdown dropdown-end">
Expand Down Expand Up @@ -60,13 +60,13 @@

<div id="mobile-menu" class="hidden md:hidden fixed top-0 left-0 right-0 z-40 bg-neutral shadow-lg pt-16">
<div class="container mx-auto px-4 py-2">
<a href="/" class="block py-3 text-white hover:text-primary transition-colors duration-300"><i class="fas fa-home mr-2"></i>Home</a>
<a href="/dhcp" class="block py-3 text-white hover:text-primary transition-colors duration-300"><i class="fas fa-network-wired mr-2"></i>DHCP</a>
<a href="/provision" class="block py-3 text-white hover:text-primary transition-colors duration-300"><i class="fas fa-cogs mr-2"></i>Provision</a>
<a href="/tftp" class="block py-3 text-white hover:text-primary transition-colors duration-300"><i class="fas fa-server mr-2"></i>TFTP</a>
<a href="/osimages" class="block py-3 text-white hover:text-primary transition-colors duration-300"><i class="fas fa-compact-disc mr-2"></i>OS Images</a>
<a href="/syslinux" class="block py-3 text-white hover:text-primary transition-colors duration-300"><i class="fas fa-microchip mr-2"></i>Syslinux</a>
<a href="/status" class="block py-3 text-white hover:text-primary transition-colors duration-300"><i class="fas fa-info-circle mr-2"></i>Status</a>
<a data-nav href="/" class="block py-3 text-white hover:text-primary transition-colors duration-300"><i class="fas fa-home mr-2"></i>Home</a>
<a data-nav href="/dhcp" class="block py-3 text-white hover:text-primary transition-colors duration-300"><i class="fas fa-network-wired mr-2"></i>DHCP</a>
<a data-nav href="/provision" class="block py-3 text-white hover:text-primary transition-colors duration-300"><i class="fas fa-cogs mr-2"></i>Provision</a>
<a data-nav href="/tftp" class="block py-3 text-white hover:text-primary transition-colors duration-300"><i class="fas fa-server mr-2"></i>TFTP</a>
<a data-nav href="/osimages" class="block py-3 text-white hover:text-primary transition-colors duration-300"><i class="fas fa-compact-disc mr-2"></i>OS Images</a>
<a data-nav href="/syslinux" class="block py-3 text-white hover:text-primary transition-colors duration-300"><i class="fas fa-microchip mr-2"></i>Syslinux</a>
<a data-nav href="/status" class="block py-3 text-white hover:text-primary transition-colors duration-300"><i class="fas fa-info-circle mr-2"></i>Status</a>
</div>
</div>

Expand Down Expand Up @@ -101,14 +101,46 @@
</div>
</div>

<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
<footer class="mt-12 pb-6 text-center text-sm text-base-content/40">
<p>Ignite &mdash; Network Boot &amp; Provisioning</p>
</footer>

<script src="https://unpkg.com/htmx.org@1.9.9/dist/htmx.min.js" defer></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/js/all.min.js"></script>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
<script>
$(document).ready(function() {
$('#menu-btn').click(function() {
$('#mobile-menu').toggleClass('hidden');
// Mobile menu: toggle, close on link tap, close on outside tap.
document.addEventListener('DOMContentLoaded', function() {
var menuBtn = document.getElementById('menu-btn');
var mobileMenu = document.getElementById('mobile-menu');
if (menuBtn && mobileMenu) {
menuBtn.addEventListener('click', function(e) {
e.stopPropagation();
mobileMenu.classList.toggle('hidden');
});
mobileMenu.querySelectorAll('a').forEach(function(a) {
a.addEventListener('click', function() {
mobileMenu.classList.add('hidden');
});
});
document.addEventListener('click', function(e) {
if (!mobileMenu.classList.contains('hidden') && !mobileMenu.contains(e.target)) {
mobileMenu.classList.add('hidden');
}
});
}

// Highlight the nav link for the current page.
var path = window.location.pathname;
document.querySelectorAll('a[data-nav]').forEach(function(a) {
var href = a.getAttribute('href');
var active = href === '/'
? path === '/'
: (path === href || path.indexOf(href + '/') === 0);
if (active) {
a.classList.add('text-primary', 'font-semibold');
a.setAttribute('aria-current', 'page');
}
});
});

Expand Down
81 changes: 57 additions & 24 deletions templates/pages/dhcp.templ
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,25 @@
<div id="modal-content"></div>

{{range .Servers}}
<div class="card bg-neutral shadow-xl mb-8">
<div class="card bg-base-200 shadow-xl mb-8">
<div class="card-body overflow-visible">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center space-x-3">
<span class="ip-address text-2xl font-bold text-primary">{{ .TFTPIP }}</span>
<span class="badge {{ .Status }} badge-lg"></span>
<span class="badge {{ .Status }} badge-lg">{{if eq .Status "badge-success"}}Running{{else}}Stopped{{end}}</span>
</div>
<div class="flex items-center space-x-2">
<button class="btn btn-sm btn-success tooltip tooltip-bottom" data-tip="Start Server" hx-post="/dhcp/start?server_id={{ .ID }}" hx-target="body" hx-swap="innerHTML"><i class="fas fa-play"></i></button>
<button class="btn btn-sm btn-error tooltip tooltip-bottom" data-tip="Stop Server" hx-post="/dhcp/stop?server_id={{ .ID }}" hx-target="body" hx-swap="innerHTML"><i class="fas fa-stop"></i></button>
{{if eq .Status "badge-success"}}
<button class="btn btn-sm btn-error tooltip tooltip-bottom" data-tip="Stop Server" hx-post="/dhcp/stop?server_id={{ .ID }}" hx-target="body" hx-swap="innerHTML"><i class="fas fa-stop mr-1"></i>Stop</button>
{{else}}
<button class="btn btn-sm btn-success tooltip tooltip-bottom" data-tip="Start Server" hx-post="/dhcp/start?server_id={{ .ID }}" hx-target="body" hx-swap="innerHTML"><i class="fas fa-play mr-1"></i>Start</button>
{{end}}
<button class="btn btn-sm btn-info tooltip tooltip-bottom" data-tip="Edit Server" hx-get="/open_modal?template=dhcpmodal&server_id={{ .ID }}" hx-target="#modal-content" hx-swap="innerHTML"><i class="fas fa-edit"></i></button>
<button class="btn btn-sm btn-warning tooltip tooltip-bottom" data-tip="Delete Server" hx-post="/dhcp/delete?server_id={{ .ID }}" hx-target="body" hx-swap="innerHTML"><i class="fas fa-trash"></i></button>
</div>
</div>

<div style="overflow: visible;">
<div class="overflow-x-auto md:overflow-visible">
<table class="table w-full">
<thead>
<tr>
Expand All @@ -50,14 +53,14 @@
<td>{{ .MAC }}</td>
<td>{{ .IP }}</td>
<td class="text-center">
<div class="badge {{.StateBadgeClass}} badge-sm">{{.StateDisplayName}}</div>
<div id="lease-state-{{.MAC}}" class="badge {{.StateBadgeClass}} badge-sm">{{.StateDisplayName}}</div>
</td>
<td class="text-center">
<button id="reserveBtn-{{ .MAC }}" class="modal-opener tooltip tooltip-top" data-tip="{{if .Static}}Reserved IP{{else}}Reserve IP{{end}}" hx-get="/open_modal?template=reservemodal&network={{$tftpip}}&mac={{.MAC}}" hx-target="#modal-content" hx-swap="innerHTML">
{{if .Static}}
<i class="fas fa-check-circle text-success text-lg"></i>
<i class="fas fa-bookmark text-success text-lg"></i>
{{else}}
<i class="fas fa-times-circle text-error text-lg"></i>
<i class="far fa-bookmark text-base-content/40 text-lg"></i>
{{end}}
</button>
</td>
Expand All @@ -74,15 +77,15 @@
<i class="fas fa-flag"></i>
</button>
<ul tabindex="0" class="dropdown-content z-[9999] menu p-2 shadow bg-base-100 rounded-box w-52">
<li><a onclick="updateLeaseState('{{.MAC}}', 'assigned')">🔵 Assigned</a></li>
<li><a onclick="updateLeaseState('{{.MAC}}', 'pxe_requested')">⚠️ PXE Requested</a></li>
<li><a onclick="updateLeaseState('{{.MAC}}', 'booting')">🚀 Booting</a></li>
<li><a onclick="updateLeaseState('{{.MAC}}', 'imaging')">💿 Imaging</a></li>
<li><a onclick="updateLeaseState('{{.MAC}}', 'imaged')">✅ Imaged</a></li>
<li><a onclick="updateLeaseState('{{.MAC}}', 'configuring')">⚙️ Configuring</a></li>
<li><a onclick="updateLeaseState('{{.MAC}}', 'complete')">🎉 Complete</a></li>
<li><a onclick="updateLeaseState('{{.MAC}}', 'failed')">❌ Failed</a></li>
<li><a onclick="updateLeaseState('{{.MAC}}', 'offline')">👻 Offline</a></li>
<li><a onclick="updateLeaseState('{{.MAC}}', 'assigned')"><i class="fas fa-circle text-info text-xs mr-2"></i>Assigned</a></li>
<li><a onclick="updateLeaseState('{{.MAC}}', 'pxe_requested')"><i class="fas fa-circle text-warning text-xs mr-2"></i>PXE Requested</a></li>
<li><a onclick="updateLeaseState('{{.MAC}}', 'booting')"><i class="fas fa-circle text-warning text-xs mr-2"></i>Booting</a></li>
<li><a onclick="updateLeaseState('{{.MAC}}', 'imaging')"><i class="fas fa-circle text-accent text-xs mr-2"></i>Imaging</a></li>
<li><a onclick="updateLeaseState('{{.MAC}}', 'imaged')"><i class="fas fa-circle text-success text-xs mr-2"></i>Imaged</a></li>
<li><a onclick="updateLeaseState('{{.MAC}}', 'configuring')"><i class="fas fa-circle text-accent text-xs mr-2"></i>Configuring</a></li>
<li><a onclick="updateLeaseState('{{.MAC}}', 'complete')"><i class="fas fa-circle text-success text-xs mr-2"></i>Complete</a></li>
<li><a onclick="updateLeaseState('{{.MAC}}', 'failed')"><i class="fas fa-circle text-error text-xs mr-2"></i>Failed</a></li>
<li><a onclick="updateLeaseState('{{.MAC}}', 'offline')"><i class="fas fa-circle text-base-content/40 text-xs mr-2"></i>Offline</a></li>
</ul>
</div>
<button class="btn btn-xs btn-secondary tooltip tooltip-top" data-tip="View History" onclick="showLeaseHistory('{{.MAC}}')">
Expand All @@ -109,29 +112,59 @@
{{end}}

<script>
function toast(message, isSuccess) {
Toastify({
text: message,
duration: 3000,
close: true,
gravity: "top",
position: "right",
backgroundColor: isSuccess
? "linear-gradient(to right, #00b09b, #96c93d)"
: "linear-gradient(to right, #FBBF24, #FF4500)",
}).showToast();
}

var stateDisplayNames = {
'assigned': 'Assigned',
'pxe_requested': 'PXE Requested',
'booting': 'Booting',
'imaging': 'Imaging',
'imaged': 'Imaged',
'configuring': 'Configuring',
'complete': 'Complete',
'failed': 'Failed',
'offline': 'Offline'
};

function updateLeaseState(mac, newState) {
if (!newState) return;

const formData = new FormData();
formData.append('state', newState);
formData.append('source', 'manual');

fetch('/dhcp/lease/state?mac=' + encodeURIComponent(mac), {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Refresh the page to show updated state
window.location.reload();
// Update the badge in place instead of reloading the page
const badge = document.getElementById('lease-state-' + mac);
if (badge) {
badge.className = 'badge ' + getStateBadgeClass(newState) + ' badge-sm';
badge.textContent = stateDisplayNames[newState] || newState;
}
toast('Lease state updated to ' + (stateDisplayNames[newState] || newState), true);
} else {
alert('Failed to update lease state: ' + (data.message || 'Unknown error'));
toast('Failed to update lease state: ' + (data.message || 'Unknown error'), false);
}
})
.catch(error => {
console.error('Error updating lease state:', error);
alert('Failed to update lease state');
toast('Failed to update lease state', false);
});
}

Expand Down Expand Up @@ -174,7 +207,7 @@ function showLeaseHistory(mac) {
})
.catch(error => {
console.error('Error fetching lease history:', error);
alert('Failed to load lease history');
toast('Failed to load lease history', false);
});
}

Expand Down
Loading