Adding dashboard code.

This commit is contained in:
Michael Woods
2025-12-21 22:28:36 -05:00
parent 7472c08269
commit ddee6226b4
4 changed files with 159 additions and 4 deletions

View File

@@ -0,0 +1,22 @@
# packetserver/http/routers/dashboard.py
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
from packetserver.http.dependencies import get_current_http_user
from packetserver.http.auth import HttpUser
from packetserver.http.server import templates # for TemplateResponse
router = APIRouter(tags=["dashboard"])
@router.get("/dashboard", response_class=HTMLResponse)
async def dashboard(request: Request, current_user: HttpUser = Depends(get_current_http_user)):
# Fetch messages via internal API call (reuse list logic)
from packetserver.http.routers.messages import get_messages as api_get_messages
messages_resp = await api_get_messages(current_user=current_user, type="all", limit=100)
messages = messages_resp["messages"]
return templates.TemplateResponse(
"dashboard.html",
{"request": request, "current_user": current_user.username, "messages": messages}
)

View File

@@ -4,7 +4,7 @@ from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from pathlib import Path from pathlib import Path
from .routers import public, profile, messages, send from .routers import public, profile, messages, send # dashboard removed for now
BASE_DIR = Path(__file__).parent.resolve() BASE_DIR = Path(__file__).parent.resolve()
@@ -14,12 +14,18 @@ app = FastAPI(
version="0.1.0", version="0.1.0",
) )
# Static and templates # Define templates EARLY (before importing dashboard)
app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
templates = Jinja2Templates(directory=BASE_DIR / "templates") templates = Jinja2Templates(directory=BASE_DIR / "templates")
# Static files
app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
# Now safe to import dashboard (it needs templates)
from .routers import dashboard # add this line
# Include routers # Include routers
app.include_router(public.router) app.include_router(public.router)
app.include_router(profile.router) app.include_router(profile.router)
app.include_router(messages.router) app.include_router(messages.router)
app.include_router(send.router) app.include_router(send.router)
app.include_router(dashboard.router) # now works

View File

@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{% block title %}PacketServer Dashboard{% endblock %}</title>
<link rel="stylesheet" href="{{ url_for('static', path='/css/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', path='/css/style.css') }}"> {# optional custom #}
</head>
<body class="bg-light">
<nav class="navbar navbar-dark bg-primary mb-4">
<div class="container-fluid">
<a class="navbar-brand" href="{{ url_for('dashboard') }}">PacketServer BBS</a>
<span class="navbar-text">
Logged in as: <strong>{{ current_user }}</strong>
{# Basic Auth note #}
<small class="text-light ms-3">(Close browser to logout)</small>
</span>
</div>
</nav>
<div class="container">
{% block content %}{% endblock %}
</div>
<script src="{{ url_for('static', path='/js/bootstrap.bundle.min.js') }}"></script>
{% block scripts %}{% endblock %}
</body>
</html>

View File

@@ -0,0 +1,98 @@
{% extends "base.html" %}
{% block title %}Dashboard - {{ current_user }}{% endblock %}
{% block content %}
<h2 class="mb-4">Messages</h2>
<!-- Compose Modal -->
<div class="modal fade" id="composeModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<form id="composeForm">
<div class="modal-header">
<h5 class="modal-title">Compose Message</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label">To (comma-separated callsigns or ALL):</label>
<input type="text" class="form-control" name="to" required placeholder="W1AW,N0CALL or ALL">
</div>
<div class="mb-3">
<label class="form-label">Message:</label>
<textarea class="form-control" name="text" rows="5" required></textarea>
</div>
<div id="composeAlert"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Send</button>
</div>
</form>
</div>
</div>
</div>
<button class="btn btn-success mb-3" data-bs-toggle="modal" data-bs-target="#composeModal">
Compose New Message
</button>
<table class="table table-striped table-hover">
<thead class="table-dark">
<tr>
<th>From</th>
<th>To</th>
<th>Preview</th>
<th>Date</th>
<th>Read</th>
</tr>
</thead>
<tbody>
{% for msg in messages %}
<tr class="clickable-row" style="cursor: pointer;" onclick="window.location='/dashboard/message/{{ msg.id }}'">
<td>{{ msg.from }}</td>
<td>{{ msg.to | join(', ') }}</td>
<td>{{ msg.text | truncate(60) | escape }}</td>
<td>{{ msg.sent_at | replace('Z', '') }}</td>
<td>{% if msg.retrieved %}<span class="text-success"></span>{% else %}<span class="text-warning"></span>{% endif %}</td>
</tr>
{% else %}
<tr><td colspan="5" class="text-center">No messages</td></tr>
{% endfor %}
</tbody>
</table>
{% endblock %}
{% block scripts %}
<script>
document.getElementById('composeForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const to = formData.get('to').split(',').map(s => s.trim().toUpperCase());
const payload = {
to: to,
text: formData.get('text')
};
try {
const resp = await fetch('/api/v1/messages', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
credentials: 'include' // sends Basic Auth
});
if (resp.ok) {
const data = await resp.json();
document.getElementById('composeAlert').innerHTML = '<div class="alert alert-success">Sent! ID: ' + data.message_id + '</div>';
setTimeout(() => location.reload(), 1500);
} else {
const err = await resp.json();
document.getElementById('composeAlert').innerHTML = '<div class="alert alert-danger">Error: ' + (err.detail || resp.status) + '</div>';
}
} catch (err) {
document.getElementById('composeAlert').innerHTML = '<div class="alert alert-danger">Network error</div>';
}
});
</script>
{% endblock %}