Files
system-docs/v4-archive/packet/docs/SPEC.md
2025-12-24 17:28:34 +00:00

13 KiB

Packet - Especificación Técnica Completa

1. Visión General

Packet permite:

  • Capturar contenido multimedia (fotos, audio, video, archivos)
  • Etiquetarlo con referencias de bibliotecas externas
  • Empaquetarlo en contenedores con hash único
  • Enviarlo a backends configurables

Principios

Principio Descripción
Zero-retention Los archivos nunca se almacenan en disco, solo en RAM hasta envío
Hash-first Todo contenedor tiene hash único generado localmente
Contenedor cerrado Una vez montado, no se inspecciona. Solo enviar o desmontar
App tonta No valida coherencia de etiquetas, solo formato. El backend decide
Offline-capable Captura sin conexión, cola de reintentos cuando hay red

2. Stack Tecnológico

Componente Tecnología
Framework Flutter 3.x
Lenguaje Dart
Estado flutter_bloc
DB Local sqflite
HTTP dio
Crypto crypto (SHA-256)
Media image_picker, record
Location geolocator
Permisos permission_handler

3. Estructura de Carpetas

packet/
├── android/
├── ios/
├── lib/
│   ├── core/
│   │   ├── constants/
│   │   │   └── app_constants.dart
│   │   ├── errors/
│   │   │   └── exceptions.dart
│   │   ├── utils/
│   │   │   ├── hash_utils.dart
│   │   │   └── retry_utils.dart
│   │   └── theme/
│   │       └── app_theme.dart
│   │
│   ├── data/
│   │   ├── datasources/
│   │   │   ├── local_database.dart
│   │   │   ├── backend_api.dart
│   │   │   └── biblioteca_api.dart
│   │   ├── models/
│   │   │   ├── destino_model.dart
│   │   │   ├── contenedor_model.dart
│   │   │   ├── etiqueta_model.dart
│   │   │   └── pack_model.dart
│   │   └── repositories/
│   │       ├── contenedor_repository.dart
│   │       ├── etiqueta_repository.dart
│   │       └── config_repository.dart
│   │
│   ├── domain/
│   │   └── entities/
│   │       ├── contenedor.dart
│   │       ├── etiqueta.dart
│   │       ├── pack.dart
│   │       ├── destino.dart
│   │       └── archivo_adjunto.dart
│   │
│   ├── presentation/
│   │   ├── bloc/
│   │   │   ├── captura/
│   │   │   ├── etiquetas/
│   │   │   ├── packs/
│   │   │   ├── pendientes/
│   │   │   └── config/
│   │   ├── pages/
│   │   │   ├── captura_page.dart
│   │   │   ├── etiquetas_page.dart
│   │   │   ├── packs_page.dart
│   │   │   ├── pendientes_page.dart
│   │   │   └── config_page.dart
│   │   ├── widgets/
│   │   │   ├── audio_recorder.dart
│   │   │   ├── destino_selector.dart
│   │   │   ├── etiqueta_grid.dart
│   │   │   └── contenedor_card.dart
│   │   └── app.dart
│   │
│   └── main.dart
│
├── test/
├── pubspec.yaml
└── README.md

4. Pantallas

Navegación

┌─────────────────────────────────────────┐
│  [Mi DECK ▼] [CORP] [+Ext]              │  ← Destinos (superior)
├─────────────────────────────────────────┤
│                                         │
│           CONTENIDO PANTALLA            │
│                                         │
├─────────────────────────────────────────┤
│  📷      🏷️      📦      ⏳      ⚙️     │  ← Navegación (inferior)
│ Captura  Tags   Packs  Pend.  Config   │
└─────────────────────────────────────────┘

4.1 Captura

Montar contenedor con contenido multimedia.

Campos:

  • Título (string, opcional)
  • Descripción (string, opcional, se procesa en backend)
  • Identificador (string 64 chars, auto-generado SHA-256)
  • Archivos (array, opcional)
  • GPS (lat/long, opcional)
  • Etiquetas (array de hashes, seleccionadas en pestaña Etiquetas)

Botones de captura:

  • 📷 Foto (cámara)
  • 🎤 Audio (con contador de tiempo)
  • 🎥 Video
  • 📎 Archivo
  • 📍 GPS

El hash se genera al abrir la app o tras cada envío. Se puede enviar contenedor vacío (solo hash). Botones de copiar y regenerar hash visibles.

4.2 Etiquetas

Seleccionar etiquetas de bibliotecas y añadir hashes externos.

Secciones:

  1. Grid HST - Iconos de biblioteca HST (se oscurecen al seleccionar)
  2. Grid DECK - Iconos de biblioteca DECK
  3. Campo hash - Pegar URL o hash externo
  4. Seleccionadas - Lista de etiquetas actuales con botón eliminar

Validación de hash externo:

  1. Usuario pega texto
  2. App busca patrón: [a-f0-9]{64}
  3. Por cada match → check verde inmediato
  4. Background: consulta /api/tags para resolver nombre
  5. Si encuentra → muestra nombre
  6. Si no → muestra hash truncado

4.3 Packs

Conjuntos predefinidos de etiquetas creados localmente.

  • 1 tap → aplica todas las etiquetas del pack
  • Creación: nombre + etiquetas actualmente seleccionadas
  • Almacenados en SQLite local

4.4 Pendientes

Cola de contenedores fallidos. Límite: 20 contenedores máximo. App se bloquea si se llena.

Por cada contenedor muestra:

  • Hash (truncado)
  • Título (o "sin título")
  • Número de archivos
  • Intentos realizados (máx 20)
  • Último intento (hora)
  • Estado (error / reintentando)

Acciones:

  • Reintentar → Intenta enviar de nuevo
  • Recuperar → Extrae archivos al almacenamiento del dispositivo y elimina contenedor

4.5 Config

URLs, llaves y bibliotecas.

Por cada destino:

  • Nombre (identificador visual)
  • URL Backend (endpoint de envío)
  • Llave (hash de 64 chars para autenticación)

Bibliotecas de iconos:

  • URL (dominio)
  • Endpoint (ruta del índice, ej: /api/tags)
  • Nombre (identificador visual)

5. Modelos de Datos

Contenedor

class Contenedor {
  String hash;              // 64 chars, generado localmente
  String? titulo;
  String? descripcion;
  List<ArchivoAdjunto> archivos;
  GpsLocation? gps;
  List<String> etiquetas;   // Lista de hashes
  DateTime createdAt;
}

Etiqueta

class Etiqueta {
  String hMaestro;          // Hash único (64 chars)
  String hGlobal;           // biblioteca + etiqueta (128 chars)
  String? mrf;              // Media reference para imagen
  String? ref;              // Código corto
  String? nombreEs;
  String? nombreEn;
  String grupo;
  bool activo;
}

Destino

class Destino {
  int id;
  String nombre;
  String url;
  String hash;              // Llave de autenticación
  bool activo;
}

Pack

class Pack {
  int id;
  String nombre;
  String icono;
  List<String> tags;        // Lista de hashes
}

ArchivoAdjunto

class ArchivoAdjunto {
  String nombre;
  String mimeType;
  Uint8List bytes;          // En memoria, nunca en disco
  FileType tipo;            // image, audio, video, document
  String? hash;
}

6. Base de Datos SQLite

CREATE TABLE registro (
    hash TEXT PRIMARY KEY,
    titulo TEXT,
    hora_envio TEXT NOT NULL,
    primera_conf TEXT,
    ultima_conf TEXT,
    destino_id INTEGER
);

CREATE TABLE destinos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    nombre TEXT NOT NULL,
    url TEXT NOT NULL,
    hash TEXT NOT NULL,
    activo INTEGER DEFAULT 1
);

CREATE TABLE bibliotecas (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    nombre TEXT NOT NULL,
    url TEXT NOT NULL,
    endpoint TEXT NOT NULL,
    h_biblioteca TEXT
);

CREATE TABLE etiquetas_cache (
    h_maestro TEXT PRIMARY KEY,
    h_global TEXT,
    mrf TEXT,
    ref TEXT,
    nombre_es TEXT,
    nombre_en TEXT,
    grupo TEXT,
    biblioteca_id INTEGER
);

CREATE TABLE packs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    nombre TEXT NOT NULL,
    icono TEXT DEFAULT 📦,
    tags TEXT NOT NULL
);

CREATE TABLE pendientes (
    hash TEXT PRIMARY KEY,
    titulo TEXT,
    contenido BLOB NOT NULL,
    intentos INTEGER DEFAULT 0,
    ultimo_intento TEXT,
    proximo_intento TEXT,
    destino_id INTEGER
);

CREATE TABLE app_state (
    key TEXT PRIMARY KEY,
    value TEXT
);

7. APIs Externas

Backend (envío)

POST {destino.url}/ingest
X-Auth-Key: {destino.hash}
Content-Type: application/json

{
  "hash": "fc2fae65158ca7c1880dd3ff9b8ed9081107b50f9cdce08df30234fe166777ce",
  "titulo": "ferr-23",
  "descripcion": "Compra en ferretería",
  "etiquetas": ["a1b2c3d4...fc2fae65...", "a1b2c3d4...b2c3d4e5..."],
  "gps": {"lat": 43.3623, "long": -8.4115},
  "archivos": [{"nombre": "foto.jpg", "tipo": "image/jpeg", "contenido": "base64..."}]
}

Response 200: { "ok": true, "received_at": "2024-12-20T10:00:00Z" }
Response 409: { "error": "hash_exists" }

Envío por chunks (archivos grandes)

POST {destino.url}/upload/init
{ "hash": "...", "total_chunks": 50, "file_name": "video.mp4" }
→ { "upload_id": "..." }

POST {destino.url}/upload/chunk/{upload_id}/{chunk_number}
Body: bytes (512KB - 1MB por chunk)
→ { "ok": true }

POST {destino.url}/upload/complete/{upload_id}
→ { "ok": true, "file_hash": "..." }

Bibliotecas (índice de etiquetas)

GET {biblioteca.url}/api/tags

Response:
{
  "count": 973,
  "biblioteca": {
    "h_biblioteca": "b7149f9e2106c566032aeb29a26e4c6cdd5f5c16b4421025c58166ee345740d1",
    "nombre": "HST",
    "publica": true
  },
  "results": [
    {
      "h_maestro": "fc2fae65...77ce",
      "h_global": "b7149f9e...fc2fae65...77ce",
      "mrf": "502dc114...",
      "ref": "inv",
      "nombre_es": "factura",
      "nombre_en": "invoice",
      "grupo": "hst",
      "imagen_url": "https://tzrtech.org/502dc...png",
      "activo": true
    }
  ]
}

Imágenes: GET {biblioteca.url}/{mrf}.png


8. Formato de Referencias

Hash simple (etiqueta): 64 caracteres hex

fc2fae65158ca7c1880dd3ff9b8ed9081107b50f9cdce08df30234fe166777ce

Referencia global (biblioteca + etiqueta): 128 caracteres

[h_biblioteca 64 chars][h_etiqueta 64 chars]

Cadena jerárquica: múltiplo de 64, sin separador

[biblioteca][nivel1][nivel2][nivel3]...

Múltiples cadenas: separadas por \n


9. Lógica de Reintentos

20 intentos en 72 horas con backoff exponencial:

Intento Espera Acumulado
1 1 min 0
2 2 min 1 min
3 5 min 3 min
4 10 min 8 min
5 20 min 18 min
6 30 min 38 min
7 1 h 1 h
8 2 h 2 h
9 3 h 4 h
10 4 h 7 h
11 5 h 11 h
12 6 h 16 h
13 6 h 22 h
14 6 h 28 h
15 8 h 34 h
16 8 h 42 h
17 8 h 50 h
18 8 h 58 h
19 6 h 66 h
20 STOP 72 h

10. Persistencia de Estado

La app restaura al abrir exactamente como se cerró:

  • Destino seleccionado
  • Pantalla activa
  • Contenedor en progreso
  • Etiquetas seleccionadas
  • Cola de pendientes
  • Configuración completa

11. Permisos

Android:

<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.CAMERA"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

iOS:

<key>NSCameraUsageDescription</key>
<string>Para capturar fotos y videos</string>
<key>NSMicrophoneUsageDescription</key>
<string>Para grabar notas de voz</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Para añadir ubicación a los contenedores</string>

12. CI/CD

name: Build

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: subosito/flutter-action@v2
      - run: flutter pub get
      - run: flutter test

  build-android:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: subosito/flutter-action@v2
      - run: flutter build apk --release

  build-ios:
    needs: test
    runs-on: macos-latest
    steps:
      - uses: actions/checkout@v3
      - uses: subosito/flutter-action@v2
      - run: flutter build ios --release --no-codesign

13. Roadmap

v1.0 (MVP)

  • 5 pantallas básicas
  • Captura foto/audio/video
  • Envío a backend único
  • Cola de pendientes
  • Packs locales

v1.1

  • Múltiples destinos
  • Envío por chunks
  • Cache de bibliotecas

v1.2

  • Notificaciones push
  • Widgets de acceso rápido

v2.0

  • Credenciales para bibliotecas privadas
  • Cadenas jerárquicas visuales
  • Sincronización entre dispositivos

14. Referencias


Versión: 1.0 Fecha: 2025-12-20