pwa configuration lista

This commit is contained in:
2025-09-29 19:25:23 -06:00
parent f4621a0b70
commit 9a6b778f54
15 changed files with 14002 additions and 83 deletions

View File

@@ -1,6 +1,6 @@
<template>
<UApp>
<div class="min-h-screen bg-slate-950 text-slate-100">
<div class="brand-shell text-[#fef9f0]">
<NuxtRouteAnnouncer />
<NuxtLayout>
<NuxtPage />

View File

@@ -1,2 +1,69 @@
@import "tailwindcss";
@import "@nuxt/ui";
:root {
--brand-bg: #14100b;
--brand-surface: #1f180f;
--brand-border: #3a2a16;
--brand-primary: #e0c080;
--brand-primary-strong: #c08040;
--brand-accent: #ffe0a0;
--brand-text: #fef9f0;
--brand-text-muted: #d8c7a6;
}
html,
body {
background-color: var(--brand-bg);
color: var(--brand-text);
}
.brand-shell {
background: radial-gradient(circle at 20% 20%, rgba(255, 224, 160, 0.08), transparent 55%),
radial-gradient(circle at 80% 10%, rgba(192, 128, 64, 0.08), transparent 45%),
var(--brand-bg);
min-height: 100vh;
}
.brand-card {
background: linear-gradient(145deg, rgba(20, 16, 11, 0.95), rgba(31, 24, 15, 0.85));
border: 1px solid rgba(224, 192, 128, 0.12);
box-shadow: 0 12px 40px rgba(12, 8, 4, 0.55);
}
.brand-divider {
border-top: 1px solid rgba(224, 192, 128, 0.16);
}
.brand-chip {
background: rgba(255, 224, 160, 0.08);
border: 1px solid rgba(255, 224, 160, 0.18);
color: var(--brand-accent);
}
.brand-table thead {
background: linear-gradient(90deg, rgba(32, 24, 15, 0.95), rgba(24, 18, 11, 0.95));
}
.brand-table tbody tr {
transition: background 0.2s ease, transform 0.2s ease;
}
.brand-table tbody tr:hover {
background: rgba(255, 224, 160, 0.06);
}
.brand-pill {
background: rgba(224, 192, 128, 0.14);
border: 1px solid rgba(224, 192, 128, 0.28);
color: var(--brand-primary);
}
.brand-badge {
color: var(--brand-bg);
background: var(--brand-primary);
}
.brand-section-title {
color: var(--brand-primary);
}

View File

@@ -1,12 +1,32 @@
<template>
<div class="mx-auto flex max-w-6xl flex-col gap-6 px-4 py-10">
<UCard>
<div class="mx-auto flex max-w-7xl flex-col gap-10 px-4 py-14 text-[#fef9f0]">
<UCard class="brand-card border border-transparent backdrop-blur-sm">
<template #header>
<div class="flex flex-col gap-2">
<h1 class="text-2xl font-semibold">Constructor de consultas</h1>
<p class="text-sm text-slate-300">
Arma una solicitud a los endpoints del backend y visualiza los resultados en modo solo lectura.
</p>
<div class="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
<div class="flex items-center gap-5">
<img
src="/logo.png"
alt="Analítica Nucleo"
class="h-16 w-16 rounded-full border border-[#ffe0a0]/40 shadow-lg shadow-[#c08040]/25"
/>
<div class="space-y-1">
<h1 class="text-3xl font-semibold tracking-tight text-[var(--brand-text)]">
Analítica Nucleo Data Studio
</h1>
<p class="text-sm text-[var(--brand-text-muted)]">
Explora y valida tus tablas Supabase desde un único panel en modo lectura.
</p>
</div>
</div>
<div class="flex flex-wrap gap-2">
<span class="brand-pill inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs uppercase tracking-[0.18em]">
<span class="inline-block h-2 w-2 rounded-full bg-[#ffe0a0]"></span>
Solo lectura
</span>
<span class="brand-pill inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs uppercase tracking-[0.18em]">
Multi-fuente
</span>
</div>
</div>
</template>
@@ -59,168 +79,195 @@
/>
</UFormField>
<p v-if="queryState.error" class="text-sm text-red-300">{{ queryState.error }}</p>
<p v-else-if="queryState.encoded" class="text-xs text-slate-400">
Segmento codificado: <code class="rounded bg-slate-800 px-2 py-1">{{ queryState.encoded }}</code>
<p v-else-if="queryState.encoded" class="text-xs text-[var(--brand-text-muted)]">
Segmento codificado:
<code class="rounded bg-[#2a2014] px-2 py-1 text-[var(--brand-accent)]">{{ queryState.encoded }}</code>
</p>
<p class="text-xs text-slate-500">
Se codifica automáticamente en base64-url para construir la ruta <code>/api/data/{{ request.table || ':tabla' }}/{{
queryState.encoded || ':query'
}}</code>.
<p class="text-xs text-[var(--brand-text-muted)]">
Se codifica automáticamente en base64-url para construir la ruta
<code class="rounded bg-[#2a2014] px-2 py-1 text-[var(--brand-accent)]">
/api/data/{{ request.table || ':tabla' }}/{{ queryState.encoded || ':query' }}
</code>.
</p>
</div>
<UAlert color="primary" variant="soft">
<template #title>Solicitud generada</template>
<template #description>
<code class="break-all text-sm">GET {{ requestPreview }}</code>
</template>
</UAlert>
<div class="flex flex-col gap-3 rounded-lg border border-[#3a2a16] bg-[#1c140c] px-4 py-3 shadow-inner shadow-black/30">
<span class="text-xs font-semibold uppercase tracking-[0.28em] text-[var(--brand-text-muted)]">
Solicitud generada
</span>
<code class="break-all text-sm text-[var(--brand-accent)]">GET {{ requestPreview }}</code>
</div>
<div class="flex justify-end gap-2">
<UButton color="neutral" variant="soft" @click="resetForm" :disabled="loading">
<UButton
variant="soft"
:ui="{ base: 'bg-transparent border border-[#3a2a16] text-[var(--brand-text-muted)] hover:bg-[#2a2014] hover:border-[#c08040]/60' }"
@click="resetForm"
:disabled="loading"
>
Limpiar
</UButton>
<UButton type="submit" :loading="loading">
<UButton
type="submit"
:loading="loading"
:ui="{ base: 'bg-[#c08040] text-[#1b1209] border border-[#d99a56] hover:bg-[#d99a56] hover:border-[#f0c07c]' }"
>
Ejecutar consulta
</UButton>
</div>
</form>
</UCard>
<div v-if="errorMessage" class="rounded-lg border border-red-500 bg-red-500/10 p-4 text-sm text-red-200">
<div v-if="errorMessage" class="rounded-lg border border-red-500/40 bg-red-500/18 p-4 text-sm text-red-200">
{{ errorMessage }}
</div>
<section v-if="hasMetadataResponse" class="flex flex-col gap-4">
<div v-if="metadataCollection.length" class="grid gap-4 md:grid-cols-2">
<UCard v-for="meta in metadataCollection" :key="meta.table">
<section v-if="hasMetadataResponse" class="flex flex-col gap-5">
<div v-if="metadataCollection.length" class="grid gap-5 md:grid-cols-2">
<UCard v-for="meta in metadataCollection" :key="meta.table" class="brand-card border border-transparent">
<template #header>
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold">Tabla {{ meta.table }}</h2>
<UBadge color="primary">{{ meta.rowCount }} registros</UBadge>
<h2 class="text-lg font-semibold brand-section-title">Tabla {{ meta.table }}</h2>
<span class="brand-badge inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-semibold tracking-wide">
{{ meta.rowCount }} registros
</span>
</div>
</template>
<dl class="grid grid-cols-2 gap-2 text-sm">
<dl class="grid grid-cols-2 gap-3 text-sm text-[var(--brand-text-muted)]">
<div>
<dt class="text-slate-400">Clave primaria</dt>
<dd class="font-medium">{{ meta.primaryKey }}</dd>
<dt class="uppercase tracking-wide text-xs">Clave primaria</dt>
<dd class="font-medium text-[var(--brand-text)]">{{ meta.primaryKey }}</dd>
</div>
<div>
<dt class="text-slate-400">Tamaño aprox.</dt>
<dd class="font-medium">{{ formatSize(meta.approxSizeBytes) }}</dd>
<dt class="uppercase tracking-wide text-xs">Tamaño aprox.</dt>
<dd class="font-medium text-[var(--brand-text)]">{{ formatSize(meta.approxSizeBytes) }}</dd>
</div>
<div>
<dt class="text-slate-400">Creación desde</dt>
<dd class="font-medium">{{ formatDate(meta.createdAtRange?.from) }}</dd>
<dt class="uppercase tracking-wide text-xs">Creación desde</dt>
<dd class="font-medium text-[var(--brand-text)]">{{ formatDate(meta.createdAtRange?.from) }}</dd>
</div>
<div>
<dt class="text-slate-400">Creación hasta</dt>
<dd class="font-medium">{{ formatDate(meta.createdAtRange?.to) }}</dd>
<dt class="uppercase tracking-wide text-xs">Creación hasta</dt>
<dd class="font-medium text-[var(--brand-text)]">{{ formatDate(meta.createdAtRange?.to) }}</dd>
</div>
</dl>
<template #footer>
<div class="text-xs text-slate-400">
<div class="brand-divider pt-3 text-xs text-[var(--brand-text-muted)]">
Columnas detectadas: {{ meta.columns.join(', ') }}
</div>
</template>
</UCard>
</div>
<div v-else-if="activeMetadata" class="grid gap-4 md:grid-cols-2">
<UCard>
<div v-else-if="activeMetadata" class="grid gap-5 md:grid-cols-2">
<UCard class="brand-card border border-transparent">
<template #header>
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold">Resumen de {{ activeMetadata.table }}</h2>
<UBadge color="primary">{{ activeMetadata.rowCount }} registros</UBadge>
<h2 class="text-lg font-semibold brand-section-title">Resumen de {{ activeMetadata.table }}</h2>
<span class="brand-badge inline-flex items-center gap-1 rounded-full px-3 py-1 text-xs font-semibold tracking-wide">
{{ activeMetadata.rowCount }} registros
</span>
</div>
</template>
<dl class="grid grid-cols-2 gap-2 text-sm">
<dl class="grid grid-cols-2 gap-3 text-sm text-[var(--brand-text-muted)]">
<div>
<dt class="text-slate-400">Clave primaria</dt>
<dd class="font-medium">{{ activeMetadata.primaryKey }}</dd>
<dt class="uppercase tracking-wide text-xs">Clave primaria</dt>
<dd class="font-medium text-[var(--brand-text)]">{{ activeMetadata.primaryKey }}</dd>
</div>
<div>
<dt class="text-slate-400">Última consulta</dt>
<dd class="font-medium">{{ formatDate(activeMetadata.lastRefreshed) }}</dd>
<dt class="uppercase tracking-wide text-xs">Última consulta</dt>
<dd class="font-medium text-[var(--brand-text)]">{{ formatDate(activeMetadata.lastRefreshed) }}</dd>
</div>
<div>
<dt class="text-slate-400">Tamaño aprox.</dt>
<dd class="font-medium">{{ formatSize(activeMetadata.approxSizeBytes) }}</dd>
<dt class="uppercase tracking-wide text-xs">Tamaño aprox.</dt>
<dd class="font-medium text-[var(--brand-text)]">{{ formatSize(activeMetadata.approxSizeBytes) }}</dd>
</div>
<div>
<dt class="text-slate-400">Rango de creación</dt>
<dd class="font-medium">
<dt class="uppercase tracking-wide text-xs">Rango de creación</dt>
<dd class="font-medium text-[var(--brand-text)]">
{{ formatDate(activeMetadata.createdAtRange?.from) }} {{ formatDate(activeMetadata.createdAtRange?.to) }}
</dd>
</div>
</dl>
<template #footer>
<div class="text-xs text-slate-400">
<div class="brand-divider pt-3 text-xs text-[var(--brand-text-muted)]">
Columnas detectadas: {{ activeMetadata.columns.join(', ') }}
</div>
</template>
</UCard>
<UCard v-if="activeMetadata.sampleRow">
<UCard v-if="activeMetadata.sampleRow" class="brand-card border border-transparent">
<template #header>
<h2 class="text-lg font-semibold">Registro de ejemplo</h2>
<h2 class="text-lg font-semibold brand-section-title">Registro de ejemplo</h2>
</template>
<pre class="overflow-auto rounded bg-slate-900 p-4 text-sm">{{ formatSample(activeMetadata.sampleRow) }}</pre>
<pre class="overflow-auto rounded bg-[#22180f] p-4 text-sm text-[var(--brand-text-muted)]">
{{ formatSample(activeMetadata.sampleRow) }}
</pre>
</UCard>
</div>
<UCard v-if="metadataRecord">
<UCard v-if="metadataRecord" class="brand-card border border-transparent">
<template #header>
<h2 class="text-lg font-semibold">Metadata del registro {{ metadataRecord.id }}</h2>
<h2 class="text-lg font-semibold brand-section-title">Metadata del registro {{ metadataRecord.id }}</h2>
</template>
<pre class="overflow-auto rounded bg-slate-900 p-4 text-sm">{{ formatSample(metadataRecord.metadata) }}</pre>
<pre class="overflow-auto rounded bg-[#22180f] p-4 text-sm text-[var(--brand-text-muted)]">
{{ formatSample(metadataRecord.metadata) }}
</pre>
</UCard>
</section>
<UCard v-if="request.type === 'data' || hasDataResponse">
<UCard v-if="request.type === 'data' || hasDataResponse" class="brand-card border border-transparent">
<template #header>
<div class="flex flex-wrap items-center justify-between gap-3">
<h2 class="text-lg font-semibold">Datos</h2>
<div class="flex flex-wrap gap-2 text-xs text-slate-300">
<h2 class="text-lg font-semibold brand-section-title">Datos</h2>
<div class="flex flex-wrap gap-2 text-xs text-[var(--brand-text-muted)]">
<template v-if="dataStats">
<UBadge color="primary">
<span class="brand-pill inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs">
{{ dataStats.table }}: {{ dataStats.count }} registros (límite {{ dataStats.limit ?? 's/d' }})
</UBadge>
</span>
</template>
<template v-else-if="dataStatsCollection.length">
<UBadge v-for="item in dataStatsCollection" :key="item.table" color="neutral">
<span
v-for="item in dataStatsCollection"
:key="item.table"
class="brand-pill inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs"
>
{{ item.table }}: {{ item.count }} registros (límite {{ item.limit ?? 's/d' }})
</UBadge>
</span>
</template>
<UBadge v-else-if="tableData.length" color="neutral">
<span v-else-if="tableData.length" class="brand-pill inline-flex items-center gap-2 rounded-full px-3 py-1 text-xs">
{{ tableData.length }} registros visibles
</UBadge>
</span>
</div>
</div>
</template>
<div v-if="loading" class="flex items-center justify-center py-10">
<span class="inline-flex h-8 w-8 animate-spin rounded-full border-2 border-slate-400 border-t-transparent align-middle" aria-hidden="true" />
<span class="sr-only">Cargando</span>
<div v-if="loading" class="flex items-center justify-center gap-3 py-10 text-[var(--brand-text-muted)]">
<span class="inline-flex h-8 w-8 animate-spin rounded-full border-2 border-[#c08040] border-t-transparent align-middle" aria-hidden="true" />
<span class="text-sm uppercase tracking-[0.3em]">Procesando</span>
</div>
<div v-else-if="!hasDataResponse" class="py-10 text-center text-sm text-slate-400">
<div v-else-if="!hasDataResponse" class="py-10 text-center text-sm text-[var(--brand-text-muted)]">
Ejecuta una consulta de datos para ver resultados aquí.
</div>
<div v-else-if="tableData.length === 0" class="py-10 text-center text-sm text-slate-400">
<div v-else-if="tableData.length === 0" class="py-10 text-center text-sm text-[var(--brand-text-muted)]">
No se encontraron registros para los criterios seleccionados.
</div>
<div v-else class="overflow-auto">
<table class="min-w-full divide-y divide-slate-800 text-sm">
<thead class="bg-slate-900/60">
<table class="brand-table min-w-full divide-y divide-[#3a2a16]/60 text-sm">
<thead>
<tr>
<th v-for="column in visibleColumns" :key="column" class="px-4 py-2 text-left font-semibold">
<th
v-for="column in visibleColumns"
:key="column"
class="px-4 py-3 text-left text-xs font-semibold uppercase tracking-[0.18em] text-[var(--brand-text-muted)]"
>
{{ column }}
</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800">
<tr v-for="(row, index) in tableData" :key="index" class="hover:bg-slate-900/40">
<td v-for="column in visibleColumns" :key="column" class="px-4 py-2">
<tbody class="brand-table divide-y divide-[#3a2a16]/40">
<tr v-for="(row, index) in tableData" :key="index" class="transition-colors">
<td v-for="column in visibleColumns" :key="column" class="px-4 py-2 text-sm text-[var(--brand-text-muted)]">
{{ formatCell(row[column]) }}
</td>
</tr>
@@ -229,11 +276,13 @@
</div>
</UCard>
<UCard v-if="rawResponse">
<UCard v-if="rawResponse" class="brand-card border border-transparent">
<template #header>
<h2 class="text-lg font-semibold">Respuesta cruda (JSON)</h2>
<h2 class="text-lg font-semibold brand-section-title">Respuesta cruda (JSON)</h2>
</template>
<pre class="max-h-96 overflow-auto rounded bg-slate-900 p-4 text-sm">{{ formatSample(rawResponse) }}</pre>
<pre class="max-h-96 overflow-auto rounded bg-[#22180f] p-4 text-sm text-[var(--brand-text-muted)]">
{{ formatSample(rawResponse) }}
</pre>
</UCard>
</div>
</template>

View File

@@ -4,7 +4,103 @@ export default defineNuxtConfig({
compatibilityDate: '2025-07-15',
devtools: { enabled: true },
css: ['~/assets/css/main.css'],
modules: ['@nuxt/image', '@nuxt/ui', '@nuxt/test-utils'],
modules: ['@nuxt/image', '@nuxt/ui', '@nuxt/test-utils', '@vite-pwa/nuxt'],
app: {
head: {
link: [
{ rel: 'icon', type: 'image/png', href: '/icons/icon-192.png' },
{ rel: 'apple-touch-icon', sizes: '192x192', href: '/icons/icon-192.png' },
{ rel: 'manifest', href: '/manifest.webmanifest' }
],
meta: [
{ name: 'theme-color', content: '#14100b' },
{ name: 'apple-mobile-web-app-capable', content: 'yes' },
{ name: 'mobile-web-app-capable', content: 'yes' },
{ name: 'apple-mobile-web-app-status-bar-style', content: 'black-translucent' }
]
}
},
pwa: {
registerType: 'autoUpdate',
strategies: 'generateSW',
manifestFilename: 'manifest.webmanifest',
manifest: {
name: 'Analítica Núcleo Data Studio',
short_name: 'Analítica',
description: 'Explora y valida tus tablas Supabase desde un único panel en modo lectura.',
start_url: '/',
scope: '/',
display: 'standalone',
background_color: '#1b1209',
theme_color: '#c08040',
icons: [
{ src: '/icons/icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/icons/icon-512.png', sizes: '512x512', type: 'image/png' },
{ src: '/icons/icon-192-maskable.png', sizes: '192x192', type: 'image/png', purpose: 'maskable' },
{ src: '/icons/icon-512-maskable.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
{ src: '/icons/icon-192-black.png', sizes: '192x192', type: 'image/png' },
{ src: '/icons/icon-512-black.png', sizes: '512x512', type: 'image/png' }
],
screenshots: [
{
src: '/screenshots/screenshot-desktop.png',
sizes: '2048x1041',
type: 'image/png',
form_factor: 'wide',
label: 'Dashboard en escritorio'
},
{
src: '/screenshots/screenshot-mobile.png',
sizes: '774x1459',
type: 'image/png',
form_factor: 'narrow',
label: 'Vista móvil'
}
]
},
workbox: {
globPatterns: ['**/*.{js,css,html,png,svg,webp,ico,json,woff2}'],
navigateFallback: '/',
maximumFileSizeToCacheInBytes: 4 * 1024 * 1024,
runtimeCaching: [
{
urlPattern: /^https:\/\/szesytydotpnuiuwybwb\.supabase\.co\//,
handler: 'NetworkFirst',
method: 'GET',
options: {
cacheName: 'supabase-data',
networkTimeoutSeconds: 10,
expiration: {
maxEntries: 60,
maxAgeSeconds: 3600
},
cacheableResponse: {
statuses: [0, 200]
}
}
}
]
},
client: {
installPrompt: true,
periodicSyncForUpdates: 3600
},
devOptions: {
enabled: process.env.NODE_ENV === 'development',
type: 'module'
},
includeAssets: [
'favicon.ico',
'icons/icon-192.png',
'icons/icon-512.png',
'icons/icon-192-maskable.png',
'icons/icon-512-maskable.png',
'icons/icon-192-black.png',
'icons/icon-512-black.png',
'screenshots/screenshot-desktop.png',
'screenshots/screenshot-mobile.png'
]
},
runtimeConfig: {
supabase: {
url: process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL,
@@ -12,4 +108,4 @@ export default defineNuxtConfig({
process.env.SUPABASE_SERVICE_ROLE_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
}
}
})
})

View File

@@ -12,6 +12,7 @@
"dependencies": {
"@nuxt/image": "^1.11.0",
"@nuxt/test-utils": "^3.19.2",
"@vite-pwa/nuxt": "^0.9.1",
"@nuxt/ui": "^4.0.0",
"@supabase/supabase-js": "^2.48.0",
"nuxt": "^4.1.2",

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

BIN
nuxt4-app/public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

13706
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff