Bulletins have a section on dashboard now.

This commit is contained in:
Michael Woods
2025-12-24 20:12:31 -05:00
parent f0f78af056
commit 6b15ad5174
6 changed files with 114 additions and 3 deletions

View File

@@ -1,5 +1,5 @@
from fastapi import APIRouter, Path, Query, Depends, HTTPException, Request, status from fastapi import APIRouter, Path, Query, Depends, HTTPException, Request, status, Form
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse, RedirectResponse
from typing import Optional, List from typing import Optional, List
from pydantic import BaseModel, Field, constr from pydantic import BaseModel, Field, constr
from datetime import datetime from datetime import datetime
@@ -130,6 +130,48 @@ async def bulletin_list_page(
{"request": request, "bulletins": bulletins, "current_user": current_user.username} {"request": request, "bulletins": bulletins, "current_user": current_user.username}
) )
@html_router.get("/bulletins/new", response_class=HTMLResponse)
async def bulletin_new_form(
request: Request,
current_user: HttpUser = Depends(get_current_http_user) # require login, consistent with site
):
return templates.TemplateResponse(
"bulletin_new.html",
{"request": request, "error": None}
)
@html_router.post("/bulletins/new")
async def bulletin_new_submit(
request: Request,
subject: str = Form(...),
body: str = Form(...),
current_user: HttpUser = Depends(get_current_http_user)
):
if not subject.strip() or not body.strip():
return templates.TemplateResponse(
"bulletin_new.html",
{"request": request, "error": "Subject and body are required."},
status_code=400
)
from packetserver.runners.http_server import get_db_connection
conn = get_db_connection()
root = conn.root()
if 'bulletins' not in root:
root['bulletins'] = PersistentList()
new_bulletin = Bulletin(
author=current_user.username,
subject=subject.strip(),
text=body.strip()
)
new_id = new_bulletin.write_new(root)
transaction.commit()
return RedirectResponse(url=f"/bulletins/{new_id}", status_code=303)
@html_router.get("/bulletins/{bid}", response_class=HTMLResponse) @html_router.get("/bulletins/{bid}", response_class=HTMLResponse)
async def bulletin_detail_page( async def bulletin_detail_page(
request: Request, request: Request,

View File

@@ -10,6 +10,7 @@ router = APIRouter(tags=["dashboard"])
# Import the function at module level (safe now that circular import is fixed) # Import the function at module level (safe now that circular import is fixed)
from packetserver.http.routers.messages import get_messages as api_get_messages from packetserver.http.routers.messages import get_messages as api_get_messages
from .bulletins import list_bulletins
@router.get("/dashboard", response_class=HTMLResponse) @router.get("/dashboard", response_class=HTMLResponse)
@@ -26,9 +27,17 @@ async def dashboard(
) )
messages = messages_resp["messages"] messages = messages_resp["messages"]
bulletins_resp = await list_bulletins(limit=10, since=None)
recent_bulletins = bulletins_resp["bulletins"]
return templates.TemplateResponse( return templates.TemplateResponse(
"dashboard.html", "dashboard.html",
{"request": request, "current_user": current_user.username, "messages": messages} {
"request": request,
"current_user": current_user.username,
"messages": messages,
"bulletins": recent_bulletins
}
) )
@router.get("/dashboard/profile", response_class=HTMLResponse) @router.get("/dashboard/profile", response_class=HTMLResponse)

View File

View File

@@ -6,6 +6,12 @@
<title>{% block title %}PacketServer Dashboard{% endblock %}</title> <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/bootstrap.min.css') }}">
<link rel="stylesheet" href="{{ url_for('static', path='/css/style.css') }}"> {# optional custom #} <link rel="stylesheet" href="{{ url_for('static', path='/css/style.css') }}"> {# optional custom #}
<style>
.bulletin-list { list-style: none; padding: 0; }
.bulletin-list li { margin: 1em 0; padding: 1em; background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.meta { color: #666; font-size: 0.9em; display: block; margin-top: 0.3em; }
.preview { margin-top: 0.5em; color: #444; }
</style>
</head> </head>
<body class="bg-light"> <body class="bg-light">
<nav class="navbar navbar-dark bg-primary mb-4"> <nav class="navbar navbar-dark bg-primary mb-4">

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>New Bulletin - PacketServer</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 2em auto; padding: 1em; background: #f9f9f9; }
h1 { text-align: center; }
.error { color: #721c24; background: #f8d7da; padding: 1em; border-radius: 5px; margin-bottom: 1em; }
label { display: block; margin: 1em 0 0.5em; font-weight: bold; }
input[type="text"], textarea { width: 100%; padding: 0.8em; box-sizing: border-box; font-family: monospace; }
textarea { height: 300px; }
button { margin-top: 1em; padding: 0.8em 1.5em; background: #007bff; color: white; border: none; cursor: pointer; }
button:hover { background: #0056b3; }
</style>
</head>
<body>
<h1>Create New Bulletin</h1>
{% if error %}
<div class="error">{{ error }}</div>
{% endif %}
<form method="post">
<label for="subject">Subject</label>
<input type="text" id="subject" name="subject" required>
<label for="body">Body</label>
<textarea id="body" name="body" required></textarea>
<button type="submit">Create Bulletin</button>
</form>
<p><a href="/bulletins">← Back to Bulletins</a> | <a href="/dashboard">Dashboard</a></p>
</body>
</html>

View File

@@ -3,6 +3,24 @@
{% block title %}Dashboard - {{ current_user }}{% endblock %} {% block title %}Dashboard - {{ current_user }}{% endblock %}
{% block content %} {% block content %}
<h2>Recent Bulletins</h2>
{% if bulletins %}
<ul class="bulletin-list">
{% for bull in bulletins %}
<li>
<strong><a href="/bulletins/{{ bull.id }}">{{ bull.subject }}</a></strong>
<span class="meta">by {{ bull.author }} on {{ bull.created_at[:10] }}</span>
<div class="preview">{{ bull.body[:150] }}{% if bull.body|length > 150 %}...{% endif %}</div>
</li>
{% endfor %}
</ul>
<p><a href="/bulletins">View all bulletins →</a></p>
{% else %}
<p>No bulletins yet.</p>
<p><a href="/bulletins/new">Create the first one!</a></p>
{% endif %}
<h2 class="mb-4">Messages</h2> <h2 class="mb-4">Messages</h2>
<!-- Compose Modal --> <!-- Compose Modal -->