# 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/`. - **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/`. ### 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/ → /(app)/inventory/ │ ├── 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/` | `app/filament/[id].tsx` | Redireciona para `/(app)/inventory/` | | `meowspool://verify-email?token=` | `app/verify-email.tsx` | Verifica e-mail via POST /auth/verify-email | | `meowspool://reset-password?token=`| `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/` → 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)