From ce6064fd25068f150920621f6ae308aece97912c Mon Sep 17 00:00:00 2001 From: rhit-steineig <116207222+rhit-steineig@users.noreply.github.com> Date: Sat, 21 Mar 2026 18:06:39 -0400 Subject: [PATCH 01/51] initial formatting --- src/lib/styles/main.css | 233 +++++++++++++++++++++++----------------- 1 file changed, 135 insertions(+), 98 deletions(-) diff --git a/src/lib/styles/main.css b/src/lib/styles/main.css index 6511ba7..692985e 100644 --- a/src/lib/styles/main.css +++ b/src/lib/styles/main.css @@ -1,9 +1,9 @@ -#home-component{ +#home-component { margin: 1rem 2vw; padding: 2rem 2% 1rem 2vw; } -.top-bar{ +.top-bar { border-radius: 0 0 10px 10px !important; padding: 0 !important; position: fixed; @@ -12,22 +12,25 @@ right: 0; width: 100%; z-index: 10; - height: auto; /* Allow height to adjust based on content */ + /* Allow height to adjust based on content */ + height: auto; } .top-bar-wrapper { width: 100%; position: relative; - z-index: 9999; /*Top bar on top bruh. Don't generate more than 9998 windows */ + /*Top bar on top bruh. Don't generate more than 9998 windows */ + z-index: 9999; } -.top-bar-flex{ +.top-bar-flex { display: flex; align-items: center; - padding: 0 1rem; /* Add padding for spacing */ + /* Add padding for spacing */ + padding: 0 1rem; } -.glass{ +.glass { background: color-mix(in oklch, var(--color-primary-800) 80%, transparent) !important; box-shadow: 0 8px 16px 0 var(--color-primary-900); backdrop-filter: blur(14px); @@ -41,8 +44,9 @@ border: 1px solid color-mix(in oklch, var(--color-primary-100) 80%, transparent); border-radius: 10px; } - -.internal-component { /* For components inset within other components*/ + +/* For components inset within other components*/ +.internal-component { margin: 1rem 0; padding: 1rem; background-color: color-mix(in oklch, var(--color-primary-900) 10%, transparent); @@ -56,24 +60,28 @@ border: 1px solid color-mix(in oklch, var(--color-primary-100) 80%, transparent); } -.small-dialog-padding{ +.small-dialog-padding { padding: 1.5rem 1.5rem 1rem 1.5rem; } -.large-dialog-noscroll{ /* Keeps add/edit dialog from scrolling */ +/* Keeps add/edit dialog from scrolling */ +.large-dialog-noscroll { height: calc(100% - 1rem); overflow: hidden; } -.large-dialog-internal{ /* Allows scrolling on internal portion of add/edit */ +/* Allows scrolling on internal portion of add/edit */ +.large-dialog-internal { height: calc(100% - 5rem); overflow-y: scroll; } .slide-out-menu { position: fixed; - z-index: 10000; /* On top of topbar (to avoid topbar dropshadow)*/ - top: var(--top-bar-height, 64px); /* were using the CSS variable with a harcoded fallback for height*/ + /* On top of topbar (to avoid topbar dropshadow)*/ + z-index: 10000; + /* were using the CSS variable with a harcoded fallback for height*/ + top: var(--top-bar-height, 64px); left: -100%; height: calc(100% - var(--top-bar-height, 64px)); transition: left 0.3s ease-in-out; @@ -84,11 +92,13 @@ padding: 1rem; display: flex; justify-content: flex-start; - overflow-y: auto; /* Allow scrolling within the menu */ - overscroll-behavior: contain; /* Prevent scroll chaining */ + /* Allow scrolling within the menu */ + overflow-y: auto; + /* Prevent scroll chaining */ + overscroll-behavior: contain; } -.util-flex{ +.util-flex { display: flex; flex-direction: row; justify-content: center; @@ -98,7 +108,7 @@ width: 50%; } -.open{ +.open { left: 0; } @@ -120,37 +130,42 @@ box-shadow: 0 8px 32px 0 color-mix(in oklch, var(--color-primary-900) 40%, transparent); border: 1px solid color-mix(in oklch, var(--color-primary-100) 80%, transparent); border-radius: 10px; - transition: 0.4s ease; /* Expand & shrink */ - display:flex; - justify-content:space-between; - overflow:hidden; - position:relative; + /* Expand & shrink */ + transition: 0.4s ease; + display: flex; + justify-content: space-between; + overflow: hidden; + position: relative; } -.item-card::before{ /* Hidden gradient that slides in on hover*/ +/* Hidden gradient that slides in on hover*/ +.item-card::before { content: ''; position: absolute; width: 200%; height: 100%; left: -200%; top: 0px; - background: + background: linear-gradient(90deg, - color-mix(in oklch, var(--color-success-800) 80%, transparent), - color-mix(in oklch, var(--color-primary-800) 80%, transparent)); - transition: 0.7s ease; /* Gradient slide in & out*/ - z-index: -1; /* Behind item card text */ + color-mix(in oklch, var(--color-success-800) 80%, transparent), + color-mix(in oklch, var(--color-primary-800) 80%, transparent)); + /* Gradient slide in & out*/ + transition: 0.7s ease; + /* Behind item card text */ + z-index: -1; } -.item-card:hover::before { /* Initial position of gradient */ +/* Initial position of gradient */ +.item-card:hover::before { left: -110%; } -.item-card:hover{ - transform:scale(1.02); +.item-card:hover { + transform: scale(1.02); } -.item-card-flex{ +.item-card-flex { display: flex; gap: 2rem; } @@ -303,7 +318,7 @@ } /* Standard flex setup for a row of buttons */ -.button-row-flex { +.button-row-flex { display: flex; flex-wrap: wrap; gap: 0.5rem; @@ -312,17 +327,19 @@ } /* Flex for the sort row of buttons on the home page*/ -.sort-flex { - display:flex; +.sort-flex { + display: flex; justify-content: space-between; align-items: center; gap: 1rem; - padding: 0 3vw; /* Remove the top padding since we're using page-with-topbar */ - height: 3rem; /* Keeps height consistant when swapping view styles*/ + /* Remove the top padding since we're using page-with-topbar */ + padding: 0 3vw; + /* Keeps height consistant when swapping view styles*/ + height: 3rem; } /* General purpose flex with standard gap size*/ -.simple-flex { +.simple-flex { display: flex; gap: 0.5rem; } @@ -358,7 +375,8 @@ padding: 0; margin: 0; will-change: transform, left, top; - transform: translateZ(0); /* hardware acceleration */ + /* hardware acceleration */ + transform: translateZ(0); } .window-bar { @@ -393,7 +411,8 @@ font-size: 14px; font-weight: bold; margin-left: 8px; - pointer-events: none; /* Prevents text selection when dragging */ + /* Prevents text selection when dragging */ + pointer-events: none; max-width: 100%; white-space: nowrap; overflow: hidden; @@ -462,7 +481,8 @@ position: absolute; top: 100%; left: 0; - width: calc(100% - 2px); /* match input width accounting for border */ + /* match input width accounting for border */ + width: calc(100% - 2px); border-radius: 4px; z-index: 10; } @@ -488,15 +508,15 @@ height: 100%; } -.simple-dialog-spacing{ +.simple-dialog-spacing { padding: 3rem 3rem 1rem; } -.self-center{ +.self-center { justify-self: center; } -.center-button-icons{ +.center-button-icons { justify-items: center; } @@ -509,19 +529,20 @@ Small component styles e.g. buttons, lists, textareas, etc. */ -#resize-none-textarea{ +#resize-none-textarea { resize: none } -.border{ +.border { border: 1px solid color-mix(in oklch, var(--color-primary-100) 80%, transparent); } -.round{ +.round { border-radius: 10px; } -.border-button { /* Standard button style*/ +/* Standard button style*/ +.border-button { border-radius: 10px; border: 1px solid color-mix(in oklch, var(--color-primary-100) 80%, transparent); padding: 0.5rem 1rem; @@ -529,11 +550,12 @@ e.g. buttons, lists, textareas, etc. color: var(--base-font-color); } -.border-button:hover{ +.border-button:hover { background: var(--color-primary-900); } -.x-button { /* "X" button to close windows/dialogs */ +/* "X" button to close windows/dialogs */ +.x-button { float: right; color: var(--color-warning-500); text-transform: uppercase; @@ -547,7 +569,8 @@ e.g. buttons, lists, textareas, etc. background-color: color-mix(in oklch, var(--color-warning-900) 20%, transparent); } -.warn-button { /* For delete, cancel, etc.*/ +/* For delete, cancel, etc.*/ +.warn-button { border-radius: 10px; border: 1px solid color-mix(in oklch, var(--color-error-400) 80%, transparent); padding: 0.5rem 1rem; @@ -559,7 +582,8 @@ e.g. buttons, lists, textareas, etc. background: var(--color-warning-900); } -.success-button { /* For create, confirm, etc. */ +/* For create, confirm, etc. */ +.success-button { border-radius: 10px; border: 1px solid var(--color-success-400); padding: 0.5rem 1rem; @@ -593,25 +617,26 @@ e.g. buttons, lists, textareas, etc. color: color-mix(in oklch, var(--base-font-color) 50%, transparent); } -.add-button:hover{ +.add-button:hover { background: var(--color-primary-900); } .small-add-button { - padding: 0 0.45rem 0.1rem; /* Ugly set of values that keeps button squared*/ + /* Ugly set of values that keeps button squared*/ + padding: 0 0.45rem 0.1rem; } -.home-view-buttons{ +.home-view-buttons { margin: 0.5rem 0 0 0; } -.menu-button:hover{ +.menu-button:hover { background: color-mix(in oklch, var(--color-primary-900) 25%, transparent); border-radius: 10px; color: var(--color-success-400); } -.toggle-background{ +.toggle-background { background: var(--color-success-600); } @@ -621,14 +646,16 @@ e.g. buttons, lists, textareas, etc. border: 1px solid color-mix(in oklch, var(--color-primary-100) 80%, transparent); padding: 0.25rem; position: absolute; - width: calc(100% - 4px); /* Account for borders */ + /* Account for borders */ + width: calc(100% - 4px); border-radius: 10px; z-index: 10; max-height: 200px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: color-mix(in oklch, var(--color-primary-100) 80%, transparent) var(--color-primary-900); - margin-top: 2px; /* Small gap between input and suggestions */ + /* Small gap between input and suggestions */ + margin-top: 2px; } .small-dialog-suggestions { @@ -662,17 +689,20 @@ e.g. buttons, lists, textareas, etc. .suggestion-item { text-align: left; - width:100%; + width: 100%; padding: 0.5rem; - display: block; /* Ensures consistent spacing */ + /* Ensures consistent spacing */ + display: block; } .suggestion-item:hover { background: var(--color-primary-800); - border-radius: 6px; /* Slight rounding on hover highlight */ + /* Slight rounding on hover highlight */ + border-radius: 6px; } -.dark-textarea { /* Should be used on any/all text input boxes*/ +/* Should be used on any/all text input boxes*/ +.dark-textarea { background: color-mix(in oklch, var(--color-primary-900) 10%, transparent); color: var(--base-font-color); border: 1px solid color-mix(in oklch, var(--color-primary-100) 80%, transparent); @@ -706,7 +736,8 @@ e.g. buttons, lists, textareas, etc. max-width: calc(100% - 20px); } -select option { /* Select dropdown options */ +/* Select dropdown options */ +select option { background: var(--color-primary-900); color: var(--base-font-color); padding: 0.5rem; @@ -727,7 +758,8 @@ select option:hover { .item-image-container.expanded .item-image { width: 100%; height: auto; - max-width: 500px; /* Hard cap on image width */ + /* Hard cap on image width */ + max-width: 500px; object-fit: contain; } @@ -780,25 +812,26 @@ select option:hover { display: flex; flex-direction: column; gap: 0.5rem; - z-index: 10000; /* hope we dont make 10001 of something */ + /* hope we dont make 10001 of something */ + z-index: 10000; width: auto; max-width: 400px; } .toaster-popover { - position: fixed; - inset: 0; - pointer-events: none; - display: flex; - justify-content: center; - align-items: flex-end; - padding-bottom: 2rem; - border: none; - background: transparent; + position: fixed; + inset: 0; + pointer-events: none; + display: flex; + justify-content: center; + align-items: flex-end; + padding-bottom: 2rem; + border: none; + background: transparent; } .toaster-popover::backdrop { - background: transparent; + background: transparent; } @@ -807,7 +840,7 @@ select option:hover { color: var(--base-font-color); padding: 0.75rem 1.5rem; border-radius: 8px; - box-shadow: 0 2px 4px rgba(0,0,0,0.2); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); text-align: left; width: 100%; } @@ -834,7 +867,7 @@ select option:hover { cursor: pointer; } -.item-card-checkbox{ +.item-card-checkbox { height: 1.25rem; width: 1.25rem; } @@ -866,7 +899,7 @@ select option:hover { } .item-subcard { - max-width:45%; + max-width: 45%; } .basis-30 { @@ -878,7 +911,7 @@ select option:hover { width: 40px; } -.vertical-dot-col { +.vertical-dot-col { /* Only for use on the rightmost column of dots to make the "draggable" icon */ margin: 0 0 0 -1rem; } @@ -886,21 +919,21 @@ select option:hover { /* Text styles */ -#title{ +#title { color: var(--base-font-color); letter-spacing: -0.1rem; transition: color 0.2s ease-in-out; } -#title:hover{ +#title:hover { color: var(--color-success-500); } -#underline-header{ +#underline-header { border-bottom: 1px solid color-mix(in oklch, var(--color-primary-100) 80%, transparent); } -.important-text{ +.important-text { font-size: 1.5rem; font-weight: bold; letter-spacing: -0.05rem; @@ -910,19 +943,19 @@ Text styles white-space: nowrap; } -.sub-text{ +.sub-text { color: color-mix(in oklch, var(--base-font-color) 80%, transparent); font-weight: lighter; overflow: hidden; text-overflow: ellipsis; } -.clickable-text{ +.clickable-text { color: var(--color-success-400); text-decoration: underline; } -.clickable-text:hover{ +.clickable-text:hover { color: var(--color-success-500); text-decoration: underline; } @@ -966,7 +999,7 @@ Text styles font-size: 2.25rem; } -.icon-small{ +.icon-small { width: 24px; height: 24px; } @@ -1016,7 +1049,7 @@ div { h1 { color: var(--base-font-color); - padding: 0rem 0rem 1rem 0rem; + padding: 0rem 0rem 1rem 0rem; } h2 { @@ -1027,8 +1060,8 @@ body { background: var(--color-surface-800) !important; background-repeat: no-repeat !important; background-size: 100% 50rem !important; - background-image: radial-gradient(at 0% 0%, oklch(75.54% 0.12 169.99deg / 0.2) 0px, transparent 55%), - radial-gradient(at 98% 1%, oklch(48.75% 0.19 22.13deg / 0.33) 0px, transparent 50%) !important; + background-image: radial-gradient(at 0% 0%, oklch(75.54% 0.12 169.99deg / 0.2) 0px, transparent 55%), + radial-gradient(at 98% 1%, oklch(48.75% 0.19 22.13deg / 0.33) 0px, transparent 50%) !important; } ul { @@ -1044,8 +1077,9 @@ select { width: 100%; } -input[type="file"] { /* File input areas (universal) */ - background: color-mix(in oklch, var(--color-primary-900) 80%, transparent); +/* File input areas (universal) */ +input[type="file"] { + background: color-mix(in oklch, var(--color-primary-900) 80%, transparent); color: var(--base-font-color); padding: 1rem; width: 100%; @@ -1062,19 +1096,22 @@ input[type="file"]::file-selector-button { margin-right: 1rem; } -input[type="file"][accept*=".csv"]{ /* File input areas (csv specific) */ +/* File input areas (csv specific) */ +input[type="file"][accept*=".csv"] { &:empty::before { content: "Drop csv files here or "; } } -input[type="file"][accept*="image"]{ /* File input areas (image specific) */ +/* File input areas (image specific) */ +input[type="file"][accept*="image"] { &:empty::before { content: "Drop image file here or "; } } -::backdrop { /* Dialog background */ +/* Dialog background */ +::backdrop { background: color-mix(in oklch, var(--color-surface-900) 70%, transparent); z-index: 9998; -} \ No newline at end of file +} \ No newline at end of file From 62976fa3e56f2a735e86eef41f3bd1a031888de0 Mon Sep 17 00:00:00 2001 From: rhit-steineig <116207222+rhit-steineig@users.noreply.github.com> Date: Sun, 29 Mar 2026 16:04:09 -0400 Subject: [PATCH 02/51] cleaned up inline css --- src/lib/components/ItemContainer.svelte | 2 +- src/lib/components/ItemDetails.svelte | 5 +---- src/lib/components/TopBar.svelte | 2 +- src/lib/components/Window.svelte | 4 +--- src/lib/styles/main.css | 20 ++++++++++++++++++++ src/routes/about/+page.svelte | 10 +++++----- src/routes/utility/+page.svelte | 2 ++ 7 files changed, 31 insertions(+), 14 deletions(-) diff --git a/src/lib/components/ItemContainer.svelte b/src/lib/components/ItemContainer.svelte index 1582d32..9624af2 100644 --- a/src/lib/components/ItemContainer.svelte +++ b/src/lib/components/ItemContainer.svelte @@ -176,7 +176,7 @@ data-item-name={i.name}> { handleSelect(i); }} /> diff --git a/src/lib/components/ItemDetails.svelte b/src/lib/components/ItemDetails.svelte index f985cc1..ffcb335 100644 --- a/src/lib/components/ItemDetails.svelte +++ b/src/lib/components/ItemDetails.svelte @@ -407,10 +407,7 @@ {#if item.itemHistory && item.itemHistory.length > 0}
  • - -
    +
    History Entries: {@render children?.()} + +{#if requireCloseConfirmation} + +
    +
    Are you sure?
    +
    + + +
    +
    +
    +{/if} From 4fce0935f6611b3eea0fe4cb04347ee064a535ac Mon Sep 17 00:00:00 2001 From: Nathan Yang Date: Sun, 5 Apr 2026 11:38:54 -0400 Subject: [PATCH 08/51] Update parent chain after edit --- src/lib/components/ItemDetails.svelte | 33 +-------------------------- src/routes/view/[id]/+page.svelte | 7 ++++-- 2 files changed, 6 insertions(+), 34 deletions(-) diff --git a/src/lib/components/ItemDetails.svelte b/src/lib/components/ItemDetails.svelte index cbf00aa..54b7560 100644 --- a/src/lib/components/ItemDetails.svelte +++ b/src/lib/components/ItemDetails.svelte @@ -12,14 +12,6 @@ import EditItem from "./EditItem.svelte"; import ItemLink from "./ItemLink.svelte"; - interface ItemUpdateEvent extends CustomEvent { - detail: { - imageChanged: boolean; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; - }; - } - interface ActionDetail { item: IBasicItemPopulated | null; itemId: string | null; @@ -50,7 +42,6 @@ let loading = $state(false); let lastItemId = $state(null); let fetchInFlight = false; - let listenerInitialized = $state(false); let imageElement = $state(undefined); let imageLoadError = $state(false); @@ -72,7 +63,7 @@ } } - async function loadParentChain() { + export async function loadParentChain() { // Only proceed if we have an item if (!item) return; @@ -162,28 +153,6 @@ } } - //Handle updates from EditItem - function handleItemUpdated(event: ItemUpdateEvent) { - if (event.detail.imageChanged) { - setTimeout(reloadImage, 500); - } - } - - $effect(() => { - if (listenerInitialized) return; - listenerInitialized = true; - window.addEventListener( - "itemUpdated", - handleItemUpdated as EventListener, - ); - return () => { - window.removeEventListener( - "itemUpdated", - handleItemUpdated as EventListener, - ); - }; - }); - let showEditDialog = $state(false); let isHistoryExpanded = $state(false); diff --git a/src/routes/view/[id]/+page.svelte b/src/routes/view/[id]/+page.svelte index 1b25c76..24c1f44 100644 --- a/src/routes/view/[id]/+page.svelte +++ b/src/routes/view/[id]/+page.svelte @@ -35,6 +35,7 @@ let unique = $state({}); let showItemTree = $state(true); let itemTree = $state<{ reload: () => Promise } | null>(null); + let itemDetails = $state<{ loadParentChain: () => Promise } | null>(null); let draggingItem = $state(null); let targetItemId = $state(undefined); let targetItemName = $state(undefined); @@ -66,9 +67,10 @@ const data: IBasicItemPopulated = await response.json(); item = data; restart(); - if (showItemTree && itemTree) { - await itemTree.reload(); + if (showItemTree) { + await itemTree?.reload(); } + await itemDetails?.loadParentChain(); } catch (err) { console.error(err); item = null; @@ -168,6 +170,7 @@ showCollapse={true}> Date: Mon, 6 Apr 2026 11:39:04 -0400 Subject: [PATCH 09/51] use local cache fixes `#7 [assetatlas] importing cache manifest from assetatlas:latest #7 ERROR: failed to configure registry cache importer: pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed ` --- docker/docker-compose.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 6044149..8f94722 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -13,8 +13,6 @@ services: args: APP_VERSION: ${APP_VERSION:-dev} BUILDKIT_INLINE_CACHE: "1" - cache_from: - - assetatlas:latest env_file: secrets.env environment: NODE_ENV: production From ffd800ae6bbbd8cf3cdaff3ec07685384b34c0f4 Mon Sep 17 00:00:00 2001 From: rhit-steineig <116207222+rhit-steineig@users.noreply.github.com> Date: Mon, 6 Apr 2026 12:30:45 -0400 Subject: [PATCH 10/51] sync sveltekit before building fixes `#30 [WARNING] Cannot find base config file "./.svelte-kit/tsconfig.json" [tsconfig.json]` --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ebd6d20..27de359 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "version": "0.0.1", "type": "module", "scripts": { - "build": "vite build", + "sync": "svelte-kit sync", + "build": "npm run sync && vite build", "start": "node build/index.js", "dev": "npm run dev:services && npx vite dev", "dev:services": "cd docker && docker-compose -f docker-compose.dev.yml up -d", From 30e8a45428b212e75cb0bb5ca3ec36d4f116b3e1 Mon Sep 17 00:00:00 2001 From: rhit-steineig <116207222+rhit-steineig@users.noreply.github.com> Date: Mon, 6 Apr 2026 12:31:46 -0400 Subject: [PATCH 11/51] update packages --- package-lock.json | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6d4caba..3215b2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8925,16 +8925,7 @@ } } }, - "node_modules/svelte/node_modules/aria-query": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", - "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", - "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/svelte-writable-derived": { + "node_modules/svelte-writable-derived": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/svelte-writable-derived/-/svelte-writable-derived-3.1.1.tgz", "integrity": "sha512-w4LR6/bYZEuCs7SGr+M54oipk/UQKtiMadyOhW0PTwAtJ/Ai12QS77sLngEcfBx2q4H8ZBQucc9ktSA5sUGZWw==", @@ -8945,7 +8936,16 @@ "peerDependencies": { "svelte": "^3.2.1 || ^4.0.0-next.1 || ^5.0.0-next.94" } - }, + }, + "node_modules/svelte/node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/svelte/node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", From e526ebe899a2b7bc9ae7d98c23866c09ef374227 Mon Sep 17 00:00:00 2001 From: rhit-steineig <116207222+rhit-steineig@users.noreply.github.com> Date: Mon, 6 Apr 2026 16:29:30 -0400 Subject: [PATCH 12/51] fix linting --- src/lib/components/EditItem.svelte | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/lib/components/EditItem.svelte b/src/lib/components/EditItem.svelte index 2062aa6..650575f 100644 --- a/src/lib/components/EditItem.svelte +++ b/src/lib/components/EditItem.svelte @@ -11,7 +11,6 @@ import Dialog from "./Dialog.svelte"; import ImageSelector from "./ImageSelector.svelte"; import InfoToolTip from "./InfoToolTip.svelte"; - import type { ITemplate } from "$lib/server/db/models/template"; let { item } = $props<{ item: IBasicItemPopulated; @@ -35,14 +34,7 @@ name: string; }; - let selectedTemplates = $state( - (item.templates ?? []) - .map((template: ITemplate) => ({ - _id: template._id.toString(), - name: template.name, - })) - .filter((template: ITemplate) => !!template._id && !!template.name) as ISelectedTemplate[] - ); + let selectedTemplates = $state([]); function getTemplateFieldIdsFromTemplates(templates: Array<{ field: { fields?: unknown[] } }>) { const fieldIds = new Set(); @@ -54,10 +46,9 @@ return fieldIds; } - const templateFieldIds = getTemplateFieldIdsFromTemplates(item.templates ?? []); // eslint-disable-next-line @typescript-eslint/no-explicit-any - let homeItemSuggestions: any[] = []; - let templateName = ""; + let homeItemSuggestions = $state([]); + let templateName = $state(""); // eslint-disable-next-line @typescript-eslint/no-explicit-any let templateSuggestions = $state([]); let selectedImage = $state(null); @@ -81,6 +72,7 @@ function buildCustomFields( currentItem: IBasicItemPopulated, ): ICustomFieldEntryInstance[] { + const templateFieldIds = getTemplateFieldIdsFromTemplates(currentItem.templates ?? []); let fields: ICustomFieldEntryInstance[] = []; if (currentItem.customFields?.length) { //First load non-template fields From 7afb6d5b4be6269d015e3332a8d16b103f4d9e51 Mon Sep 17 00:00:00 2001 From: Emile Marois Date: Tue, 7 Apr 2026 10:45:50 -0400 Subject: [PATCH 13/51] Added "pinned" field to basicItem + migration --- .../20260407143553-add_pinned_field.cjs | 31 +++++++++++++++++++ src/lib/server/db/models/basicItem.ts | 3 ++ 2 files changed, 34 insertions(+) create mode 100644 docker/migrations/20260407143553-add_pinned_field.cjs diff --git a/docker/migrations/20260407143553-add_pinned_field.cjs b/docker/migrations/20260407143553-add_pinned_field.cjs new file mode 100644 index 0000000..86ac4c8 --- /dev/null +++ b/docker/migrations/20260407143553-add_pinned_field.cjs @@ -0,0 +1,31 @@ +module.exports = { + /** + * @param db {import('mongodb').Db} + * @param client {import('mongodb').MongoClient} + * @returns {Promise} + */ + async up(db, client) { + // TODO write your migration here. + // See https://github.com/seppevs/migrate-mongo/#creating-a-new-migration-script + // Example: + // await db.collection('albums').updateOne({artist: 'The Beatles'}, {$set: {blacklisted: true}}); + const basicItemsCollection = db.collection('basicitems'); + await basicItemsCollection.updateMany( + { pinned: { $exists: false } }, + { $set: { pinned: false } } + ); + }, + + /** + * @param db {import('mongodb').Db} + * @param client {import('mongodb').MongoClient} + * @returns {Promise} + */ + async down(db, client) { + const basicItemsCollection = db.collection('basicitems'); + await basicItemsCollection.updateMany( + { pinned: { $exists: true } }, + { $unset: { pinned: "" } } + ); + } +}; diff --git a/src/lib/server/db/models/basicItem.ts b/src/lib/server/db/models/basicItem.ts index 51d2c36..21e7f79 100644 --- a/src/lib/server/db/models/basicItem.ts +++ b/src/lib/server/db/models/basicItem.ts @@ -10,6 +10,7 @@ const { model, models } = mongoose; export interface IBasicItem extends Document { //we can add more stuff here _id: Types.ObjectId;//we need this underscore i think name: string; + pinned: boolean; description: string; tags: string[]; containedItems?: Array;//for nested items @@ -33,6 +34,7 @@ export interface IBasicItem extends Document { //we can add more stuff here export interface IBasicItemPopulated { _id: Types.ObjectId; name: string; + pinned: boolean; description?: string; tags: string[]; containedItems?: Array; @@ -57,6 +59,7 @@ export interface IBasicItemPopulated { const BasicItemSchema: Schema = new Schema({ //id: { type: Number, unique: true }, //we dont need this because mongodb default _id works name: { type: String, required: true }, + pinned: { type: Boolean, default: false }, description: { type: String, required: false }, // createdAt: { type: Date, required: true }, // updatedAt: { type: Date, required: true }, From 42fc5f48838069f95218eefa7e85b80d1f60d3e2 Mon Sep 17 00:00:00 2001 From: Emile Marois Date: Tue, 7 Apr 2026 11:45:36 -0400 Subject: [PATCH 14/51] Added pinning to UI (rough draft) --- src/lib/components/ItemTree.svelte | 68 ++++++++++++++++++++++- src/routes/api/items/tree/[id]/+server.ts | 37 ++++++++++-- 2 files changed, 98 insertions(+), 7 deletions(-) diff --git a/src/lib/components/ItemTree.svelte b/src/lib/components/ItemTree.svelte index c0e7ba9..059011b 100644 --- a/src/lib/components/ItemTree.svelte +++ b/src/lib/components/ItemTree.svelte @@ -1,7 +1,7 @@ From 8e5d0e11b42c479594a04043f6107feb05320415 Mon Sep 17 00:00:00 2001 From: Nathan Yang Date: Mon, 20 Apr 2026 20:27:46 -0400 Subject: [PATCH 16/51] Require confirmation even when using esc key --- src/lib/components/Dialog.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/components/Dialog.svelte b/src/lib/components/Dialog.svelte index 4e5b2a3..ed41b1c 100644 --- a/src/lib/components/Dialog.svelte +++ b/src/lib/components/Dialog.svelte @@ -28,8 +28,9 @@ create?.(); }); - function handleClose() { + function handleClose(event: CustomEvent) { if (requireCloseConfirmation) { + event.preventDefault(); closeConfirmationDialog?.showModal(); } else { closeDialog(); @@ -44,6 +45,7 @@ Date: Mon, 20 Apr 2026 21:03:06 -0400 Subject: [PATCH 17/51] token-based solution --- src/routes/view/[id]/+page.svelte | 30 +++++++++++++++++++----------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/src/routes/view/[id]/+page.svelte b/src/routes/view/[id]/+page.svelte index 24c1f44..dd76bbf 100644 --- a/src/routes/view/[id]/+page.svelte +++ b/src/routes/view/[id]/+page.svelte @@ -36,6 +36,7 @@ let showItemTree = $state(true); let itemTree = $state<{ reload: () => Promise } | null>(null); let itemDetails = $state<{ loadParentChain: () => Promise } | null>(null); + let additionalItemRefreshToken = $state(0); let draggingItem = $state(null); let targetItemId = $state(undefined); let targetItemName = $state(undefined); @@ -55,6 +56,10 @@ unique = {}; } + function refreshAdditionalItemWindows() { + additionalItemRefreshToken += 1; + } + async function fetchItem(id: string) { try { const response = await fetch(`/api/items/${id}`); @@ -71,6 +76,7 @@ await itemTree?.reload(); } await itemDetails?.loadParentChain(); + refreshAdditionalItemWindows(); } catch (err) { console.error(err); item = null; @@ -213,17 +219,19 @@ showCollapse={true} on:close={() => handleCloseWindow(itemWindow.id)} on:openNewTab={() => openInNewTab(itemWindow.id)}> - - handleUpdateTitle(itemWindow.id, e)} /> + {#key `${itemWindow.id}-${additionalItemRefreshToken}`} + + handleUpdateTitle(itemWindow.id, e)} /> + {/key} {/each}
    From 8595bbed2a7e8ea89ef18b8300ec51c1bc77f554 Mon Sep 17 00:00:00 2001 From: Nathan Yang Date: Mon, 20 Apr 2026 21:12:05 -0400 Subject: [PATCH 18/51] update child chains --- src/lib/components/ItemDetails.svelte | 10 ++++++ src/routes/view/[id]/+page.svelte | 45 ++++++++++++++------------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/lib/components/ItemDetails.svelte b/src/lib/components/ItemDetails.svelte index 54b7560..d66f5dc 100644 --- a/src/lib/components/ItemDetails.svelte +++ b/src/lib/components/ItemDetails.svelte @@ -63,6 +63,16 @@ } } + export async function reload() { + const idToLoad = itemId ?? item?._id?.toString() ?? null; + if (idToLoad) { + await loadItemById(idToLoad); + } + await loadParentChain(); + await updateTitle(); + await reloadImage(); + } + export async function loadParentChain() { // Only proceed if we have an item if (!item) return; diff --git a/src/routes/view/[id]/+page.svelte b/src/routes/view/[id]/+page.svelte index dd76bbf..19c4011 100644 --- a/src/routes/view/[id]/+page.svelte +++ b/src/routes/view/[id]/+page.svelte @@ -35,8 +35,7 @@ let unique = $state({}); let showItemTree = $state(true); let itemTree = $state<{ reload: () => Promise } | null>(null); - let itemDetails = $state<{ loadParentChain: () => Promise } | null>(null); - let additionalItemRefreshToken = $state(0); + let mainItemDetails = $state<{ reload: () => Promise } | null>(null); let draggingItem = $state(null); let targetItemId = $state(undefined); let targetItemName = $state(undefined); @@ -56,8 +55,12 @@ unique = {}; } - function refreshAdditionalItemWindows() { - additionalItemRefreshToken += 1; + async function refreshAdditionalItemWindows() { + await Promise.all( + additionalWindows.map( + (windowItem) => windowItem.detailsRef?.reload() ?? Promise.resolve(), + ), + ); } async function fetchItem(id: string) { @@ -75,8 +78,8 @@ if (showItemTree) { await itemTree?.reload(); } - await itemDetails?.loadParentChain(); - refreshAdditionalItemWindows(); + await mainItemDetails?.reload(); + await refreshAdditionalItemWindows(); } catch (err) { console.error(err); item = null; @@ -98,6 +101,7 @@ name: string; x: number; y: number; + detailsRef: { reload: () => Promise } | null; } let additionalWindows = $state([]); @@ -121,7 +125,7 @@ additionalWindows = [ ...additionalWindows, - { id, name: "Loading...", x: offsetX, y: offsetY }, + { id, name: "Loading...", x: offsetX, y: offsetY, detailsRef: null }, ]; } @@ -176,7 +180,7 @@ showCollapse={true}> handleCloseWindow(itemWindow.id)} on:openNewTab={() => openInNewTab(itemWindow.id)}> - {#key `${itemWindow.id}-${additionalItemRefreshToken}`} - - handleUpdateTitle(itemWindow.id, e)} /> - {/key} + + handleUpdateTitle(itemWindow.id, e)} /> {/each}
    From c9a94c6fe6fe9e334706ad399c268399dfcad0dc Mon Sep 17 00:00:00 2001 From: Nathan Yang Date: Mon, 20 Apr 2026 21:56:18 -0400 Subject: [PATCH 19/51] reset scrolling in dialogs --- src/lib/components/CreateItemDesktop.svelte | 14 ++++++++++++-- src/lib/components/CreateItemMobile.svelte | 1 + src/lib/components/Dialog.svelte | 3 ++- 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/lib/components/CreateItemDesktop.svelte b/src/lib/components/CreateItemDesktop.svelte index 08b2ba0..ed45fc6 100644 --- a/src/lib/components/CreateItemDesktop.svelte +++ b/src/lib/components/CreateItemDesktop.svelte @@ -54,6 +54,7 @@ let templateSelectionDialog: HTMLDialogElement | undefined = $state(); let showCreateTemplateDialog = $state(false); let showTemplateSelectionDialog = $state(false); + let formContainer: HTMLElement | undefined = $state(); let imageSelector: ImageSelector; const dispatch = createEventDispatcher(); @@ -93,7 +94,15 @@ } - {}} close={resetAllFields}> + {}} + close={() => { + formContainer?.scrollTo(0,0); + resetAllFields(); + }}> {#if originalItem}

    Duplicate & Edit Item @@ -103,10 +112,11 @@ Create New Item

    {/if} -
    +
    { event.preventDefault(); + formContainer?.scrollTo(0,0); submitAndCloseItem(dialog, imageSelector); } }> diff --git a/src/lib/components/CreateItemMobile.svelte b/src/lib/components/CreateItemMobile.svelte index c1b85ec..5e6781e 100644 --- a/src/lib/components/CreateItemMobile.svelte +++ b/src/lib/components/CreateItemMobile.svelte @@ -95,6 +95,7 @@ if (success) { imageSelector.resetImage(); partialResetFields(); + dialog.scrollTo(0,0); } } diff --git a/src/lib/components/Dialog.svelte b/src/lib/components/Dialog.svelte index 267a956..8a0f1f7 100644 --- a/src/lib/components/Dialog.svelte +++ b/src/lib/components/Dialog.svelte @@ -24,8 +24,8 @@ }); function handleClose() { - dialog?.close(); close(); + dialog?.close(); } @@ -34,6 +34,7 @@ ? 'large-dialog-noscroll' : ''}" style="overflow: {canOverflow ? 'visible' : 'auto'}" + oncancel={handleClose} bind:this={dialog}> {@render children?.()} From de53af01022cf33171c6ab7ca272b0b97cfc7b35 Mon Sep 17 00:00:00 2001 From: Emile Marois Date: Tue, 21 Apr 2026 10:32:41 -0400 Subject: [PATCH 20/51] Improved pinning UI --- src/lib/components/ItemTree.svelte | 6 +++--- src/lib/styles/main.css | 8 ++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/lib/components/ItemTree.svelte b/src/lib/components/ItemTree.svelte index 059011b..cd0955b 100644 --- a/src/lib/components/ItemTree.svelte +++ b/src/lib/components/ItemTree.svelte @@ -281,14 +281,14 @@ class="tree-item-card important-text {node.item._id === currentId ? 'current' : ''}" aria-current={node.item._id === currentId}> -
    +
    {node.item.name} {:else} - + {/if}
    diff --git a/src/lib/styles/main.css b/src/lib/styles/main.css index 6511ba7..c78c767 100644 --- a/src/lib/styles/main.css +++ b/src/lib/styles/main.css @@ -202,6 +202,14 @@ max-width: 24rem; } +.full-width{ + width: 100%; +} + +.grayed-out{ + opacity: 0.5; +} + .tree-item-button { background: transparent; border: none; From ef659c3f31e0d3e33c792bbacf154e396a5d9b84 Mon Sep 17 00:00:00 2001 From: rhit-steineig <116207222+rhit-steineig@users.noreply.github.com> Date: Tue, 21 Apr 2026 11:37:58 -0400 Subject: [PATCH 21/51] reduce duplication in src/routes/api/customFields/ task 9 in #156 --- src/lib/utility/checkItemHelper.ts | 31 +++++++++++++++++++ .../api/customFields/checkItemId/+server.ts | 19 ++---------- .../api/customFields/checkItemName/+server.ts | 19 ++---------- 3 files changed, 37 insertions(+), 32 deletions(-) create mode 100644 src/lib/utility/checkItemHelper.ts diff --git a/src/lib/utility/checkItemHelper.ts b/src/lib/utility/checkItemHelper.ts new file mode 100644 index 0000000..97ea83b --- /dev/null +++ b/src/lib/utility/checkItemHelper.ts @@ -0,0 +1,31 @@ +import BasicItem from '$lib/server/db/models/basicItem'; +import { error, json } from '@sveltejs/kit'; + +export async function checkItemAttribute(attributeName: string, url: URL) { + let searchParam, attributeKey, otherAttributeName, otherAttributeKey: string; + if (attributeName === 'id') { + searchParam = 'itemID'; + attributeKey = '_id'; + otherAttributeName = 'name'; + otherAttributeKey = 'name'; + } else if (attributeName === 'name') { + searchParam = 'itemName'; + attributeKey = 'name'; + otherAttributeName = 'id'; + otherAttributeKey = '_id'; + } else { + throw error(500, 'Invalid attribute provided: ' + attributeName); + } + + const attribute = url.searchParams.get(searchParam); + if (!attribute) { + throw error(400, `No item ${attributeName} provided`); + } + + const otherAttribute = await BasicItem.findOne({ [attributeKey]: attribute }).select(otherAttributeKey).exec(); + if (!otherAttribute) { + return json({ [otherAttributeName]: null }); + } + + return json({ [otherAttributeName]: otherAttribute[otherAttributeKey].toString() }); +} diff --git a/src/routes/api/customFields/checkItemId/+server.ts b/src/routes/api/customFields/checkItemId/+server.ts index 38f33b8..a2cbb0c 100644 --- a/src/routes/api/customFields/checkItemId/+server.ts +++ b/src/routes/api/customFields/checkItemId/+server.ts @@ -1,19 +1,6 @@ -import { json, error } from '@sveltejs/kit'; +import { checkItemAttribute } from '$lib/utility/checkItemHelper'; import type { RequestHandler } from '@sveltejs/kit'; -import BasicItem from '$lib/server/db/models/basicItem'; export const GET: RequestHandler = async ({ url }) => { - const itemID = url.searchParams.get('itemID'); - - if (!itemID) { - throw error(400, 'No item ID provided'); - } - - const itemName = await BasicItem.findOne({ _id: itemID }).select('name').exec(); - - if (!itemName) { - return json({ name: null }); - } - - return json({ name: itemName.name }); -}; \ No newline at end of file + return checkItemAttribute('id', url); +}; diff --git a/src/routes/api/customFields/checkItemName/+server.ts b/src/routes/api/customFields/checkItemName/+server.ts index af7f3ca..0ac0e63 100644 --- a/src/routes/api/customFields/checkItemName/+server.ts +++ b/src/routes/api/customFields/checkItemName/+server.ts @@ -1,19 +1,6 @@ -import { json, error } from '@sveltejs/kit'; +import { checkItemAttribute } from '$lib/utility/checkItemHelper'; import type { RequestHandler } from '@sveltejs/kit'; -import BasicItem from '$lib/server/db/models/basicItem'; export const GET: RequestHandler = async ({ url }) => { - const itemName = url.searchParams.get('itemName'); - - if (!itemName) { - throw error(400, 'No item name provided'); - } - - const itemId = await BasicItem.findOne({ name: itemName }).select('_id').exec(); - - if (!itemId) { - return json({ id: null }); - } - - return json({ id: itemId._id.toString() }); -}; \ No newline at end of file + return checkItemAttribute('name', url); +}; From 7b145146744d4079e14fb2ab8ec52fa1fde2c11f Mon Sep 17 00:00:00 2001 From: rhit-steineig <116207222+rhit-steineig@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:41:26 -0400 Subject: [PATCH 22/51] reduced duplication in src/routes/api/ between items/[id]/+server.ts and templates/[id]/+server.ts --- src/lib/utility/routesHelper.ts | 41 ++++++++++++++++++++++++ src/routes/api/items/[id]/+server.ts | 39 +++++++--------------- src/routes/api/templates/[id]/+server.ts | 41 +++++++----------------- tests/item.test.ts | 16 ++++----- tests/template.test.ts | 2 +- 5 files changed, 73 insertions(+), 66 deletions(-) create mode 100644 src/lib/utility/routesHelper.ts diff --git a/src/lib/utility/routesHelper.ts b/src/lib/utility/routesHelper.ts new file mode 100644 index 0000000..6883289 --- /dev/null +++ b/src/lib/utility/routesHelper.ts @@ -0,0 +1,41 @@ +import { error, json } from '@sveltejs/kit'; +import mongoose from 'mongoose'; + +interface Params { + id?: string | undefined; + parentID?: string | undefined; + type?: string | undefined; + templateName?: string | undefined; +} + +function validateID(id: string | undefined) { + if (!id || !mongoose.Types.ObjectId.isValid(id)) { + throw error(400, 'Invalid MongoDB ID'); + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function getHelper(params: Params, getFunction: (id: string) => Promise) { + const { id } = params; + validateID(id); + + const retrieved = await getFunction(id as string); + if (!retrieved) { + throw error(404, 'Get failed: could not find match for id: ' + id); + } + + return json(retrieved, { status: 200 }); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export async function deleteHelper(params: Params, deleteFunction: (id: string) => Promise) { + const { id } = params; + validateID(id); + + const deleted = await deleteFunction(id as string); + if (!deleted) { + throw error(404, 'Delete failed: could not find match for id: ' + id); + } + + return json({ message: 'Delete successful', deleted }, { status: 200 }); +} diff --git a/src/routes/api/items/[id]/+server.ts b/src/routes/api/items/[id]/+server.ts index c2479b2..321c28c 100644 --- a/src/routes/api/items/[id]/+server.ts +++ b/src/routes/api/items/[id]/+server.ts @@ -1,17 +1,14 @@ import BasicItem from '$lib/server/db/models/basicItem.js'; +import { deleteHelper, getHelper } from '$lib/utility/routesHelper'; import type { RequestHandler } from '@sveltejs/kit'; import { error, json } from '@sveltejs/kit'; -import mongoose from 'mongoose'; export const GET: RequestHandler = async ({ params }) => { - const { id } = params; - - if (!id || !mongoose.Types.ObjectId.isValid(id)) { - throw error(400, 'Not a valid MongoDB ID'); - } + return await getHelper(params, getItem); +}; - console.log('Fetching item:', id); - const item = await BasicItem.findById(id) +const getItem = async (id: string) => { + return await BasicItem.findById(id) .populate('templates.field') .populate('parentItem') .populate('homeItem') @@ -19,13 +16,7 @@ export const GET: RequestHandler = async ({ params }) => { .populate('customFields.field') .populate('itemHistory.location') .exec(); - - if (!item) { - throw error(404, 'Cannot get: Item not found'); - } - - return json(item, { status: 200 }); -}; +} export const PATCH: RequestHandler = async ({ params, request }) => { const { id } = params; @@ -84,17 +75,9 @@ export const PATCH: RequestHandler = async ({ params, request }) => { }; export const DELETE: RequestHandler = async ({ params }) => { - const { id } = params; - - if (!id || !mongoose.Types.ObjectId.isValid(id)) { - throw error(400, 'Not a valid MongoDB ID'); - } - - const item = await BasicItem.findOneAndDelete({ _id: id }); - - if (!item) { - throw error(404, 'Cannot delete: Item not found'); - } - - return json({ message: 'Item deleted successfully', item }, { status: 200 }); + return await deleteHelper(params, deleteItem); }; + +const deleteItem = async (id: string) => { + return await BasicItem.findOneAndDelete({ _id: id }); +} diff --git a/src/routes/api/templates/[id]/+server.ts b/src/routes/api/templates/[id]/+server.ts index 4970676..904adb5 100644 --- a/src/routes/api/templates/[id]/+server.ts +++ b/src/routes/api/templates/[id]/+server.ts @@ -1,36 +1,19 @@ -import { json, error } from '@sveltejs/kit'; -import type { RequestHandler } from '@sveltejs/kit'; -import mongoose from 'mongoose'; import Template from '$lib/server/db/models/template.js'; +import { deleteHelper, getHelper } from '$lib/utility/routesHelper'; +import type { RequestHandler } from '@sveltejs/kit'; export const GET: RequestHandler = async ({ params }) => { - const { id } = params; - - if (!id || !mongoose.Types.ObjectId.isValid(id)) { - throw error(400, 'Invalid template ID'); - } - - const template = await Template.findById(id).populate('fields').exec(); - - if (!template) { - throw error(404, 'Template not found'); - } - - return json(template); + return await getHelper(params, getTemplate); }; -export const DELETE: RequestHandler = async ({ params }) => { - const { id } = params; - - if (!id || !mongoose.Types.ObjectId.isValid(id)) { - throw error(400, 'Invalid template ID'); - } +const getTemplate = async (id: string) => { + return await Template.findById(id).populate('fields').exec(); +} - const template = await Template.findByIdAndDelete(id).exec(); - - if (!template) { - throw error(404, 'Template not found'); - } +export const DELETE: RequestHandler = async ({ params }) => { + return await deleteHelper(params, deleteTemplate); +}; - return json({ message: 'Template deleted successfully' }); -}; \ No newline at end of file +const deleteTemplate = async (id: string) => { + return await Template.findByIdAndDelete(id).exec(); +} diff --git a/tests/item.test.ts b/tests/item.test.ts index c0e03e2..5fae52c 100644 --- a/tests/item.test.ts +++ b/tests/item.test.ts @@ -187,7 +187,7 @@ describe('Item API', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { expect(err.status).toBe(404); - expect(err.body?.message).toBe('Cannot get: Item not found'); + expect(err.body?.message).toBe('Get failed: could not find match for id: ' + nonExistentId.toString()); } }); @@ -290,7 +290,7 @@ describe('Item API', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (err: any) { expect(err.status).toBe(404); - expect(err.body?.message).toBe('Cannot get: Item not found'); + expect(err.body?.message).toBe('Get failed: could not find match for id: ' + nonExistentId.toString()); } }); @@ -307,7 +307,7 @@ describe('Item API', () => { const body = await response.json(); expect(response.status).toBe(200); - expect(body.message).toBe('Item deleted successfully'); + expect(body.message).toBe('Delete successful'); const deletedItem = await BasicItem.findById(newItem._id).exec(); expect(deletedItem).toBeNull(); @@ -543,7 +543,7 @@ describe('Item API', () => { const deleteBody = await deleteResponse.json(); expect(deleteResponse.status).toBe(200); - expect(deleteBody.message).toBe('Item deleted successfully'); + expect(deleteBody.message).toBe('Delete successful'); const updatedNestedItem = await BasicItem.findById(nestedItem._id).exec(); expect(updatedNestedItem).not.toBeNull(); @@ -563,7 +563,7 @@ describe('Item API', () => { const deleteTopBody = await deleteTopLevelResponse.json(); expect(deleteTopLevelResponse.status).toBe(200); - expect(deleteTopBody.message).toBe('Item deleted successfully'); + expect(deleteTopBody.message).toBe('Delete successful'); const updatedNestedItemAfterTopLevelDelete = await BasicItem.findById(nestedItem._id).exec(); expect(updatedNestedItemAfterTopLevelDelete).not.toBeNull(); @@ -684,7 +684,7 @@ describe('Item API', () => { const deleteBody = await deleteResponse.json(); expect(deleteResponse.status).toBe(200); - expect(deleteBody.message).toBe('Item deleted successfully'); + expect(deleteBody.message).toBe('Delete successful'); const updatedItemWithHome = await BasicItem.findById(itemWithHome._id).exec(); expect(updatedItemWithHome).not.toBeNull(); @@ -890,7 +890,7 @@ describe('Item and Template API', () => { const deleteBody = await deleteResponse.json(); expect(deleteResponse.status).toBe(200); - expect(deleteBody.message).toBe('Template deleted successfully'); + expect(deleteBody.message).toBe('Delete successful'); const updatedItem = await BasicItem.findById(createdItem._id).exec(); expect(updatedItem).not.toBeNull(); @@ -906,7 +906,7 @@ describe('Item and Template API', () => { const deleteBody2 = await deleteResponse2.json(); expect(deleteResponse2.status).toBe(200); - expect(deleteBody2.message).toBe('Template deleted successfully'); + expect(deleteBody2.message).toBe('Delete successful'); const updatedItem2 = await BasicItem.findById(createdItem._id).exec(); expect(updatedItem2).not.toBeNull(); diff --git a/tests/template.test.ts b/tests/template.test.ts index 8626e03..513209e 100644 --- a/tests/template.test.ts +++ b/tests/template.test.ts @@ -312,7 +312,7 @@ describe('Template API', () => { const response = await deleteTemplateHandler(deleteEvent); expect(response.status).toBe(200); const responseBody = await response.json(); - expect(responseBody.message).toBe('Template deleted successfully'); + expect(responseBody.message).toBe('Delete successful'); const deletedTemplate = await Template.findById(template._id).exec(); expect(deletedTemplate).toBeNull(); From fde35bc57ebc5f21a897adf195098cadc4f248d5 Mon Sep 17 00:00:00 2001 From: rhit-steineig <116207222+rhit-steineig@users.noreply.github.com> Date: Tue, 21 Apr 2026 15:49:11 -0400 Subject: [PATCH 23/51] should have been included in merge commit c76acbac6e5da3f9638e83844fe637c57f53e809 --- src/lib/utility/pageHelper.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/utility/pageHelper.ts b/src/lib/utility/pageHelper.ts index ef8e834..fc07ed6 100644 --- a/src/lib/utility/pageHelper.ts +++ b/src/lib/utility/pageHelper.ts @@ -3,6 +3,7 @@ export interface ItemWindow { name: string; x: number; y: number; + detailsRef: { reload: () => Promise } | null; } export function removeItemWindow(itemWindows: ItemWindow[], id: string) { @@ -18,7 +19,7 @@ export function openItemHelper(itemWindows: ItemWindow[], id: string){ } const offset = 30 * itemWindows.length + 50; - const newItemWindow = { id, name: "Loading...", x: offset, y: offset }; + const newItemWindow = { id, name: "Loading...", x: offset, y: offset, detailsRef: null }; itemWindows.push(newItemWindow); return itemWindows; } From 7b9c901e886cf05d08b9a2206ce0e1231af99c73 Mon Sep 17 00:00:00 2001 From: Nathan Yang Date: Tue, 21 Apr 2026 16:51:52 -0400 Subject: [PATCH 24/51] fixed tests --- tests/components/createItem.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/components/createItem.test.ts b/tests/components/createItem.test.ts index d155509..3fdab53 100644 --- a/tests/components/createItem.test.ts +++ b/tests/components/createItem.test.ts @@ -12,6 +12,13 @@ Object.defineProperty(HTMLElement.prototype, 'animate', { }; }, }); +Object.defineProperty(HTMLElement.prototype, 'scrollTo', { + value: function () { return { + finished: Promise.resolve(), + cancel: function() {} + }; }, +}); + if (typeof HTMLDialogElement !== 'undefined') { if (!HTMLDialogElement.prototype.close) { HTMLDialogElement.prototype.close = vi.fn(); @@ -28,6 +35,7 @@ if (typeof HTMLDialogElement !== 'undefined') { } + function renderComponent(props = {}) { return render(CreateItem, { props: { dialog: document.createElement('dialog') as HTMLDialogElement, ...props } }); } From 6ec3c998f6dc56f946da5abc8d9e88cd0e192c04 Mon Sep 17 00:00:00 2001 From: Nathan Yang Date: Tue, 21 Apr 2026 18:21:37 -0400 Subject: [PATCH 25/51] Fix escape key issues --- src/lib/components/Dialog.svelte | 65 ++++++++++++++++++-------------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/src/lib/components/Dialog.svelte b/src/lib/components/Dialog.svelte index ed41b1c..7ad3e3b 100644 --- a/src/lib/components/Dialog.svelte +++ b/src/lib/components/Dialog.svelte @@ -21,24 +21,37 @@ requireCloseConfirmation: boolean; } = $props(); - let closeConfirmationDialog: HTMLDialogElement | undefined = $state(undefined); + let isConfirmationOpen = $state(false); onMount(() => { - closeConfirmationDialog?.close(); create?.(); }); - function handleClose(event: CustomEvent) { + function openConfirmation() { + isConfirmationOpen = true; + } + + function closeConfirmation() { + isConfirmationOpen = false; + } + + function handleClose(event?: Event) { + event?.preventDefault(); + if (requireCloseConfirmation) { - event.preventDefault(); - closeConfirmationDialog?.showModal(); - } else { - closeDialog(); + if (isConfirmationOpen) { + closeConfirmation(); + } else { + openConfirmation(); + } + return; } + + closeDialog(); } function closeDialog() { - closeConfirmationDialog?.close(); + closeConfirmation(); dialog?.close(); close(); } @@ -53,25 +66,21 @@ bind:this={dialog}> {@render children?.()} -
    - -{#if requireCloseConfirmation} - -
    -
    Are you sure?
    -
    - - + {#if requireCloseConfirmation && isConfirmationOpen} +
    +
    +
    +
    Are you sure?
    +
    + + +
    +
    -
    -{/if} + {/if} +
    From 237b23ff1148835cb5c2eb9c0bb8f9f133fed54e Mon Sep 17 00:00:00 2001 From: Emile Marois Date: Wed, 22 Apr 2026 07:16:17 -0400 Subject: [PATCH 26/51] Moved ConfiguringOauthProviders --- .../ConfiguringOauthProviders.md => ConfiguringOauthProviders.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/assetatlas/ConfiguringOauthProviders.md => ConfiguringOauthProviders.md (100%) diff --git a/.github/assetatlas/ConfiguringOauthProviders.md b/ConfiguringOauthProviders.md similarity index 100% rename from .github/assetatlas/ConfiguringOauthProviders.md rename to ConfiguringOauthProviders.md From 9c7746e0a7d80c0a4c8ae322f1be49917c124368 Mon Sep 17 00:00:00 2001 From: Nathan Yang Date: Tue, 21 Apr 2026 20:36:42 -0400 Subject: [PATCH 27/51] fix dialog closing without confirmation --- src/lib/components/Dialog.svelte | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/lib/components/Dialog.svelte b/src/lib/components/Dialog.svelte index 7ad3e3b..3a55eb9 100644 --- a/src/lib/components/Dialog.svelte +++ b/src/lib/components/Dialog.svelte @@ -59,6 +59,11 @@ { + if (event.key === "Escape") { + handleClose(event); + } + }} class="glass dialog-component self-center {isLarge ? 'large-dialog-noscroll' : ''}" From 5e218a0087b6d8bd00f0533b553e597f54bc6aee Mon Sep 17 00:00:00 2001 From: Emile Marois Date: Mon, 27 Apr 2026 14:35:37 -0400 Subject: [PATCH 28/51] Moved OauthDoc to docs folder --- README.md | 2 +- .../ConfiguringOauthProviders.md | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename ConfiguringOauthProviders.md => docs/ConfiguringOauthProviders.md (100%) diff --git a/README.md b/README.md index f24a762..0b353db 100644 --- a/README.md +++ b/README.md @@ -71,4 +71,4 @@ docker compose -f docker-compose-ghcr-tailscale.yml down ## Also -See [ConfiguringOauthProviders.md](./ConfiguringOauthProviders.md) for instructions on how to set up OAuth providers to allow users to log in and log out of their accounts +See [ConfiguringOauthProviders.md](./docs/ConfiguringOauthProviders.md) for instructions on how to set up OAuth providers to allow users to log in and log out of their accounts diff --git a/ConfiguringOauthProviders.md b/docs/ConfiguringOauthProviders.md similarity index 100% rename from ConfiguringOauthProviders.md rename to docs/ConfiguringOauthProviders.md From 27ba7b2dbbfe9058c7e1905f1a8ed06086974f0e Mon Sep 17 00:00:00 2001 From: Nathan Yang Date: Mon, 27 Apr 2026 16:15:54 -0400 Subject: [PATCH 29/51] curse you linter --- src/lib/components/CreateItemDesktop.svelte | 4 ++-- src/lib/components/Dialog.svelte | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/lib/components/CreateItemDesktop.svelte b/src/lib/components/CreateItemDesktop.svelte index 34a7ed3..041e2c0 100644 --- a/src/lib/components/CreateItemDesktop.svelte +++ b/src/lib/components/CreateItemDesktop.svelte @@ -103,8 +103,8 @@ formContainer?.scrollTo(0,0); resetAllFields(); }} - requireCloseConfirmation={true} - > + requireCloseConfirmation={true} +> {#if originalItem}

    Duplicate & Edit Item diff --git a/src/lib/components/Dialog.svelte b/src/lib/components/Dialog.svelte index 867aadc..3a55eb9 100644 --- a/src/lib/components/Dialog.svelte +++ b/src/lib/components/Dialog.svelte @@ -68,7 +68,6 @@ ? 'large-dialog-noscroll' : ''}" style="overflow: {canOverflow ? 'visible' : 'auto'}" - oncancel={handleClose} bind:this={dialog}> {@render children?.()} From 2981e711fe98e420cb19e5d33d9cfc41407a806c Mon Sep 17 00:00:00 2001 From: Nathan Yang Date: Mon, 27 Apr 2026 17:18:02 -0400 Subject: [PATCH 30/51] refresh item windows after edits --- src/lib/components/EditItem.svelte | 6 ----- src/routes/view/[id]/+page.svelte | 35 +++++++++++++----------------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/lib/components/EditItem.svelte b/src/lib/components/EditItem.svelte index 650575f..b47eb13 100644 --- a/src/lib/components/EditItem.svelte +++ b/src/lib/components/EditItem.svelte @@ -631,10 +631,6 @@ }), ); formData.append("customFields", JSON.stringify(formattedFields)); - //TODO Remove after testing - for (const [key, value] of formData.entries()) { - console.log(`${key}: ${value}`); - } // Handle image if (removeExistingImage) { @@ -653,8 +649,6 @@ if (!response.ok) throw new Error(data.message || "Failed to update item"); - // Notify parent and close dialog - dispatch("close"); actionStore.addMessage("Item updated successfully"); dispatch("itemUpdated"); } catch (err) { diff --git a/src/routes/view/[id]/+page.svelte b/src/routes/view/[id]/+page.svelte index f97d27b..1cd7482 100644 --- a/src/routes/view/[id]/+page.svelte +++ b/src/routes/view/[id]/+page.svelte @@ -38,9 +38,9 @@ let itemTree = $state<{ reload: () => Promise } | null>(null); let mainItemDetails = $state<{ reload: () => Promise } | null>(null); let draggingItem = $state(null); - let targetItemId = $state(undefined); - let targetItemName = $state(undefined); let currentLogin = $state(); + let availableItems = $state([]); + let additionalWindows = $state([]); $effect(() => { if (browser && item) { @@ -56,6 +56,14 @@ unique = {}; } + async function refreshWindows() { + if (showItemTree) { + await itemTree?.reload(); + } + await mainItemDetails?.reload(); + await refreshAdditionalItemWindows(); + } + async function refreshAdditionalItemWindows() { await Promise.all( additionalWindows.map( @@ -76,19 +84,13 @@ const data: IBasicItemPopulated = await response.json(); item = data; restart(); - if (showItemTree) { - await itemTree?.reload(); - } - await mainItemDetails?.reload(); - await refreshAdditionalItemWindows(); + await refreshWindows(); } catch (err) { console.error(err); item = null; } } - let availableItems = $state([]); - function handleDelete() { deleteDialog?.close(); goto(resolve("/")); @@ -105,8 +107,6 @@ detailsRef: { reload: () => Promise } | null; } - let additionalWindows = $state([]); - function handleOpenItem(event: CustomEvent) { const { id } = event.detail; @@ -245,8 +245,8 @@ parentId={item._id.toString()} currentId={item._id.toString()} {draggingItem} - {targetItemId} - {targetItemName} + targetItemId={targetItem?._id.toString()} + targetItemName={targetItem?.name} showMoveDialog={false} useWindowView={true} on:openItem={handleOpenItem} /> @@ -324,18 +324,13 @@ create={() => {}} close={() => { editDialog?.close(); - targetItem = null; }}> { + on:itemUpdated={() => { + refreshWindows(); editDialog?.close(); targetItem = null; - }} - on:itemUpdated={() => { - if (data.item?._id && targetItem?._id && data.item._id.toString() === targetItem._id.toString()) { - fetchItem(data.item._id.toString()); - } }} />

    {/if} From 1a997648b3529d457e2c97ef2a454fe5d1061f1e Mon Sep 17 00:00:00 2001 From: Emile Marois Date: Tue, 28 Apr 2026 10:40:55 -0400 Subject: [PATCH 31/51] Added new sorting options Co-authored-by: Copilot --- src/routes/+page.svelte | 2 ++ src/routes/api/items/search/+server.ts | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 69f2393..1d9bbf5 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -245,9 +245,11 @@ bind:value={sortOption} onchange={handleSortChange}> + + {/if} diff --git a/src/routes/api/items/search/+server.ts b/src/routes/api/items/search/+server.ts index cd96fe0..ccab874 100644 --- a/src/routes/api/items/search/+server.ts +++ b/src/routes/api/items/search/+server.ts @@ -10,12 +10,16 @@ function sortItems(items: IBasicItemPopulated[], sortOption: string): IBasicItem switch (sortOption) { case 'alphabetical': return a.name.localeCompare(b.name); + case 'reverseAlphabetical': + return b.name.localeCompare(a.name); case 'firstAdded': return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime(); case 'lastAdded': return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(); case 'recentlyChanged': return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime(); + case 'oldestChanged': + return new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime(); default: return a.name.localeCompare(b.name); } From a7cfd245e376521588a855420ff868da82d2f309 Mon Sep 17 00:00:00 2001 From: Emile Marois Date: Tue, 28 Apr 2026 12:56:55 -0400 Subject: [PATCH 32/51] Added new getLastUpdatedItem function --- src/routes/+page.svelte | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 69f2393..2d06ced 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -34,6 +34,7 @@ }>(); let searchResults = $state([]); + let lastUpdatedItem = $state(null); let sortOption = $state("alphabetical"); let exactSearch = $state(false); let viewMode = $state("list"); @@ -104,6 +105,7 @@ const data = await response.json(); searchResults = data as IBasicItemPopulated[]; itemCount = searchResults.length; + getLastUpdatedItem(); if (showItemTree && itemTreeRef) { await itemTreeRef.reload(); } @@ -112,6 +114,31 @@ } } + async function getLastUpdatedItem(){ + try { + const response = await fetch( + `/api/items/search?` + + `name=${encodeURIComponent('')}&` + + `sort=${encodeURIComponent('recentlyChanged')}&` + + `exact=${'false'}`, + { + method: "GET", + headers: { "Content-Type": "application/json" }, + }, + ); + + if (!response.ok) throw new Error("Failed to fetch items"); + + const data = await response.json(); + const fullSearchResults = data as IBasicItemPopulated[]; + if(fullSearchResults.length > 0){ + lastUpdatedItem = fullSearchResults[0]; + } + } catch (err) { + console.error("Home: Error searching items:", err); + } + } + function toggleView() { if (viewMode === "list") { viewMode = "tree"; @@ -279,6 +306,20 @@ +
    + Last Edited Item: + {#if itemCount > 0} + getLastUpdatedItem} + bind:showMoveDialog + bind:draggingItem + bind:targetItemId + bind:targetItemName /> + {/if} + +
    + {#if viewMode === "list"} {#if itemCount > 0} Date: Tue, 28 Apr 2026 14:46:41 -0400 Subject: [PATCH 33/51] Fixed issues + improved style Co-authored-by: Copilot --- src/lib/components/SingleItemCard.svelte | 92 ++++++++++++++++++++++++ src/lib/styles/main.css | 12 ++++ src/routes/+page.svelte | 30 ++++---- 3 files changed, 117 insertions(+), 17 deletions(-) create mode 100644 src/lib/components/SingleItemCard.svelte diff --git a/src/lib/components/SingleItemCard.svelte b/src/lib/components/SingleItemCard.svelte new file mode 100644 index 0000000..da70b1d --- /dev/null +++ b/src/lib/components/SingleItemCard.svelte @@ -0,0 +1,92 @@ + + + + + diff --git a/src/lib/styles/main.css b/src/lib/styles/main.css index cc21549..3d6610b 100644 --- a/src/lib/styles/main.css +++ b/src/lib/styles/main.css @@ -46,6 +46,18 @@ border-radius: 10px; } +.last-edited-item-panel { + width: 50%; + max-width: 50%; +} + +@media (max-width: 768px) { + .last-edited-item-panel { + width: 100%; + max-width: 100%; + } +} + /* For components inset within other components*/ .internal-component { margin: 1rem 0; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 2d06ced..c0b84aa 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -4,6 +4,7 @@ import Dialog from "$lib/components/Dialog.svelte"; import EditItem from "$lib/components/EditItem.svelte"; import ItemContainer from "$lib/components/ItemContainer.svelte"; + import SingleItemCard from "$lib/components/SingleItemCard.svelte"; import ItemDetails from "$lib/components/ItemDetails.svelte"; import ItemTree from "$lib/components/ItemTree.svelte"; import Menu from "$lib/components/Menu.svelte"; @@ -131,11 +132,9 @@ const data = await response.json(); const fullSearchResults = data as IBasicItemPopulated[]; - if(fullSearchResults.length > 0){ - lastUpdatedItem = fullSearchResults[0]; - } + lastUpdatedItem = fullSearchResults[0] ?? null; } catch (err) { - console.error("Home: Error searching items:", err); + console.error("Home: Error loading last updated item:", err); } } @@ -234,6 +233,7 @@ onMount(() => { document.title = "Home - AssetAtlas"; restoreToggleStates(); + void getLastUpdatedItem(); unsubscribe = topBarHeight.subscribe((value) => { currentTopBarHeight = value; }); @@ -305,20 +305,16 @@ - -
    - Last Edited Item: - {#if itemCount > 0} - getLastUpdatedItem} - bind:showMoveDialog - bind:draggingItem - bind:targetItemId - bind:targetItemName /> - {/if} + + {#if lastUpdatedItem} +
    + Last Edited Item: -
    + + + +
    + {/if} {#if viewMode === "list"} {#if itemCount > 0} From 7c76cd6a3ab3d4eeb50999695b1259bd1d19a4e4 Mon Sep 17 00:00:00 2001 From: rhit-steineig <116207222+rhit-steineig@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:24:03 -0400 Subject: [PATCH 34/51] additional refactoring to further reduce duplication and increase clarity in functionality that gets item names/ids --- cspell.json | 2 + src/lib/components/CreateItemDesktop.svelte | 11 +++-- src/lib/components/CreateItemMobile.svelte | 10 ++--- src/lib/components/CustomFieldPicker.svelte | 4 +- src/lib/components/EditItem.svelte | 43 ++----------------- src/lib/components/ItemDetails.svelte | 21 +-------- src/lib/stores/createItemStore.svelte.ts | 12 +++--- src/lib/utility/attributeRetriever.ts | 31 +++++++++++++ src/lib/utility/checkItemHelper.ts | 31 ------------- .../api/customFields/checkItemId/+server.ts | 6 --- .../api/customFields/checkItemName/+server.ts | 6 --- .../getItemIdGivenName/+server.ts | 6 +++ .../getItemNameGivenId/+server.ts | 6 +++ tests/customField.test.ts | 20 +++------ 14 files changed, 75 insertions(+), 134 deletions(-) create mode 100644 src/lib/utility/attributeRetriever.ts delete mode 100644 src/lib/utility/checkItemHelper.ts delete mode 100644 src/routes/api/customFields/checkItemId/+server.ts delete mode 100644 src/routes/api/customFields/checkItemName/+server.ts create mode 100644 src/routes/api/customFields/getItemIdGivenName/+server.ts create mode 100644 src/routes/api/customFields/getItemNameGivenId/+server.ts diff --git a/cspell.json b/cspell.json index 9bebe09..b40beff 100644 --- a/cspell.json +++ b/cspell.json @@ -12,7 +12,9 @@ "oklch", "pycache", "pyenv", + "Recents", "rollup", + "skeletonlabs", "vite" ], // flagWords - list of words to be always considered incorrect diff --git a/src/lib/components/CreateItemDesktop.svelte b/src/lib/components/CreateItemDesktop.svelte index 08b2ba0..33c1cfd 100644 --- a/src/lib/components/CreateItemDesktop.svelte +++ b/src/lib/components/CreateItemDesktop.svelte @@ -1,9 +1,10 @@
    - {#if !getEditOnLogin() || ($login?.isLoggedIn && $login?.permissionLevel > 1)} + {#if permissionsAllowEdit(2)} - {#if !getEditOnLogin() || ($login?.isLoggedIn && $login?.permissionLevel > 1)} + {#if permissionsAllowEdit(2)}
    - {#if lastUpdatedItem} -
    - Last Edited Item: - - - - -
    - {/if} + {#if viewMode === "list"} {#if itemCount > 0} + {#if lastUpdatedItem} +
    + Last Edited Item: + + + + +
    + {/if} handleSearch(searchQuery)} @@ -356,6 +358,16 @@ {/if} {:else if showItemTree} + {#if lastUpdatedItem} + + + + + {/if} Date: Sun, 3 May 2026 19:46:09 -0400 Subject: [PATCH 39/51] reduced duplicated login logic --- src/lib/utility/loginHelper.ts | 24 +++++++++++++++++++ src/routes/api/auth/login/+server.ts | 27 ++++----------------- src/routes/api/auth/register/+server.ts | 32 +++++++------------------ 3 files changed, 37 insertions(+), 46 deletions(-) create mode 100644 src/lib/utility/loginHelper.ts diff --git a/src/lib/utility/loginHelper.ts b/src/lib/utility/loginHelper.ts new file mode 100644 index 0000000..941aba7 --- /dev/null +++ b/src/lib/utility/loginHelper.ts @@ -0,0 +1,24 @@ +import { json } from '@sveltejs/kit'; +import jwt from 'jsonwebtoken'; + +const JWT_SECRET = process.env.JWT_SECRET || 'your_jwt_secret_key'; +const TOKEN_EXPIRATION = '24h'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function loginCore(user: any, successMessage: string, successCode: number){ + const token = jwt.sign( + { id: user._id, username: user.username, permissionLevel: user.permissionLevel }, + JWT_SECRET, + { expiresIn: TOKEN_EXPIRATION } + ); + + return json({ + message: successMessage, + token, + user: { + id: user._id, + username: user.username, + permissionLevel: user.permissionLevel + } + }, { status: successCode }); +} diff --git a/src/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts index 8bc609e..dff8aff 100644 --- a/src/routes/api/auth/login/+server.ts +++ b/src/routes/api/auth/login/+server.ts @@ -1,9 +1,7 @@ -import { json, error } from '@sveltejs/kit'; -import type { RequestHandler } from '@sveltejs/kit'; -import jwt from 'jsonwebtoken'; import User from '$lib/server/db/models/user.js'; - -const JWT_SECRET = process.env.JWT_SECRET || 'your_jwt_secret_key'; +import { loginCore } from '$lib/utility/loginHelper'; +import type { RequestHandler } from '@sveltejs/kit'; +import { error } from '@sveltejs/kit'; export const POST: RequestHandler = async ({ request }) => { const { username, password } = await request.json(); @@ -20,20 +18,5 @@ export const POST: RequestHandler = async ({ request }) => { throw error(401, 'Invalid credentials'); } - // Generate JWT token - const token = jwt.sign( - { id: user._id, username: user.username, permissionLevel: user.permissionLevel }, - JWT_SECRET, - { expiresIn: '24h' } - ); - - return json({ - message: 'Login successful', - token, - user: { - id: user._id, - username: user.username, - permissionLevel: user.permissionLevel - } - }, { status: 200 }); -}; \ No newline at end of file + return loginCore(user, 'Login successful', 200); +}; diff --git a/src/routes/api/auth/register/+server.ts b/src/routes/api/auth/register/+server.ts index a8e3478..e7ecd14 100644 --- a/src/routes/api/auth/register/+server.ts +++ b/src/routes/api/auth/register/+server.ts @@ -1,9 +1,7 @@ -import { json, error } from '@sveltejs/kit'; -import type { RequestHandler } from '@sveltejs/kit'; -import jwt from 'jsonwebtoken'; import User from '$lib/server/db/models/user.js'; - -const JWT_SECRET = process.env.JWT_SECRET || 'your_jwt_secret_key'; +import { loginCore } from '$lib/utility/loginHelper'; +import type { RequestHandler } from '@sveltejs/kit'; +import { error } from '@sveltejs/kit'; export const POST: RequestHandler = async ({ request }) => { const { username, password } = await request.json(); @@ -22,28 +20,14 @@ export const POST: RequestHandler = async ({ request }) => { const user = new User({ username, passwordHash: password, - // First user gets level 10, after default is 1 + // First user gets level 10, afterwards default is 1 permissionLevel: isFirstUser ? 10 : 1 }); await user.save(); - // Generate JWT token - const token = jwt.sign( - { id: user._id, username: user.username, permissionLevel: user.permissionLevel }, - JWT_SECRET, - { expiresIn: '24h' } - ); + const userIdentifier = isFirstUser ? 'Admin user' : 'User'; + const successMessage = userIdentifier + " registered successfully"; - return json({ - message: isFirstUser ? - 'Admin user registered successfully' : - 'User registered successfully', - token, - user: { - id: user._id, - username: user.username, - permissionLevel: user.permissionLevel - } - }, { status: 201 }); -}; \ No newline at end of file + return loginCore(user, successMessage, 201); +}; From 2cf16a3c811af9e42d7ebe97523598e57111ee0b Mon Sep 17 00:00:00 2001 From: rhit-steineig <116207222+rhit-steineig@users.noreply.github.com> Date: Sun, 3 May 2026 19:59:36 -0400 Subject: [PATCH 40/51] general auth cleanup --- src/lib/server/auth.ts | 7 +++---- src/lib/server/db/models/authenticatorAccount.ts | 4 ++-- src/lib/server/db/models/login.ts | 4 ++-- src/lib/server/db/models/user.ts | 6 +++--- src/routes/api/auth/permisisons/+server.ts | 7 ++++--- src/routes/api/auth/profile/+server.ts | 4 ++-- src/routes/api/auth/users/+server.ts | 8 ++++---- src/routes/users/+page.server.ts | 2 +- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 26ed36b..03ed8f9 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -1,6 +1,6 @@ -import jwt from 'jsonwebtoken'; import type { RequestEvent } from '@sveltejs/kit'; import { error } from '@sveltejs/kit'; +import jwt from 'jsonwebtoken'; const JWT_SECRET = process.env.JWT_SECRET || 'your_jwt_secret_key'; @@ -19,11 +19,10 @@ interface OAuthJWTPayload { type JWTPayload = UserJWTPayload | OAuthJWTPayload; -function isOAuthPayload(payload: JWTPayload): payload is OAuthJWTPayload { +export function isOAuthPayload(payload: JWTPayload): payload is OAuthJWTPayload { return 'sub_id' in payload; } - export function verifyToken(token: string | undefined): JWTPayload | null { if (!token || token === '') return null; @@ -75,4 +74,4 @@ export function getUserId(payload: JWTPayload): string { export function getUserName(payload: JWTPayload): string { return isOAuthPayload(payload) ? payload.name : payload.username; -} \ No newline at end of file +} diff --git a/src/lib/server/db/models/authenticatorAccount.ts b/src/lib/server/db/models/authenticatorAccount.ts index 5cbf7a6..ff0e075 100644 --- a/src/lib/server/db/models/authenticatorAccount.ts +++ b/src/lib/server/db/models/authenticatorAccount.ts @@ -1,5 +1,5 @@ -import mongoose from 'mongoose'; import type { Document } from 'mongoose'; +import mongoose from 'mongoose'; const { Schema, model } = mongoose; export interface IAuthenticatorAccount extends Document { @@ -22,4 +22,4 @@ interface AuthenticatorAccountModel extends mongoose.Model; } -export const AuthenticatorAccount = model('AuthenticatorAccount', AuthenticatorAccountSchema); \ No newline at end of file +export const AuthenticatorAccount = model('AuthenticatorAccount', AuthenticatorAccountSchema); diff --git a/src/lib/server/db/models/login.ts b/src/lib/server/db/models/login.ts index cfdc268..68ab547 100644 --- a/src/lib/server/db/models/login.ts +++ b/src/lib/server/db/models/login.ts @@ -1,5 +1,5 @@ -import mongoose, { Types } from 'mongoose'; import type { Document } from 'mongoose'; +import mongoose, { Types } from 'mongoose'; const { Schema, model } = mongoose; export enum ServiceType { @@ -37,4 +37,4 @@ interface LoginModel extends mongoose.Model { findByLoginId(login_id: string): Promise; } -export const Login = model('Login', LoginSchema); \ No newline at end of file +export const Login = model('Login', LoginSchema); diff --git a/src/lib/server/db/models/user.ts b/src/lib/server/db/models/user.ts index 0f3d6d7..f54578e 100644 --- a/src/lib/server/db/models/user.ts +++ b/src/lib/server/db/models/user.ts @@ -1,12 +1,12 @@ -import mongoose, { Types } from 'mongoose'; +import bcrypt from 'bcryptjs'; import type { Document } from 'mongoose'; +import mongoose, { Types } from 'mongoose'; const { Schema, model } = mongoose; -import bcrypt from 'bcryptjs'; export interface IUser extends Document { _id: Types.ObjectId; username: string; - passwordHash: string; //we dont store actual password + passwordHash: string; //we don't store actual password permissionLevel: number; createdAt: Date; updatedAt: Date; diff --git a/src/routes/api/auth/permisisons/+server.ts b/src/routes/api/auth/permisisons/+server.ts index 3b85741..9b3555a 100644 --- a/src/routes/api/auth/permisisons/+server.ts +++ b/src/routes/api/auth/permisisons/+server.ts @@ -1,4 +1,4 @@ -import { requirePermissionLevel } from '$lib/server/auth.js'; +import { isOAuthPayload, requirePermissionLevel } from '$lib/server/auth.js'; import User from '$lib/server/db/models/user.js'; import type { RequestHandler } from '@sveltejs/kit'; import { error, json } from '@sveltejs/kit'; @@ -28,7 +28,8 @@ export const PUT: RequestHandler = async (event) => { } // Don't allow modifying own permissions - if (targetUser._id.toString() === currentUser.id) { + let currentUserId: string = isOAuthPayload(currentUser) ? currentUser.sub_id : currentUser.id; + if (targetUser._id.toString() === currentUserId) { throw error(400, 'Cannot modify your own permission level'); } @@ -53,4 +54,4 @@ export const PUT: RequestHandler = async (event) => { permissionLevel: targetUser.permissionLevel } }); -}; \ No newline at end of file +}; diff --git a/src/routes/api/auth/profile/+server.ts b/src/routes/api/auth/profile/+server.ts index 6fdf114..2ed32d4 100644 --- a/src/routes/api/auth/profile/+server.ts +++ b/src/routes/api/auth/profile/+server.ts @@ -1,4 +1,4 @@ -import { requireAuth } from '$lib/server/auth.js'; +import { isOAuthPayload, requireAuth } from '$lib/server/auth.js'; import { Login } from '$lib/server/db/models/login.js'; import User from '$lib/server/db/models/user.js'; import type { RequestHandler } from '@sveltejs/kit'; @@ -9,7 +9,7 @@ export const GET: RequestHandler = async (event) => { const authUser = requireAuth(event); // Check if OAuth (Login model) - if ('sub_id' in authUser) { + if (isOAuthPayload(authUser)) { const login = await Login.findOne({ login_id: authUser.sub_id }); if (!login) { throw error(404, 'User not found'); diff --git a/src/routes/api/auth/users/+server.ts b/src/routes/api/auth/users/+server.ts index 725f167..fca7b2d 100644 --- a/src/routes/api/auth/users/+server.ts +++ b/src/routes/api/auth/users/+server.ts @@ -1,8 +1,8 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from '@sveltejs/kit'; import { requirePermissionLevel } from '$lib/server/auth.js'; -import User from '$lib/server/db/models/user.js'; import { Login } from '$lib/server/db/models/login.js'; +import User from '$lib/server/db/models/user.js'; +import type { RequestHandler } from '@sveltejs/kit'; +import { json } from '@sveltejs/kit'; export const GET: RequestHandler = async (event) => { try { @@ -39,4 +39,4 @@ export const GET: RequestHandler = async (event) => { error: err instanceof Error ? err.message : String(err) }, { status: 500 }); } -}; \ No newline at end of file +}; diff --git a/src/routes/users/+page.server.ts b/src/routes/users/+page.server.ts index e1ebca4..782646e 100644 --- a/src/routes/users/+page.server.ts +++ b/src/routes/users/+page.server.ts @@ -6,4 +6,4 @@ export const load: PageServerLoad = async ({ cookies }) => { return { isLoggedIn: !!authToken }; -}; \ No newline at end of file +}; From 8877da2f6cc3989d04a3d0d73839ce875da0551e Mon Sep 17 00:00:00 2001 From: Nathan Yang Date: Sun, 3 May 2026 20:31:30 -0400 Subject: [PATCH 41/51] Update item windows on home page tree view --- src/routes/+page.svelte | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 3de8468..c71d699 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -148,8 +148,18 @@ additionalItemWindows = removeItemWindow(additionalItemWindows, id); } + async function refreshAdditionalItemWindows() { + await Promise.all( + additionalItemWindows.map( + (windowItem) => windowItem.detailsRef?.reload() ?? Promise.resolve(), + ), + ); + } + async function handleItemUpdated() { await handleSearch(searchQuery); + await refreshAdditionalItemWindows(); + actionEditDialog?.close(); } const handleShowActionDialog = ( @@ -327,6 +337,7 @@ on:close={() => handleCloseWindow(window.id)} on:openNewTab={() => openInNewTab(window.id)}> From 8be491661acd1f1d9d72ec28c8812d4204df7038 Mon Sep 17 00:00:00 2001 From: Ian Steiner <116207222+rhit-steineig@users.noreply.github.com> Date: Mon, 4 May 2026 01:10:42 +0000 Subject: [PATCH 42/51] Fixed spelling and updated cspell (#374) --- cspell.json | 20 +++++++++ src/lib/components/DeleteItem.svelte | 2 +- src/lib/server/db/models/basicItem.ts | 4 +- src/lib/server/db/models/user.ts | 2 +- src/lib/styles/main.css | 10 ++--- .../CSVFormatterPopulated.ts | 22 +++++----- .../{formating => formatting}/Formatter.ts | 0 src/lib/utility/index.ts | 2 +- .../{permisisons => permissions}/+server.ts | 0 src/routes/api/authenticator/check/+server.ts | 4 +- src/routes/api/items/move/+server.ts | 2 +- src/routes/utility/+page.svelte | 2 +- src/routes/view/[id]/+page.svelte | 6 +-- start.py | 6 +-- tests/auth.test.ts | 44 +++++++++---------- tests/itemExport.test.ts | 2 +- tests/login.test.ts | 16 +++---- tests/performance.test.ts | 2 +- tests/templateExport.test.ts | 2 +- 19 files changed, 82 insertions(+), 66 deletions(-) rename src/lib/utility/{formating => formatting}/CSVFormatterPopulated.ts (93%) rename src/lib/utility/{formating => formatting}/Formatter.ts (100%) rename src/routes/api/auth/{permisisons => permissions}/+server.ts (100%) diff --git a/cspell.json b/cspell.json index 9bebe09..b9c79e8 100644 --- a/cspell.json +++ b/cspell.json @@ -6,13 +6,33 @@ "language": "en", // words - list of words to be always considered correct "words": [ + "assetatlas", "buildx", + "columnspan", + "customfields", "dbmdl", + "deleter", + "dropshadow", "gridfs", + "hoverable", + "hovertiptext", + "keyuri", + "noscroll", "oklch", + "otpauth", + "otplib", + "padx", + "pady", "pycache", "pyenv", + "qrcode", + "Recents", "rollup", + "skeletonlabs", + "subcard", + "tailscalecompose", + "topbar", + "totp", "vite" ], // flagWords - list of words to be always considered incorrect diff --git a/src/lib/components/DeleteItem.svelte b/src/lib/components/DeleteItem.svelte index 4952c93..5007ae2 100644 --- a/src/lib/components/DeleteItem.svelte +++ b/src/lib/components/DeleteItem.svelte @@ -29,7 +29,7 @@ if (response.ok) { onDelete(itemId); actionStore.addMessage('Item deleted successfully!'); - //Use the same navigation pattern that ItemContainer uses (IF WE DONT DO THIS THEN POPUP DISAPPEARS) + //Use the same navigation pattern that ItemContainer uses (IF WE DON'T DO THIS THEN POPUP DISAPPEARS) window.history.pushState({}, '', '/'); window.dispatchEvent(new PopStateEvent('popstate')); } else { diff --git a/src/lib/server/db/models/basicItem.ts b/src/lib/server/db/models/basicItem.ts index 21e7f79..56da22f 100644 --- a/src/lib/server/db/models/basicItem.ts +++ b/src/lib/server/db/models/basicItem.ts @@ -57,7 +57,7 @@ export interface IBasicItemPopulated { } const BasicItemSchema: Schema = new Schema({ - //id: { type: Number, unique: true }, //we dont need this because mongodb default _id works + //id: { type: Number, unique: true }, //we don't need this because mongodb default _id works name: { type: String, required: true }, pinned: { type: Boolean, default: false }, description: { type: String, required: false }, @@ -87,7 +87,7 @@ const BasicItemSchema: Schema = new Schema({ image: { type: String, required: false }, }, { - timestamps: true, //this should mean we dont need to state createdAt and updatedAt feilds + timestamps: true, //this should mean we don't need to state createdAt and updatedAt fields strict: false, } ); diff --git a/src/lib/server/db/models/user.ts b/src/lib/server/db/models/user.ts index 0f3d6d7..f7a69d1 100644 --- a/src/lib/server/db/models/user.ts +++ b/src/lib/server/db/models/user.ts @@ -6,7 +6,7 @@ import bcrypt from 'bcryptjs'; export interface IUser extends Document { _id: Types.ObjectId; username: string; - passwordHash: string; //we dont store actual password + passwordHash: string; //we don't store actual password permissionLevel: number; createdAt: Date; updatedAt: Date; diff --git a/src/lib/styles/main.css b/src/lib/styles/main.css index d362e69..dc77ce7 100644 --- a/src/lib/styles/main.css +++ b/src/lib/styles/main.css @@ -20,7 +20,7 @@ .top-bar-wrapper { width: 100%; position: relative; - /*Top bar on top bruh. Don't generate more than 9998 windows */ + /*Top bar on top. Don't generate more than 9998 windows */ z-index: 9999; } @@ -81,7 +81,7 @@ position: fixed; /* On top of topbar (to avoid topbar dropshadow)*/ z-index: 10000; - /* were using the CSS variable with a harcoded fallback for height*/ + /* were using the CSS variable with a hardcoded fallback for height*/ top: var(--top-bar-height, 64px); left: -100%; height: calc(100% - var(--top-bar-height, 64px)); @@ -349,7 +349,7 @@ gap: 1rem; /* Remove the top padding since we're using page-with-topbar */ padding: 0 3vw; - /* Keeps height consistant when swapping view styles*/ + /* Keeps height consistent when swapping view styles*/ height: 3rem; } @@ -541,7 +541,7 @@ /* Small component styles -e.g. buttons, lists, textareas, etc. +e.g. buttons, lists, text areas, etc. */ #resize-none-textarea { @@ -827,7 +827,7 @@ select option:hover { display: flex; flex-direction: column; gap: 0.5rem; - /* hope we dont make 10001 of something */ + /* hope we don't make 10001 of something */ z-index: 10000; width: auto; max-width: 400px; diff --git a/src/lib/utility/formating/CSVFormatterPopulated.ts b/src/lib/utility/formatting/CSVFormatterPopulated.ts similarity index 93% rename from src/lib/utility/formating/CSVFormatterPopulated.ts rename to src/lib/utility/formatting/CSVFormatterPopulated.ts index c030337..dcc27a4 100644 --- a/src/lib/utility/formating/CSVFormatterPopulated.ts +++ b/src/lib/utility/formatting/CSVFormatterPopulated.ts @@ -34,22 +34,22 @@ export class CSVFormatterPopulated { } formatTemplate(template: ITemplatePopulated) { - let firstline = ""; - let secondline = ""; - firstline += template.name + ","; - secondline += ","; + let firstLine = ""; + let secondLine = ""; + firstLine += template.name + ","; + secondLine += ","; for (let i = 0; i < template.fields.length; i++) { const customField = template.fields[i]; - firstline += customField.fieldName; - secondline += customField.dataType; + firstLine += customField.fieldName; + secondLine += customField.dataType; if (i != template.fields.length - 1) { - firstline += ","; - secondline += "," + firstLine += ","; + secondLine += "," } } - firstline += "\n"; - secondline += "\n"; - return firstline + secondline; + firstLine += "\n"; + secondLine += "\n"; + return firstLine + secondLine; } formatItems(): string { diff --git a/src/lib/utility/formating/Formatter.ts b/src/lib/utility/formatting/Formatter.ts similarity index 100% rename from src/lib/utility/formating/Formatter.ts rename to src/lib/utility/formatting/Formatter.ts diff --git a/src/lib/utility/index.ts b/src/lib/utility/index.ts index f0971a7..2f99105 100644 --- a/src/lib/utility/index.ts +++ b/src/lib/utility/index.ts @@ -2,7 +2,7 @@ import { CSVItemParserPopulated } from "./parsing/CSVItemParserPopulated.js"; import { CSVTemplateParser } from "./parsing/CSVTemplateParser.js"; import { FileLoader } from "./file/FileLoader.js"; import { ParserManager } from "./parsing/ParserManager.js"; -import { CSVFormatterPopulated } from "./formating/CSVFormatterPopulated.js"; +import { CSVFormatterPopulated } from "./formatting/CSVFormatterPopulated.js"; import { FileExporter } from "./file/FileExporter.js"; diff --git a/src/routes/api/auth/permisisons/+server.ts b/src/routes/api/auth/permissions/+server.ts similarity index 100% rename from src/routes/api/auth/permisisons/+server.ts rename to src/routes/api/auth/permissions/+server.ts diff --git a/src/routes/api/authenticator/check/+server.ts b/src/routes/api/authenticator/check/+server.ts index 7cb7321..ec28c0b 100644 --- a/src/routes/api/authenticator/check/+server.ts +++ b/src/routes/api/authenticator/check/+server.ts @@ -35,10 +35,10 @@ export const POST: RequestHandler = async ({ request }) => { const secret = account.login_id; // Create OTP auth URL - const otpauth = authenticator.keyuri(username, 'AssetAtlas', secret); + const otpAuth = authenticator.keyuri(username, 'AssetAtlas', secret); // Generate QR code - const qrCodeDataUrl = await qrcode.toDataURL(otpauth); + const qrCodeDataUrl = await qrcode.toDataURL(otpAuth); if (accountExisted) { return json({ diff --git a/src/routes/api/items/move/+server.ts b/src/routes/api/items/move/+server.ts index 1185fd6..a80e808 100644 --- a/src/routes/api/items/move/+server.ts +++ b/src/routes/api/items/move/+server.ts @@ -19,7 +19,7 @@ export const POST: RequestHandler = async ({ request }) => { const newParentItem = await BasicItem.findById(newParent).exec(); if (!newParentItem) { - throw error(404, 'New parent item dockernot found'); + throw error(404, 'New parent item docker not found'); } } diff --git a/src/routes/utility/+page.svelte b/src/routes/utility/+page.svelte index 7e659d7..546bd25 100644 --- a/src/routes/utility/+page.svelte +++ b/src/routes/utility/+page.svelte @@ -10,7 +10,7 @@ downloadDataFile, downloadFile, } from "$lib/utility/file/FileDownloader.js"; - import { CSVFormatterPopulated } from "$lib/utility/formating/CSVFormatterPopulated.js"; + import { CSVFormatterPopulated } from "$lib/utility/formatting/CSVFormatterPopulated.js"; import JSZip from "jszip"; import { onMount } from "svelte"; diff --git a/src/routes/view/[id]/+page.svelte b/src/routes/view/[id]/+page.svelte index f5062bf..43a5284 100644 --- a/src/routes/view/[id]/+page.svelte +++ b/src/routes/view/[id]/+page.svelte @@ -103,11 +103,9 @@ function handleOpenItem(event: CustomEvent) { const { id } = event.detail; const thisItemIsTheMainItem = id === data.item?._id; - if (thisItemIsTheMainItem) { - return; + if (!thisItemIsTheMainItem) { + additionalWindows = openItemHelper(additionalWindows, id); } - - additionalWindows = openItemHelper(additionalWindows, id); } function handleUpdateTitle(windowId: string, event: CustomEvent) { diff --git a/start.py b/start.py index 46a0ff7..b2a0130 100644 --- a/start.py +++ b/start.py @@ -10,8 +10,6 @@ from typing import List from env_writer import set_env_variable -# cspell:ignore padx pady - SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) IN_NEW_TAB = 2 @@ -163,7 +161,7 @@ def run_docker_compose(mode: str): try: url = "" base_compose_file = os.path.join(SCRIPT_DIR, "docker", "docker-compose.yml") - tailscalecompose__file = os.path.join(SCRIPT_DIR, "docker", "docker-compose-tailscale.yml") + tailscalecompose_file = os.path.join(SCRIPT_DIR, "docker", "docker-compose-tailscale.yml") if mode == "local": url = "http://localhost:3000" @@ -177,7 +175,7 @@ def run_docker_compose(mode: str): "-f", base_compose_file, "-f", - tailscalecompose__file, + tailscalecompose_file, "up", "-d", ] + (["--build"] if build_var.get() else []) diff --git a/tests/auth.test.ts b/tests/auth.test.ts index d1f4401..faa7e00 100644 --- a/tests/auth.test.ts +++ b/tests/auth.test.ts @@ -1,7 +1,7 @@ import { requireAuth, requirePermissionLevel } from '$lib/server/auth.js'; import User from '$lib/server/db/models/user.js'; import { POST as loginHandler } from '$routes/api/auth/login/+server.js'; -import { PUT as permissionsHandler } from '$routes/api/auth/permisisons/+server.js'; +import { PUT as permissionsHandler } from '$routes/api/auth/permissions/+server.js'; import { GET as profileHandler } from '$routes/api/auth/profile/+server.js'; import { POST as registerHandler } from '$routes/api/auth/register/+server.js'; import type { RequestEvent } from '@sveltejs/kit'; @@ -102,7 +102,7 @@ describe('Authentication API', () => { it('should register users and assign appropriate permission levels', async () => { // First user should get admin privileges (level 10) const firstUserData = { - username: 'adminuser', + username: 'adminUser', password: 'password123' }; @@ -117,13 +117,13 @@ describe('Authentication API', () => { expect(firstResponse.status).toBe(201); expect(firstBody).toHaveProperty('token'); - expect(firstBody.user).toHaveProperty('username', 'adminuser'); + expect(firstBody.user).toHaveProperty('username', 'adminUser'); expect(firstBody.user).toHaveProperty('permissionLevel', 10); expect(firstBody.message).toBe('Admin user registered successfully'); // Second user should get regular privileges (level 1) const secondUserData = { - username: 'regularuser', + username: 'regularUser', password: 'password123' }; @@ -138,13 +138,13 @@ describe('Authentication API', () => { expect(secondResponse.status).toBe(201); expect(secondBody).toHaveProperty('token'); - expect(secondBody.user).toHaveProperty('username', 'regularuser'); + expect(secondBody.user).toHaveProperty('username', 'regularUser'); expect(secondBody.user).toHaveProperty('permissionLevel', 1); expect(secondBody.message).toBe('User registered successfully'); // Verify users in database - const adminUser = await User.findOne({ username: 'adminuser' }).select('username permissionLevel'); - const regularUser = await User.findOne({ username: 'regularuser' }).select('username permissionLevel'); + const adminUser = await User.findOne({ username: 'adminUser' }).select('username permissionLevel'); + const regularUser = await User.findOne({ username: 'regularUser' }).select('username permissionLevel'); expect(adminUser).not.toBeNull(); expect(adminUser?.permissionLevel).toBe(10); @@ -156,7 +156,7 @@ describe('Authentication API', () => { it('should not register a user with duplicate username', async () => { // Register a user first const userData = { - username: 'testuser', + username: 'testUser', password: 'password123' }; @@ -189,7 +189,7 @@ describe('Authentication API', () => { it('should login a user with correct credentials', async () => { // Register a user first const userData = { - username: 'loginuser', + username: 'loginUser', password: 'password123' }; @@ -219,7 +219,7 @@ describe('Authentication API', () => { it('should not login with incorrect credentials', async () => { // Register user first const userData = { - username: 'loginuser', + username: 'loginUser', password: 'password123' }; @@ -233,8 +233,8 @@ describe('Authentication API', () => { // Try login with wrong password const loginData = { - username: 'loginuser', - password: 'wrongpassword' + username: 'loginUser', + password: 'wrongPassword' }; const loginEvent = createMockEvent({ @@ -256,7 +256,7 @@ describe('Authentication API', () => { it('should access protected route with valid token from login', async () => { // Register and login to get a token const userData = { - username: 'protecteduser', + username: 'protectedUser', password: 'password123' }; @@ -308,7 +308,7 @@ describe('Authentication API', () => { it('should deny access with invalid token', async () => { const protectedEvent = createMockEvent({ method: 'GET', - headers: { 'Authorization': 'Bearer invalidtoken' }, + headers: { 'Authorization': 'Bearer invalidToken' }, url: 'http://localhost:3000/api/mock/protected' }); @@ -322,7 +322,7 @@ describe('Authentication API', () => { it('should deny access to admin route with insufficient permissions', async () => { // Register admin user (first user) and regular user (second user) const adminUserData = { - username: 'adminuser', + username: 'adminUser', password: 'password123' }; @@ -335,7 +335,7 @@ describe('Authentication API', () => { await registerHandler(adminRegisterEvent); const regularUserData = { - username: 'regularuser', + username: 'regularUser', password: 'password123' }; @@ -375,7 +375,7 @@ describe('Authentication API', () => { it('should allow access to admin route with sufficient permissions', async () => { // Register first user (will automatically get admin level 10) const adminUserData = { - username: 'adminuser', + username: 'adminUser', password: 'password123' }; @@ -406,7 +406,7 @@ describe('Authentication API', () => { it('should retrieve user profile with valid token', async () => { // Register a user and use the token for authentication const userData = { - username: 'profileuser', + username: 'profileUser', password: 'password123' }; @@ -431,14 +431,14 @@ describe('Authentication API', () => { const body = await response.json(); expect(response.status).toBe(200); - expect(body).toHaveProperty('username', 'profileuser'); + expect(body).toHaveProperty('username', 'profileUser'); expect(body).not.toHaveProperty('passwordHash'); }); it('should allow admin users to update another user\'s permission level', async () => { // Register admin user const adminUserData = { - username: 'adminuser', + username: 'adminUser', password: 'password123' }; @@ -454,7 +454,7 @@ describe('Authentication API', () => { // Register regular user const regularUserData = { - username: 'regularuser', + username: 'regularUser', password: 'password123' }; @@ -502,7 +502,7 @@ describe('Authentication API', () => { it('should not allow a user to modify their own permission level', async () => { // Register admin user const adminUserData = { - username: 'adminuser', + username: 'adminUser', password: 'password123' }; diff --git a/tests/itemExport.test.ts b/tests/itemExport.test.ts index 3aa9d3c..3633c98 100644 --- a/tests/itemExport.test.ts +++ b/tests/itemExport.test.ts @@ -4,7 +4,7 @@ import { RecentItems } from '$lib/server/db/models/recentItems.js'; import Template, { type ITemplatePopulated } from '$lib/server/db/models/template.js'; import { FileExporter } from "$lib/utility/file/FileExporter.js"; import { FileLoader } from "$lib/utility/file/FileLoader.js"; -import { CSVFormatterPopulated } from "$lib/utility/formating/CSVFormatterPopulated.js"; +import { CSVFormatterPopulated } from "$lib/utility/formatting/CSVFormatterPopulated.js"; import { ParserManager } from "$lib/utility/parsing/ParserManager.js"; import { GET as searchItemsHandler } from '$routes/api/items/search/+server.js'; import { GET as getTemplatesHandler } from '$routes/api/templates/+server.js'; diff --git a/tests/login.test.ts b/tests/login.test.ts index b9b8de5..b8d90fa 100644 --- a/tests/login.test.ts +++ b/tests/login.test.ts @@ -351,7 +351,7 @@ describe('OAuth API', () => { const mockUser = { id: 123456789, name: 'Test User', - login: 'testuser' + login: 'testUser' }; mockValidateAuthorizationCode.mockResolvedValue({ @@ -404,7 +404,7 @@ describe('OAuth API', () => { const mockUser = { id: 987654321, name: 'Existing User', - login: 'existinguser' + login: 'existingUser' }; mockValidateAuthorizationCode.mockResolvedValue({ @@ -445,7 +445,7 @@ describe('OAuth API', () => { describe('POST /api/authenticator/check', () => { it('should create new authenticator account and return QR code', async () => { - const username = 'testuser'; + const username = 'testUser'; const event = createMockEvent({ method: 'POST', @@ -482,7 +482,7 @@ describe('OAuth API', () => { }); it('should see existing authenticator account and only return otpCode', async () => { - const username = 'testuser_existing'; + const username = 'testUser_existing'; const existingLogin = new Login({ login_id: 'mock-secret-exists', name: username, @@ -545,7 +545,7 @@ describe('OAuth API', () => { describe('GET /api/authenticator/verify', () => { it('should verify valid OTP code', async () => { - const username = 'testuser_existing'; + const username = 'testUser_existing'; const code = '123456'; const existingLogin = new Login({ @@ -617,7 +617,7 @@ describe('OAuth API', () => { }); it('should return 404 error for non-existent account', async () => { - const username = 'nonexistentuser'; + const username = 'nonExistentUser'; const code = '123'; const event = createMockEvent({ @@ -635,8 +635,8 @@ describe('OAuth API', () => { }); it('should return error for invalid OTP code', async () => { - const username = 'testuser_existing'; - const code = 'wrongcode'; + const username = 'testUser_existing'; + const code = 'wrongCode'; const existingLogin = new Login({ login_id: 'mock-secret-exists', name: username, diff --git a/tests/performance.test.ts b/tests/performance.test.ts index 223f3ea..54d6f00 100644 --- a/tests/performance.test.ts +++ b/tests/performance.test.ts @@ -1,7 +1,7 @@ import type { IBasicItemPopulated } from '$lib/server/db/models/basicItem.js'; import { FileExporter } from '$lib/utility/file/FileExporter.js'; import { FileLoader } from '$lib/utility/file/FileLoader.js'; -import { CSVFormatterPopulated } from '$lib/utility/formating/CSVFormatterPopulated.js'; +import { CSVFormatterPopulated } from '$lib/utility/formatting/CSVFormatterPopulated.js'; import { CSVItemParserPopulated } from '$lib/utility/parsing/CSVItemParserPopulated.js'; import { POST as createItemHandler } from '$routes/api/items/+server.js'; import { GET as searchItemsHandler } from '$routes/api/items/search/+server.js'; diff --git a/tests/templateExport.test.ts b/tests/templateExport.test.ts index 3d38d1e..9bdf5ce 100644 --- a/tests/templateExport.test.ts +++ b/tests/templateExport.test.ts @@ -1,6 +1,6 @@ import CustomField from "$lib/server/db/models/customField.js"; import Template, { type ITemplatePopulated } from "$lib/server/db/models/template.js"; -import { CSVFormatterPopulated } from "$lib/utility/formating/CSVFormatterPopulated.js"; +import { CSVFormatterPopulated } from "$lib/utility/formatting/CSVFormatterPopulated.js"; import { describe, expect, it } from "vitest"; describe("Testing Template Formatting", () => { From 61cab980dc7463c48a1d4bba52553b986b25a788 Mon Sep 17 00:00:00 2001 From: rhit-steineig <116207222+rhit-steineig@users.noreply.github.com> Date: Sun, 3 May 2026 16:44:31 -0400 Subject: [PATCH 43/51] home page item display area fits on screen --- src/lib/styles/main.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/styles/main.css b/src/lib/styles/main.css index dc77ce7..1aeff69 100644 --- a/src/lib/styles/main.css +++ b/src/lib/styles/main.css @@ -1,7 +1,7 @@ #home-component { - width: 100vw; + width: 96vw; margin: 1rem 2vw; - padding: 2rem 2% 1rem 2vw; + padding: 2rem 2vw 1rem; } .top-bar { From 6b5b3f902b3dfdd2fd58f18a45d87685277f9c35 Mon Sep 17 00:00:00 2001 From: rhit-steineig <116207222+rhit-steineig@users.noreply.github.com> Date: Sun, 3 May 2026 22:35:20 -0400 Subject: [PATCH 44/51] reduced duplication in callback code --- .../api/oauth/callbackGithub/+server.ts | 73 +++------------- .../api/oauth/callbackGoogle/+server.ts | 83 ++++-------------- src/routes/api/oauth/callbackHelper.ts | 87 +++++++++++++++++++ 3 files changed, 118 insertions(+), 125 deletions(-) create mode 100644 src/routes/api/oauth/callbackHelper.ts diff --git a/src/routes/api/oauth/callbackGithub/+server.ts b/src/routes/api/oauth/callbackGithub/+server.ts index d97d1bf..458a0cf 100644 --- a/src/routes/api/oauth/callbackGithub/+server.ts +++ b/src/routes/api/oauth/callbackGithub/+server.ts @@ -1,82 +1,35 @@ -import { redirect, error } from '@sveltejs/kit'; +import { ServiceType } from '$lib/server/db/models/login.js'; import type { RequestHandler } from '@sveltejs/kit'; -import jwt from 'jsonwebtoken'; +import { redirect, type Cookies } from '@sveltejs/kit'; import * as arctic from 'arctic'; -import { Login, ServiceType } from '$lib/server/db/models/login.js'; +import { callbackCore, getUser, processCallbackError, validateState } from '../callbackHelper'; -const JWT_SECRET = process.env.JWT_SECRET || 'your_jwt_secret_key'; const github_client_id = process.env.GITHUB_CLIENT_ID || ''; const github_client_secret = process.env.GITHUB_CLIENT_SECRET || ''; const github_redirect_uri = process.env.GITHUB_REDIRECT_URI || ''; export const GET: RequestHandler = async ({ url, cookies }) => { const code = url.searchParams.get('code'); - const returnedState = url.searchParams.get('state'); - const storedState = cookies.get('github_oauth_state'); - - if (!code || !returnedState || !storedState || returnedState !== storedState) { - throw error(400, 'Invalid state returned'); - } + validateState(url, cookies, code, "github_oauth_state"); try { const github = new arctic.GitHub(github_client_id, github_client_secret, github_redirect_uri); - const tokens = await github.validateAuthorizationCode(code); + const tokens = await github.validateAuthorizationCode(code as string); const accessToken = tokens.accessToken(); - const response = await fetch('https://api.github.com/user', { - headers: { - Authorization: `Bearer ${accessToken}` - } - }); - - const user = await response.json(); + const user = await getUser('https://api.github.com/user', accessToken); const id = String(user.id); const name = user.name || user.login; - let existingLogin = await Login.findOne({ login_id: id, service_type: ServiceType.GITHUB }); - - if (!existingLogin) { - const loginCount = await Login.countDocuments({}); - const isFirstLogin = loginCount === 0; - - const newLogin = new Login({ - login_id: id, - service_type: ServiceType.GITHUB, - name: name, - permissionLevel: isFirstLogin ? 10 : 1 - }); - - await newLogin.save(); - existingLogin = newLogin; - } - - const token = jwt.sign( - { - sub_id: existingLogin.login_id, - name: name, - permissionLevel: existingLogin.permissionLevel - }, - JWT_SECRET, - { expiresIn: '7d' } - ); - - cookies.delete('github_oauth_state', { path: '/' }); - cookies.set('auth_token', token, { - path: '/', - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - maxAge: 60 * 60 * 24 * 7 - }); + await callbackCore(id, name, ServiceType.GITHUB, cookies, deleteOldCookies); throw redirect(302, '/oauth/success'); } catch (e) { - if (e instanceof arctic.OAuth2RequestError) { - throw error(400, `OAuth2 request error: ${e.message}`); - } - if (e instanceof arctic.ArcticFetchError) { - throw error(400, `Arctic Fetch Error: ${e.message}`); - } - throw e; + processCallbackError(e); + return new Response('Authentication failed', { status: 400 }); } }; + +const deleteOldCookies = (cookies: Cookies) => { + cookies.delete('github_oauth_state', { path: '/' }); +} diff --git a/src/routes/api/oauth/callbackGoogle/+server.ts b/src/routes/api/oauth/callbackGoogle/+server.ts index 66ab5f0..f34aa04 100644 --- a/src/routes/api/oauth/callbackGoogle/+server.ts +++ b/src/routes/api/oauth/callbackGoogle/+server.ts @@ -1,88 +1,41 @@ -import { redirect, error } from '@sveltejs/kit'; +import { ServiceType } from '$lib/server/db/models/login.js'; import type { RequestHandler } from '@sveltejs/kit'; -import jwt from 'jsonwebtoken'; +import { error, redirect, type Cookies } from '@sveltejs/kit'; import * as arctic from 'arctic'; -import { Login, ServiceType } from '$lib/server/db/models/login.js'; +import { callbackCore, getUser, processCallbackError, validateState } from '../callbackHelper'; -const JWT_SECRET = process.env.JWT_SECRET || 'your_jwt_secret_key'; const google_client_id = process.env.GOOGLE_CLIENT_ID || ''; const google_client_secret = process.env.GOOGLE_CLIENT_SECRET || ''; const google_redirect_uri = process.env.GOOGLE_REDIRECT_URI || ''; export const GET: RequestHandler = async ({ url, cookies }) => { const code = url.searchParams.get('code'); - const returnedState = url.searchParams.get('state'); - const storedState = cookies.get('google_oauth_state'); - const codeVerifier = cookies.get('google_code_verifier'); - - if (!code || !returnedState || !storedState || returnedState !== storedState) { - throw error(400, 'Invalid state returned'); - } - - if (!codeVerifier) { + validateState(url, cookies, code, "google_oauth_state"); + + const googleCodeVerifier = cookies.get('google_code_verifier'); + if (!googleCodeVerifier) { throw error(400, 'Code verifier not found'); } try { const google = new arctic.Google(google_client_id, google_client_secret, google_redirect_uri); - const tokens = await google.validateAuthorizationCode(code, codeVerifier); + const tokens = await google.validateAuthorizationCode(code as string, googleCodeVerifier); const accessToken = tokens.accessToken(); - const response = await fetch('https://openidconnect.googleapis.com/v1/userinfo', { - headers: { - Authorization: `Bearer ${accessToken}` - } - }); - - const user = await response.json(); - const sub_id = user.sub; + const user = await getUser('https://openidconnect.googleapis.com/v1/userinfo', accessToken); + const id = user.sub; const name = user.name; - let existingLogin = await Login.findOne({ login_id: sub_id, service_type: ServiceType.GOOGLE }); - - if (!existingLogin) { - const loginCount = await Login.countDocuments({}); - const isFirstLogin = loginCount === 0; - - const newLogin = new Login({ - login_id: sub_id, - name: name, - service_type: ServiceType.GOOGLE, - permissionLevel: isFirstLogin ? 10 : 1 - }); - - await newLogin.save(); - existingLogin = newLogin; - } - - const token = jwt.sign( - { - sub_id: existingLogin.login_id, - name: name, - permissionLevel: existingLogin.permissionLevel - }, - JWT_SECRET, - { expiresIn: '7d' } - ); - - cookies.delete('google_oauth_state', { path: '/' }); - cookies.delete('google_code_verifier', { path: '/' }); - cookies.set('auth_token', token, { - path: '/', - httpOnly: true, - secure: process.env.NODE_ENV === 'production', - sameSite: 'lax', - maxAge: 60 * 60 * 24 * 7 - }); + await callbackCore(id, name, ServiceType.GOOGLE, cookies, deleteOldCookies); throw redirect(302, '/oauth/success'); } catch (e) { - if (e instanceof arctic.OAuth2RequestError) { - throw error(400, `OAuth2 request error: ${e.message}`); - } - if (e instanceof arctic.ArcticFetchError) { - throw error(400, `Arctic Fetch Error: ${e.message}`); - } - throw e; + processCallbackError(e); + return new Response('Authentication failed', { status: 400 }); } }; + +const deleteOldCookies = (cookies: Cookies) => { + cookies.delete('google_oauth_state', { path: '/' }); + cookies.delete('google_code_verifier', { path: '/' }); +} diff --git a/src/routes/api/oauth/callbackHelper.ts b/src/routes/api/oauth/callbackHelper.ts new file mode 100644 index 0000000..462b553 --- /dev/null +++ b/src/routes/api/oauth/callbackHelper.ts @@ -0,0 +1,87 @@ +import { Login, ServiceType } from '$lib/server/db/models/login.js'; +import { error, type Cookies } from '@sveltejs/kit'; +import * as arctic from 'arctic'; +import jwt from 'jsonwebtoken'; + +const JWT_SECRET = process.env.JWT_SECRET || 'your_jwt_secret_key'; +const TOKEN_EXPIRATION_STR = '7d'; +const TOKEN_EXPIRATION_SECONDS = 60 * 60 * 24 * 7; + +export function validateState(url: URL, cookies: Cookies, code: string | null, storedStateKey: string) { + const returnedState = url.searchParams.get('state'); + const storedState = cookies.get(storedStateKey); + + const valid = code && returnedState && storedState && returnedState === storedState; + if (!valid) { + throw error(400, 'Invalid state returned'); + } +} + +export async function getUser(url: string, accessToken: string){ + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${accessToken}` + } + }); + const user = await response.json(); + return user; +} + +export async function callbackCore(id: string, name: string, serviceType: ServiceType, cookies: Cookies, deleteOldCookies: (cookies: Cookies) => void) { + let existingLogin = await Login.findOne({ login_id: id, service_type: serviceType}); + if (!existingLogin) { + existingLogin = await getNewLogin(id, name, serviceType); + } + + const token = getToken(existingLogin, name); + deleteOldCookies(cookies); + setNewCookies(cookies, token); +} + +async function getNewLogin(id: string, name: string, serviceType: ServiceType,) { + const loginCount = await Login.countDocuments({}); + const isFirstLogin = loginCount === 0; + + const newLogin = new Login({ + login_id: id, + name: name, + service_type: serviceType, + permissionLevel: isFirstLogin ? 10 : 1 + }); + + await newLogin.save(); + return newLogin; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function getToken(existingLogin: any, name: string) { + return jwt.sign( + { + sub_id: existingLogin.login_id, + name: name, + permissionLevel: existingLogin.permissionLevel + }, + JWT_SECRET, + { expiresIn: TOKEN_EXPIRATION_STR } + ); +} + +function setNewCookies(cookies: Cookies, token: string){ + cookies.set('auth_token', token, { + path: '/', + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'lax', + maxAge: TOKEN_EXPIRATION_SECONDS + }); +} + +export function processCallbackError(callbackError: unknown) { + if (callbackError instanceof arctic.OAuth2RequestError) { + throw error(400, `OAuth2 request error: ${callbackError.message}`); + } + if (callbackError instanceof arctic.ArcticFetchError) { + throw error(400, `Arctic Fetch Error: ${callbackError.message}`); + } + throw callbackError; +} From 77dd8aa1d79303a2e7ebdfb38459e6a5ef2e6b9d Mon Sep 17 00:00:00 2001 From: Emile Marois Date: Tue, 28 Apr 2026 10:49:27 -0400 Subject: [PATCH 45/51] Improved dimensions for sorting options box --- src/lib/styles/main.css | 2 +- src/routes/+page.svelte | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/lib/styles/main.css b/src/lib/styles/main.css index cc21549..83a97d0 100644 --- a/src/lib/styles/main.css +++ b/src/lib/styles/main.css @@ -858,7 +858,7 @@ select option:hover { } .sort-container { - min-width: 8rem; + min-width: 11rem; max-width: 50%; } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 1d9bbf5..39e1979 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -246,10 +246,10 @@ onchange={handleSortChange}> - - - - + + + + {/if} From 16d38c15dce96e424c35a9514c9eb381139e9db7 Mon Sep 17 00:00:00 2001 From: Emile Marois Date: Tue, 12 May 2026 12:46:44 -0400 Subject: [PATCH 46/51] Moved style to main.css --- src/lib/components/SingleItemCard.svelte | 75 +++--------------------- src/lib/styles/main.css | 63 +++++++++++++++++++- 2 files changed, 70 insertions(+), 68 deletions(-) diff --git a/src/lib/components/SingleItemCard.svelte b/src/lib/components/SingleItemCard.svelte index 56d4e67..32935aa 100644 --- a/src/lib/components/SingleItemCard.svelte +++ b/src/lib/components/SingleItemCard.svelte @@ -2,6 +2,7 @@ import type { IBasicItemPopulated } from "$lib/server/db/models/basicItem.js"; import { createEventDispatcher } from "svelte"; import ItemCardOptions from "./ItemCardOptions.svelte"; + import "$lib/styles/main.css"; let { item = $bindable() } = $props<{ item: IBasicItemPopulated; @@ -14,81 +15,21 @@ } -