Bug Fixes + Duplicate Support (#1016)

* Update README.md supporter list

* Fix: Multiple bug fixes and features bundle (#888, #991, #617, #984) (#1007)

* fix: resolve location creation failures, broken image uploads, and invalid URL handling

- Add missing addToast import in LocationDetails.svelte for proper error feedback
- Add objectId check and error response handling in ImageManagement.svelte to prevent ghost images
- Add Content-Type check in +page.server.ts image action to handle non-JSON backend responses
- Add client-side URL validation in LocationDetails.svelte (invalid URLs → null)
- Improve Django field error extraction for user-friendly toast messages
- Clean up empty description fields (whitespace → null)
- Update BUGFIX_DOCUMENTATION.md with detailed fix descriptions

* feat: bug fixes and new features bundle

Bug fixes:
- fix: resolve PATCH location with visits (#888)
- fix: Wikipedia/URL image upload via server-side proxy (#991)
- fix: private/public toggle race condition (#617)
- fix: location creation feedback (addToast import)
- fix: invalid URL handling for locations and collections
- fix: world map country highlighting (bg-*-200 -> bg-*-400)
- fix: clipboard API polyfill for HTTP contexts
- fix: MultipleObjectsReturned for duplicate images
- fix: SvelteKit proxy sessionid cookie forwarding

Features:
- feat: duplicate location button (list + detail view)
- feat: duplicate collection button
- feat: i18n translations for 19 languages
- feat: improved error handling and user feedback

Technical:
- Backend: fetch_from_url endpoint with SSRF protection
- Backend: validate_link() for collections
- Backend: file_permissions filter() instead of get()
- Frontend: copyToClipboard() helper function
- Frontend: clipboard polyfill via server-side injection

* chore: switch docker-compose from image to build

Use local source code builds instead of upstream :latest images
to preserve our custom patches and fixes.

* fix: lodging save errors, AI language support, and i18n improvements

- Fix Lodging save: add res.ok checks, error toasts, isSaving state (#984)
- Fix URL validation: silently set invalid URLs to null (Lodging, Transportation)
- Fix AI description language: pass user locale to Wikipedia API
- Fix missing i18n keys: Strava toggle buttons (show/hide)
- Add CHANGELOG.md
- Remove internal documentation from public tracking
- Update .gitignore for Cursor IDE and internal docs

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: update location duplication handling, improve UI feedback, and enhance localization support

---------

Co-authored-by: AdventureLog Bugfix <bugfix@adventurelog.local>
Co-authored-by: madmp87 <info@so-pa.de>
Co-authored-by: Mathias Ponnwitz <devuser@dockge-dev.fritz.box>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Sean Morley <mail@seanmorley.com>

* Enhance duplication functionality for collections and locations; update UI to reflect changes

* Potential fix for code scanning alert no. 49: Information exposure through an exception

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Update Django and Pillow versions in requirements.txt

* Fix error logging for image fetch timeout in ContentImageViewSet

* Update requirements.txt to include jaraco.context and wheel for security fixes

* Update app version and add security vulnerabilities to .trivyignore

* Update backend/server/adventures/views/collection_view.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update frontend/src/lib/types.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Reorder build and image directives in docker-compose.yml for clarity

* Refactor code structure for improved readability and maintainability

* Remove inline clipboard polyfill script injection from server hooks (#1019)

* Initial plan

* Remove inline clipboard polyfill script injection from hooks.server.ts

Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com>

* Fix unhandled promise rejections in copyToClipboard click handlers (#1018)

* Initial plan

* Fix: make copyToClipboard handlers async with try/catch error toast

Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com>

* Harden `fetch_from_url` image proxy: require auth, rate-limit, and strengthen SSRF protections (#1017)

* Initial plan

* Harden fetch_from_url: require auth, rate-limit, block non-standard ports, check all IPs, re-validate redirects

Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com>

* Fix subregion filtering in world travel page to exclude null values

* Update package.json to use caret (^) for versioning in overrides

* fix: update package dependencies for compatibility and stability

- Added cookie dependency with version constraint <0.7.0
- Updated svelte dependency to allow versions <=5.51.4
- Updated @sveltejs/adapter-vercel dependency to allow versions <6.3.2

* Refactor code structure for improved readability and maintainability

---------

Co-authored-by: madmp87 <79420509+madmp87@users.noreply.github.com>
Co-authored-by: AdventureLog Bugfix <bugfix@adventurelog.local>
Co-authored-by: madmp87 <info@so-pa.de>
Co-authored-by: Mathias Ponnwitz <devuser@dockge-dev.fritz.box>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
Sean Morley
2026-02-20 20:49:24 -05:00
committed by GitHub
parent c008f0c264
commit bec90fe2a5
57 changed files with 21743 additions and 20304 deletions

View File

@@ -91,9 +91,18 @@
</svelte:head>
<style>
/* Prevent unwanted horizontal scroll caused by positioned dropdowns or transformed ancestors on small screens */
:global(html),
:global(body) {
/* Prevent unwanted horizontal scroll and ensure single scrollbar */
:global(html) {
overflow-x: hidden;
overflow-y: auto;
}
:global(body) {
overflow: hidden;
}
/* Ensure slot content doesn't create nested scrollbars */
:global(body > div) {
overflow: visible;
}
</style>

View File

@@ -63,7 +63,8 @@ async function handleRequest(
}
// Set the new csrf token in both headers and cookies
const cookieHeader = `csrftoken=${csrfToken}; Path=/; HttpOnly; SameSite=Lax`;
const sessionId = cookies.get('sessionid');
const cookieHeader = `csrftoken=${csrfToken}` + (sessionId ? `; sessionid=${sessionId}` : '');
try {
const response = await fetch(targetUrl, {

View File

@@ -65,7 +65,8 @@ async function handleRequest(
}
// Set the new csrf token in both headers and cookies
const cookieHeader = `csrftoken=${csrfToken}; Path=/; HttpOnly; SameSite=Lax`;
const sessionId = cookies.get('sessionid');
const cookieHeader = `csrftoken=${csrfToken}` + (sessionId ? `; sessionid=${sessionId}` : '');
try {
const response = await fetch(targetUrl, {

View File

@@ -80,7 +80,6 @@ export const load = (async (event) => {
}) satisfies PageServerLoad;
export const actions: Actions = {
restoreData: async (event) => {
if (!event.locals.user) {
return redirect(302, '/');

View File

@@ -191,6 +191,21 @@
isShowingCollectionModal = false;
}
function duplicateCollectionInList(event: CustomEvent<SlimCollection | Collection>) {
const duplicatedCollection = event.detail as SlimCollection;
collections = [
duplicatedCollection,
...collections.filter((collection) => collection.id !== duplicatedCollection.id)
];
archivedCollections = archivedCollections.filter(
(collection) => collection.id !== duplicatedCollection.id
);
activeView = 'owned';
}
async function editCollection(event: CustomEvent<SlimCollection>) {
const slim = event.detail;
try {
@@ -574,7 +589,7 @@
{:else}
<!-- Collections Grid -->
<div
class="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-6"
class="grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-6"
>
{#each currentCollections as collection (collection.id)}
<CollectionCard
@@ -584,6 +599,7 @@
on:edit={editCollection}
on:archive={archiveCollection}
on:unarchive={unarchiveCollection}
on:duplicate={duplicateCollectionInList}
user={data.user}
on:leave={(e) => {
collectionIdToLeave = e.detail;

View File

@@ -1179,7 +1179,7 @@
<!-- Itinerary View -->
{#if currentView === 'itinerary'}
<CollectionItineraryPlanner
{collection}
bind:collection
user={data.user}
canModify={canModifyCollection}
/>

9
frontend/src/routes/locations/+page.server.ts Normal file → Executable file
View File

@@ -74,6 +74,15 @@ export const actions: Actions = {
},
body: formData
});
// Handle non-JSON responses gracefully (e.g. HTML error pages from backend)
const contentType = res.headers.get('content-type') || '';
if (!contentType.includes('application/json')) {
const text = await res.text();
console.error(`Image upload failed with status ${res.status}:`, text.substring(0, 200));
return { error: `Image upload failed (status ${res.status})` };
}
let data = await res.json();
return data;
},

View File

@@ -253,7 +253,7 @@
{:else}
<!-- Adventures Grid -->
<div
class="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-2 xl:grid-cols-3 gap-6"
class="grid grid-cols-1 sm:grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-6"
>
{#each adventures as adventure}
<LocationCard
@@ -261,6 +261,10 @@
{adventure}
on:delete={deleteAdventure}
on:edit={editAdventure}
on:duplicate={(e) => {
// Add the new location to the beginning of the list
adventures = [e.detail, ...adventures];
}}
/>
{/each}
</div>

View File

@@ -14,9 +14,12 @@
import LightbulbOn from '~icons/mdi/lightbulb-on';
import WeatherSunset from '~icons/mdi/weather-sunset';
import ClipboardList from '~icons/mdi/clipboard-list';
import ContentCopy from '~icons/mdi/content-copy';
import DotsVertical from '~icons/mdi/dots-vertical';
import ImageDisplayModal from '$lib/components/ImageDisplayModal.svelte';
import AttachmentCard from '$lib/components/cards/AttachmentCard.svelte';
import { getActivityColor, getBasemapUrl, isAllDay } from '$lib';
import { addToast } from '$lib/toasts';
import { getActivityColor, getBasemapUrl, isAllDay, copyToClipboard } from '$lib';
import ActivityCard from '$lib/components/cards/ActivityCard.svelte';
import TrailCard from '$lib/components/cards/TrailCard.svelte';
import NewLocationModal from '$lib/components/locations/LocationModal.svelte';
@@ -126,6 +129,32 @@
return measurementSystem === 'imperial' ? totalMeters * 3.28084 : totalMeters;
}
let isDuplicating = false;
let isFabMenuOpen = false;
async function duplicateAdventure() {
if (isDuplicating) return;
isDuplicating = true;
isFabMenuOpen = false;
try {
const res = await fetch(`/api/locations/${adventure.id}/duplicate/`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' }
});
if (res.ok) {
const newLocation = await res.json();
addToast('success', $t('adventures.location_duplicate_success'));
goto(`/locations/${newLocation.id}`);
} else {
addToast('error', $t('adventures.location_duplicate_error'));
}
} catch (e) {
addToast('error', $t('adventures.location_duplicate_error'));
} finally {
isDuplicating = false;
}
}
function closeImageModal() {
isImageModalOpen = false;
}
@@ -183,12 +212,40 @@
{#if adventure}
{#if data.user?.uuid && adventure.user?.uuid && data.user.uuid === adventure.user.uuid}
<div class="fixed bottom-6 right-6 z-50">
<button
class="btn btn-primary btn-circle w-16 h-16 shadow-xl hover:shadow-2xl transition-all duration-300 hover:scale-110"
on:click={() => (isEditModalOpen = true)}
>
<ClipboardList class="w-8 h-8" />
</button>
<div class="dropdown dropdown-top dropdown-end" class:dropdown-open={isFabMenuOpen}>
<button
class="btn btn-primary btn-circle w-16 h-16 shadow-xl hover:shadow-2xl transition-all duration-300 hover:scale-110"
on:click={() => (isFabMenuOpen = !isFabMenuOpen)}
>
<DotsVertical class="w-8 h-8" />
</button>
<ul
class="dropdown-content menu bg-base-100 rounded-box w-52 p-2 shadow-lg border border-base-300 mb-2"
>
<li>
<button
on:click={() => {
isFabMenuOpen = false;
isEditModalOpen = true;
}}
class="flex items-center gap-2"
>
<ClipboardList class="w-5 h-5" />
{$t('adventures.edit_location')}
</button>
</li>
<li>
<button
on:click={duplicateAdventure}
class="flex items-center gap-2"
disabled={isDuplicating}
>
<ContentCopy class="w-5 h-5" />
{isDuplicating ? '...' : $t('adventures.duplicate_location')}
</button>
</li>
</ul>
</div>
</div>
{/if}
@@ -648,19 +705,27 @@
<div class="flex gap-2">
<button
class="btn btn-xs btn-ghost flex-1 text-xs"
on:click={() =>
navigator.clipboard.writeText(
`${adventure.latitude}, ${adventure.longitude}`
)}
on:click={async () => {
try {
await copyToClipboard(`${adventure.latitude}, ${adventure.longitude}`);
} catch {
addToast('error', $t('adventures.copy_failed'));
}
}}
>
📋 {$t('adventures.copy_coordinates')}
</button>
<button
class="btn btn-xs btn-ghost flex-1 text-xs"
on:click={() =>
navigator.clipboard.writeText(
`https://www.google.com/maps/@${adventure.latitude},${adventure.longitude},15z`
)}
on:click={async () => {
try {
await copyToClipboard(
`https://www.google.com/maps/@${adventure.latitude},${adventure.longitude},15z`
);
} catch {
addToast('error', $t('adventures.copy_failed'));
}
}}
>
🔗 {$t('adventures.copy_link')}
</button>

View File

@@ -294,10 +294,10 @@
$: showLocalTripTime = Boolean(
localTravelWindow &&
primaryTripTimezone(
transportation?.start_timezone ?? null,
transportation?.end_timezone ?? null
) !== localTimeZone
primaryTripTimezone(
transportation?.start_timezone ?? null,
transportation?.end_timezone ?? null
) !== localTimeZone
);
</script>

View File

@@ -157,7 +157,13 @@
goto(`/worldtravel/${countryCode}`);
}
worldSubregions = [...new Set(allCountries.map((country) => country.subregion))];
worldSubregions = [
...new Set(
allCountries
.map((country) => country.subregion)
.filter((subregion): subregion is string => subregion !== null)
)
];
worldSubregions = worldSubregions.filter((subregion) => subregion !== '');
console.log(worldSubregions);