Fix apps dropdown menu positioning near the grid button

- Changed apps-dropdown right position from 60px to 0 in app.css
- Wrapped apps button and dropdown in a container with position:relative
- Moved dropdown to be a sibling of the button inside the container
- Removed duplicate dropdown from header-right section
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-01-03 17:19:17 -03:00
parent 2c852715f8
commit b4c49314a6
35 changed files with 2800 additions and 1631 deletions

View file

@ -67,7 +67,7 @@
<aside class="metrics-sidebar">
<div
class="metric-card"
hx-get="/api/analytics/messages/count"
hx-get="/api/ui/analytics/messages/count"
hx-trigger="load, every 30s"
hx-swap="innerHTML"
>
@ -90,7 +90,7 @@
<div
class="metric-card"
hx-get="/api/analytics/sessions/active"
hx-get="/api/ui/analytics/sessions/active"
hx-trigger="load, every 10s"
hx-swap="innerHTML"
>
@ -127,7 +127,7 @@
<div
class="metric-card"
hx-get="/api/analytics/response/avg"
hx-get="/api/ui/analytics/response/avg"
hx-trigger="load, every 30s"
hx-swap="innerHTML"
>
@ -150,7 +150,7 @@
<div
class="metric-card"
hx-get="/api/analytics/llm/tokens"
hx-get="/api/ui/analytics/llm/tokens"
hx-trigger="load, every 60s"
hx-swap="innerHTML"
>
@ -187,7 +187,7 @@
<div
class="metric-card"
hx-get="/api/analytics/storage/usage"
hx-get="/api/ui/analytics/storage/usage"
hx-trigger="load, every 120s"
hx-swap="innerHTML"
>
@ -221,7 +221,7 @@
<div
class="metric-card"
hx-get="/api/analytics/errors/count"
hx-get="/api/ui/analytics/errors/count"
hx-trigger="load, every 30s"
hx-swap="innerHTML"
>
@ -289,7 +289,7 @@
<div
class="chart-container"
id="messagesChart"
hx-get="/api/analytics/timeseries/messages"
hx-get="/api/ui/analytics/timeseries/messages"
hx-trigger="load, every 60s"
hx-swap="innerHTML"
>
@ -324,7 +324,7 @@
<div
class="chart-container"
id="responseChart"
hx-get="/api/analytics/timeseries/response_time"
hx-get="/api/ui/analytics/timeseries/response_time"
hx-trigger="load, every 60s"
hx-swap="innerHTML"
>
@ -343,7 +343,7 @@
<div
class="chart-container"
id="channelChart"
hx-get="/api/analytics/channels/distribution"
hx-get="/api/ui/analytics/channels/distribution"
hx-trigger="load, every 120s"
hx-swap="innerHTML"
>
@ -362,7 +362,7 @@
<div
class="chart-container"
id="botChart"
hx-get="/api/analytics/bots/performance"
hx-get="/api/ui/analytics/bots/performance"
hx-trigger="load, every 60s"
hx-swap="innerHTML"
>
@ -504,7 +504,7 @@
<div
class="activity-feed"
id="activityFeed"
hx-get="/api/analytics/activity/recent"
hx-get="/api/ui/analytics/activity/recent"
hx-trigger="load, every 15s"
hx-swap="innerHTML"
>
@ -514,7 +514,7 @@
<h3 class="mt-4">Top Queries</h3>
<div
class="top-queries"
hx-get="/api/analytics/queries/top"
hx-get="/api/ui/analytics/queries/top"
hx-trigger="load, every 60s"
hx-swap="innerHTML"
>
@ -598,7 +598,7 @@
messagesContainer.scrollTop = messagesContainer.scrollHeight;
try {
const response = await fetch("/api/analytics/query", {
const response = await fetch("/api/ui/analytics/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",

View file

@ -73,7 +73,7 @@ async function sendAnalyticsQuery() {
messagesContainer.scrollTop = messagesContainer.scrollHeight;
try {
const response = await fetch("/api/analytics/query", {
const response = await fetch("/api/ui/analytics/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",

View file

@ -14,7 +14,7 @@
<!-- Quick Actions -->
<div class="sidebar-actions">
<button class="btn-primary-full" id="new-event-btn"
hx-get="/ui/calendar/event/new"
hx-get="/api/ui/calendar/events/new"
hx-target="#event-modal-content"
hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -55,7 +55,7 @@
<div class="section-header">
<h3>My Calendars</h3>
<button class="btn-icon-sm" title="Add Calendar"
hx-get="/ui/calendar/new"
hx-get="/api/ui/calendar/calendars/new"
hx-target="#calendar-modal-content">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
@ -64,7 +64,7 @@
</button>
</div>
<div class="calendars-list" id="calendars-list"
hx-get="/ui/calendar/list"
hx-get="/api/ui/calendar/calendars"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Calendars loaded here -->
@ -75,7 +75,7 @@
<div class="sidebar-section">
<h3>Upcoming</h3>
<div class="upcoming-events" id="upcoming-events"
hx-get="/ui/calendar/upcoming"
hx-get="/api/ui/calendar/events/upcoming"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Upcoming events loaded here -->
@ -137,12 +137,6 @@
<button class="view-btn active" data-view="week">Week</button>
<button class="view-btn" data-view="month">Month</button>
</div>
<button class="btn-icon" id="calendar-settings" title="Settings">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-2 2 2 2 0 01-2-2v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83 0 2 2 0 010-2.83l.06-.06a1.65 1.65 0 00.33-1.82 1.65 1.65 0 00-1.51-1H3a2 2 0 01-2-2 2 2 0 012-2h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 010-2.83 2 2 0 012.83 0l.06.06a1.65 1.65 0 001.82.33H9a1.65 1.65 0 001-1.51V3a2 2 0 012-2 2 2 0 012 2v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 0 2 2 0 010 2.83l-.06.06a1.65 1.65 0 00-.33 1.82V9a1.65 1.65 0 001.51 1H21a2 2 0 012 2 2 2 0 01-2 2h-.09a1.65 1.65 0 00-1.51 1z"></path>
</svg>
</button>
</div>
</div>
@ -423,7 +417,7 @@
<div class="form-group">
<label>Import to calendar</label>
<select name="calendar_id"
hx-get="/ui/calendar/list"
hx-get="/api/ui/calendar/calendars"
hx-trigger="load"
hx-swap="innerHTML">
<option value="">Default Calendar</option>

View file

@ -1,5 +1,84 @@
/* Chat module styles - including chat layout and projector component */
/* Toast Notifications */
.toast-container {
position: fixed;
top: 80px;
right: 24px;
display: flex;
flex-direction: column;
gap: 12px;
z-index: 10002;
}
.toast {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 18px;
background: var(--surface, #1a1a24);
border: 1px solid var(--border, #2a2a2a);
border-radius: 10px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.6);
animation: toast-in 0.3s ease;
}
.toast-success {
border-color: var(--success, #22c55e);
}
.toast-error {
border-color: var(--error, #ef4444);
}
.toast-warning {
border-color: var(--warning, #f59e0b);
}
.toast-info {
border-color: var(--accent, #3b82f6);
}
.toast-icon {
font-size: 16px;
}
.toast-message {
font-size: 13px;
color: #ffffff;
}
.toast-close {
background: transparent;
border: none;
color: #888888;
font-size: 18px;
cursor: pointer;
padding: 0;
line-height: 1;
}
.toast-close:hover {
color: #ffffff;
}
.toast-fade-out {
opacity: 0;
transform: translateX(20px);
transition: all 0.3s ease;
}
@keyframes toast-in {
from {
opacity: 0;
transform: translateX(20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
/* Chat Layout */
.chat-layout {
display: flex;
@ -15,30 +94,7 @@
box-sizing: border-box;
}
/* Connection Status */
.connection-status {
position: fixed;
top: 80px;
right: 20px;
width: 10px;
height: 10px;
border-radius: 50%;
z-index: 1000;
transition: all 0.3s;
}
.connection-status.connecting {
background: #f59e0b;
animation: pulse 1.5s infinite;
}
.connection-status.connected {
background: #10b981;
}
.connection-status.disconnected {
background: #ef4444;
}
/* Connection Status - use shared styles from app.css */
@keyframes pulse {
0%,
@ -96,21 +152,95 @@
}
.user-message {
background: var(--accent-color, #3b82f6);
color: white;
background: var(--accent, var(--primary, #3b82f6));
color: #ffffff !important;
border-bottom-right-radius: 4px;
}
/* Light accent themes need dark text on user messages */
[data-theme="sentient"] .user-message,
[data-theme="y2kglow"] .user-message,
[data-theme="arcadeflash"] .user-message,
[data-theme="green"] .user-message,
[data-theme="jazzage"] .user-message,
[data-theme="mellowgold"] .user-message,
[data-theme="polaroidmemories"] .user-message,
[data-theme="seasidepostcard"] .user-message,
[data-theme="saturdaycartoons"] .user-message,
[data-theme="light"] .user-message,
[data-theme="typewriter"] .user-message,
[data-theme="3dbevel"] .user-message {
color: #000000 !important;
}
.bot-message {
background: var(--secondary-bg, #f3f4f6);
color: var(--text-primary, #1f2937);
background: var(--surface, var(--card, #2a2a2a));
color: #ffffff !important;
border-bottom-left-radius: 4px;
}
/* Dark theme adjustments */
[data-theme="dark"] .bot-message {
background: #374151;
color: #f9fafb;
.bot-message,
.bot-message p,
.bot-message span,
.bot-message li,
.bot-message a,
.bot-message strong,
.bot-message em,
.bot-message h1,
.bot-message h2,
.bot-message h3,
.bot-message h4 {
color: #ffffff !important;
}
/* Light background themes need dark bot message text */
[data-theme="light"] .bot-message,
[data-theme="light"] .bot-message p,
[data-theme="light"] .bot-message span,
[data-theme="light"] .bot-message li,
[data-theme="light"] .bot-message a,
[data-theme="light"] .bot-message strong,
[data-theme="light"] .bot-message em,
[data-theme="light"] .bot-message h1,
[data-theme="light"] .bot-message h2,
[data-theme="light"] .bot-message h3,
[data-theme="light"] .bot-message h4,
[data-theme="polaroidmemories"] .bot-message,
[data-theme="polaroidmemories"] .bot-message p,
[data-theme="polaroidmemories"] .bot-message span,
[data-theme="polaroidmemories"] .bot-message li,
[data-theme="polaroidmemories"] .bot-message a,
[data-theme="polaroidmemories"] .bot-message h1,
[data-theme="polaroidmemories"] .bot-message h2,
[data-theme="seasidepostcard"] .bot-message,
[data-theme="seasidepostcard"] .bot-message p,
[data-theme="seasidepostcard"] .bot-message span,
[data-theme="seasidepostcard"] .bot-message li,
[data-theme="seasidepostcard"] .bot-message a,
[data-theme="seasidepostcard"] .bot-message h1,
[data-theme="seasidepostcard"] .bot-message h2,
[data-theme="saturdaycartoons"] .bot-message,
[data-theme="saturdaycartoons"] .bot-message p,
[data-theme="saturdaycartoons"] .bot-message span,
[data-theme="saturdaycartoons"] .bot-message li,
[data-theme="saturdaycartoons"] .bot-message a,
[data-theme="saturdaycartoons"] .bot-message h1,
[data-theme="saturdaycartoons"] .bot-message h2,
[data-theme="typewriter"] .bot-message,
[data-theme="typewriter"] .bot-message p,
[data-theme="typewriter"] .bot-message span,
[data-theme="typewriter"] .bot-message li,
[data-theme="typewriter"] .bot-message a,
[data-theme="typewriter"] .bot-message h1,
[data-theme="typewriter"] .bot-message h2,
[data-theme="3dbevel"] .bot-message,
[data-theme="3dbevel"] .bot-message p,
[data-theme="3dbevel"] .bot-message span,
[data-theme="3dbevel"] .bot-message li,
[data-theme="3dbevel"] .bot-message a,
[data-theme="3dbevel"] .bot-message h1,
[data-theme="3dbevel"] .bot-message h2 {
color: #000000 !important;
}
/* Markdown content in bot messages */
@ -123,7 +253,7 @@
}
.bot-message code {
background: rgba(0, 0, 0, 0.1);
background: rgba(255, 255, 255, 0.1);
padding: 2px 6px;
border-radius: 4px;
font-family: "Monaco", "Menlo", monospace;
@ -131,7 +261,7 @@
}
.bot-message pre {
background: rgba(0, 0, 0, 0.1);
background: rgba(0, 0, 0, 0.2);
padding: 12px;
border-radius: 8px;
overflow-x: auto;
@ -146,8 +276,8 @@
/* Footer */
footer {
padding: 16px 0;
border-top: 1px solid var(--border-color, #e5e7eb);
background: var(--primary-bg, #ffffff);
border-top: 1px solid var(--border, var(--border-color, #2a2a2a));
background: transparent;
}
/* Suggestions */
@ -176,20 +306,16 @@ footer {
}
/* Input Container */
.input-container {
.input-container,
form.input-container {
display: flex;
gap: 12px;
align-items: center;
padding: 8px 12px;
background: var(--surface, var(--secondary-bg, #f9fafb));
border: 1px solid var(--border, var(--border-color, #e5e7eb));
background: var(--surface, var(--card, #1a1a24));
border: 1px solid var(--border, var(--border-color, #2a2a2a));
border-radius: 28px;
}
[data-theme="dark"] .input-container,
[data-theme="sentient"] .input-container {
background: var(--surface, #1a1a24);
border-color: var(--border, #2a2a3a);
margin: 0;
}
.input-container:focus-within {
@ -203,23 +329,18 @@ footer {
border-radius: 20px;
border: none;
background: transparent;
color: var(--text-primary, #1f2937);
color: #ffffff;
font-size: 15px;
outline: none;
transition: all 0.2s;
}
[data-theme="dark"] #messageInput,
[data-theme="sentient"] #messageInput {
color: var(--text-primary, #ffffff);
}
#messageInput:focus {
outline: none;
}
#messageInput::placeholder {
color: var(--text-muted, #9ca3af);
color: #888888;
}
#voiceBtn,

View file

@ -1,12 +1,53 @@
<link rel="stylesheet" href="chat/chat.css" />
<script>
// WebSocket URL - use relative path to go through botui proxy
const WS_BASE_URL =
window.location.protocol === "https:" ? "wss://" : "ws://";
const WS_URL = `${WS_BASE_URL}${window.location.host}`;
// Message Type Constants
const MessageType = {
<div class="chat-layout" id="chat-app">
<main id="messages"></main>
<footer>
<div class="suggestions-container" id="suggestions"></div>
<form class="input-container" id="chatForm">
<input
name="content"
id="messageInput"
type="text"
placeholder="Message..."
autofocus
/>
<button type="button" id="voiceBtn" title="Voice">🎤</button>
<button type="submit" id="sendBtn" title="Send"></button>
</form>
</footer>
<button class="scroll-to-bottom" id="scrollToBottom"></button>
</div>
<script>
(function () {
"use strict";
// ==========================================================================
// NOTIFICATION HELPER - Uses GBAlerts bell system
// ==========================================================================
function notify(message, type) {
type = type || "info";
if (window.GBAlerts) {
if (type === "success") {
window.GBAlerts.info("Chat", message);
} else if (type === "error") {
window.GBAlerts.warning("Chat", message);
} else {
window.GBAlerts.info("Chat", message);
}
}
}
// ==========================================================================
// CONFIGURATION
// ==========================================================================
var WS_BASE_URL =
window.location.protocol === "https:" ? "wss://" : "ws://";
var WS_URL = WS_BASE_URL + window.location.host;
var MessageType = {
EXTERNAL: 0,
USER: 1,
BOT_RESPONSE: 2,
@ -15,76 +56,79 @@
CONTEXT_CHANGE: 5,
};
// State
let ws = null,
currentSessionId = null,
currentUserId = null,
currentBotId = "default";
let isStreaming = false,
streamingMessageId = null,
// ==========================================================================
// STATE
// ==========================================================================
var ws = null;
var currentSessionId = null;
var currentUserId = null;
var currentBotId = "default";
var isStreaming = false;
var streamingMessageId = null;
var currentStreamingContent = "";
var reconnectAttempts = 0;
var maxReconnectAttempts = 5;
// ==========================================================================
// MESSAGE FUNCTIONS
// ==========================================================================
function escapeHtml(text) {
var div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
}
function addMessage(sender, content, msgId) {
var messages = document.getElementById("messages");
if (!messages) return;
var div = document.createElement("div");
div.className = "message " + sender;
if (msgId) div.id = msgId;
if (sender === "user") {
div.innerHTML =
'<div class="message-content user-message">' +
escapeHtml(content) +
"</div>";
} else {
var parsed =
typeof marked !== "undefined" && marked.parse
? marked.parse(content)
: escapeHtml(content);
div.innerHTML =
'<div class="message-content bot-message">' +
parsed +
"</div>";
}
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}
function updateStreaming(content) {
var el = document.getElementById(streamingMessageId);
if (el) {
var parsed =
typeof marked !== "undefined" && marked.parse
? marked.parse(content)
: escapeHtml(content);
el.querySelector(".message-content").innerHTML = parsed;
}
}
function finalizeStreaming() {
var el = document.getElementById(streamingMessageId);
if (el) {
var parsed =
typeof marked !== "undefined" && marked.parse
? marked.parse(currentStreamingContent)
: escapeHtml(currentStreamingContent);
el.querySelector(".message-content").innerHTML = parsed;
el.removeAttribute("id");
}
streamingMessageId = null;
currentStreamingContent = "";
let reconnectAttempts = 0;
const maxReconnectAttempts = 5;
// Initialize auth and WebSocket
async function initChat() {
try {
updateConnectionStatus("connecting");
const botName = "default";
// Use the botui proxy for auth (handles SSL cert issues)
const response = await fetch(
`/api/auth?bot_name=${encodeURIComponent(botName)}`,
);
const auth = await response.json();
currentUserId = auth.user_id;
currentSessionId = auth.session_id;
currentBotId = auth.bot_id || "default";
console.log("Auth:", {
currentUserId,
currentSessionId,
currentBotId,
});
connectWebSocket();
} catch (e) {
console.error("Auth failed:", e);
updateConnectionStatus("disconnected");
setTimeout(initChat, 3000);
}
}
function connectWebSocket() {
if (ws) ws.close();
// Use the botui proxy for WebSocket (handles SSL cert issues)
const url = `${WS_URL}/ws?session_id=${currentSessionId}&user_id=${currentUserId}`;
ws = new WebSocket(url);
ws.onopen = () => {
console.log("WebSocket connected");
updateConnectionStatus("connected");
reconnectAttempts = 0;
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === "connected") return;
if (data.message_type === MessageType.BOT_RESPONSE) {
processMessage(data);
}
} catch (e) {
console.error("WS message error:", e);
}
};
ws.onclose = () => {
updateConnectionStatus("disconnected");
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
setTimeout(connectWebSocket, 1000 * reconnectAttempts);
}
};
ws.onerror = (e) => console.error("WebSocket error:", e);
}
function processMessage(data) {
@ -100,7 +144,11 @@
isStreaming = true;
streamingMessageId = "streaming-" + Date.now();
currentStreamingContent = data.content || "";
addMessage("bot", currentStreamingContent, streamingMessageId);
addMessage(
"bot",
currentStreamingContent,
streamingMessageId,
);
} else {
currentStreamingContent += data.content || "";
updateStreaming(currentStreamingContent);
@ -108,47 +156,28 @@
}
}
function addMessage(sender, content, msgId = null) {
const messages = document.getElementById("messages");
const div = document.createElement("div");
div.className = `message ${sender}`;
if (msgId) div.id = msgId;
if (sender === "user") {
div.innerHTML = `<div class="message-content user-message">${escapeHtml(content)}</div>`;
} else {
div.innerHTML = `<div class="message-content bot-message">${marked.parse(content)}</div>`;
}
messages.appendChild(div);
messages.scrollTop = messages.scrollHeight;
}
function updateStreaming(content) {
const el = document.getElementById(streamingMessageId);
if (el)
el.querySelector(".message-content").innerHTML =
marked.parse(content);
}
function finalizeStreaming() {
const el = document.getElementById(streamingMessageId);
if (el) {
el.querySelector(".message-content").innerHTML = marked.parse(
currentStreamingContent,
);
el.removeAttribute("id");
}
streamingMessageId = null;
currentStreamingContent = "";
}
// ==========================================================================
// SEND MESSAGE - GLOBAL FUNCTION
// ==========================================================================
function sendMessage() {
const input = document.getElementById("messageInput");
const content = input.value.trim();
if (!content || !ws || ws.readyState !== WebSocket.OPEN) return;
var input = document.getElementById("messageInput");
if (!input) {
console.error("Chat input not found");
return;
}
var content = input.value.trim();
if (!content) {
return;
}
// Always show user message locally
addMessage("user", content);
input.value = "";
input.focus();
// Try to send via WebSocket if connected
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(
JSON.stringify({
bot_id: currentBotId,
@ -160,78 +189,124 @@
timestamp: new Date().toISOString(),
}),
);
input.value = "";
input.focus();
} else {
notify("Not connected to server. Message not sent.", "warning");
}
}
function updateConnectionStatus(status) {
const el = document.getElementById("connectionStatus");
if (el) el.className = `connection-status ${status}`;
// Make sendMessage globally available immediately
window.sendMessage = sendMessage;
// ==========================================================================
// WEBSOCKET CONNECTION
// ==========================================================================
function connectWebSocket() {
if (ws) {
ws.close();
}
function escapeHtml(text) {
const div = document.createElement("div");
div.textContent = text;
return div.innerHTML;
var url =
WS_URL +
"/ws?session_id=" +
currentSessionId +
"&user_id=" +
currentUserId;
ws = new WebSocket(url);
ws.onopen = function () {
console.log("WebSocket connected");
reconnectAttempts = 0;
};
ws.onmessage = function (event) {
try {
var data = JSON.parse(event.data);
if (data.type === "connected") return;
if (data.message_type === MessageType.BOT_RESPONSE) {
processMessage(data);
}
} catch (e) {
console.error("WS message error:", e);
}
};
ws.onclose = function () {
notify("Disconnected from chat server", "error");
if (reconnectAttempts < maxReconnectAttempts) {
reconnectAttempts++;
setTimeout(connectWebSocket, 1000 * reconnectAttempts);
}
};
ws.onerror = function (e) {
console.error("WebSocket error:", e);
};
}
// Initialize chat - runs immediately when script is executed
// (works both on full page load and HTMX partial load)
function setupChat() {
const input = document.getElementById("messageInput");
const sendBtn = document.getElementById("sendBtn");
// ==========================================================================
// INITIALIZATION
// ==========================================================================
function initChat() {
var botName = "default";
fetch("/api/auth?bot_name=" + encodeURIComponent(botName))
.then(function (response) {
return response.json();
})
.then(function (auth) {
currentUserId = auth.user_id;
currentSessionId = auth.session_id;
currentBotId = auth.bot_id || "default";
console.log("Auth:", {
currentUserId: currentUserId,
currentSessionId: currentSessionId,
currentBotId: currentBotId,
});
connectWebSocket();
})
.catch(function (e) {
console.error("Auth failed:", e);
notify("Failed to connect to chat server", "error");
setTimeout(initChat, 3000);
});
}
function setupEventHandlers() {
var form = document.getElementById("chatForm");
var input = document.getElementById("messageInput");
var sendBtn = document.getElementById("sendBtn");
if (form) {
form.onsubmit = function (e) {
e.preventDefault();
sendMessage();
return false;
};
}
if (sendBtn) sendBtn.onclick = sendMessage;
if (input) {
input.addEventListener("keypress", (e) => {
if (e.key === "Enter") sendMessage();
});
input.onkeydown = function (e) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
sendMessage();
}
};
}
if (sendBtn) {
sendBtn.onclick = function (e) {
e.preventDefault();
sendMessage();
};
}
}
// Run setup immediately
setupEventHandlers();
initChat();
}
// Initialize after a micro-delay to ensure DOM is ready
// This works for both full page loads and HTMX partial loads
setTimeout(() => {
if (
document.getElementById("messageInput") &&
!window.chatInitialized
) {
window.chatInitialized = true;
setupChat();
}
}, 0);
// Fallback for full page load
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
if (!window.chatInitialized) {
window.chatInitialized = true;
setupChat();
}
});
}
console.log(
"Chat module initialized, sendMessage is:",
typeof window.sendMessage,
);
})();
</script>
<div class="chat-layout" id="chat-app">
<div id="connectionStatus" class="connection-status disconnected"></div>
<main id="messages"></main>
<footer>
<div class="suggestions-container" id="suggestions"></div>
<div class="input-container">
<input
name="content"
id="messageInput"
type="text"
placeholder="Message..."
autofocus
/>
<button type="button" id="voiceBtn" title="Voice">🎤</button>
<button type="button" id="sendBtn" title="Send"></button>
</div>
</footer>
<button class="scroll-to-bottom" id="scrollToBottom"></button>
</div>

View file

@ -126,7 +126,7 @@
/* Header */
--header-bg: hsla(var(--background) / 0.8);
--header-border: hsla(var(--border) / 0.8);
--header-height: 64px;
--header-height: 40px;
/* Input Fields */
--input-bg: hsl(var(--input));
@ -296,11 +296,11 @@ body {
/* LAYOUT STRUCTURE */
/* ============================================ */
#main-content {
height: calc(100vh - 64px);
height: calc(100vh - var(--header-height));
width: 100%;
overflow: hidden;
position: relative;
margin-top: 64px;
margin-top: var(--header-height);
}
.section {
@ -323,14 +323,14 @@ body {
left: 0;
right: 0;
height: var(--header-height);
background: var(--header-bg);
background: var(--bg, var(--surface, #0a0a0a));
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border-bottom: 1px solid var(--header-border);
border-bottom: 1px solid var(--border, var(--border-color, #2a2a2a));
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--space-lg);
padding: 0 var(--space-md);
z-index: var(--z-sticky);
box-shadow: var(--shadow-sm);
transition: all var(--transition-smooth);
@ -370,29 +370,31 @@ body {
.app-tab {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
gap: 4px;
padding: 6px 10px;
border-radius: var(--radius-md);
color: var(--text-secondary);
color: var(--text-secondary, #888888);
text-decoration: none;
font-size: 13px;
font-size: 12px;
font-weight: 500;
transition: all var(--transition-fast);
border: 1px solid transparent;
}
.app-tab:hover {
background: var(--bg-hover);
color: var(--text-primary);
background: var(--surface-hover, var(--bg-hover, rgba(255, 255, 255, 0.1)));
color: var(--text, var(--text-primary, #ffffff));
}
.app-tab.active {
background: var(--accent-light);
color: var(--accent-color);
border-color: var(--accent-color);
background: var(--primary-light, rgba(212, 245, 5, 0.15));
color: var(--primary, var(--accent-color, #d4f505));
border-color: var(--primary, var(--accent-color, #d4f505));
}
.app-tab svg {
width: 14px;
height: 14px;
opacity: 0.7;
}
@ -406,10 +408,11 @@ body {
.header-search {
display: flex;
align-items: center;
gap: var(--space-sm);
padding: 8px 14px;
background: var(--glass-bg);
border: 1px solid var(--border-color);
gap: var(--space-xs);
padding: 4px 12px;
height: 28px;
background: var(--surface, var(--glass-bg, rgba(255, 255, 255, 0.05)));
border: 1px solid var(--border, var(--border-color, #2a2a2a));
border-radius: var(--radius-full);
width: 100%;
transition: all var(--transition-fast);
@ -430,7 +433,7 @@ body {
border: none;
background: transparent;
color: var(--text-primary);
font-size: 14px;
font-size: 12px;
outline: none;
}
@ -439,11 +442,11 @@ body {
}
.search-shortcut {
padding: 2px 6px;
padding: 2px 5px;
background: var(--bg-hover);
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 11px;
border-radius: 3px;
font-size: 10px;
color: var(--text-secondary);
font-family: inherit;
}
@ -475,13 +478,13 @@ body {
.logo-wrapper {
display: flex;
align-items: center;
gap: var(--space-sm);
gap: var(--space-xs);
cursor: pointer;
padding: var(--space-sm);
border-radius: var(--radius-md);
padding: 4px 8px;
border-radius: var(--radius-sm);
transition: all var(--transition-fast);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
background: transparent;
border: none;
}
.logo-wrapper:hover {
@ -491,11 +494,10 @@ body {
}
.logo-icon {
width: 36px;
height: 36px;
background: url("https://pragmatismo.com.br/icons/general-bots.svg")
center/contain no-repeat;
border-radius: var(--radius-sm);
width: 32px;
height: 20px;
color: var(--text, var(--text-primary, #ffffff));
flex-shrink: 0;
}
.logo-text {
@ -509,16 +511,16 @@ body {
/* ICON BUTTONS (Apps, Theme, User) */
/* ============================================ */
.icon-button {
width: 40px;
height: 40px;
width: 28px;
height: 28px;
border-radius: var(--radius-full);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
border: 1px solid var(--border-color);
background: var(--glass-bg);
color: var(--text-primary);
border: 1px solid var(--border, var(--border-color, #2a2a2a));
background: var(--surface, var(--glass-bg, rgba(255, 255, 255, 0.05)));
color: var(--text, var(--text-primary, #ffffff));
transition: all var(--transition-fast);
backdrop-filter: blur(10px);
}
@ -534,9 +536,15 @@ body {
transform: translateY(0);
}
.icon-button.active {
background: var(--accent-dim, rgba(212, 245, 5, 0.15));
border-color: var(--accent, var(--accent-color, #d4f505));
color: var(--accent, var(--accent-color, #d4f505));
}
.icon-button svg {
width: 20px;
height: 20px;
width: 16px;
height: 16px;
}
/* ============================================ */
@ -579,12 +587,12 @@ body {
.apps-dropdown {
position: absolute;
top: calc(100% + 8px);
right: 60px;
right: 0;
width: 280px;
background: var(--glass-bg);
background: var(--surface, var(--bg, #0a0a0a));
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid var(--glass-border);
border: 1px solid var(--border, var(--border-color, #2a2a2a));
border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl);
padding: var(--space-md);
@ -625,21 +633,21 @@ body {
padding: var(--space-md);
border-radius: var(--radius-lg);
text-decoration: none;
color: var(--text-primary);
color: var(--text, var(--text-primary, #ffffff));
transition: all var(--transition-fast);
cursor: pointer;
border: 1px solid transparent;
}
.app-item:hover {
background: var(--bg-hover);
border-color: var(--border-color);
background: var(--surface-hover, var(--bg-hover, rgba(255, 255, 255, 0.1)));
border-color: var(--border, var(--border-color, #2a2a2a));
transform: translateY(-2px);
}
.app-item.active {
background: var(--accent-light);
border-color: var(--accent-color);
background: var(--primary-light, rgba(212, 245, 5, 0.15));
border-color: var(--primary, var(--accent-color, #d4f505));
}
.app-icon {
@ -650,15 +658,53 @@ body {
.app-item span {
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
color: var(--text, var(--text-primary, #ffffff));
}
/* ============================================ */
/* SETTINGS LINK */
/* ============================================ */
.settings-link {
display: flex;
align-items: center;
gap: 8px;
margin-top: var(--space-md);
padding: var(--space-sm) var(--space-md);
border-top: 1px solid var(--border, var(--border-color, #2a2a2a));
color: var(--text, var(--text-primary, #ffffff));
text-decoration: none;
font-size: 14px;
font-weight: 500;
border-radius: var(--radius-md);
transition: background var(--transition-fast);
}
.settings-link:hover {
background: var(--surface-hover, var(--bg-hover, rgba(255, 255, 255, 0.1)));
}
.settings-link svg {
opacity: 0.7;
}
.settings-link:hover svg {
opacity: 1;
}
/* ============================================ */
/* SETTINGS PANEL THEME FIX */
/* ============================================ */
.settings-panel {
background: var(--surface, var(--bg, #0a0a0a)) !important;
border-color: var(--border, var(--border-color, #2a2a2a)) !important;
}
/* ============================================ */
/* USER AVATAR */
/* ============================================ */
.user-avatar {
width: 40px;
height: 40px;
width: 28px;
height: 28px;
border-radius: var(--radius-full);
background: var(--accent-gradient);
display: flex;
@ -666,7 +712,7 @@ body {
justify-content: center;
color: white;
font-weight: 700;
font-size: 16px;
font-size: 12px;
cursor: pointer;
transition: all var(--transition-fast);
box-shadow: var(--shadow-sm);
@ -1032,16 +1078,107 @@ body {
border-bottom: 1px solid var(--border, #2a2a2a);
cursor: pointer;
transition: background 0.2s;
position: relative;
}
.notification-item:hover {
background: var(--surface-hover, #1e1e1e);
}
.notification-item:last-child {
border-bottom: none;
}
.notification-item.unread {
background: rgba(197, 248, 42, 0.05);
}
.notification-icon {
font-size: 20px;
flex-shrink: 0;
width: 24px;
text-align: center;
}
.notification-content {
flex: 1;
min-width: 0;
}
.notification-title {
font-size: 13px;
font-weight: 600;
color: var(--text, #fff);
margin-bottom: 2px;
}
.notification-message {
font-size: 12px;
color: var(--text-secondary, #888);
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.notification-time {
font-size: 11px;
color: var(--text-muted, #666);
}
.notification-action {
background: var(--primary, #c5f82a);
color: #000;
border: none;
padding: 6px 12px;
border-radius: 6px;
font-size: 11px;
font-weight: 600;
cursor: pointer;
flex-shrink: 0;
transition: all 0.2s;
}
.notification-action:hover {
background: var(--primary-hover, #b5e520);
}
.notification-dismiss {
background: transparent;
border: none;
color: var(--text-muted, #666);
font-size: 16px;
cursor: pointer;
padding: 0;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: color 0.2s;
}
.notification-dismiss:hover {
color: var(--text, #fff);
}
.notification-item.notification-success {
border-left: 3px solid var(--success, #22c55e);
}
.notification-item.notification-error {
border-left: 3px solid var(--error, #ef4444);
}
.notification-item.notification-warning {
border-left: 3px solid var(--warning, #f59e0b);
}
.notification-item.notification-info {
border-left: 3px solid var(--primary, #c5f82a);
}
.notification-item.success .notification-item-icon {
color: var(--success, #22c55e);
}

View file

@ -1093,6 +1093,33 @@ body {
margin-bottom: 12px;
}
.settings-link {
display: flex;
align-items: center;
gap: 8px;
margin-top: 16px;
padding: 12px;
border-top: 1px solid var(--border, #2a2a2a);
color: var(--text-primary, #ffffff);
text-decoration: none;
font-size: 14px;
font-weight: 500;
border-radius: 8px;
transition: background 0.2s ease;
}
.settings-link:hover {
background: var(--surface-hover, rgba(255, 255, 255, 0.05));
}
.settings-link svg {
opacity: 0.7;
}
.settings-link:hover svg {
opacity: 1;
}
/* ============================================ */
/* SETTINGS SHORTCUTS */
/* ============================================ */

View file

@ -154,6 +154,12 @@
border-color: var(--border);
}
[data-theme="sentient"] .icon-button.active {
color: var(--accent);
background: var(--accent-dim);
border-color: var(--accent);
}
/* Notification Badge */
[data-theme="sentient"] .notification-badge {
background: var(--accent);
@ -756,22 +762,48 @@
}
[data-theme="sentient"] .app-item {
color: var(--text-secondary);
color: var(--text);
padding: 12px;
border-radius: 8px;
transition: all 0.2s ease;
}
[data-theme="sentient"] .app-item span {
color: var(--text);
}
[data-theme="sentient"] .app-item .app-icon {
color: var(--text);
}
[data-theme="sentient"] .app-item .app-icon svg {
stroke: var(--text);
}
[data-theme="sentient"] .app-item:hover {
background: var(--surface-hover);
color: var(--text);
}
[data-theme="sentient"] .app-item:hover span,
[data-theme="sentient"] .app-item:hover .app-icon {
color: var(--text);
}
[data-theme="sentient"] .app-item.active {
background: var(--accent-light);
color: var(--accent);
}
[data-theme="sentient"] .app-item.active span,
[data-theme="sentient"] .app-item.active .app-icon {
color: var(--accent);
}
[data-theme="sentient"] .app-item.active .app-icon svg {
stroke: var(--accent);
}
/* ============================================ */
/* FORMS & INPUTS */
/* ============================================ */

View file

@ -171,26 +171,7 @@
</button>
</div>
</div>
<div class="drive-toolbar-center">
<div class="search-box">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.3-4.3"></path>
</svg>
<input
type="text"
id="search-input"
placeholder="Search in Drive"
/>
</div>
</div>
<div class="drive-toolbar-center"></div>
<div class="drive-toolbar-right">
<button
class="btn-icon view-toggle"

File diff suppressed because it is too large Load diff

View file

@ -85,11 +85,9 @@ document.addEventListener("keydown", (e) => {
document.body.addEventListener("htmx:afterSwap", (e) => {
if (e.detail.target.id === "main-content") {
const hash = window.location.hash || "#chat";
document.querySelectorAll(".app-item").forEach((item) => {
item.classList.toggle("active", item.getAttribute("href") === hash);
});
// Close settings panel on navigation
if (settingsPanel) settingsPanel.classList.remove("show");
// Note: Active class handling is done in htmx-app.js updateActiveNav()
}
});

View file

@ -214,22 +214,112 @@
updateActiveNav(path);
});
// Also listen for htmx:afterSwap to catch all navigation
document.addEventListener("htmx:afterSwap", (event) => {
setTimeout(() => {
const path = window.location.hash || window.location.pathname;
updateActiveNav(path);
}, 10);
});
// Handle hash change
window.addEventListener("hashchange", (event) => {
updateActiveNav(window.location.hash);
});
// Handle browser back/forward
window.addEventListener("popstate", (event) => {
updateActiveNav(window.location.pathname);
updateActiveNav(window.location.hash || window.location.pathname);
});
// Handle direct clicks on app tabs and app items
document.addEventListener("click", (event) => {
const appTab = event.target.closest(".app-tab");
const appItem = event.target.closest(".app-item");
if (appTab || appItem) {
const element = appTab || appItem;
const href = element.getAttribute("href");
if (href) {
// Immediately update active state on click
updateActiveNav(href);
}
}
});
}
// Update active navigation item
// Get current section from URL
function getCurrentSection() {
const hash = window.location.hash;
if (hash && hash.length > 1) {
// Handle both #section and /#section formats
return hash.replace(/^#\/?/, "").split("/")[0].split("?")[0];
}
return "chat";
}
// Update active navigation item and page title
function updateActiveNav(path) {
document.querySelectorAll(".nav-item, .app-item").forEach((item) => {
const href = item.getAttribute("href");
if (href === path || (path === "/" && href === "/chat")) {
item.classList.add("active");
} else {
// Extract section name from path
// Handles: "/#chat", "#chat", "/chat", "chat", "/#paper", "#paper"
let section;
if (path && path.length > 0) {
// Remove leading /, #, or /# combinations
section = path
.replace(/^[/#]+/, "")
.split("/")[0]
.split("?")[0];
}
// Fallback to current URL hash if section is empty
if (!section) {
section = getCurrentSection();
}
// First, remove ALL active classes from all tabs, items, and apps button
document.querySelectorAll(".app-tab.active").forEach((item) => {
item.classList.remove("active");
});
document.querySelectorAll(".app-item.active").forEach((item) => {
item.classList.remove("active");
});
// Remove active from apps button
const appsButton = document.getElementById("appsButton");
if (appsButton) {
appsButton.classList.remove("active");
}
// Check if section exists in the main header tabs
let foundInHeaderTabs = false;
document.querySelectorAll(".app-tab").forEach((item) => {
const dataSection = item.getAttribute("data-section");
const href = item.getAttribute("href");
const itemSection = dataSection || (href ? href.replace(/^#/, "") : "");
if (itemSection === section) {
item.classList.add("active");
foundInHeaderTabs = true;
}
});
// Update app items in launcher dropdown (always mark the current section)
document.querySelectorAll(".app-item").forEach((item) => {
const href = item.getAttribute("href");
const dataSection = item.getAttribute("data-section");
const itemSection = dataSection || (href ? href.replace(/^#/, "") : "");
if (itemSection === section) {
item.classList.add("active");
}
});
// If section is NOT in header tabs, select the apps button instead
if (!foundInHeaderTabs && appsButton) {
appsButton.classList.add("active");
}
// Update page title
const sectionName = section.charAt(0).toUpperCase() + section.slice(1);
document.title = sectionName + " - General Bots";
}
// Initialize keyboard shortcuts
@ -314,8 +404,8 @@
// Initialize theme
initTheme();
// Set initial active nav
updateActiveNav(window.location.pathname);
// Set initial active nav based on hash or default to chat
updateActiveNav(window.location.hash || "#chat");
console.log("HTMX application initialized");
}

View file

@ -12,7 +12,7 @@
<!-- Folder List -->
<div class="folders-section">
<div class="nav-item active" data-folder="inbox" hx-get="/ui/email/list?folder=inbox" hx-target="#mail-list" hx-swap="innerHTML">
<div class="nav-item active" data-folder="inbox" hx-get="/api/ui/email/list?folder=inbox" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12"/>
<path d="M5.45 5.11L2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>
@ -20,20 +20,20 @@
<span>Inbox</span>
<span class="folder-badge unread" id="inbox-count">0</span>
</div>
<div class="nav-item" data-folder="starred" hx-get="/ui/email/list?folder=starred" hx-target="#mail-list" hx-swap="innerHTML">
<div class="nav-item" data-folder="starred" hx-get="/api/ui/email/list?folder=starred" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
</svg>
<span>Starred</span>
</div>
<div class="nav-item" data-folder="sent" hx-get="/ui/email/list?folder=sent" hx-target="#mail-list" hx-swap="innerHTML">
<div class="nav-item" data-folder="sent" hx-get="/api/ui/email/list?folder=sent" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="22" y1="2" x2="11" y2="13"/>
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
</svg>
<span>Sent</span>
</div>
<div class="nav-item" data-folder="scheduled" hx-get="/ui/email/list?folder=scheduled" hx-target="#mail-list" hx-swap="innerHTML">
<div class="nav-item" data-folder="scheduled" hx-get="/api/ui/email/list?folder=scheduled" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"/>
<polyline points="12 6 12 12 16 14"/>
@ -41,7 +41,7 @@
<span>Scheduled</span>
<span class="folder-badge" id="scheduled-count">0</span>
</div>
<div class="nav-item" data-folder="drafts" hx-get="/ui/email/list?folder=drafts" hx-target="#mail-list" hx-swap="innerHTML">
<div class="nav-item" data-folder="drafts" hx-get="/api/ui/email/list?folder=drafts" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
@ -58,7 +58,7 @@
</svg>
<span>Tracking</span>
</div>
<div class="nav-item" data-folder="spam" hx-get="/ui/email/list?folder=spam" hx-target="#mail-list" hx-swap="innerHTML">
<div class="nav-item" data-folder="spam" hx-get="/api/ui/email/list?folder=spam" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
<line x1="12" y1="9" x2="12" y2="13"/>
@ -66,7 +66,7 @@
</svg>
<span>Spam</span>
</div>
<div class="nav-item" data-folder="trash" hx-get="/ui/email/list?folder=trash" hx-target="#mail-list" hx-swap="innerHTML">
<div class="nav-item" data-folder="trash" hx-get="/api/ui/email/list?folder=trash" hx-target="#mail-list" hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
@ -87,7 +87,7 @@
</button>
</div>
<div id="accounts-list"
hx-get="/ui/email/accounts"
hx-get="/api/ui/email/accounts"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Accounts loaded here -->
@ -105,7 +105,7 @@
</svg>
</button>
</div>
<div id="labels-list" hx-get="/ui/email/labels" hx-trigger="load" hx-swap="innerHTML">
<div id="labels-list" hx-get="/api/ui/email/labels" hx-trigger="load" hx-swap="innerHTML">
<div class="label-item" style="--label-color: #ef4444;">
<span class="label-dot"></span>
<span>Important</span>
@ -169,7 +169,7 @@
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<input type="text" placeholder="Search emails..." id="email-search"
hx-get="/ui/email/search" hx-trigger="keyup changed delay:300ms"
hx-get="/api/ui/email/search" hx-trigger="keyup changed delay:300ms"
hx-target="#mail-list" hx-include="this" name="q"/>
</div>
<button class="icon-btn" onclick="refreshMailList()" title="Refresh">
@ -212,7 +212,7 @@
</button>
</div>
<div id="mail-list" hx-get="/ui/email/list?folder=inbox" hx-trigger="load" hx-swap="innerHTML">
<div id="mail-list" hx-get="/api/ui/email/list?folder=inbox" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-state">
<div class="spinner"></div>
<p>Loading emails...</p>
@ -421,7 +421,7 @@
</svg>
</button>
</div>
<div class="templates-list" id="templates-list" hx-get="/ui/email/templates" hx-trigger="load" hx-swap="innerHTML">
<div class="templates-list" id="templates-list" hx-get="/api/ui/email/templates" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-state"><div class="spinner"></div></div>
</div>
<div class="modal-footer">
@ -443,7 +443,7 @@
</svg>
</button>
</div>
<div class="signatures-list" id="signatures-list" hx-get="/ui/email/signatures" hx-trigger="load" hx-swap="innerHTML">
<div class="signatures-list" id="signatures-list" hx-get="/api/ui/email/signatures" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-state"><div class="spinner"></div></div>
</div>
<div class="modal-footer">
@ -465,7 +465,7 @@
</svg>
</button>
</div>
<div class="rules-list" id="rules-list" hx-get="/ui/email/rules" hx-trigger="load" hx-swap="innerHTML">
<div class="rules-list" id="rules-list" hx-get="/api/ui/email/rules" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-state"><div class="spinner"></div></div>
</div>
<div class="modal-footer">
@ -487,7 +487,7 @@
</svg>
</button>
</div>
<form id="autoresponder-form" hx-post="/ui/email/auto-responder" hx-swap="none">
<form id="autoresponder-form" hx-post="/api/ui/email/auto-responder" hx-swap="none">
<div class="form-group">
<label class="toggle-label">
<input type="checkbox" name="enabled" id="autoresponder-enabled"/>

View file

@ -12,7 +12,7 @@
Alert Configuration
</h2>
<span class="alert-summary"
hx-get="/api/monitoring/alerts/summary"
hx-get="/api/ui/monitoring/alerts/summary"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
<span class="summary-item critical">0 Critical</span>
@ -95,7 +95,7 @@
</div>
<div class="alerts-list" id="alerts-list"
hx-get="/api/monitoring/alerts/active"
hx-get="/api/ui/monitoring/alerts/active"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
<div class="alert-placeholder">
@ -137,7 +137,7 @@
</div>
<div class="rules-grid" id="rules-grid"
hx-get="/api/monitoring/alerts/rules"
hx-get="/api/ui/monitoring/alerts/rules"
hx-trigger="load"
hx-swap="innerHTML">
<div class="rule-card skeleton">
@ -178,7 +178,7 @@
</div>
<div class="history-list" id="history-list"
hx-get="/api/monitoring/alerts/history"
hx-get="/api/ui/monitoring/alerts/history"
hx-trigger="load"
hx-swap="innerHTML">
<div class="loading-state">
@ -210,7 +210,7 @@
</button>
</div>
<form id="create-alert-form"
hx-post="/api/monitoring/alerts/rules"
hx-post="/api/ui/monitoring/alerts/rules"
hx-target="#rules-grid"
hx-swap="innerHTML"
hx-on::after-request="closeCreateAlertModal()">

View file

@ -97,7 +97,7 @@ function acknowledgeAlert(alertId) {
function acknowledgeAllAlerts() {
if (confirm('Are you sure you want to acknowledge all active alerts?')) {
htmx.ajax('POST', '/api/monitoring/alerts/acknowledge-all', {
htmx.ajax('POST', '/api/ui/monitoring/alerts/acknowledge-all', {
swap: 'none'
}).then(() => {
htmx.trigger('#alerts-list', 'refresh');

View file

@ -3,7 +3,7 @@
<div class="health-container">
<!-- Health Overview -->
<div class="health-overview"
hx-get="/api/monitoring/health/overview"
hx-get="/api/ui/monitoring/health/overview"
hx-trigger="load, every 10s"
hx-swap="innerHTML">
<div class="health-status healthy">
@ -24,7 +24,7 @@
<!-- Uptime Stats -->
<div class="uptime-stats">
<div class="stat-card"
hx-get="/api/monitoring/health/uptime"
hx-get="/api/ui/monitoring/health/uptime"
hx-trigger="load, every 60s"
hx-swap="innerHTML">
<div class="stat-icon">
@ -39,7 +39,7 @@
</div>
</div>
<div class="stat-card"
hx-get="/api/monitoring/health/uptime-percent"
hx-get="/api/ui/monitoring/health/uptime-percent"
hx-trigger="load, every 60s"
hx-swap="innerHTML">
<div class="stat-icon success">
@ -53,7 +53,7 @@
</div>
</div>
<div class="stat-card"
hx-get="/api/monitoring/health/last-incident"
hx-get="/api/ui/monitoring/health/last-incident"
hx-trigger="load, every 60s"
hx-swap="innerHTML">
<div class="stat-icon warning">
@ -69,7 +69,7 @@
</div>
</div>
<div class="stat-card"
hx-get="/api/monitoring/health/response-time"
hx-get="/api/ui/monitoring/health/response-time"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
<div class="stat-icon info">
@ -113,7 +113,7 @@
</div>
<div class="health-checks-grid" id="health-checks"
hx-get="/api/monitoring/health/checks"
hx-get="/api/ui/monitoring/health/checks"
hx-trigger="load, every 10s"
hx-swap="innerHTML">
<div class="health-check-card">
@ -200,7 +200,7 @@
</div>
<div class="dependencies-list" id="dependencies"
hx-get="/api/monitoring/health/dependencies"
hx-get="/api/ui/monitoring/health/dependencies"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
<div class="dependency-row">
@ -277,7 +277,7 @@
</div>
<div class="uptime-chart" id="uptime-chart"
hx-get="/api/monitoring/health/uptime-history"
hx-get="/api/ui/monitoring/health/uptime-history"
hx-trigger="load"
hx-swap="innerHTML">
<div class="uptime-bars">
@ -319,7 +319,7 @@
</div>
<div class="incidents-list" id="incidents"
hx-get="/api/monitoring/health/incidents"
hx-get="/api/ui/monitoring/health/incidents"
hx-trigger="load"
hx-swap="innerHTML">
<div class="incident-placeholder">

View file

@ -246,11 +246,11 @@
<rect x="0" y="0" width="160" height="85" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="12" y="24" fill="#64748b" font-family="system-ui" font-size="10" font-weight="500">ACTIVE SESSIONS</text>
<text x="12" y="58" fill="#f8fafc" font-family="system-ui" font-size="32" font-weight="700"
hx-get="/api/monitoring/metric/sessions"
hx-get="/api/ui/monitoring/metric/sessions"
hx-trigger="load, every 5s"
hx-swap="innerHTML">--</text>
<text x="148" y="70" fill="#10b981" font-family="system-ui" font-size="11" text-anchor="end"
hx-get="/api/monitoring/trend/sessions"
hx-get="/api/ui/monitoring/trend/sessions"
hx-trigger="load, every 5s"
hx-swap="innerHTML">↑ 0%</text>
</g>
@ -260,11 +260,11 @@
<rect x="0" y="0" width="160" height="85" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="12" y="24" fill="#64748b" font-family="system-ui" font-size="10" font-weight="500">MESSAGES TODAY</text>
<text x="12" y="58" fill="#f8fafc" font-family="system-ui" font-size="32" font-weight="700"
hx-get="/api/monitoring/metric/messages"
hx-get="/api/ui/monitoring/metric/messages"
hx-trigger="load, every 10s"
hx-swap="innerHTML">--</text>
<text x="148" y="70" fill="#94a3b8" font-family="system-ui" font-size="11" text-anchor="end"
hx-get="/api/monitoring/rate/messages"
hx-get="/api/ui/monitoring/rate/messages"
hx-trigger="load, every 10s"
hx-swap="innerHTML">0/hr</text>
</g>
@ -274,7 +274,7 @@
<rect x="0" y="0" width="160" height="85" rx="10" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<text x="12" y="24" fill="#64748b" font-family="system-ui" font-size="10" font-weight="500">AVG RESPONSE</text>
<text x="12" y="58" fill="#f8fafc" font-family="system-ui" font-size="32" font-weight="700"
hx-get="/api/monitoring/metric/response_time"
hx-get="/api/ui/monitoring/metric/response_time"
hx-trigger="load, every 10s"
hx-swap="innerHTML">--</text>
<text x="148" y="70" fill="#94a3b8" font-family="system-ui" font-size="11" text-anchor="end">ms</text>
@ -283,7 +283,7 @@
<!-- ==================== RESOURCE BARS ==================== -->
<g transform="translate(320, 580)" class="resource-bars"
hx-get="/api/monitoring/resources/bars"
hx-get="/api/ui/monitoring/resources/bars"
hx-trigger="load, every 15s"
hx-swap="innerHTML">
<!-- CPU -->
@ -325,7 +325,7 @@
<rect x="0" y="0" width="520" height="40" rx="8" fill="#1e293b" stroke="#334155" stroke-width="1"/>
<circle cx="18" cy="20" r="5" fill="#10b981" class="pulse-dot"/>
<text x="34" y="25" fill="#94a3b8" font-family="system-ui" font-size="12"
hx-get="/api/monitoring/activity/latest"
hx-get="/api/ui/monitoring/activity/latest"
hx-trigger="load, every 5s"
hx-swap="innerHTML">System monitoring active...</text>
</g>
@ -335,7 +335,7 @@
<g transform="translate(600, 45)">
<text text-anchor="middle" fill="#f8fafc" font-family="system-ui" font-size="22" font-weight="600">Live System Monitor</text>
<text y="24" text-anchor="middle" fill="#64748b" font-family="system-ui" font-size="12"
hx-get="/api/monitoring/timestamp"
hx-get="/api/ui/monitoring/timestamp"
hx-trigger="load, every 5s"
hx-swap="innerHTML">Last updated: --</text>
</g>
@ -361,7 +361,7 @@
<!-- Hidden service status updater -->
<div id="service-status-container" style="display: none;"
hx-get="/api/monitoring/services"
hx-get="/api/ui/monitoring/services"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
</div>

View file

@ -2,10 +2,23 @@
<!-- Sidebar Navigation -->
<aside class="monitoring-sidebar">
<div class="monitoring-header">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" opacity="0.3"></circle>
<circle cx="12" cy="12" r="6.5" opacity="0.6"></circle>
<circle cx="12" cy="12" r="2" fill="currentColor" stroke="none"></circle>
<circle
cx="12"
cy="12"
r="2"
fill="currentColor"
stroke="none"
></circle>
<line x1="12" y1="2" x2="12" y2="5"></line>
<line x1="12" y1="19" x2="12" y2="22"></line>
<line x1="2" y1="12" x2="5" y2="12"></line>
@ -15,13 +28,23 @@
</div>
<nav class="monitoring-nav">
<a href="#dashboard" class="nav-item active"
hx-get="/api/monitoring/dashboard"
<a
href="#dashboard"
class="nav-item active"
hx-get="/api/ui/monitoring/dashboard"
hx-target="#monitoring-content"
hx-swap="innerHTML"
hx-push-url="false"
onclick="setActiveNav(this)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
onclick="setActiveNav(this)"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="3" y="3" width="7" height="9"></rect>
<rect x="14" y="3" width="7" height="5"></rect>
<rect x="14" y="12" width="7" height="9"></rect>
@ -29,27 +52,61 @@
</svg>
<span>Dashboard</span>
</a>
<a href="#services" class="nav-item"
hx-get="/api/monitoring/services"
<a
href="#services"
class="nav-item"
hx-get="/api/ui/monitoring/services"
hx-target="#monitoring-content"
hx-swap="innerHTML"
hx-push-url="false"
onclick="setActiveNav(this)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect>
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect>
onclick="setActiveNav(this)"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect
x="2"
y="2"
width="20"
height="8"
rx="2"
ry="2"
></rect>
<rect
x="2"
y="14"
width="20"
height="8"
rx="2"
ry="2"
></rect>
<line x1="6" y1="6" x2="6.01" y2="6"></line>
<line x1="6" y1="18" x2="6.01" y2="18"></line>
</svg>
<span>Services</span>
</a>
<a href="#resources" class="nav-item"
hx-get="/api/monitoring/resources"
<a
href="#resources"
class="nav-item"
hx-get="/api/ui/monitoring/resources"
hx-target="#monitoring-content"
hx-swap="innerHTML"
hx-push-url="false"
onclick="setActiveNav(this)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
onclick="setActiveNav(this)"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
<rect x="9" y="9" width="6" height="6"></rect>
<line x1="9" y1="1" x2="9" y2="4"></line>
@ -63,14 +120,26 @@
</svg>
<span>Resources</span>
</a>
<a href="#logs" class="nav-item"
hx-get="/api/monitoring/logs"
<a
href="#logs"
class="nav-item"
hx-get="/api/ui/monitoring/logs"
hx-target="#monitoring-content"
hx-swap="innerHTML"
hx-push-url="false"
onclick="setActiveNav(this)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
onclick="setActiveNav(this)"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"
></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
@ -78,55 +147,100 @@
</svg>
<span>Logs</span>
</a>
<a href="#metrics" class="nav-item"
hx-get="/api/monitoring/metrics"
<a
href="#metrics"
class="nav-item"
hx-get="/api/ui/monitoring/metrics"
hx-target="#monitoring-content"
hx-swap="innerHTML"
hx-push-url="false"
onclick="setActiveNav(this)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
onclick="setActiveNav(this)"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="18" y1="20" x2="18" y2="10"></line>
<line x1="12" y1="20" x2="12" y2="4"></line>
<line x1="6" y1="20" x2="6" y2="14"></line>
</svg>
<span>Metrics</span>
</a>
<a href="#alerts" class="nav-item"
hx-get="/api/monitoring/alerts"
<a
href="#alerts"
class="nav-item"
hx-get="/api/ui/monitoring/alerts"
hx-target="#monitoring-content"
hx-swap="innerHTML"
hx-push-url="false"
onclick="setActiveNav(this)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
onclick="setActiveNav(this)"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"
></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
</svg>
<span>Alerts</span>
<span class="alert-badge" id="alert-count"
hx-get="/api/monitoring/alerts/count"
<span
class="alert-badge"
id="alert-count"
hx-get="/api/ui/monitoring/alerts/count"
hx-trigger="load, every 30s"
hx-swap="innerHTML">0</span>
hx-swap="innerHTML"
>0</span
>
</a>
<a href="#health" class="nav-item"
hx-get="/api/monitoring/health"
<a
href="#health"
class="nav-item"
hx-get="/api/ui/monitoring/health"
hx-target="#monitoring-content"
hx-swap="innerHTML"
hx-push-url="false"
onclick="setActiveNav(this)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
onclick="setActiveNav(this)"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>
</svg>
<span>Health</span>
<span class="health-indicator"
hx-get="/api/monitoring/health/status"
<span
class="health-indicator"
hx-get="/api/ui/monitoring/health/status"
hx-trigger="load, every 10s"
hx-swap="outerHTML"></span>
hx-swap="outerHTML"
></span>
</a>
</nav>
<div class="monitoring-footer">
<a href="/suite" class="back-link">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
@ -134,10 +248,13 @@
</a>
<div class="system-status">
<span class="status-indicator running"></span>
<span class="status-text"
hx-get="/api/monitoring/system/status"
<span
class="status-text"
hx-get="/api/ui/monitoring/system/status"
hx-trigger="load, every 15s"
hx-swap="innerHTML">All Systems Operational</span>
hx-swap="innerHTML"
>All Systems Operational</span
>
</div>
</div>
</aside>
@ -147,14 +264,20 @@
<div class="monitoring-toolbar">
<div class="toolbar-left">
<h1 id="page-title">Dashboard</h1>
<span class="last-updated"
hx-get="/api/monitoring/timestamp"
<span
class="last-updated"
hx-get="/api/ui/monitoring/timestamp"
hx-trigger="load, every 5s"
hx-swap="innerHTML">--</span>
hx-swap="innerHTML"
>--</span
>
</div>
<div class="toolbar-right">
<div class="time-range-selector">
<select id="time-range" onchange="updateTimeRange(this.value)">
<select
id="time-range"
onchange="updateTimeRange(this.value)"
>
<option value="15m">Last 15 minutes</option>
<option value="1h" selected>Last 1 hour</option>
<option value="6h">Last 6 hours</option>
@ -163,28 +286,78 @@
<option value="30d">Last 30 days</option>
</select>
</div>
<button class="toolbar-btn" onclick="refreshMonitoring()" title="Refresh">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<button
class="toolbar-btn"
onclick="refreshMonitoring()"
title="Refresh"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="23 4 23 10 17 10"></polyline>
<polyline points="1 20 1 14 7 14"></polyline>
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path>
<path
d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"
></path>
</svg>
</button>
<button class="toolbar-btn" onclick="toggleAutoRefresh()" id="auto-refresh-btn" title="Auto Refresh">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<button
class="toolbar-btn"
onclick="toggleAutoRefresh()"
id="auto-refresh-btn"
title="Auto Refresh"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
</button>
<button class="toolbar-btn" onclick="exportData()" title="Export">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<button
class="toolbar-btn"
onclick="exportData()"
title="Export"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"
></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</button>
<a href="/metrics" target="_blank" class="toolbar-btn" title="Prometheus Metrics">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<a
href="/metrics"
target="_blank"
class="toolbar-btn"
title="Prometheus Metrics"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12" y2="12"></line>
<line x1="12" y1="16" x2="12.01" y2="16"></line>
@ -193,10 +366,13 @@
</div>
</div>
<div class="monitoring-content" id="monitoring-content"
hx-get="/api/monitoring/dashboard"
<div
class="monitoring-content"
id="monitoring-content"
hx-get="/api/ui/monitoring/dashboard"
hx-trigger="load"
hx-swap="innerHTML">
hx-swap="innerHTML"
>
<!-- Dashboard content loaded via HTMX -->
<div class="loading-state">
<div class="spinner"></div>
@ -208,22 +384,40 @@
<!-- Quick Stats Bar (always visible) -->
<div class="quick-stats-bar">
<div class="quick-stat"
hx-get="/api/monitoring/quick/cpu"
<div
class="quick-stat"
hx-get="/api/ui/monitoring/quick/cpu"
hx-trigger="load, every 5s"
hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
hx-swap="innerHTML"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="4" y="4" width="16" height="16" rx="2"></rect>
<rect x="9" y="9" width="6" height="6"></rect>
</svg>
<span class="stat-label">CPU</span>
<span class="stat-value">--%</span>
</div>
<div class="quick-stat"
hx-get="/api/monitoring/quick/memory"
<div
class="quick-stat"
hx-get="/api/ui/monitoring/quick/memory"
hx-trigger="load, every 5s"
hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
hx-swap="innerHTML"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="2" y="6" width="20" height="12" rx="2"></rect>
<line x1="6" y1="12" x2="6" y2="12"></line>
<line x1="10" y1="12" x2="10" y2="12"></line>
@ -233,11 +427,20 @@
<span class="stat-label">Memory</span>
<span class="stat-value">--%</span>
</div>
<div class="quick-stat"
hx-get="/api/monitoring/quick/disk"
<div
class="quick-stat"
hx-get="/api/ui/monitoring/quick/disk"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
hx-swap="innerHTML"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<ellipse cx="12" cy="5" rx="9" ry="3"></ellipse>
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"></path>
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"></path>
@ -245,11 +448,20 @@
<span class="stat-label">Disk</span>
<span class="stat-value">--%</span>
</div>
<div class="quick-stat"
hx-get="/api/monitoring/quick/network"
<div
class="quick-stat"
hx-get="/api/ui/monitoring/quick/network"
hx-trigger="load, every 5s"
hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
hx-swap="innerHTML"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M5 12.55a11 11 0 0 1 14.08 0"></path>
<path d="M1.42 9a16 16 0 0 1 21.16 0"></path>
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"></path>
@ -258,11 +470,20 @@
<span class="stat-label">Network</span>
<span class="stat-value">-- MB/s</span>
</div>
<div class="quick-stat"
hx-get="/api/monitoring/quick/requests"
<div
class="quick-stat"
hx-get="/api/ui/monitoring/quick/requests"
hx-trigger="load, every 5s"
hx-swap="innerHTML">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
hx-swap="innerHTML"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline>
</svg>
<span class="stat-label">Requests</span>
@ -270,5 +491,5 @@
</div>
</div>
<link rel="stylesheet" href="/static/suite/monitoring/monitoring.css">
<link rel="stylesheet" href="/static/suite/monitoring/monitoring.css" />
<script src="/static/suite/monitoring/monitoring.js"></script>

View file

@ -18,7 +18,7 @@
<div class="filter-group">
<label for="service-filter">Service</label>
<select id="service-filter" onchange="applyLogFilters()"
hx-get="/api/monitoring/logs/services"
hx-get="/api/ui/monitoring/logs/services"
hx-trigger="load"
hx-swap="innerHTML">
<option value="all">All Services</option>

View file

@ -13,7 +13,7 @@
Metrics Dashboard
</h2>
<span class="last-sync"
hx-get="/api/monitoring/metrics/last-sync"
hx-get="/api/ui/monitoring/metrics/last-sync"
hx-trigger="load, every 30s"
hx-swap="innerHTML">Last sync: --</span>
</div>
@ -45,7 +45,7 @@
<!-- Key Metrics Overview -->
<div class="key-metrics"
hx-get="/api/analytics/dashboard"
hx-get="/api/ui/analytics/dashboard"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
<div class="metric-card">
@ -168,7 +168,7 @@
</div>
</div>
<div class="chart-body" id="requests-chart"
hx-get="/api/analytics/metric?name=requests"
hx-get="/api/ui/analytics/metric?name=requests"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
<svg viewBox="0 0 600 200" class="chart-svg">
@ -207,7 +207,7 @@
</div>
</div>
<div class="chart-body" id="latency-chart"
hx-get="/api/analytics/metric?name=latency"
hx-get="/api/ui/analytics/metric?name=latency"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
<svg viewBox="0 0 600 200" class="chart-svg">
@ -247,7 +247,7 @@
</div>
</div>
<div class="chart-body" id="errors-chart"
hx-get="/api/analytics/metric?name=errors"
hx-get="/api/ui/analytics/metric?name=errors"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
<svg viewBox="0 0 600 200" class="chart-svg">
@ -289,7 +289,7 @@
</div>
</div>
<div class="chart-body" id="throughput-chart"
hx-get="/api/analytics/metric?name=throughput"
hx-get="/api/ui/analytics/metric?name=throughput"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
<svg viewBox="0 0 600 200" class="chart-svg">
@ -356,7 +356,7 @@
</div>
<div class="metrics-table-container">
<table class="metrics-table" id="metrics-table"
hx-get="/api/analytics/metrics/list"
hx-get="/api/ui/analytics/metrics/list"
hx-trigger="load"
hx-swap="innerHTML">
<thead>

View file

@ -80,7 +80,7 @@
</button>
<span
class="last-updated"
hx-get="/api/monitoring/timestamp"
hx-get="/api/ui/monitoring/timestamp"
hx-trigger="load, every 5s"
hx-swap="innerHTML"
>--</span
@ -778,7 +778,7 @@
font-family="system-ui"
font-size="28"
font-weight="700"
hx-get="/api/monitoring/metric/sessions"
hx-get="/api/ui/monitoring/metric/sessions"
hx-trigger="load, every 5s"
hx-swap="innerHTML"
>
@ -791,7 +791,7 @@
font-family="system-ui"
font-size="10"
text-anchor="end"
hx-get="/api/monitoring/trend/sessions"
hx-get="/api/ui/monitoring/trend/sessions"
hx-trigger="load, every 5s"
hx-swap="innerHTML"
>
@ -828,7 +828,7 @@
font-family="system-ui"
font-size="28"
font-weight="700"
hx-get="/api/monitoring/metric/messages"
hx-get="/api/ui/monitoring/metric/messages"
hx-trigger="load, every 10s"
hx-swap="innerHTML"
>
@ -841,7 +841,7 @@
font-family="system-ui"
font-size="10"
text-anchor="end"
hx-get="/api/monitoring/rate/messages"
hx-get="/api/ui/monitoring/rate/messages"
hx-trigger="load, every 10s"
hx-swap="innerHTML"
>
@ -878,7 +878,7 @@
font-family="system-ui"
font-size="28"
font-weight="700"
hx-get="/api/monitoring/metric/response_time"
hx-get="/api/ui/monitoring/metric/response_time"
hx-trigger="load, every 10s"
hx-swap="innerHTML"
>
@ -900,7 +900,7 @@
<g
transform="translate(330, 545)"
hx-get="/api/monitoring/resources/bars"
hx-get="/api/ui/monitoring/resources/bars"
hx-trigger="load, every 15s"
hx-swap="innerHTML"
>
@ -1095,7 +1095,7 @@
fill="#94a3b8"
font-family="system-ui"
font-size="11"
hx-get="/api/monitoring/activity/latest"
hx-get="/api/ui/monitoring/activity/latest"
hx-trigger="load, every 5s"
hx-swap="innerHTML"
>
@ -1151,7 +1151,7 @@
<!-- Sessions Panel -->
<div
class="monitor-panel"
hx-get="/api/monitoring/sessions"
hx-get="/api/ui/monitoring/sessions"
hx-trigger="load, every 5s"
hx-swap="innerHTML"
>
@ -1189,7 +1189,7 @@
<!-- Messages Panel -->
<div
class="monitor-panel"
hx-get="/api/monitoring/messages"
hx-get="/api/ui/monitoring/messages"
hx-trigger="load, every 10s"
hx-swap="innerHTML"
>
@ -1220,7 +1220,7 @@
<!-- Resources Panel -->
<div
class="monitor-panel resources-panel"
hx-get="/api/monitoring/resources"
hx-get="/api/ui/monitoring/resources"
hx-trigger="load, every 15s"
hx-swap="innerHTML"
>
@ -1271,7 +1271,7 @@
<!-- Services Panel -->
<div
class="monitor-panel services-panel"
hx-get="/api/monitoring/services"
hx-get="/api/ui/monitoring/services"
hx-trigger="load, every 30s"
hx-swap="innerHTML"
>
@ -1324,7 +1324,7 @@
<!-- Bots Panel -->
<div
class="monitor-panel bots-panel"
hx-get="/api/monitoring/bots"
hx-get="/api/ui/monitoring/bots"
hx-trigger="load, every 30s"
hx-swap="innerHTML"
>
@ -1369,7 +1369,7 @@
<div
id="service-status-container"
style="display: none"
hx-get="/api/monitoring/services/status"
hx-get="/api/ui/monitoring/services/status"
hx-trigger="load, every 30s"
hx-swap="innerHTML"
></div>

View file

@ -5,7 +5,7 @@
<div class="resource-cards">
<!-- CPU Card -->
<div class="resource-card cpu-card"
hx-get="/api/monitoring/resources/cpu"
hx-get="/api/ui/monitoring/resources/cpu"
hx-trigger="load, every 5s"
hx-swap="innerHTML">
<div class="card-icon">
@ -34,7 +34,7 @@
<!-- Memory Card -->
<div class="resource-card memory-card"
hx-get="/api/monitoring/resources/memory"
hx-get="/api/ui/monitoring/resources/memory"
hx-trigger="load, every 5s"
hx-swap="innerHTML">
<div class="card-icon">
@ -58,7 +58,7 @@
<!-- Disk Card -->
<div class="resource-card disk-card"
hx-get="/api/monitoring/resources/disk"
hx-get="/api/ui/monitoring/resources/disk"
hx-trigger="load, every 30s"
hx-swap="innerHTML">
<div class="card-icon">
@ -80,7 +80,7 @@
<!-- Network Card -->
<div class="resource-card network-card"
hx-get="/api/monitoring/resources/network"
hx-get="/api/ui/monitoring/resources/network"
hx-trigger="load, every 5s"
hx-swap="innerHTML">
<div class="card-icon">
@ -132,7 +132,7 @@
</div>
</div>
<div class="chart-container" id="cpu-chart"
hx-get="/api/monitoring/charts/cpu"
hx-get="/api/ui/monitoring/charts/cpu"
hx-trigger="load, every 10s"
hx-swap="innerHTML">
<div class="chart-placeholder">
@ -173,7 +173,7 @@
</div>
</div>
<div class="chart-container" id="memory-chart"
hx-get="/api/monitoring/charts/memory"
hx-get="/api/ui/monitoring/charts/memory"
hx-trigger="load, every 10s"
hx-swap="innerHTML">
<div class="chart-placeholder">
@ -217,7 +217,7 @@
</button>
</div>
<div class="detail-content" id="disk-partitions"
hx-get="/api/monitoring/resources/disk/partitions"
hx-get="/api/ui/monitoring/resources/disk/partitions"
hx-trigger="load, every 60s"
hx-swap="innerHTML">
<div class="partition-row">
@ -259,7 +259,7 @@
</select>
</div>
<div class="detail-content" id="process-list"
hx-get="/api/monitoring/resources/processes"
hx-get="/api/ui/monitoring/resources/processes"
hx-trigger="load, every 5s"
hx-swap="innerHTML">
<table class="process-table">
@ -295,7 +295,7 @@
</h3>
</div>
<div class="detail-content" id="network-interfaces"
hx-get="/api/monitoring/resources/network/interfaces"
hx-get="/api/ui/monitoring/resources/network/interfaces"
hx-trigger="load, every 10s"
hx-swap="innerHTML">
<div class="interface-row">
@ -334,7 +334,7 @@
</h3>
</div>
<div class="detail-content system-info"
hx-get="/api/monitoring/resources/system"
hx-get="/api/ui/monitoring/resources/system"
hx-trigger="load"
hx-swap="innerHTML">
<div class="info-row">

View file

@ -5,7 +5,7 @@
<div class="sidebar-header">
<h2>Paper</h2>
<button class="btn-icon" title="New Note"
hx-post="/api/paper/new"
hx-post="/api/ui/paper/new"
hx-target="#paper-list"
hx-swap="afterbegin">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -20,7 +20,7 @@
<input type="text"
placeholder="Search notes..."
name="q"
hx-get="/api/paper/search"
hx-get="/api/ui/paper/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#paper-list">
<svg class="search-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -31,7 +31,7 @@
<!-- Notes List -->
<div class="paper-list" id="paper-list"
hx-get="/api/paper/list"
hx-get="/api/ui/paper/list"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Notes loaded here -->
@ -41,19 +41,19 @@
<div class="sidebar-section">
<h3>Quick Start</h3>
<div class="template-grid">
<button class="template-btn" hx-post="/api/paper/template/blank" hx-target="#editor-content">
<button class="template-btn" hx-post="/api/ui/paper/template/blank" hx-target="#editor-content">
<span class="template-icon">📄</span>
<span>Blank</span>
</button>
<button class="template-btn" hx-post="/api/paper/template/meeting" hx-target="#editor-content">
<button class="template-btn" hx-post="/api/ui/paper/template/meeting" hx-target="#editor-content">
<span class="template-icon">📋</span>
<span>Meeting</span>
</button>
<button class="template-btn" hx-post="/api/paper/template/todo" hx-target="#editor-content">
<button class="template-btn" hx-post="/api/ui/paper/template/todo" hx-target="#editor-content">
<span class="template-icon"></span>
<span>To-Do</span>
</button>
<button class="template-btn" hx-post="/api/paper/template/research" hx-target="#editor-content">
<button class="template-btn" hx-post="/api/ui/paper/template/research" hx-target="#editor-content">
<span class="template-icon">🔬</span>
<span>Research</span>
</button>
@ -239,7 +239,7 @@
</svg>
<span>AI</span>
</button>
<button class="btn-icon" hx-post="/api/paper/save" hx-include="#editor-content" title="Save">
<button class="btn-icon" hx-post="/api/ui/paper/save" hx-include="#editor-content" title="Save">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"></path>
<polyline points="17 21 17 13 7 13 7 21"></polyline>
@ -270,7 +270,7 @@
id="editor-content"
contenteditable="true"
data-placeholder="Start writing, or type / for commands..."
hx-post="/api/paper/autosave"
hx-post="/api/ui/paper/autosave"
hx-trigger="keyup changed delay:2000ms"
hx-swap="none"></div>
</div>
@ -426,16 +426,16 @@
<div class="ai-panel-content">
<!-- Quick Actions -->
<div class="ai-quick-actions">
<button class="ai-action-btn" hx-post="/api/paper/ai/summarize" hx-include="[name='selected-text']">
<button class="ai-action-btn" hx-post="/api/ui/paper/ai/summarize" hx-include="[name='selected-text']">
<span>📝</span> Summarize
</button>
<button class="ai-action-btn" hx-post="/api/paper/ai/expand" hx-include="[name='selected-text']">
<button class="ai-action-btn" hx-post="/api/ui/paper/ai/expand" hx-include="[name='selected-text']">
<span>📖</span> Expand
</button>
<button class="ai-action-btn" hx-post="/api/paper/ai/improve" hx-include="[name='selected-text']">
<button class="ai-action-btn" hx-post="/api/ui/paper/ai/improve" hx-include="[name='selected-text']">
<span>✏️</span> Improve
</button>
<button class="ai-action-btn" hx-post="/api/paper/ai/simplify" hx-include="[name='selected-text']">
<button class="ai-action-btn" hx-post="/api/ui/paper/ai/simplify" hx-include="[name='selected-text']">
<span>🎯</span> Simplify
</button>
</div>
@ -463,7 +463,7 @@
<option value="zh">Chinese</option>
<option value="ja">Japanese</option>
</select>
<button class="ai-action-btn" hx-post="/api/paper/ai/translate" hx-include="#translate-lang, [name='selected-text']">
<button class="ai-action-btn" hx-post="/api/ui/paper/ai/translate" hx-include="#translate-lang, [name='selected-text']">
Translate
</button>
</div>
@ -472,7 +472,7 @@
<div class="ai-custom-section">
<label>Custom</label> AI Command</label>
<textarea id="ai-custom-prompt" placeholder="Tell AI what to do with selected text..."></textarea>
<button class="btn-primary" hx-post="/api/paper/ai/custom" hx-include="#ai-custom-prompt, [name='selected-text']">
<button class="btn-primary" hx-post="/api/ui/paper/ai/custom" hx-include="#ai-custom-prompt, [name='selected-text']">
Run Command
</button>
</div>
@ -543,23 +543,23 @@
</div>
<div class="modal-body">
<div class="export-options">
<button class="export-option" hx-get="/api/paper/export/pdf" hx-swap="none">
<button class="export-option" hx-get="/api/ui/paper/export/pdf" hx-swap="none">
<span class="export-icon">📄</span>
<span class="export-label">PDF</span>
</button>
<button class="export-option" hx-get="/api/paper/export/docx" hx-swap="none">
<button class="export-option" hx-get="/api/ui/paper/export/docx" hx-swap="none">
<span class="export-icon">📝</span>
<span class="export-label">Word (.docx)</span>
</button>
<button class="export-option" hx-get="/api/paper/export/md" hx-swap="none">
<button class="export-option" hx-get="/api/ui/paper/export/md" hx-swap="none">
<span class="export-icon">📑</span>
<span class="export-label">Markdown</span>
</button>
<button class="export-option" hx-get="/api/paper/export/html" hx-swap="none">
<button class="export-option" hx-get="/api/ui/paper/export/html" hx-swap="none">
<span class="export-icon">🌐</span>
<span class="export-label">HTML</span>
</button>
<button class="export-option" hx-get="/api/paper/export/txt" hx-swap="none">
<button class="export-option" hx-get="/api/ui/paper/export/txt" hx-swap="none">
<span class="export-icon">📋</span>
<span class="export-label">Plain Text</span>
</button>
@ -1534,7 +1534,7 @@
status.textContent = 'Saving...';
status.className = 'status-item save-status saving';
htmx.ajax('POST', '/api/paper/save', {
htmx.ajax('POST', '/api/ui/paper/save', {
target: 'none',
values: {
title: title.innerText,
@ -1681,7 +1681,7 @@
const tone = this.dataset.tone;
const selectedText = document.getElementById('selected-text-input').value;
htmx.ajax('POST', '/api/paper/ai/tone', {
htmx.ajax('POST', '/api/ui/paper/ai/tone', {
target: '#ai-response-content',
values: {
tone: tone,

View file

@ -473,7 +473,7 @@
const selectedText = document.getElementById('selected-text-input')?.value || '';
if (typeof htmx !== 'undefined') {
htmx.ajax('POST', '/api/paper/ai/tone', {
htmx.ajax('POST', '/api/ui/paper/ai/tone', {
target: '#ai-response-content',
values: { tone, text: selectedText }
}).then(() => {
@ -609,7 +609,7 @@
const content = elements.editor?.innerHTML || '';
if (typeof htmx !== 'undefined') {
htmx.ajax('POST', '/api/paper/save', {
htmx.ajax('POST', '/api/ui/paper/save', {
swap: 'none',
values: { title, content }
}).then(() => {

View file

@ -50,7 +50,7 @@
<div class="section-header">
<h3>Collections</h3>
<button class="btn-icon-sm" title="New Collection"
hx-post="/api/research/collections/new"
hx-post="/api/ui/research/collections/new"
hx-target="#collections-list"
hx-swap="afterbegin">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -60,7 +60,7 @@
</button>
</div>
<div class="collections-list" id="collections-list"
hx-get="/api/research/collections"
hx-get="/api/ui/research/collections"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Collections loaded here -->
@ -71,7 +71,7 @@
<div class="sidebar-section">
<h3>Recent</h3>
<div class="recent-list" id="recent-searches"
hx-get="/api/research/recent"
hx-get="/api/ui/research/recent"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Recent searches loaded here -->
@ -83,7 +83,7 @@
<div class="section-header">
<h3>Prompts</h3>
<button class="btn-icon-sm" title="Browse All"
hx-get="/api/research/prompts"
hx-get="/api/ui/research/prompts"
hx-target="#main-results">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"></polyline>
@ -133,7 +133,7 @@
<!-- Search Header -->
<div class="search-header">
<form class="search-form" id="research-form"
hx-post="/api/research/search"
hx-post="/api/ui/research/search"
hx-target="#main-results"
hx-indicator="#search-indicator">
<div class="search-input-wrapper">
@ -217,7 +217,7 @@
<div class="trending-section">
<h3>Trending</h3>
<div class="trending-tags" id="trending-tags"
hx-get="/api/research/trending"
hx-get="/api/ui/research/trending"
hx-trigger="load"
hx-swap="innerHTML">
<!-- Trending tags loaded here -->
@ -237,7 +237,7 @@
<h3>Sources</h3>
<div class="sources-actions">
<button class="btn-icon-sm" title="Export Citations"
hx-get="/api/research/export-citations"
hx-get="/api/ui/research/export-citations"
hx-swap="none">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"></path>
@ -1386,7 +1386,7 @@
const content = document.getElementById('answer-content');
if (content) {
// Send to Paper via HTMX
htmx.ajax('POST', '/api/paper/import', {
htmx.ajax('POST', '/api/ui/paper/import', {
values: {
content: content.innerHTML,
title: searchInput.value
@ -1422,7 +1422,7 @@
function updateSourceCounts() {
// This would typically come from the API response
// For now, we'll use placeholder logic
htmx.ajax('GET', '/api/research/source-counts', {
htmx.ajax('GET', '/api/ui/research/source-counts', {
swap: 'none'
}).then(response => {
// Update counts in sidebar

View file

@ -257,7 +257,7 @@
if (exportBtn) {
const content = document.getElementById('answer-content');
if (content && typeof htmx !== 'undefined') {
htmx.ajax('POST', '/api/paper/import', {
htmx.ajax('POST', '/api/ui/paper/import', {
values: {
content: content.innerHTML,
title: elements.searchInput?.value || 'Research Export'
@ -287,7 +287,7 @@
*/
function updateSourceCounts() {
if (typeof htmx !== 'undefined') {
htmx.ajax('GET', '/api/research/source-counts', {
htmx.ajax('GET', '/api/ui/research/source-counts', {
swap: 'none'
}).then(response => {
// Update counts in sidebar if response contains them
@ -304,7 +304,7 @@
const collectionName = prompt('Enter collection name:');
if (collectionName && typeof htmx !== 'undefined') {
const content = document.getElementById('answer-content');
htmx.ajax('POST', '/api/research/collections/save', {
htmx.ajax('POST', '/api/ui/research/collections/save', {
values: {
collection: collectionName,
content: content?.innerHTML || '',

View file

@ -10,7 +10,7 @@
</div>
<nav class="settings-nav">
<a href="#profile" class="nav</span>-item active" onclick="showSection('profile', this)">
<a href="#profile" class="nav-item active" onclick="showSection('profile', this)">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
<circle cx="12" cy="7" r="4"></circle>
@ -683,7 +683,7 @@
<div class="setting-card">
<div class="card-header">
<h2>Webhooks</h2>
<button class="btn-primary btn-sm" onclick="document.getElementById('create-webhook-modal').showModal()">
<button class="btn-primary btn-sm" onclick="document.getElementById('add-webhook-modal').showModal()">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
@ -1701,22 +1701,43 @@
/* Modal Styles */
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100%;
height: 100%;
max-width: 100%;
max-height: 100%;
background: transparent;
border: none;
padding: 0;
max-width: 500px;
width: 90%;
margin: 0;
z-index: 10000;
display: none;
align-items: center;
justify-content: center;
}
.modal[open] {
display: flex;
}
.modal::backdrop {
background: rgba(0, 0, 0, 0.5);
background: rgba(0, 0, 0, 0.7);
}
.modal-content {
background: var(--surface);
background: var(--surface, #1a1a24);
border-radius: 16px;
border: 1px solid var(--border);
border: 1px solid var(--border, #2a2a2a);
overflow: hidden;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
max-width: 500px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
}
.modal-header {

View file

@ -13,7 +13,7 @@
</div>
</div>
<div class="accounts-grid" id="accounts-list" hx-get="/api/sources/accounts/list" hx-trigger="load" hx-swap="innerHTML">
<div class="accounts-grid" id="accounts-list" hx-get="/api/ui/sources/accounts/list" hx-trigger="load" hx-swap="innerHTML">
<div class="loading-spinner">
<div class="spinner"></div>
<p>Loading accounts...</p>

File diff suppressed because it is too large Load diff

View file

@ -271,7 +271,7 @@
const collectionName = prompt("Enter collection name:");
if (collectionName && typeof htmx !== "undefined") {
htmx
.ajax("POST", "/api/sources/prompts/save", {
.ajax("POST", "/api/ui/sources/prompts/save", {
values: {
promptId,
collection: collectionName,
@ -652,7 +652,7 @@
.then(() => {
showToast("Repository connected");
// Refresh the repo card
htmx.ajax("GET", "/api/sources/repositories", {
htmx.ajax("GET", "/api/ui/sources/repositories", {
target: "#content-area",
swap: "innerHTML",
});
@ -672,7 +672,7 @@
})
.then(() => {
showToast("Repository disconnected");
htmx.ajax("GET", "/api/sources/repositories", {
htmx.ajax("GET", "/api/ui/sources/repositories", {
target: "#content-area",
swap: "innerHTML",
});
@ -779,6 +779,46 @@
});
}
/**
* Filter MCP catalog by category
*/
function filterMcpCategory(btn, category) {
document
.querySelectorAll("#mcp-category-filter .category-btn")
.forEach((b) => {
b.classList.remove("active");
b.style.background = "#f5f5f5";
b.style.color = "#333";
});
btn.classList.add("active");
btn.style.background = "#2196F3";
btn.style.color = "white";
document.querySelectorAll(".server-card").forEach((card) => {
if (category === "all" || card.dataset.category === category) {
card.style.display = "";
} else {
card.style.display = "none";
}
});
}
/**
* Add MCP server from catalog
*/
function addCatalogServer(id, name) {
if (confirm('Add "' + name + '" to your MCP configuration?')) {
if (typeof htmx !== "undefined") {
htmx.ajax("POST", "/api/ui/sources/mcp/add-from-catalog", {
values: { server_id: id },
target: "#mcp-grid",
swap: "innerHTML",
});
showToast('Server "' + name + '" added to configuration');
}
}
}
/**
* Show toast notification
*/
@ -823,7 +863,13 @@
openApp,
editApp,
insertMention,
filterMcpCategory,
addCatalogServer,
getTaskContext: window.getTaskContext,
clearTaskContext: window.clearTaskContext,
};
// Expose globally for inline onclick handlers
window.filterMcpCategory = filterMcpCategory;
window.addCatalogServer = addCatalogServer;
})();

View file

@ -97,7 +97,7 @@
<div
class="intent-list"
id="intent-list"
hx-get="/api/autotask/list"
hx-get="/api/ui/autotask/list"
hx-trigger="load, every 10s"
hx-swap="innerHTML"
>
@ -130,7 +130,7 @@
<div class="modal-body">
<form
id="new-intent-form"
hx-post="/api/autotask/create"
hx-post="/api/ui/autotask/create"
hx-ext="json-enc"
hx-target="#intent-list"
hx-swap="afterbegin"

View file

@ -134,10 +134,11 @@
flex-shrink: 0;
}
/* Status section - compact, single line */
/* Status section - slightly taller */
.taskmd-section-status {
flex: 0 0 auto;
max-height: 60px;
min-height: 80px;
max-height: 100px;
overflow: hidden !important;
}
@ -150,12 +151,12 @@
overflow: hidden !important;
}
/* Terminal section - fixed at bottom, compact */
/* Terminal section - fixed at bottom, taller */
.taskmd-section-terminal {
flex: 0 0 100px;
height: 100px;
min-height: 100px;
max-height: 100px;
flex: 0 0 150px;
height: 150px;
min-height: 150px;
max-height: 200px;
display: flex;
flex-direction: column;
overflow: hidden !important;
@ -173,10 +174,11 @@
flex-shrink: 0;
}
/* STATUS Section - single line */
/* STATUS Section - better spacing */
.taskmd-status-content {
padding: 12px 24px;
padding: 16px 24px;
background: var(--surface, #111);
min-height: 50px;
}
.status-row {
@ -993,8 +995,8 @@
flex-shrink: 0;
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 20px 24px;
gap: 10px;
padding: 10px 20px;
border-top: 1px solid var(--border, #1a1a1a);
background: var(--bg-secondary, #0d0d0d);
}
@ -1002,11 +1004,11 @@
.taskmd-actions .btn-action-rich {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
gap: 6px;
padding: 8px 16px;
border: none;
border-radius: 8px;
font-size: 14px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
@ -1017,14 +1019,14 @@
background: var(--success, #22c55e);
color: #fff;
font-weight: 600;
padding: 12px 24px;
font-size: 15px;
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.4);
padding: 8px 18px;
font-size: 13px;
box-shadow: 0 2px 8px rgba(34, 197, 94, 0.3);
}
.taskmd-actions .btn-open-app:hover {
background: #16a34a;
box-shadow: 0 6px 20px rgba(34, 197, 94, 0.5);
box-shadow: 0 4px 12px rgba(34, 197, 94, 0.4);
transform: translateY(-1px);
}
@ -1032,6 +1034,8 @@
background: transparent;
border: 1px solid var(--border, #333);
color: var(--text-secondary, #888);
padding: 6px 14px;
font-size: 12px;
}
.taskmd-actions .btn-cancel:hover {
@ -1071,24 +1075,28 @@
/* Scrollbar */
.task-detail-rich::-webkit-scrollbar,
.taskmd-terminal-output::-webkit-scrollbar {
width: 6px;
.taskmd-terminal-output::-webkit-scrollbar,
.taskmd-progress-content::-webkit-scrollbar {
width: 8px;
}
.task-detail-rich::-webkit-scrollbar-track,
.taskmd-terminal-output::-webkit-scrollbar-track {
background: #0a0a0a;
.taskmd-terminal-output::-webkit-scrollbar-track,
.taskmd-progress-content::-webkit-scrollbar-track {
background: var(--surface, #1a1a1a);
}
.task-detail-rich::-webkit-scrollbar-thumb,
.taskmd-terminal-output::-webkit-scrollbar-thumb {
background: #222;
border-radius: 3px;
.taskmd-terminal-output::-webkit-scrollbar-thumb,
.taskmd-progress-content::-webkit-scrollbar-thumb {
background: var(--primary, #c5f82a);
border-radius: 4px;
}
.task-detail-rich::-webkit-scrollbar-thumb:hover,
.taskmd-terminal-output::-webkit-scrollbar-thumb:hover {
background: #333;
.taskmd-terminal-output::-webkit-scrollbar-thumb:hover,
.taskmd-progress-content::-webkit-scrollbar-thumb:hover {
background: var(--primary-hover, #d4ff4a);
}
/* =============================================================================

View file

@ -555,7 +555,7 @@ body:has(.tasks-app) {
color: var(--error, #ef4444);
}
.priority-badge.priority-normal {
.priority-badge.priority-medium {
background: var(--surface, #1a1a1a);
color: var(--text-secondary, #888);
}
@ -1973,7 +1973,7 @@ body:has(.tasks-app) {
.floating-progress-log {
max-height: 150px;
overflow-y: auto;
background: var(--bg, #0a0a0a);
background: transparent;
border: 1px solid var(--border, #2a2a2a);
border-radius: 6px;
padding: 8px;
@ -2008,7 +2008,7 @@ body:has(.tasks-app) {
.floating-llm-terminal {
max-height: 120px;
overflow-y: auto;
background: var(--bg, #0a0a0a);
background: transparent;
border: 1px solid var(--border, #2a2a2a);
border-radius: 6px;
padding: 8px;
@ -2703,7 +2703,7 @@ body:has(.tasks-app) {
/* Terminal Section Rich */
.terminal-section-rich {
background: #0a0a0a;
background: var(--bg, var(--surface, #0a0a0a));
border: 2px solid var(--border, #2a2a2a);
border-radius: 12px;
}
@ -2747,7 +2747,7 @@ body:has(.tasks-app) {
}
.terminal-output-rich {
background: #000;
background: transparent;
border-radius: 10px;
padding: 16px 20px;
font-family: "JetBrains Mono", "Fira Code", "Consolas", monospace;

View file

@ -6,7 +6,7 @@
<div class="tasks-app">
<!-- Hidden element to load stats on page load -->
<div
hx-get="/api/tasks/stats"
hx-get="/api/ui/tasks/stats"
hx-trigger="load, taskCreated from:body"
hx-swap="innerHTML"
style="display: none"
@ -17,7 +17,7 @@
<button
class="filter-pill"
data-filter="complete"
hx-get="/api/tasks?filter=complete"
hx-get="/api/ui/tasks?filter=complete"
hx-target="#task-list"
hx-swap="innerHTML"
>
@ -28,7 +28,7 @@
<button
class="filter-pill active"
data-filter="all"
hx-get="/api/tasks?filter=all"
hx-get="/api/ui/tasks?filter=all"
hx-target="#task-list"
hx-swap="innerHTML"
>
@ -39,7 +39,7 @@
<button
class="filter-pill"
data-filter="active"
hx-get="/api/tasks?filter=active"
hx-get="/api/ui/tasks?filter=active"
hx-target="#task-list"
hx-swap="innerHTML"
>
@ -50,7 +50,7 @@
<button
class="filter-pill"
data-filter="awaiting"
hx-get="/api/tasks?filter=awaiting"
hx-get="/api/ui/tasks?filter=awaiting"
hx-target="#task-list"
hx-swap="innerHTML"
>
@ -61,7 +61,7 @@
<button
class="filter-pill"
data-filter="paused"
hx-get="/api/tasks?filter=paused"
hx-get="/api/ui/tasks?filter=paused"
hx-target="#task-list"
hx-swap="innerHTML"
>
@ -72,7 +72,7 @@
<button
class="filter-pill"
data-filter="blocked"
hx-get="/api/tasks?filter=blocked"
hx-get="/api/ui/tasks?filter=blocked"
hx-target="#task-list"
hx-swap="innerHTML"
>
@ -100,7 +100,7 @@
<button
id="quick-intent-btn"
class="btn-create-run"
hx-post="/api/autotask/create"
hx-post="/api/ui/autotask/create"
hx-ext="json-enc"
hx-include="#quick-intent-input"
hx-target="#intent-result-hidden"
@ -123,7 +123,7 @@
<div
class="tasks-list-scroll"
id="task-list"
hx-get="/api/tasks?filter=all"
hx-get="/api/ui/tasks?filter=all"
hx-trigger="load, taskCreated from:body throttle:2s"
hx-swap="innerHTML transition:false"
>
@ -242,9 +242,10 @@
<div class="form-group">
<label for="intent-priority">Priority</label>
<select id="intent-priority" name="priority">
<option value="normal">Normal</option>
<option value="high">High</option>
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
<option value="urgent">Urgent</option>
</select>
</div>
<div class="form-group">

View file

@ -2196,7 +2196,7 @@ function addTaskCardToList(taskId, title, status) {
</div>
<div class="task-card-meta">
<span class="task-card-status ${statusClass}">${statusText}</span>
<span class="task-card-priority">normal</span>
<span class="task-card-priority">medium</span>
</div>
<div class="task-card-progress">
<div class="task-progress-bar">