Adding dashboard code.
This commit is contained in:
22
packetserver/http/routers/dashboard.py
Normal file
22
packetserver/http/routers/dashboard.py
Normal 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}
|
||||
)
|
||||
@@ -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(send.router)
|
||||
app.include_router(dashboard.router) # now works
|
||||
29
packetserver/http/templates/base.html
Normal file
29
packetserver/http/templates/base.html
Normal 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>
|
||||
98
packetserver/http/templates/dashboard.html
Normal file
98
packetserver/http/templates/dashboard.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user