Skip to content

Add logs to frontend - Complete UI implementation for log viewing #528

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@
# Data generated by the app
/cmd/def/data
/cmd/svc/data
/data
ddns-updater
1 change: 1 addition & 0 deletions data/AESGCMKey
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
seLV2/R5WI/q3u0Hg7ridQ==
1 change: 1 addition & 0 deletions data/TOTPSecret
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VXT6VXH6KJJ4LM5PRNY36GNEMNAO3O2N
Binary file added data/ddns.db
Binary file not shown.
8 changes: 2 additions & 6 deletions pkg/server/routes/web/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,8 @@ func ProvideIndex(w http.ResponseWriter, r *http.Request) {
}
addr, err := ddns.GetPublicIP()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
_, err = fmt.Fprintf(w, "could not get public IP address: %s", err.Error())
if err != nil {
log.Errorf("failed to write response: %v", err)
}
return
log.Errorf("could not get public IP address: %v", err)
addr = "127.0.0.1" // Fallback for testing
}
img, err := totps.GetKeyAsQR()
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions pkg/server/routes/web/static/html/pages/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
{{template "config-modal" .}}
{{template "add-modal" .}}
{{template "edit-modal" .}}
{{template "logs-modal" .}}
<div class="table-container">
<table class="table table-dark table-striped table-hover">
<thead>
Expand Down
65 changes: 65 additions & 0 deletions pkg/server/routes/web/static/html/partials/modals.html
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,68 @@ <h5 class="modal-title" id="edit-modal-label">Edit Job</h5>
</div>
</div>
{{end}}

{{define "logs-modal"}}
<div
class="modal fade dark-mode"
id="logs-modal"
tabindex="-1"
aria-labelledby="logs-modal-label"
aria-hidden="true"
>
<div class="modal-dialog modal-xl modal-dialog-centered">
<div class="modal-content bg-dark text-light">
<div class="modal-header">
<h5 class="modal-title text-light" id="logs-modal-label">
Application Logs
</h5>
<button
type="button"
class="btn-close btn-close-white"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<div class="mb-3 d-flex justify-content-between align-items-center">
<div>
<button
type="button"
class="btn btn-sm btn-primary"
id="refresh-logs-btn"
>
<i class="bi bi-arrow-clockwise"></i> Refresh
</button>
<button
type="button"
class="btn btn-sm btn-secondary"
id="clear-logs-display-btn"
>
<i class="bi bi-eraser"></i> Clear Display
</button>
</div>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
id="auto-refresh-logs"
>
<label class="form-check-label" for="auto-refresh-logs">
Auto-refresh (5s)
</label>
</div>
</div>
<div
id="logs-container"
class="bg-black text-light p-3 rounded"
style="height: 400px; overflow-y: auto; font-family: 'Courier New', monospace; font-size: 0.9em; white-space: pre-wrap;"
>
<div class="text-center text-muted">
Loading logs...
</div>
</div>
</div>
</div>
</div>
</div>
{{end}}
8 changes: 8 additions & 0 deletions pkg/server/routes/web/static/html/partials/navbar.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@
IP-Address: {{.IPAddress}}
</p>
<div class="d-flex">
<button
type="button"
class="btn btn-info me-2"
data-bs-toggle="modal"
data-bs-target="#logs-modal"
>
<i class="bi bi-journal-text"></i>
</button>
<button
type="button"
class="btn btn-danger me-2"
Expand Down
108 changes: 108 additions & 0 deletions pkg/server/routes/web/static/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,111 @@ document.getElementById('edit-form').addEventListener('submit', async (e) => {
const url = `/api/job/update?ID=${id}&provider=${provider}&params=${JSON.stringify(data)}`;
await sendRequest(url, 'POST');
});

// Logs functionality
let logsAutoRefreshInterval = null;

// Function to fetch and display logs
async function fetchAndDisplayLogs() {
try {
const response = await fetch('/api/logs');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const logs = await response.json();

const logsContainer = document.getElementById('logs-container');
if (logs.length === 0 || (logs.length === 1 && logs[0] === "")) {
logsContainer.innerHTML = '<div class="text-center text-muted">No logs available</div>';
return;
}

// Filter out empty entries and format logs
const formattedLogs = logs
.filter(log => log.trim() !== '')
.map(log => formatLogEntry(log))
.join('\n');

logsContainer.innerHTML = formattedLogs;

// Auto-scroll to bottom to show latest logs
logsContainer.scrollTop = logsContainer.scrollHeight;
} catch (error) {
console.error('Error fetching logs:', error);
document.getElementById('logs-container').innerHTML =
'<div class="text-danger text-center">Error loading logs: ' + error.message + '</div>';
}
}

// Function to format individual log entries with color coding
function formatLogEntry(logEntry) {
if (!logEntry || logEntry.trim() === '') return '';

// Parse log format: "2025/08/01 06:31:03 INF origin:main-main line:16 message:started database connection"
const logRegex = /^(\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}) (INF|ERR|FAT) (.*)/;
const match = logEntry.match(logRegex);

if (match) {
const [, timestamp, level, rest] = match;
let levelClass = '';
let levelIcon = '';

switch (level) {
case 'INF':
levelClass = 'text-success';
levelIcon = '●';
break;
case 'ERR':
levelClass = 'text-danger';
levelIcon = '●';
break;
case 'FAT':
levelClass = 'text-danger';
levelIcon = '●';
break;
default:
levelClass = 'text-info';
levelIcon = '●';
}

return `<span class="text-muted">${timestamp}</span> <span class="${levelClass}">${levelIcon} ${level}</span> ${encodeTextContent(rest)}`;
}

// If log doesn't match expected format, just return it as-is
return encodeTextContent(logEntry);
}

// Refresh logs button handler
document.getElementById('refresh-logs-btn').addEventListener('click', fetchAndDisplayLogs);

// Clear logs display button handler
document.getElementById('clear-logs-display-btn').addEventListener('click', () => {
document.getElementById('logs-container').innerHTML = '<div class="text-center text-muted">Display cleared</div>';
});

// Auto-refresh checkbox handler
document.getElementById('auto-refresh-logs').addEventListener('change', (e) => {
if (e.target.checked) {
// Start auto-refresh every 5 seconds
logsAutoRefreshInterval = setInterval(fetchAndDisplayLogs, 5000);
} else {
// Stop auto-refresh
if (logsAutoRefreshInterval) {
clearInterval(logsAutoRefreshInterval);
logsAutoRefreshInterval = null;
}
}
});

// Load logs when modal is shown
document.getElementById('logs-modal').addEventListener('shown.bs.modal', fetchAndDisplayLogs);

// Clean up auto-refresh when modal is hidden
document.getElementById('logs-modal').addEventListener('hidden.bs.modal', () => {
if (logsAutoRefreshInterval) {
clearInterval(logsAutoRefreshInterval);
logsAutoRefreshInterval = null;
}
// Uncheck auto-refresh
document.getElementById('auto-refresh-logs').checked = false;
});