From d9d9f3f610af9af425325b1c81f8f351caa2d716 Mon Sep 17 00:00:00 2001 From: Samuel James <143277412+SamuelSJames@users.noreply.github.com> Date: Sat, 20 Jun 2026 09:09:44 -0400 Subject: [PATCH] Add bulk delete-all for bookmarks (#20) * Add editable display-name field to generic integrations Lets users set a custom name for Proxmox, Docker, AWS, Remote Desktop, Netbird, Cloudflare, Uptime Kuma, and Weather integrations, separate from the host/IP field, mirroring the SSH host rename pattern. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4 * Surface the new-integration name field as a labeled input The name field for new generic integrations was a faint header input with only placeholder text, easy to miss. Move it into the form grid as a proper labeled "Name" field next to the other connection fields. Co-Authored-By: Claude Sonnet 4.6 Claude-Session: https://claude.ai/code/session_016kF4hZWEkRCPPvCZTeXxn4 * Add file upload for SSH private key and certificate fields Lets users pick a key file from disk (e.g. ~/.ssh) instead of pasting its contents into the Private Key / OPKSSH Certificate fields. * Fix SSH private key paste corrupting multi-line PEM format Private Key and Certificate fields were single-line elements, which strip newlines on paste and corrupt PEM-formatted keys (causing 'Unsupported key format' errors). Render them as multi-line textareas instead so pasted keys keep their line breaks. * Add JSON-converted bookmark import file for Archnest data import Converts homarr-bookmarks.md into the format expected by /api/data/import. * Auto-populate bookmark icons via favicon service in import JSON Each bookmark now points to Google's favicon endpoint for its domain instead of having no icon at all. * Add bulk delete-all for bookmarks Adds DELETE /api/bookmarks to clear every bookmark in one request, and a "Delete All" button (with confirmation) on the BookNest page so re-imports don't require deleting dozens of entries one at a time. --------- Co-authored-by: Claude --- backend/src/routes/bookmarks.ts | 7 + homarr-bookmarks-import.json | 609 ++++++++++++++++++++++++++++++++ src/lib/api.ts | 1 + src/pages/BookNest.tsx | 35 +- 4 files changed, 651 insertions(+), 1 deletion(-) create mode 100644 homarr-bookmarks-import.json diff --git a/backend/src/routes/bookmarks.ts b/backend/src/routes/bookmarks.ts index b7f17cc..730b721 100644 --- a/backend/src/routes/bookmarks.ts +++ b/backend/src/routes/bookmarks.ts @@ -71,6 +71,13 @@ export async function bookmarkRoutes(app: FastifyInstance) { return { ok: true } }) + app.delete('/api/bookmarks', async (_req, reply) => { + const count = (db.prepare('SELECT COUNT(*) as count FROM bookmarks').get() as { count: number }).count + db.prepare('DELETE FROM bookmarks').run() + logEvent('bookmark_deleted', `Deleted all ${count} bookmarks`) + return reply.code(204).send() + }) + app.delete('/api/bookmarks/:id', async (req, reply) => { const id = Number((req.params as { id: string }).id) const existing = db.prepare('SELECT title FROM bookmarks WHERE id = ?').get(id) as { title: string } | undefined diff --git a/homarr-bookmarks-import.json b/homarr-bookmarks-import.json new file mode 100644 index 0000000..76bc847 --- /dev/null +++ b/homarr-bookmarks-import.json @@ -0,0 +1,609 @@ +{ + "version": 1, + "integrations": [], + "bookmarkCategories": [ + { + "id": 1, + "name": "Infrastructure & Self-Hosted", + "icon": null, + "sortOrder": 0 + }, + { + "id": 2, + "name": "Development & Code / Notes", + "icon": null, + "sortOrder": 1 + }, + { + "id": 3, + "name": "Lab & Networking", + "icon": null, + "sortOrder": 2 + }, + { + "id": 4, + "name": "Amazon Work / AWS", + "icon": null, + "sortOrder": 3 + }, + { + "id": 5, + "name": "AI Tools", + "icon": null, + "sortOrder": 4 + }, + { + "id": 6, + "name": "Learning", + "icon": null, + "sortOrder": 5 + }, + { + "id": 7, + "name": "Finance & Investing", + "icon": null, + "sortOrder": 6 + }, + { + "id": 8, + "name": "Bills, Loans, Insurance & Taxes", + "icon": null, + "sortOrder": 7 + }, + { + "id": 9, + "name": "Life / Streaming / Misc", + "icon": null, + "sortOrder": 8 + } + ], + "bookmarks": [ + { + "categoryId": 1, + "title": "CasaOS", + "url": "https://casaos.snsnetlabs.com", + "icon": "https://www.google.com/s2/favicons?domain=casaos.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 1, + "title": "Proxmox (pve1)", + "url": "https://pve1.snsnetlabs.com", + "icon": "https://www.google.com/s2/favicons?domain=pve1.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 1, + "title": "Proxmox (mtr)", + "url": "https://mtr.snsnetlabs.com", + "icon": "https://www.google.com/s2/favicons?domain=mtr.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 1, + "title": "Portainer", + "url": "https://portainer.snsnetlabs.com", + "icon": "https://www.google.com/s2/favicons?domain=portainer.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 1, + "title": "Cockpit", + "url": "https://cockpit.snsnetlabs.com/system", + "icon": "https://www.google.com/s2/favicons?domain=cockpit.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 1, + "title": "Cloudflare", + "url": "https://dash.cloudflare.com/b95d12adb602dae911702cd8f27b9410/snsnetlabs.com", + "icon": "https://www.google.com/s2/favicons?domain=dash.cloudflare.com&sz=64", + "favorite": false + }, + { + "categoryId": 1, + "title": "Nginx Proxy Manager (NPM)", + "url": "https://npm2.snsnetlabs.com", + "icon": "https://www.google.com/s2/favicons?domain=npm2.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 1, + "title": "NetBird", + "url": "https://app.netbird.io/peers", + "icon": "https://www.google.com/s2/favicons?domain=app.netbird.io&sz=64", + "favorite": false + }, + { + "categoryId": 1, + "title": "Linode", + "url": "https://cloud.linode.com/linodes", + "icon": "https://www.google.com/s2/favicons?domain=cloud.linode.com&sz=64", + "favorite": false + }, + { + "categoryId": 1, + "title": "RackNerd", + "url": "https://my.racknerd.com/clientarea.php", + "icon": "https://www.google.com/s2/favicons?domain=my.racknerd.com&sz=64", + "favorite": false + }, + { + "categoryId": 1, + "title": "Dozzle", + "url": "https://dozzle.snsnetlabs.com", + "icon": "https://www.google.com/s2/favicons?domain=dozzle.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 1, + "title": "Guacamole", + "url": "https://guac.snsnetlabs.com", + "icon": "https://www.google.com/s2/favicons?domain=guac.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 1, + "title": "Nexterm", + "url": "https://nexterm.snsnetlabs.com/servers", + "icon": "https://www.google.com/s2/favicons?domain=nexterm.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 1, + "title": "Glance", + "url": "https://glance.snsnetlabs.com", + "icon": "https://www.google.com/s2/favicons?domain=glance.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 1, + "title": "OpenClaw", + "url": "https://openclaw.snsnetlabs.com", + "icon": "https://www.google.com/s2/favicons?domain=openclaw.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 1, + "title": "Homarr", + "url": "https://archnest.snsnetlabs.com", + "icon": "https://www.google.com/s2/favicons?domain=archnest.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 1, + "title": "Uptime Kuma", + "url": "https://uptime.snsnetlabs.com", + "icon": "https://www.google.com/s2/favicons?domain=uptime.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 2, + "title": "GitHub", + "url": "https://github.com/", + "icon": "https://www.google.com/s2/favicons?domain=github.com&sz=64", + "favorite": false + }, + { + "categoryId": 2, + "title": "Gitea", + "url": "https://gitea.snsnetlabs.com/gitea", + "icon": "https://www.google.com/s2/favicons?domain=gitea.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 2, + "title": "Forgejo", + "url": "http://192.168.122.102:3000", + "icon": "https://www.google.com/s2/favicons?domain=192.168.122.102:3000&sz=64", + "favorite": false + }, + { + "categoryId": 2, + "title": "Code Server", + "url": "https://code.snsnetlabs.com", + "icon": "https://www.google.com/s2/favicons?domain=code.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 2, + "title": "Trillium Notes", + "url": "https://notes.snsnetlabs.com", + "icon": "https://www.google.com/s2/favicons?domain=notes.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 2, + "title": "Obsidian", + "url": "https://obsidian.snsnetlabs.com", + "icon": "https://www.google.com/s2/favicons?domain=obsidian.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 2, + "title": "IT-Tools", + "url": "https://it-tools.tech/", + "icon": "https://www.google.com/s2/favicons?domain=it-tools.tech&sz=64", + "favorite": false + }, + { + "categoryId": 2, + "title": "Resume Site (samjam-tech)", + "url": "https://samuelsjames.github.io/samjam-tech/", + "icon": "https://www.google.com/s2/favicons?domain=samuelsjames.github.io&sz=64", + "favorite": false + }, + { + "categoryId": 3, + "title": "GNS3", + "url": "https://gns3.snsnetlabs.com/static/web-ui/controller/1/projects", + "icon": "https://www.google.com/s2/favicons?domain=gns3.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 3, + "title": "PNETLab", + "url": "https://pnet.snsnetlabs.com", + "icon": "https://www.google.com/s2/favicons?domain=pnet.snsnetlabs.com&sz=64", + "favorite": false + }, + { + "categoryId": 4, + "title": "A2Z (Home)", + "url": "https://atoz.amazon.work/home", + "icon": "https://www.google.com/s2/favicons?domain=atoz.amazon.work&sz=64", + "favorite": false + }, + { + "categoryId": 4, + "title": "A2Z (Schedule)", + "url": "https://atoz.amazon.work/schedule", + "icon": "https://www.google.com/s2/favicons?domain=atoz.amazon.work&sz=64", + "favorite": false + }, + { + "categoryId": 4, + "title": "Amazon Payroll", + "url": "https://payroll.amazon.work/", + "icon": "https://www.google.com/s2/favicons?domain=payroll.amazon.work&sz=64", + "favorite": false + }, + { + "categoryId": 4, + "title": "Phone Tool", + "url": "https://phonetool.amazon.com/users/ssamjame", + "icon": "https://www.google.com/s2/favicons?domain=phonetool.amazon.com&sz=64", + "favorite": false + }, + { + "categoryId": 4, + "title": "Slack", + "url": "https://amazon.enterprise.slack.com/", + "icon": "https://www.google.com/s2/favicons?domain=amazon.enterprise.slack.com&sz=64", + "favorite": false + }, + { + "categoryId": 4, + "title": "Quip", + "url": "https://quip-amazon.com/all", + "icon": "https://www.google.com/s2/favicons?domain=quip-amazon.com&sz=64", + "favorite": false + }, + { + "categoryId": 4, + "title": "Outlook", + "url": "https://outlook.office.com/mail/", + "icon": "https://www.google.com/s2/favicons?domain=outlook.office.com&sz=64", + "favorite": false + }, + { + "categoryId": 4, + "title": "Meetings", + "url": "https://meetings.amazon.com/#/room-booking", + "icon": "https://www.google.com/s2/favicons?domain=meetings.amazon.com&sz=64", + "favorite": false + }, + { + "categoryId": 4, + "title": "Boost", + "url": "https://app.boost.aws.a2z.com/platform", + "icon": "https://www.google.com/s2/favicons?domain=app.boost.aws.a2z.com&sz=64", + "favorite": false + }, + { + "categoryId": 4, + "title": "Inventory (Argo)", + "url": "https://aws.argo.ocean-wave.aws.a2z.com/inventory-receive/receive/", + "icon": "https://www.google.com/s2/favicons?domain=aws.argo.ocean-wave.aws.a2z.com&sz=64", + "favorite": false + }, + { + "categoryId": 4, + "title": "Mobility", + "url": "https://prod.us-east-1.mobility.scm.aws.dev/part/search", + "icon": "https://www.google.com/s2/favicons?domain=prod.us-east-1.mobility.scm.aws.dev&sz=64", + "favorite": false + }, + { + "categoryId": 4, + "title": "RDPM (Cable Mgmt)", + "url": "https://rdpm.amazon.com/cable_management", + "icon": "https://www.google.com/s2/favicons?domain=rdpm.amazon.com&sz=64", + "favorite": false + }, + { + "categoryId": 4, + "title": "Interview", + "url": "https://hire.amazon.com/interviewing", + "icon": "https://www.google.com/s2/favicons?domain=hire.amazon.com&sz=64", + "favorite": false + }, + { + "categoryId": 5, + "title": "PartyRock", + "url": "https://internal.partyrock.aws.dev/image", + "icon": "https://www.google.com/s2/favicons?domain=internal.partyrock.aws.dev&sz=64", + "favorite": false + }, + { + "categoryId": 5, + "title": "Chat Claude", + "url": "https://prod.matome.tools.aws.dev/chat-claude-4", + "icon": "https://www.google.com/s2/favicons?domain=prod.matome.tools.aws.dev&sz=64", + "favorite": false + }, + { + "categoryId": 6, + "title": "WGU Student Portal", + "url": "https://my.wgu.edu/degree-plan", + "icon": "https://www.google.com/s2/favicons?domain=my.wgu.edu&sz=64", + "favorite": false + }, + { + "categoryId": 6, + "title": "Udemy (WGU)", + "url": "https://wgu.udemy.com/organization/home/", + "icon": "https://www.google.com/s2/favicons?domain=wgu.udemy.com&sz=64", + "favorite": false + }, + { + "categoryId": 6, + "title": "LinkedIn Learning", + "url": "https://www.linkedin.com/learning/?u=2045532", + "icon": "https://www.google.com/s2/favicons?domain=www.linkedin.com&sz=64", + "favorite": false + }, + { + "categoryId": 6, + "title": "Toastmasters", + "url": "https://www.toastmasters.org/", + "icon": "https://www.google.com/s2/favicons?domain=www.toastmasters.org&sz=64", + "favorite": false + }, + { + "categoryId": 7, + "title": "Ally", + "url": "https://secure.ally.com/", + "icon": "https://www.google.com/s2/favicons?domain=secure.ally.com&sz=64", + "favorite": false + }, + { + "categoryId": 7, + "title": "Capital One", + "url": "https://www.capitalone.com/", + "icon": "https://www.google.com/s2/favicons?domain=www.capitalone.com&sz=64", + "favorite": false + }, + { + "categoryId": 7, + "title": "Credit One", + "url": "https://www.creditonebank.com/", + "icon": "https://www.google.com/s2/favicons?domain=www.creditonebank.com&sz=64", + "favorite": false + }, + { + "categoryId": 7, + "title": "Acorns", + "url": "https://app.acorns.com/present", + "icon": "https://www.google.com/s2/favicons?domain=app.acorns.com&sz=64", + "favorite": false + }, + { + "categoryId": 7, + "title": "Stash", + "url": "https://app.stash.com/", + "icon": "https://www.google.com/s2/favicons?domain=app.stash.com&sz=64", + "favorite": false + }, + { + "categoryId": 7, + "title": "Fundrise", + "url": "https://fundrise.com/account/overview", + "icon": "https://www.google.com/s2/favicons?domain=fundrise.com&sz=64", + "favorite": false + }, + { + "categoryId": 7, + "title": "Roots Investments", + "url": "https://app.investwithroots.com/dashboard", + "icon": "https://www.google.com/s2/favicons?domain=app.investwithroots.com&sz=64", + "favorite": false + }, + { + "categoryId": 7, + "title": "Fidelity", + "url": "https://digital.fidelity.com/prgw/digital/login/full-page", + "icon": "https://www.google.com/s2/favicons?domain=digital.fidelity.com&sz=64", + "favorite": false + }, + { + "categoryId": 7, + "title": "Experian", + "url": "https://usa.experian.com/mfe/dashboard", + "icon": "https://www.google.com/s2/favicons?domain=usa.experian.com&sz=64", + "favorite": false + }, + { + "categoryId": 8, + "title": "Ford Motor Credit", + "url": "https://accountmanager.ford.com/dashboard", + "icon": "https://www.google.com/s2/favicons?domain=accountmanager.ford.com&sz=64", + "favorite": false + }, + { + "categoryId": 8, + "title": "Freedom Mortgage", + "url": "https://www.freedommortgage.com/", + "icon": "https://www.google.com/s2/favicons?domain=www.freedommortgage.com&sz=64", + "favorite": false + }, + { + "categoryId": 8, + "title": "Climb Investco (Student Loan)", + "url": "https://www.climbloanservicingbyuas.com/account", + "icon": "https://www.google.com/s2/favicons?domain=www.climbloanservicingbyuas.com&sz=64", + "favorite": false + }, + { + "categoryId": 8, + "title": "AT&T", + "url": "https://www.att.com/", + "icon": "https://www.google.com/s2/favicons?domain=www.att.com&sz=64", + "favorite": false + }, + { + "categoryId": 8, + "title": "Progressive (Auto)", + "url": "https://www.progressive.com/", + "icon": "https://www.google.com/s2/favicons?domain=www.progressive.com&sz=64", + "favorite": false + }, + { + "categoryId": 8, + "title": "Tower Hill Insurance", + "url": "https://customerportal.thig.com/#/home", + "icon": "https://www.google.com/s2/favicons?domain=customerportal.thig.com&sz=64", + "favorite": false + }, + { + "categoryId": 8, + "title": "TurboTax", + "url": "https://myturbotax.intuit.com/", + "icon": "https://www.google.com/s2/favicons?domain=myturbotax.intuit.com&sz=64", + "favorite": false + }, + { + "categoryId": 8, + "title": "FreeTaxUSA", + "url": "https://www.freetaxusa.com/", + "icon": "https://www.google.com/s2/favicons?domain=www.freetaxusa.com&sz=64", + "favorite": false + }, + { + "categoryId": 8, + "title": "IRS", + "url": "https://www.irs.gov/", + "icon": "https://www.google.com/s2/favicons?domain=www.irs.gov&sz=64", + "favorite": false + }, + { + "categoryId": 8, + "title": "EveryDollar (Budget)", + "url": "https://www.everydollar.com/app/budget", + "icon": "https://www.google.com/s2/favicons?domain=www.everydollar.com&sz=64", + "favorite": false + }, + { + "categoryId": 9, + "title": "Google Calendar", + "url": "https://calendar.google.com/calendar/u/0/r", + "icon": "https://www.google.com/s2/favicons?domain=calendar.google.com&sz=64", + "favorite": false + }, + { + "categoryId": 9, + "title": "WhatsApp", + "url": "https://web.whatsapp.com/", + "icon": "https://www.google.com/s2/favicons?domain=web.whatsapp.com&sz=64", + "favorite": false + }, + { + "categoryId": 9, + "title": "Feedly", + "url": "https://feedly.com/i/my/me", + "icon": "https://www.google.com/s2/favicons?domain=feedly.com&sz=64", + "favorite": false + }, + { + "categoryId": 9, + "title": "FreeConference", + "url": "https://hello.freeconference.com/", + "icon": "https://www.google.com/s2/favicons?domain=hello.freeconference.com&sz=64", + "favorite": false + }, + { + "categoryId": 9, + "title": "Amazon Shopping", + "url": "https://amazon.com", + "icon": "https://www.google.com/s2/favicons?domain=amazon.com&sz=64", + "favorite": false + }, + { + "categoryId": 9, + "title": "Amazon Prime Video", + "url": "https://www.amazon.com/gp/video/movie", + "icon": "https://www.google.com/s2/favicons?domain=www.amazon.com&sz=64", + "favorite": false + }, + { + "categoryId": 9, + "title": "Netflix", + "url": "https://www.netflix.com/browse", + "icon": "https://www.google.com/s2/favicons?domain=www.netflix.com&sz=64", + "favorite": false + }, + { + "categoryId": 9, + "title": "Disney+", + "url": "https://www.disneyplus.com/home", + "icon": "https://www.google.com/s2/favicons?domain=www.disneyplus.com&sz=64", + "favorite": false + }, + { + "categoryId": 9, + "title": "Peacock", + "url": "https://www.peacocktv.com/watch/movies/highlights", + "icon": "https://www.google.com/s2/favicons?domain=www.peacocktv.com&sz=64", + "favorite": false + }, + { + "categoryId": 9, + "title": "Paramount+", + "url": "https://www.paramountplus.com/movies/", + "icon": "https://www.google.com/s2/favicons?domain=www.paramountplus.com&sz=64", + "favorite": false + }, + { + "categoryId": 9, + "title": "Tubi", + "url": "https://tubitv.com/", + "icon": "https://www.google.com/s2/favicons?domain=tubitv.com&sz=64", + "favorite": false + }, + { + "categoryId": 9, + "title": "DailyWire+", + "url": "https://www.dailywire.com/watch", + "icon": "https://www.google.com/s2/favicons?domain=www.dailywire.com&sz=64", + "favorite": false + }, + { + "categoryId": 9, + "title": "Wellness", + "url": "https://wellness.snsnetlabs.com", + "icon": "https://www.google.com/s2/favicons?domain=wellness.snsnetlabs.com&sz=64", + "favorite": false + } + ], + "tunnels": [] +} \ No newline at end of file diff --git a/src/lib/api.ts b/src/lib/api.ts index 798fdb4..50ee4bd 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -69,6 +69,7 @@ export const api = { updateBookmark: (id: number, data: Partial<{ categoryId: number | null; title: string; url: string; icon: string; favorite: boolean }>) => apiFetch<{ ok: boolean }>(`/bookmarks/${id}`, { method: 'PUT', body: JSON.stringify(data) }), deleteBookmark: (id: number) => apiFetch(`/bookmarks/${id}`, { method: 'DELETE' }), + deleteAllBookmarks: () => apiFetch('/bookmarks', { method: 'DELETE' }), listEvents: (limit = 20) => apiFetch<{ events: Event[] }>(`/events?limit=${limit}`), listResources: () => apiFetch<{ resources: Resource[] }>('/integrations/resources'), diff --git a/src/pages/BookNest.tsx b/src/pages/BookNest.tsx index 0d99535..70864ce 100644 --- a/src/pages/BookNest.tsx +++ b/src/pages/BookNest.tsx @@ -465,6 +465,19 @@ export default function BookNest() { } } + async function deleteAllBookmarks() { + const count = bookmarks?.length ?? 0 + if (count === 0) return + if (!window.confirm(`Delete all ${count} bookmark${count === 1 ? '' : 's'}? This cannot be undone.`)) return + const prevBookmarks = bookmarks + setBookmarks([]) + try { + await api.deleteAllBookmarks() + } catch { + setBookmarks(prevBookmarks) + } + } + async function toggleFavorite(bookmark: Bookmark) { const next = bookmark.favorite ? false : true setBookmarks((prev) => prev?.map((b) => (b.id === bookmark.id ? { ...b, favorite: next ? 1 : 0 } : b)) ?? prev) @@ -541,12 +554,32 @@ export default function BookNest() { )}
{/* Page stats — sits directly under the hero title/subtitle, like the blueprint */} -
+
{bookmarks.length} Links {categories.length} Categories {favorites.length} Favorites
+ {bookmarks.length > 0 && ( + + )}
{/* Main column */}