Changes
diff --git a/model-switcher/main.py b/model-switcher/main.py
index 36607a4..9f79b3c 100644
--- a/model-switcher/main.py
+++ b/model-switcher/main.py
@@ -64,6 +64,8 @@ class RequestHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/api/models":
self.handleGetModels()
+ elif self.path == "/api/logs":
+ self.handleGetLogs()
else:
self.serveStatic()
@@ -108,6 +110,26 @@ class RequestHandler(http.server.BaseHTTPRequestHandler):
except Exception as e:
self.send_json_response(500, {"error": str(e)})
+ def handleGetLogs(self):
+ try:
+ # -u: service name
+ # -n 100: last 100 lines
+ # --no-pager: don't pipe to less/more
+ # -r: reverse order (newest first)
+ cmd = ["journalctl", "-u", SERVICE_NAME, "-n", "100", "--no-pager", "-r"]
+ result = subprocess.run(cmd, capture_output=True, text=True, check=False)
+
+ if result.returncode != 0:
+ # If journalctl fails, it might be due to permissions or service not found
+ # We return the error message
+ logs = [f"Error fetching logs: {result.stderr.strip() or 'Unknown error'}"]
+ else:
+ logs = result.stdout.splitlines()
+
+ self.send_json_response(200, {"logs": logs})
+ except Exception as e:
+ self.send_json_response(500, {"error": str(e)})
+
def handleSwitchModel(self):
content_length = int(self.headers.get('Content-Length', 0))
post_data = self.rfile.read(content_length)
diff --git a/model-switcher/static/index.html b/model-switcher/static/index.html
index 4c8755e..88667e2 100644
--- a/model-switcher/static/index.html
+++ b/model-switcher/static/index.html
@@ -23,6 +23,10 @@
const [loading, setLoading] = useState(true);
const [switching, setSwitching] = useState(false);
const [message, setMessage] = useState({ text: '', type: '' });
+
+ // Log state
+ const [logs, setLogs] = useState([]);
+ const [loadingLogs, setLoadingLogs] = useState(false);
const fetchModels = async () => {
try {
@@ -41,6 +45,23 @@
}
};
+ const fetchLogs = async () => {
+ try {
+ setLoadingLogs(true);
+ const response = await fetch('/api/logs');
+ const data = await response.json();
+ if (data.error) throw new Error(data.error);
+
+ setLogs(data.logs || []);
+ } catch (err) {
+ // Just log to console or show a small error in the log section?
+ // We'll put a fake log entry for error
+ setLogs(['Error fetching logs: ' + err.message]);
+ } finally {
+ setLoadingLogs(false);
+ }
+ };
+
const switchModel = async () => {
if (!selectedModel || selectedModel === currentModel) return;
@@ -59,6 +80,8 @@
setMessage({ text: 'Model switched successfully!', type: 'success' });
setCurrentModel(selectedModel);
+ // Refresh logs after switch
+ setTimeout(fetchLogs, 2000);
} catch (err) {
setMessage({ text: 'Failed to switch model: ' + err.message, type: 'error' });
} finally {
@@ -68,6 +91,7 @@
useEffect(() => {
fetchModels();
+ fetchLogs();
}, []);
if (loading) return html`<div class="container"><p>Loading models...</p></div>`;
@@ -109,7 +133,7 @@
>
${switching ? 'Restarting...' : 'Apply Selection'}
</button>
- <button class="secondary" onClick="${fetchModels}" disabled="${switching}">Refresh</button>
+ <button class="secondary" onClick="${fetchModels}" disabled="${switching}">Refresh Models</button>
</div>
${message.text && html`
@@ -117,6 +141,21 @@
${message.text}
</div>
`}
+
+ <div class="log-section">
+ <div class="log-header">
+ <h2>Service Logs</h2>
+ <button class="secondary" style="width: auto; padding: 0.4rem 0.8rem; font-size: 0.8rem;" onClick="${fetchLogs}" disabled="${loadingLogs}">
+ ${loadingLogs ? 'Refreshing...' : 'Refresh Logs'}
+ </button>
+ </div>
+ <div class="log-viewer">
+ ${logs.length === 0
+ ? html`<div class="log-entry">No logs available or service not found.</div>`
+ : logs.map((line, i) => html`<div class="log-entry" key="${i}">${line}</div>`)
+ }
+ </div>
+ </div>
</main>
</div>
`;
@@ -125,4 +164,4 @@
render(html`<${App} />`, document.getElementById('app'));
</script>
</body>
-</html>
+</html>
\ No newline at end of file
diff --git a/model-switcher/static/style.css b/model-switcher/static/style.css
index a66f781..0387917 100644
--- a/model-switcher/static/style.css
+++ b/model-switcher/static/style.css
@@ -9,6 +9,8 @@
--error-color: #ef4444;
--info-color: #3b82f6;
--border-color: #e2e8f0;
+ --log-bg: #1e293b;
+ --log-text: #e2e8f0;
}
body {
@@ -111,6 +113,7 @@ code {
.actions {
display: flex;
gap: 1rem;
+ margin-bottom: 1.5rem;
}
button {
@@ -168,3 +171,59 @@ button:disabled {
animation: spin 1s linear infinite;
margin-left: 8px;
}
+
+/* Log Viewer Styles */
+.log-section {
+ margin-top: 2rem;
+ border-top: 1px solid var(--border-color);
+ padding-top: 1rem;
+}
+
+.log-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 0.5rem;
+}
+
+.log-header h2 {
+ font-size: 1.2rem;
+ color: var(--text-color);
+ margin: 0;
+}
+
+.log-viewer {
+ background-color: var(--log-bg);
+ color: var(--log-text);
+ padding: 1rem;
+ border-radius: 8px;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ font-size: 0.85rem;
+ height: 300px;
+ overflow-y: auto;
+ white-space: pre-wrap;
+ word-break: break-all;
+ display: flex;
+ flex-direction: column-reverse; /* Since we get logs reverse (newest first), but typically logs are read top-to-bottom or bottom-to-top.
+ The user said "newest line first". So top of the container should be newest.
+ Wait, if I get a list ["newest", "2nd newest", ...], simply rendering them in order will put newest at top.
+ So column-reverse might NOT be needed if I just render them in the order I receive them.
+ Actually, usually log viewers put newest at the bottom and auto-scroll.
+ But the user specifically requested "newest line first".
+ This implies:
+ Line 1: (Newest)
+ Line 2: (Older)
+ ...
+ So I should render them in the order I receive them (since journalctl -r gives newest first).
+ */
+ flex-direction: column;
+}
+
+.log-entry {
+ padding: 2px 0;
+ border-bottom: 1px solid rgba(255,255,255,0.1);
+}
+
+.log-entry:last-child {
+ border-bottom: none;
+}
\ No newline at end of file