feat: presença por sessão, eventos SSE em tempo real e autosave

- presence.go: hub SSE + registro de presença (TTL 30s por heartbeat)
- GET /api/events (EventSource, auth cookie/Bearer) transmite saved/presence;
  POST /api/presence publica {session, notes[]}; create/update/restore/delete
  publicam savedEvent com a session de origem (eco ignorado pelo autor)
- PUT/POST/restore aceitam campo opcional session
- UI: id de sessão em sessionStorage; selo 👥 na aba, na lista de notas e no
  topo do editor quando outra sessão tem a mesma nota aberta; aba LIMPA
  recarrega sozinha ao receber saved; aba SUJA abre o diálogo de conflito
  automaticamente (conflictPending evita diálogos empilhados)
- Autosave: debounce de 1,5s em qualquer alteração (rascunho vira nota),
  mensagem de revisão 'autosave', status 'salvando…', toggle nas Preferências
- Testes: SSE hello/presence/saved + presença 400/401 via httptest
This commit is contained in:
lgcosta committed 2026-11-04 13:35:33 -03:00
1 parent 3b91c1a509
commit a55591d208
9 files changed
+543 -15

No files matched your search

+6
View File
@@ -29,6 +29,8 @@ type Server struct {
token string
webFS fs.FS
md goldmark.Markdown
hub *hub
pres *presenceReg
}
// New cria o servidor da API. webFS é o filesystem embutido do frontend
@@ -42,6 +44,8 @@ func New(st *store.Store, token string, webFS fs.FS) *Server {
goldmark.WithExtensions(extension.GFM),
goldmark.WithRendererOptions(htmlr.WithHardWraps()),
),
hub: newHub(),
pres: newPresenceReg(),
}
}
@@ -70,6 +74,8 @@ func (s *Server) Handler() http.Handler {
mux.HandleFunc("PUT /api/categories/{id}", s.auth(s.handleUpdateCategory))
mux.HandleFunc("DELETE /api/categories/{id}", s.auth(s.handleDeleteCategory))
mux.HandleFunc("POST /api/render", s.auth(s.handleRender))
mux.HandleFunc("GET /api/events", s.auth(s.handleEvents))
mux.HandleFunc("POST /api/presence", s.auth(s.handlePresence))
mux.HandleFunc("GET /{$}", s.serveIndex)
mux.Handle("GET /", http.FileServerFS(s.webFS))