Sessions now update live across all users and devices

This commit is contained in:
NPS Agent
2026-05-18 11:41:02 +09:30
parent 75c6614e81
commit a17aafbbc9
5 changed files with 108 additions and 16 deletions
+68 -14
View File
@@ -1,15 +1,37 @@
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi import FastAPI, Depends, HTTPException, status, Request
from fastapi.responses import StreamingResponse
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session
from sqlalchemy.sql import func
from typing import List
from typing import List, Dict
import uuid
import json
import asyncio
from . import models, schemas, auth, database
from .database import engine, get_db
models.Base.metadata.create_all(bind=engine)
class EventNotifier:
def __init__(self):
self.queues = []
def subscribe(self):
q = asyncio.Queue()
self.queues.append(q)
return q
def unsubscribe(self, q):
if q in self.queues:
self.queues.remove(q)
async def broadcast(self, message: str):
for q in self.queues:
await q.put(message)
manager = EventNotifier()
app = FastAPI(title="Dashy API")
app.add_middleware(
@@ -20,6 +42,26 @@ app.add_middleware(
allow_headers=["*"],
)
@app.get("/stream")
async def message_stream(request: Request):
async def event_generator():
q = manager.subscribe()
try:
while True:
if await request.is_disconnected():
break
# use a timeout so we can periodically check for disconnects
try:
msg = await asyncio.wait_for(q.get(), timeout=2.0)
yield f"data: {msg}\n\n"
except asyncio.TimeoutError:
# just a keep-alive ping
yield ": keepalive\n\n"
finally:
manager.unsubscribe(q)
return StreamingResponse(event_generator(), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "Connection": "keep-alive"})
@app.post("/token", response_model=schemas.Token)
async def login_for_access_token(form_data: schemas.UserLogin, db: Session = Depends(get_db)):
# Search by ID or Name
@@ -42,7 +84,7 @@ def read_users(db: Session = Depends(get_db)):
return db.query(models.User).all()
@app.post("/users", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
async def create_user(user: schemas.UserCreate, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
db_user = models.User(
id=user.id,
name=user.name,
@@ -57,10 +99,11 @@ def create_user(user: schemas.UserCreate, db: Session = Depends(get_db), current
db.add(db_user)
db.commit()
db.refresh(db_user)
await manager.broadcast(json.dumps({"type": "refresh"}))
return db_user
@app.patch("/users/{user_id}", response_model=schemas.User)
def update_user(user_id: str, user_update: schemas.UserUpdate, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
async def update_user(user_id: str, user_update: schemas.UserUpdate, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
if current_user.account_type != "admin" and current_user.id != user_id:
raise HTTPException(status_code=403, detail="Not enough permissions")
@@ -74,10 +117,11 @@ def update_user(user_id: str, user_update: schemas.UserUpdate, db: Session = Dep
db.commit()
db.refresh(db_user)
await manager.broadcast(json.dumps({"type": "refresh"}))
return db_user
@app.post("/users/{user_id}/password")
def change_password(user_id: str, pwd_data: schemas.PasswordChange, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
async def change_password(user_id: str, pwd_data: schemas.PasswordChange, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
if current_user.id != user_id:
raise HTTPException(status_code=403, detail="Cannot change another user's password")
@@ -86,10 +130,11 @@ def change_password(user_id: str, pwd_data: schemas.PasswordChange, db: Session
current_user.password_hash = auth.get_password_hash(pwd_data.new_password)
db.commit()
await manager.broadcast(json.dumps({"type": "refresh"}))
return {"message": "Password updated successfully"}
@app.delete("/users/{user_id}")
def delete_user(user_id: str, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
async def delete_user(user_id: str, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
if current_user.account_type != "admin":
raise HTTPException(status_code=403, detail="Not enough permissions")
@@ -112,6 +157,7 @@ def delete_user(user_id: str, db: Session = Depends(get_db), current_user: model
db.delete(db_user)
db.commit()
await manager.broadcast(json.dumps({"type": "refresh"}))
return {"message": "User deleted"}
@app.get("/tasks", response_model=List[schemas.Task])
@@ -125,7 +171,7 @@ def read_deleted_tasks(db: Session = Depends(get_db), current_user: models.User
return db.query(models.Task).filter(models.Task.deleted_at != None).order_by(models.Task.position.asc()).all()
@app.post("/tasks", response_model=schemas.Task)
def create_task(task: schemas.TaskCreate, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
async def create_task(task: schemas.TaskCreate, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
task_id = task.id or f"t_{uuid.uuid4().hex[:8]}"
# Calculate position (max in column + 1000)
@@ -155,10 +201,11 @@ def create_task(task: schemas.TaskCreate, db: Session = Depends(get_db), current
db.commit()
db.refresh(db_task)
await manager.broadcast(json.dumps({"type": "refresh"}))
return db_task
@app.patch("/tasks/{task_id}", response_model=schemas.Task)
def update_task(task_id: str, task_update: schemas.TaskUpdate, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
async def update_task(task_id: str, task_update: schemas.TaskUpdate, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
db_task = db.query(models.Task).filter(models.Task.id == task_id).first()
if not db_task:
raise HTTPException(status_code=404, detail="Task not found")
@@ -169,16 +216,18 @@ def update_task(task_id: str, task_update: schemas.TaskUpdate, db: Session = Dep
db.commit()
db.refresh(db_task)
await manager.broadcast(json.dumps({"type": "refresh"}))
return db_task
@app.delete("/tasks/{task_id}")
def delete_task(task_id: str, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
async def delete_task(task_id: str, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
db_task = db.query(models.Task).filter(models.Task.id == task_id).first()
if not db_task:
raise HTTPException(status_code=404, detail="Task not found")
db_task.deleted_at = func.now()
db.commit()
await manager.broadcast(json.dumps({"type": "refresh"}))
return {"message": "Task moved to trash"}
@app.get("/tasks/{task_id}/notes", response_model=List[schemas.TaskNote])
@@ -186,7 +235,7 @@ def read_task_notes(task_id: str, db: Session = Depends(get_db), current_user: m
return db.query(models.TaskNote).filter(models.TaskNote.task_id == task_id).order_by(models.TaskNote.created_at.desc()).all()
@app.post("/tasks/{task_id}/notes", response_model=schemas.TaskNote)
def create_task_note(task_id: str, note: schemas.TaskNoteBase, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
async def create_task_note(task_id: str, note: schemas.TaskNoteBase, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
db_note = models.TaskNote(
task_id=task_id,
author_id=current_user.id,
@@ -195,10 +244,11 @@ def create_task_note(task_id: str, note: schemas.TaskNoteBase, db: Session = Dep
db.add(db_note)
db.commit()
db.refresh(db_note)
await manager.broadcast(json.dumps({"type": "refresh"}))
return db_note
@app.post("/tasks/{task_id}/restore", response_model=schemas.Task)
def restore_task(task_id: str, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
async def restore_task(task_id: str, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
if current_user.account_type != "admin":
raise HTTPException(status_code=403, detail="Not enough permissions")
@@ -209,20 +259,22 @@ def restore_task(task_id: str, db: Session = Depends(get_db), current_user: mode
db_task.deleted_at = None
db.commit()
db.refresh(db_task)
await manager.broadcast(json.dumps({"type": "refresh"}))
return db_task
@app.get("/workspace", response_model=schemas.Workspace)
def read_workspace(db: Session = Depends(get_db)):
async def read_workspace(db: Session = Depends(get_db)):
ws = db.query(models.Workspace).first()
if not ws:
ws = models.Workspace(id="default", name="murchison-auto", timezone="Pacific/Auckland")
db.add(ws)
db.commit()
db.refresh(ws)
await manager.broadcast(json.dumps({"type": "refresh"}))
return ws
@app.patch("/workspace", response_model=schemas.Workspace)
def update_workspace(ws_update: schemas.WorkspaceUpdate, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
async def update_workspace(ws_update: schemas.WorkspaceUpdate, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
if current_user.account_type != "admin":
raise HTTPException(status_code=403, detail="Not enough permissions")
@@ -237,6 +289,7 @@ def update_workspace(ws_update: schemas.WorkspaceUpdate, db: Session = Depends(g
db.commit()
db.refresh(ws)
await manager.broadcast(json.dumps({"type": "refresh"}))
return ws
@app.get("/audit", response_model=List[schemas.AuditLog])
@@ -244,7 +297,7 @@ def read_audit(db: Session = Depends(get_db), current_user: models.User = Depend
return db.query(models.AuditLog).order_by(models.AuditLog.at.desc()).all()
@app.post("/audit", response_model=schemas.AuditLog)
def create_audit(audit: schemas.AuditLogBase, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
async def create_audit(audit: schemas.AuditLogBase, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
audit_id = f"a_{uuid.uuid4().hex[:8]}"
db_audit = models.AuditLog(
id=audit_id,
@@ -256,4 +309,5 @@ def create_audit(audit: schemas.AuditLogBase, db: Session = Depends(get_db), cur
db.add(db_audit)
db.commit()
db.refresh(db_audit)
await manager.broadcast(json.dumps({"type": "refresh"}))
return db_audit