Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions app/src/context/DataContext.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,12 @@
import PreviewPublishModal from '../components/PreviewPublishModal';
import RecoveryModal from '../components/RecoveryModal';
import { apiClient } from '../lib/apiClient';
import { isLifecycleEnabled } from '../lib/lifecycleFlag';
import { trackGuestAdoption, trackPendingAge } from '../lib/lifecycleTelemetry';

const DataContext = createContext(null);

export function DataProvider({ children }) {
const { user, logout } = useAuth();
// Evaluated once at mount; rollback by setting localStorage lifecycle_override=false.
const lifecycleEnabledRef = useRef(isLifecycleEnabled());
const [savedCalcs, setSavedCalcs] = useState([]);
const [customYields, setCustomYields] = useState([]);
const [customSpecies, setCustomSpeciesState] = useState({});
Expand Down Expand Up @@ -85,13 +82,7 @@
const coordinator = useMemo(() => createSyncCoordinator(repo), [repo]);

// Load from IndexedDB when scope changes.
// When lifecycle is disabled (emergency rollback), skip IndexedDB — data loads on sync.
useEffect(() => {
if (!lifecycleEnabledRef.current) {
loadedScopeRef.current = scope;
setDataLoaded(true);
return;
}
let cancelled = false;
Comment on lines 85 to 86

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep honoring the lifecycle rollback flag

When VITE_LIFECYCLE_ENABLED=false or localStorage.lifecycle_override is set to false, this effect now unconditionally loads the identity-scoped repository, and triggerSync likewise runs the coordinator. Those settings were introduced specifically as deployment-time and per-browser emergency rollback controls, so deleting their checks makes an unhealthy lifecycle rollout impossible to disable and contradicts the claimed no-behavior-change cleanup. Preserve the gating until the rollback mechanism is deliberately retired.

Useful? React with 👍 / 👎.

const loadingScope = scope;
loadedScopeRef.current = null;
Expand Down Expand Up @@ -170,7 +161,7 @@
}

return { conflictCount: conflicts.length };
}, [repo, coordinator]);

Check warning on line 164 in app/src/context/DataContext.jsx

View workflow job for this annotation

GitHub Actions / Lint · Test · Build

React Hook useCallback has an unnecessary dependency: 'coordinator'. Either exclude it or remove the dependency array

// Detect guest records when user transitions from null → authenticated.
useEffect(() => {
Expand All @@ -184,7 +175,6 @@
}, [user]);

const triggerSync = useCallback(async () => {
if (!lifecycleEnabledRef.current) return;
if (!hasAuthCredential(user) || !navigator.onLine) return;
if (signingOutRef.current) return;
const gen = syncGenRef.current;
Expand Down
6 changes: 3 additions & 3 deletions app/src/lib/apiClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
* listUserDataRaw(extraHeaders) → Promise<Response>
*
* Methods suffixed *Raw return the raw Response so callers can inspect
* status codes (e.g. syncEngine needs to detect 401 and 404 without throwing).
* status codes (e.g. syncCoordinator needs to detect 401 and 404 without throwing).
*/

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -333,9 +333,9 @@ export function createApiClient(options = {}) {
}

// ---------------------
// Sync-engine raw API
// Sync coordinator raw API
// ---------------------
// These return the raw Response so syncEngine can inspect status codes
// These return the raw Response so syncCoordinator can inspect status codes
// (401 to break the loop, 404 as an acceptable delete outcome).

/**
Expand Down
51 changes: 0 additions & 51 deletions app/src/lib/lifecycleFlag.js

This file was deleted.

90 changes: 0 additions & 90 deletions app/src/lib/lifecycleFlag.test.js

This file was deleted.

166 changes: 0 additions & 166 deletions app/src/lib/localStore.js
Original file line number Diff line number Diff line change
@@ -1,177 +1,11 @@
import { get, set } from 'idb-keyval';

// Keys for our three stores
const SAVED_CALCS_KEY = 'fish-calc-saved-calcs';
const CUSTOM_YIELDS_KEY = 'fish-calc-custom-yields';
const CUSTOM_SPECIES_KEY = 'fish-calc-custom-species';

// --- Generic synced store factory ---

function createSyncedStore(storageKey) {
const getAll = async () => (await get(storageKey)) || [];

const add = async (item) => {
const items = await getAll();
const newItem = {
...item,
id: item.id || crypto.randomUUID(),
syncStatus: 'local',
updatedAt: new Date().toISOString(),
createdAt: item.createdAt || new Date().toISOString(),
};
items.push(newItem);
await set(storageKey, items);
return newItem;
};

const update = async (id, data) => {
const items = await getAll();
const index = items.findIndex((i) => i.id === id);
if (index === -1) return null;
items[index] = {
...items[index],
...data,
syncStatus: items[index].syncStatus === 'synced' ? 'local' : items[index].syncStatus,
updatedAt: new Date().toISOString(),
};
await set(storageKey, items);
return items[index];
};

const remove = async (id) => {
const items = await getAll();
const item = items.find((i) => i.id === id);
if (!item) return;

if (item.syncStatus === 'synced') {
item.syncStatus = 'pending-delete';
item.updatedAt = new Date().toISOString();
await set(storageKey, items);
} else {
await set(storageKey, items.filter((i) => i.id !== id));
}
};

const markSynced = async (id, serverId, serverRevision) => {
const items = await getAll();
const item = items.find((i) => i.id === id);
if (item) {
item.syncStatus = 'synced';
if (serverId) item.serverId = serverId;
if (serverRevision != null) item.serverRevision = serverRevision;
await set(storageKey, items);
}
};

const removeSyncedDelete = async (id) => {
const items = await getAll();
await set(storageKey, items.filter((i) => i.id !== id));
};

return { getAll, add, update, remove, markSynced, removeSyncedDelete };
}

// --- Store instances ---

const calcsStore = createSyncedStore(SAVED_CALCS_KEY);
const yieldsStore = createSyncedStore(CUSTOM_YIELDS_KEY);

// --- Named exports for backward compatibility ---

export const getSavedCalcs = calcsStore.getAll;
export const addSavedCalc = calcsStore.add;
export const deleteSavedCalc = calcsStore.remove;

export const getCustomYields = yieldsStore.getAll;
export const addCustomYield = yieldsStore.add;
export const updateCustomYield = yieldsStore.update;
export const deleteCustomYield = yieldsStore.remove;

// --- Sync helpers (typed exports instead of string dispatch) ---

export const markCalcSynced = calcsStore.markSynced;
export const markYieldSynced = yieldsStore.markSynced;
export const removeCalcSyncedDelete = calcsStore.removeSyncedDelete;
export const removeYieldSyncedDelete = yieldsStore.removeSyncedDelete;

export async function getAllPendingSync() {
const [calcs, yields] = await Promise.all([calcsStore.getAll(), yieldsStore.getAll()]);
return {
calcs: calcs.filter((c) => c.syncStatus === 'local' || c.syncStatus === 'pending-delete'),
yields: yields.filter((y) => y.syncStatus === 'local' || y.syncStatus === 'pending-delete'),
};
}

// --- Custom Species (different data model, no sync) ---

export async function getCustomSpecies() {
return (await get(CUSTOM_SPECIES_KEY)) || {};
}

export async function setCustomSpecies(data) {
await set(CUSTOM_SPECIES_KEY, data);
}

// --- Bulk merge operations (keep separate due to different field mappings) ---

export async function mergeSyncedCalcs(serverCalcs) {
const local = await calcsStore.getAll();
// Stringify all local serverIds for type-safe comparison (server returns ints,
// local may store them as numbers or strings depending on how they were saved).
// Also track items that are pending-delete so we never resurrect a tombstone.
const localServerIds = new Set(local.filter((c) => c.serverId).map((c) => String(c.serverId)));
const pendingDeleteIds = new Set(
local.filter((c) => c.syncStatus === 'pending-delete' && c.serverId).map((c) => String(c.serverId))
);

for (const sc of serverCalcs) {
const sid = String(sc.id);
// Skip if already tracked locally (any syncStatus) — especially pending-delete
// tombstones, which must not be resurrected by a pull from the server.
if (localServerIds.has(sid) || pendingDeleteIds.has(sid)) continue;

local.push({
...sc,
serverId: sc.id,
id: crypto.randomUUID(),
syncStatus: 'synced',
updatedAt: sc.created_at || new Date().toISOString(),
createdAt: sc.created_at || new Date().toISOString(),
});
}
await set(SAVED_CALCS_KEY, local);
}

export async function mergeSyncedYields(serverYields) {
const local = await yieldsStore.getAll();

for (const sy of serverYields) {
const existing = local.find((y) => String(y.serverId) === String(sy.id));
if (existing?.syncStatus === 'synced') {
// Refresh fields and revision for synced records so future update/delete
// sends the correct expected_revision. Leave pending-delete and local-edit
// records alone — they represent in-flight changes and must not be clobbered.
Object.assign(existing, {
species: sy.species,
product: sy.product,
yield: sy.yield,
source: sy.source || 'User Input',
serverRevision: sy.revision ?? null,
});
} else if (!existing) {
local.push({
species: sy.species,
product: sy.product,
yield: sy.yield,
source: sy.source || 'User Input',
serverId: sy.id,
serverRevision: sy.revision ?? null,
id: crypto.randomUUID(),
syncStatus: 'synced',
updatedAt: new Date().toISOString(),
});
}
// else: pending-delete tombstone or local edit — leave unchanged
}
await set(CUSTOM_YIELDS_KEY, local);
}
Loading
Loading