/ Current View source
Projects/PanoReady/Architecture diagrams

Architecture diagrams

Visual companion to the system design document. Every diagram reflects the current implementation — component and module names match the files in the repository.

1. System overview

PanoReady has no backend. Everything below the browser boundary is a download the user explicitly triggers; nothing crosses the network.

flowchart TB
    User(["👤 User"])

    subgraph Browser["🌐 Browser sandbox — nothing leaves this box unless the user downloads it"]
        direction TB

        subgraph UI["UI layer — Next.js 16 / React 19"]
            Intake["STIXIntake\nfile selection + workbook preview"]
            ValidateWF["ValidateAndFixWorkflow\n6-stage review"]
            CompareWF["CompareWorkflow\nchange review + export"]
        end

        subgraph Adapters["Intake adapters"]
            XmlParser["fast-xml-parser\nSTIX XML → records"]
            XlsxImporter["lib/excel.ts\nXLSM → canonical fields"]
        end

        subgraph Core["Canonical model + engines"]
            Canonical["lib/canonical.ts\nCanonicalUpload model"]
            Validator["lib/validator.ts\nvalidation + gate"]
            Cleaner["lib/cleaning.ts + lib/cleaner.ts\nmapping cleanup, formatting"]
            Compare["lib/compare.ts\nsnapshot diffing"]
            Rules["lib/rulesets.ts\nbuilt-in + custom rules"]
        end

        subgraph Out["Output"]
            Export["lib/stixExport.ts\nstructure-preserving XML writes"]
            Reports["CSV / XLSX generators"]
            Zip["@zip.js/zip.js\nAES-256 ZIP"]
        end

        Storage[("localStorage\nrulesets · workbook metadata")]
    end

    Downloads(["⬇️ XML · reports · ZIP"])

    User -->|selects file| Intake
    Intake --> XmlParser & XlsxImporter
    XmlParser --> Canonical
    XlsxImporter --> Canonical
    Canonical --> Validator
    Validator <--> Rules
    Validator --> ValidateWF
    ValidateWF --> Cleaner
    Canonical --> Compare
    Compare --> CompareWF
    ValidateWF --> Export
    CompareWF --> Export
    Export --> Reports --> Zip
    Rules <--> Storage
    Zip --> Downloads
    Export --> Downloads
    Reports --> Downloads
    Downloads --> User

    classDef ui fill:#3b6ea5,stroke:#1c3f66,color:#fff
    classDef adapter fill:#5c8a3a,stroke:#33511f,color:#fff
    classDef core fill:#a55b3b,stroke:#6b3620,color:#fff
    classDef out fill:#7a4fa0,stroke:#4a2f61,color:#fff
    classDef store fill:#8a8a8a,stroke:#4d4d4d,color:#fff
    class Intake,ValidateWF,CompareWF ui
    class XmlParser,XlsxImporter adapter
    class Canonical,Validator,Cleaner,Compare,Rules core
    class Export,Reports,Zip out
    class Storage store

2. Component map

How the top-level React components route into the lib/ engines. Arrows show direct imports/calls, not data direction.

flowchart LR
    Page["app/page.tsx"] --> Intake["STIXIntake.tsx"]
    Page --> VWF["ValidateAndFixWorkflow.tsx"]
    Page --> CWF["CompareWorkflow.tsx"]

    Intake -->|"importWorkbook / xlsmMetadata"| Excel["lib/excel.ts"]
    Intake -->|"parseSTIXXml"| Validator["lib/validator.ts"]
    Intake -->|"compareSTIXFiles"| Compare["lib/compare.ts"]

    VWF --> Assess["AssessmentView"]
    VWF --> CleanView["CleaningView / CleaningSummaryView"]
    VWF --> Issues["IssuesView"]
    VWF --> Fix["FixView"]
    VWF --> Revalidate["RevalidateView"]
    VWF --> Download["DownloadView"]

    Assess -->|"validateXml"| Validator
    Fix --> AddressCard["AddressRepairCard"]
    AddressCard -->|"analyze*"| AddressRepair["lib/addressRepair.ts"]
    Fix -->|"applyValidationFixes"| Validator
    CleanView -->|"applyCleaningProfile / discoverFieldValues"| Cleaning["lib/cleaning.ts"]
    Revalidate -->|"validateXml"| Validator
    Download -->|"prettyPrintXml"| Cleaner["lib/cleaner.ts"]
    Download -->|"processExport / buildSchoolCounts"| PullInfo["lib/pullInfo.ts"]
    Download -->|"generate*Csv"| Validator

    Validator -->|"parseCanonicalXml"| Canonical["lib/canonical.ts"]
    Validator -->|"analyzePhoneNumber"| Phone["lib/phoneNumber.ts"]
    Validator -->|"normalizeCanadianPostalCode"| Postal["lib/postalCode.ts"]

    VWF -->|"defaultRules / listCustomRulesets"| Rulesets["lib/rulesets.ts"]
    Intake --> RulesetSelector["RulesetSelector"] --> Rulesets

    CWF -->|"extractSchoolXml / applyReviewCorrections"| Export["lib/stixExport.ts"]
    CWF -->|"toCsv / downloadText / downloadBlob"| Utils["lib/utils.ts"]

    classDef page fill:#2f4f6f,stroke:#16283b,color:#fff
    classDef view fill:#3b6ea5,stroke:#1c3f66,color:#fff
    classDef lib fill:#a55b3b,stroke:#6b3620,color:#fff
    class Page page
    class Intake,VWF,CWF,Assess,CleanView,Issues,Fix,Revalidate,Download,AddressCard,RulesetSelector view
    class Excel,Validator,Compare,Canonical,Phone,Postal,AddressRepair,Cleaning,Cleaner,PullInfo,Rulesets,Export,Utils lib

3. Validate & Fix — stage flow

The six on-screen stages described in the Validate & Fix workflow guide, as ValidateAndFixWorkflow's internal state machine. "Clean summary" is optional and only reachable from Automatic fixes.

stateDiagram-v2
    [*] --> Assessing

    Assessing: 1 · Assess quality\n(AssessmentView → validateXml)
    Issues: 1 · Review issues\n(IssuesView)
    CleanSummary: Cleaning applied\n(CleaningSummaryView, optional)
    Fix: 2 · Automatic fixes\n(FixView "automatic")
    Manual: 3 · Manual fixes\n(FixView "manual")
    Revalidate: 4 · Recheck\n(RevalidateView → validateXml)
    Download: 5 · Download\n(DownloadView)

    Assessing --> Issues: validation complete
    Issues --> Fix: choose fixes to review
    Issues --> Download: skip when gate is\nREADY / REVIEW_REQUIRED
    Fix --> CleanSummary: apply cleaning mapping\n(optional, in advanced options)
    CleanSummary --> Fix: continue\n(fixes recorded, xml revalidated)
    Fix --> Manual: continue
    Manual --> Revalidate: apply selected fixes
    Revalidate --> Manual: return to fixes
    Revalidate --> Download: continue
    Download --> Manual: return to fixes
    Download --> [*]: exit / start over

    note right of Issues
        Backward navigation via
        WorkflowProgress is allowed to
        any completed earlier stage.
    end note

4. Compare Files — data flow

flowchart TD
    Prev["Previous STIX/XLSM"] --> ParsePrev["parseCanonicalXml"]
    Curr["Current STIX/XLSM"] --> ParseCurr["parseCanonicalXml"]

    ParsePrev --> Diff["compareSTIXFiles()\nmatch by OEN / name+DOB,\ndiff fields, detect school moves"]
    ParseCurr --> Diff

    Diff --> Comparison["STIXComparison\nschool · record · field · transfer changes\n+ signal (stable/moderate/high)"]

    Comparison --> Dashboard["CompareWorkflow dashboard\nKPIs · signal banner"]
    Dashboard --> Schools["School overview"]
    Dashboard --> Records["Record details\n(per-record review)"]
    Dashboard --> Fields["Field changes"]
    Dashboard --> Transfers["Transfers"]

    Records --> Decision{"Reviewer decision\nper changed field"}
    Decision -->|Confirm| Confirmed["decisions[key] = confirmed"]
    Decision -->|Needs fix| Correction["corrections[key] = new value"]

    Confirmed --> ExportStep["applyReviewCorrections()\nover extractSchoolXml()"]
    Correction --> ExportStep

    ExportStep --> XmlOut["Current XML\n(full or school-scoped)"]
    ExportStep --> ZipOut["AES-256 encrypted ZIP"]
    Dashboard --> CsvOut["School-change CSV\nReview-log CSV"]

    classDef input fill:#5c8a3a,stroke:#33511f,color:#fff
    classDef engine fill:#a55b3b,stroke:#6b3620,color:#fff
    classDef ui fill:#3b6ea5,stroke:#1c3f66,color:#fff
    classDef output fill:#7a4fa0,stroke:#4a2f61,color:#fff
    class Prev,Curr input
    class ParsePrev,ParseCurr,Diff,ExportStep engine
    class Dashboard,Schools,Records,Fields,Transfers,Decision ui
    class Comparison,XmlOut,ZipOut,CsvOut,Confirmed,Correction output

5. Workbook (XLSM) intake pipeline

How .xlsm uploads become STIX XML before either workflow ever sees them (lib/excel.ts).

flowchart LR
    File[".xlsm file"] --> Meta["xlsmMetadata()\ndiscover worksheet,\nfile-level metadata fields"]
    Meta --> Cache[("localStorage\npanoready:xlsm-metadata:v2:…")]
    Cache -. restores prior edits .-> Meta

    Meta --> Import["importWorkbook()"]
    Import --> Header["Header discovery\ncanonical aliases, not fixed positions"]
    Header --> Mapping{"Column status"}
    Mapping -->|MAPPED| Values["Value normalization\ndates, phones, units, lookups"]
    Mapping -->|UNMAPPED / DUPLICATE| Override["User column override\n(map or ignore)"]
    Override --> Values
    Mapping -->|IGNORED| Skip["excluded from output"]

    Values --> Preview["ImportPreview\ncolumns · diagnostics · counts"]
    Preview -->|blocking diagnostics present| Blocked["Import blocked\n(unmapped/duplicate column,\nambiguous date/phone, formula, count mismatch)"]
    Preview -->|clean| XmlOut["Generated STIX XML text"]

    XmlOut --> Downstream["parseSTIXXml() / validateXml()\n(same path as native XML upload)"]

    classDef step fill:#a55b3b,stroke:#6b3620,color:#fff
    classDef decision fill:#c9962c,stroke:#7a5a15,color:#111
    classDef terminal fill:#7a4fa0,stroke:#4a2f61,color:#fff
    class Meta,Import,Header,Values,Preview step
    class Mapping decision
    class XmlOut,Blocked,Downstream terminal

6. Canonical data model

The shared in-memory contract that both workflows validate, diff, and export against (lib/canonical.ts, lib/types.ts).

classDiagram
    class CanonicalUpload {
        schemaVersion: string
        batchId: string
        metadata: CanonicalMetadata
        schools: CanonicalSchool[]
        diagnostics: ValidationIssue[]
    }
    class CanonicalMetadata {
        createDate, createTime, createdBy: string
        contactPhone: CanonicalPhone
        contactEmail, fullUpload: string
        boardNumber, boardName: string
    }
    class CanonicalSchool {
        schoolId, schoolNumber, name: string
        students: CanonicalStudent[]
    }
    class CanonicalStudent {
        recordId, oen, grade, className: string
        name, aliasName: CanonicalName
        gender, birthDate, language: string
        countryOfOrigin: string
        guardians: CanonicalGuardian[]
        address: CanonicalAddress
        phone: CanonicalPhone
        provenance: Map~field, raw+sourceLocation~
    }
    class CanonicalGuardian {
        name: CanonicalName
        relationship: string
        phone: CanonicalPhone
    }
    class CanonicalAddress {
        unit, streetNumber, streetNumberSuffix: string
        streetName, streetType, streetDirection: string
        ruralRoute, poBoxNumber: string
        city, province, postalCode: string
    }
    class CanonicalName {
        first, middle, last: string
    }
    class CanonicalPhone {
        number, type: string
    }
    class ValidationIssue {
        severity: error|warning|info
        ruleId, field, message: string
        autoFixable: bool
        repairProposal: AddressRepairProposal
    }
    class AppliedFix {
        issueId, recordId, field: string
        oldValue, newValue: string
        ruleId: string
        appliedAt: number
    }
    class ValidateSession {
        fileName, originalXml: string
        initialResult: ValidationResult
        revalidatedResult: ValidationResult
        fixes: AppliedFix[]
        finalXml: string
    }
    class ValidationResult {
        issues: ValidationIssue[]
        records: StudentRecord[]
        gate: GateState
    }

    CanonicalUpload "1" *-- "1" CanonicalMetadata
    CanonicalUpload "1" *-- "*" CanonicalSchool
    CanonicalSchool "1" *-- "*" CanonicalStudent
    CanonicalStudent "1" *-- "1" CanonicalAddress
    CanonicalStudent "1" *-- "0..2" CanonicalGuardian
    CanonicalStudent "1" o-- "0..1" CanonicalPhone
    CanonicalStudent "1" *-- "1..2" CanonicalName
    CanonicalGuardian "1" o-- "0..1" CanonicalPhone
    CanonicalGuardian "1" *-- "1" CanonicalName
    CanonicalMetadata "1" o-- "0..1" CanonicalPhone
    CanonicalUpload "1" *-- "*" ValidationIssue : diagnostics
    ValidationResult "1" *-- "*" ValidationIssue
    ValidateSession "1" *-- "2" ValidationResult
    ValidateSession "1" *-- "*" AppliedFix

7. Deployment topology

next.config.ts switches output mode by environment; there is still no server-side data path in any of them — only where the static assets are served from changes.

flowchart LR
    subgraph Dev["Local development"]
        NextDev["next dev / next start\nnode server, port 3000"]
    end

    subgraph Jupyter["JupyterHub / Kubeflow proxy"]
        Proxy["Notebook proxy\nstrips path prefix"] --> NextServer["next start\nassetPrefix = NB_PREFIX/proxy/3000"]
    end

    subgraph Pages["GitHub Pages (GITHUB_PAGES=true)"]
        StaticBuild["next build --webpack\noutput: 'export'"] --> StaticFiles["Static files\nbasePath /PanoReady"]
        StaticFiles --> CDN["GitHub Pages CDN"]
    end

    Browser1(["Browser"]) --> NextDev
    Browser2(["Browser"]) --> Proxy
    Browser3(["Browser"]) --> CDN

    classDef env fill:#3b6ea5,stroke:#1c3f66,color:#fff
    classDef host fill:#5c8a3a,stroke:#33511f,color:#fff
    class NextDev,NextServer,StaticBuild env
    class Proxy,StaticFiles,CDN host

Diagrams are Mermaid and render directly on GitHub and in the published MkDocs site. Regenerate them when a workflow stage, module boundary, or data contract changes — see system-design-panoready.md for the prose description each diagram summarizes.