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 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()
@@ -14,12 +14,18 @@ app = FastAPI(
version="0.1.0",
)
# Static and templates
app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
# Define templates EARLY (before importing dashboard)
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
app.include_router(public.router)
app.include_router(profile.router)
app.include_router(messages.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 %}