feat: add dashboard ping button and .env file support
Add a "Send IRC Message" section to the admin dashboard that sends a configurable privmsg to any IRC nick or channel via all connected bots. New POST /admin/ping endpoint broadcasts a "privmsg" WebSocket message type, handled by both Limnoria and Sopel plugins. Also enable pydantic-settings .env file loading (python-dotenv) and add .env.example documenting all NTR_* configuration variables. Made-with: Cursor
This commit is contained in:
@@ -67,6 +67,21 @@
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn-group { display: flex; gap: 4px; }
|
||||
.ping-section {
|
||||
margin-top: 2rem;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid #333;
|
||||
}
|
||||
.ping-form {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.ping-form label { margin-bottom: 0; }
|
||||
.ping-form input { margin-bottom: 0; }
|
||||
.ping-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
.ping-btn.success { background: #4caf50; border-color: #4caf50; pointer-events: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -89,6 +104,22 @@
|
||||
<p>Loading shows...</p>
|
||||
</section>
|
||||
|
||||
<section class="ping-section">
|
||||
<h4>Send IRC Message</h4>
|
||||
<div class="ping-form">
|
||||
<label>
|
||||
Target
|
||||
<input type="text" id="ping-target" value="{{PING_TARGET}}" placeholder="nick or #channel">
|
||||
</label>
|
||||
<label>
|
||||
Message
|
||||
<input type="text" id="ping-message" value="{{PING_MESSAGE}}" placeholder="message text">
|
||||
</label>
|
||||
<button class="ping-btn" id="ping-btn" disabled title="No bots connected"
|
||||
onclick="sendPing(this)">Send</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="toast" id="toast"></div>
|
||||
</main>
|
||||
|
||||
@@ -256,6 +287,39 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function sendPing(btn) {
|
||||
const target = document.getElementById("ping-target").value.trim();
|
||||
const message = document.getElementById("ping-message").value.trim();
|
||||
if (!target || !message) {
|
||||
showToast("Target and message are required", true);
|
||||
return;
|
||||
}
|
||||
btn.disabled = true;
|
||||
btn.textContent = "...";
|
||||
try {
|
||||
const resp = await fetch("/admin/ping", {
|
||||
method: "POST",
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: JSON.stringify({target, message}),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const data = await resp.json().catch(() => ({}));
|
||||
throw new Error(data.detail || "Send failed");
|
||||
}
|
||||
btn.textContent = "\u2713";
|
||||
btn.classList.add("success");
|
||||
setTimeout(() => {
|
||||
btn.textContent = "Send";
|
||||
btn.classList.remove("success");
|
||||
btn.disabled = subscriberCount === 0;
|
||||
}, 2000);
|
||||
} catch (e) {
|
||||
showToast(e.message, true);
|
||||
btn.textContent = "Send";
|
||||
btn.disabled = subscriberCount === 0;
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(count, clients) {
|
||||
subscriberCount = count;
|
||||
const dot = document.getElementById("status-dot");
|
||||
@@ -288,6 +352,11 @@
|
||||
btn.title = "";
|
||||
}
|
||||
});
|
||||
const pingBtn = document.getElementById("ping-btn");
|
||||
if (pingBtn && !pingBtn.classList.contains("success")) {
|
||||
pingBtn.disabled = count === 0;
|
||||
pingBtn.title = count === 0 ? "No bots connected" : "";
|
||||
}
|
||||
}
|
||||
|
||||
let wsBackoff = 1000;
|
||||
|
||||
Reference in New Issue
Block a user