Files
MeowSpool/mobile/agent.md
T
Felipe 9c0b3d584c feat: enhance filament management and UI improvements
- Update SpoolPresetRepository to include user_id in the update query.
- Expand .gitignore to include various environment and temporary files.
- Revise agent documentation for better clarity and formatting.
- Implement pull-to-refresh functionality in the inventory list.
- Integrate API calls for deleting and updating filaments, ensuring state synchronization.
- Add custom color picker for filament color selection with hex validation.
- Update AndroidManifest and Gradle files for improved configuration and permissions.
- Refactor MainActivity and MainApplication for better splash screen handling.
- Update styles and colors for a cohesive UI experience.
- Replace splash screen logos and icons with new assets.
2026-03-14 14:14:06 -03:00

514 lines
22 KiB
Markdown

# MeowSpool Mobile — Agent Guide
## Visão Geral
Aplicação mobile do **MeowSpool** construída com **React Native** + **Expo** (SDK ~51), usando **expo-router** para navegação baseada em sistema de arquivos. A arquitetura espelha o backend Rust: **Clean Code / Hexagonal**, com camadas bem delimitadas do domínio até a apresentação.
O app é **offline-first**: todos os dados são persistidos localmente em **SQLite** (expo-sqlite + SQLCipher) e sincronizados com o backend Rust em background usando estratégia **last-write-wins** via campo `updatedAt`.
---
## Stack
| Componente | Biblioteca | Observação |
| ----------------- | ----------------------------------- | --------------------------------------------------------- |
| Framework | `expo` ~51 | Managed Workflow |
| Navigation | `expo-router` ~3 | File-system routing |
| Linguagem | TypeScript 5.x | strict mode |
| Node Version | 22 LTS | Gerenciado via `mise` (`.mise.toml`) |
| Java Version | 17 | Obrigatório para React Native 0.81 (Java 24 quebra CMake) |
| UI | React Native + `@expo/vector-icons` | Ionicons |
| Forms | `react-hook-form` + `zod` | Validação em runtime |
| Estado global | `zustand` | Stores em `src/store/` |
| HTTP | `axios` | Interceptor Bearer + refresh automático |
| DB local | `expo-sqlite` + SQLCipher | WAL mode, foreign keys |
| Auth persistência | `expo-secure-store` | JWT cifrado no keychain |
| Safe Area | `react-native-safe-area-context` | |
| Gesture Handler | `react-native-gesture-handler` | |
| Animations | `react-native-reanimated` | |
| QR Code render | `react-native-qrcode-svg` | Render de QR Code em tela |
| Câmera / Scanner | `expo-camera` ~17 | ML Kit barcode scan; requer development build |
---
## Arquitetura de Camadas
```
src/
├── domain/ ← Entidades puras (sem dependências externas)
├── ports/ ← Interfaces/contratos (traits equivalentes ao Rust)
├── application/ ← Use Cases (orquestram domínio + ports)
├── adapters/
│ ├── remote/ ← Implementações HTTP (Axios → API Rust)
│ └── local/ ← Implementações SQLite (expo-sqlite)
├── store/ ← Estado em memória (Zustand) — cache das queries
├── shared/ ← Design tokens, constantes, utilitários
└── presentation/
└── components/ ← Componentes UI reutilizáveis
```
### Regra de dependência
```
presentation → store → application → ports ← adapters
domain
```
Nenhuma camada interna importa de camadas externas. Os `adapters` implementam os `ports`.
---
## Estrutura de Arquivos
```
mobile/
├── .mise.toml ← node 22 LTS, java 17
├── app.json ← Expo config, scheme "meowspool"
├── babel.config.js ← module-resolver + reanimated
├── tsconfig.json ← aliases @domain, @ports, @application,
│ @adapters, @presentation, @store, @shared
├── index.js ← expo-router entry point
├── package.json
├── app/ ← expo-router file-system routes
│ ├── _layout.tsx ← Root layout (carrega sessão)
│ ├── (auth)/
│ │ ├── _layout.tsx ← Redireciona se já autenticado
│ │ ├── login.tsx ← G3-0
│ │ ├── register.tsx ← IX-0
│ │ ├── forgot-password.tsx ← L2-0
│ │ ├── verify-email.tsx ← RU-0
│ │ ├── reset-password.tsx ← 117-0
│ │ └── password-reset-done.tsx ← 135-0
│ └── (app)/
│ ├── _layout.tsx ← Stack autenticado
│ ├── (tabs)/
│ │ ├── _layout.tsx ← Bottom tabs: Início|Estoque|[+]|Config|Perfil
│ │ ├── home.tsx ← 1-0 Dashboard
│ │ ├── inventory.tsx ← CW-0 Inventário
│ │ ├── add.tsx ← FAB → redireciona para /inventory/new
│ │ ├── config.tsx ← M4-0 Presets de Carretéis
│ │ └── profile.tsx ← 1LQ-0 Perfil
│ ├── inventory/
│ │ ├── new.tsx ← 2X-0 Cadastro de Filamento
│ │ ├── filters.tsx ← 17H-0 Filtros (bottom sheet)
│ │ ├── [id].tsx ← 6L-0 Detalhe do Filamento
│ │ └── [id]/
│ │ └── edit.tsx ← 13O-0 Editar Filamento
│ ├── config/
│ │ └── presets/
│ │ ├── new.tsx ← QM-0 Novo Preset
│ │ └── [id]/
│ │ └── edit.tsx ← 1KD-0 Editar Preset
│ ├── scanner.tsx ← Scanner de QR Code (expo-camera ML Kit)
│ └── filaments/
│ └── [id]/
│ ├── qrcode.tsx ← Ver QR Code (react-native-qrcode-svg)
│ └── label.tsx ← Exportar Etiqueta SVG
└── src/
├── domain/
│ ├── Filament.ts ← interface Filament, calcNetWeight
│ ├── SpoolPreset.ts ← interface SpoolPreset, isUserOwnedPreset
│ ├── User.ts ← interface User, AuthSession, DTOs
│ └── Dashboard.ts ← DashboardData, MaterialSummary, etc.
├── ports/
│ ├── FilamentRepository.ts ← interface IFilamentRepository
│ ├── SpoolPresetRepository.ts ← interface ISpoolPresetRepository
│ └── AuthRepository.ts ← interface IAuthRepository
├── application/
│ ├── filament/
│ │ ├── CreateFilamentUseCase.ts
│ │ ├── UpdateFilamentUseCase.ts
│ │ ├── ListFilamentsUseCase.ts
│ │ └── DeleteFilamentUseCase.ts
│ ├── preset/
│ │ └── PresetUseCases.ts ← List, Create, Update, Delete
│ └── auth/
│ └── AuthUseCases.ts ← Login, Register, Google, Logout, etc.
├── adapters/
│ ├── remote/
│ │ ├── httpClient.ts ← Axios + interceptors JWT + refresh
│ │ ├── ApiAuthRepository.ts ← /api/v1/auth/*
│ │ ├── ApiFilamentRepository.ts ← /api/v1/filaments/*
│ │ └── ApiSpoolPresetRepository.ts ← /api/v1/spool-presets/*
│ └── local/
│ ├── database.ts ← init SQLite, WAL, foreign keys, tabelas
│ ├── LocalFilamentRepository.ts
│ └── LocalSpoolPresetRepository.ts
├── store/
│ ├── authStore.ts ← Zustand: sessão, SecureStore
│ ├── filamentStore.ts ← Zustand: lista + filtros em memória
│ └── presetStore.ts ← Zustand: systemPresets + userPresets
├── shared/
│ ├── theme.ts ← Design tokens (cores, tipografia, espaçamento)
│ ├── constants.ts ← API_BASE_URL, SecureStore keys, MATERIALS
│ └── utils/
│ └── filament.ts ← calcFilamentPercentage, formatWeight, etc.
└── presentation/
└── components/
├── ui/
│ ├── Button.tsx ← primary, secondary, ghost, danger
│ ├── Input.tsx ← label, leftIcon, rightLabel, isPassword, error
│ ├── Card.tsx ← container surface com borda
│ └── Badge.tsx ← badge dinâmico de estoque
├── layout/
│ ├── Screen.tsx ← SafeArea + scroll + keyboardAvoiding
│ └── Header.tsx ← título centralizado, back, slot direito
└── filament/
├── StockBar.tsx ← barra de progresso + badge numérico
├── ColorSwatch.tsx ← quadrado/círculo com cor hex
└── FilamentCard.tsx ← card de lista
```
---
## Design Tokens
Arquivo: `src/shared/theme.ts`
### Paleta (siamês)
| Token | Valor | Uso |
| --------------- | ----------- | --------------------------------- |
| `bgBase` | `#1E1B18` | Fundo geral |
| `bgSurface` | `#2A2622` | Cards, containers |
| `bgHover` | `#332F2B` | Hover/pressed em cards |
| `textPrimary` | `#F5EEDC` | Títulos, peso líquido |
| `textSecondary` | `#C9C1B0` | Descrições, labels |
| `accent` | `#38BCC2` | Botões de ação, elementos ativos |
| `accentMuted` | `#38BCC226` | Background de badges accent (10%) |
| `stockLow` | `#FF6B6B` | ≤ 15% — vermelho |
| `stockMedium` | `#FF9F43` | ≤ 35% — laranja |
| `stockOk` | `#38BCC2` | > 35% — accent |
| `border` | `#3D3830` | Bordas sutis |
| `error` | `#FF6B6B` | Mensagens de erro |
### Tipografia
- UI: **Inter**
- Mono (slugs, hex, URLs): **JetBrains Mono**
- App é **dark-mode exclusivo** — nenhum suporte a light mode
---
## Navegação (expo-router)
### Estrutura de grupos
```
(auth) ← sem autenticação; redireciona para (app) se sessão válida
(app) ← com autenticação; redireciona para (auth) se sem sessão
(tabs) ← bottom tabs fixos
```
### Deep links
- Scheme: `meowspool://`
- Filamento: `meowspool://filament/<id>``/(app)/inventory/<id>` (singular, alinhado com backend)
- O scanner (`scanner.tsx`) faz match via `/meowspool:\/\/filament\/([^/]+)/` e navega para `/(app)/inventory/<id>`
- O QR Code de cada filamento exibe `meowspool://filament/<id>` usando `react-native-qrcode-svg`
---
## Estratégia Offline-First
1. **Escrita**: toda mutação persiste primeiro no SQLite (`synced = 0`) e enfileira em `sync_queue`.
2. **Leitura**: lê sempre do SQLite; a API é usada apenas para sync.
3. **Sync**: background job consome `sync_queue` e envia para o backend Rust.
4. **Conflito**: last-write-wins via `updatedAt` (ISO 8601). O backend é a fonte de verdade em conflitos.
5. **Auth offline**: token armazenado 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
```
---
## Convenções de Código
### Nomenclatura
- **Componentes React**: PascalCase (`FilamentCard.tsx`)
- **Hooks**: camelCase com prefixo `use` (`useFilamentStore`)
- **Use Cases**: PascalCase com sufixo `UseCase` (`CreateFilamentUseCase`)
- **Repositórios**: PascalCase com prefixo de implementação (`ApiFilamentRepository`, `LocalFilamentRepository`)
- **Stores**: camelCase (`filamentStore.ts`), exportados como `useXxxStore`
### Imports
Sempre usar aliases em vez de caminhos relativos:
```ts
import { Filament } from "@domain/Filament";
import { useFilamentStore } from "@store/filamentStore";
import { colors } from "@shared/theme";
import { Button } from "@presentation/components/ui/Button";
```
### Componentes de tela
- Toda tela usa `SafeAreaView` com `backgroundColor: colors.bgBase`
- Header manual (não usa o header do expo-router): `headerShown: false`
- CTA fixo: `View` com `padding: spacing[5]` + `borderTopWidth: 1` + `borderTopColor: colors.border`
- ScrollViews com `showsVerticalScrollIndicator={false}` e `keyboardShouldPersistTaps="handled"`
### Formulários
- `react-hook-form` + `zodResolver` para todos os formulários
- Validações no schema Zod (nunca inline)
- `isLoading` local no componente durante submit
- Erros de API exibidos via `Alert.alert`
---
## API do Backend (rotas relevantes)
Base: `EXPO_PUBLIC_API_URL` (padrão: `http://localhost:3000/api/v1`)
### Auth
| Método | Rota | Descrição |
| ------ | ----------------------- | --------------------------- |
| POST | `/auth/login` | Login email/senha |
| POST | `/auth/register` | Cadastro |
| POST | `/auth/google` | OAuth Google |
| POST | `/auth/refresh` | Refresh token |
| POST | `/auth/logout` | Logout |
| POST | `/auth/forgot-password` | Solicitar reset de senha |
| POST | `/auth/reset-password` | Confirmar reset com token |
| POST | `/auth/verify-email` | Verificar e-mail com código |
### Filaments
| Método | Rota | Descrição |
| ------ | ----------------------- | -------------------- |
| GET | `/filaments` | Listar (com filtros) |
| POST | `/filaments` | Criar |
| GET | `/filaments/:id` | Detalhe |
| PATCH | `/filaments/:id` | Atualizar |
| DELETE | `/filaments/:id` | Excluir |
| GET | `/filaments/:id/qrcode` | QR Code SVG |
| GET | `/filaments/:id/label` | Etiqueta SVG |
### Spool Presets
| Método | Rota | Descrição |
| ------ | -------------------- | --------------------------- |
| GET | `/spool-presets` | Listar (sistema + usuário) |
| POST | `/spool-presets` | Criar preset do usuário |
| PATCH | `/spool-presets/:id` | Atualizar preset do usuário |
| DELETE | `/spool-presets/:id` | Excluir preset do usuário |
### Dashboard
| Método | Rota | Descrição |
| ------ | ------------ | ---------------------------- |
| GET | `/dashboard` | Dados agregados do dashboard |
---
## Próximos Passos (integrações pendentes)
- [ ] Instanciar e injetar repositórios concretos nos Use Cases (DI container simples ou Context)
- [ ] Implementar sync background com `sync_queue` SQLite → API
- [x] Integrar `react-native-qrcode-svg` para render real do QR Code
- [x] Scanner de QR Code com `expo-camera` v17 (ML Kit) → `scanner.tsx`
- [ ] Gerar SVG de etiqueta (integração com `/filaments/:id/label` do backend)
- [ ] Expo Notifications para alertas de estoque baixo
- [ ] Google OAuth com `expo-auth-session`
- [ ] Testes de integração com Jest + Testing Library
---
## Mudanças Recentes (14/03/2026)
### ✅ Implementações Completadas
#### 1. **Delete Filament — Integração com API**
- **Arquivos**: `app/(app)/inventory/[id].tsx`, `app/(app)/inventory/[id]/edit.tsx`
- **Mudança**: `handleDelete()` agora chama `deleteFilamentUseCase.execute()` antes de remover do store local
- **Código**:
```typescript
const handleDelete = async () => {
try {
const user = useAuthStore.getState().user;
await deleteFilamentUseCase.execute(filament!.id, user?.id);
removeFilament(filament!.id);
Alert.alert("Sucesso", "Filamento deletado da API");
} catch (error) {
Alert.alert("Erro", "Falha ao deletar: " + (error as Error).message);
}
};
```
- **Impacto**: Delete agora funciona corretamente em ambos os screens (detail e edit)
#### 2. **Pull-to-Refresh — Inventory List**
- **Arquivo**: `app/(tabs)/inventory.tsx`
- **Mudança**: Adicionado `RefreshControl` ao FlatList com chamada a `listFilamentsUseCase.execute()`
- **Código**:
```typescript
const [isRefreshing, setIsRefreshing] = useState(false);
const onRefresh = async () => {
setIsRefreshing(true);
try {
const user = useAuthStore.getState().user;
const result = await listFilamentsUseCase.execute(user?.id);
setFilaments(result);
} finally {
setIsRefreshing(false);
}
};
<FlatList
refreshControl={<RefreshControl refreshing={isRefreshing} onRefresh={onRefresh} />}
// ... resto do componente
/>
```
- **Impacto**: Usuários podem puxar para baixo e sincronizar lista sem reabrir o app
#### 3. **Update Filament — Integração com API**
- **Arquivo**: `app/(app)/inventory/[id]/edit.tsx`
- **Mudança**: `onSubmit()` agora chama `updateFilamentUseCase.execute()` em vez de apenas atualizar o store local
- **Código**:
```typescript
const onSubmit = async () => {
try {
const user = useAuthStore.getState().user;
const result = await updateFilamentUseCase.execute(
{
id: filament!.id,
brand: formData.brand,
model: formData.model,
color: formData.color,
weight_g: parseFloat(formData.weight_g),
},
user?.id,
);
updateFilament(result);
Alert.alert("Sucesso", "Filamento atualizado na API");
router.back();
} catch (error) {
Alert.alert("Erro", "Falha ao atualizar: " + (error as Error).message);
}
};
```
- **Impacto**: Edições de filamento agora persistem no servidor
#### 4. **Custom Color Picker — Hex Input com Validação**
- **Arquivos**: `app/(app)/inventory/new.tsx`, `app/(app)/inventory/[id]/edit.tsx`
- **Mudança**: Adicionado `handleOpenColorPicker()` que usa `Alert.prompt()` para capturar hexadecimais
- **Validação**:
```typescript
const isValidHex = (hex: string): boolean => {
return /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/.test(hex);
};
const normalizeHex = (hex: string): string => {
return hex.startsWith("#") ? hex : `#${hex}`;
};
const handleOpenColorPicker = () => {
setIsOpeningColorPicker(true);
Alert.prompt(
"Hex Color",
"Enter hex color (e.g., #FF5733 or FF5733)",
[
{
text: "Cancel",
onPress: () => setIsOpeningColorPicker(false),
style: "cancel",
},
{
text: "Add Color",
onPress: (input: string | undefined) => {
if (input && isValidHex(input)) {
setFormData((prev) => ({ ...prev, color: normalizeHex(input) }));
} else {
Alert.alert("Invalid hex color", "Please enter a valid hex code");
}
setIsOpeningColorPicker(false);
},
},
],
"plain-text",
);
};
```
- **Integração**: Botão "+" vinculado a `handleOpenColorPicker`
- **Impacto**: Usuários podem agora inserir cores customizadas por código hexadecimal
#### 5. **Sincronização de Estado — API-First Pattern**
- **Padrão**: Todas operações agora seguem: **API call → store update**
- **Benefício**: Source of truth centralizada no servidor
- **Implementação**:
- Delete: `deleteFilamentUseCase` → `removeFilament()`
- Update: `updateFilamentUseCase` → `updateFilament(result)`
- Refresh: `listFilamentsUseCase` → `setFilaments(result)`
- Create: `createFilamentUseCase` → `addFilament(result)`
### Arquitetura Mantida
- Todos use cases injetados via `container.ts` (Dependency Injection)
- Zod schemas usados para validação de formulários
- JWT interceptor em Axios automaticamente adiciona `Authorization` header
- Error handling com `Alert.alert()` visível ao usuário
---
## Executando o Projeto
```bash
# Instalar dependências (na pasta mobile/)
mise install # garante Node 22 e Java 17
npm install
# Iniciar servidor de desenvolvimento (Expo Go — sem câmera ML Kit)
npx expo start
# Build nativo Android (necessário para expo-camera ML Kit barcode scan)
mise exec -- npx expo run:android
# Build nativo iOS
mise exec -- npx expo run:ios
```
> **Variável de ambiente**: crie `mobile/.env` com:
>
> ```
> EXPO_PUBLIC_API_URL=http://localhost:3000/api/v1
> ```
> **Atenção ao build Android**: o projeto usa `namespace "com.meowspool"` no `build.gradle` mas
> o código fonte fica em `com.meowspool.app`. Por isso `MainActivity.kt` e `MainApplication.kt`
> importam explicitamente `com.meowspool.R` e `com.meowspool.BuildConfig`, e o `AndroidManifest.xml`
> usa nomes totalmente qualificados (`com.meowspool.app.MainApplication`, `com.meowspool.app.MainActivity`).
> Não usar nomes relativos (`.MainApplication`) no manifest — eles resolvem para `com.meowspool.*`
> (sem `.app`), causando `ClassNotFoundException` em runtime.