diff --git a/src/routes/(app)/companies/[companyId]/links/+page.server.ts b/src/routes/(app)/companies/[companyId]/links/+page.server.ts index 6e32398..876ce0b 100644 --- a/src/routes/(app)/companies/[companyId]/links/+page.server.ts +++ b/src/routes/(app)/companies/[companyId]/links/+page.server.ts @@ -5,8 +5,39 @@ import { companyLinks, userCompanyLinks, users } from '$lib/server/db/schema.js' import { requireCompanyRoleAny } from '$lib/server/authorization.js'; import { logCompanyEvent } from '$lib/server/audit.js'; import { validateLinkUrl, InvalidLinkUrlError } from '$lib/server/links/validate.js'; +import { fetchFavicon } from '$lib/server/favicons/index.js'; import { and, asc, eq, isNull, sql } from 'drizzle-orm'; +function scheduleCompanyFavicon(linkId: string, url: string): void { + void (async () => { + try { + const result = await fetchFavicon(url); + if (!result) return; + await db + .update(companyLinks) + .set({ faviconUrl: result.dataUrl, faviconFetchedAt: result.fetchedAt }) + .where(eq(companyLinks.id, linkId)); + } catch (err) { + console.error('[favicon] company link fetch failed', err); + } + })(); +} + +function schedulePersonalFavicon(linkId: string, url: string): void { + void (async () => { + try { + const result = await fetchFavicon(url); + if (!result) return; + await db + .update(userCompanyLinks) + .set({ faviconUrl: result.dataUrl, faviconFetchedAt: result.fetchedAt }) + .where(eq(userCompanyLinks.id, linkId)); + } catch (err) { + console.error('[favicon] personal link fetch failed', err); + } + })(); +} + const CATEGORIES = [ 'internal_tool', 'communication', @@ -186,6 +217,8 @@ export const actions: Actions = { { linkId: inserted.id, category, url: validUrl } ); + scheduleCompanyFavicon(inserted.id, validUrl); + return { success: true, action: 'addCompanyLink' }; }, @@ -244,6 +277,8 @@ export const actions: Actions = { { linkId: id, category } ); + if (urlChanged) scheduleCompanyFavicon(id, validUrl); + return { success: true, action: 'updateCompanyLink' }; }, @@ -336,16 +371,21 @@ export const actions: Actions = { category }); - await db.insert(userCompanyLinks).values({ - userId: user.id, - companyId: params.companyId, - category, - customLabel, - title, - url: validUrl, - description, - sortOrder - }); + const [inserted] = await db + .insert(userCompanyLinks) + .values({ + userId: user.id, + companyId: params.companyId, + category, + customLabel, + title, + url: validUrl, + description, + sortOrder + }) + .returning({ id: userCompanyLinks.id }); + + schedulePersonalFavicon(inserted.id, validUrl); return { success: true, action: 'addPersonalLink' }; }, @@ -404,6 +444,8 @@ export const actions: Actions = { }) .where(eq(userCompanyLinks.id, id)); + if (urlChanged) schedulePersonalFavicon(id, validUrl); + return { success: true, action: 'updatePersonalLink' }; }, @@ -465,5 +507,82 @@ export const actions: Actions = { }); return { success: true, action: 'reorderPersonalLinks' }; + }, + + refreshCompanyFavicon: async ({ request, locals, params }) => { + await requireCompanyRoleAny(locals, params.companyId, ['admin', 'manager']); + const fd = await request.formData(); + const id = trimOrNull(fd.get('id')); + if (!id) return fail(400, { action: 'refreshCompanyFavicon', error: 'Link id is required' }); + + const [existing] = await db + .select({ id: companyLinks.id, url: companyLinks.url }) + .from(companyLinks) + .where( + and( + eq(companyLinks.id, id), + eq(companyLinks.companyId, params.companyId), + isNull(companyLinks.deletedAt) + ) + ) + .limit(1); + if (!existing) error(404, 'Link not found'); + + const result = await fetchFavicon(existing.url); + await db + .update(companyLinks) + .set({ + faviconUrl: result?.dataUrl ?? null, + faviconFetchedAt: result?.fetchedAt ?? new Date() + }) + .where(eq(companyLinks.id, id)); + + return { + success: true, + action: 'refreshCompanyFavicon', + faviconFound: Boolean(result) + }; + }, + + refreshPersonalFavicon: async ({ request, locals, params }) => { + const { user } = await requireCompanyRoleAny(locals, params.companyId, [ + 'admin', + 'manager', + 'user', + 'viewer', + 'hr', + 'accountant' + ]); + const fd = await request.formData(); + const id = trimOrNull(fd.get('id')); + if (!id) return fail(400, { action: 'refreshPersonalFavicon', error: 'Link id is required' }); + + const [existing] = await db + .select({ id: userCompanyLinks.id, url: userCompanyLinks.url }) + .from(userCompanyLinks) + .where( + and( + eq(userCompanyLinks.id, id), + eq(userCompanyLinks.userId, user.id), + eq(userCompanyLinks.companyId, params.companyId) + ) + ) + .limit(1); + if (!existing) error(404, 'Link not found'); + + const result = await fetchFavicon(existing.url); + await db + .update(userCompanyLinks) + .set({ + faviconUrl: result?.dataUrl ?? null, + faviconFetchedAt: result?.fetchedAt ?? new Date() + }) + .where(eq(userCompanyLinks.id, id)); + + return { + success: true, + action: 'refreshPersonalFavicon', + faviconFound: Boolean(result) + }; } }; diff --git a/src/routes/(app)/companies/[companyId]/links/+page.svelte b/src/routes/(app)/companies/[companyId]/links/+page.svelte index 3eea4cb..e08f796 100644 --- a/src/routes/(app)/companies/[companyId]/links/+page.svelte +++ b/src/routes/(app)/companies/[companyId]/links/+page.svelte @@ -69,6 +69,7 @@ canEdit: boolean; editAction: string; deleteAction: string; + refreshAction: string; emptyMessage: string; }; @@ -98,6 +99,7 @@ canEdit: data.canManage, editAction: '?/updateCompanyLink', deleteAction: '?/deleteCompanyLink', + refreshAction: '?/refreshCompanyFavicon', emptyMessage: data.canManage ? 'No company links yet. Click "+ New Link" to add the first one.' : 'No company links have been added yet.' @@ -106,6 +108,7 @@ canEdit: true, editAction: '?/updatePersonalLink', deleteAction: '?/deletePersonalLink', + refreshAction: '?/refreshPersonalFavicon', emptyMessage: 'No personal bookmarks yet. Bookmarks added here are private to you and scoped to this company.' }; @@ -349,6 +352,22 @@ {/if} {#if editingId === link.id} +
async ({ update }) => { + await update({ reset: false }); + }} + class="mt-2" + > + + +