feat(pwa): app instalável com service worker e shell offline
- manifest.webmanifest: standalone, theme/background color, ícones 192/512 any+maskable+monochrome (PNGs em web/icons) e atalho 'Nova nota' (?new=1) - sw.js: cache versionado (notas-shell-v1); navegação network-first com fallback ao shell (offline), estáticos stale-while-revalidate, /api/* e SSE sempre passthrough; activate limpa caches e notifica janelas - app.js: registro do SW, beforeinstallprompt com item 'Instalar Aplicativo…' no menu Ajuda (escondido em standalone/após instalar), toast de atualização, aviso offline/online, deep-link ?new=1 - index.html: meta tags theme-color/mobile-web-app/apple-touch-icon/manifest - server: staticHandler com MIME application/manifest+json e Cache-Control no-cache para /sw.js; embed inclui manifest, sw e ícones - README: seção PWA + orientação de serving (Go atual e equivalente Rust)
This commit is contained in:
1 parent
63fd024f68
commit
18d33ed141
12 files changed
+243
-3
No files matched your search
@@ -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 `<link rel="manifest">`.
|
||||
- **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/<aa>/<hash-sha256>` (content-addressed, como objetos do
|
||||
|
||||
Reference in new issue
Block a user