diff --git a/README.md b/README.md index bae53c0..52bc3aa 100644 --- a/README.md +++ b/README.md @@ -223,6 +223,59 @@ curl -X POST -H "Authorization: Bearer $T" -H 'Content-Type: application/json' \ # (clientes que ignoram o evento podem sobrescrever a própria edição sem querer) ``` +## PWA — instalável e offline + +O app é um **PWA completo**: instalável no Linux/Windows/macOS/Android (menu **Ajuda → +Instalar Aplicativo…**, ou o ícone da barra de endereço), com janela `standalone` sem chrome +do navegador e atalho de área de trabalho "Nova nota" (`/?new=1`). + +- **`web/manifest.webmanifest`** — `name/short_name/description/start_url/scope/theme_color/ + background_color`, `display: standalone` (+ `display_override` com window-controls-overlay), + ícones **192/512 em `any`, `maskable` e `monochrome`** (PNGs reais em `web/icons/`) e atalho. +- **`web/sw.js`** — Service Worker com cache versionado (`notas-shell-v1`): + - `install` pré-cacheia o shell e `skipWaiting`; `activate` expurga caches antigos, + `clients.claim()` e avisa as janelas (`notas:updated` → toast "App atualizado"); + - navegação: **network-first com fallback ao shell** (abre offline); + - estáticos: **stale-while-revalidate** (carregamento instantâneo); + - **`/api/*` nunca é interceptado** — dados sempre frescos e o SSE de tempo real não quebra. + - **Ao publicar mudanças de assets, bump na constante `CACHE`** do `sw.js`. +- **Meta tags** em `index.html`: `theme-color`, `mobile-web-app-capable`, + `apple-mobile-web-app-*`, `apple-touch-icon` e ``. +- **Offline UX**: shell + interface funcionam sem rede; ações que precisam do servidor mostram + toast claro ("Sem conexão — o app abre offline; salvar precisa do servidor"). + +### Servindo os arquivos (backend Go atual) + +Já implementado em `internal/api/server.go` (`staticHandler`): + +- `sw.js` → `Cache-Control: no-cache, no-store, must-revalidate` + `Content-Type: + text/javascript` (a detecção de update do SW é por bytes — nunca cacheie!); +- `manifest.webmanifest` → `Content-Type: application/manifest+json; charset=utf-8` + (o Go não conhece a extensão `.webmanifest` por padrão); +- tudo vem do `go:embed` (`web/embed.go` inclui `manifest.webmanifest sw.js icons`). +- O Service Worker só registra em origem segura: **HTTPS ou `localhost`/`127.0.0.1`** — em IP + de LAN puro (`http://192.168.x.x`) o navegador bloqueia SW; use um túnel/reverse proxy TLS + para testar instalação real fora de localhost. + +### Equivalente em Rust (axum, caso o backend migre) + +```rust +// MIME do manifest + no-cache no SW +async fn manifest() -> impl IntoResponse { + ([(header::CONTENT_TYPE, "application/manifest+json")], include_str!("../web/manifest.webmanifest")) +} +async fn sw() -> impl IntoResponse { + [ + (header::CONTENT_TYPE, "text/javascript"), + (header::CACHE_CONTROL, "no-cache, no-store, must-revalidate"), + ] +} +``` +No actix-web, o `fs::Files` já define MIME por extensão, mas registre antes: +`mime::BABFILE` não — use `mime_guess::init::detect_EXTENSION("webmanifest", +"application/manifest+json")` e um `wrap_fn` que sobrepõe `Cache-Control` quando o caminho for +`/sw.js`. (No `tower-http` `SetResponseHeaderLayer` com `Match` por path é o caminho limpo.) + ## Como funciona o versionamento - O conteúdo é guardado em `data/blobs//` (content-addressed, como objetos do diff --git a/internal/api/server.go b/internal/api/server.go index 0b6c852..5bcb6ed 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -78,11 +78,30 @@ func (s *Server) Handler() http.Handler { mux.HandleFunc("POST /api/presence", s.auth(s.handlePresence)) mux.HandleFunc("GET /{$}", s.serveIndex) - mux.Handle("GET /", http.FileServerFS(s.webFS)) + mux.Handle("GET /", s.staticHandler()) return s.wrap(mux) } +// staticHandler serve o FS embutido com os cabeçalhos que PWA exige: +// sw.js nunca pode ser cacheado pelo browser (a detecção de nova versão é por +// bytes) e o manifest ganha o MIME application/manifest+json. +func (s *Server) staticHandler() http.Handler { + files := http.FileServerFS(s.webFS) + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/sw.js": + w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate") + w.Header().Set("Content-Type", "text/javascript; charset=utf-8") + files.ServeHTTP(w, r) + return + case "/manifest.webmanifest": + w.Header().Set("Content-Type", "application/manifest+json; charset=utf-8") + } + files.ServeHTTP(w, r) + }) +} + // wrap aplica middlewares globais: recuperação, headers de segurança e log. func (s *Server) wrap(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/web/embed.go b/web/embed.go index f1e72d7..4472076 100644 --- a/web/embed.go +++ b/web/embed.go @@ -3,7 +3,7 @@ package web import "embed" -// FS contém index.html, css/ e js/ na raiz. +// FS contém index.html, css/, js/, icons/, sw.js e manifest.webmanifest na raiz. // -//go:embed index.html css js +//go:embed index.html manifest.webmanifest sw.js css js icons var FS embed.FS diff --git a/web/icons/icon-192.png b/web/icons/icon-192.png new file mode 100644 index 0000000..5aa11d3 Binary files /dev/null and b/web/icons/icon-192.png differ diff --git a/web/icons/icon-512.png b/web/icons/icon-512.png new file mode 100644 index 0000000..ec88508 Binary files /dev/null and b/web/icons/icon-512.png differ diff --git a/web/icons/maskable-192.png b/web/icons/maskable-192.png new file mode 100644 index 0000000..9dd8630 Binary files /dev/null and b/web/icons/maskable-192.png differ diff --git a/web/icons/maskable-512.png b/web/icons/maskable-512.png new file mode 100644 index 0000000..7afade6 Binary files /dev/null and b/web/icons/maskable-512.png differ diff --git a/web/icons/monochrome-512.png b/web/icons/monochrome-512.png new file mode 100644 index 0000000..8ef675d Binary files /dev/null and b/web/icons/monochrome-512.png differ diff --git a/web/index.html b/web/index.html index ff070ff..57ea50a 100644 --- a/web/index.html +++ b/web/index.html @@ -6,6 +6,14 @@ Notas — editor estilo Mousepad + + + + + + + + @@ -171,6 +179,7 @@