From 2c2d008dc2c00513e77b7de8a916fbc887179e1e Mon Sep 17 00:00:00 2001 From: Maurice <61554723+mauriceboe@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:22:38 +0200 Subject: [PATCH] fix: clear every open Sonar security issue, and half the reliability ones (#2037) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(security): mint idempotency keys properly, and resolve the extractor by path Three findings out of a sweep of every open Sonar security issue. The other 49 turned out to be false positives and are documented in sonar-project.properties rather than changed — each entry scoped to a file, never a whole rule, so a future Math.random() on an auth path still fails. The idempotency key is the one that can lose data. Both producers fell back to Math.random when crypto.randomUUID was missing, and randomUUID needs a secure context — TREK's own quickstart is http://localhost:3000 and LAN installs run on http://192.168.x.x, so on the deployments the README documents the fallback is what actually runs. The axios one was `Math.random().toString(36).slice(2)`, which is not length-stable: 0.5 yields a single character and 0 yields the empty string, which the interceptor reads as "no key" and skips deduplication for entirely. Two writes sharing a key collapse into one — the second gets the first one's cached response and the edit is gone with no error anywhere. Both now come from utils/randomId, which has the getRandomValues rung that works in insecure contexts too. The extractor probe stored the bare binary name, so every extraction re-resolved it through whatever PATH held at that moment. The official image sets KITINERARY_EXTRACTOR_PATH and never reaches that branch, but a bare-metal install without it ran an unqualified name from a PATH it does not control. It now walks the search path itself and keeps the absolute path of the file it verified; execFileSync also drops the /bin/sh hop the old probe needed. PATH comes through app-config because process.env is walled off in src/. * fix: four tests and sorts that were not doing what they claimed Sonar reported 43 high-impact reliability issues. Reading all of them, four are real; the rest are cosmetic or would be made worse by the suggested change. budget.test.ts hand-built `paid: true`, but both emitters send `paid: body.paid ? 1 : 0` and the handler stores the value verbatim — the test asserted a payload the server never sends. The line below it already used a number. admin.controller.test.ts had a `.resolves` assertion with no await. The promise was dropped, so that line could not fail the test. The two to-do category lists sorted with a bare .sort(), which is byte order: every accented name files behind the whole ASCII range, so in a German trip "Übernachtung" lands after "Zelt". TREK ships 23 locales, so this is real data. One side effect worth knowing: katColor picks a category's dot colour by its index, so re-sorting can move which category shows which colour — it already moves whenever a category is added, and the colour carries no meaning. Deliberately not changed: the two `| 0` truncations (Math.trunc is not equivalent — `| 0` wraps at 32 bits and these carry millisecond timestamps), the two .map() callbacks whose second parameter is declared and used, and the sha1 in thumbnail.service (its own comment says a swap would orphan every existing thumbnail). * refactor: give every button a type, and prefer the Number statics Two mechanical sweeps, no behaviour change intended in either. A @@ -470,7 +470,7 @@ function LlmParsingConfig({ addon }: { addon: Addon }) { {installed.length > 0 && (
{installed.map(name => ( -
{installedHere ? ( - ) : ( - )} @@ -526,7 +526,7 @@ function LlmParsingConfig({ addon }: { addon: Addon }) { )} - @@ -589,7 +589,7 @@ function AddonRow({ addon, onToggle, t, nameOverride, descriptionOverride, statu {isComingSoon ? t('admin.addons.disabled') : enabledState ? t('admin.addons.enabled') : t('admin.addons.disabled')} {!hideToggle && ( - )} {expanded && hidden > 0 && ( - @@ -146,7 +146,7 @@ export default function AdminMcpTokensPanel() { {new Date(session.created_at).toLocaleDateString(locale)} - - @@ -240,11 +240,11 @@ export default function AdminMcpTokensPanel() {

{t('admin.mcpTokens.deleteTitle')}

{t('admin.mcpTokens.deleteMessage')}

- - diff --git a/client/src/components/Admin/AdminPluginsPanel.tsx b/client/src/components/Admin/AdminPluginsPanel.tsx index 4ca8ae1f1..b0c468d65 100644 --- a/client/src/components/Admin/AdminPluginsPanel.tsx +++ b/client/src/components/Admin/AdminPluginsPanel.tsx @@ -187,7 +187,7 @@ const PERM_KEYS = PLUGIN_PERMISSIONS const KNOWN_TYPES = ['widget', 'page', 'integration', 'trip-page'] function isNewer(a: string, b: string): boolean { - const nums = (v: string) => v.split('-')[0].split('.').map(n => parseInt(n, 10) || 0) + const nums = (v: string) => v.split('-')[0].split('.').map(n => Number.parseInt(n, 10) || 0) const pa = nums(a), pb = nums(b) for (let i = 0; i < 3; i++) { const x = pa[i] || 0, y = pb[i] || 0 @@ -795,11 +795,11 @@ export default function AdminPluginsPanel() {
- - @@ -842,11 +842,11 @@ export default function AdminPluginsPanel() { valueLabel={sortLabel(sort, t)} onPick={v => setSort(v as SortKey)} /> - - @@ -871,7 +871,7 @@ export default function AdminPluginsPanel() {
{t('admin.plugins.updatesAvailable', { count: updatable.length })} - @@ -913,7 +913,7 @@ export default function AdminPluginsPanel() {
e.stopPropagation()}>
{errorsFor.id} — {t('admin.plugins.errorLog')} - +
{errorsFor.rows.length === 0 ?

{t('admin.plugins.noErrors')}

: @@ -935,7 +935,7 @@ export default function AdminPluginsPanel() {
e.stopPropagation()}>
{egressFor.id} — {t('admin.plugins.allowedHosts')} - +
{!egressFor.supported ? ( @@ -949,7 +949,7 @@ export default function AdminPluginsPanel() { {egressFor.hosts.map(h => (
{h} -

{t('admin.plugins.empty')}

-
@@ -1158,7 +1158,7 @@ function InstalledRow({ p, t, busy, menu, setMenu, hasUpdate, latestVer, blocked
{t('admin.plugins.updateBlocked', { reason: p.updateBlock.detail ?? p.updateBlock.code })} - @@ -1181,7 +1181,7 @@ function InstalledRow({ p, t, busy, menu, setMenu, hasUpdate, latestVer, blocked Actionable, and warning-toned until at least one host exists, because until then the plugin cannot reach anything and looks silently broken. */} {p.operatorEgress && ( - @@ -1221,7 +1221,7 @@ function InstalledRow({ p, t, busy, menu, setMenu, hasUpdate, latestVer, blocked
- @@ -1275,7 +1275,7 @@ function anchorMenu(trigger: HTMLElement | null): CSSProperties { function MenuItem({ icon, label, onClick, danger }: { icon: React.ReactNode; label: string; onClick: () => void; danger?: boolean }) { return ( - +
@@ -1426,7 +1426,7 @@ function PluginDetailModal({ item, installed, busy, onInstall, onClose, t, local

{item.author}{item.latest ? ` · v${item.latest}` : ''}

-
- {offerRetrust && ( - @@ -1685,8 +1685,8 @@ function UpdateConsentDialog({ data, unsigned, t, onApprove, onLater }: { )}
- - + +
@@ -1726,7 +1726,7 @@ function DependencyResolveDialog({ data, t, busy, installedIds, onDownload, onCl : t('admin.plugins.dep.requires', { version: r.constraint })}
- @@ -1737,7 +1737,7 @@ function DependencyResolveDialog({ data, t, busy, installedIds, onDownload, onCl )}
- +
@@ -1763,7 +1763,7 @@ function SecurityInfo({ t }: { t: T }) {

{t('admin.plugins.reviewedMeaning')}

- - @@ -274,14 +274,14 @@ export default function BackupPanel() {
- - -
- - - diff --git a/client/src/components/Admin/DefaultUserSettingsTab.tsx b/client/src/components/Admin/DefaultUserSettingsTab.tsx index 365303efa..a9bd11fc9 100644 --- a/client/src/components/Admin/DefaultUserSettingsTab.tsx +++ b/client/src/components/Admin/DefaultUserSettingsTab.tsx @@ -86,7 +86,7 @@ function OptionButton({ children: React.ReactNode }) { return ( - @@ -171,8 +171,8 @@ export default function PackingTemplateManager() { setCreateName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleCreateTemplate(); if (e.key === 'Escape') setShowCreate(false) }} placeholder={t('admin.packingTemplates.namePlaceholder')} className={inputStyle} /> - - + + )} @@ -187,7 +187,7 @@ export default function PackingTemplateManager() {
{/* Template row */}
- @@ -202,9 +202,9 @@ export default function PackingTemplateManager() { {tmpl.category_count} {t('admin.packingTemplates.categories')} · {tmpl.item_count} {t('admin.packingTemplates.items')} - -
@@ -228,11 +228,11 @@ export default function PackingTemplateManager() { {cat.name} )} {catItems.length} - - -
@@ -246,15 +246,15 @@ export default function PackingTemplateManager() { setEditItemName(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleRenameItem(item.id); if (e.key === 'Escape') setEditingItemId(null) }} className="flex-1 px-2 py-1 border border-slate-200 rounded-lg text-sm" /> - - + + ) : ( <> {item.name} - - )} @@ -268,9 +268,9 @@ export default function PackingTemplateManager() { onKeyDown={e => { if (e.key === 'Enter' && newItemName.trim()) handleAddItem(cat.id); if (e.key === 'Escape') { setAddingItemToCatId(null); setNewItemName('') } }} placeholder={t('admin.packingTemplates.itemName')} className="flex-1 px-2 py-1 border border-slate-200 rounded-lg text-sm" /> - - )} @@ -287,11 +287,11 @@ export default function PackingTemplateManager() { onKeyDown={e => { if (e.key === 'Enter') handleAddCategory(); if (e.key === 'Escape') { setAddingCategory(false); setNewCatName('') } }} placeholder={t('admin.packingTemplates.categoryName')} className="flex-1 px-3 py-2 border border-slate-200 rounded-lg text-sm" /> - - + + ) : ( - diff --git a/client/src/components/Admin/PermissionsPanel.tsx b/client/src/components/Admin/PermissionsPanel.tsx index acab4f0c6..6b2b149b7 100644 --- a/client/src/components/Admin/PermissionsPanel.tsx +++ b/client/src/components/Admin/PermissionsPanel.tsx @@ -104,7 +104,7 @@ export default function PermissionsPanel(): React.ReactElement {

{t('perm.subtitle')}

- -

@@ -364,7 +364,7 @@ export default function AdminStoragePanel(): React.ReactElement { {t(`storage.source.${row.source}`)} - {row.source !== 'env' && ( - )} {row.source === 'settings' && ( - )} @@ -433,7 +433,7 @@ export default function AdminStoragePanel(): React.ReactElement { failed: String(backfill.failed), })}

- - -
@@ -552,7 +552,7 @@ export default function AdminStoragePanel(): React.ReactElement { total: String(m.total), })}

- - - - @@ -87,7 +87,7 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro placeholder={t('budget.categoryName')} style={{ flex: 1, minWidth: 0, border: '1px solid var(--border-primary)', borderRadius: 10, padding: '9px 14px', fontSize: 'calc(13px * var(--fs-scale-body, 1))', outline: 'none', fontFamily: 'inherit', background: 'var(--bg-card)', color: 'var(--text-primary)' }} /> - - ))} - } + {canEdit && } ))} @@ -589,7 +589,7 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps {t('costs.youPaid')} · {fmt0(totals.myPaid)} {canEdit && ( - )} @@ -628,7 +628,7 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
{t('costs.settleUp')} {(settlement?.flows || []).length}
{canEdit && ( - + )}
{SettleFlows()} @@ -638,7 +638,7 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
{t('costs.expenses')}
-
{(['all', 'mine', 'owed'] as const).map(f => ( - + ))}
@@ -705,7 +705,7 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps } return (
- +
) @@ -839,8 +839,8 @@ export default function CostsPanel({ tripId, tripMembers = [] }: CostsPanelProps
{canEdit && (
- - + +
)}
@@ -970,7 +970,7 @@ function SettlementModal({ tripId, people, me, editing, currency, onClose, onSav const [cur, setCur] = useState((editing?.currency || currency).toUpperCase()) const [saving, setSaving] = useState(false) - const amt = parseFloat(amount) || 0 + const amt = Number.parseFloat(amount) || 0 const valid = amt > 0 && fromId !== toId const opts = people.map(p => ({ value: String(p.id), label: p.id === me ? t('costs.you') : p.username })) @@ -991,8 +991,8 @@ function SettlementModal({ tripId, people, me, editing, currency, onClose, onSav - - + + }>
@@ -1116,8 +1116,8 @@ export function ExpenseModal({ tripId, base, people, me, editing, prefill, onClo return calculateTicketShares(ticketItems) }, [ticketItems]) - const totalNum = isTicketMode ? ticketInfo.total : (parseFloat(total) || 0) - const splitSum = [...participants].reduce((sum, id) => sum + (parseFloat(customAmounts[id]) || 0), 0) + const totalNum = isTicketMode ? ticketInfo.total : (Number.parseFloat(total) || 0) + const splitSum = [...participants].reduce((sum, id) => sum + (Number.parseFloat(customAmounts[id]) || 0), 0) const customBalanced = Math.round(splitSum * 100) === Math.round(totalNum * 100) const each = participants.size > 0 ? totalNum / participants.size : 0 const equalShares = useMemo(() => { @@ -1130,13 +1130,13 @@ export function ExpenseModal({ tripId, base, people, me, editing, prefill, onClo const enteredSum = [...participants] .filter(id => customAmounts[id]) - .reduce((sum, id) => sum + (parseFloat(customAmounts[id]) || 0), 0) + .reduce((sum, id) => sum + (Number.parseFloat(customAmounts[id]) || 0), 0) const remaining = Math.max(0, totalNum - enteredSum) return splitEqualShares(remaining, emptyParts.map(id => ({ user_id: id })), editing?.id || 0) }, [totalNum, participants, customAmounts, editing]) - const ticketValid = ticketItems.length > 0 && ticketItems.every(item => item.name.trim().length > 0 && (parseFloat(item.price) || 0) > 0 && item.participants.size > 0) + const ticketValid = ticketItems.length > 0 && ticketItems.every(item => item.name.trim().length > 0 && (Number.parseFloat(item.price) || 0) > 0 && item.participants.size > 0) const payersOk = !multiPayer || (payerIds.size > 0 && payersBalanced(payerAmounts, payerIds, totalNum)) const valid = name.trim().length > 0 && payersOk && ( isTicketMode @@ -1263,7 +1263,7 @@ export function ExpenseModal({ tripId, base, people, me, editing, prefill, onClo // dropping the payer would store the entry with a total of 0. const payerList = multiPayer ? [...payerIds] - .map(id => ({ user_id: id, amount: parseFloat(payerAmounts[id]) || 0 })) + .map(id => ({ user_id: id, amount: Number.parseFloat(payerAmounts[id]) || 0 })) .filter(p => p.amount > 0) : payerId > 0 ? [{ user_id: payerId, amount: totalNum }] : [] // A receipt line can name somebody who is not ticked as a participant. Sending @@ -1275,7 +1275,7 @@ export function ExpenseModal({ tripId, base, people, me, editing, prefill, onClo const memberList = memberIds.map(id => ({ user_id: id, amount: splitMode === 'custom' - ? (parseFloat(customAmounts[id]) || 0) + ? (Number.parseFloat(customAmounts[id]) || 0) : splitMode === 'ticket' ? (ticketInfo.shares[id] || 0) : null @@ -1315,8 +1315,8 @@ export function ExpenseModal({ tripId, base, people, me, editing, prefill, onClo - - + +
}> {/* Two columns on desktop: what the expense was on the left, how it is @@ -1370,7 +1370,7 @@ export function ExpenseModal({ tripId, base, people, me, editing, prefill, onClo {COST_CATEGORY_LIST.map(c => { const Icon = c.Icon; const on = cat === c.key return ( - {own && canEdit && ( - } - {canEdit && {categories.map(cat => ( - } - )} - {canEdit && } - {canEdit && } - {canEdit && @@ -96,7 +96,7 @@ export function CategorySettingsModal({ onClose, categories, categoryColors, onS {/* Color swatches */}
{NOTE_COLORS.map(c => ( - - + +
@@ -50,19 +50,19 @@ export function FilePreviewPortal({ file, onClose }: FilePreviewPortalProps) {
{file.original_name}
- - + +
{(isPdf || isTxt) ? (

- +

) : (
- +
)} diff --git a/client/src/components/Collab/CollabPanel.tsx b/client/src/components/Collab/CollabPanel.tsx index e75f5e3f2..98100b01b 100644 --- a/client/src/components/Collab/CollabPanel.tsx +++ b/client/src/components/Collab/CollabPanel.tsx @@ -170,7 +170,7 @@ export default function CollabPanel({ tripId, tripMembers = [], collabFeatures } {tabs.map(tab => { const active = mobileTab === tab.id return ( - )} - @@ -47,7 +47,7 @@ export default function CollectionsWidget({ onOpen }: { onOpen: () => void }): R ) : (
{lists.slice(0, 6).map(list => ( -
- @@ -67,7 +67,7 @@ export function AssignModal(S: FileManagerState) { const placeBtn = (p: Place, idx: number) => { const isLinked = file.place_id === p.id || (file.linked_place_ids || []).includes(p.id) return ( - - - @@ -156,7 +156,7 @@ function ThumbImg({ file, active, onClick }: { file: TripFile & { url: string }; }, [file.url, fileIsVideo, visible]) return ( - - - diff --git a/client/src/components/Files/FileManagerPdfPreviewModal.tsx b/client/src/components/Files/FileManagerPdfPreviewModal.tsx index 9c3c97f54..55932dc27 100644 --- a/client/src/components/Files/FileManagerPdfPreviewModal.tsx +++ b/client/src/components/Files/FileManagerPdfPreviewModal.tsx @@ -18,21 +18,21 @@ export function PdfPreviewModal(S: FileManagerState) {
{previewFile.original_name}
- - - +

diff --git a/client/src/components/Files/FileManagerRow.tsx b/client/src/components/Files/FileManagerRow.tsx index 79fda174d..861697366 100644 --- a/client/src/components/Files/FileManagerRow.tsx +++ b/client/src/components/Files/FileManagerRow.tsx @@ -97,34 +97,34 @@ export function FileRow(p: FileManagerState & { file: TripFile; isTrash?: boolea
{isTrash ? ( <> - {can('file_delete', trip) && } - {can('file_delete', trip) && } ) : ( <> - - {can('file_edit', trip) && } - - - {can('file_delete', trip) && } diff --git a/client/src/components/Files/FileManagerToolbar.tsx b/client/src/components/Files/FileManagerToolbar.tsx index 085d7626f..2c77017e1 100644 --- a/client/src/components/Files/FileManagerToolbar.tsx +++ b/client/src/components/Files/FileManagerToolbar.tsx @@ -36,7 +36,7 @@ export function FileManagerToolbar(S: FileManagerState) { : tab.id === 'collab' ? files.filter(f => f.note_id).length : 0 return ( -
@@ -102,7 +102,7 @@ export default function ContributorInviteDialog({ journeyId, existingUserIds, on
{(['viewer', 'editor'] as const).map(r => ( -
- -
@@ -81,7 +81,7 @@ export function AddTripDialog({ journeyId, existingTripIds, onClose, onAdded }:
)} - {menuOpen && createPortal( <>
setMenuOpen(false)} />
- - + +
, document.body, @@ -113,15 +113,15 @@ export function EntryCard({ entry, readOnly, onEdit, onDelete, onPhotoClick }: {
{!readOnly && (
- {menuOpen && createPortal( <>
setMenuOpen(false)} />
- - + +
, document.body, diff --git a/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx b/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx index 0a8792ea6..6baaa6d65 100644 --- a/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx +++ b/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx @@ -209,7 +209,7 @@ export function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, trips, setPendingFiles(prev => [...prev, ...normalized]) } - const contextLocation = isValidGeoPoint({ lat: locationLat ?? NaN, lng: locationLng ?? NaN }) + const contextLocation = isValidGeoPoint({ lat: locationLat ?? Number.NaN, lng: locationLng ?? Number.NaN }) ? { lat: locationLat!, lng: locationLng!, name: locationName || undefined } : null @@ -256,7 +256,7 @@ export function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, trips,

{entry.id === 0 ? t('journey.detail.newEntry') : t('journey.detail.editEntry')}

-
@@ -275,7 +275,7 @@ export function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, trips, { (e.target as HTMLInputElement).value = '' }} className="hidden" /> { (e.target as HTMLInputElement).value = '' }} className="hidden" />
- {galleryPhotos.length > 0 && ( - )} - -
{pendingProviderGroups.length > 0 && ( - )} @@ -380,7 +380,7 @@ export function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, trips,
{availableProviders.map(provider => ( - )} - )}
))} - )}
))} - - +
diff --git a/client/src/components/Journey/JourneyDetailPageExpandableStory.tsx b/client/src/components/Journey/JourneyDetailPageExpandableStory.tsx index d26b53733..bb93db665 100644 --- a/client/src/components/Journey/JourneyDetailPageExpandableStory.tsx +++ b/client/src/components/Journey/JourneyDetailPageExpandableStory.tsx @@ -35,7 +35,7 @@ export function ExpandableStory({ story }: { story: string }) {
{clamped && !expanded && ( - )} {expanded && ( - } @@ -176,7 +176,7 @@ export function ProviderPicker({ provider, userId, entries, trips, existingAsset { id: 'all' as const, label: t('journey.picker.allPhotos'), short: t('common.all') }, { id: 'album' as const, label: t('journey.picker.albums') }, ].map(f => ( - @@ -243,7 +243,7 @@ export function ProviderPicker({ provider, userId, entries, trips, existingAsset {filter === 'album' && (
{albums.map((a: any) => ( - -
@@ -105,7 +105,7 @@ export function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite,
-
- - - - + diff --git a/client/src/components/Journey/JourneyDetailPageVerdictSection.tsx b/client/src/components/Journey/JourneyDetailPageVerdictSection.tsx index 23fb3141b..f7ea2794a 100644 --- a/client/src/components/Journey/JourneyDetailPageVerdictSection.tsx +++ b/client/src/components/Journey/JourneyDetailPageVerdictSection.tsx @@ -9,7 +9,7 @@ export function VerdictSection({ pros, cons }: { pros: string[]; cons: string[] return (
{/* Header — clickable on mobile */} - - - {!readOnly && (
- -
- - {user?.role === 'admin' && ( -
-
)} - )} - - - - + diff --git a/client/src/components/Packing/ApplyTemplateButton.tsx b/client/src/components/Packing/ApplyTemplateButton.tsx index 7fa920f98..01de45335 100644 --- a/client/src/components/Packing/ApplyTemplateButton.tsx +++ b/client/src/components/Packing/ApplyTemplateButton.tsx @@ -59,7 +59,7 @@ export default function ApplyTemplateButton({ tripId, visibility, style, classNa return (
- ) : null} - {canEdit && } + {canEdit && }
{/* Members */}
@@ -101,7 +101,7 @@ export function BagCard({ bag, bagItems, totalWeight, pct, tripId, tripMembers, ))} {canEdit && ( - )} @@ -110,7 +110,7 @@ export function BagCard({ bag, bagItems, totalWeight, pct, tripId, tripMembers, {tripMembers.map(m => { const isSelected = memberIds.includes(m.id) return ( -
diff --git a/client/src/components/Packing/PackingListPanelBagModal.tsx b/client/src/components/Packing/PackingListPanelBagModal.tsx index 650ce8632..268b5496c 100644 --- a/client/src/components/Packing/PackingListPanelBagModal.tsx +++ b/client/src/components/Packing/PackingListPanelBagModal.tsx @@ -20,7 +20,7 @@ export function BagModal(S: PackingState) { onClick={e => e.stopPropagation()}>

{t('packing.bags')}

- +
{bags.map(bag => { @@ -66,13 +66,13 @@ export function BagModal(S: PackingState) { onKeyDown={e => { if (e.key === 'Enter') handleCreateBag(); if (e.key === 'Escape') { setShowAddBag(false); setNewBagName('') } }} placeholder={t('packing.bagName')} style={{ flex: 1, padding: '8px 12px', borderRadius: 10, border: '1px solid var(--border-primary)', fontSize: 'calc(13px * var(--fs-scale-body, 1))', fontFamily: 'inherit', outline: 'none' }} /> - ) : ( - ) : ( - diff --git a/client/src/components/Packing/PackingListPanelCategoryGroup.tsx b/client/src/components/Packing/PackingListPanelCategoryGroup.tsx index 8c2bad168..b31beb2d0 100644 --- a/client/src/components/Packing/PackingListPanelCategoryGroup.tsx +++ b/client/src/components/Packing/PackingListPanelCategoryGroup.tsx @@ -115,7 +115,7 @@ export function KategorieGruppe({ kategorie, items, tripId, allCategories, onRen return (
- @@ -165,7 +165,7 @@ export function KategorieGruppe({ kategorie, items, tripId, allCategories, onRen ))} {canEdit && (
- @@ -300,18 +300,18 @@ export function KategorieGruppe({ kategorie, items, tripId, allCategories, onRen style={{ flex: 1, padding: '6px 10px', borderRadius: 8, border: '1px solid var(--border-primary)', fontSize: 'calc(12.5px * var(--fs-scale-body, 1))', fontFamily: 'inherit', outline: 'none', color: 'var(--text-primary)', background: 'var(--bg-input)' }} /> {/* disabled while the field is empty, so no extra guard here */} - -
) : ( - - + +
)} {inlineHeader && canEdit && ( - )} {inlineHeader && canEdit && abgehakt > 0 && ( -
)} {inlineHeader && canEdit && isAdmin && items.length > 0 && !showSaveTemplate && ( - )} {bagTrackingEnabled && ( - - ) : ( -
- -
)} - )} {bags.map(b => ( - ) : ( - - @@ -344,7 +344,7 @@ export function ArtikelZeile({ item, tripId, categories, onCategoryChange: _onCa {canEdit && (
-
@@ -371,13 +371,13 @@ export default function AirTrailImportModal({ isOpen, onClose, tripId, pushUndo
- -
@@ -184,13 +184,13 @@ export default function BookingImportModal({ isOpen, onClose, tripId }: BookingI {/* Footer */}
- - )} @@ -182,14 +182,14 @@ export default function DayDetailPanel({ day, days, places, categories = [], tri )} {!collapsed && formattedDate &&
{formattedDate}
}
- - } - {canEditDays && } @@ -468,7 +468,7 @@ function AccommodationList({ dayAccommodations, day, reservations, canEditDays, ) })} {/* Add another hotel */} - {canEditDays && } ) : ( - canEditDays ? @@ -584,7 +584,7 @@ function HotelPickerModal({ showHotelPicker, setShowHotelPicker, font, t, hotelD size="sm" /> - {categories.map(c => ( - - )} @@ -1765,7 +1765,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP const toLegIndex = legPart ? Number(legPart.slice(3)) : null if (placeId) { - onAssignToDay?.(parseInt(placeId), day.id) + onAssignToDay?.(Number.parseInt(placeId), day.id) } else if (fromReservationId && fromDayId !== day.id) { const r = reservations.find(x => x.id === Number(fromReservationId)) if (r) { const update = computeMultiDayMove(r, day.id, phase); tripActions.updateReservation(tripId, r.id, update).catch((err: unknown) => toast.error(err instanceof Error ? err.message : t('common.unknownError'))) } @@ -1791,7 +1791,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP } if (!assignmentId && !noteId && !placeId) { dragDataRef.current = null; window.__dragData = null; return } if (placeId) { - onAssignToDay?.(parseInt(placeId), day.id) + onAssignToDay?.(Number.parseInt(placeId), day.id) setDropTargetKey(null); window.__dragData = null; return } if (assignmentId && fromDayId !== day.id) { @@ -1909,7 +1909,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP const { placeId, assignmentId: fromAssignmentId, noteId, reservationId: fromReservationId, fromDayId, phase } = getDragData(e) if (placeId) { const pos = placeItems.findIndex(i => i.data.id === assignment.id) - onAssignToDay?.(parseInt(placeId), day.id, pos >= 0 ? pos : undefined) + onAssignToDay?.(Number.parseInt(placeId), day.id, pos >= 0 ? pos : undefined) setDropTargetKey(null); window.__dragData = null } else if (fromReservationId && fromDayId !== day.id) { const r = reservations.find(x => x.id === Number(fromReservationId)) @@ -2154,15 +2154,15 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP )} {canEditDays &&
- -
} {canEditDays && onAddBookingToAssignment && hoveredAssignmentId === assignment.id && ( - - + + } {canEditDays &&
- - + +
} @@ -2580,7 +2580,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP const { placeId, assignmentId, noteId, reservationId: fromReservationId, fromDayId, phase } = getDragData(e) // Neuer Ort von der Orte-Liste if (placeId) { - onAssignToDay?.(parseInt(placeId), day.id) + onAssignToDay?.(Number.parseInt(placeId), day.id) setDropTargetKey(null); window.__dragData = null; return } if (fromReservationId && fromDayId !== day.id) { @@ -2618,7 +2618,7 @@ const DayPlanSidebar = React.memo(function DayPlanSidebar(props: DayPlanSidebarP {(isSelected || (showRouteToolsWhenExpanded && isExpanded)) && routeToolsRoutable && (
- {/* Open the day's stops as a route in Google Maps (planned order). #1255 */} -
@@ -58,7 +58,7 @@ export function MobileAddPlaceButton({ dayId, places, assignments, onAssign, onA
)} {filtered.slice(0, 20).map(p => ( - - diff --git a/client/src/components/Planner/DayPlanSidebarToolbar.tsx b/client/src/components/Planner/DayPlanSidebarToolbar.tsx index e41a20b0d..94a1833ef 100644 --- a/client/src/components/Planner/DayPlanSidebarToolbar.tsx +++ b/client/src/components/Planner/DayPlanSidebarToolbar.tsx @@ -94,7 +94,7 @@ export function DayPlanSidebarToolbar({ const label = allExpanded ? t('dayplan.collapseAll') : t('dayplan.expandAll') return ( - )} - - - - @@ -47,7 +47,7 @@ export function PlacesBulkCategoryModal({ count, categories, onPick, onClose }: {categories.map(c => { const CatIcon = getCategoryIcon(c.icon) return ( - diff --git a/client/src/components/Planner/PlacesSidebarHeader.tsx b/client/src/components/Planner/PlacesSidebarHeader.tsx index c8a52e48d..a760588dc 100644 --- a/client/src/components/Planner/PlacesSidebarHeader.tsx +++ b/client/src/components/Planner/PlacesSidebarHeader.tsx @@ -30,7 +30,7 @@ export function PlacesHeader(S: SidebarState) { } = S return (
- {canEditPlaces && - )} @@ -162,7 +162,7 @@ export function PlacesHeader(S: SidebarState) { : `${categoryFilters.size} ${t('places.categoriesSelected')}` return (
- {categories.length > 0 && } {/* Minimum-stars filter (#1435), same floors as the collections bar. */} - }
diff --git a/client/src/components/Planner/PlacesSidebarListImportModal.tsx b/client/src/components/Planner/PlacesSidebarListImportModal.tsx index dc0c613dd..9ab840e43 100644 --- a/client/src/components/Planner/PlacesSidebarListImportModal.tsx +++ b/client/src/components/Planner/PlacesSidebarListImportModal.tsx @@ -25,7 +25,7 @@ export function ListImportModal(S: SidebarState) { {hasMultipleListImportProviders && (
{availableListImportProviders.map(provider => ( -
)}
- - {/* Edit */} {canEditPlaces && ( -
{!selectMode && !inDay && selectedDayId && ( - - )} {canEdit && ( -
- - @@ -561,7 +561,7 @@ function Section({ title, count, children, defaultOpen = true, accent, storageKe }, [open, storageKey]) return (
-
{canEdit && ( - - + +
, @@ -837,7 +837,7 @@ export default function ReservationsPanel({ tripId, reservations, days, assignme <>
- )} - diff --git a/client/src/components/Planner/SubscribeLinks.tsx b/client/src/components/Planner/SubscribeLinks.tsx index e5eed99ab..38ae62c3d 100644 --- a/client/src/components/Planner/SubscribeLinks.tsx +++ b/client/src/components/Planner/SubscribeLinks.tsx @@ -114,7 +114,7 @@ function UrlRow({ label, hint, url, copied, onCopy }: { }}> {url}
- )} @@ -198,7 +198,7 @@ export default function TransitJourneyModal({ reservation, onClose, onSave, onDe
{canEdit && ( - )} diff --git a/client/src/components/Planner/TransitSearchPanel.tsx b/client/src/components/Planner/TransitSearchPanel.tsx index dd8478b67..100fc4151 100644 --- a/client/src/components/Planner/TransitSearchPanel.tsx +++ b/client/src/components/Planner/TransitSearchPanel.tsx @@ -149,7 +149,7 @@ function StopPicker({ label, value, onPick, quickPicks, near, placeholder }: {
{results.length > 0 ? results.map((r, i) => ( - )) : quickPicks.map((p, i) => ( - - + +
@@ -552,7 +552,7 @@ export default function TransitSearchPanel({ day, days, places, accommodations = {MODE_GROUPS.map(m => { const active = activeModes.has(m.key) return ( -
- +
diff --git a/client/src/components/Settings/AccountTab.tsx b/client/src/components/Settings/AccountTab.tsx index e9a2488b6..84798bbb9 100644 --- a/client/src/components/Settings/AccountTab.tsx +++ b/client/src/components/Settings/AccountTab.tsx @@ -198,7 +198,7 @@ export default function AccountTab(): React.ReactElement { placeholder={t('settings.confirmPassword')} className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm focus:ring-2 focus:ring-slate-400 focus:border-transparent" /> -
)} - {user?.avatar_url && ( - - -
- - ))} @@ -413,7 +413,7 @@ export default function AppearanceSettingsTab(): React.ReactElement {
- - @@ -424,12 +424,12 @@ function IntegrationsMcpSection(props: any) { )}
- - @@ -526,7 +526,7 @@ function IntegrationsMcpSection(props: any) { )}

- - @@ -585,14 +585,14 @@ function McpTokenModals(props: any) {
                     {mcpCreatedToken}
                   
-
- @@ -611,11 +611,11 @@ function McpTokenModals(props: any) {

{t('settings.mcp.deleteTokenTitle')}

{t('settings.mcp.deleteTokenMessage')}

- - @@ -697,11 +697,11 @@ function OAuthClientModals(props: any) {
- - @@ -735,7 +735,7 @@ function OAuthClientModals(props: any) { {oauthCreatedClient.client_secret} - @@ -750,7 +750,7 @@ function OAuthClientModals(props: any) { )}
- @@ -769,11 +769,11 @@ function OAuthClientModals(props: any) {

{t('settings.oauth.deleteClient')}

{t('settings.oauth.deleteClientMessage')}

- - @@ -790,11 +790,11 @@ function OAuthClientModals(props: any) {

{t('settings.oauth.rotateSecret')}

{t('settings.oauth.rotateSecretMessage')}

- - @@ -818,14 +818,14 @@ function OAuthClientModals(props: any) { {oauthRotatedSecret} -
- @@ -842,11 +842,11 @@ function OAuthClientModals(props: any) {

{t('settings.oauth.revokeSession')}

{t('settings.oauth.revokeSessionMessage')}

- - diff --git a/client/src/components/Settings/LlmConnectionSection.tsx b/client/src/components/Settings/LlmConnectionSection.tsx index 67e8bcf92..bd1841655 100644 --- a/client/src/components/Settings/LlmConnectionSection.tsx +++ b/client/src/components/Settings/LlmConnectionSection.tsx @@ -143,7 +143,7 @@ export default function LlmConnectionSection(): React.ReactElement {

{t('settings.aiParsing.multimodalHint')}

-
- - )} - - - -
@@ -334,7 +334,7 @@ export default function OfflineTab(): React.ReactElement {
- - - : } + ? + : }
) } @@ -176,7 +176,7 @@ function PluginSettingsForm({ id, name, icon }: { id: string; name: string; icon ))}
{hasFields && ( - - @@ -59,7 +59,7 @@ export function SaveIndicator({ if (state.status === 'error') { return ( - diff --git a/client/src/components/Studio/SpreadView.tsx b/client/src/components/Studio/SpreadView.tsx index d5440e853..182e8957f 100644 --- a/client/src/components/Studio/SpreadView.tsx +++ b/client/src/components/Studio/SpreadView.tsx @@ -38,7 +38,7 @@ const round2 = (n: number) => Math.round(n * 100) / 100 /** #rrggbb plus an alpha, for the fades a cover panel needs. */ function hexToRgba(hex: string, alpha: number): string { - const n = parseInt(hex.slice(1), 16) + const n = Number.parseInt(hex.slice(1), 16) return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})` } diff --git a/client/src/components/Studio/StudioCanvas.tsx b/client/src/components/Studio/StudioCanvas.tsx index 518b09ce4..eaddd692b 100644 --- a/client/src/components/Studio/StudioCanvas.tsx +++ b/client/src/components/Studio/StudioCanvas.tsx @@ -394,15 +394,15 @@ export function StudioCanvas({ style={{ left: (x0 + x1) / 2, top: Math.max(6, y0 - 12) }} onPointerDown={e => e.stopPropagation()} > - {sel.length === 1 && ( <> - - @@ -417,13 +417,13 @@ export function StudioCanvas({ a rotation past the angle you wanted and back is a worse way to arrive at either. */} - - - @@ -85,7 +85,7 @@ export function Choice({ value, options, onPick, aria return (
{options.map(o => ( - - - -
@@ -236,18 +236,18 @@ export function StudioElementsPanel({ page, t }: { page: BookPageSetup; t: (k: s
{t('journey.studio.text')}
- - - - - - )} diff --git a/client/src/components/Studio/StudioExport.tsx b/client/src/components/Studio/StudioExport.tsx index e1c570ba2..c8d90798d 100644 --- a/client/src/components/Studio/StudioExport.tsx +++ b/client/src/components/Studio/StudioExport.tsx @@ -81,7 +81,7 @@ export function StudioExport({
{t('journey.studio.export')} -
@@ -109,7 +109,7 @@ export function StudioExport({
{t('journey.studio.exportFinishing')} -
- - + @@ -176,7 +176,7 @@ function Choice({
{label} {options.map(opt => ( - - - {FRAME_SHAPES.filter(sh => sh !== 'rect').map(sh => ( - - - -
diff --git a/client/src/components/Studio/StudioShell.tsx b/client/src/components/Studio/StudioShell.tsx index 383bb012e..3d5a93e0e 100644 --- a/client/src/components/Studio/StudioShell.tsx +++ b/client/src/components/Studio/StudioShell.tsx @@ -46,7 +46,7 @@ export default function StudioShell() {
{s.t('journey.studio.desktopOnlyHint')}
- - - {s.zoomPercent}% - - {/* @@ -453,7 +453,7 @@ function Workbench({ s, bookView }: { s: Studio; bookView: boolean }) { photographs, which is the thing worth passing on. */} - - - - - )}
- -
@@ -448,7 +448,7 @@ function ContentPanel({ {tab === 'photos' ? (
{photos.map(p => ( - )} {e.story && ( - )} {e.location && ( - )} @@ -508,7 +508,7 @@ function ContentPanel({ mark placed here and one the auto layout placed read alike. */} {e.date && ( - )} {e.lat != null && e.lng != null && ( - )} {e.weather && WEATHER_CONFIG[e.weather] && ( - )} {(e.pros.length > 0 || e.cons.length > 0) && ( - diff --git a/client/src/components/Studio/StudioSwatches.tsx b/client/src/components/Studio/StudioSwatches.tsx index d3fbb0ccd..0dd60c0f2 100644 --- a/client/src/components/Studio/StudioSwatches.tsx +++ b/client/src/components/Studio/StudioSwatches.tsx @@ -20,7 +20,7 @@ export function Swatches({ value, onPick }: { value: string; onPick: (c: string) return (
{SWATCHES.map(c => ( -
diff --git a/client/src/components/Studio/StudioTravelPanel.tsx b/client/src/components/Studio/StudioTravelPanel.tsx index e3a4c61c2..d0cbf9a64 100644 --- a/client/src/components/Studio/StudioTravelPanel.tsx +++ b/client/src/components/Studio/StudioTravelPanel.tsx @@ -68,7 +68,7 @@ function Card({ label, onClick, children, recommended = false, title }: { children: React.ReactNode }) { return ( - )} @@ -161,7 +161,7 @@ export function ReleaseNoticeModal({ notice, visible, onDismiss, onCTA, onSecond
{t(release.supportTextKey)}
{notice.cta && ( -
{notice.dismissible && ( - +
) : ( -
@@ -421,7 +421,7 @@ function DetailPane({ item, tripId, categories, members, onClose }: { const cfg = PRIO_CONFIG[p] const isActive = priority === p return ( - -
@@ -668,7 +668,7 @@ function NewTaskPane({ tripId, categories, members, defaultCategory, onCreated, const cfg = PRIO_CONFIG[p] const isActive = priority === p return ( -
- -
- - )} {canRemove && ( - - {companyHolidaysEnabled && ( - - @@ -149,10 +149,10 @@ export default function VacayPersons() { /> )}
- -
{PRESET_COLORS.map(c => ( -
-
@@ -526,7 +526,7 @@ function CalendarRow({ cal, countries, calendarType, onUpdate, onDelete }: { /> )}
- - @@ -151,10 +151,10 @@ export default function VacaySharedCalendars() { /> )}
- -
- -
- -
)} {targets.map(target => ( - @@ -956,7 +956,7 @@ export default function MAdminPluginsPanel() { {egressFor.hosts.map(h => (
{h} - @@ -1186,7 +1186,7 @@ function InstalledRow({ p, t, busy, hasUpdate, latestVer, blocked, onToggle, onU Actionable, and warning-toned until at least one host exists, because until then the plugin cannot reach anything and looks silently broken. */} {p.operatorEgress && ( - @@ -1267,7 +1267,7 @@ function EmptyState({ t, onDiscover }: { t: T; onDiscover: () => void }) {

{t('admin.plugins.empty')}

- @@ -1340,7 +1340,7 @@ function RegistryList({ items, onInstall, onOpenDetail, busy, t, installedIds, f {formatCompactCount(item.downloadCount)}
)} - +
@@ -1416,7 +1416,7 @@ function PluginDetailSheet({ item, installed, busy, onInstall, onClose, t, local

{item.author}{item.latest ? ` · v${item.latest}` : ''}

- @@ -1748,7 +1748,7 @@ function SecurityInfo({ t }: { t: T }) {

{t('admin.plugins.reviewedMeaning')}

- - - - @@ -643,12 +643,12 @@ function SidebarContent({ data, stats, countries, selectedCountry, countryDetail
- -
) : (
- ))} {countryDetail.manually_marked && onUnmarkCountry && ( - @@ -177,11 +177,11 @@ function DashboardPageDesktop(): React.ReactElement {

{t('dashboard.title')}

- - - + + +
- -
@@ -228,7 +228,7 @@ function DashboardPageDesktop(): React.ReactElement { /> ))} {tripFilter === 'planned' && !isLoading && ( -
- - - - + + + +
@@ -559,10 +559,10 @@ function TripCard({ trip, locale, badges, onOpen, onEdit, onCopy, onArchive, onD :
}
{statusLabel}
- - - - + + + +

{trip.title}

@@ -636,7 +636,7 @@ function CurrencyTool(): React.ReactElement { const currencies = rates ? Object.keys(rates).sort() : CURRENCIES const ccyOptions = currencies.map(c => ({ value: c, label: c })) const rate = rates?.[to] ?? null - const converted = rate != null ? (parseFloat(amount.replace(',', '.')) || 0) * rate : null + const converted = rate != null ? (Number.parseFloat(amount.replace(',', '.')) || 0) * rate : null const swap = () => { setFrom(to); setTo(from) } @@ -644,7 +644,7 @@ function CurrencyTool(): React.ReactElement {
{t('dashboard.currency')}
- +
@@ -652,7 +652,7 @@ function CurrencyTool(): React.ReactElement { setAmount(e.target.value)} inputMode="decimal" /> setFrom(String(v))} options={ccyOptions} searchable size="sm" style={{ marginTop: 6 }} />
- +
{t('dashboard.fx.to')}
@@ -739,7 +739,7 @@ function TimezoneTool({ locale }: { locale: string }): React.ReactElement {
{t('dashboard.timezone')}
-
@@ -757,7 +757,7 @@ function TimezoneTool({ locale }: { locale: string }): React.ReactElement {
{offsetLabel(tz)}
{timeIn(tz)}
- +
))} {zones.length === 0 && ( diff --git a/client/src/pages/HelpPage.tsx b/client/src/pages/HelpPage.tsx index 12018ebab..4d5902178 100644 --- a/client/src/pages/HelpPage.tsx +++ b/client/src/pages/HelpPage.tsx @@ -72,7 +72,7 @@ export default function HelpPage() { {/* Content */}
{/* Mobile nav toggle */} -
diff --git a/client/src/pages/InAppNotificationsPage.tsx b/client/src/pages/InAppNotificationsPage.tsx index ea66e3886..a34c3a706 100644 --- a/client/src/pages/InAppNotificationsPage.tsx +++ b/client/src/pages/InAppNotificationsPage.tsx @@ -44,7 +44,7 @@ function InAppNotificationsPageDesktop(): React.ReactElement { {/* Bulk actions */}
{unreadCount > 0 && ( - )} {notifications.length > 0 && ( - - - -
- - {canEditJourney && ( -
-
{canEditJourney && ( - + )}
@@ -320,7 +320,7 @@ function JourneyDetailPageDesktop() { { id: 'gallery' as const, icon: Grid, label: t('journey.share.gallery') }, ] ).map(v => ( - )} {canEditEntries && view === 'gallery' && ( - )} {!feedEdge.atBottom && ( - - - - - )} - - diff --git a/client/src/pages/VacayPage.tsx b/client/src/pages/VacayPage.tsx index 420d266ed..cbfa25860 100644 --- a/client/src/pages/VacayPage.tsx +++ b/client/src/pages/VacayPage.tsx @@ -48,19 +48,19 @@ function VacayPageDesktop(): React.ReactElement {
- -
{selectedYear}
- -
@@ -129,13 +129,13 @@ function VacayPageDesktop(): React.ReactElement {

{t('admin.addons.catalog.vacay.name')}

- -
- -
@@ -232,11 +232,11 @@ function VacayPageDesktop(): React.ReactElement {
- - diff --git a/client/src/pages/admin/AdminNotificationsPanel.tsx b/client/src/pages/admin/AdminNotificationsPanel.tsx index d9173075f..a62072acd 100644 --- a/client/src/pages/admin/AdminNotificationsPanel.tsx +++ b/client/src/pages/admin/AdminNotificationsPanel.tsx @@ -93,7 +93,7 @@ export default function AdminNotificationsPanel({ t, toast }: { t: (k: string) = const isOn = matrix.preferences[eventType]?.[ch] ?? true return (
-
-
- -
- - - - - - - - )} - @@ -239,8 +239,8 @@ export default function AdminUsersTab({ admin, t, locale }: AdminUsersTabProps): )}
- - + +
diff --git a/client/src/pages/atlas/AtlasCountrySearch.tsx b/client/src/pages/atlas/AtlasCountrySearch.tsx index 800a50bf4..4c7bd0855 100644 --- a/client/src/pages/atlas/AtlasCountrySearch.tsx +++ b/client/src/pages/atlas/AtlasCountrySearch.tsx @@ -113,7 +113,7 @@ export default function AtlasCountrySearch({ }} /> {search.trim() && ( -