diff --git a/apps/web/src/routes/api/integrations/feeds/+server.ts b/apps/web/src/routes/api/integrations/feeds/+server.ts index 2a3f9c5..8cbcd2f 100644 --- a/apps/web/src/routes/api/integrations/feeds/+server.ts +++ b/apps/web/src/routes/api/integrations/feeds/+server.ts @@ -20,7 +20,7 @@ export const GET: RequestHandler = async (event) => { ON gm.group_id = g.id AND gm.user_id = ? AND gm.is_active = 1 INNER JOIN allowed_groups ag ON ag.group_id = g.id AND ag.status = 'allowed' - WHERE COALESCE(g.active, 1) = 1 + WHERE COALESCE(g.active, 1) = 1 AND COALESCE(g.is_community, 0) = 0 AND COALESCE(g.archived, 0) = 0 ORDER BY (g.name IS NULL) ASC, g.name ASC, g.id ASC` ) .all(userId) as Array<{ id: string; name: string | null }>; diff --git a/apps/web/src/routes/api/integrations/feeds/rotate/+server.ts b/apps/web/src/routes/api/integrations/feeds/rotate/+server.ts index 267ea0e..f8f5c06 100644 --- a/apps/web/src/routes/api/integrations/feeds/rotate/+server.ts +++ b/apps/web/src/routes/api/integrations/feeds/rotate/+server.ts @@ -43,7 +43,7 @@ export const POST: RequestHandler = async (event) => { ON gm.group_id = g.id AND gm.user_id = ? AND gm.is_active = 1 INNER JOIN allowed_groups ag ON ag.group_id = g.id AND ag.status = 'allowed' - WHERE g.id = ? AND COALESCE(g.active, 1) = 1 + WHERE g.id = ? AND COALESCE(g.active, 1) = 1 AND COALESCE(g.is_community, 0) = 0 AND COALESCE(g.archived, 0) = 0 LIMIT 1` ) .get(userId, groupId) as any; diff --git a/apps/web/src/routes/ics/aggregate/[token].ics/+server.ts b/apps/web/src/routes/ics/aggregate/[token].ics/+server.ts index 04327b8..cbab64d 100644 --- a/apps/web/src/routes/ics/aggregate/[token].ics/+server.ts +++ b/apps/web/src/routes/ics/aggregate/[token].ics/+server.ts @@ -49,7 +49,7 @@ export const GET: RequestHandler = async ({ params, request }) => { .prepare( `SELECT t.id, t.description, t.due_date, g.name AS group_name FROM tasks t - INNER JOIN groups g ON g.id = t.group_id AND COALESCE(g.active,1)=1 AND COALESCE(g.archived,0)=0 + INNER JOIN groups g ON g.id = t.group_id AND COALESCE(g.active,1)=1 AND COALESCE(g.archived,0)=0 AND COALESCE(g.is_community,0)=0 INNER JOIN group_members gm ON gm.group_id = t.group_id AND gm.user_id = ? AND gm.is_active = 1 INNER JOIN allowed_groups ag ON ag.group_id = t.group_id AND ag.status = 'allowed' WHERE COALESCE(t.completed, 0) = 0 diff --git a/apps/web/src/routes/ics/group/[token].ics/+server.ts b/apps/web/src/routes/ics/group/[token].ics/+server.ts index b3c50f5..4c2b78b 100644 --- a/apps/web/src/routes/ics/group/[token].ics/+server.ts +++ b/apps/web/src/routes/ics/group/[token].ics/+server.ts @@ -42,9 +42,9 @@ export const GET: RequestHandler = async ({ params, request }) => { // Validar estado del grupo (activo y no archivado); en caso contrario, tratar como feed caducado const gRow = db - .prepare(`SELECT COALESCE(active,1) as active, COALESCE(archived,0) as archived FROM groups WHERE id = ?`) + .prepare(`SELECT COALESCE(active,1) as active, COALESCE(archived,0) as archived, COALESCE(is_community,0) as is_community FROM groups WHERE id = ?`) .get(row.group_id) as any; - if (!gRow || Number(gRow.active || 0) !== 1 || Number(gRow.archived || 0) === 1) { + if (!gRow || Number(gRow.active || 0) !== 1 || Number(gRow.archived || 0) === 1 || Number(gRow.is_community || 0) === 1) { db.prepare(`UPDATE calendar_tokens SET last_used_at = ? WHERE id = ?`).run(toIsoSql(), row.id); return new Response('Gone', { status: 410 }); } diff --git a/apps/web/src/routes/ics/personal/[token].ics/+server.ts b/apps/web/src/routes/ics/personal/[token].ics/+server.ts index 0619756..539f99e 100644 --- a/apps/web/src/routes/ics/personal/[token].ics/+server.ts +++ b/apps/web/src/routes/ics/personal/[token].ics/+server.ts @@ -56,7 +56,7 @@ export const GET: RequestHandler = async ({ params, request }) => { AND t.due_date IS NOT NULL AND t.due_date >= ? AND t.due_date <= ? AND EXISTS (SELECT 1 FROM task_assignments a WHERE a.task_id = t.id AND a.user_id = ?) - AND (t.group_id IS NULL OR (gm.user_id IS NOT NULL AND ag.group_id IS NOT NULL AND COALESCE(g.active,1)=1)) + AND (t.group_id IS NULL OR (gm.user_id IS NOT NULL AND ag.group_id IS NOT NULL AND COALESCE(g.active,1)=1 AND COALESCE(g.is_community,0)=0 AND COALESCE(g.archived,0)=0)) ORDER BY t.due_date ASC, t.id ASC` ) .all(row.user_id, startYmd, endYmd, row.user_id) as Array<{ id: number; description: string; due_date: string; group_name: string | null }>; diff --git a/src/db/migrations/index.ts b/src/db/migrations/index.ts index 45a3ac4..1f44184 100644 --- a/src/db/migrations/index.ts +++ b/src/db/migrations/index.ts @@ -436,5 +436,22 @@ export const migrations: Migration[] = [ END; `); } + }, + { + version: 16, + name: 'groups-is-community', + checksum: 'v16-groups-is-community-2025-10-19', + up: (db: Database) => { + try { + const cols = db.query(`PRAGMA table_info(groups)`).all() as any[]; + const hasCol = Array.isArray(cols) && cols.some((c: any) => String(c.name) === 'is_community'); + if (!hasCol) { + db.exec(`ALTER TABLE groups ADD COLUMN is_community BOOLEAN NOT NULL DEFAULT 0;`); + } + } catch {} + try { + db.exec(`CREATE INDEX IF NOT EXISTS idx_groups_is_community ON groups (is_community);`); + } catch {} + } } ]; diff --git a/src/services/group-sync.ts b/src/services/group-sync.ts index 17690bd..1578d09 100644 --- a/src/services/group-sync.ts +++ b/src/services/group-sync.ts @@ -87,30 +87,30 @@ export class GroupSyncService { console.log('ℹ️ Grupos crudos de la API:', JSON.stringify(groups, null, 2)); console.log('ℹ️ Sin filtrar por comunidad (modo multicomunidad). Total grupos:', groups.length); - const dbGroupsBefore = this.dbInstance.prepare('SELECT id, active, COALESCE(archived,0) AS archived, name FROM groups').all(); + const dbGroupsBefore = this.dbInstance.prepare('SELECT id, active, COALESCE(archived,0) AS archived, COALESCE(is_community,0) AS is_community, name FROM groups').all(); console.log('ℹ️ Grupos en DB antes de upsert:', dbGroupsBefore); const result = await this.upsertGroups(groups); - const dbGroupsAfter = this.dbInstance.prepare('SELECT id, active, COALESCE(archived,0) AS archived, name FROM groups').all(); + const dbGroupsAfter = this.dbInstance.prepare('SELECT id, active, COALESCE(archived,0) AS archived, COALESCE(is_community,0) AS is_community, name FROM groups').all(); console.log('ℹ️ Grupos en DB después de upsert:', dbGroupsAfter); // Detectar grupos que pasaron de activos a inactivos (y no están archivados) en este sync try { - const beforeMap = new Map(); + const beforeMap = new Map(); for (const r of dbGroupsBefore as any[]) { - beforeMap.set(String(r.id), { active: Number(r.active || 0), archived: Number(r.archived || 0), name: r.name ? String(r.name) : null }); + beforeMap.set(String(r.id), { active: Number(r.active || 0), archived: Number(r.archived || 0), is_community: Number((r as any).is_community || 0), name: r.name ? String(r.name) : null }); } - const afterMap = new Map(); + const afterMap = new Map(); for (const r of dbGroupsAfter as any[]) { - afterMap.set(String(r.id), { active: Number(r.active || 0), archived: Number(r.archived || 0), name: r.name ? String(r.name) : null }); + afterMap.set(String(r.id), { active: Number(r.active || 0), archived: Number(r.archived || 0), is_community: Number((r as any).is_community || 0), name: r.name ? String(r.name) : null }); } const newlyDeactivated: Array<{ id: string; name: string | null }> = []; for (const [id, b] of beforeMap.entries()) { const a = afterMap.get(id); if (!a) continue; - if (Number(b.active) === 1 && Number(a.active) === 0 && Number(a.archived) === 0) { + if (Number(b.active) === 1 && Number(a.active) === 0 && Number(a.archived) === 0 && Number(a.is_community || 0) === 0 && Number(b.is_community || 0) === 0) { newlyDeactivated.push({ id, name: a.name ?? b.name ?? null }); } } @@ -274,7 +274,7 @@ export class GroupSyncService { } private static cacheActiveGroups(): void { - const groups = this.dbInstance.prepare('SELECT id, name FROM groups WHERE active = TRUE').all(); + const groups = this.dbInstance.prepare('SELECT id, name FROM groups WHERE active = TRUE AND COALESCE(is_community,0) = 0 AND COALESCE(archived,0) = 0').all(); this.activeGroupsCache.clear(); for (const group of groups) { this.activeGroupsCache.set(group.id, group.name); @@ -288,6 +288,9 @@ export class GroupSyncService { if (!Array.isArray(allGroups) || allGroups.length === 0) return 0; const nameById = new Map(); for (const g of allGroups) { + // Omitir grupos "comunidad/announce" no operativos + const isComm = !!((g as any)?.isCommunity || (g as any)?.is_community || (g as any)?.isCommunityAnnounce || (g as any)?.is_community_announce); + if (isComm) continue; if (!g?.id) continue; const name = String(g.subject || '').trim(); if (!name) continue; @@ -324,7 +327,7 @@ export class GroupSyncService { } private static getActiveGroupsCount(): number { - const result = this.dbInstance.prepare('SELECT COUNT(*) as count FROM groups WHERE active = TRUE').get(); + const result = this.dbInstance.prepare('SELECT COUNT(*) as count FROM groups WHERE active = TRUE AND COALESCE(is_community,0) = 0 AND COALESCE(archived,0) = 0').get(); return result?.count || 0; } @@ -412,21 +415,49 @@ export class GroupSyncService { const existing = this.dbInstance.prepare('SELECT 1 FROM groups WHERE id = ?').get(group.id); console.log('Checking group:', group.id, 'exists:', !!existing); + const isCommunityFlag = !!(((group as any)?.isCommunity) || ((group as any)?.is_community) || ((group as any)?.isCommunityAnnounce) || ((group as any)?.is_community_announce)); + if (existing) { const updateResult = this.dbInstance.prepare( - 'UPDATE groups SET name = ?, community_id = COALESCE(?, community_id), active = TRUE, last_verified = CURRENT_TIMESTAMP WHERE id = ?' - ).run(group.subject, group.linkedParent || null, group.id); + 'UPDATE groups SET name = ?, community_id = COALESCE(?, community_id), is_community = ?, active = TRUE, last_verified = CURRENT_TIMESTAMP WHERE id = ?' + ).run(group.subject, group.linkedParent || null, isCommunityFlag ? 1 : 0, group.id); console.log('Updated group:', group.id, 'result:', updateResult); updated++; } else { const insertResult = this.dbInstance.prepare( - 'INSERT INTO groups (id, community_id, name, active) VALUES (?, ?, ?, TRUE)' - ).run(group.id, (group.linkedParent ?? ''), group.subject); + 'INSERT INTO groups (id, community_id, name, active, is_community) VALUES (?, ?, ?, TRUE, ?)' + ).run(group.id, (group.linkedParent ?? ''), group.subject, isCommunityFlag ? 1 : 0); console.log('Added group:', group.id, 'result:', insertResult); added++; } - // Propagar subject como label a allowed_groups (no degrada estado; actualiza label si cambia) - try { (AllowedGroups as any).dbInstance = this.dbInstance; AllowedGroups.upsertPending(group.id, group.subject, null); } catch {} + // Propagar subject a allowed_groups: + // - Si es grupo "comunidad/announce", bloquearlo. + // - En caso contrario, upsert pending y label. + try { + (AllowedGroups as any).dbInstance = this.dbInstance; + if (isCommunityFlag) { + AllowedGroups.setStatus(group.id, 'blocked', group.subject); + } else { + AllowedGroups.upsertPending(group.id, group.subject, null); + } + } catch {} + // Si es grupo de comunidad, limpiar residuos: revocar tokens y desactivar membresías + if (isCommunityFlag) { + try { + this.dbInstance.prepare(` + UPDATE calendar_tokens + SET revoked_at = strftime('%Y-%m-%d %H:%M:%f','now') + WHERE group_id = ? AND revoked_at IS NULL + `).run(group.id); + } catch {} + try { + this.dbInstance.prepare(` + UPDATE group_members + SET is_active = 0 + WHERE group_id = ? AND is_active = 1 + `).run(group.id); + } catch {} + } } return { added, updated }; @@ -1014,6 +1045,8 @@ export class GroupSyncService { FROM group_members gm JOIN groups g ON g.id = gm.group_id WHERE gm.user_id = ? AND gm.is_active = 1 AND g.active = 1 + AND COALESCE(g.is_community,0) = 0 + AND COALESCE(g.archived,0) = 0 `).all(userId) as any[]; const set = new Set(); for (const r of rows) { diff --git a/src/tasks/service.ts b/src/tasks/service.ts index 8c3cca3..0a87a88 100644 --- a/src/tasks/service.ts +++ b/src/tasks/service.ts @@ -78,7 +78,7 @@ export class TaskService { } catch {} if (groupIdToInsert) { - const exists = this.dbInstance.prepare(`SELECT 1 FROM groups WHERE id = ?`).get(groupIdToInsert); + const exists = this.dbInstance.prepare(`SELECT 1 FROM groups WHERE id = ? AND COALESCE(is_community,0) = 0`).get(groupIdToInsert); if (!exists) { groupIdToInsert = null; } @@ -606,6 +606,7 @@ export class TaskService { WHERE g2.id = t.group_id AND COALESCE(g2.active,1)=1 AND COALESCE(g2.archived,0)=0 + AND COALESCE(g2.is_community,0)=0 )) ORDER BY CASE WHEN t.due_date IS NULL THEN 1 ELSE 0 END, @@ -636,6 +637,7 @@ export class TaskService { WHERE g2.id = t.group_id AND COALESCE(g2.active,1)=1 AND COALESCE(g2.archived,0)=0 + AND COALESCE(g2.is_community,0)=0 )) `) .get() as any;