mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
98b97edb00
An adversarial review of the diff found sixteen defects that survived verification. The substantive ones: - The new `packing:bag-totals` event was never classified in wsEventPolicy, so the registry-parity ratchet was red. It belongs in HANDLED_OUTSIDE_TRIP_STORE: it carries no state to apply, which is the point of it. - The Journey Settings tracks switch called onSaved(), which closes the dialog. That destroyed the switch the moment it was flipped AND skipped the unsaved-changes guard, so a title typed but not saved was silently lost. It refreshes instead. - Deleting a bag sends no totals ping, yet packing_items.bag_id is ON DELETE SET NULL: everything in the bag lands in the unassigned pile and both figures move. The comment claiming otherwise was wrong. - Only the REST handlers pinged. The MCP tools and the plugin RPC are two more live write paths, and since the displayed number now comes from the server rather than from the item events, weights simply stopped updating for anything written through them. - Every item write pinged, including checking an item off — the most frequent packing write there is — and clearing twelve checked items sent twelve. The ping now fires only when a weight-relevant field changed, and the clients coalesce bursts and never overlap two refetches. - Bags have no repo and no Dexie table, so offline the server totals froze at the last online read and went blind to the mutation queue. A stale absolute number measured against an airline limit is worse than an honest partial one, so offline the surfaces sum what they can see. - SIGTERM arriving mid-bootstrap called server.close() on undefined, and the fire-and-forget promise turned that into an unhandled rejection: on Node 22 a crash, which is the opposite of the fix. - Promise.allSettled swallowed a rejected nestApp.close(), reporting a clean exit 0 while the crons and the plugin children were never torn down. This bug class is diagnosed from container logs. Also: the track fetch gate read a stale `current` during journey-to-journey navigation and fetched the previous journey's tracks; listBags ran the weight aggregate twice per request, on a route the ping now fires constantly; the grand total counted unassigned weight whose row was not rendered; the client declared show_trip_tracks boolean while the wire carries SQLite 0|1; and the six broadcast calls had no test, so removing all of them left the suite green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01S5VwmiBQ9sPCJwfmpv64r3
88 lines
5.5 KiB
TypeScript
88 lines
5.5 KiB
TypeScript
import { Plus } from 'lucide-react'
|
|
import type { PackingState } from './usePackingListPanel'
|
|
import { bagFillPct, bagTotalWeight, countsTowardsMyLoad, unassignedTotalWeight } from './packingListPanel.helpers'
|
|
import { BagCard } from './PackingListPanelBagCard'
|
|
|
|
export function BagSidebar(S: PackingState) {
|
|
const {
|
|
t, bags, items, tripId, tripMembers, canEdit, currentUserId, handleDeleteBag, handleUpdateBag, handleSetBagMembers,
|
|
showAddBag, setShowAddBag, newBagName, setNewBagName, handleCreateBag, unassignedWeightGrams, serverWeightsFresh,
|
|
} = S
|
|
// The ITEM LISTS still describe what you are carrying — an item someone shared
|
|
// with you stays in your list, but they are the one bringing it (#1767).
|
|
const myItems = items.filter(i => countsTowardsMyLoad(i, currentUserId))
|
|
// The WEIGHTS no longer do: a bag's load is the bag's, whoever packed it (#2191).
|
|
const bagWeightOf = (bag: typeof bags[number]) =>
|
|
bagTotalWeight(bag, myItems.filter(i => i.bag_id === bag.id), serverWeightsFresh)
|
|
// Reference for bags without a limit of their own — computed once instead of per bag.
|
|
const heaviestBagWeight = Math.max(...bags.map(bagWeightOf), 1)
|
|
return (
|
|
<div className="hidden xl:block" style={{ width: 260, marginLeft: 16, borderLeft: '1px solid var(--border-secondary)', overflowY: 'auto', padding: 16, flexShrink: 0 }}>
|
|
<div style={{ fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontWeight: 700, textTransform: 'uppercase', letterSpacing: '0.05em', color: 'var(--text-faint)', marginBottom: 12 }}>
|
|
{t('packing.bags')}
|
|
</div>
|
|
|
|
{bags.map(bag => {
|
|
const bagItems = myItems.filter(i => i.bag_id === bag.id)
|
|
const totalWeight = bagWeightOf(bag)
|
|
const pct = bagFillPct(totalWeight, bag.weight_limit_grams, heaviestBagWeight)
|
|
return (
|
|
<BagCard key={bag.id} bag={bag} bagItems={bagItems} totalWeight={totalWeight} pct={pct} tripId={tripId} tripMembers={tripMembers} canEdit={canEdit} onDelete={() => handleDeleteBag(bag.id)} onUpdate={handleUpdateBag} onSetMembers={handleSetBagMembers} t={t} compact />
|
|
)
|
|
})}
|
|
|
|
{/* Unassigned */}
|
|
{(() => {
|
|
const unassigned = myItems.filter(i => !i.bag_id)
|
|
const unassignedWeight = unassignedTotalWeight(unassignedWeightGrams, unassigned, serverWeightsFresh)
|
|
// Shown whenever there is weight to account for, even with no visible
|
|
// items — the grand total counts it either way (#2191).
|
|
if (unassigned.length === 0 && unassignedWeight === 0) return null
|
|
return (
|
|
<div style={{ marginBottom: 14, opacity: 0.6 }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 4 }}>
|
|
<span style={{ width: 10, height: 10, borderRadius: '50%', border: '2px dashed var(--border-primary)', flexShrink: 0 }} />
|
|
<span style={{ flex: 1, fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 600, color: 'var(--text-faint)' }}>{t('packing.noBag')}</span>
|
|
<span style={{ fontSize: 'calc(11px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)' }}>
|
|
{unassignedWeight >= 1000 ? `${(unassignedWeight / 1000).toFixed(1)} kg` : `${unassignedWeight} g`}
|
|
</span>
|
|
</div>
|
|
<div style={{ fontSize: 'calc(10px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)' }}>{unassigned.length} {t('admin.packingTemplates.items')}</div>
|
|
</div>
|
|
)
|
|
})()}
|
|
|
|
{/* Total */}
|
|
<div style={{ borderTop: '1px solid var(--border-secondary)', paddingTop: 10, marginTop: 6 }}>
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 'calc(12px * var(--fs-scale-body, 1))', fontWeight: 700, color: 'var(--text-primary)' }}>
|
|
<span>{t('packing.totalWeight')}</span>
|
|
<span>{(() => {
|
|
// Same rule as the rows above it (#2191).
|
|
const w = bags.reduce((s, b) => s + bagWeightOf(b), 0)
|
|
+ unassignedTotalWeight(unassignedWeightGrams, myItems.filter(i => !i.bag_id), serverWeightsFresh)
|
|
return w >= 1000 ? `${(w / 1000).toFixed(1)} kg` : `${w} g`
|
|
})()}</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Add bag */}
|
|
{canEdit && (showAddBag ? (
|
|
<div style={{ display: 'flex', gap: 4, marginTop: 12 }}>
|
|
<input autoFocus value={newBagName} onChange={e => setNewBagName(e.target.value)}
|
|
onKeyDown={e => { if (e.key === 'Enter') handleCreateBag(); if (e.key === 'Escape') { setShowAddBag(false); setNewBagName('') } }}
|
|
placeholder={t('packing.bagName')}
|
|
style={{ flex: 1, padding: '5px 8px', borderRadius: 8, border: '1px solid var(--border-primary)', fontSize: 'calc(11px * var(--fs-scale-caption, 1))', fontFamily: 'inherit', outline: 'none' }} />
|
|
<button type="button" onClick={handleCreateBag} style={{ padding: '4px 8px', borderRadius: 8, border: 'none', background: 'var(--text-primary)', color: 'var(--bg-primary)', cursor: 'pointer', display: 'flex', alignItems: 'center' }}>
|
|
<Plus size={12} />
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<button type="button" onClick={() => setShowAddBag(true)}
|
|
style={{ display: 'flex', alignItems: 'center', gap: 4, marginTop: 12, padding: '5px 8px', borderRadius: 8, border: '1px dashed var(--border-primary)', background: 'none', cursor: 'pointer', fontSize: 'calc(11px * var(--fs-scale-caption, 1))', color: 'var(--text-faint)', fontFamily: 'inherit', width: '100%' }}>
|
|
<Plus size={11} /> {t('packing.addBag')}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|