Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a3352a96c | ||
|
|
a21c43684c |
13
AGENTS.md
13
AGENTS.md
@@ -25,3 +25,16 @@ Skills del proyecto en `skills/`:
|
||||
Seguir OpenSpec workflow: propose -> apply -> archive
|
||||
Usar skills cuando aplique al contexto
|
||||
Mantener código siguiendo arquitectura limpia
|
||||
|
||||
## Release Android
|
||||
- Antes de publicar una APK, ejecutar `npm run test:run`, `npm run test:e2e`, `npm exec vue-tsc -- --noEmit` y `npm run build`.
|
||||
- Ejecutar `npx cap sync android` y `./gradlew assembleDebug` usando el builder Android del repositorio cuando el host no tenga Java.
|
||||
- Verificar el APK con `scripts/verify-apk.sh <apk> <versionName> <versionCode>` antes de adjuntarlo a Gitea.
|
||||
- El script debe confirmar `applicationId`, `versionName`, `versionCode`, bundle JavaScript, fuentes MDI y textos críticos de la versión.
|
||||
- Incrementar `versionCode` para cada APK publicable; mantener la misma firma y publicar versiones beta como prerelease.
|
||||
- Después de subir un asset, comparar su SHA256 con el APK verificado localmente y comprobar el asset de la release mediante la API de Gitea.
|
||||
|
||||
## Persistencia local
|
||||
- La app debe abrir Dexie explícitamente antes de montar Vue.
|
||||
- Las migraciones deben agregar una nueva versión Dexie y transformar datos; nunca deben borrar datos automáticamente.
|
||||
- El reset de la base es una acción manual, confirmada y visible sólo como último recurso.
|
||||
|
||||
4
apps/toldo-app/package-lock.json
generated
4
apps/toldo-app/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "toldo-app",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "toldo-app",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.6",
|
||||
"dependencies": {
|
||||
"@capacitor/android": "^6.2.1",
|
||||
"@capacitor/core": "^6.2.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "toldo-app",
|
||||
"private": true,
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.6",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
@@ -19,7 +19,7 @@ describe('contratos de operación y respaldo', () => {
|
||||
|
||||
it('expone la versión actual en las exportaciones', () => {
|
||||
const result = new ExportImportService().exportAll({ productores: [], productos: [], movimientosInventario: [], ventas: [], ventaLineas: [], gastos: [], ajustes: [], cuadres: [] });
|
||||
expect(result.appVersion).toBe('0.0.4');
|
||||
expect(result.appVersion).toBe('0.0.6');
|
||||
});
|
||||
|
||||
it('exporta jornadas junto con los datos operativos', () => {
|
||||
|
||||
22
apps/toldo-app/src/__tests__/database-bootstrap.test.ts
Normal file
22
apps/toldo-app/src/__tests__/database-bootstrap.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { db, openDatabase, resetDatabase } from '@toldo/data';
|
||||
|
||||
describe('bootstrap y recuperación de base local', () => {
|
||||
beforeEach(async () => {
|
||||
await resetDatabase();
|
||||
});
|
||||
|
||||
it('abre el esquema actual y conserva datos al cerrar y abrir', async () => {
|
||||
await db.productores.add({ id: 'persisted', nombre: 'Productor persistente', activo: true, createdAt: new Date() } as any);
|
||||
db.close();
|
||||
await openDatabase();
|
||||
expect(await db.productores.get('persisted')).toMatchObject({ nombre: 'Productor persistente' });
|
||||
expect(db.auditorias).toBeTruthy();
|
||||
});
|
||||
|
||||
it('restablece sólo cuando se solicita explícitamente', async () => {
|
||||
await db.productores.add({ id: 'to-reset', nombre: 'Temporal', activo: true, createdAt: new Date() } as any);
|
||||
await resetDatabase();
|
||||
expect(await db.productores.count()).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -2,5 +2,27 @@ import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import vuetify from './plugins/vuetify'
|
||||
import { openDatabase, resetDatabase } from '@toldo/data'
|
||||
|
||||
async function bootstrap() {
|
||||
try {
|
||||
await openDatabase()
|
||||
createApp(App).use(router).use(vuetify).mount('#app')
|
||||
} catch (error) {
|
||||
const root = document.querySelector('#app')
|
||||
if (!root) return
|
||||
root.innerHTML = ''
|
||||
const card = document.createElement('main')
|
||||
card.className = 'database-recovery'
|
||||
card.innerHTML = '<h1>No se pudo abrir los datos locales</h1><p>La aplicación no continuará para evitar mostrar información incompleta. Puedes intentar recargar o restablecer el entorno como último recurso.</p>'
|
||||
const details = document.createElement('pre')
|
||||
details.textContent = error instanceof Error ? error.message : String(error)
|
||||
const button = document.createElement('button')
|
||||
button.textContent = 'Restablecer entorno local'
|
||||
button.onclick = async () => { button.disabled = true; await resetDatabase(); window.location.reload() }
|
||||
card.append(details, button)
|
||||
root.append(card)
|
||||
}
|
||||
}
|
||||
|
||||
bootstrap()
|
||||
|
||||
@@ -56,6 +56,11 @@ body {
|
||||
background: #f5f7f2;
|
||||
}
|
||||
|
||||
.database-recovery { max-width: 640px; margin: 12vh auto; padding: 24px; font-family: system-ui, sans-serif; color: #26352b; background: #fffdf8; border: 1px solid #d9e2d7; border-radius: 12px; box-shadow: 0 8px 30px rgba(38, 53, 43, .12); }
|
||||
.database-recovery p { margin: 12px 0; }
|
||||
.database-recovery pre { white-space: pre-wrap; font-size: 12px; color: #6b4a25; background: #f7f1e7; padding: 12px; border-radius: 8px; }
|
||||
.database-recovery button { padding: 12px 16px; color: white; background: #2d5f3b; border: 0; border-radius: 8px; cursor: pointer; }
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-family: var(--heading);
|
||||
|
||||
@@ -8,19 +8,25 @@
|
||||
|
||||
<section>
|
||||
<v-card><v-card-title>Restaurar datos</v-card-title><v-card-text><v-file-input accept=".json" label="Seleccionar copia JSON" @update:model-value="archivo = $event" /><v-alert v-if="mensaje" :type="tipoMensaje" variant="tonal" class="mb-3">{{ mensaje }}</v-alert><v-btn @click="importar" :disabled="!archivo" color="secondary" prepend-icon="mdi-upload">Importar datos</v-btn></v-card-text></v-card>
|
||||
<v-card class="mt-4" variant="tonal" color="error"><v-card-title>Mantenimiento</v-card-title><v-card-text><div class="text-body-2 mb-3">Restablece la base local sólo si la aplicación no puede migrarla. Esta acción elimina ventas, inventario, jornadas y respaldos locales.</div><v-btn color="error" variant="outlined" prepend-icon="mdi-database-refresh" @click="reiniciarEntorno">Restablecer entorno local</v-btn></v-card-text></v-card>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { db } from '@toldo/data';
|
||||
import { db, resetDatabase } from '@toldo/data';
|
||||
import { ExportImportService } from '@toldo/services';
|
||||
|
||||
const exportService = new ExportImportService();
|
||||
const archivo = ref<File | null>(null);
|
||||
const mensaje = ref('');
|
||||
const tipoMensaje = ref<'success' | 'error'>('success');
|
||||
async function reiniciarEntorno() {
|
||||
if (!window.confirm('Se eliminarán todos los datos locales. Exporta un respaldo antes de continuar. ¿Restablecer?')) return;
|
||||
await resetDatabase();
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function onFileChange(e: any) {
|
||||
archivo.value = e.target.files[0];
|
||||
|
||||
@@ -29,3 +29,18 @@ test('permite volver a inicio desde Jornada e Inventario', async ({ page }) => {
|
||||
await page.getByTestId('back-button').click();
|
||||
await expect(page.getByText('Panel de control')).toBeVisible();
|
||||
});
|
||||
|
||||
test('muestra los flujos compactos en móvil', async ({ page }) => {
|
||||
test.skip(test.info().project.name !== 'mobile', 'Este escenario se ejecuta en el proyecto móvil.');
|
||||
await page.setViewportSize({ width: 390, height: 844 });
|
||||
await page.goto('/inventario');
|
||||
await expect(page.getByRole('tab', { name: 'Catálogo' })).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: 'Ingresos' })).toBeVisible();
|
||||
await expect(page.getByRole('tab', { name: 'Ajustes' })).toBeVisible();
|
||||
await expect(page.getByText('Migas de pan')).toHaveCount(0);
|
||||
await page.goto('/ventas');
|
||||
await expect(page.getByRole('button', { name: 'Buscar productos' })).toBeVisible();
|
||||
await page.goto('/jornada');
|
||||
await expect(page.getByLabel('Efectivo contado (obligatorio)')).toBeVisible();
|
||||
await expect(page.getByLabel('Dinero en banco por QR (opcional)')).toBeVisible();
|
||||
});
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
versionName=0.0.4
|
||||
versionCode=4
|
||||
versionName=0.0.6
|
||||
versionCode=6
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-10-25
|
||||
23
openspec/changes/harden-database-bootstrap-reset/design.md
Normal file
23
openspec/changes/harden-database-bootstrap-reset/design.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Design
|
||||
|
||||
## Context
|
||||
|
||||
Dexie abre la base al ejecutar la primera consulta. El esquema tiene migraciones 1, 2 y 3, pero `main.ts` monta Vue inmediatamente y no distingue una base en migración de un error de operación.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:** hacer explícita la apertura, evitar operaciones contra una base no lista, ofrecer recuperación manual y preservar datos durante actualizaciones compatibles.
|
||||
|
||||
**Non-Goals:** borrar o reconstruir automáticamente una base con error, sincronizar datos con servidor o reemplazar las migraciones Dexie.
|
||||
|
||||
## Decisions
|
||||
|
||||
- `db.open()` se ejecutará antes de montar la aplicación.
|
||||
- Un fallo de apertura montará una vista mínima de recuperación con mensaje técnico resumido y botón de reset destructivo.
|
||||
- El reset se expondrá también desde Exportar/Importar, exigiendo confirmación y recomendando exportar antes.
|
||||
- Las migraciones se mantendrán como versiones Dexie; cada cambio futuro de esquema deberá agregar una versión y upgrade explícito.
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- El usuario puede resetear por error -> confirmación explícita y texto de pérdida de datos.
|
||||
- Una base realmente corrupta puede no permitir exportación -> la pantalla explicará que el reset es último recurso.
|
||||
29
openspec/changes/harden-database-bootstrap-reset/proposal.md
Normal file
29
openspec/changes/harden-database-bootstrap-reset/proposal.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Proposal
|
||||
|
||||
## Why
|
||||
|
||||
La app abre Dexie de forma implícita y no informa cuando una migración o apertura de IndexedDB falla. Esto puede parecer una pérdida de datos y obliga a borrar manualmente el almacenamiento para continuar.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Abrir y validar la base local antes de montar Vue.
|
||||
- Mostrar una pantalla de recuperación si la apertura/migración falla.
|
||||
- Añadir reset manual del entorno desde Exportar/Importar, con confirmación destructiva.
|
||||
- Mantener las migraciones Dexie y documentar que nunca deben borrar datos automáticamente.
|
||||
- Añadir pruebas de persistencia y reset.
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `database-bootstrap-recovery`: apertura explícita, diagnóstico y recuperación controlada de IndexedDB.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `toldo-poc`: persistencia local y operación offline.
|
||||
|
||||
## Impact
|
||||
|
||||
- `packages/data`: bootstrap/reset de base.
|
||||
- `apps/toldo-app`: arranque, pantalla de error y mantenimiento.
|
||||
- Tests Vitest, documentación de agentes y APK.
|
||||
@@ -0,0 +1,33 @@
|
||||
# Spec Delta
|
||||
|
||||
## Purpose
|
||||
|
||||
Protege los datos locales durante actualizaciones y ofrece una recuperación explícita cuando IndexedDB no puede abrirse o migrarse.
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Apertura explícita de base
|
||||
The system SHALL open and complete local database migrations before allowing operational views to run.
|
||||
|
||||
#### Scenario: Actualización compatible
|
||||
- **GIVEN** data exists in an older supported schema
|
||||
- **WHEN** the app starts after an update
|
||||
- **THEN** migrations complete before the main UI mounts
|
||||
- **AND** existing records remain available
|
||||
|
||||
### Requirement: Recuperación ante error
|
||||
The system SHALL show a recovery screen when the local database cannot be opened.
|
||||
|
||||
#### Scenario: Migración fallida
|
||||
- **WHEN** database opening fails
|
||||
- **THEN** the app shows an actionable error
|
||||
- **AND** no operational action is rendered as if data were empty
|
||||
- **AND** user can explicitly reset local data
|
||||
|
||||
### Requirement: Reset manual
|
||||
The system SHALL require explicit confirmation before deleting all local data.
|
||||
|
||||
#### Scenario: Reset confirmado
|
||||
- **WHEN** user confirms reset from maintenance
|
||||
- **THEN** local database is deleted and recreated on reload
|
||||
- **AND** the app starts with an empty environment
|
||||
12
openspec/changes/harden-database-bootstrap-reset/tasks.md
Normal file
12
openspec/changes/harden-database-bootstrap-reset/tasks.md
Normal file
@@ -0,0 +1,12 @@
|
||||
# Tasks
|
||||
|
||||
## 1. Bootstrap y recuperación
|
||||
|
||||
- [x] 1.1 Añadir apertura explícita Dexie antes de montar Vue y vista de error recuperable
|
||||
- [x] 1.2 Añadir helper de reset local sin usarlo automáticamente
|
||||
- [x] 1.3 Añadir reset manual en Exportar/Importar con confirmación
|
||||
|
||||
## 2. Verificación
|
||||
|
||||
- [x] 2.1 Cubrir apertura, persistencia y reset con tests
|
||||
- [x] 2.2 Ejecutar tests, `vue-tsc`, Playwright y build/APK
|
||||
@@ -8,7 +8,7 @@ La aplicación usa versiones distintas o hardcodeadas entre npm, Android y las e
|
||||
|
||||
- Definir `versionName` y `versionCode` en un archivo versionado compartido.
|
||||
- Usar esos valores en Gradle, npm y exportaciones JSON.
|
||||
- Publicar la siguiente versión como `0.0.4` con `versionCode 4`.
|
||||
- Publicar la siguiente versión como `0.0.6` con `versionCode 6`.
|
||||
- Validar el `applicationId` y la versión en el APK generado.
|
||||
|
||||
## Capabilities
|
||||
|
||||
@@ -9,10 +9,10 @@ Asegura que cada actualización de ToldoConjunto pueda identificarse y actualiza
|
||||
### Requirement: Versionado consistente
|
||||
The system SHALL expose the same semantic version in npm metadata, exported data, and Android versionName.
|
||||
|
||||
#### Scenario: Version 0.0.4
|
||||
#### Scenario: Version 0.0.6
|
||||
- **WHEN** the app is built
|
||||
- **THEN** npm and exported data report `0.0.4`
|
||||
- **AND** Android reports versionName `0.0.4` and versionCode `4`
|
||||
- **THEN** npm and exported data report `0.0.6`
|
||||
- **AND** Android reports versionName `0.0.6` and versionCode `6`
|
||||
|
||||
### Requirement: Identidad Android estable
|
||||
The system SHALL preserve the application id `com.ilusion.toldoconjunto` across releases.
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
## 1. Versionado
|
||||
|
||||
- [x] 1.1 Crear la fuente compartida con `versionName=0.0.4` y `versionCode=4`, y actualizar npm/exportaciones
|
||||
- [x] 1.1 Crear la fuente compartida con `versionName=0.0.6` y `versionCode=6`, y actualizar npm/exportaciones
|
||||
- [x] 1.2 Configurar Gradle para leer la fuente compartida y conservar el applicationId
|
||||
- [x] 1.3 Añadir test que compruebe versionado y compatibilidad de exportación
|
||||
|
||||
|
||||
@@ -52,3 +52,13 @@ export class ToldoDB extends Dexie {
|
||||
}
|
||||
|
||||
export const db = new ToldoDB();
|
||||
|
||||
export async function openDatabase() {
|
||||
await db.open();
|
||||
return db;
|
||||
}
|
||||
|
||||
export async function resetDatabase() {
|
||||
await db.delete();
|
||||
await db.open();
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export interface ExportData {
|
||||
}
|
||||
|
||||
export const CURRENT_SCHEMA_VERSION = 1;
|
||||
export const APP_VERSION = '0.0.4';
|
||||
export const APP_VERSION = '0.0.6';
|
||||
|
||||
export class ExportImportService {
|
||||
exportAll(data: ExportData['data']): ExportData {
|
||||
|
||||
20
scripts/verify-apk.sh
Executable file
20
scripts/verify-apk.sh
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
apk="${1:?Uso: verify-apk.sh <apk> <versionName> <versionCode>}"
|
||||
version_name="${2:?Falta versionName}"
|
||||
version_code="${3:?Falta versionCode}"
|
||||
aapt_bin="${AAPT:-aapt2}"
|
||||
tmp_dir="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp_dir"' EXIT
|
||||
|
||||
badging="$($aapt_bin dump badging "$apk")"
|
||||
printf '%s\n' "$badging" | grep -F "package: name='com.ilusion.toldoconjunto' versionCode='$version_code' versionName='$version_name'" >/dev/null
|
||||
|
||||
unzip -q "$apk" -d "$tmp_dir"
|
||||
find "$tmp_dir/assets/public/assets" -type f -name '*.js' -print0 | xargs -0 grep -F -l 'Buscar productos' >/dev/null
|
||||
find "$tmp_dir/assets/public/assets" -type f -name '*.js' -print0 | xargs -0 grep -F -l 'Dinero en banco por QR' >/dev/null
|
||||
find "$tmp_dir/assets/public/assets" -type f -name '*.js' -print0 | xargs -0 grep -F -l 'Cerrar caja y jornada' >/dev/null
|
||||
find "$tmp_dir/assets/public/assets" -type f -name '*.woff2' | grep materialdesignicons >/dev/null
|
||||
|
||||
printf 'APK verificada: applicationId=com.ilusion.toldoconjunto versionName=%s versionCode=%s\n' "$version_name" "$version_code"
|
||||
Reference in New Issue
Block a user