fix(import): migration preview and theater read all fiscal years, not just the newest (#1614)
The /sie-data route parsed only the newest fiscal year's SIE file for the import preview and the returned SIEData.parsed. Mid-year provider exports have few or zero vouchers in the newest year, so the first real Fortnox migration (3 fiscal years, 4153 vouchers) previewed "0 verifikationer" and drew an almost-empty migration theater while the import itself landed all 4153 vouchers from the older files. - New mergeParsedSIEFiles (lib/import/sie-merge.ts): pure, browser-clean whole-dataset merge (accounts union first-wins, vouchers concatenated, fiscal years union oldest-first re-indexed newest=0, balances and issues concatenated, dimensions deduped), with unit tests. - /sie-data parses each file exactly once, builds the preview from the merged parse and returns parsed: merged; response shape unchanged. Validation stays newest-file-only so no previously accepted dataset is newly rejected. - /preview drops latestOnly and computes sieStats from the merged parse: the connect step's "Hittade X konton och Y verifikationer" line renders from THESE stats, so this is where the founder-visible count was lying. - The migration theater spreads its account waves across ~10s and births an additional wave on each real step label during the SIE phase (progress <= 55), through a shared rate-limited gate, so the canvas keeps performing over a multi-minute run. Narration labels and progress remain the wizard's real values; reduced motion unchanged. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Jakob Wennberg
Claude Fable 5
parent
ea85f4c084
commit
b556475b01
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import TheaterCanvas, { type TheaterCanvasHandle } from '@/components/import/TheaterCanvas'
|
||||
@@ -59,9 +59,33 @@ export default function ArcimMigrationTheater({
|
||||
return () => window.clearInterval(id)
|
||||
}, [])
|
||||
|
||||
// Account waves (TheaterCanvas assigns account nodes wave = i % 4) are
|
||||
// rationed through one gate shared by the timer schedule and the
|
||||
// step-label reactions below. The floor between births matters: the
|
||||
// wizard emits three step labels within the first second ("Startar",
|
||||
// "Importerar bokföringsdata", "fil 1 av N"), and ungated spawns would
|
||||
// collapse the whole build back into the opening two seconds: the exact
|
||||
// "nothing left to perform" failure this pacing exists to fix.
|
||||
const accWaveRef = useRef(0)
|
||||
const nextWaveAtRef = useRef(0)
|
||||
const spawnNextAccountWave = useCallback(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas || accWaveRef.current >= 4) return
|
||||
const now = Date.now()
|
||||
if (now < nextWaveAtRef.current) return
|
||||
canvas.spawn('account', accWaveRef.current)
|
||||
accWaveRef.current += 1
|
||||
nextWaveAtRef.current = now + 1800
|
||||
}, [])
|
||||
|
||||
// Opening beat: the GL skeleton (year rings, buckets, account waves)
|
||||
// builds while phase 1 writes the journal. Reduced motion renders the
|
||||
// settled graph instead (via the settled prop below).
|
||||
// builds while phase 1 writes the journal. The account waves are spread
|
||||
// across ~10s instead of the first two: a multi-year migration runs for
|
||||
// minutes and the canvas must still be performing when file 2 starts.
|
||||
// Real step labels can pull waves forward through the shared gate; these
|
||||
// timers are the ceiling that guarantees a full build by ~10s even if
|
||||
// phase 1 hangs on one file. Reduced motion renders the settled graph
|
||||
// instead (via the settled prop below).
|
||||
useEffect(() => {
|
||||
if (reduced) return
|
||||
const canvas = () => canvasRef.current
|
||||
@@ -71,12 +95,13 @@ export default function ArcimMigrationTheater({
|
||||
at(1200, () => {
|
||||
canvas()?.spawn('bucket')
|
||||
canvas()?.feed(2100)
|
||||
;[0, 1, 2, 3].forEach((w) =>
|
||||
timers.push(window.setTimeout(() => canvas()?.spawn('account', w), 200 + w * 420)),
|
||||
)
|
||||
})
|
||||
// Floor slightly below the first timer so its own spawn never loses to
|
||||
// setTimeout jitter.
|
||||
nextWaveAtRef.current = Date.now() + 2300
|
||||
;[2400, 5000, 7600, 10200].forEach((ms) => at(ms, spawnNextAccountWave))
|
||||
return () => timers.forEach((id) => window.clearTimeout(id))
|
||||
}, [reduced])
|
||||
}, [reduced, spawnNextAccountWave])
|
||||
|
||||
// Real-event narration: append each distinct step label once, and let the
|
||||
// canvas mark the milestone. The entity phase (bar past its 55% handoff)
|
||||
@@ -94,13 +119,19 @@ export default function ArcimMigrationTheater({
|
||||
if (reduced) return
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
// SIE phase (bar at or below its 55% handoff): each real step, one per
|
||||
// posted file, also births the next account wave through the rationing
|
||||
// gate, so a long phase 1 keeps building the graph between the timer
|
||||
// beats. Only canvas pacing reacts here: labels and progress stay the
|
||||
// wizard's real values.
|
||||
if (progress <= 55) spawnNextAccountWave()
|
||||
if (progress > 55 && cpWaveRef.current < 3) {
|
||||
canvas.spawn('counterparty', cpWaveRef.current)
|
||||
cpWaveRef.current += 1
|
||||
}
|
||||
canvas.feed(900)
|
||||
if (progress >= 95) canvas.pulse()
|
||||
}, [currentStep, progress, reduced])
|
||||
}, [currentStep, progress, reduced, spawnNextAccountWave])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
||||
Reference in New Issue
Block a user