diff --git a/apps/web/src/lib/ui/data/FeedCard.svelte b/apps/web/src/lib/ui/data/FeedCard.svelte index cb0ec96..50eedc7 100644 --- a/apps/web/src/lib/ui/data/FeedCard.svelte +++ b/apps/web/src/lib/ui/data/FeedCard.svelte @@ -29,7 +29,11 @@
{title}
{#if description}
{description}
{/if} - {#if url}
{url}
{/if} + {#if url} +
{url}
+ {:else} +
Por seguridad, la URL no se muestra. Pulsa “Rotar” para generar una nueva.
+ {/if}
@@ -70,4 +74,9 @@ gap: 8px; flex-shrink: 0; } + .hint { + margin-top: 6px; + color: var(--color-text-muted); + font-size: 12px; + } diff --git a/apps/web/src/lib/ui/layout/AppShell.svelte b/apps/web/src/lib/ui/layout/AppShell.svelte index 324aca2..9cf18a8 100644 --- a/apps/web/src/lib/ui/layout/AppShell.svelte +++ b/apps/web/src/lib/ui/layout/AppShell.svelte @@ -9,6 +9,7 @@
diff --git a/apps/web/src/routes/api/integrations/feeds/+server.ts b/apps/web/src/routes/api/integrations/feeds/+server.ts index 5e14580..7ef69ad 100644 --- a/apps/web/src/routes/api/integrations/feeds/+server.ts +++ b/apps/web/src/routes/api/integrations/feeds/+server.ts @@ -46,14 +46,14 @@ export const GET: RequestHandler = async (event) => { })(); // Por grupo (B): autogenerar si falta - const groupFeeds: Array<{ groupId: string; url: string | null }> = []; + const groupFeeds: Array<{ groupId: string; groupName: string | null; url: string | null }> = []; for (const g of groups) { const ex = await findActiveToken('group', userId, g.id); if (ex) { - groupFeeds.push({ groupId: g.id, url: null }); + groupFeeds.push({ groupId: g.id, groupName: g.name ?? null, url: null }); } else { const created = await createCalendarTokenUrl('group', userId, g.id); - groupFeeds.push({ groupId: g.id, url: created.url }); + groupFeeds.push({ groupId: g.id, groupName: g.name ?? null, url: created.url }); } } diff --git a/apps/web/src/routes/app/integrations/+page.server.ts b/apps/web/src/routes/app/integrations/+page.server.ts new file mode 100644 index 0000000..b871c61 --- /dev/null +++ b/apps/web/src/routes/app/integrations/+page.server.ts @@ -0,0 +1,14 @@ +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ fetch }) => { + const res = await fetch('/api/integrations/feeds', { method: 'GET', headers: { 'cache-control': 'no-store' } }); + if (!res.ok) { + return { + personal: { url: null }, + aggregate: { url: null }, + groups: [] + }; + } + const data = await res.json(); + return data; +}; diff --git a/apps/web/src/routes/app/integrations/+page.svelte b/apps/web/src/routes/app/integrations/+page.svelte index 4430280..4394840 100644 --- a/apps/web/src/routes/app/integrations/+page.svelte +++ b/apps/web/src/routes/app/integrations/+page.svelte @@ -1,10 +1,55 @@ @@ -20,16 +65,32 @@ title="Mis tareas (con fecha)" description="Suscríbete a este feed en tu calendario para ver tus tareas con fecha de vencimiento." url={personalUrl} - on:rotate={() => {/* se conectará a POST /api/integrations/feeds/rotate */}} + rotating={!!rotating['personal']} + on:rotate={() => rotate('personal')} + /> + +

Feed multigrupo

+ rotate('aggregate')} />

Feeds por grupo (sin responsable)

- {#if groupFeeds.length === 0} + {#if groups.length === 0} No hay grupos disponibles todavía. Cuando los haya, verás aquí sus feeds ICS. {:else}
- {#each groupFeeds as g} - + {#each groups as g (g.groupId)} + rotate('group', g.groupId)} + /> {/each}
{/if} diff --git a/tests/web/app.integrations.page.test.ts b/tests/web/app.integrations.page.test.ts new file mode 100644 index 0000000..443663b --- /dev/null +++ b/tests/web/app.integrations.page.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect, afterAll } from 'bun:test'; +import Database from 'bun:sqlite'; +import { startWebServer } from './helpers/server'; +import { createTempDb } from './helpers/db'; + +async function sha256Hex(input: string): Promise { + const enc = new TextEncoder().encode(input); + const buf = await crypto.subtle.digest('SHA-256', enc); + const bytes = new Uint8Array(buf); + return Array.from(bytes) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); +} + +function toIsoSql(d = new Date()): string { + return d.toISOString().replace('T', ' ').replace('Z', ''); +} + +describe('Web page - /app/integrations', () => { + const PORT = 19124; + const BASE = `http://127.0.0.1:${PORT}`; + const USER = '34600123456'; + const GROUP = '123@g.us'; + const SID = 'sid-test-456'; + + const tmp = createTempDb(); + const db: any = tmp.db as Database; + + // Sembrar datos mínimos + db.exec(`INSERT OR IGNORE INTO users (id) VALUES ('${USER}')`); + db.exec(`INSERT OR IGNORE INTO groups (id, community_id, name, active) VALUES ('${GROUP}', 'comm1', 'Group Test', 1)`); + db.exec(`INSERT OR IGNORE INTO allowed_groups (group_id, status, discovered_at, updated_at) VALUES ('${GROUP}', 'allowed', '${toIsoSql()}', '${toIsoSql()}')`); + db.exec(`INSERT OR IGNORE INTO group_members (group_id, user_id, is_admin, is_active, first_seen_at, last_seen_at) VALUES ('${GROUP}', '${USER}', 0, 1, '${toIsoSql()}', '${toIsoSql()}')`); + + const sidHashPromise = sha256Hex(SID); + const serverPromise = startWebServer({ + port: PORT, + env: { + DB_PATH: tmp.path, + WEB_BASE_URL: BASE + } + }); + + let server: Awaited | null = null; + + afterAll(async () => { + try { await server?.stop(); } catch {} + try { tmp.cleanup(); } catch {} + }); + + it('GET /app/integrations: muestra URLs ICS (personal, grupo y multigrupo) cuando se autogeneran', async () => { + server = await serverPromise; + + const sidHash = await sidHashPromise; + // Insertar sesión + db.exec(` + INSERT OR REPLACE INTO web_sessions (id, user_id, session_hash, created_at, last_seen_at, expires_at) + VALUES ('sess-2', '${USER}', '${sidHash}', '${toIsoSql()}', '${toIsoSql()}', '${toIsoSql(new Date(Date.now() + 2 * 60 * 60 * 1000))}') + `); + + const res = await fetch(`${BASE}/app/integrations`, { + headers: { cookie: `sid=${SID}` } + }); + expect(res.status).toBe(200); + const html = await res.text(); + + // Debe incluir títulos y al menos una URL .ics + expect(html.includes('Integraciones')).toBe(true); + expect(html.includes('Mis tareas (con fecha)')).toBe(true); + expect(html.includes('Mis grupos (sin responsable)')).toBe(true); + expect(html.includes('/ics/personal/')).toBe(true); + expect(html.includes('/ics/group/')).toBe(true); + expect(html.includes('/ics/aggregate/')).toBe(true); + expect(html.includes('.ics')).toBe(true); + }); +});