Files
MeowSpool/PRD.md
T
Felipe 34cbd4a861 feat: implement Google OAuth support for Android and iOS
- Added support for Google OAuth with separate client IDs for Android and iOS.
- Updated `verify_google_id_token` to validate `aud` against both client IDs and check `email_verified`.
- Modified `google_oauth_handler` to accept and process the new client IDs.
- Enhanced security by enforcing explicit JWT algorithm validation.
- Updated mobile app to handle Google OAuth flow using `expo-auth-session`.
- Fixed API request to send `id_token` in snake_case as expected by the backend.
- Added necessary environment variables for Google client IDs in mobile app.
- Implemented intent filter for Google OAuth redirect in AndroidManifest.xml.
2026-03-19 15:24:08 -03:00

574 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MeowSpool — Documento de Referência do Projeto
**Canvas de Design:** https://app.paper.design/file/01KKM0HA6NBV44VCQDFQHC2MRY
---
## 1. Visão Geral
MeowSpool é uma solução para entusiastas e profissionais de impressão 3D gerenciarem seu estoque de filamentos. O diferencial está na precisão do cálculo de peso líquido (descontando o peso do carretel via presets), na identidade visual inspirada em gatos siameses, e na integração com etiquetas físicas exportáveis (SVG/PDF/QR Code/NFC).
**Foco do desenvolvimento atual:** App Mobile (React Native + Expo) + Backend em Rust. A interface Web é planejada para uma fase posterior, após a estabilização do mobile e da API.
---
## 2. Objetivos
- **Precisão:** Eliminar o "chute" de quanto filamento resta no rolo.
- **Agilidade:** Facilitar o cadastro e a consulta via dispositivos móveis, mesmo offline.
- **Organização:** Centralizar parâmetros técnicos (temperatura/fluxo) para consulta rápida.
---
## 3. Público-Alvo
- Hobbistas de impressão 3D.
- Donos de "Print Farms" (fazendas de impressão).
- Projetistas que trabalham com diversos materiais (PLA, ABS, PETG, etc).
---
## 4. Identidade Visual
### 4.1 Nome e Conceito
**MeowSpool** — fusão de "Meow" (gato) e "Spool" (carretel). A identidade visual é inspirada no gato siamês: tons quentes e escuros com detalhes em azul-aço que remetem aos olhos característicos da raça.
App é **dark mode exclusivo** — nenhum suporte a light mode.
### 4.2 Paleta de Cores
| Token | Hex | Função na Interface |
|-----------------|-------------|-------------------------------------------------|
| `bgBase` | `#1E1B18` | Fundo geral |
| `bgSurface` | `#2A2622` | Cards, contêineres, widgets |
| `bgHover` | `#332F2B` | Estados de hover em cards e itens de lista |
| `textPrimary` | `#F5EEDC` | Títulos, peso líquido (texto importante) |
| `textSecondary` | `#C9C1B0` | Descrições, labels (material, marca) |
| `accent` | `#38BCC2` | Botões de ação, elementos ativos |
| `accentMuted` | `#38BCC226` | Backgrounds de badges e status (10% opacidade) |
| `stockLow` | `#FF6B6B` | ≤ 15% — vermelho |
| `stockMedium` | `#FF9F43` | ≤ 35% — laranja |
| `stockOk` | `#38BCC2` | > 35% — accent |
| `border` | `#3D3830` | Bordas sutis |
| `error` | `#FF6B6B` | Mensagens de erro |
### 4.3 Tipografia
- UI geral: **Inter**
- Slugs, hex, URLs, código: **JetBrains Mono**
---
## 5. Requisitos Funcionais (MVP)
### 5.1 Autenticação
- **Métodos suportados:** Email/senha e OAuth via Google.
- **Verificação de e-mail obrigatória** antes do primeiro login.
- **Sessão:** Token JWT com refresh token. O app mobile persiste a sessão localmente para acesso offline.
- **Fluxos:** Login, Cadastro, Verificar E-mail, Reenviar Verificação, Esqueci a Senha, Redefinir Senha.
### 5.2 Gestão de Inventário
- **Cadastro de Filamento:** Material, Marca, Modelo, Cor (hex), Temperatura Hotend, Temperatura Mesa, Fator de Fluxo/Extrusão, Notas.
- **Calculadora de Peso Líquido:** `Peso_Líquido = Peso_Total - Peso_Carretel` (via preset selecionado).
- **Filtros:** Material, marca, busca textual, nível de estoque (`low`/`medium`/`ok`).
- **Ordenação:** Peso líquido asc/desc, criado em desc (padrão).
### 5.3 Presets de Carretéis
**Presets do Sistema (built-in):**
| Nome do Preset | Peso do Carretel |
|------------------------|-----------------|
| Bambu Lab (Plástico) | 250g |
| Elegoo (Papelão) | 200g |
| Creality (Plástico) | 230g |
| Prusament (Plástico) | 201g |
| Sunlu (Papelão) | 200g |
| Polymaker (Plástico) | 220g |
| Genérico Papelão 1kg | 200g |
| Genérico Plástico 1kg | 250g |
**Presets Customizados:** O usuário pode criar, editar e excluir seus próprios presets. Presets do sistema são somente leitura.
### 5.4 Identificação e Etiquetas
- **QR Code:** Cada filamento gera QR Code com deep link `meowspool://filament/<id>`.
- **Etiqueta SVG:** `GET /filaments/:id/label.svg` — parâmetros `width_mm`, `height_mm`, `fields`, `dpi`.
- **Etiqueta PDF:** `GET /filaments/:id/label.pdf` — mesmos parâmetros.
- **Impressoras suportadas:** Padrão (50×30mm), Niimbot D11/D110 (22×14mm, dpi=203), Brother/Dymo (dpi=300).
- **Campos selecionáveis:** `color`, `name`, `material_brand`, `net_weight`, `print_temp`, `qrcode`.
- **NFC:** Leitura e gravação de tags NTAG215 com URI `meowspool://filament/<id>`.
### 5.5 Dashboard
- Total de estoque em kg.
- Contagem de filamentos com estoque baixo.
- Resumo por material (contagem + peso total).
- Filamentos com estoque baixo (≤15%).
- Filamentos adicionados recentemente.
---
## 6. Arquitetura do Sistema
Ambos os lados seguem **Arquitetura Hexagonal (Ports & Adapters)**:
```
adapters/inbound (HTTP / Tela)
|
v
application (Use Cases / Services)
|
v
domain (Entidades puras)
|
^
ports (Interfaces/traits)
|
^
adapters/outbound (PostgreSQL / SQLite / NFC / HTTP)
```
**Regra fundamental:** O `domain` e os `ports` nunca importam de `adapters` ou `application`.
---
## 7. Backend (Rust)
### 7.1 Stack
| Componente | Biblioteca |
|------------------|----------------------------------------|
| HTTP Framework | `axum` 0.7 |
| Async Runtime | `tokio` (full) |
| ORM / Query | `sqlx` (postgres, uuid, time, macros) |
| Autenticação JWT | `jsonwebtoken` 9.x |
| Hash de senha | `argon2` 0.5 (Argon2id) |
| OAuth Google | `oauth2` 4.x |
| Serialização | `serde`, `serde_json` |
| Erros | `thiserror` |
| Env vars | `dotenvy` |
| Logging | `tracing`, `tracing-subscriber` |
| Validação | `validator` |
| HTTP Client | `reqwest` |
| Email (SMTP) | `lettre` (STARTTLS) |
| Geração QR Code | `qrcode` + `image` |
| Geração PDF | `printpdf` 0.7 |
### 7.2 Estrutura de Pastas
```
backend/
├── Cargo.toml
├── migrations/
│ ├── 20240101000001_create_users.sql
│ ├── 20240101000002_create_spool_presets.sql
│ ├── 20240101000003_create_filaments.sql
│ └── 20240101000004_create_token_tables.sql ← password_reset_tokens + email_verification_tokens
└── src/
├── main.rs ← entry point: config, DB, router, servidor
├── config.rs ← struct Config lida de variáveis de ambiente
├── error.rs ← AppError unificado com IntoResponse
├── router.rs ← composição de todas as rotas Axum
├── domain/ ← entidades puras (User, Filament, SpoolPreset)
├── ports/ ← traits: UserRepository, FilamentRepository, SpoolPresetRepository
├── application/
│ ├── auth_service.rs ← login, register, OAuth, refresh, logout
│ ├── email_token_service.rs← forgot_password, verify_email, reset_password
│ ├── filament_service.rs ← CRUD, peso líquido, QR Code, SVG, PDF
│ └── spool_preset_service.rs
├── infrastructure/
│ └── email_service.rs ← SMTP via lettre
└── adapters/
├── inbound/ ← handlers Axum + middleware JWT
└── outbound/ ← implementações PostgreSQL dos ports
```
### 7.3 Rotas da API
Base: `/api/v1`
**Auth — `/api/v1/auth`**
| Método | Rota | Acesso |
|--------|-------------------------|--------------------------------|
| POST | `/register` | Público — retorna 201 sem token|
| POST | `/login` | Público |
| POST | `/oauth/google` | Público |
| POST | `/refresh` | Público (requer refresh token) |
| POST | `/logout` | Autenticado |
| POST | `/resend-verification` | Público — sempre retorna 200 |
| POST | `/forgot-password` | Público |
| POST | `/verify-email` | Público |
| POST | `/reset-password` | Público (requer token) |
**Redirects para Deep Links**
| Método | Rota | Descrição |
|--------|-------------------|------------------------------------------------------------|
| GET | `/verify-email` | Redireciona 302 → `{APP_SCHEME}://verify-email?token=xxx` |
| GET | `/reset-password` | Redireciona 302 → `{APP_SCHEME}://reset-password?token=xxx`|
**Filaments — `/api/v1/filaments`**
| Método | Rota | Acesso |
|--------|------------------|-------------|
| GET | `/` | Autenticado |
| POST | `/` | Autenticado |
| GET | `/:id` | Autenticado |
| PUT | `/:id` | Autenticado |
| DELETE | `/:id` | Autenticado |
| GET | `/:id/qrcode` | Autenticado |
| GET | `/:id/label.svg` | Autenticado |
| GET | `/:id/label.pdf` | Autenticado |
**Spool Presets — `/api/v1/spool-presets`**
| Método | Rota | Acesso |
|--------|--------|--------------------------------------|
| GET | `/` | Autenticado |
| POST | `/` | Autenticado |
| PUT | `/:id` | Autenticado (apenas presets do user) |
| DELETE | `/:id` | Autenticado (apenas presets do user) |
**Outros**
| Método | Rota | Acesso |
|--------|--------------|-------------|
| GET | `/dashboard` | Autenticado |
| GET | `/users/me` | Autenticado |
| PUT | `/users/me` | Autenticado |
### 7.4 Padrão de Erros
```json
{ "error": "not found", "code": "NOT_FOUND" }
```
| Variante AppError | HTTP | Code |
|------------------------|------|------------------------|
| `NotFound` | 404 | `NOT_FOUND` |
| `Unauthorized` | 401 | `UNAUTHORIZED` |
| `Forbidden` | 403 | `FORBIDDEN` |
| `EmailNotVerified` | 403 | `EMAIL_NOT_VERIFIED` |
| `Conflict` | 409 | `CONFLICT` |
| `BadRequest` | 400 | `BAD_REQUEST` |
| `Validation` | 422 | `VALIDATION_ERROR` |
| `Internal` | 500 | `INTERNAL_ERROR` |
### 7.5 Variáveis de Ambiente
| Variável | Obrigatória | Padrão |
|---------------------------|-------------|-------------------------|
| `DATABASE_URL` | ✅ | — |
| `JWT_SECRET` | ✅ | — |
| `JWT_EXPIRY_SECS` | ❌ | `3600` |
| `JWT_REFRESH_EXPIRY_SECS` | ❌ | `2592000` |
| `GOOGLE_CLIENT_ID` | ❌ | — |
| `GOOGLE_CLIENT_SECRET` | ❌ | — |
| `HOST` / `PORT` | ❌ | `0.0.0.0` / `8080` |
| `SMTP_HOST` / `SMTP_PORT` | ❌ | `smtp.gmail.com` / `587`|
| `SMTP_USER` / `SMTP_PASS` | ❌ | — |
| `EMAIL_FROM` | ❌ | `noreply@meowspool.app` |
| `APP_BASE_URL` | ❌ | `http://localhost:8080` |
| `APP_SCHEME` | ❌ | `meowspool` |
> Em produção: `APP_BASE_URL=https://meowspool.felipecncloud.com/api/v1`
### 7.6 Como Rodar Localmente
```bash
cd backend/
cp .env.example .env
# PostgreSQL via Docker
docker run -d --name meowspool-db \
-e POSTGRES_USER=meowspool \
-e POSTGRES_PASSWORD=meowspool \
-e POSTGRES_DB=meowspool \
-p 5432:5432 postgres:16-alpine
sqlx migrate run
cargo watch -x run
```
---
## 8. Mobile (React Native + Expo)
### 8.1 Stack
| Componente | Biblioteca |
|-------------------|-------------------------------------|
| Framework | `expo` ~54 (Bare Workflow) |
| Navigation | `expo-router` ~4 (file-system) |
| Linguagem | TypeScript 5.x (strict) |
| Node Version | 22 LTS (via `mise`) |
| Java Version | 17 (obrigatório — Java 24 quebra CMake) |
| Forms | `react-hook-form` + `zod` |
| Estado global | `zustand` |
| HTTP | `axios` (interceptor Bearer + refresh) |
| DB local | `expo-sqlite` + SQLCipher |
| Auth persistência | `expo-secure-store` |
| UI | React Native + Ionicons |
| QR Code render | `react-native-qrcode-svg` |
| SVG inline | `react-native-svg` (SvgXml) |
| Câmera / Scanner | `expo-camera` ~17 (ML Kit) |
| NFC | `react-native-nfc-manager` |
| File system | `expo-file-system/legacy` |
| Compartilhamento | `expo-sharing` (iOS) |
| Abrir arquivo | `expo-intent-launcher` (Android) |
| Animações | `react-native-reanimated` |
> **Bare Workflow:** o diretório `android/` é comitado. Qualquer pacote com módulo nativo exige `npx expo prebuild --platform android` + rebuild do APK.
### 8.2 Estrutura de Pastas
```
mobile/
├── .mise.toml ← node 22 LTS, java 17
├── app.json ← Expo config, scheme "meowspool"
├── app/ ← expo-router (file-system routes)
│ ├── _layout.tsx ← Root layout (auth guard + sessão)
│ ├── (auth)/
│ │ ├── login.tsx ← G3-0
│ │ ├── register.tsx ← IX-0
│ │ ├── forgot-password.tsx ← L2-0
│ │ ├── check-email.tsx ← RU-0 (pós-cadastro / e-mail não verificado)
│ │ ├── reset-password.tsx ← 117-0
│ │ └── password-reset-done.tsx ← 135-0
│ ├── filament/[id].tsx ← Deep link: meowspool://filament/<id> → /(app)/inventory/<id>
│ ├── verify-email.tsx ← Deep link: meowspool://verify-email?token=xxx
│ └── (app)/
│ ├── (tabs)/
│ │ ├── home.tsx ← 1-0 Dashboard
│ │ ├── inventory.tsx ← CW-0 Inventário
│ │ ├── add.tsx ← FAB → /inventory/new
│ │ ├── config.tsx ← M4-0 Presets de Carretéis
│ │ └── profile.tsx ← 1LQ-0 Perfil
│ ├── inventory/
│ │ ├── new.tsx ← 2X-0 Cadastro
│ │ ├── filters.tsx ← 17H-0 Filtros (bottom sheet)
│ │ ├── [id].tsx ← 6L-0 Detalhe
│ │ └── [id]/edit.tsx ← 13O-0 Editar
│ ├── config/presets/
│ │ ├── new.tsx ← QM-0 Novo Preset
│ │ └── [id]/edit.tsx ← 1KD-0 Editar Preset
│ ├── scanner.tsx ← Scanner QR Code (ML Kit)
│ ├── nfc-reader.tsx ← 1RY-0 Leitura NFC (bottom sheet modal)
│ └── filaments/[id]/
│ ├── qrcode.tsx ← Ver QR Code
│ ├── label.tsx ← Exportar Etiqueta (PDF/SVG)
│ └── write-nfc.tsx ← Gravar tag NTAG215
└── src/
├── domain/ ← Entidades puras (Filament, SpoolPreset, User, Dashboard)
├── ports/ ← Interfaces (IFilamentRepository, ISpoolPresetRepository, IAuthRepository, INFCRepository)
├── application/ ← Use Cases (filament/, preset/, auth/, nfc/)
├── adapters/
│ ├── remote/ ← Axios → API Rust (ApiFilamentRepository, etc.)
│ ├── local/ ← SQLite (LocalFilamentRepository, LocalSpoolPresetRepository)
│ └── nfc/ ← react-native-nfc-manager (NFCManagerRepository)
├── store/ ← Zustand: authStore, filamentStore, presetStore
├── shared/ ← theme.ts, constants.ts, utils/
└── presentation/components/
├── ui/ ← Button, Input, Card, Badge
├── layout/ ← Screen, Header
└── filament/ ← StockBar, ColorSwatch, FilamentCard
```
### 8.3 Navegação e Deep Links
- Scheme: `meowspool://` (configurado em `app.json`)
- Grupos: `(auth)` — sem sessão | `(app)` — autenticado | `(app)/(tabs)` — bottom tabs
| Deep Link | Handler | Ação |
|----------------------------------------|--------------------------------|---------------------------------------------|
| `meowspool://filament/<id>` | `app/filament/[id].tsx` | Redireciona para `/(app)/inventory/<id>` |
| `meowspool://verify-email?token=<tok>` | `app/verify-email.tsx` | Verifica e-mail via POST /auth/verify-email |
| `meowspool://reset-password?token=<tok>`| `app/(auth)/reset-password.tsx`| Exibe formulário de nova senha |
> **Links de e-mail:** o backend gera `https://meowspool.felipecncloud.com/api/v1/verify-email?token=xxx` (aceito por clientes de e-mail). O backend redireciona 302 → `meowspool://...`. O OS abre o app.
### 8.4 Estratégia Offline-First
1. **Escrita:** toda mutação persiste primeiro no SQLite (`synced = 0`) e enfileira em `sync_queue`.
2. **Leitura:** sempre do SQLite; a API é usada apenas para sync.
3. **Sync:** background job consome `sync_queue` e envia para o backend.
4. **Conflito:** last-write-wins via `updatedAt`. O backend é a fonte de verdade.
5. **Auth offline:** token no `expo-secure-store`; refresh automático via interceptor Axios.
**Schema SQLite:**
```sql
-- filaments
id TEXT PRIMARY KEY, user_id TEXT, material TEXT, brand TEXT, model TEXT,
color_hex TEXT, spool_preset_id TEXT, total_weight_g REAL, net_weight_g REAL,
temp_hotend_c REAL, temp_bed_c REAL, flow_factor_pct REAL, notes TEXT,
synced INTEGER DEFAULT 0, updated_at TEXT, created_at TEXT
-- spool_presets
id TEXT PRIMARY KEY, user_id TEXT, name TEXT, spool_weight_g REAL,
is_system INTEGER DEFAULT 0, synced INTEGER DEFAULT 0, created_at TEXT
-- sync_queue
id INTEGER PRIMARY KEY AUTOINCREMENT, entity_type TEXT, entity_id TEXT,
operation TEXT, payload TEXT, created_at TEXT
```
### 8.5 Como Rodar Localmente
```bash
cd mobile/
mise install # garante Node 22 e Java 17
npm install
# Criar .env
echo "EXPO_PUBLIC_API_URL=http://localhost:3000/api/v1" > .env
# Dev (Expo Go — sem câmera ML Kit / NFC)
npx expo start
# Build nativo Android (necessário para câmera ML Kit e NFC)
mise exec -- npx expo run:android
```
> **Atenção ao build Android:** o projeto usa `namespace "com.meowspool"` no `build.gradle`, mas o código fonte fica em `com.meowspool.app`. Use sempre nomes totalmente qualificados no `AndroidManifest.xml`.
---
## 9. Estrutura de Dados (Entidades Principais)
### `users`
| Campo | Tipo | Descrição |
|------------------|-----------|-------------------------------------|
| id | UUID | Identificador único |
| email | string | Email do usuário |
| password_hash | string? | Hash Argon2id (nulo se OAuth) |
| google_id | string? | ID OAuth Google (opcional) |
| email_verified | boolean | Obrigatório para login |
| created_at | timestamp | |
### `spool_presets`
| Campo | Tipo | Descrição |
|----------------|-----------|-------------------------------------------------|
| id | UUID | Identificador único |
| name | string | Nome do preset |
| spool_weight_g | integer | Peso do carretel vazio em gramas |
| is_system | boolean | `true` = built-in (read-only para usuários) |
| user_id | UUID? | Nulo para presets do sistema |
| created_at | timestamp | |
### `filaments`
| Campo | Tipo | Descrição |
|-----------------|-----------|-----------------------------------------|
| id | UUID | Identificador único |
| user_id | UUID | Dono do filamento |
| material | string | PLA, ABS, PETG, TPU, ASA, PA, PC… |
| brand | string | Marca |
| model | string? | Modelo/linha |
| color_hex | string | Cor em hex (`#FF5733`) |
| spool_preset_id | UUID | Preset de carretel usado |
| total_weight_g | real | Peso total medido na balança (gramas) |
| net_weight_g | real | Calculado: total peso do carretel |
| temp_hotend_c | real? | Temperatura do hotend (°C) |
| temp_bed_c | real? | Temperatura da mesa (°C) |
| flow_factor_pct | real? | Fator de fluxo (%) |
| notes | text? | Observações livres |
| updated_at | timestamp | Usado para resolução de conflitos sync |
| created_at | timestamp | |
### `password_reset_tokens` / `email_verification_tokens`
| Campo | Tipo | Descrição |
|------------|-------------|--------------------------------------|
| id | UUID | PK |
| token | TEXT UNIQUE | Token único (indexado) |
| user_id | UUID | FK para users |
| expires_at | TIMESTAMPTZ | TTL: 1h (reset) / 24h (verificação) |
| used_at | TIMESTAMPTZ | NULL = não usado; preenchido ao usar |
---
## 10. Fluxo do Usuário
### Cadastro e Login
1. Usuário se cadastra → backend envia e-mail de verificação (background).
2. Usuário clica no link `https://...` → backend redireciona 302 → deep link → app verifica.
3. Login com e-mail não verificado → `403 EMAIL_NOT_VERIFIED` → app sugere reenvio.
4. Login com e-mail verificado → tokens JWT emitidos → sessão persistida no SecureStore.
### Cadastro de Filamento
1. Usuário abre o App (autenticado) → "Novo Filamento".
2. Preenche marca, material, modelo, seleciona cor hex.
3. Seleciona preset de carretel ou cria um novo.
4. Digita o peso total (ex: 850g) → app calcula: **600g disponível**.
5. Salva localmente (offline-first) e sincroniza quando online.
6. QR Code e etiqueta ficam disponíveis imediatamente.
### Identificação via QR Code / NFC
1. Usuário aponta câmera para QR Code **ou** aproxima tag NFC do carretel.
2. App lê `meowspool://filament/<id>` → navega direto para o detalhe do filamento.
---
## 11. Artboards de Design
| ID | Tela | Seção |
|---------|-----------------------------|-------|
| `G3-0` | Login | 5.1 |
| `IX-0` | Cadastro | 5.1 |
| `L2-0` | Recuperação de Senha | 5.1 |
| `RU-0` | Verificação de Email | 5.1 |
| `117-0` | Redefinir Senha | 5.1 |
| `135-0` | Senha Redefinida | 5.1 |
| `1-0` | Home / Dashboard | 5.5 |
| `CW-0` | Inventário | 5.2 |
| `17H-0` | Filtros (bottom sheet) | 5.2 |
| `2X-0` | Cadastro de Filamento | 5.2 |
| `13O-0` | Editar Filamento | 5.2 |
| `6L-0` | Detalhe do Filamento | 5.2 |
| `M4-0` | Presets de Carretéis | 5.3 |
| `QM-0` | Novo Preset | 5.3 |
| `1KD-0` | Editar Preset | 5.3 |
| `1CF-0` | Ver QR Code | 5.4 |
| `1FZ-0` | Exportar Etiqueta | 5.4 |
| `1RY-0` | Leitura NFC (modal) | 5.4 |
| `1LQ-0` | Perfil / Conta | 5.1 |
---
## 12. Roadmap
### Pendente — Mobile
- [ ] Sync background com `sync_queue` SQLite → API
- [ ] Google OAuth com `expo-auth-session`
- [ ] NFC suporte iOS (entitlement `com.apple.developer.nfc.readwrite`)
- [ ] Expo Notifications para alertas de estoque baixo
- [ ] Testes de integração com Jest + Testing Library
### Pendente — Infraestrutura
- [ ] DI container formal (instanciar e injetar repositórios concretos)
### Fase 2 — Web (planejado)
- [ ] Front-end Web com Next.js (dashboard + inventário)
- [ ] Cache com Redis / HTTP Cache
- [ ] Histórico de uso por projeto/impressão
- [ ] Custo por grama
- [ ] Importação em lote (CSV/JSON)
- [ ] Pool público de presets compartilhados pela comunidade
### Concluído
- [x] CRUD de filamentos (mobile + API)
- [x] CRUD de presets (mobile + API)
- [x] Dashboard (mobile + API)
- [x] QR Code geração e scanner (ML Kit)
- [x] Exportação de etiqueta SVG e PDF
- [x] Suporte Niimbot D11/D110 (22×14mm) com layout adaptativo
- [x] NFC leitura e gravação NTAG215 (Android)
- [x] Deep links (filamento, verificação de e-mail, reset de senha)
- [x] Verificação de e-mail obrigatória + reenvio
- [x] Fluxo completo de reset de senha
- [x] Redirect HTTP → deep link (compatibilidade com clientes de e-mail)