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 { 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)
};
}
};
@@ -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}
<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
method="POST"
action={opts.editAction}