Wire favicon fetch and refresh action into links page
Validate / validate (push) Successful in 27s

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-15 11:56:31 +07:00
parent 1ef68a4d0d
commit 2c2353e2e7
2 changed files with 148 additions and 10 deletions
@@ -5,8 +5,39 @@ import { companyLinks, userCompanyLinks, users } from '$lib/server/db/schema.js'
import { requireCompanyRoleAny } from '$lib/server/authorization.js'; import { requireCompanyRoleAny } from '$lib/server/authorization.js';
import { logCompanyEvent } from '$lib/server/audit.js'; import { logCompanyEvent } from '$lib/server/audit.js';
import { validateLinkUrl, InvalidLinkUrlError } from '$lib/server/links/validate.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'; 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 = [ const CATEGORIES = [
'internal_tool', 'internal_tool',
'communication', 'communication',
@@ -186,6 +217,8 @@ export const actions: Actions = {
{ linkId: inserted.id, category, url: validUrl } { linkId: inserted.id, category, url: validUrl }
); );
scheduleCompanyFavicon(inserted.id, validUrl);
return { success: true, action: 'addCompanyLink' }; return { success: true, action: 'addCompanyLink' };
}, },
@@ -244,6 +277,8 @@ export const actions: Actions = {
{ linkId: id, category } { linkId: id, category }
); );
if (urlChanged) scheduleCompanyFavicon(id, validUrl);
return { success: true, action: 'updateCompanyLink' }; return { success: true, action: 'updateCompanyLink' };
}, },
@@ -336,7 +371,9 @@ export const actions: Actions = {
category category
}); });
await db.insert(userCompanyLinks).values({ const [inserted] = await db
.insert(userCompanyLinks)
.values({
userId: user.id, userId: user.id,
companyId: params.companyId, companyId: params.companyId,
category, category,
@@ -345,7 +382,10 @@ export const actions: Actions = {
url: validUrl, url: validUrl,
description, description,
sortOrder sortOrder
}); })
.returning({ id: userCompanyLinks.id });
schedulePersonalFavicon(inserted.id, validUrl);
return { success: true, action: 'addPersonalLink' }; return { success: true, action: 'addPersonalLink' };
}, },
@@ -404,6 +444,8 @@ export const actions: Actions = {
}) })
.where(eq(userCompanyLinks.id, id)); .where(eq(userCompanyLinks.id, id));
if (urlChanged) schedulePersonalFavicon(id, validUrl);
return { success: true, action: 'updatePersonalLink' }; return { success: true, action: 'updatePersonalLink' };
}, },
@@ -465,5 +507,82 @@ export const actions: Actions = {
}); });
return { success: true, action: 'reorderPersonalLinks' }; 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)
};
} }
}; };
@@ -69,6 +69,7 @@
canEdit: boolean; canEdit: boolean;
editAction: string; editAction: string;
deleteAction: string; deleteAction: string;
refreshAction: string;
emptyMessage: string; emptyMessage: string;
}; };
@@ -98,6 +99,7 @@
canEdit: data.canManage, canEdit: data.canManage,
editAction: '?/updateCompanyLink', editAction: '?/updateCompanyLink',
deleteAction: '?/deleteCompanyLink', deleteAction: '?/deleteCompanyLink',
refreshAction: '?/refreshCompanyFavicon',
emptyMessage: data.canManage emptyMessage: data.canManage
? 'No company links yet. Click "+ New Link" to add the first one.' ? 'No company links yet. Click "+ New Link" to add the first one.'
: 'No company links have been added yet.' : 'No company links have been added yet.'
@@ -106,6 +108,7 @@
canEdit: true, canEdit: true,
editAction: '?/updatePersonalLink', editAction: '?/updatePersonalLink',
deleteAction: '?/deletePersonalLink', deleteAction: '?/deletePersonalLink',
refreshAction: '?/refreshPersonalFavicon',
emptyMessage: emptyMessage:
'No personal bookmarks yet. Bookmarks added here are private to you and scoped to this company.' 'No personal bookmarks yet. Bookmarks added here are private to you and scoped to this company.'
}; };
@@ -349,6 +352,22 @@
{/if} {/if}
{#if editingId === link.id} {#if editingId === link.id}
<form
method="POST"
action={opts.refreshAction}
use:enhance={() => async ({ update }) => {
await update({ reset: false });
}}
class="mt-2"
>
<input type="hidden" name="id" value={link.id} />
<button
type="submit"
class="text-xs font-medium text-gray-600 underline hover:text-gray-800 dark:text-gray-300 dark:hover:text-white"
>
Refresh favicon
</button>
</form>
<form <form
method="POST" method="POST"
action={opts.editAction} action={opts.editAction}