Format markdown files

This commit is contained in:
Przemek Więch
2026-07-05 22:51:04 +02:00
parent 72abbc7e7e
commit 85df7d13bb
27 changed files with 3602 additions and 1541 deletions

View File

@@ -10,15 +10,18 @@
## 2026-04-20
- Shift-click to show a person's details without rearranging the tree (by FrankBuchholz)
- Shift-click to show a person's details without rearranging the tree (by
FrankBuchholz)
## 2026-02-21
- Improved saving PDF files. Decreased file size and increased chart size that can be saved as PDF.
- Improved saving PDF files. Decreased file size and increased chart size that
can be saved as PDF.
## 2026-02-13
- Show header information of the gedcom file on the side panel (by FrankBuchholz)
- Show header information of the gedcom file on the side panel (by
FrankBuchholz)
## 2026-01-19
@@ -30,7 +33,8 @@
## 2025-01-21
- Added new chart type based on [github.com/donatso/family-chart](https://github.com/donatso/family-chart)
- Added new chart type based on
[github.com/donatso/family-chart](https://github.com/donatso/family-chart)
## 2023-08-25

View File

@@ -1,56 +1,93 @@
# Project Structure
This directory is the root directory of the **Topola Genealogy Viewer** project. It serves as the central hub containing project configuration, build setup, dependency management, and the main entry points for development, testing, and deployment. The actual application source code, assets, and tests are organized into subdirectories.
This directory is the root directory of the **Topola Genealogy Viewer** project.
It serves as the central hub containing project configuration, build setup,
dependency management, and the main entry points for development, testing, and
deployment. The actual application source code, assets, and tests are organized
into subdirectories.
## Purpose of this Directory
The root directory orchestrates the project. It contains configuration files for various tools (Vite, ESLint, Jest, Playwright, TypeScript), metadata about dependencies (`package.json`), and scripts for common tasks. It glues together the source code in `src`, the static assets in `public`, and the tests in `tests`.
The root directory orchestrates the project. It contains configuration files for
various tools (Vite, ESLint, Jest, Playwright, TypeScript), metadata about
dependencies (`package.json`), and scripts for common tasks. It glues together
the source code in `src`, the static assets in `public`, and the tests in
`tests`.
## Files in this Directory (Categorized by Role)
Here is an enumeration of the files in this directory, categorized by their role in the project:
Here is an enumeration of the files in this directory, categorized by their role
in the project:
### Application Entry Point
* **[index.html](index.html)**: The main HTML entry point for the application.
- **[index.html](index.html)**: The main HTML entry point for the application.
### Build & Environment
* **[.env](.env)**: Contains environment variables for the project.
* **[vite.config.mts](vite.config.mts)**: Configuration file for the Vite build tool.
- **[.env](.env)**: Contains environment variables for the project.
- **[vite.config.mts](vite.config.mts)**: Configuration file for the Vite build
tool.
### Dependencies & Scripts
* **[package.json](package.json)**: Defines the project's dependencies, scripts, version, and other metadata.
* **[package-lock.json](package-lock.json)**: Automatically generated file that describes the exact dependency tree that was generated by npm.
* **[deploy-wikitree.sh](deploy-wikitree.sh)**: A shell script used to deploy the application files to WikiTree servers using `lftp`.
- **[package.json](package.json)**: Defines the project's dependencies, scripts,
version, and other metadata.
- **[package-lock.json](package-lock.json)**: Automatically generated file that
describes the exact dependency tree that was generated by npm.
- **[deploy-wikitree.sh](deploy-wikitree.sh)**: A shell script used to deploy
the application files to WikiTree servers using `lftp`.
### Tool Configuration
* **[.eslintrc.js](.eslintrc.js)**: Configuration file for ESLint, used for identifying and reporting on patterns in JavaScript/TypeScript code.
* **[.gitignore](.gitignore)**: Specifies files and directories that should be ignored by Git.
* **[prettier.config.mjs](prettier.config.mjs)**: Configuration for Prettier code formatter.
* **[tsconfig.json](tsconfig.json)**: Configuration file for the TypeScript compiler.
- **[.eslintrc.js](.eslintrc.js)**: Configuration file for ESLint, used for
identifying and reporting on patterns in JavaScript/TypeScript code.
- **[.gitignore](.gitignore)**: Specifies files and directories that should be
ignored by Git.
- **[prettier.config.mjs](prettier.config.mjs)**: Configuration for Prettier
code formatter.
- **[tsconfig.json](tsconfig.json)**: Configuration file for the TypeScript
compiler.
### Testing
* **[playwright.config.ts](playwright.config.ts)**: Configuration file for Playwright end-to-end testing framework.
* **[playwright.prober.config.ts](playwright.prober.config.ts)**: Separate Playwright config for prober (live smoke) tests against deployed URLs.
* **[jest.config.ts](jest.config.ts)**: Configuration file for the Jest testing framework used for unit tests.
- **[playwright.config.ts](playwright.config.ts)**: Configuration file for
Playwright end-to-end testing framework.
- **[playwright.prober.config.ts](playwright.prober.config.ts)**: Separate
Playwright config for prober (live smoke) tests against deployed URLs.
- **[jest.config.ts](jest.config.ts)**: Configuration file for the Jest testing
framework used for unit tests.
### Documentation & Assets
* **[README.md](README.md)**: The main project documentation, covering features, examples, and usage instructions.
* **[CHANGELOG.md](CHANGELOG.md)**: Records a log of notable changes made to the project over time.
* **[docs/README.md](docs/README.md)**: Registry for system design files describing containerization, AI protocol interfaces, Playwright integration, and screenshot testing.
* **[LICENSE](LICENSE)**: The license file for the project (Apache License 2.0).
* **[screenshot.png](screenshot.png)**: An image showing a screenshot of the application, used in the README.md.
- **[README.md](README.md)**: The main project documentation, covering features,
examples, and usage instructions.
- **[CHANGELOG.md](CHANGELOG.md)**: Records a log of notable changes made to the
project over time.
- **[docs/README.md](docs/README.md)**: Registry for system design files
describing containerization, AI protocol interfaces, Playwright integration,
and screenshot testing.
- **[LICENSE](LICENSE)**: The license file for the project (Apache License 2.0).
- **[screenshot.png](screenshot.png)**: An image showing a screenshot of the
application, used in the README.md.
## Subdirectories Overview
Based on the documentation in the subdirectories, here is a high-level overview of the project's structure:
Based on the documentation in the subdirectories, here is a high-level overview
of the project's structure:
* **[docs](docs)**: High-level technical and architectural design documents.
* **[src](src)**: The core of the application.
* **[datasource](src/datasource)**: Handles loading and transforming genealogical data from various sources (files, URLs, WikiTree).
* **[menu](src/menu)**: Contains navigation and menu components.
* **[sidepanel](src/sidepanel)**: Implements the side panel for details and configuration.
* **[translations](src/translations)**: Holds localization JSON files.
* **[util](src/util)**: Common utilities for dates, analytics, and data processing.
* **[public](public)**: Contains static assets (like the favicon) served directly at the root path.
* **[tests](tests)**: Contains end-to-end tests.
* **[tests/probers](tests/probers)**: Live smoke tests (probers) that run against deployed URLs and external services.
- **[docs](docs)**: High-level technical and architectural design documents.
- **[src](src)**: The core of the application.
- **[datasource](src/datasource)**: Handles loading and transforming
genealogical data from various sources (files, URLs, WikiTree).
- **[menu](src/menu)**: Contains navigation and menu components.
- **[sidepanel](src/sidepanel)**: Implements the side panel for details and
configuration.
- **[translations](src/translations)**: Holds localization JSON files.
- **[util](src/util)**: Common utilities for dates, analytics, and data
processing.
- **[public](public)**: Contains static assets (like the favicon) served
directly at the root path.
- **[tests](tests)**: Contains end-to-end tests.
- **[tests/probers](tests/probers)**: Live smoke tests (probers) that run
against deployed URLs and external services.

142
README.md
View File

@@ -17,18 +17,19 @@ If you find this project useful, consider buying me a coffee.
[![buy me a coffee](https://www.buymeacoffee.com/assets/img/custom_images/yellow_img.png)](https://www.buymeacoffee.com/pewu)
## Features
* Hourglass chart
* All relatives chart
* Click on a person to focus
* Open standard GEDCOM files you can export from any genealogy application
* Load from URL (just point to a GEDCOM file on the Web)
* Privacy your files do not leave your computer
* Print the whole genealogy tree
* Export to PDF, PNG, SVG
* Side panel with details
* Configuration options
* Permalinks when loading from URL
* Cool transition animations
- Hourglass chart
- All relatives chart
- Click on a person to focus
- Open standard GEDCOM files you can export from any genealogy application
- Load from URL (just point to a GEDCOM file on the Web)
- Privacy your files do not leave your computer
- Print the whole genealogy tree
- Export to PDF, PNG, SVG
- Side panel with details
- Configuration options
- Permalinks when loading from URL
- Cool transition animations
[Changelog](CHANGELOG.md)
@@ -36,62 +37,82 @@ If you find this project useful, consider buying me a coffee.
Here is an example from the Web:
* [J. F. Kennedy](https://pewu.github.io/topola-viewer/#/view?url=https%3A%2F%2Fchronoplexsoftware.com%2Fmyfamilytree%2Fsamples%2FThe%2520Kennedy%2520Family.gdz) (from [chronoplexsoftware.com](https://chronoplexsoftware.com/myfamilytree/samples/))
* [Shakespeare](https://pewu.github.io/topola-viewer/#/view?url=https%3A%2F%2Fwebtreeprint.com%2Ftp_downloader.php%3Fpath%3Dfamous_gedcoms%2Fshakespeare.ged%26file%3Dshakespeare.ged) (from [webtreeprint.com](https://webtreeprint.com/tp_famous_gedcoms.php))
* [Maria Skłodowska-Curie](https://apps.wikitree.com/apps/wiech13/topola-viewer/#/view?indi=Sk%C5%82odowska-2&source=wikitree) (from [WikiTree](https://www.wikitree.com/wiki/Sk%C5%82odowska-2))
- [J. F. Kennedy](https://pewu.github.io/topola-viewer/#/view?url=https%3A%2F%2Fchronoplexsoftware.com%2Fmyfamilytree%2Fsamples%2FThe%2520Kennedy%2520Family.gdz)
(from
[chronoplexsoftware.com](https://chronoplexsoftware.com/myfamilytree/samples/))
- [Shakespeare](https://pewu.github.io/topola-viewer/#/view?url=https%3A%2F%2Fwebtreeprint.com%2Ftp_downloader.php%3Fpath%3Dfamous_gedcoms%2Fshakespeare.ged%26file%3Dshakespeare.ged)
(from [webtreeprint.com](https://webtreeprint.com/tp_famous_gedcoms.php))
- [Maria Skłodowska-Curie](https://apps.wikitree.com/apps/wiech13/topola-viewer/#/view?indi=Sk%C5%82odowska-2&source=wikitree)
(from [WikiTree](https://www.wikitree.com/wiki/Sk%C5%82odowska-2))
If you have data in a genealogy database, you can export your data in GEDCOM format and load it using the "Load from file" menu.
If you have data in a genealogy database, you can export your data in GEDCOM
format and load it using the "Load from file" menu.
## Supported file formats
Topola Genealogy Viewer supports the following file formats:
* **`.ged`** Standard GEDCOM file containing genealogical data.
* **`.gdz`** / **`.gedzip`** / **`.zip`** A ZIP archive that bundles a GEDCOM file together with associated media (photos, documents). This is useful when your family tree references image files and you want them to display in the chart.
- **`.ged`** Standard GEDCOM file containing genealogical data.
- **`.gdz`** / **`.gedzip`** / **`.zip`** A ZIP archive that bundles a GEDCOM
file together with associated media (photos, documents). This is useful when
your family tree references image files and you want them to display in the
chart.
### GEDZIP archives
A `.gdz` file is a standard ZIP archive. When you load one, Topola Viewer automatically unzips it in the browser (your data never leaves your computer), finds the first `.ged` file inside and uses it as the genealogy data, and treats all other files as images. Image file paths are normalized (backslashes converted to forward slashes, case lowered) so they match the references in the GEDCOM.
A `.gdz` file is a standard ZIP archive. When you load one, Topola Viewer
automatically unzips it in the browser (your data never leaves your computer),
finds the first `.ged` file inside and uses it as the genealogy data, and treats
all other files as images. Image file paths are normalized (backslashes
converted to forward slashes, case lowered) so they match the references in the
GEDCOM.
To create a `.gdz` archive, place your `.ged` file and image files (e.g. in a `photos/` folder) into a directory and zip them together:
To create a `.gdz` archive, place your `.ged` file and image files (e.g. in a
`photos/` folder) into a directory and zip them together:
```bash
zip -r family.gdz family.ged photos/
```
The resulting `family.gdz` file can be loaded via "Load from file", loaded from a URL, or mounted into a Docker container (see [Docker Container Deployment](#docker-container-deployment)).
The resulting `family.gdz` file can be loaded via "Load from file", loaded from
a URL, or mounted into a Docker container (see
[Docker Container Deployment](#docker-container-deployment)).
## Integrations
Topola Genealogy Viewer is being integrated into more and more Web and desktop applications.
Here are the current integrations:
Topola Genealogy Viewer is being integrated into more and more Web and desktop
applications. Here are the current integrations:
### Gramps
To view your [Gramps](https://gramps-project.org/) data in Topola Genealogy Viewer,
install [*Interactive Family Tree*](https://gramps-project.org/wiki/index.php/Interactive_Family_Tree)
plugin from the Gramps plugin manager. The plugin will add a
*Tools->Analysis and Exploration->Interactive Family Tree* menu item to Gramps.
To view your [Gramps](https://gramps-project.org/) data in Topola Genealogy
Viewer, install
[_Interactive Family Tree_](https://gramps-project.org/wiki/index.php/Interactive_Family_Tree)
plugin from the Gramps plugin manager. The plugin will add a _Tools->Analysis
and Exploration->Interactive Family Tree_ menu item to Gramps.
Source code: https://github.com/gramps-project/addons-source/tree/master/Topola
### Webtrees
Embed Topola Genealogy Viewer in your [Webtrees](https://www.webtrees.net/) installation with the
Embed Topola Genealogy Viewer in your [Webtrees](https://www.webtrees.net/)
installation with the
[Topola interactive tree addon](https://webtrees.net/download/modules#simple-auto-login---by-fanningert---20---website).
Source code: https://github.com/PeWu/topola-webtrees
### WikiTree
You can browse the [WikiTree](https://www.wikitree.com/) genealogy tree using Topola Genealogy Viewer.
On a WikiTree profile page go to the *Family Tree & Tools* tab and click the *Dynamic Tree by Topola* link.
You can browse the [WikiTree](https://www.wikitree.com/) genealogy tree using
Topola Genealogy Viewer. On a WikiTree profile page go to the _Family Tree &
Tools_ tab and click the _Dynamic Tree by Topola_ link.
Example:
[Stephen Hawking](https://apps.wikitree.com/apps/wiech13/topola-viewer/#/view?source=wikitree&standalone=false&indi=Hawking-7)
Topola Genealogy Viewer is hosted on [apps.wikitree.com](https://apps.wikitree.com/apps/wiech13/topola-viewer)
to benefit from the ability of being logged in to the WikiTree API.
Topola Genealogy Viewer is hosted on
[apps.wikitree.com](https://apps.wikitree.com/apps/wiech13/topola-viewer) to
benefit from the ability of being logged in to the WikiTree API.
## Running locally
@@ -102,19 +123,25 @@ npm start
## Self-hosting
You can host Topola Genealogy Viewer on your own server. There are no specific requirements for the hosting server. There is no code that is executed on the server side. The server only hosts the application files and whole application runs in the browser.
You can host Topola Genealogy Viewer on your own server. There are no specific
requirements for the hosting server. There is no code that is executed on the
server side. The server only hosts the application files and whole application
runs in the browser.
You can build Topola Genealogy Viewer from source code or take a ready-to-deploy package.
You can build Topola Genealogy Viewer from source code or take a ready-to-deploy
package.
### Bulid your own
Here are the commands to build the application:
```
git clone https://github.com/PeWu/topola-viewer.git
cd topola-viewer
npm install
npm run build
```
Now, take the contents of the `dist/` folder and host it on your own server.
### Use an existing package
@@ -126,21 +153,27 @@ These are the exact files that are hosted on GitHub pages.
### Build for your own data only
You can run Topola Viewer in a "single tree mode" that displays only the GEDCOM you specify. Specify the URL to a GEDCOM file in the `VITE_STATIC_URL` environment variable when building and running the application.
You can run Topola Viewer in a "single tree mode" that displays only the GEDCOM
you specify. Specify the URL to a GEDCOM file in the `VITE_STATIC_URL`
environment variable when building and running the application.
Run locally with the specified data URL:
```shell
VITE_STATIC_URL=https://example.org/sample.ged npm start
```
<details>
<summary>For Windows CMD:</summary>
```cmd
set VITE_STATIC_URL=https://example.org/sample.ged && npm run build
```
</details>
Build with the specified data URL:
```shell
VITE_STATIC_URL=https://example.org/sample.ged npm run build
```
@@ -151,13 +184,15 @@ VITE_STATIC_URL=https://example.org/sample.ged npm run build
```cmd
set VITE_STATIC_URL=https://example.org/sample.ged && npm run build
```
</details>
The `dist/` folder will contain files that can be hosted on a Web server.
### Build without Google Analytics
Set `VITE_GOOGLE_ANALYTICS=false` to exclude Google Analytics from the build output. This will remove the external JavaScript dependency.
Set `VITE_GOOGLE_ANALYTICS=false` to exclude Google Analytics from the build
output. This will remove the external JavaScript dependency.
```shell
VITE_GOOGLE_ANALYTICS=false npm run build
@@ -169,27 +204,38 @@ VITE_GOOGLE_ANALYTICS=false npm run build
```cmd
set VITE_GOOGLE_ANALYTICS=false && npm run build
```
</details>
This may be combined with the other build environment variables described above.
### Alternative build
The [topola-webpack](https://github.com/develancer/topola-webpack) tool can build a Topola Genealogy Viewer package bundled together with a GEDCOM file.
The [topola-webpack](https://github.com/develancer/topola-webpack) tool can
build a Topola Genealogy Viewer package bundled together with a GEDCOM file.
## Docker Container Deployment
Topola Viewer can be run locally or deployed to standard cloud environments using Docker.
Topola Viewer can be run locally or deployed to standard cloud environments
using Docker.
### Running Topola Viewer
To pull and run Topola Viewer:
```bash
docker run -d -p 8080:8080 ghcr.io/pewu/topola-viewer:latest
```
Open your web browser and go to `http://localhost:8080` to upload your family tree files locally.
Open your web browser and go to `http://localhost:8080` to upload your family
tree files locally.
### Running with Your Own Data (Zero-Build Run)
You can serve a standalone, pre-loaded family tree with zero compilation by mounting your family tree data (a `.ged` file or a zipped `.gdz` archive containing photos) directly into the running container:
You can serve a standalone, pre-loaded family tree with zero compilation by
mounting your family tree data (a `.ged` file or a zipped `.gdz` archive
containing photos) directly into the running container:
```bash
docker run -d -p 8080:8080 \
-e STATIC_URL=my_family.gdz \
@@ -198,16 +244,22 @@ docker run -d -p 8080:8080 \
```
### Building the Base Image Locally
To build the base image from source:
```bash
docker build -t topola-viewer -f docker/Dockerfile .
```
### Ready-To-Use Standalone Templates
For creating completely self-contained Docker images that bundle your genealogy data and serve it instantly, see these pre-configured examples:
1. **[Simple Standalone Tree](docker/examples/simple/)**: Demonstrates how to package and pre-load a `.ged` file directly inside a custom image.
2. **[Standalone Tree with Photos](docker/examples/photos/)**: Packages your family tree and a `photos/` folder into a valid `.gdz` archive on-the-fly.
For creating completely self-contained Docker images that bundle your genealogy
data and serve it instantly, see these pre-configured examples:
1. **[Simple Standalone Tree](docker/examples/simple/)**: Demonstrates how to
package and pre-load a `.ged` file directly inside a custom image.
2. **[Standalone Tree with Photos](docker/examples/photos/)**: Packages your
family tree and a `photos/` folder into a valid `.gdz` archive on-the-fly.
## Additional options
@@ -217,4 +269,6 @@ Add `&handleCors=false` to the URL to avoid using the CORS proxy
### `embedded`
Add `&embedded=true` to the URL. This option removes the options to open a different file. It is an option that was intended to be used when Topola Genealogy Viewer is in an iframe.
Add `&embedded=true` to the URL. This option removes the options to open a
different file. It is an option that was intended to be used when Topola
Genealogy Viewer is in an iframe.

View File

@@ -1,118 +1,303 @@
# Refactoring App.tsx Design Document
## Problem Description
The main application component, [App](../src/app.tsx), has grown into a large and complex monolith that coordinates URL routing, query parameter synchronization, asynchronous data fetching, UI layout rendering, and external integrations like the WebMCP bridge. Because these distinct responsibilities are combined within a single file, the codebase has become difficult to navigate, test, and safely modify without introducing unintended regressions. This refactoring effort aims to systematically decouple these concerns by decomposing the monolith into small, single-responsibility modules, separating the page-level UI layouts, and establishing the URL query parameters as the single source of truth for viewer state.
The main application component, [App](../src/app.tsx), has grown into a large
and complex monolith that coordinates URL routing, query parameter
synchronization, asynchronous data fetching, UI layout rendering, and external
integrations like the WebMCP bridge. Because these distinct responsibilities are
combined within a single file, the codebase has become difficult to navigate,
test, and safely modify without introducing unintended regressions. This
refactoring effort aims to systematically decouple these concerns by decomposing
the monolith into small, single-responsibility modules, separating the
page-level UI layouts, and establishing the URL query parameters as the single
source of truth for viewer state.
## The Technical Plan
At a high level, the refactored architecture splits user interface representation from business logic and external connections. Instead of a single central orchestrator holding all states, the app relies on the browser's URL to drive the active view and delegates sub-features (like data fetching, authentication, and external syncing) to dedicated hooks and services.
At a high level, the refactored architecture splits user interface
representation from business logic and external connections. Instead of a single
central orchestrator holding all states, the app relies on the browser's URL to
drive the active view and delegates sub-features (like data fetching,
authentication, and external syncing) to dedicated hooks and services.
### Explanatory Overview
* **Routing & Page Views:** The root [App](../src/app.tsx) component will serve purely as a routing switch, but will retain the Google Drive "Open with" flow redirection logic. It decides whether to display the landing screen (`IntroPage`) or the visual family tree explorer (`ViewPage`). It is also responsible for stripping external authorization query parameters (like `state` and `authcode`) from the browser window's URL.
* **Layout Nesting (TopBar & SidePanel):** Rather than having a global layout shell wrapping all pages, each page will compose its own layout explicitly. This allows the `ViewPage` to pass live chart selection data, configuration, and export triggers directly to its own rendered `TopBar`, while the `IntroPage` remains clean and completely decoupled from viewer states. To prevent locking the user interface on loading errors, `ViewPage` will render the `TopBar` even when in loading or error states.
* **URL as Single Source of Truth (SSOT):** Rather than keeping user settings (like the selected person, active chart view, and display preferences) in temporary React memory and continuously syncing them to the URL, the application will read these properties directly from the URL query string on every render. This includes the detail selection sidebar state (`detailIndi` will map to `?detail=...`). Changing a setting now simply updates the browser's URL, and React automatically updates the view.
* **Data Service Layer:** All logic involved in downloading, caching, and parsing GEDCOM or WikiTree files from different sources is fully isolated from the user interface, triggered via a unified API spec on the `ViewPage`.
* **WebMCP & Auth Hooks:** Third-party integrations and security layers (like the WebMCP developer bridge and Google Drive sign-in states) are extracted into custom React hooks. These hooks manage side effects independently, removing clutter from the layout rendering code.
- **Routing & Page Views:** The root [App](../src/app.tsx) component will serve
purely as a routing switch, but will retain the Google Drive "Open with" flow
redirection logic. It decides whether to display the landing screen
(`IntroPage`) or the visual family tree explorer (`ViewPage`). It is also
responsible for stripping external authorization query parameters (like
`state` and `authcode`) from the browser window's URL.
- **Layout Nesting (TopBar & SidePanel):** Rather than having a global layout
shell wrapping all pages, each page will compose its own layout explicitly.
This allows the `ViewPage` to pass live chart selection data, configuration,
and export triggers directly to its own rendered `TopBar`, while the
`IntroPage` remains clean and completely decoupled from viewer states. To
prevent locking the user interface on loading errors, `ViewPage` will render
the `TopBar` even when in loading or error states.
- **URL as Single Source of Truth (SSOT):** Rather than keeping user settings
(like the selected person, active chart view, and display preferences) in
temporary React memory and continuously syncing them to the URL, the
application will read these properties directly from the URL query string on
every render. This includes the detail selection sidebar state (`detailIndi`
will map to `?detail=...`). Changing a setting now simply updates the
browser's URL, and React automatically updates the view.
- **Data Service Layer:** All logic involved in downloading, caching, and
parsing GEDCOM or WikiTree files from different sources is fully isolated from
the user interface, triggered via a unified API spec on the `ViewPage`.
- **WebMCP & Auth Hooks:** Third-party integrations and security layers (like
the WebMCP developer bridge and Google Drive sign-in states) are extracted
into custom React hooks. These hooks manage side effects independently,
removing clutter from the layout rendering code.
## Alternatives Considered
### 1. Global State / Prop Drilling in Root Component
* **Approach:** Keep all the viewer and configuration state inside the root `App` component and pass them down as props to the nested components (such as `IntroPage` and `ViewPage`).
* **Why Rejected:** This would fail to address the core problem. The root `app.tsx` file would remain a massive state-managing monolith containing dozens of hooks and effects, making it just as hard to maintain and test as before.
- **Approach:** Keep all the viewer and configuration state inside the root
`App` component and pass them down as props to the nested components (such as
`IntroPage` and `ViewPage`).
- **Why Rejected:** This would fail to address the core problem. The root
`app.tsx` file would remain a massive state-managing monolith containing
dozens of hooks and effects, making it just as hard to maintain and test as
before.
### 2. React Context for Viewer Session
* **Approach:** Create a global React Context (e.g., `GenealogySessionContext`) that holds the active viewer's data, parameters, and callback handlers, and wrap the application root in a Context Provider.
* **Why Rejected:** While a valid pattern, introducing Context adds boilerplate and can lead to unnecessary re-renders. Explicit page-level nesting—where `ViewPage` and `IntroPage` each explicitly render their own `TopBar` headers—is a simpler and more isolated design. It allows components to consume direct, local props without global state providers.
- **Approach:** Create a global React Context (e.g., `GenealogySessionContext`)
that holds the active viewer's data, parameters, and callback handlers, and
wrap the application root in a Context Provider.
- **Why Rejected:** While a valid pattern, introducing Context adds boilerplate
and can lead to unnecessary re-renders. Explicit page-level nesting—where
`ViewPage` and `IntroPage` each explicitly render their own `TopBar`
headers—is a simpler and more isolated design. It allows components to consume
direct, local props without global state providers.
### 3. React-State-to-URL Syncing (Bidirectional Sync)
* **Approach:** Maintain React state variables (`useState`) for the selected individual, chart view type, and configuration, and keep them synchronized with the URL query parameters using `useEffect` loops.
* **Why Rejected:** Bidirectional synchronization is extremely error-prone and a frequent cause of infinite rendering loops and race conditions (especially with async network requests). Transitioning to the **URL as the Single Source of Truth** (deriving all active states directly from the URL on render) completely removes the sync hooks and ensures browser back/forward buttons work correctly out of the box.
- **Approach:** Maintain React state variables (`useState`) for the selected
individual, chart view type, and configuration, and keep them synchronized
with the URL query parameters using `useEffect` loops.
- **Why Rejected:** Bidirectional synchronization is extremely error-prone and a
frequent cause of infinite rendering loops and race conditions (especially
with async network requests). Transitioning to the **URL as the Single Source
of Truth** (deriving all active states directly from the URL on render)
completely removes the sync hooks and ensures browser back/forward buttons
work correctly out of the box.
### 4. Component-First Refactoring (Extracting Visual Leaf Nodes)
* **Approach:** Start by extracting pure UI layout leaf nodes (such as the chart display wrappers or sidebar layouts) before addressing the business logic.
* **Why Rejected:** The primary complexity in `app.tsx` lies in the lifecycle management and side effects, not the JSX markup size. Starting with leaf components leaves the complex state orchestrations untouched. Instead, we prioritized extracting utility modules, isolated background hooks (like `WebMcpBridge`), and transitioning states to the URL before separating UI pages.
- **Approach:** Start by extracting pure UI layout leaf nodes (such as the chart
display wrappers or sidebar layouts) before addressing the business logic.
- **Why Rejected:** The primary complexity in `app.tsx` lies in the lifecycle
management and side effects, not the JSX markup size. Starting with leaf
components leaves the complex state orchestrations untouched. Instead, we
prioritized extracting utility modules, isolated background hooks (like
`WebMcpBridge`), and transitioning states to the URL before separating UI
pages.
## Detailed Implementation
To refactor `app.tsx` safely and maintain continuous code correctness, we will modify one file and create eight new ones. This section maps out the role of each file and the step-by-step implementation plan.
To refactor `app.tsx` safely and maintain continuous code correctness, we will
modify one file and create eight new ones. This section maps out the role of
each file and the step-by-step implementation plan.
### Affected Files and Rationale
#### 1. [MODIFY] `src/app.tsx`
* **Role:** Will be stripped down to a lightweight Router component that only defines page-level paths, coordinates Google Drive "Open with" redirection synchronously on render, and renders `<IntroPage />` or `<ViewPage />`.
* **Rationale:** Eliminates the monolithic coordinator by delegating view orchestration, state synchronization, and page rendering to dedicated page components and hooks, while retaining initial payload interception.
- **Role:** Will be stripped down to a lightweight Router component that only
defines page-level paths, coordinates Google Drive "Open with" redirection
synchronously on render, and renders `<IntroPage />` or `<ViewPage />`.
- **Rationale:** Eliminates the monolithic coordinator by delegating view
orchestration, state synchronization, and page rendering to dedicated page
components and hooks, while retaining initial payload interception.
#### 2. [NEW] `src/components/error_display.tsx`
* **Role:** Will contain the presentational components `ErrorMessage` and `ErrorPopup`.
* **Rationale:** Keeps simple UI leaf components separated from the main business logic and state coordinates.
- **Role:** Will contain the presentational components `ErrorMessage` and
`ErrorPopup`.
- **Rationale:** Keeps simple UI leaf components separated from the main
business logic and state coordinates.
#### 3. [NEW] `src/datasource/instances.ts`
* **Role:** Declares and exports global datasource class instances (`uploadedDataSource`, `gedcomUrlDataSource`, `embeddedDataSource`, `googleDriveDataSource`).
* **Rationale:** Avoids declaring stateful data service instances inside layout and view components, improving separation of concerns.
- **Role:** Declares and exports global datasource class instances
(`uploadedDataSource`, `gedcomUrlDataSource`, `embeddedDataSource`,
`googleDriveDataSource`).
- **Rationale:** Avoids declaring stateful data service instances inside layout
and view components, improving separation of concerns.
#### 4. [NEW] `src/util/url_args.ts`
* **Role:** Exports `getStaticUrl`, `getParamFromSearch`, and `getArguments` utility functions, along with their related TypeScript interfaces (`Arguments`, `DataSourceSpec`), including support for the `detail` query parameter. It also provides a pure helper function (`getUrlForArgs`) that accepts current/new arguments and returns a path/query object suitable for passing to React Router's `navigate` function.
* **Rationale:** Moves parsing and decoding logic for query strings out of the React components, allowing this code to be tested independently in isolation.
- **Role:** Exports `getStaticUrl`, `getParamFromSearch`, and `getArguments`
utility functions, along with their related TypeScript interfaces
(`Arguments`, `DataSourceSpec`), including support for the `detail` query
parameter. It also provides a pure helper function (`getUrlForArgs`) that
accepts current/new arguments and returns a path/query object suitable for
passing to React Router's `navigate` function.
- **Rationale:** Moves parsing and decoding logic for query strings out of the
React components, allowing this code to be tested independently in isolation.
#### 5. [NEW] `src/util/url_args.spec.ts`
* **Role:** Jest unit tests verifying the correctness of `getArguments` under different query configurations (e.g. WikiTree authcodes, Google Drive file IDs, static URLs, local uploads).
* **Rationale:** URL parsing acts as the gateway to all loader code; ensuring this logic is 100% correct via unit testing prevents regressions during URL SSOT changes.
- **Role:** Jest unit tests verifying the correctness of `getArguments` under
different query configurations (e.g. WikiTree authcodes, Google Drive file
IDs, static URLs, local uploads).
- **Rationale:** URL parsing acts as the gateway to all loader code; ensuring
this logic is 100% correct via unit testing prevents regressions during URL
SSOT changes.
#### 6. [NEW] `src/hooks/use_webmcp_bridge.ts`
* **Role:** Custom React hook `useWebMcpBridge(data, detailIndi, onSelection)` called in `ViewPage` encapsulating the WebMCP bridge creation, tool registration, state synchronization, and cleanup effects.
* **Rationale:** Isolates external developer tools syncing from the UI components.
- **Role:** Custom React hook `useWebMcpBridge(data, detailIndi, onSelection)`
called in `ViewPage` encapsulating the WebMCP bridge creation, tool
registration, state synchronization, and cleanup effects.
- **Rationale:** Isolates external developer tools syncing from the UI
components.
#### 7. [NEW] `src/hooks/use_google_auth.ts`
* **Role:** Custom React hook `useGoogleAuth()` encapsulating access to the global `googleDriveService` authentication token, session storage cache clearing, and login/logout trigger states.
* **Rationale:** Provides shared authentication state that can be consumed by the `TopBar` headers of both page components without needing parent state lifting.
- **Role:** Custom React hook `useGoogleAuth()` encapsulating access to the
global `googleDriveService` authentication token, session storage cache
clearing, and login/logout trigger states.
- **Rationale:** Provides shared authentication state that can be consumed by
the `TopBar` headers of both page components without needing parent state
lifting.
#### 8. [NEW] `src/pages/view_page.tsx`
* **Role:** The page view orchestrating the chart visualizer workspace. It handles asynchronous loader triggers, keeps local data loading states, derives view configurations from the URL, and renders the Sidebar, SidePanel, and its own explicit `TopBar` (including rendering on loading/error states). It also hosts the state and rendering for `<GoogleAuthModal>` to handle Google Drive auth errors caught during data loading, handles memoization of `WikiTreeDataSource` (due to its react-intl context requirement), performs Object URL cleanup on unmount, and runs `updateChartWithConfig` synchronously during the render pass to synchronize URL-derived configuration settings with the in-memory mutated chart data.
* **Rationale:** Consolidates the chart workspace state and render tree away from the root router component.
- **Role:** The page view orchestrating the chart visualizer workspace. It
handles asynchronous loader triggers, keeps local data loading states, derives
view configurations from the URL, and renders the Sidebar, SidePanel, and its
own explicit `TopBar` (including rendering on loading/error states). It also
hosts the state and rendering for `<GoogleAuthModal>` to handle Google Drive
auth errors caught during data loading, handles memoization of
`WikiTreeDataSource` (due to its react-intl context requirement), performs
Object URL cleanup on unmount, and runs `updateChartWithConfig` synchronously
during the render pass to synchronize URL-derived configuration settings with
the in-memory mutated chart data.
- **Rationale:** Consolidates the chart workspace state and render tree away
from the root router component.
#### 9. [NEW] `src/pages/intro_page.tsx`
* **Role:** The landing page component wrapping the `Intro` presentation and rendering its own simple `TopBar`.
* **Rationale:** Decouples the landing screen structure from the chart visualizer's stateful layout shell.
- **Role:** The landing page component wrapping the `Intro` presentation and
rendering its own simple `TopBar`.
- **Rationale:** Decouples the landing screen structure from the chart
visualizer's stateful layout shell.
---
### Step-by-Step Execution Plan
#### Phase 1: Pure Component and Utility Extraction
* [x] **Step 1.1:** Create `src/components/error_display.tsx` and move `ErrorMessage` and `ErrorPopup` from `src/app.tsx`. Update imports in `src/app.tsx`.
* [x] **Step 1.2:** Create `src/datasource/instances.ts` and move data source class instantiations. Update references in `src/app.tsx`. Refactor `EmbeddedDataSource` to clean up its message event listener when the loading promise resolves or rejects (or track listener state) to prevent duplicate event listener leaks on multiple page mounts.
* [x] **Step 1.3:** Create `src/util/url_args.ts` (the parsing utility) and `src/util/url_args.spec.ts` (its Jest unit test suite) together. Extract URL query parameter parsing functions and types from `src/app.tsx`, write comprehensive tests, update imports in `src/app.tsx`, and run `npm test` to verify.
* [x] **Step 1.4:** Modify `src/menu/top_bar.tsx` to make chart-specific props and event handlers optional (e.g. `data`, `allowAllRelativesChart`, `allowPrintAndDownload`, `eventHandlers`, etc.), preparing the component for rendering on the landing screen without dummy properties.
- [x] **Step 1.1:** Create `src/components/error_display.tsx` and move
`ErrorMessage` and `ErrorPopup` from `src/app.tsx`. Update imports in
`src/app.tsx`.
- [x] **Step 1.2:** Create `src/datasource/instances.ts` and move data source
class instantiations. Update references in `src/app.tsx`. Refactor
`EmbeddedDataSource` to clean up its message event listener when the
loading promise resolves or rejects (or track listener state) to prevent
duplicate event listener leaks on multiple page mounts.
- [x] **Step 1.3:** Create `src/util/url_args.ts` (the parsing utility) and
`src/util/url_args.spec.ts` (its Jest unit test suite) together. Extract
URL query parameter parsing functions and types from `src/app.tsx`, write
comprehensive tests, update imports in `src/app.tsx`, and run `npm test`
to verify.
- [x] **Step 1.4:** Modify `src/menu/top_bar.tsx` to make chart-specific props
and event handlers optional (e.g. `data`, `allowAllRelativesChart`,
`allowPrintAndDownload`, `eventHandlers`, etc.), preparing the component
for rendering on the landing screen without dummy properties.
#### Phase 2: WebMCP Bridge Extraction
* [x] **Step 2.1:** Create `src/hooks/use_webmcp_bridge.ts` wrapping WebMCP registration and synchronization effects.
* [x] **Step 2.2:** Replace the inline WebMCP logic and `useEffect` blocks in `src/app.tsx` with a single call to the custom `useWebMcpBridge` hook. Verify using `npm test`. Note: During Phase 4, this hook call will be moved from `src/app.tsx` into `src/pages/view_page.tsx` where the layout state is relocated.
- [x] **Step 2.1:** Create `src/hooks/use_webmcp_bridge.ts` wrapping WebMCP
registration and synchronization effects.
- [x] **Step 2.2:** Replace the inline WebMCP logic and `useEffect` blocks in
`src/app.tsx` with a single call to the custom `useWebMcpBridge` hook.
Verify using `npm test`. Note: During Phase 4, this hook call will be
moved from `src/app.tsx` into `src/pages/view_page.tsx` where the layout
state is relocated.
#### Phase 3: Shifting to URL as Single Source of Truth
We will gradually eliminate React state variables in `src/app.tsx` and derive them (including settings like `standalone`, `showWikiTreeMenus`, and `freezeAnimation`) directly from the `useLocation()` search query parameters on render:
* [x] **Step 3.1a:** Shift `chartType` to URL as SSOT. Derive it using `useMemo` from search parameters, replace calls to `setChartType` with URL updates, and remove the React state variable and sync effect.
* [x] **Step 3.1b:** Shift `standalone` to URL as SSOT. Derive it directly from search parameters on render and remove its React state variable.
* [x] **Step 3.1c:** Shift `showWikiTreeMenus` to URL as SSOT. Derive it directly from search parameters on render and remove its React state variable.
* [x] **Step 3.1d:** Shift `freezeAnimation` to URL as SSOT. Derive it directly from search parameters on render and remove its React state variable.
* [x] **Step 3.2:** Extract `selection` and `detailIndi` state. Parse them directly from URL params (`indi`, `gen`, and `detail`); update display selectors. Remove React states. Ensure that chart selection changes (`onSelection` callback) explicitly clear or update the `detail` query parameter to match the new selection to avoid getting stuck on the old details viewport. Also update the detail-only selection handler (`onDetailSelection`) to update the `detail` query parameter in the URL.
* [x] **Step 3.3:** Extract `showSidePanel` state. Derive state directly from `?sidePanel=` parameter. Remove React state. Enhance URL args helpers to allow generating path/query target objects with replaced values, and ensure layout settings (`sidePanel`) and configuration changes use `replace: true` to prevent polluting the browser history stack.
* [x] **Step 3.4:** Extract `config` state. Parse display settings on render using `argsToConfig` helper. Remove state. In `ViewPage`, run `updateChartWithConfig(config, data)` synchronously during the render pass (e.g. in the `useMemo` that derives query parameters/config) to update the in-memory chart data before it is rendered by the `<Chart>` child component. Run E2E and visual tests to verify no rendering regressions occurred.
We will gradually eliminate React state variables in `src/app.tsx` and derive
them (including settings like `standalone`, `showWikiTreeMenus`, and
`freezeAnimation`) directly from the `useLocation()` search query parameters on
render:
- [x] **Step 3.1a:** Shift `chartType` to URL as SSOT. Derive it using `useMemo`
from search parameters, replace calls to `setChartType` with URL updates,
and remove the React state variable and sync effect.
- [x] **Step 3.1b:** Shift `standalone` to URL as SSOT. Derive it directly from
search parameters on render and remove its React state variable.
- [x] **Step 3.1c:** Shift `showWikiTreeMenus` to URL as SSOT. Derive it
directly from search parameters on render and remove its React state
variable.
- [x] **Step 3.1d:** Shift `freezeAnimation` to URL as SSOT. Derive it directly
from search parameters on render and remove its React state variable.
- [x] **Step 3.2:** Extract `selection` and `detailIndi` state. Parse them
directly from URL params (`indi`, `gen`, and `detail`); update display
selectors. Remove React states. Ensure that chart selection changes
(`onSelection` callback) explicitly clear or update the `detail` query
parameter to match the new selection to avoid getting stuck on the old
details viewport. Also update the detail-only selection handler
(`onDetailSelection`) to update the `detail` query parameter in the URL.
- [x] **Step 3.3:** Extract `showSidePanel` state. Derive state directly from
`?sidePanel=` parameter. Remove React state. Enhance URL args helpers to
allow generating path/query target objects with replaced values, and
ensure layout settings (`sidePanel`) and configuration changes use
`replace: true` to prevent polluting the browser history stack.
- [x] **Step 3.4:** Extract `config` state. Parse display settings on render
using `argsToConfig` helper. Remove state. In `ViewPage`, run
`updateChartWithConfig(config, data)` synchronously during the render pass
(e.g. in the `useMemo` that derives query parameters/config) to update the
in-memory chart data before it is rendered by the `<Chart>` child
component. Run E2E and visual tests to verify no rendering regressions
occurred.
#### Phase 4: Structural Page Nesting
* [x] **Step 4.1:** Create `src/hooks/use_google_auth.ts` to manage Google Drive authentication token updates. Remove `hasGoogleToken` state from `src/app.tsx`.
* [x] **Step 4.2:** Create `src/pages/view_page.tsx` containing layout rendering (`renderMainArea`), asynchronous data loaders, view state management, `revokeObjectUrls` cleanups, and `<GoogleAuthModal>` error fallback state and rendering.
* [x] **Step 4.3:** Create `src/pages/intro_page.tsx` rendering the landing screen and its own local header.
* [x] **Step 4.4:** Refactor `src/app.tsx` to serve as a pure routing switch routing to `<IntroPage />` or `<ViewPage />`. In `App`, handle Google Drive redirection synchronously during the render pass by returning a `<Navigate replace />` element (preventing render flashing), and ensure the redirection parser checks both the router `location.search` and the external `window.location.search` to handle HashRouter query structure robustly. Once external parameters (like `state` or `authcode`) are detected, they must be stripped from `window.location.search` using `window.history.replaceState` to prevent redirection loops on page refresh. Remove all layout state logic from the root file, including moving the `useWebMcpBridge` hook invocation into `<ViewPage />`. Run final verification scripts (`npm run check:all`).
- [x] **Step 4.1:** Create `src/hooks/use_google_auth.ts` to manage Google Drive
authentication token updates. Remove `hasGoogleToken` state from
`src/app.tsx`.
- [x] **Step 4.2:** Create `src/pages/view_page.tsx` containing layout rendering
(`renderMainArea`), asynchronous data loaders, view state management,
`revokeObjectUrls` cleanups, and `<GoogleAuthModal>` error fallback state
and rendering.
- [x] **Step 4.3:** Create `src/pages/intro_page.tsx` rendering the landing
screen and its own local header.
- [x] **Step 4.4:** Refactor `src/app.tsx` to serve as a pure routing switch
routing to `<IntroPage />` or `<ViewPage />`. In `App`, handle Google
Drive redirection synchronously during the render pass by returning a
`<Navigate replace />` element (preventing render flashing), and ensure
the redirection parser checks both the router `location.search` and the
external `window.location.search` to handle HashRouter query structure
robustly. Once external parameters (like `state` or `authcode`) are
detected, they must be stripped from `window.location.search` using
`window.history.replaceState` to prevent redirection loops on page
refresh. Remove all layout state logic from the root file, including
moving the `useWebMcpBridge` hook invocation into `<ViewPage />`. Run
final verification scripts (`npm run check:all`).
## Future Considerations
### 1. Image Object URL Memory Management
* Instead of managing image revocation inside the component lifecycle (which can break back-navigation since the `ViewPage` unmounts and revokes URLs), we will explore having the global datasource singletons in `instances.ts` (such as `UploadedDataSource` and `GoogleDriveDataSource`) manage the lifetime of their image URL maps. When `loadData` is called on a datasource, it will check if it has a reference to a previous image map and explicitly revoke it before loading the new dataset.
- Instead of managing image revocation inside the component lifecycle (which can
break back-navigation since the `ViewPage` unmounts and revokes URLs), we will
explore having the global datasource singletons in `instances.ts` (such as
`UploadedDataSource` and `GoogleDriveDataSource`) manage the lifetime of their
image URL maps. When `loadData` is called on a datasource, it will check if it
has a reference to a previous image map and explicitly revoke it before
loading the new dataset.
### 2. TopBar Layout Refactoring
* Refactor `top_bar.tsx` to make all chart-specific event handlers and properties optional, or split the header layout from the stateful workspace menus. This will eliminate passing dummy or empty handlers from the `IntroPage`.
- Refactor `top_bar.tsx` to make all chart-specific event handlers and
properties optional, or split the header layout from the stateful workspace
menus. This will eliminate passing dummy or empty handlers from the
`IntroPage`.

View File

@@ -2,79 +2,164 @@
## 1. Problem Description
Topola Viewer is a modern, client-side web application designed for exploring and visualizing genealogy data from GEDCOM files. Currently, running a custom instance with a pre-loaded family tree and photos requires manual local development builds or setting up complex web hosting environments. This project aims to package Topola Viewer inside an ultra-lightweight, secure, and production-ready Docker container. This container will allow users to instantly deploy a vanilla viewer or easily serve their own private, self-contained family trees with zero local compilation or build-tool dependencies.
Topola Viewer is a modern, client-side web application designed for exploring
and visualizing genealogy data from GEDCOM files. Currently, running a custom
instance with a pre-loaded family tree and photos requires manual local
development builds or setting up complex web hosting environments. This project
aims to package Topola Viewer inside an ultra-lightweight, secure, and
production-ready Docker container. This container will allow users to instantly
deploy a vanilla viewer or easily serve their own private, self-contained family
trees with zero local compilation or build-tool dependencies.
## 2. Major Components & How They Interact
At a high level, the containerized Topola Viewer consists of three primary components working together to serve and display family trees:
At a high level, the containerized Topola Viewer consists of three primary
components working together to serve and display family trees:
1. **The User's Web Browser (The App)**: This is where Topola Viewer actually runs. Since it is a single-page React application, the browser downloads the application files once and executes all the logic, chart rendering, and user interactions locally on the user's computer.
2. **The Caddy Web Server (The Helper)**: This is a lightweight, secure, and fast background server running inside the Docker container. Its job is to serve the application files (HTML, CSS, JavaScript) and the family tree package when the browser requests them. Additionally, Caddy dynamically updates the main HTML page on-the-fly when it is loaded, injecting the path or address of the family tree file specified by the container's configuration.
3. **The Genealogy Data (The Data)**: This is the family tree description file. Depending on whether you have local photo assets:
* **Unzipped File (`.ged`)**: If you do not have local photos, you can serve your standard, plain-text `.ged` file directly.
* **Zipped Archive (`.gdz` or `.zip`)**: If you want to bundle family photos, you compress the `.ged` file and the photos together into a single archive. Topola Viewer will automatically unzip and map the photos to the tree.
Serving this as a single file (either `.ged` or `.gdz`) keeps the container setup simple and highly portable.
1. **The User's Web Browser (The App)**: This is where Topola Viewer actually
runs. Since it is a single-page React application, the browser downloads the
application files once and executes all the logic, chart rendering, and user
interactions locally on the user's computer.
2. **The Caddy Web Server (The Helper)**: This is a lightweight, secure, and
fast background server running inside the Docker container. Its job is to
serve the application files (HTML, CSS, JavaScript) and the family tree
package when the browser requests them. Additionally, Caddy dynamically
updates the main HTML page on-the-fly when it is loaded, injecting the path
or address of the family tree file specified by the container's
configuration.
3. **The Genealogy Data (The Data)**: This is the family tree description file.
Depending on whether you have local photo assets:
- **Unzipped File (`.ged`)**: If you do not have local photos, you can serve
your standard, plain-text `.ged` file directly.
- **Zipped Archive (`.gdz` or `.zip`)**: If you want to bundle family photos,
you compress the `.ged` file and the photos together into a single archive.
Topola Viewer will automatically unzip and map the photos to the tree.
Serving this as a single file (either `.ged` or `.gdz`) keeps the container
setup simple and highly portable.
## 3. Alternative Designs Considered & Rejected
To ensure future development does not regress or re-argue established choices, this section documents the architectural patterns that were thoroughly evaluated but ultimately rejected.
To ensure future development does not regress or re-argue established choices,
this section documents the architectural patterns that were thoroughly evaluated
but ultimately rejected.
### Option A: Build-Time Variable Injection (`VITE_STATIC_URL`)
* **Design Proposal**: Build custom container images by setting `VITE_STATIC_URL` during the Vite production build step (`npm run build`) inside the Dockerfile.
* **Why Rejected**:
* **Heavy & Slow Builds**: Recompiling a modern React SPA inside Docker requires a full Node.js runtime, downloading `node_modules`, and compiling TypeScript. This takes minutes, consumes significant system resources, and makes building a custom image locally a highly friction-filled developer experience.
* **No Runtime Dynamism**: Since the static URL is hardcoded into minified JS assets during compiling, users cannot change their genealogy file path dynamically when running the container. A simple task like pointing the container to a new GEDCOM file would require a full image rebuild, rather than a simple environment variable or volume mount adjustment.
- **Design Proposal**: Build custom container images by setting
`VITE_STATIC_URL` during the Vite production build step (`npm run build`)
inside the Dockerfile.
- **Why Rejected**:
- **Heavy & Slow Builds**: Recompiling a modern React SPA inside Docker
requires a full Node.js runtime, downloading `node_modules`, and compiling
TypeScript. This takes minutes, consumes significant system resources, and
makes building a custom image locally a highly friction-filled developer
experience.
- **No Runtime Dynamism**: Since the static URL is hardcoded into minified JS
assets during compiling, users cannot change their genealogy file path
dynamically when running the container. A simple task like pointing the
container to a new GEDCOM file would require a full image rebuild, rather
than a simple environment variable or volume mount adjustment.
### Option B: Client-Side Config Fetching (`fetch('/config.json')`)
* **Design Proposal**: At React application startup, trigger an asynchronous HTTP request (`fetch('/config.json')`) to retrieve the configuration and target tree location.
* **Why Rejected**:
* **Unnecessary Startup Latency**: Every asynchronous network call in a client-side SPA blocks the React application mount lifecycle. Even on high-performance servers, fetching `/config.json` introduces an extra network roundtrip at boot time.
- **Design Proposal**: At React application startup, trigger an asynchronous
HTTP request (`fetch('/config.json')`) to retrieve the configuration and
target tree location.
- **Why Rejected**:
- **Unnecessary Startup Latency**: Every asynchronous network call in a
client-side SPA blocks the React application mount lifecycle. Even on
high-performance servers, fetching `/config.json` introduces an extra
network roundtrip at boot time.
### Option C: Shell-Based Template Rendering (Nginx Alpine + `envsubst`)
* **Design Proposal**: Run the static application on an Nginx Alpine base container, using a startup shell script and the `envsubst` tool to substitute environment variables inside `index.html` before launching the web server.
* **Why Rejected**:
* **Vulnerability Attack Surface**: Standard Linux and Alpine base images contain shell environments (`/bin/sh`), package managers (`apk`), and standard operating system utilities. These represent a non-zero container attack surface, leading to vulnerability alerts (CVEs) in enterprise environments.
- **Design Proposal**: Run the static application on an Nginx Alpine base
container, using a startup shell script and the `envsubst` tool to substitute
environment variables inside `index.html` before launching the web server.
- **Why Rejected**:
- **Vulnerability Attack Surface**: Standard Linux and Alpine base images
contain shell environments (`/bin/sh`), package managers (`apk`), and
standard operating system utilities. These represent a non-zero container
attack surface, leading to vulnerability alerts (CVEs) in enterprise
environments.
### Option D: Custom Compiled Go Static Server
* **Design Proposal**: Compile a custom, lightweight 35-line Go static web server program that handles SPA routing and dynamically injects environment variables directly into the `index.html` served from memory.
* **Why Rejected**:
* **Maintenance Complexity Overhead**: Although highly performant and lightweight (~10MB total container size), introducing custom compiled server source code adds to repository maintenance. Developers would have to test, audit, and maintain custom Go HTTP routing logic alongside their main React/TypeScript codebase. Using an off-the-shelf server (Caddy) eliminates this maintenance entirely.
- **Design Proposal**: Compile a custom, lightweight 35-line Go static web
server program that handles SPA routing and dynamically injects environment
variables directly into the `index.html` served from memory.
- **Why Rejected**:
- **Maintenance Complexity Overhead**: Although highly performant and
lightweight (~10MB total container size), introducing custom compiled server
source code adds to repository maintenance. Developers would have to test,
audit, and maintain custom Go HTTP routing logic alongside their main
React/TypeScript codebase. Using an off-the-shelf server (Caddy) eliminates
this maintenance entirely.
### Option E: Multi-Architecture Image Support (`linux/arm64`)
* **Design Proposal**: Package and publish multi-architecture container images targeting both standard x86_64 (`linux/amd64`) and ARM64 (`linux/arm64`) platforms.
* **Why Rejected**:
* **Pipeline Emulation Latency**: Building multi-architecture images on standard AMD64 GitHub Actions runners requires virtualized instruction emulation via QEMU. Emulating the TypeScript build and Go compiler inside a QEMU virtual environment increases container compilation cycles by up to 10x to 20x, significantly bloating release delays.
* **No Server-Side Execution Penalty**: Topola Viewer is a pure, client-side React single-page application (SPA). The browser executes all chart rendering and data logic on the end-user's computer (whether it runs on x86_64, ARM64/Apple Silicon, or mobile platforms). The container's internal web server (Caddy) simply serves static HTML/JS assets. Running the `linux/amd64` image under standard Docker architecture translation (e.g., Rosetta 2 or Docker Desktop VM) on ARM64/M-series hosts has absolutely zero visible performance penalty.
* **Workflow Stability**: Focusing exclusively on `linux/amd64` standardizes our GitHub Actions runner steps, completely removes complex third-party dependencies like `setup-qemu-action`, and ensures build cycles remain blazingly fast, secure, and reliable.
- **Design Proposal**: Package and publish multi-architecture container images
targeting both standard x86_64 (`linux/amd64`) and ARM64 (`linux/arm64`)
platforms.
- **Why Rejected**:
- **Pipeline Emulation Latency**: Building multi-architecture images on
standard AMD64 GitHub Actions runners requires virtualized instruction
emulation via QEMU. Emulating the TypeScript build and Go compiler inside a
QEMU virtual environment increases container compilation cycles by up to 10x
to 20x, significantly bloating release delays.
- **No Server-Side Execution Penalty**: Topola Viewer is a pure, client-side
React single-page application (SPA). The browser executes all chart
rendering and data logic on the end-user's computer (whether it runs on
x86_64, ARM64/Apple Silicon, or mobile platforms). The container's internal
web server (Caddy) simply serves static HTML/JS assets. Running the
`linux/amd64` image under standard Docker architecture translation (e.g.,
Rosetta 2 or Docker Desktop VM) on ARM64/M-series hosts has absolutely zero
visible performance penalty.
- **Workflow Stability**: Focusing exclusively on `linux/amd64` standardizes
our GitHub Actions runner steps, completely removes complex third-party
dependencies like `setup-qemu-action`, and ensures build cycles remain
blazingly fast, secure, and reliable.
## 4. Detailed Implementation Plan
This section outlines the complete, step-by-step implementation plan. It lists every file that will be modified or created, the exact code modifications or configurations required, and the explicit engineering rationale for each.
This section outlines the complete, step-by-step implementation plan. It lists
every file that will be modified or created, the exact code modifications or
configurations required, and the explicit engineering rationale for each.
### Step 1: Add Global Config Injection Target to HTML
* **Target File**: [index.html](../index.html) (Modify)
* **Action**: Add a `<meta>` tag containing the dynamic injection placeholder inside the `<head>` tag:
- **Target File**: [index.html](../index.html) (Modify)
- **Action**: Add a `<meta>` tag containing the dynamic injection placeholder
inside the `<head>` tag:
```html
<meta name="topola-static-url" content="{{ env `STATIC_URL` | html }}">
<meta name="topola-static-url" content="{{ env `STATIC_URL` | html }}" />
```
* **Rationale**:
* Storing the configuration safely inside a `<meta>` tag content attribute ensures it is parsed strictly as data rather than executable code, preventing Reflected Cross-Site Scripting (XSS) or script injection vulnerabilities.
* **HTML Parsing Protection**: Enclosing the template expression in backticks (`` `STATIC_URL` ``) prevents syntactically broken nested double quotes in the HTML `content` attribute, which would otherwise break standard browser HTML tag parsing.
* The Caddy `| html` filter guarantees that the environment variable is fully HTML-entity-escaped before injection.
* The placeholder syntax `{{ env `STATIC_URL` | html }}` is evaluated dynamically by Caddy when serving the page, adding zero network latency.
- **Rationale**:
- Storing the configuration safely inside a `<meta>` tag content attribute
ensures it is parsed strictly as data rather than executable code,
preventing Reflected Cross-Site Scripting (XSS) or script injection
vulnerabilities.
- **HTML Parsing Protection**: Enclosing the template expression in backticks
(`` `STATIC_URL` ``) prevents syntactically broken nested double quotes in
the HTML `content` attribute, which would otherwise break standard browser
HTML tag parsing.
- The Caddy `| html` filter guarantees that the environment variable is fully
HTML-entity-escaped before injection.
- The placeholder syntax `{{ env `STATIC_URL` | html }}` is evaluated
dynamically by Caddy when serving the page, adding zero network latency.
### Step 2: Update Application Boot Logic to Handle Global Config
* **Target File**: [src/app.tsx](../src/app.tsx) (Modify)
* **Action**: Update the application boot logic to handle dynamic config via the `<meta>` tag or Vite static URL, override standalone/CORS properties, and adjust route settings:
* **Config Resolution**: Retrieve the statically-served GEDCOM/GDZ URL.
* **Arguments Setup**: Disable standalone mode and bypass CORS handling when a static URL is provided.
* **Conditional Routing**: Force routing directly to `/view` (bypassing the standard intro landing page) when `staticUrl` is set.
- **Target File**: [src/app.tsx](../src/app.tsx) (Modify)
- **Action**: Update the application boot logic to handle dynamic config via the
`<meta>` tag or Vite static URL, override standalone/CORS properties, and
adjust route settings:
- **Config Resolution**: Retrieve the statically-served GEDCOM/GDZ URL.
- **Arguments Setup**: Disable standalone mode and bypass CORS handling when a
static URL is provided.
- **Conditional Routing**: Force routing directly to `/view` (bypassing the
standard intro landing page) when `staticUrl` is set.
```typescript
// 1. Global Config Resolution
function getStaticUrl(): string | undefined {
@@ -122,14 +207,26 @@ This section outlines the complete, step-by-step implementation plan. It lists e
</Routes>
)}
```
* **Rationale**:
* **Security & Safety**: Retrieving configuration via the DOM's `<meta>` element ensures we parse data context safely rather than relying on direct executable script template injection.
* **UX Modularity (Zero-Friction Mode)**: Bypassing the Intro page and disabling the standard "open file" menus (`standalone: false`) transforms the general-purpose viewer into a streamlined, dedicated instance for the preloaded tree.
* **Backward Compatibility**: Keeps standard dev runs (`npm start`) and static deployments (GitHub Pages/WikiTree) working out-of-the-box because the template string `{{ env "STATIC_URL" }}` is safely ignored when it hasn't been evaluated by Caddy.
- **Rationale**:
- **Security & Safety**: Retrieving configuration via the DOM's `<meta>`
element ensures we parse data context safely rather than relying on direct
executable script template injection.
- **UX Modularity (Zero-Friction Mode)**: Bypassing the Intro page and
disabling the standard "open file" menus (`standalone: false`) transforms
the general-purpose viewer into a streamlined, dedicated instance for the
preloaded tree.
- **Backward Compatibility**: Keeps standard dev runs (`npm start`) and static
deployments (GitHub Pages/WikiTree) working out-of-the-box because the
template string `{{ env "STATIC_URL" }}` is safely ignored when it hasn't
been evaluated by Caddy.
### Step 3: Create Caddy Server Configuration
* **Target File**: `docker/Caddyfile` (New File in dedicated directory)
* **Action**: Add Caddy serving rules to handle robust security headers, optimized caching, SPA routing, and active template evaluation:
- **Target File**: `docker/Caddyfile` (New File in dedicated directory)
- **Action**: Add Caddy serving rules to handle robust security headers,
optimized caching, SPA routing, and active template evaluation:
```caddy
{
# Disable administrative API to prevent permission errors in read-only environments
@@ -173,19 +270,36 @@ This section outlines the complete, step-by-step implementation plan. It lists e
try_files {path} /index.html
}
```
* **Rationale**:
* **Security Headers**: Protects production deployments against clickjacking, MIME-sniffing, and referrer leaks through standard secure response headers.
* **Disable Admin Endpoint**: Setting `admin off` prevents Caddy from attempting to bind administrative sockets or write auto-saved configuration files (`caddy_autosave.json`) to its working directory, avoiding fatal startup failures in rootless containers and secure read-only filesystems.
* **Asset Compression**: Adding `encode gzip zstd` ensures that large client-side JS bundles and plain-text GEDCOM files are compressed, dramatically reducing load-time latency and server bandwidth costs.
* **Precise Caching Rules**: Restructures caching to only apply `immutable` tags to assets in `/assets/*` (which contains Vite's hashed JS/CSS files). This ensures user-supplied dynamic family trees (`.ged`, `.gdz`) and mounted photos do not get cached permanently, allowing instant runtime updates.
* **HTML Templating**: Caddy dynamically processes variables (like `{{ env `STATIC_URL` | html }}`) for HTML documents, avoiding performance overhead on other resources.
* **Non-Root Port**: Serving from port `8080` permits the container to run as an unprivileged user without root capabilities.
- **Rationale**:
- **Security Headers**: Protects production deployments against clickjacking,
MIME-sniffing, and referrer leaks through standard secure response headers.
- **Disable Admin Endpoint**: Setting `admin off` prevents Caddy from
attempting to bind administrative sockets or write auto-saved configuration
files (`caddy_autosave.json`) to its working directory, avoiding fatal
startup failures in rootless containers and secure read-only filesystems.
- **Asset Compression**: Adding `encode gzip zstd` ensures that large
client-side JS bundles and plain-text GEDCOM files are compressed,
dramatically reducing load-time latency and server bandwidth costs.
- **Precise Caching Rules**: Restructures caching to only apply `immutable`
tags to assets in `/assets/*` (which contains Vite's hashed JS/CSS files).
This ensures user-supplied dynamic family trees (`.ged`, `.gdz`) and mounted
photos do not get cached permanently, allowing instant runtime updates.
- **HTML Templating**: Caddy dynamically processes variables (like
`{{ env `STATIC_URL` | html }}`) for HTML documents, avoiding performance
overhead on other resources.
- **Non-Root Port**: Serving from port `8080` permits the container to run as
an unprivileged user without root capabilities.
---
### Step 4: Create Multi-Stage Dockerfile
* **Target File**: `docker/Dockerfile` (New File in dedicated directory)
* **Action**: Write the multi-stage compilation and assembly pipeline using a pre-compiled Caddy binary and Google's library-free Distroless Static base image running as nonroot:
- **Target File**: `docker/Dockerfile` (New File in dedicated directory)
- **Action**: Write the multi-stage compilation and assembly pipeline using a
pre-compiled Caddy binary and Google's library-free Distroless Static base
image running as nonroot:
```dockerfile
# Stage 1: Compile the React/TypeScript bundle
FROM node:20-alpine AS react-builder
@@ -212,16 +326,34 @@ This section outlines the complete, step-by-step implementation plan. It lists e
EXPOSE 8080
ENTRYPOINT ["/usr/bin/caddy", "run", "--config", "./Caddyfile", "--adapter", "caddyfile"]
```
* **Rationale**:
* **Clean Compilation Separation**: Restructures compilation using a multi-stage build where the heavy Node.js and TypeScript compiler packages are restricted entirely to the builder stage, keeping the final runner image extremely lightweight and free of development tools.
* **Static Caddy Binary**: Copying the pre-compiled, statically linked Caddy binary directly from the official `caddy` Alpine image. Since the official Caddy binary is a pure, library-independent Go executable built without CGO, it runs flawlessly on top of a Google Distroless Static image, completely bypassing the need to compile Caddy from source and reducing build times by several minutes.
* **Distroless Static Base**: Standardizing on `distroless/static-debian12` removes *all* dynamic libraries, shells, and packages from the runtime container. This cuts container image size to under ~25MB and eliminates runtime vulnerability scan alerts (CVEs) completely.
* **Non-Root Execution**: Switching to `USER nonroot:nonroot` inside the production stage ensures the application runs with minimum privileges, satisfying strict enterprise and Kubernetes execution policies.
* **Explicit File Ownership**: Using `COPY --chown=nonroot:nonroot` guarantees that all files in the runtime container are owned by the runtime non-root user, bypassing permissions conflicts.
- **Rationale**:
- **Clean Compilation Separation**: Restructures compilation using a
multi-stage build where the heavy Node.js and TypeScript compiler packages
are restricted entirely to the builder stage, keeping the final runner image
extremely lightweight and free of development tools.
- **Static Caddy Binary**: Copying the pre-compiled, statically linked Caddy
binary directly from the official `caddy` Alpine image. Since the official
Caddy binary is a pure, library-independent Go executable built without CGO,
it runs flawlessly on top of a Google Distroless Static image, completely
bypassing the need to compile Caddy from source and reducing build times by
several minutes.
- **Distroless Static Base**: Standardizing on `distroless/static-debian12`
removes _all_ dynamic libraries, shells, and packages from the runtime
container. This cuts container image size to under ~25MB and eliminates
runtime vulnerability scan alerts (CVEs) completely.
- **Non-Root Execution**: Switching to `USER nonroot:nonroot` inside the
production stage ensures the application runs with minimum privileges,
satisfying strict enterprise and Kubernetes execution policies.
- **Explicit File Ownership**: Using `COPY --chown=nonroot:nonroot` guarantees
that all files in the runtime container are owned by the runtime non-root
user, bypassing permissions conflicts.
### Step 5: Create Docker Ignore File
* **Target File**: `.dockerignore` (New File at root)
* **Action**: Exclude local development folders and build environments from entering the Docker context:
- **Target File**: `.dockerignore` (New File at root)
- **Action**: Exclude local development folders and build environments from
entering the Docker context:
```text
node_modules
dist
@@ -231,12 +363,17 @@ This section outlines the complete, step-by-step implementation plan. It lists e
.vscode
README.md
```
* **Rationale**:
* Speeds up local `docker build` times by preventing megabytes of local folders (like `node_modules` and local `dist` directories) from uploading to the Docker daemon build context.
- **Rationale**:
- Speeds up local `docker build` times by preventing megabytes of local
folders (like `node_modules` and local `dist` directories) from uploading to
the Docker daemon build context.
### Step 6: Create GitHub Actions Container Deployment Workflow
* **Target File**: `.github/workflows/deploy-docker.yml` (New File)
* **Action**: Setup a GitHub Actions workflow to compile, tag, and publish the container images to GHCR, handling lowercase names and safe tagging:
- **Target File**: `.github/workflows/deploy-docker.yml` (New File)
- **Action**: Setup a GitHub Actions workflow to compile, tag, and publish the
container images to GHCR, handling lowercase names and safe tagging:
```yaml
name: Build and Publish Docker Image
@@ -290,17 +427,33 @@ This section outlines the complete, step-by-step implementation plan. It lists e
cache-from: type=gha
cache-to: type=gha,mode=max
```
* **Rationale**:
* **Strict Lowercase Registry Names**: Registry image names must be strictly lowercase. Converting the repository path to lowercase prevents Docker push failures due to uppercase organization or repository names (e.g. `PeWu`).
* **Safe Production Tagging**: Restricts pushing the `latest` tag to master branch runs only, preventing development branches or forks from accidentally overwriting the stable master production image.
* **No QEMU Dependency**: Because the image targets standard `linux/amd64` servers directly, we bypass slow CPU instruction emulation completely. This eliminates the `setup-qemu-action` dependency, protecting the workflow against virtualizer crashes and speeding up build initialization.
* **Git SHA Tagging**: Generates git SHA tags automatically, allowing precise auditing and rolling back of deployments.
* **Modern Actions Standard**: Upgrades all critical deployment actions to their latest major releases to optimize runner speeds, secure security improvements, and align with Node 20/22 GitHub Action standard runner specifications.
* **Active GitHub Caching**: Leverages standard action build cache stores to reuse layers and accelerate build cycles.
- **Rationale**:
- **Strict Lowercase Registry Names**: Registry image names must be strictly
lowercase. Converting the repository path to lowercase prevents Docker push
failures due to uppercase organization or repository names (e.g. `PeWu`).
- **Safe Production Tagging**: Restricts pushing the `latest` tag to master
branch runs only, preventing development branches or forks from accidentally
overwriting the stable master production image.
- **No QEMU Dependency**: Because the image targets standard `linux/amd64`
servers directly, we bypass slow CPU instruction emulation completely. This
eliminates the `setup-qemu-action` dependency, protecting the workflow
against virtualizer crashes and speeding up build initialization.
- **Git SHA Tagging**: Generates git SHA tags automatically, allowing precise
auditing and rolling back of deployments.
- **Modern Actions Standard**: Upgrades all critical deployment actions to
their latest major releases to optimize runner speeds, secure security
improvements, and align with Node 20/22 GitHub Action standard runner
specifications.
- **Active GitHub Caching**: Leverages standard action build cache stores to
reuse layers and accelerate build cycles.
### Step 7: Couple Docker Publication to existing Main Deployment Pipeline
* **Target File**: [.github/workflows/deploy-everywhere.yml](deploy-everywhere.yml) (Modify)
* **Action**: Integrate the newly created Docker workflow as a concurrent job:
- **Target File**:
[.github/workflows/deploy-everywhere.yml](deploy-everywhere.yml) (Modify)
- **Action**: Integrate the newly created Docker workflow as a concurrent job:
```yaml
name: Deploy everywhere
@@ -318,26 +471,42 @@ This section outlines the complete, step-by-step implementation plan. It lists e
uses: ./.github/workflows/deploy-docker.yml
secrets: inherit
```
* **Rationale**:
* Integrates the container deployment pipeline seamlessly into the main release trigger (`deploy-everywhere`), ensuring that Docker, GH Pages, and WikiTree versions are always updated in lockstep.
- **Rationale**:
- Integrates the container deployment pipeline seamlessly into the main
release trigger (`deploy-everywhere`), ensuring that Docker, GH Pages, and
WikiTree versions are always updated in lockstep.
### Step 8: Document Container Usage in Main README
* **Target File**: `README.md` (Modify)
* **Action**: Add a dedicated "Docker Container Deployment" section with clear run, build, mount, and standalone template instructions:
```markdown
- **Target File**: `README.md` (Modify)
- **Action**: Add a dedicated "Docker Container Deployment" section with clear
run, build, mount, and standalone template instructions:
````markdown
## Docker Container Deployment
Topola Viewer can be run locally or deployed to standard cloud environments using Docker.
Topola Viewer can be run locally or deployed to standard cloud environments
using Docker.
### Running Topola Viewer
To pull and run Topola Viewer:
```bash
docker run -d -p 8080:8080 ghcr.io/pewu/topola-viewer:latest
```
Open your web browser and go to `http://localhost:8080` to upload your family tree files locally.
````
Open your web browser and go to `http://localhost:8080` to upload your family
tree files locally.
### Running with Your Own Data (Zero-Build Run)
You can serve a standalone, pre-loaded family tree with zero compilation by mounting your family tree data (a `.ged` file or a zipped `.gdz` archive containing photos) directly into the running container:
You can serve a standalone, pre-loaded family tree with zero compilation by
mounting your family tree data (a `.ged` file or a zipped `.gdz` archive
containing photos) directly into the running container:
```bash
docker run -d -p 8080:8080 \
-e STATIC_URL=my_family.gdz \
@@ -346,34 +515,55 @@ This section outlines the complete, step-by-step implementation plan. It lists e
```
### Building the Base Image Locally
To build the base image from source:
```bash
docker build -t topola-viewer -f docker/Dockerfile .
```
### Ready-To-Use Standalone Templates
For creating completely self-contained Docker images that bundle your genealogy data and serve it instantly, see these pre-configured examples:
1. **[Simple Standalone Tree](docker/examples/simple/)**: Demonstrates how to package and pre-load a `.ged` file directly inside a custom image.
2. **[Standalone Tree with Photos](docker/examples/photos/)**: Packages your family tree and a `photos/` folder into a valid `.gdz` archive on-the-fly.
For creating completely self-contained Docker images that bundle your
genealogy data and serve it instantly, see these pre-configured examples:
1. **[Simple Standalone Tree](docker/examples/simple/)**: Demonstrates how to
package and pre-load a `.ged` file directly inside a custom image.
2. **[Standalone Tree with Photos](docker/examples/photos/)**: Packages your
family tree and a `photos/` folder into a valid `.gdz` archive on-the-fly.
```
* **Rationale**:
* Ensures the new Docker feature has first-class visibility, clear instructions, and easy references to packaged standalone templates for end users, enabling both basic runs, volume-mounted local data serving, and built-in custom imagery.
```
- **Rationale**:
- Ensures the new Docker feature has first-class visibility, clear
instructions, and easy references to packaged standalone templates for end
users, enabling both basic runs, volume-mounted local data serving, and
built-in custom imagery.
### Step 9: Provide Custom Image Templates (Simple & Zipped on the Fly)
* **Target Files**:
* `docker/examples/simple/Dockerfile` (New File)
* `docker/examples/simple/README.md` (New File)
* `docker/examples/simple/family.ged` (New simple, valid example GEDCOM file)
* `docker/examples/photos/Dockerfile` (New File)
* `docker/examples/photos/README.md` (New File)
* `docker/examples/photos/family.ged` (New simple, valid example GEDCOM file)
* `docker/examples/photos/photos/I1.jpg` (New simple, valid example photo asset)
* `docker/examples/photos/photos/I2.jpg` (New simple, valid example photo asset)
* **Action**: Create two dedicated subdirectories containing turnkey templates. To make these examples instantly runnable out-of-the-box, we provide simple, valid example files (`family.ged`, `I1.jpg`, and `I2.jpg`) inside the repository so users can immediately run `docker build` and test the container features without having to prepare their own private files first.
- **Target Files**:
- `docker/examples/simple/Dockerfile` (New File)
- `docker/examples/simple/README.md` (New File)
- `docker/examples/simple/family.ged` (New simple, valid example GEDCOM file)
- `docker/examples/photos/Dockerfile` (New File)
- `docker/examples/photos/README.md` (New File)
- `docker/examples/photos/family.ged` (New simple, valid example GEDCOM file)
- `docker/examples/photos/photos/I1.jpg` (New simple, valid example photo
asset)
- `docker/examples/photos/photos/I2.jpg` (New simple, valid example photo
asset)
- **Action**: Create two dedicated subdirectories containing turnkey templates.
To make these examples instantly runnable out-of-the-box, we provide simple,
valid example files (`family.ged`, `I1.jpg`, and `I2.jpg`) inside the
repository so users can immediately run `docker build` and test the container
features without having to prepare their own private files first.
#### 1. Simple GEDCOM Template (`docker/examples/simple`)
* **`docker/examples/simple/Dockerfile`**:
- **`docker/examples/simple/Dockerfile`**:
```dockerfile
# Start from the official compiled container
FROM ghcr.io/pewu/topola-viewer:latest
@@ -384,11 +574,14 @@ This section outlines the complete, step-by-step implementation plan. It lists e
# Configure server to pre-load this raw GEDCOM file
ENV STATIC_URL=family.ged
```
* **`docker/examples/simple/README.md`**:
```markdown
- **`docker/examples/simple/README.md`**:
````markdown
# Standalone GEDCOM Container Example
This example builds a self-contained image that hosts a single `.ged` file directly (no photos).
This example builds a self-contained image that hosts a single `.ged` file
directly (no photos).
## Instructions
@@ -397,14 +590,21 @@ This section outlines the complete, step-by-step implementation plan. It lists e
```bash
docker build -t my-simple-tree .
```
````
3. Run your container:
```bash
docker run -d -p 8080:8080 my-simple-tree
```
```
```
#### 2. Zipped Media Template (`docker/examples/photos`)
* **`docker/examples/photos/Dockerfile`**:
- **`docker/examples/photos/Dockerfile`**:
```dockerfile
# Stage 1: Multi-stage helper to zip GEDCOM & photos together preserving directory structure
FROM alpine:latest AS zipper
@@ -420,15 +620,23 @@ This section outlines the complete, step-by-step implementation plan. It lists e
COPY --from=zipper /build/family.gdz /app/public/family.gdz
ENV STATIC_URL=family.gdz
```
* **`docker/examples/photos/README.md`**:
```markdown
- **`docker/examples/photos/README.md`**:
````markdown
# Standalone Zipped Family Tree Container with Photos
This example leverages a multi-stage Docker build to automatically compress your `.ged` file and `photos/` folder into a secure `.gdz` archive on-the-fly, preserving your image directory path structures.
This example leverages a multi-stage Docker build to automatically compress
your `.ged` file and `photos/` folder into a secure `.gdz` archive on-the-fly,
preserving your image directory path structures.
## Structure
* Place your `family.ged` file here.
* Place your photos also in this directory, or inside a `photos/` folder in this directory. If you put the photos in the `photos/` directory, make sure your GEDCOM file contains file references containing the `photos/` prefix. See the sample [family.ged](family.ged).
- Place your `family.ged` file here.
- Place your photos also in this directory, or inside a `photos/` folder in
this directory. If you put the photos in the `photos/` directory, make sure
your GEDCOM file contains file references containing the `photos/` prefix.
See the sample [family.ged](family.ged).
## Instructions
@@ -436,12 +644,28 @@ This section outlines the complete, step-by-step implementation plan. It lists e
```bash
docker build -t my-photo-tree .
```
````
2. Run your container:
```bash
docker run -d -p 8080:8080 my-photo-tree
```
```
* **Rationale**:
* **Simple Template**: Demonstrates the standard, zero-friction path for users who just have a raw `.ged` file.
* **Photos Template (On-The-Fly Zipper)**: Solves the problem of executing commands in a shell-less, commandless distroless container. By spinning up a lightweight Alpine zipper image to execute the native `zip` tool, and copying *only* the finished `.gdz` artifact into the final stage, we achieve a completely self-contained, secure target image.
* **Preserved Path Zip File Structure**: Packaging photos by zipping from the build root (`zip -r family.gdz family.ged photos/`) preserves the exact folder hierarchy relative to the GEDCOM. This guarantees that any complex or structured media folders (e.g., `photos/1990s/wedding.jpg`) match the exact file references declared inside the GEDCOM file, avoiding broken images from flattened zip scopes.
```
- **Rationale**:
- **Simple Template**: Demonstrates the standard, zero-friction path for users
who just have a raw `.ged` file.
- **Photos Template (On-The-Fly Zipper)**: Solves the problem of executing
commands in a shell-less, commandless distroless container. By spinning up a
lightweight Alpine zipper image to execute the native `zip` tool, and
copying _only_ the finished `.gdz` artifact into the final stage, we achieve
a completely self-contained, secure target image.
- **Preserved Path Zip File Structure**: Packaging photos by zipping from the
build root (`zip -r family.gdz family.ged photos/`) preserves the exact
folder hierarchy relative to the GEDCOM. This guarantees that any complex or
structured media folders (e.g., `photos/1990s/wedding.jpg`) match the exact
file references declared inside the GEDCOM file, avoiding broken images from
flattened zip scopes.

View File

@@ -2,86 +2,224 @@
## Problem
Topola Viewer renders an interactive graphical family tree centered on a selected individual, displaying connected nodes for ancestors, descendants, and spouses. However, as family structures expand or users zoom in for detail, directly related family members frequently flow off the visible screen boundaries, requiring tedious canvas manipulation to locate and select them. To improve navigation efficiency and provide clear relational context at a glance, the application needs a dedicated layout block in the side panel that consolidates and displays the focused person's parents, spouses, and children. Providing these primary relationships as direct clickable links ensures users can rapidly transition focus between immediate family members regardless of their current viewport position on the graphical canvas.
Topola Viewer renders an interactive graphical family tree centered on a
selected individual, displaying connected nodes for ancestors, descendants, and
spouses. However, as family structures expand or users zoom in for detail,
directly related family members frequently flow off the visible screen
boundaries, requiring tedious canvas manipulation to locate and select them. To
improve navigation efficiency and provide clear relational context at a glance,
the application needs a dedicated layout block in the side panel that
consolidates and displays the focused person's parents, spouses, and children.
Providing these primary relationships as direct clickable links ensures users
can rapidly transition focus between immediate family members regardless of
their current viewport position on the graphical canvas.
## Technical Plan
To introduce the immediate family view without disrupting existing features, the implementation introduces a focused visual component that acts as an intermediary between the raw genealogy data and the user interface.
To introduce the immediate family view without disrupting existing features, the
implementation introduces a focused visual component that acts as an
intermediary between the raw genealogy data and the user interface.
The system relies on three primary components working together:
1. **Genealogy Data Store**: The core repository containing all parsed individuals and family records. It provides lookup utilities to resolve cross-references between children and their parents or spouses.
2. **Immediate Family Component**: A dedicated new interface element embedded directly inside the side panel. It queries the data store for the currently selected person, extracts their primary set of parents, and groups all associated children under their respective spouses or partners.
3. **Navigation Module**: When a user clicks on any parent, spouse, or child link within the new block, this module updates the application's active web address with the chosen relative's identifier. This action instantly re-centers both the side panel and the primary visual tree onto the newly selected family member.
1. **Genealogy Data Store**: The core repository containing all parsed
individuals and family records. It provides lookup utilities to resolve
cross-references between children and their parents or spouses.
2. **Immediate Family Component**: A dedicated new interface element embedded
directly inside the side panel. It queries the data store for the currently
selected person, extracts their primary set of parents, and groups all
associated children under their respective spouses or partners.
3. **Navigation Module**: When a user clicks on any parent, spouse, or child
link within the new block, this module updates the application's active web
address with the chosen relative's identifier. This action instantly
re-centers both the side panel and the primary visual tree onto the newly
selected family member.
## Alternatives
During the initial design discussions, several alternative implementation strategies were evaluated and ultimately ruled out to ensure optimal user experience and maintainability:
During the initial design discussions, several alternative implementation
strategies were evaluated and ultimately ruled out to ensure optimal user
experience and maintainability:
### 1. Integrating Relationships into the Event Timeline
* **Concept**: Interweave parents and children directly into the chronologically sorted lifecycle event list (e.g., rendering children as "Child Born" timeline events and placing parents near the target person's birth event).
* **Reason for Rejection**: Structural relationships are fundamentally distinct from point-in-time events. Forcing them into an event-driven layout scatters family members across the full vertical height of the panel, creating severe timeline clutter and undermining the core goal of rapid navigation.
- **Concept**: Interweave parents and children directly into the chronologically
sorted lifecycle event list (e.g., rendering children as "Child Born" timeline
events and placing parents near the target person's birth event).
- **Reason for Rejection**: Structural relationships are fundamentally distinct
from point-in-time events. Forcing them into an event-driven layout scatters
family members across the full vertical height of the panel, creating severe
timeline clutter and undermining the core goal of rapid navigation.
### 2. Removing the Timeline Spouse Link
* **Concept**: Strip out the standalone spouse navigation link from the chronological marriage event entries to prevent duplicating the link in both the timeline and the new Immediate Family block.
* **Reason for Rejection**: Detaching the spouse link from the timeline breaks historical context, separating the marriage ceremony record from the actual partner involved. Maintaining both preserves event completeness while establishing a centralized navigation block.
- **Concept**: Strip out the standalone spouse navigation link from the
chronological marriage event entries to prevent duplicating the link in both
the timeline and the new Immediate Family block.
- **Reason for Rejection**: Detaching the spouse link from the timeline breaks
historical context, separating the marriage ceremony record from the actual
partner involved. Maintaining both preserves event completeness while
establishing a centralized navigation block.
### 3. Rendering a Flat List of Children
* **Concept**: Consolidate all children into a single flat list ordered strictly by birth date, omitting spousal boundaries.
* **Reason for Rejection**: Fails to represent blended families clearly. Explicitly grouping children under their respective spouse or partner headers makes full-sibling versus half-sibling structures immediately obvious to the user.
- **Concept**: Consolidate all children into a single flat list ordered strictly
by birth date, omitting spousal boundaries.
- **Reason for Rejection**: Fails to represent blended families clearly.
Explicitly grouping children under their respective spouse or partner headers
makes full-sibling versus half-sibling structures immediately obvious to the
user.
### 4. Uniformly Showing or Hiding Unknown Spouses
* **Concept**: Apply a blanket rule to either always display an "Unknown Spouse" header for single-parent records or entirely hide unknown headers across all scenarios.
* **Reason for Rejection**: Always showing the header adds unnecessary visual noise to standard single-family layouts. Always hiding it obscures complex relationship boundaries when an individual has children across multiple partners where some partner names are unrecorded. The conditional approach dynamically cleans up single-family displays while preserving vital structural boundaries for multi-partner families.
- **Concept**: Apply a blanket rule to either always display an "Unknown Spouse"
header for single-parent records or entirely hide unknown headers across all
scenarios.
- **Reason for Rejection**: Always showing the header adds unnecessary visual
noise to standard single-family layouts. Always hiding it obscures complex
relationship boundaries when an individual has children across multiple
partners where some partner names are unrecorded. The conditional approach
dynamically cleans up single-family displays while preserving vital structural
boundaries for multi-partner families.
### 5. Supporting Multiple Sets of Parents Immediately
* **Concept**: Query all associated `FAMC` records to display biological, adoptive, and foster parent sets side-by-side from day one.
* **Reason for Rejection**: Introduces substantial UI complexity for labeling pedigree types (`PEDI` sub-tags) and requires wider structural refactoring of active focus state handling across the core canvas layout. Deferring to a single primary parent set keeps the initial feature scope robust and achievable.
- **Concept**: Query all associated `FAMC` records to display biological,
adoptive, and foster parent sets side-by-side from day one.
- **Reason for Rejection**: Introduces substantial UI complexity for labeling
pedigree types (`PEDI` sub-tags) and requires wider structural refactoring of
active focus state handling across the core canvas layout. Deferring to a
single primary parent set keeps the initial feature scope robust and
achievable.
## Detailed Implementation
Executing this feature requires clean extensions across the side panel architecture. To maintain high code quality and ensure strict boundaries, the implementation avoids modifying shared core state or event logic directly, instead isolating rendering responsibilities into modular layers.
Executing this feature requires clean extensions across the side panel
architecture. To maintain high code quality and ensure strict boundaries, the
implementation avoids modifying shared core state or event logic directly,
instead isolating rendering responsibilities into modular layers.
Below is the exhaustive file-by-file breakdown enumerating every file that will be created or modified, accompanied by detailed implementation step guidelines and specific technical rationale.
Below is the exhaustive file-by-file breakdown enumerating every file that will
be created or modified, accompanied by detailed implementation step guidelines
and specific technical rationale.
### 1. Component Creation
#### [NEW] [immediate-family.tsx](../src/sidepanel/details/immediate-family.tsx)
* **Rationale**: Encapsulating the new feature inside a dedicated component file separates concerns, avoids bloating the orchestration component ([details.tsx](../src/sidepanel/details/details.tsx)), and centralizes specialized rendering logic for single parent sets, childless spouses, conditional unknown headers, and chronologically sorted child groupings.
* **Implementation Steps**:
1. **Imports**: Import React elements, Semantic UI layout wrappers (`Item`, `Header`), navigation routing dependencies (`Link`, `useLocation` from `react-router`, `query-string`), localization wrappers (`FormattedMessage`, `useIntl` from `react-intl`), and GEDCOM utility functions from `../../util/gedcom_util` ([dereference](../src/util/gedcom_util.ts#L171-L183), [getName](../src/util/gedcom_util.ts#L292-L302), [pointerToId](../src/util/gedcom_util.ts#L42-L44), [resolveDate](../src/util/gedcom_util.ts#L339-L342)).
2. **Relative Link Helper**: Define an internal component (e.g., `RelativeLink`) that renders an individual relative profile as a `<Link>`. It extracts the target relative's plain ID directly from the raw sub-entry string via `pointerToId(subEntry.data)` before dereferencing to prevent runtime crashes on broken references. It parses `location.search` using `useLocation()`, sets the `indi` query parameter to the target ID, stringifies the search parameters, routes to `/view`, and outputs the resolved display name via `getName` (falling back to localized unknown string if undefined).
3. **Parents Block Renderer**: Implement a rendering method that extracts the active individual's tree array. Find the first sub-entry where `tag === 'FAMC'`. Guard against `undefined` if parents are unrecorded. Safely dereference this pointer against the repository's family mapping array to locate the parental family record. Scan the resolved family entry for all `HUSB` and `WIFE` sub-entries. Extract plain IDs via `pointerToId(subEntry.data)` and dereference each against the repository's individual mapping array to resolve profile records. Conditionally output localized sub-headers (e.g., "Father", "Mother") accompanied by clickable `RelativeLink` instances only for present parent records.
4. **Spouses and Children Block Renderer**: Implement a rendering method that iterates over all `FAMS` tags within the active individual's tree. Dereference each entry to fetch its corresponding family record. For each family record, extract the partner/spouse pointers (`HUSB` or `WIFE` tags ensuring `!subEntry.data.includes(props.indi)` to prevent extracting the focused person as their own spouse) and extract all child records (`CHIL`).
5. **Child Record Dereferencing and Sorting**: Explicitly dereference each extracted `CHIL` sub-entry against `gedcom.indis` to access child profile records. Extract their birth dates using `resolveDate` and sort the children array chronologically by birth date before outputting to ensure correct relational flow.
6. **Conditional Unknown Spouse Logic**: Prior to outputting headers, evaluate the total length of the mapped `FAMS` array. Guard against incomplete/empty family entries by suppressing groups containing neither a valid spouse pointer nor children. If a spouse profile pointer is absent and total `FAMS` equals `1`, suppress the spouse header output. If a spouse pointer is absent and total valid `FAMS` groups exceed `1`, output a visible "Spouse: Unknown" block header to clearly define half-sibling boundaries.
7. **Sequential Group Output**: Output each valid spousal family group as an encapsulated sub-block. Render childless spouses as complete standalone items. Output the resolved, sorted children collection as relative links below their respective spouse/partner header.
8. **Container Wrapper**: Export the primary `ImmediateFamily` component wrapper. If active parent, spouse, or child nodes exist, wrap the consolidated layout blocks inside a single Semantic UI `<Item>` container structured with `<Item.Content>`. If no immediate family members exist, return `null` to prevent empty dividers in the side panel DOM.
- **Rationale**: Encapsulating the new feature inside a dedicated component file
separates concerns, avoids bloating the orchestration component
([details.tsx](../src/sidepanel/details/details.tsx)), and centralizes
specialized rendering logic for single parent sets, childless spouses,
conditional unknown headers, and chronologically sorted child groupings.
- **Implementation Steps**:
1. **Imports**: Import React elements, Semantic UI layout wrappers (`Item`,
`Header`), navigation routing dependencies (`Link`, `useLocation` from
`react-router`, `query-string`), localization wrappers (`FormattedMessage`,
`useIntl` from `react-intl`), and GEDCOM utility functions from
`../../util/gedcom_util`
([dereference](../src/util/gedcom_util.ts#L171-L183),
[getName](../src/util/gedcom_util.ts#L292-L302),
[pointerToId](../src/util/gedcom_util.ts#L42-L44),
[resolveDate](../src/util/gedcom_util.ts#L339-L342)).
2. **Relative Link Helper**: Define an internal component (e.g.,
`RelativeLink`) that renders an individual relative profile as a `<Link>`.
It extracts the target relative's plain ID directly from the raw sub-entry
string via `pointerToId(subEntry.data)` before dereferencing to prevent
runtime crashes on broken references. It parses `location.search` using
`useLocation()`, sets the `indi` query parameter to the target ID,
stringifies the search parameters, routes to `/view`, and outputs the
resolved display name via `getName` (falling back to localized unknown
string if undefined).
3. **Parents Block Renderer**: Implement a rendering method that extracts the
active individual's tree array. Find the first sub-entry where
`tag === 'FAMC'`. Guard against `undefined` if parents are unrecorded.
Safely dereference this pointer against the repository's family mapping
array to locate the parental family record. Scan the resolved family entry
for all `HUSB` and `WIFE` sub-entries. Extract plain IDs via
`pointerToId(subEntry.data)` and dereference each against the repository's
individual mapping array to resolve profile records. Conditionally output
localized sub-headers (e.g., "Father", "Mother") accompanied by clickable
`RelativeLink` instances only for present parent records.
4. **Spouses and Children Block Renderer**: Implement a rendering method that
iterates over all `FAMS` tags within the active individual's tree.
Dereference each entry to fetch its corresponding family record. For each
family record, extract the partner/spouse pointers (`HUSB` or `WIFE` tags
ensuring `!subEntry.data.includes(props.indi)` to prevent extracting the
focused person as their own spouse) and extract all child records (`CHIL`).
5. **Child Record Dereferencing and Sorting**: Explicitly dereference each
extracted `CHIL` sub-entry against `gedcom.indis` to access child profile
records. Extract their birth dates using `resolveDate` and sort the
children array chronologically by birth date before outputting to ensure
correct relational flow.
6. **Conditional Unknown Spouse Logic**: Prior to outputting headers, evaluate
the total length of the mapped `FAMS` array. Guard against incomplete/empty
family entries by suppressing groups containing neither a valid spouse
pointer nor children. If a spouse profile pointer is absent and total
`FAMS` equals `1`, suppress the spouse header output. If a spouse pointer
is absent and total valid `FAMS` groups exceed `1`, output a visible
"Spouse: Unknown" block header to clearly define half-sibling boundaries.
7. **Sequential Group Output**: Output each valid spousal family group as an
encapsulated sub-block. Render childless spouses as complete standalone
items. Output the resolved, sorted children collection as relative links
below their respective spouse/partner header.
8. **Container Wrapper**: Export the primary `ImmediateFamily` component
wrapper. If active parent, spouse, or child nodes exist, wrap the
consolidated layout blocks inside a single Semantic UI `<Item>` container
structured with `<Item.Content>`. If no immediate family members exist,
return `null` to prevent empty dividers in the side panel DOM.
### 2. Orchestration Layer Modification
#### [MODIFY] [details.tsx](../src/sidepanel/details/details.tsx)
* **Rationale**: Acts as the root layout orchestrator for the side panel info tab. Needs to import and render the new view block near the top of the component flow to guarantee primary relational links are visible above the fold without requiring users to scroll past massive event timelines.
* **Implementation Steps**:
- **Rationale**: Acts as the root layout orchestrator for the side panel info
tab. Needs to import and render the new view block near the top of the
component flow to guarantee primary relational links are visible above the
fold without requiring users to scroll past massive event timelines.
- **Implementation Steps**:
1. Import the `ImmediateFamily` component from `./immediate-family`.
2. Locate the main [Details](../src/sidepanel/details/details.tsx#L338-L387) component export function.
3. Identify the execution block rendering `{getSectionForEachMatchingEntry(entries, props.gedcom, ['OBJE'], imageDetails)}` (line 350-355).
4. Directly below this block, and strictly *before* the `<Events>` timeline rendering execution line, embed `<ImmediateFamily gedcom={props.gedcom} indi={props.indi} />` directly within the `<Item.Group divided>` flow. Rely on the component's internal conditional logic to render the outer item bounds.
2. Locate the main [Details](../src/sidepanel/details/details.tsx#L338-L387)
component export function.
3. Identify the execution block rendering
`{getSectionForEachMatchingEntry(entries, props.gedcom, ['OBJE'], imageDetails)}`
(line 350-355).
4. Directly below this block, and strictly _before_ the `<Events>` timeline
rendering execution line, embed
`<ImmediateFamily gedcom={props.gedcom} indi={props.indi} />` directly
within the `<Item.Group divided>` flow. Rely on the component's internal
conditional logic to render the outer item bounds.
### 3. Registry and Documentation Updates
#### [MODIFY] [README.md](../src/sidepanel/details/README.md)
* **Rationale**: Functions as the official index cataloging all available component views within the side panel details sub-package. Documenting the new file preserves architectural readability.
* **Implementation Steps**:
- **Rationale**: Functions as the official index cataloging all available
component views within the side panel details sub-package. Documenting the new
file preserves architectural readability.
- **Implementation Steps**:
1. Locate the alphabetical files registry list.
2. Insert a descriptive entry for `immediate-family.tsx`, explaining its role as a dedicated side panel module for grouping and displaying parents, spouses, and children as rapid-navigation links.
2. Insert a descriptive entry for `immediate-family.tsx`, explaining its role
as a dedicated side panel module for grouping and displaying parents,
spouses, and children as rapid-navigation links.
### 4. Localization Contracts
#### [MODIFY] Translation JSON Files (`src/translations/*.json`)
* **Rationale**: Guarantees that every new UI header label introduced by the feature supports multi-language localization cleanly across all supported international environments.
* **Implementation Steps**:
1. Establish explicit string descriptor keys to be mapped across all JSON locale files (e.g., `src/translations/en.json`, `de.json`, `pl.json`, `fr.json`, `it.json`, etc.) to avoid production fallback leakage:
* `"family.immediate_family": "Immediate Family"`
* `"family.parents": "Parents"`
* `"family.father": "Father"`
* `"family.mother": "Mother"`
* `"family.spouse": "Spouse"`
* `"family.unknown_spouse": "Unknown Spouse"`
* `"family.children": "Children"`
2. Ensure corresponding `<FormattedMessage>` invocations inside `immediate-family.tsx` reference these specific string descriptor keys.
- **Rationale**: Guarantees that every new UI header label introduced by the
feature supports multi-language localization cleanly across all supported
international environments.
- **Implementation Steps**:
1. Establish explicit string descriptor keys to be mapped across all JSON
locale files (e.g., `src/translations/en.json`, `de.json`, `pl.json`,
`fr.json`, `it.json`, etc.) to avoid production fallback leakage:
- `"family.immediate_family": "Immediate Family"`
- `"family.parents": "Parents"`
- `"family.father": "Father"`
- `"family.mother": "Mother"`
- `"family.spouse": "Spouse"`
- `"family.unknown_spouse": "Unknown Spouse"`
- `"family.children": "Children"`
2. Ensure corresponding `<FormattedMessage>` invocations inside
`immediate-family.tsx` reference these specific string descriptor keys.

View File

@@ -1,273 +1,530 @@
# Load from Google Drive
## Problem Statement
Currently, Topola Viewer operates primarily on local file uploads, external HTTP URLs, or integrations with APIs like WikiTree. However, users who maintain their genealogy files (such as `.ged` and `.gdz` files) on cloud storage platforms like Google Drive face significant friction when trying to view, share, or collaborate on their family trees. The lack of integrated cloud storage support makes sharing interactive trees between collaborators cumbersome, requiring them to manually download and re-upload files. Integrating Google Drive directly into Topola Viewer as a secure, read-only storage provider will allow users to seamlessly load their trees from the cloud and easily collaborate using direct shared links.
Currently, Topola Viewer operates primarily on local file uploads, external HTTP
URLs, or integrations with APIs like WikiTree. However, users who maintain their
genealogy files (such as `.ged` and `.gdz` files) on cloud storage platforms
like Google Drive face significant friction when trying to view, share, or
collaborate on their family trees. The lack of integrated cloud storage support
makes sharing interactive trees between collaborators cumbersome, requiring them
to manually download and re-upload files. Integrating Google Drive directly into
Topola Viewer as a secure, read-only storage provider will allow users to
seamlessly load their trees from the cloud and easily collaborate using direct
shared links.
## The Technical Plan
To support Google Drive files, the application needs to orchestrate authentication, file selection, and download. The integration consists of five main components working together:
To support Google Drive files, the application needs to orchestrate
authentication, file selection, and download. The integration consists of five
main components working together:
### Major Components
1. **Google Drive Service**: This is a helper component that manages the connection to Google's APIs. It loads the Google libraries, triggers the login popup, caches the authorization token, and opens the Google Picker file selector.
2. **Google Drive Data Source**: A data loader that is registered in Topola's data source system. When the app is asked to load a file ID, this component uses the active authorization token to download the raw file content directly from Google Drive's secure servers.
3. **App Router / Shell**: The main orchestrator of the application. It parses parameters from the URL (like `fileId` and `source=google-drive`) and initiates the loading process. If the file download fails due to access issues, it manages the state of the fallback dialog.
4. **Access Authorization Modal**: A fallback dialog that displays when a user clicks a shared link to a Google Drive file that they do not yet have permission to view. It guides them to authenticate and select the target file using the Google Picker, which dynamically grants the app permission to access that file.
5. **Google Drive Menu**: A button added to Topola Viewer's menu bar that allows users to connect their Google account and browse their Drive files.
1. **Google Drive Service**: This is a helper component that manages the
connection to Google's APIs. It loads the Google libraries, triggers the
login popup, caches the authorization token, and opens the Google Picker file
selector.
2. **Google Drive Data Source**: A data loader that is registered in Topola's
data source system. When the app is asked to load a file ID, this component
uses the active authorization token to download the raw file content directly
from Google Drive's secure servers.
3. **App Router / Shell**: The main orchestrator of the application. It parses
parameters from the URL (like `fileId` and `source=google-drive`) and
initiates the loading process. If the file download fails due to access
issues, it manages the state of the fallback dialog.
4. **Access Authorization Modal**: A fallback dialog that displays when a user
clicks a shared link to a Google Drive file that they do not yet have
permission to view. It guides them to authenticate and select the target file
using the Google Picker, which dynamically grants the app permission to
access that file.
5. **Google Drive Menu**: A button added to Topola Viewer's menu bar that allows
users to connect their Google account and browse their Drive files.
## Alternatives Considered and Rejected
During the design phase, several alternative approaches were considered but rejected due to security, privacy, or complexity concerns:
During the design phase, several alternative approaches were considered but
rejected due to security, privacy, or complexity concerns:
### 1. Using the Broad `drive.readonly` Scope
* **Alternative**: Requesting permission to read any file in the user's Google Drive so that shared links could be opened immediately without additional user interaction.
* **Why Rejected**: Google classifies `drive.readonly` as a restricted scope. Publishing an app using this scope requires an annual third-party security assessment (CASA audit) which is prohibitively expensive for an open-source, non-profit project. Using the `drive.file` scope keeps the application within the non-sensitive tier while protecting user privacy by only granting access to files the user explicitly selects.
- **Alternative**: Requesting permission to read any file in the user's Google
Drive so that shared links could be opened immediately without additional user
interaction.
- **Why Rejected**: Google classifies `drive.readonly` as a restricted scope.
Publishing an app using this scope requires an annual third-party security
assessment (CASA audit) which is prohibitively expensive for an open-source,
non-profit project. Using the `drive.file` scope keeps the application within
the non-sensitive tier while protecting user privacy by only granting access
to files the user explicitly selects.
### 2. Anonymous Downloads for Publicly Shared Files
* **Alternative**: Attempting to download files shared with "anyone with the link" directly using standard public URLs without requiring a Google sign-in.
* **Why Rejected**: Public Google Drive URLs do not reliably support browser CORS headers for anonymous requests, and larger files trigger virus-warning pages that return HTML rather than raw file contents. Requiring a Google login for all Google Drive operations allows the app to fetch via the official API with an authorization token, which avoids both CORS and virus-scan issues and simplifies the code.
- **Alternative**: Attempting to download files shared with "anyone with the
link" directly using standard public URLs without requiring a Google sign-in.
- **Why Rejected**: Public Google Drive URLs do not reliably support browser
CORS headers for anonymous requests, and larger files trigger virus-warning
pages that return HTML rather than raw file contents. Requiring a Google login
for all Google Drive operations allows the app to fetch via the official API
with an authorization token, which avoids both CORS and virus-scan issues and
simplifies the code.
### 3. Routing Google Drive Traffic Through a CORS Proxy
* **Alternative**: Using the existing `topolaproxy.bieda.it` proxy to download files in order to bypass CORS restrictions.
* **Why Rejected**: Routing traffic through a third-party proxy poses a severe security risk because it would require transmitting the user's Google OAuth access token to an external service. Additionally, family tree files contain sensitive personal information, and routing them through an unauthenticated proxy violates privacy-first principles. Direct client-side requests to `googleapis.com` ensure all traffic remains securely encrypted between the user's browser and Google.
- **Alternative**: Using the existing `topolaproxy.bieda.it` proxy to download
files in order to bypass CORS restrictions.
- **Why Rejected**: Routing traffic through a third-party proxy poses a severe
security risk because it would require transmitting the user's Google OAuth
access token to an external service. Additionally, family tree files contain
sensitive personal information, and routing them through an unauthenticated
proxy violates privacy-first principles. Direct client-side requests to
`googleapis.com` ensure all traffic remains securely encrypted between the
user's browser and Google.
### 4. Implementing Write/Edit Support
* **Alternative**: Allowing users to modify their family trees and write back the changes directly to Google Drive.
* **Why Rejected**: Topola Viewer is architected as a read-only visualization tool. Implementing full write support would require developing a robust GEDCOM and GEDZIP serialization engine, state mutation synchronization, and conflict resolution mechanisms. Limiting this feature to read-only viewing aligns with the application's core purpose and avoids significant, high-risk complexity.
- **Alternative**: Allowing users to modify their family trees and write back
the changes directly to Google Drive.
- **Why Rejected**: Topola Viewer is architected as a read-only visualization
tool. Implementing full write support would require developing a robust GEDCOM
and GEDZIP serialization engine, state mutation synchronization, and conflict
resolution mechanisms. Limiting this feature to read-only viewing aligns with
the application's core purpose and avoids significant, high-risk complexity.
## Detailed Implementation Plan
This section outlines every file that will be created or modified to implement the Google Drive storage feature, along with the technical rationale for each change.
This section outlines every file that will be created or modified to implement
the Google Drive storage feature, along with the technical rationale for each
change.
---
### 1. Build and Dependencies Setup
#### [MODIFY] [package.json](../package.json)
* **Rationale**: Type safety is critical for global variables introduced by Google scripts.
* **Changes**: Add the following type definitions to `devDependencies`:
* `@types/gapi` for the legacy Google API client.
* `@types/google.picker` for the Google Picker API.
* `@types/google.accounts` for the modern Google Identity Services (GIS) token client.
*Note: In contrast to the initial design where GAPI and GIS scripts were loaded statically in `index.html`, the final implementation loads these scripts dynamically on demand using the custom `loadScript()` helper function in `google_drive_service.ts` to improve performance and only download third-party assets when the user interacts with Google Drive.*
- **Rationale**: Type safety is critical for global variables introduced by
Google scripts.
- **Changes**: Add the following type definitions to `devDependencies`:
- `@types/gapi` for the legacy Google API client.
- `@types/google.picker` for the Google Picker API.
- `@types/google.accounts` for the modern Google Identity Services (GIS) token
client.
_Note: In contrast to the initial design where GAPI and GIS scripts were loaded
statically in `index.html`, the final implementation loads these scripts
dynamically on demand using the custom `loadScript()` helper function in
`google_drive_service.ts` to improve performance and only download third-party
assets when the user interacts with Google Drive._
---
### 2. Data Source Layer
#### [MODIFY] [src/datasource/data_source.ts](../src/datasource/data_source.ts)
* **Rationale**: Topola Viewer abstracts data loading using the `DataSource` interface and `DataSourceEnum`. We must register the Google Drive type here.
* **Changes**: Add a `GOOGLE_DRIVE` entry to the `DataSourceEnum` export.
- **Rationale**: Topola Viewer abstracts data loading using the `DataSource`
interface and `DataSourceEnum`. We must register the Google Drive type here.
- **Changes**: Add a `GOOGLE_DRIVE` entry to the `DataSourceEnum` export.
#### [MODIFY] [src/datasource/load_data.ts](../src/datasource/load_data.ts)
* **Rationale**: Restructure file loading logic so that Google Drive datasource can parse file content and perform initial caching without code duplication.
* **Changes**: Export a new helper function, `loadAndPrepareFile(blob: Blob, cacheId: string): Promise<TopolaData>`, which calls `loadFile()` and `prepareData()` inside a try-catch block, handling `revokeObjectUrls()` in case of parsing errors.
- **Rationale**: Restructure file loading logic so that Google Drive datasource
can parse file content and perform initial caching without code duplication.
- **Changes**: Export a new helper function,
`loadAndPrepareFile(blob: Blob, cacheId: string): Promise<TopolaData>`, which
calls `loadFile()` and `prepareData()` inside a try-catch block, handling
`revokeObjectUrls()` in case of parsing errors.
#### [NEW] [src/datasource/google_drive.ts](../src/datasource/google_drive.ts)
* **Rationale**: Defines the logic for checking and loading Google Drive files.
* **Changes**:
* Define the interface `GoogleDriveSourceSpec` that contains the `fileId` and a `source` field mapped to `DataSourceEnum.GOOGLE_DRIVE`.
* Create `GoogleDriveAuthError` (extends `Error`) to signal authentication failures or access denial back to the App controller.
* Implement the `GoogleDriveDataSource` class matching the `DataSource` interface. The `loadData` function:
1. Checks `sessionStorage` under `google-drive:{fileId}` to see if the parsed file is already cached (avoiding network requests on page refresh).
2. If not cached, gets the active access token from `googleDriveService`. If missing or expired, throws `GoogleDriveAuthError`.
3. Executes a direct `fetch` to Google's REST endpoint (`https://www.googleapis.com/drive/v3/files/{fileId}?alt=media`) using the token.
4. Inspects response status: if 401, 403, or 404, throws `GoogleDriveAuthError`; if any other non-200 code, throws a standard `Error`.
5. Parses the downloaded blob using `loadFile` (reusing existing GEDCOM/GEDZIP zip parsing).
- **Rationale**: Defines the logic for checking and loading Google Drive files.
- **Changes**:
- Define the interface `GoogleDriveSourceSpec` that contains the `fileId` and
a `source` field mapped to `DataSourceEnum.GOOGLE_DRIVE`.
- Create `GoogleDriveAuthError` (extends `Error`) to signal authentication
failures or access denial back to the App controller.
- Implement the `GoogleDriveDataSource` class matching the `DataSource`
interface. The `loadData` function:
1. Checks `sessionStorage` under `google-drive:{fileId}` to see if the
parsed file is already cached (avoiding network requests on page
refresh).
2. If not cached, gets the active access token from `googleDriveService`. If
missing or expired, throws `GoogleDriveAuthError`.
3. Executes a direct `fetch` to Google's REST endpoint
(`https://www.googleapis.com/drive/v3/files/{fileId}?alt=media`) using
the token.
4. Inspects response status: if 401, 403, or 404, throws
`GoogleDriveAuthError`; if any other non-200 code, throws a standard
`Error`.
5. Parses the downloaded blob using `loadFile` (reusing existing
GEDCOM/GEDZIP zip parsing).
#### [NEW] [src/datasource/google_drive_service.ts](../src/datasource/google_drive_service.ts)
* **Rationale**: Keeps all interactions with Google Identity Services and GAPI initialization isolated, protecting the core app from dependency leakage.
* **Changes**:
* Create a singleton `googleDriveService` class.
* Implement an initialization method `init()` that dynamically loads GAPI and GIS scripts using a helper function `loadScript(src)`. It then loads GAPI's `picker` library via `gapi.load('picker', ...)`. Access `window.gapi` and `window.google` using direct global references or `(window as any)` casting to prevent TypeScript compilation errors.
* Handle OAuth initialization via `google.accounts.oauth2.initTokenClient` and store the access token in memory. Cache the token and its computed absolute expiration timestamp (`Date.now() + expires_in * 1000`) in `sessionStorage` to survive page refreshes in the same tab.
* **Asynchronous Initialization**: Store the initialization Promise as `this.initPromise` (which resolves when global scripts are loaded and GAPI/GIS are initialized). All public service methods must await `this.initPromise` before executing to prevent race conditions and runtime errors.
* **Promise-based OAuth Request**: Since `initTokenClient` accepts a single static callback, `requestToken` should store the current Promise's `resolve` and `reject` handlers on the service instance (`this.pendingAuthResolve` and `this.pendingAuthReject`) and invoke them inside the GIS callback.
* Provide helper methods:
* `getAccessToken()`: Returns the cached token if it has not expired (checked against the absolute expiration timestamp stored in `sessionStorage`).
* `requestToken(forceAccountSelect)`: Returns a Promise that resolves when the Google Identity Services popup completes auth. If `forceAccountSelect` is true, sets the GIS configuration `prompt` parameter to `'select_account'` (critical for switching accounts on 403 errors).
* `showPicker(onPicked, onCancel)`: Calculates display dimensions based on the window size (`Math.min` bounds) to ensure responsiveness on mobile, then builds and opens the Google Picker.
* Set the developer API key via `.setDeveloperKey(import.meta.env.VITE_GOOGLE_API_KEY)` on the `PickerBuilder`.
* Configure `DocsView` using `google.picker.ViewId.DOCS` and restrict it to files matching `.setMimeTypes('application/x-gedcom,text/vnd.familysearch.gedcom,application/x-zip')` to cover `.ged` and `.gdz` formats.
* Set the origin using `.setOrigin(window.location.origin)` to prevent cross-origin issues between the Picker iframe and the viewer page.
* In the Picker callback, check for `data.action === google.picker.Action.PICKED` and retrieve the file ID via `data[google.picker.Response.DOCUMENTS][0][google.picker.Document.ID]`, triggering `onPicked`.
* If `data.action === google.picker.Action.CANCEL`, trigger the `onCancel` callback (if provided) to handle dialog closing gracefully.
* `signOut()`: Revokes the token using `google.accounts.oauth2.revoke`, clears memory and `sessionStorage` tokens, and resets local state.
- **Rationale**: Keeps all interactions with Google Identity Services and GAPI
initialization isolated, protecting the core app from dependency leakage.
- **Changes**:
- Create a singleton `googleDriveService` class.
- Implement an initialization method `init()` that dynamically loads GAPI and
GIS scripts using a helper function `loadScript(src)`. It then loads GAPI's
`picker` library via `gapi.load('picker', ...)`. Access `window.gapi` and
`window.google` using direct global references or `(window as any)` casting
to prevent TypeScript compilation errors.
- Handle OAuth initialization via `google.accounts.oauth2.initTokenClient` and
store the access token in memory. Cache the token and its computed absolute
expiration timestamp (`Date.now() + expires_in * 1000`) in `sessionStorage`
to survive page refreshes in the same tab.
- **Asynchronous Initialization**: Store the initialization Promise as
`this.initPromise` (which resolves when global scripts are loaded and
GAPI/GIS are initialized). All public service methods must await
`this.initPromise` before executing to prevent race conditions and runtime
errors.
- **Promise-based OAuth Request**: Since `initTokenClient` accepts a single
static callback, `requestToken` should store the current Promise's `resolve`
and `reject` handlers on the service instance (`this.pendingAuthResolve` and
`this.pendingAuthReject`) and invoke them inside the GIS callback.
- Provide helper methods:
- `getAccessToken()`: Returns the cached token if it has not expired
(checked against the absolute expiration timestamp stored in
`sessionStorage`).
- `requestToken(forceAccountSelect)`: Returns a Promise that resolves when
the Google Identity Services popup completes auth. If `forceAccountSelect`
is true, sets the GIS configuration `prompt` parameter to
`'select_account'` (critical for switching accounts on 403 errors).
- `showPicker(onPicked, onCancel)`: Calculates display dimensions based on
the window size (`Math.min` bounds) to ensure responsiveness on mobile,
then builds and opens the Google Picker.
- Set the developer API key via
`.setDeveloperKey(import.meta.env.VITE_GOOGLE_API_KEY)` on the
`PickerBuilder`.
- Configure `DocsView` using `google.picker.ViewId.DOCS` and restrict it
to files matching
`.setMimeTypes('application/x-gedcom,text/vnd.familysearch.gedcom,application/x-zip')`
to cover `.ged` and `.gdz` formats.
- Set the origin using `.setOrigin(window.location.origin)` to prevent
cross-origin issues between the Picker iframe and the viewer page.
- In the Picker callback, check for
`data.action === google.picker.Action.PICKED` and retrieve the file ID
via
`data[google.picker.Response.DOCUMENTS][0][google.picker.Document.ID]`,
triggering `onPicked`.
- If `data.action === google.picker.Action.CANCEL`, trigger the `onCancel`
callback (if provided) to handle dialog closing gracefully.
- `signOut()`: Revokes the token using `google.accounts.oauth2.revoke`,
clears memory and `sessionStorage` tokens, and resets local state.
#### [NEW] [src/datasource/test_helpers.ts](../src/datasource/test_helpers.ts)
* **Rationale**: Provides a mock `sessionStorage` utility for test environments.
* **Changes**:
* Define `mockSessionStorage()` which mocks `global.sessionStorage` with a key-value dictionary and returns it for inspection.
- **Rationale**: Provides a mock `sessionStorage` utility for test environments.
- **Changes**:
- Define `mockSessionStorage()` which mocks `global.sessionStorage` with a
key-value dictionary and returns it for inspection.
#### [NEW] [tests/import_meta_transformer.js](../tests/import_meta_transformer.js)
* **Rationale**: Translates Vite's `import.meta.env` to `process.env` so that tests running in Jest (a Node environment) can correctly access configuration properties.
- **Rationale**: Translates Vite's `import.meta.env` to `process.env` so that
tests running in Jest (a Node environment) can correctly access configuration
properties.
#### [MODIFY] [jest.config.ts](../jest.config.ts)
* **Rationale**: Registers the custom `import_meta_transformer.js` for TS and TSX files.
- **Rationale**: Registers the custom `import_meta_transformer.js` for TS and
TSX files.
#### [NEW] [src/datasource/google_drive.spec.ts](../src/datasource/google_drive.spec.ts)
* **Rationale**: Unit testing for the `GoogleDriveDataSource` class, verifying that the data source is instantiated, fetches, and parses data correctly, handles cached items, and throws authentication errors when expected.
- **Rationale**: Unit testing for the `GoogleDriveDataSource` class, verifying
that the data source is instantiated, fetches, and parses data correctly,
handles cached items, and throws authentication errors when expected.
#### [NEW] [src/datasource/google_drive_service.spec.ts](../src/datasource/google_drive_service.spec.ts)
* **Rationale**: Unit testing for the `GoogleDriveService` class including script loading, token caching, account selection, token revocation, and cache clearing.
- **Rationale**: Unit testing for the `GoogleDriveService` class including
script loading, token caching, account selection, token revocation, and cache
clearing.
---
### 3. User Interface Integration
#### [NEW] [src/menu/google_drive_menu.tsx](../src/menu/google_drive_menu.tsx)
* **Rationale**: Provides the user entry point in the navigation bar to open files.
* **Changes**:
* Implement `GoogleDriveMenu` using the `MenuItem` abstraction.
* When clicked, it requests an OAuth access token via `googleDriveService`. If successful, it launches the Google Picker.
* When a file is selected, it extracts the ID and navigates to `/view` with search query params set to `source=google-drive&fileId={id}`.
- **Rationale**: Provides the user entry point in the navigation bar to open
files.
- **Changes**:
- Implement `GoogleDriveMenu` using the `MenuItem` abstraction.
- When clicked, it requests an OAuth access token via `googleDriveService`. If
successful, it launches the Google Picker.
- When a file is selected, it extracts the ID and navigates to `/view` with
search query params set to `source=google-drive&fileId={id}`.
#### [MODIFY] [src/menu/top_bar.tsx](../src/menu/top_bar.tsx)
* **Rationale**: TopBar contains open actions for uploads, URL loading, and WikiTree. We must inject the Google Drive options here.
* **Changes**:
* Add props: `onGoogleSignOut?: () => void`, `hasGoogleToken: boolean`, and `onGoogleTokenAcquired?: () => void`.
* Check if `VITE_GOOGLE_CLIENT_ID` and `VITE_GOOGLE_API_KEY` exist in the build environment.
* If present, render `GoogleDriveMenu` alongside other menus in the desktop and mobile file selectors.
* Inject a **Google Drive Sign Out / Disconnect** button into the top bar in the same location where WikiTree login/logout options appear (on the right-aligned side of the menu for desktop, and list for mobile), rendering only if `hasGoogleToken` is true. Clicking it triggers a sign-out flow that:
- **Rationale**: TopBar contains open actions for uploads, URL loading, and
WikiTree. We must inject the Google Drive options here.
- **Changes**:
- Add props: `onGoogleSignOut?: () => void`, `hasGoogleToken: boolean`, and
`onGoogleTokenAcquired?: () => void`.
- Check if `VITE_GOOGLE_CLIENT_ID` and `VITE_GOOGLE_API_KEY` exist in the
build environment.
- If present, render `GoogleDriveMenu` alongside other menus in the desktop
and mobile file selectors.
- Inject a **Google Drive Sign Out / Disconnect** button into the top bar in
the same location where WikiTree login/logout options appear (on the
right-aligned side of the menu for desktop, and list for mobile), rendering
only if `hasGoogleToken` is true. Clicking it triggers a sign-out flow that:
1. Calls the `onGoogleSignOut` callback.
#### [NEW] [src/menu/google_auth_modal.tsx](../src/menu/google_auth_modal.tsx)
* **Rationale**: Essential for private file sharing support under `drive.file` and bypasses browser popup blockers.
* **Changes**:
* Define props interface:
* `failedFileId: string`: The ID of the file that failed to load.
* `onAuthSuccess: (fileId: string) => void`: Callback triggered when permission is successfully granted or a file is picked.
* `onCancel: () => void`: Callback triggered when the user cancels the modal.
* Create a modal view overlay displayed in the center of the screen.
* Prompt: "Google Drive Access Required".
* Provides a button to initiate connection. The modal implements a two-tier strategy tailored to `drive.file` constraints:
1. Clicking the button first runs a quick OAuth request (`googleDriveService.requestToken()`) and attempts to download the target file ID. Due to `drive.file` limitations, this direct request will fail (with 403 or 404) on the first try if the app has not been authorized for this file yet in the user's Google Drive.
2. In case of access failure (403/404), the modal UI will display detailed instructions guiding the user to select the file manually using the Google Picker. Selecting the file in the Picker explicitly grants the app permission to read it.
3. **Shared Link Constraint**: If the file was shared via "Anyone with the link can view", it may not appear in the user's Picker search or "Shared with me" list until they open the link once in Google Drive or add a shortcut to "My Drive". The modal should document this instruction to guide users opening shared links.
* **Popup Blocker Handling**: The OAuth popup is only triggered when the user explicitly clicks the "Connect" button (a direct user gesture). The modal must never attempt to open the OAuth popup automatically on mount.
* **Non-blocking Loader**: When the login flow is pending, show a spinner on the button itself or a cancelable loader. Do not show an un-cancelable full-screen loading overlay, as Google Identity Services does not notify the app if the user closes the sign-in popup. The connect button must not be permanently disabled while loading; it should allow the user to click it again to retry or provide a manual reset/timeout in case they closed the login popup.
* **Switch Account Button**: Provides a "Switch Account" button that allows users to authenticate with a different Google account directly from the modal dialog. In the implementation, this button is rendered conditionally once picker instructions are visible (after a failed direct download attempt).
* **Cancel Action**: Provide a "Cancel" button. If clicked, calls `props.onCancel()` which redirects the user back to the homepage `/` (Intro).
- **Rationale**: Essential for private file sharing support under `drive.file`
and bypasses browser popup blockers.
- **Changes**:
- Define props interface:
- `failedFileId: string`: The ID of the file that failed to load.
- `onAuthSuccess: (fileId: string) => void`: Callback triggered when
permission is successfully granted or a file is picked.
- `onCancel: () => void`: Callback triggered when the user cancels the
modal.
- Create a modal view overlay displayed in the center of the screen.
- Prompt: "Google Drive Access Required".
- Provides a button to initiate connection. The modal implements a two-tier
strategy tailored to `drive.file` constraints:
1. Clicking the button first runs a quick OAuth request
(`googleDriveService.requestToken()`) and attempts to download the target
file ID. Due to `drive.file` limitations, this direct request will fail
(with 403 or 404) on the first try if the app has not been authorized for
this file yet in the user's Google Drive.
2. In case of access failure (403/404), the modal UI will display detailed
instructions guiding the user to select the file manually using the
Google Picker. Selecting the file in the Picker explicitly grants the app
permission to read it.
3. **Shared Link Constraint**: If the file was shared via "Anyone with the
link can view", it may not appear in the user's Picker search or "Shared
with me" list until they open the link once in Google Drive or add a
shortcut to "My Drive". The modal should document this instruction to
guide users opening shared links.
- **Popup Blocker Handling**: The OAuth popup is only triggered when the user
explicitly clicks the "Connect" button (a direct user gesture). The modal
must never attempt to open the OAuth popup automatically on mount.
- **Non-blocking Loader**: When the login flow is pending, show a spinner on
the button itself or a cancelable loader. Do not show an un-cancelable
full-screen loading overlay, as Google Identity Services does not notify the
app if the user closes the sign-in popup. The connect button must not be
permanently disabled while loading; it should allow the user to click it
again to retry or provide a manual reset/timeout in case they closed the
login popup.
- **Switch Account Button**: Provides a "Switch Account" button that allows
users to authenticate with a different Google account directly from the
modal dialog. In the implementation, this button is rendered conditionally
once picker instructions are visible (after a failed direct download
attempt).
- **Cancel Action**: Provide a "Cancel" button. If clicked, calls
`props.onCancel()` which redirects the user back to the homepage `/`
(Intro).
---
### 4. Main App Orchestration
#### [MODIFY] [src/app.tsx](../src/app.tsx)
* **Rationale**: The central application container responsible for routing, state tracking, error displaying, and data orchestration.
* **Changes**:
* **Type & Registration**: Add `GoogleDriveSourceSpec` to the `DataSourceSpec` union. Instantiate `googleDriveDataSource` via `const googleDriveDataSource = new GoogleDriveDataSource();` and register it in both the `loadData()` and `isNewData()` switch statements in `app.tsx`.
* **Credentials Check & Error Handling**: During data loading, if `source=google-drive` but `VITE_GOOGLE_CLIENT_ID` or `VITE_GOOGLE_API_KEY` is missing from the environment, throw an error signaling that Google Drive integration is not configured.
* **"Open with" Query Parameter**: Add a root-level `useEffect` or route handler to inspect the `state` query parameter using React Router's `location.search` hook. If present:
1. Wrap the query parsing logic in a `try/catch` block to handle cases where the `state` parameter is not valid JSON (e.g. from other OAuth providers).
2. Check that the parsed JSON contains Google Drive specific keys (`action === 'open'` and a non-empty `ids` array) before triggering the redirect.
3. If valid, extract the first file ID (`ids[0]`) from the array and ignore any other IDs. Perform a client-side redirect (soft navigate) to `/view?source=google-drive&fileId={fileId}` using `{ replace: true }` so it does not pollute the browser history and break the back button.
4. Clear the `state` query parameter from the URL by performing the client-side redirect using `{ replace: true }`, which replaces the URL in the history and implicitly clears the `state` parameter to prevent infinite redirect loops on subsequent rendering and routing cycles.
* **Argument Parsing**: Update `getArguments()` to support `source=google-drive` and `fileId` query params, returning a `GoogleDriveSourceSpec`.
* **Google Auth State**: Add React state for showing the `<GoogleAuthModal />`, storing the `failedFileId`, and tracking `hasGoogleToken` (updated upon successful login or logout to ensure proper reactivity in `TopBar`).
* **Catching Auth Errors**: Update the main `useEffect` data loading sequence. If `loadData()` throws `GoogleDriveAuthError`, do *not* set `state` to `AppState.ERROR` (which shows a scary red error banner). Instead, keep a clean background (e.g. keep `AppState.LOADING` or transition to a non-error background) and set `showAuthModal` to true.
* **Resolving Auth Fallback (Avoiding Deadlock)**:
* If the user selects a *new* file in the Picker, perform a soft `navigate` to update the URL parameters, triggering `isNewData` and initiating a normal reload.
* If the user successfully authorizes and selects the *same* file ID (matching `failedFileId`), do *not* navigate (as the URL matches and would result in a no-op). Instead, explicitly reset `state` to `AppState.INITIAL`. This forces the `useEffect` to execute the load sequence again using the newly acquired access token.
* If the user successfully selects the target file in the auth modal's callback, trigger url updating to refresh the page and successfully render the chart.
* **Disconnect Callback**: Pass `onGoogleSignOut` to `TopBar` that:
- **Rationale**: The central application container responsible for routing,
state tracking, error displaying, and data orchestration.
- **Changes**:
- **Type & Registration**: Add `GoogleDriveSourceSpec` to the `DataSourceSpec`
union. Instantiate `googleDriveDataSource` via
`const googleDriveDataSource = new GoogleDriveDataSource();` and register it
in both the `loadData()` and `isNewData()` switch statements in `app.tsx`.
- **Credentials Check & Error Handling**: During data loading, if
`source=google-drive` but `VITE_GOOGLE_CLIENT_ID` or `VITE_GOOGLE_API_KEY`
is missing from the environment, throw an error signaling that Google Drive
integration is not configured.
- **"Open with" Query Parameter**: Add a root-level `useEffect` or route
handler to inspect the `state` query parameter using React Router's
`location.search` hook. If present:
1. Wrap the query parsing logic in a `try/catch` block to handle cases where
the `state` parameter is not valid JSON (e.g. from other OAuth
providers).
2. Check that the parsed JSON contains Google Drive specific keys
(`action === 'open'` and a non-empty `ids` array) before triggering the
redirect.
3. If valid, extract the first file ID (`ids[0]`) from the array and ignore
any other IDs. Perform a client-side redirect (soft navigate) to
`/view?source=google-drive&fileId={fileId}` using `{ replace: true }` so
it does not pollute the browser history and break the back button.
4. Clear the `state` query parameter from the URL by performing the
client-side redirect using `{ replace: true }`, which replaces the URL in
the history and implicitly clears the `state` parameter to prevent
infinite redirect loops on subsequent rendering and routing cycles.
- **Argument Parsing**: Update `getArguments()` to support
`source=google-drive` and `fileId` query params, returning a
`GoogleDriveSourceSpec`.
- **Google Auth State**: Add React state for showing the
`<GoogleAuthModal />`, storing the `failedFileId`, and tracking
`hasGoogleToken` (updated upon successful login or logout to ensure proper
reactivity in `TopBar`).
- **Catching Auth Errors**: Update the main `useEffect` data loading sequence.
If `loadData()` throws `GoogleDriveAuthError`, do _not_ set `state` to
`AppState.ERROR` (which shows a scary red error banner). Instead, keep a
clean background (e.g. keep `AppState.LOADING` or transition to a non-error
background) and set `showAuthModal` to true.
- **Resolving Auth Fallback (Avoiding Deadlock)**:
- If the user selects a _new_ file in the Picker, perform a soft `navigate`
to update the URL parameters, triggering `isNewData` and initiating a
normal reload.
- If the user successfully authorizes and selects the _same_ file ID
(matching `failedFileId`), do _not_ navigate (as the URL matches and would
result in a no-op). Instead, explicitly reset `state` to
`AppState.INITIAL`. This forces the `useEffect` to execute the load
sequence again using the newly acquired access token.
- If the user successfully selects the target file in the auth modal's
callback, trigger url updating to refresh the page and successfully render
the chart.
- **Disconnect Callback**: Pass `onGoogleSignOut` to `TopBar` that:
1. Revokes the token using `googleDriveService.signOut()`.
2. Clears the active `data` and `selection` state in `App` and revokes all media Object URLs.
3. Clears the active `sessionStorage` cache (all keys starting with `google-drive:`) to prevent unauthorized access by subsequent users on shared/public devices.
2. Clears the active `data` and `selection` state in `App` and revokes all
media Object URLs.
3. Clears the active `sessionStorage` cache (all keys starting with
`google-drive:`) to prevent unauthorized access by subsequent users on
shared/public devices.
4. Redirects the user back to the home route `/` (Intro).
---
### 5. Localization Integration
#### [MODIFY] [src/translations/*.json](../src/translations) and [src/app.tsx](../src/app.tsx)
* **Rationale**: Ensure all user-facing Google Drive integration UI strings are fully translated and localized.
* **Changes**: Add translation keys for the new UI elements across all 7 localization files (`bg.json`, `cs.json`, `de.json`, `fr.json`, `it.json`, `pl.json`, `ru.json`). For the default English catalog, declare messages using the `defaultMessage` prop in components or as arguments in `intl.formatMessage` calls. Use a `TopolaError` with code `'GOOGLE_DRIVE_NOT_CONFIGURED'` to propagate configuration errors so they can be parsed and translated by `getI18nMessage()`.
* `menu.load_from_google_drive`: "Load from Google Drive" (or language equivalent)
* `menu.google_sign_out`: "Disconnect Google Drive" / "Sign out"
* `google_auth.title`: "Google Drive Access Required"
* `google_auth.instructions`: "To view this file, you must authenticate and select the file from your Google Drive to grant permissions."
* `google_auth.grant_button`: "Grant Access & Select File"
* `google_auth.cancel`: "Cancel"
* `google_auth.picker_instructions_header`: "Permissions Required"
* `google_auth.picker_instructions`: "The application does not have permission to read this file. Please select it in the file browser popup to grant access. If this is a shared file that doesn't show up, try adding a shortcut to your Drive first."
* `google_auth.switch_account_button`: "Switch Account"
* `error.GOOGLE_DRIVE_NOT_CONFIGURED`: "Google Drive integration is not configured."
#### [MODIFY] [src/translations/\*.json](../src/translations) and [src/app.tsx](../src/app.tsx)
- **Rationale**: Ensure all user-facing Google Drive integration UI strings are
fully translated and localized.
- **Changes**: Add translation keys for the new UI elements across all 7
localization files (`bg.json`, `cs.json`, `de.json`, `fr.json`, `it.json`,
`pl.json`, `ru.json`). For the default English catalog, declare messages using
the `defaultMessage` prop in components or as arguments in
`intl.formatMessage` calls. Use a `TopolaError` with code
`'GOOGLE_DRIVE_NOT_CONFIGURED'` to propagate configuration errors so they can
be parsed and translated by `getI18nMessage()`.
- `menu.load_from_google_drive`: "Load from Google Drive" (or language
equivalent)
- `menu.google_sign_out`: "Disconnect Google Drive" / "Sign out"
- `google_auth.title`: "Google Drive Access Required"
- `google_auth.instructions`: "To view this file, you must authenticate and
select the file from your Google Drive to grant permissions."
- `google_auth.grant_button`: "Grant Access & Select File"
- `google_auth.cancel`: "Cancel"
- `google_auth.picker_instructions_header`: "Permissions Required"
- `google_auth.picker_instructions`: "The application does not have permission
to read this file. Please select it in the file browser popup to grant
access. If this is a shared file that doesn't show up, try adding a shortcut
to your Drive first."
- `google_auth.switch_account_button`: "Switch Account"
- `error.GOOGLE_DRIVE_NOT_CONFIGURED`: "Google Drive integration is not
configured."
---
## Google Cloud Platform Setup Guide
To support authentication and file picking, Topola Viewer must be connected to a Google Cloud Platform (GCP) project. Below are the step-by-step instructions to configure the GCP resources, credentials, and Workspace Marketplace listings.
To support authentication and file picking, Topola Viewer must be connected to a
Google Cloud Platform (GCP) project. Below are the step-by-step instructions to
configure the GCP resources, credentials, and Workspace Marketplace listings.
### Step 1: Create a Google Cloud Project
1. Go to the [Google Cloud Console](https://console.cloud.google.com/).
2. Click the project dropdown in the top navigation and select **New Project**.
3. Enter a name (e.g., `Topola Viewer`) and click **Create**.
### Step 2: Enable Required APIs
1. Navigate to **APIs & Services** $\rightarrow$ **Library**.
2. Search for and enable the following APIs:
* **Google Drive API** (for file downloading).
* **Google Picker API** (for the file browser popup).
* **Google Workspace Marketplace SDK** (required to enable the "Open with" context menu inside Google Drive).
- **Google Drive API** (for file downloading).
- **Google Picker API** (for the file browser popup).
- **Google Workspace Marketplace SDK** (required to enable the "Open with"
context menu inside Google Drive).
### Step 3: Configure the OAuth Consent Screen
1. Navigate to **APIs & Services** $\rightarrow$ **OAuth consent screen**.
2. Select **External** for User Type (so any Google user can log in) and click **Create**.
3. Complete the **App information** (App name, Support email, Developer contact information) and click **Save and Continue**.
2. Select **External** for User Type (so any Google user can log in) and click
**Create**.
3. Complete the **App information** (App name, Support email, Developer contact
information) and click **Save and Continue**.
4. In the **Scopes** step, click **Add or Remove Scopes**:
* Add the scope: `https://www.googleapis.com/auth/drive.file`
* This scope is classified as non-sensitive, meaning Topola Viewer does *not* require expensive security reviews or CASA audits to be verified by Google.
- Add the scope: `https://www.googleapis.com/auth/drive.file`
- This scope is classified as non-sensitive, meaning Topola Viewer does _not_
require expensive security reviews or CASA audits to be verified by Google.
5. Save and continue to finish the configuration.
### Step 4: Create OAuth 2.0 Credentials
1. Navigate to **APIs & Services** $\rightarrow$ **Credentials**.
2. Click **Create Credentials** $\rightarrow$ **OAuth client ID**.
3. Set the **Application type** to **Web application**.
4. Under **Authorized JavaScript origins**, add the domains where the app is deployed, as well as local environments:
* `https://pewu.github.io` (Production)
* `https://apps.wikitree.com` (Wikitree Integration)
* `http://localhost:3000` (Local testing and development)
> [!IMPORTANT]
> Google's OAuth validation is strict. In local development, ensure you access the application via `http://localhost:3000` rather than `http://127.0.0.1:3000`, as using the raw IP address will result in origin authorization failures unless `http://127.0.0.1:3000` is also explicitly added to this list.
5. Click **Create**. Copy the generated **Client ID** to use in the app environment variables (`VITE_GOOGLE_CLIENT_ID`).
4. Under **Authorized JavaScript origins**, add the domains where the app is
deployed, as well as local environments:
- `https://pewu.github.io` (Production)
- `https://apps.wikitree.com` (Wikitree Integration)
- `http://localhost:3000` (Local testing and development)
> [!IMPORTANT] Google's OAuth validation is strict. In local development,
> ensure you access the application via `http://localhost:3000` rather than
> `http://127.0.0.1:3000`, as using the raw IP address will result in
> origin authorization failures unless `http://127.0.0.1:3000` is also
> explicitly added to this list.
5. Click **Create**. Copy the generated **Client ID** to use in the app
environment variables (`VITE_GOOGLE_CLIENT_ID`).
### Step 5: Create an API Key (for Google Picker)
1. On the **Credentials** screen, click **Create Credentials** $\rightarrow$ **API key**.
1. On the **Credentials** screen, click **Create Credentials** $\rightarrow$
**API key**.
2. Edit the newly created API key to add restrictions:
* **Application restrictions**: Choose **HTTP referrers (web sites)**.
* Add the authorized referrers:
* `https://pewu.github.io/topola/viewer/*`
* `https://apps.wikitree.com/apps/wiech13/topola-viewer/*`
* `http://localhost:3000/*`
* **API restrictions**: Restrict the key to only allow requests to **both** the **Google Picker API** and the **Google Drive API** (the Picker component queries user files via the Drive API under the hood; restricting it strictly to the Picker API will cause requests to fail).
3. Save the key. Copy the generated **API Key** to use in the app environment variables (`VITE_GOOGLE_API_KEY`).
- **Application restrictions**: Choose **HTTP referrers (web sites)**.
- Add the authorized referrers:
- `https://pewu.github.io/topola/viewer/*`
- `https://apps.wikitree.com/apps/wiech13/topola-viewer/*`
- `http://localhost:3000/*`
- **API restrictions**: Restrict the key to only allow requests to **both**
the **Google Picker API** and the **Google Drive API** (the Picker
component queries user files via the Drive API under the hood; restricting
it strictly to the Picker API will cause requests to fail).
3. Save the key. Copy the generated **API Key** to use in the app environment
variables (`VITE_GOOGLE_API_KEY`).
### Step 6: Configure the Workspace Marketplace SDK ("Open with" Integration)
1. Navigate to **APIs & Services** $\rightarrow$ **Enabled APIs & Services**, and select the **Google Workspace Marketplace SDK**.
1. Navigate to **APIs & Services** $\rightarrow$ **Enabled APIs & Services**,
and select the **Google Workspace Marketplace SDK**.
2. Click **App Integration** $\rightarrow$ **Configuration**.
3. Enable the **Drive Extension** integration check.
4. Configure the **Open URL**:
* URL: `https://pewu.github.io/topola-viewer/` (or the corresponding deploy URL).
- URL: `https://pewu.github.io/topola-viewer/` (or the corresponding deploy
URL).
5. Set up **File Handlers**:
* Under **Default File Extensions**, register `.ged` and `.gdz`.
6. (Optional) Configure the **Store Listing** to publish the extension to the public Google Workspace Marketplace, making it searchable and installable for anyone.
- Under **Default File Extensions**, register `.ged` and `.gdz`.
6. (Optional) Configure the **Store Listing** to publish the extension to the
public Google Workspace Marketplace, making it searchable and installable for
anyone.
## Testing Strategy
Due to the dependency on third-party external Google APIs and credentials, testing this feature is divided into automated mocking (for CI environments and unit tests) and manual verification (for local development).
Due to the dependency on third-party external Google APIs and credentials,
testing this feature is divided into automated mocking (for CI environments and
unit tests) and manual verification (for local development).
### 1. Automated Unit Testing
All automated unit tests are run via **Jest**. Since Node environments do not load Google's external CDN scripts, we must isolate and mock these dependencies.
All automated unit tests are run via **Jest**. Since Node environments do not
load Google's external CDN scripts, we must isolate and mock these dependencies.
#### Mocking Global Scripts
Create a mock utility in the tests setup file to intercept calls to the global `google` and `gapi` interfaces:
Create a mock utility in the tests setup file to intercept calls to the global
`google` and `gapi` interfaces:
```typescript
global.gapi = {
load: jest.fn((api: string, callback: () => void) => callback()),
@@ -304,24 +561,37 @@ global.google = {
```
#### Data Source Tests
In `src/datasource/google_drive.spec.ts` [NEW]:
* **Verify `isNewData()`**: Confirm it returns true only when the `fileId` changes.
* **Verify `loadData()` Success**: Mock `window.fetch` to return a mock blob, mock `getAccessToken()` to return a dummy token, and verify the data source parses the file stream correctly.
* **Verify `loadData()` Auth Failure**: Mock `getAccessToken()` to return `null` or configure `window.fetch` to return `403 Forbidden`, and assert that `GoogleDriveAuthError` is thrown.
- **Verify `isNewData()`**: Confirm it returns true only when the `fileId`
changes.
- **Verify `loadData()` Success**: Mock `window.fetch` to return a mock blob,
mock `getAccessToken()` to return a dummy token, and verify the data source
parses the file stream correctly.
- **Verify `loadData()` Auth Failure**: Mock `getAccessToken()` to return `null`
or configure `window.fetch` to return `403 Forbidden`, and assert that
`GoogleDriveAuthError` is thrown.
#### URL Parsing & Orchestration Tests
In existing test suites or manual validation:
* Verify that URL query arguments containing `source=google-drive&fileId=XYZ` are parsed into the correct `GoogleDriveSourceSpec`.
* Verify that if `loadData()` throws `GoogleDriveAuthError`, the application transitions state, avoids setting a global error message, and triggers the rendering of the `<GoogleAuthModal />` fallback popup.
- Verify that URL query arguments containing `source=google-drive&fileId=XYZ`
are parsed into the correct `GoogleDriveSourceSpec`.
- Verify that if `loadData()` throws `GoogleDriveAuthError`, the application
transitions state, avoids setting a global error message, and triggers the
rendering of the `<GoogleAuthModal />` fallback popup.
---
### 2. Manual Verification Checklist
Manual testing should be performed locally to verify the full OAuth loop and UI presentation:
Manual testing should be performed locally to verify the full OAuth loop and UI
presentation:
| Test Case | Action | Expected Result |
| :--- | :--- | :--- |
| :---------------------------- | :---------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------- |
| **Missing Credentials** | Run the app with empty `VITE_GOOGLE_CLIENT_ID` / `VITE_GOOGLE_API_KEY` values. | The Google Drive option is completely hidden from the TopBar menus; local file upload and WikiTree operations continue working normally. |
| **Picker Load** | Click "Load from Google Drive" in the menu, log in when prompted, select a `.ged`/`.gdz` file. | Google Picker popup closes, selected file is fetched directly from Google APIs, URL hash updates, and the family chart renders. |
| **Shared Link Auth Fallback** | Load: `http://localhost:3000/#/view?source=google-drive&fileId=UNAUTHORIZED_ID` | The "Google Drive Access Required" modal overlays the screen, preventing rendering. |
@@ -333,47 +603,65 @@ Manual testing should be performed locally to verify the full OAuth loop and UI
### 3. Continuous Integration (CI) Safety
* In CI pipelines (e.g. GitHub Actions workflows), Google API keys are not present in the environment.
* Because the feature checks for the presence of environment variables at compile/build time, the Google Drive menus are automatically omitted.
* This ensures that visual regression tests (`npm run test:visual`) and E2E test runs (`npm run test:e2e`) pass without requiring live OAuth secrets or mock servers.
- In CI pipelines (e.g. GitHub Actions workflows), Google API keys are not
present in the environment.
- Because the feature checks for the presence of environment variables at
compile/build time, the Google Drive menus are automatically omitted.
- This ensures that visual regression tests (`npm run test:visual`) and E2E test
runs (`npm run test:e2e`) pass without requiring live OAuth secrets or mock
servers.
---
## Build and Deployment CI/CD Configuration
To enable the Google Drive integration on the official public deployments, the Google API credentials must be injected during the automated build process in GitHub Actions.
To enable the Google Drive integration on the official public deployments, the
Google API credentials must be injected during the automated build process in
GitHub Actions.
### Required GitHub Secrets
You must configure the following repository secrets in your GitHub project settings:
1. `GOOGLE_CLIENT_ID`: The official OAuth Client ID for `pewu.github.io` / `apps.wikitree.com`.
You must configure the following repository secrets in your GitHub project
settings:
1. `GOOGLE_CLIENT_ID`: The official OAuth Client ID for `pewu.github.io` /
`apps.wikitree.com`.
2. `GOOGLE_API_KEY`: The official Google Picker API Key.
### Workflow Modifications
The build step (`npm run build`) in the deployment workflows must receive these secrets as environment variables.
The build step (`npm run build`) in the deployment workflows must receive these
secrets as environment variables.
#### 1. Deployment to GitHub Pages (`.github/workflows/deploy-gh-pages.yml`)
Pass the secrets to the build step:
```yaml
- run: npm run build
- run: npm run build
env:
VITE_GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
VITE_GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
```
#### 2. Deployment to WikiTree Apps (`.github/workflows/deploy-wikitree-apps.yml`)
Pass the secrets to the build step:
```yaml
- run: npm run build
- run: npm run build
env:
VITE_GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
VITE_GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
```
#### 3. Reusable Workflow Inheritance (`.github/workflows/deploy-everywhere.yml`)
Enable secret inheritance on jobs calling these workflows to ensure secrets are propagated:
Enable secret inheritance on jobs calling these workflows to ensure secrets are
propagated:
```yaml
deploy-gh-pages:
deploy-gh-pages:
uses: ./.github/workflows/deploy-gh-pages.yml
secrets: inherit
```
@@ -382,37 +670,91 @@ Enable secret inheritance on jobs calling these workflows to ensure secrets are
## Security and Privacy
Since Topola Viewer is a purely client-side application that manages sensitive genealogical data, security and user privacy are core design pillars for this integration.
Since Topola Viewer is a purely client-side application that manages sensitive
genealogical data, security and user privacy are core design pillars for this
integration.
### 1. Protection of Public API Credentials
The application's client-side architecture requires compiling the public Google `Client ID` and `API Key` directly into the JavaScript bundle. These values are protected from abuse using Google Cloud Platform constraints:
* **OAuth Domain Whitelisting**: The `Client ID` restricts **Authorized JavaScript Origins** and **Redirect URIs** to `https://pewu.github.io`, `https://apps.wikitree.com`, and `http://localhost:3000`. Google's authorization server will automatically block login attempts initiated from any other domains.
* **API Key Restrictions**: The `API Key` enforces **HTTP Referrer Restrictions** matching the authorized domains. Additionally, the key is scoped via **API Restrictions** to only permit requests to the **Google Picker API** and **Google Drive API**, preventing its use on other Google Cloud services.
* **No Secrets Compiled**: The application never utilizes or compiles an OAuth `Client Secret`, which is only required for server-side integrations.
The application's client-side architecture requires compiling the public Google
`Client ID` and `API Key` directly into the JavaScript bundle. These values are
protected from abuse using Google Cloud Platform constraints:
- **OAuth Domain Whitelisting**: The `Client ID` restricts **Authorized
JavaScript Origins** and **Redirect URIs** to `https://pewu.github.io`,
`https://apps.wikitree.com`, and `http://localhost:3000`. Google's
authorization server will automatically block login attempts initiated from
any other domains.
- **API Key Restrictions**: The `API Key` enforces **HTTP Referrer
Restrictions** matching the authorized domains. Additionally, the key is
scoped via **API Restrictions** to only permit requests to the **Google Picker
API** and **Google Drive API**, preventing its use on other Google Cloud
services.
- **No Secrets Compiled**: The application never utilizes or compiles an OAuth
`Client Secret`, which is only required for server-side integrations.
### 2. Data Privacy & Zero-Server Architecture
* **Direct Client-to-Google Communication**: All requests to Google Drive APIs are initiated directly from the user's browser.
* **No Third-Party Proxies**: To prevent token leakage and maintain data confidentiality, Google Drive downloads **never** route through the `topolaproxy.bieda.it` CORS proxy.
* **Local Parsing**: The fetched GEDCOM or GEDZIP file content is parsed entirely within the browser using local JavaScript. No family data, files, or access tokens are ever transmitted to, processed by, or stored on external servers.
* **Short-Lived Auth Tokens**: The application uses short-lived access tokens that expire automatically. No persistent refresh tokens are requested or stored.
* **Session Storage Cache Security**: To optimize load times, parsed tree data is cached in `sessionStorage` under `google-drive:{fileId}`. To protect user privacy on shared or public computers, signing out or disconnecting Google Drive explicitly purges all `google-drive:*` keys from the browser's `sessionStorage`.
- **Direct Client-to-Google Communication**: All requests to Google Drive APIs
are initiated directly from the user's browser.
- **No Third-Party Proxies**: To prevent token leakage and maintain data
confidentiality, Google Drive downloads **never** route through the
`topolaproxy.bieda.it` CORS proxy.
- **Local Parsing**: The fetched GEDCOM or GEDZIP file content is parsed
entirely within the browser using local JavaScript. No family data, files, or
access tokens are ever transmitted to, processed by, or stored on external
servers.
- **Short-Lived Auth Tokens**: The application uses short-lived access tokens
that expire automatically. No persistent refresh tokens are requested or
stored.
- **Session Storage Cache Security**: To optimize load times, parsed tree data
is cached in `sessionStorage` under `google-drive:{fileId}`. To protect user
privacy on shared or public computers, signing out or disconnecting Google
Drive explicitly purges all `google-drive:*` keys from the browser's
`sessionStorage`.
### 3. Principle of Least Privilege (OAuth Scopes)
Instead of requesting the broad `drive.readonly` scope, which grants visibility into a user's entire Google Drive, this feature utilizes the restrictive `drive.file` scope. Under this scope:
* The application only has permission to view files that the user has explicitly opened with the app (either by selecting the file in the Google Picker or opening it via the "Open with" Google Drive UI option).
* Topola Viewer remains blind to all other files and directories in the user's Google Drive.
Instead of requesting the broad `drive.readonly` scope, which grants visibility
into a user's entire Google Drive, this feature utilizes the restrictive
`drive.file` scope. Under this scope:
- The application only has permission to view files that the user has explicitly
opened with the app (either by selecting the file in the Google Picker or
opening it via the "Open with" Google Drive UI option).
- Topola Viewer remains blind to all other files and directories in the user's
Google Drive.
## Known Limitations
1. **Image Loading from Plain `.ged` Files**: Only `.gdz` archives are supported for viewing media files from Google Drive. Plain `.ged` files will only display genealogical data, and any relative/external image paths will render as broken images because standard HTML `<img>` tags cannot transmit the `Authorization` header required for private Google Drive requests.
2. **Iframe Embedding (WikiTree)**: Running Google Drive authentication and loading files from Google Drive inside an embedding iframe (such as on `apps.wikitree.com`) can fail due to iframe popup sandboxing and origin mismatch restrictions. This configuration is not actively supported.
3. **Session Refresh Cache**: When a page is refreshed, zipped images cached in `sessionStorage` might have invalid Object URLs. We choose to ignore this problem until it becomes a visible UX issue.
4. **Access Modal Lockout**: If a user logs into a Google account that does not have permissions to read the file, they are prompted with the "Access Required" modal. Since this modal blocks the viewport, they cannot click the top-bar "Disconnect" button to switch Google accounts, requiring them to click "Cancel" to return to the homepage first.
1. **Image Loading from Plain `.ged` Files**: Only `.gdz` archives are supported
for viewing media files from Google Drive. Plain `.ged` files will only
display genealogical data, and any relative/external image paths will render
as broken images because standard HTML `<img>` tags cannot transmit the
`Authorization` header required for private Google Drive requests.
2. **Iframe Embedding (WikiTree)**: Running Google Drive authentication and
loading files from Google Drive inside an embedding iframe (such as on
`apps.wikitree.com`) can fail due to iframe popup sandboxing and origin
mismatch restrictions. This configuration is not actively supported.
3. **Session Refresh Cache**: When a page is refreshed, zipped images cached in
`sessionStorage` might have invalid Object URLs. We choose to ignore this
problem until it becomes a visible UX issue.
4. **Access Modal Lockout**: If a user logs into a Google account that does not
have permissions to read the file, they are prompted with the "Access
Required" modal. Since this modal blocks the viewport, they cannot click the
top-bar "Disconnect" button to switch Google accounts, requiring them to
click "Cancel" to return to the homepage first.
---
## Future Improvements
1. **Self-Hosting Runtime Configuration Support**: Read `google-client-id` and `google-api-key` from HTML `<meta>` tags at runtime (similar to `topola-static-url`), allowing self-hosters to deploy and configure Google Drive support without rebuilding the static assets.
2. **Fetch Cancellation & Race Condition Mitigation**: Implement an `AbortController` or active boolean flag cancellation check in `app.tsx`'s `useEffect` loader to ensure slow background downloads do not overwrite newer datasets if a user changes the source/file before the previous request finishes.
1. **Self-Hosting Runtime Configuration Support**: Read `google-client-id` and
`google-api-key` from HTML `<meta>` tags at runtime (similar to
`topola-static-url`), allowing self-hosters to deploy and configure Google
Drive support without rebuilding the static assets.
2. **Fetch Cancellation & Race Condition Mitigation**: Implement an
`AbortController` or active boolean flag cancellation check in `app.tsx`'s
`useEffect` loader to ensure slow background downloads do not overwrite newer
datasets if a user changes the source/file before the previous request
finishes.

View File

@@ -2,13 +2,28 @@
## 1. Business & Functional Problem Statement
Topola Viewer currently relies on a Cypress end-to-end (E2E) test suite to verify critical user flows, such as interactive chart rendering, search capabilities, and experimental WebMCP integrations. However, the existing setup is brittle and hard to maintain because it depends on live, external internet connections to fetch remote GEDCOM trees, uses loosely-typed JavaScript spec files, and suffers from complex process orchestration. Additionally, Cypress's historical architectural limitations make testing cross-origin iframe structures in embedded mode slow and difficult to simulate locally. Migrating this testing suite to Playwright in TypeScript will establish a fast, fully type-safe, hermetic, and self-contained validation pipeline, ensuring absolute quality control and deployment confidence.
Topola Viewer currently relies on a Cypress end-to-end (E2E) test suite to
verify critical user flows, such as interactive chart rendering, search
capabilities, and experimental WebMCP integrations. However, the existing setup
is brittle and hard to maintain because it depends on live, external internet
connections to fetch remote GEDCOM trees, uses loosely-typed JavaScript spec
files, and suffers from complex process orchestration. Additionally, Cypress's
historical architectural limitations make testing cross-origin iframe structures
in embedded mode slow and difficult to simulate locally. Migrating this testing
suite to Playwright in TypeScript will establish a fast, fully type-safe,
hermetic, and self-contained validation pipeline, ensuring absolute quality
control and deployment confidence.
## 2. The Technical Plan
The technical solution relies on transitioning our testing engine from Cypress to Playwright, utilizing a self-contained, local-first execution model. Instead of relying on external servers or separate terminal scripts to boot up our application, Playwright will orchestrate the entire lifecycle from start to finish.
The technical solution relies on transitioning our testing engine from Cypress
to Playwright, utilizing a self-contained, local-first execution model. Instead
of relying on external servers or separate terminal scripts to boot up our
application, Playwright will orchestrate the entire lifecycle from start to
finish.
Here is a block diagram of how the major components fit together during a test run:
Here is a block diagram of how the major components fit together during a test
run:
```mermaid
graph TD
@@ -43,224 +58,469 @@ graph TD
### Breakdown of Major Components
#### 1. The Playwright Test Runner
The central brain of our testing environment. It handles parsing our TypeScript test specifications, managing test execution, starting the local web server, controlling the virtual browser instances, and generating reports.
The central brain of our testing environment. It handles parsing our TypeScript
test specifications, managing test execution, starting the local web server,
controlling the virtual browser instances, and generating reports.
#### 2. The Local Web Server (Vite / Preview)
A local server hosting the Topola Viewer application. To ensure E2E tests validate the exact production bundle that will be deployed, the test runner in CI environments boots the production-built files via Vite's preview server (`npx vite preview --port 3000 --strictPort`), with the build process decoupled and executed earlier in the CI workflow job. Local environments dynamically run Vite dev server (`npx vite --no-open --port 3000 --strictPort`) or reuse an existing running server to maintain fast iteration cycles.
A local server hosting the Topola Viewer application. To ensure E2E tests
validate the exact production bundle that will be deployed, the test runner in
CI environments boots the production-built files via Vite's preview server
(`npx vite preview --port 3000 --strictPort`), with the build process decoupled
and executed earlier in the CI workflow job. Local environments dynamically run
Vite dev server (`npx vite --no-open --port 3000 --strictPort`) or reuse an
existing running server to maintain fast iteration cycles.
#### 3. The Built-in Network Router (Interception)
The central network traffic controller. When the application attempts to fetch the family tree, our network router intercepts the request and returns our offline sample data instead. To prevent browser CORS blocks, the mock response explicitly supplies the `Access-Control-Allow-Origin: *` header. Wildcard interceptors match any request ending with `/family.ged`, allowing E2E tests to load family tree data consistently from local mock data without hardcoding public GitHub URL paths or relying on CORS proxy overrides.
The central network traffic controller. When the application attempts to fetch
the family tree, our network router intercepts the request and returns our
offline sample data instead. To prevent browser CORS blocks, the mock response
explicitly supplies the `Access-Control-Allow-Origin: *` header. Wildcard
interceptors match any request ending with `/family.ged`, allowing E2E tests to
load family tree data consistently from local mock data without hardcoding
public GitHub URL paths or relying on CORS proxy overrides.
#### 4. The Virtual Page Generator (Iframe Wrapper)
A mechanism for generating temporary webpage wrappers directly in-memory during test execution. To simulate how our app works inside an iframe, Playwright serves a dynamic wrapper page on a mock URL `/test-embedded-frame.html`. This wrapper page loads our app inside an iframe and handles standard bidirectional postMessage handshakes. Because the application uses `HashRouter`, the iframe source points to the hash route `/#/view?embedded=true&handleCors=false` to ensure React Router matches the `/view` route.
A mechanism for generating temporary webpage wrappers directly in-memory during
test execution. To simulate how our app works inside an iframe, Playwright
serves a dynamic wrapper page on a mock URL `/test-embedded-frame.html`. This
wrapper page loads our app inside an iframe and handles standard bidirectional
postMessage handshakes. Because the application uses `HashRouter`, the iframe
source points to the hash route `/#/view?embedded=true&handleCors=false` to
ensure React Router matches the `/view` route.
#### 5. The Browser Environment Injector (Init Scripts)
Allows the test runner to pre-configure the browser's environment before the web page's own code executes. This is essential for simulating experimental features like WebMCP tool registration. The injector exposes a mock registration API that pushes registered tools to the browser's global `window.__registeredTools` array. The runner evaluates execution blocks *inside* the browser context to invoke these tools' non-serializable callbacks, asserting tool registration and visual UI updates.
Allows the test runner to pre-configure the browser's environment before the web
page's own code executes. This is essential for simulating experimental features
like WebMCP tool registration. The injector exposes a mock registration API that
pushes registered tools to the browser's global `window.__registeredTools`
array. The runner evaluates execution blocks _inside_ the browser context to
invoke these tools' non-serializable callbacks, asserting tool registration and
visual UI updates.
## 3. Alternatives Considered & Rejected
To establish clear architectural guardrails and prevent future regressions or redundant work, this section documents the key design alternatives that were evaluated and subsequently ruled out.
To establish clear architectural guardrails and prevent future regressions or
redundant work, this section documents the key design alternatives that were
evaluated and subsequently ruled out.
### A. Phased Migration (Coexistence)
* **Considered:** Running both Cypress and Playwright concurrently in the repository and migrating the five test specs one by one over time.
* **Rejected because:** Dual-framework coexistence introduces significant developer friction and technical debt. Developers would have to maintain duplicate configuration files, manage dual dependency pools in `package.json`, and configure complex dual execution stages in GitHub Actions. Given our E2E suite is compact, a "clean break" eliminates Cypress-related dependencies immediately.
- **Considered:** Running both Cypress and Playwright concurrently in the
repository and migrating the five test specs one by one over time.
- **Rejected because:** Dual-framework coexistence introduces significant
developer friction and technical debt. Developers would have to maintain
duplicate configuration files, manage dual dependency pools in `package.json`,
and configure complex dual execution stages in GitHub Actions. Given our E2E
suite is compact, a "clean break" eliminates Cypress-related dependencies
immediately.
### B. Running E2E Tests Against Live URLs (Real-World Feeds)
* **Considered:** Continuing the Cypress pattern of loading the sample family tree directly from GitHub raw servers over the public internet.
* **Rejected because:** Live URL calls in automated E2E tests are a primary source of test flakiness. Tests can fail randomly due to DNS resolution lags, server downtime, or GitHub API rate-limiting, causing false negatives in our CI pipeline. Using Playwright's network routing to intercept these calls and fulfill them with a local fixture guarantees absolute hermeticity and consistency.
- **Considered:** Continuing the Cypress pattern of loading the sample family
tree directly from GitHub raw servers over the public internet.
- **Rejected because:** Live URL calls in automated E2E tests are a primary
source of test flakiness. Tests can fail randomly due to DNS resolution lags,
server downtime, or GitHub API rate-limiting, causing false negatives in our
CI pipeline. Using Playwright's network routing to intercept these calls and
fulfill them with a local fixture guarantees absolute hermeticity and
consistency.
### C. Maintaining Physical HTML Wrapper Files for Iframe Tests
* **Considered:** Storing a physical `tests/fixtures/embedded_frame.html` file in the repository to act as the container for iframe tests.
* **Rejected because:** Storing a physical, standalone test-only wrapper file can create environment port synchronization issues and static asset mapping overhead.
* **Actual Implementation Note:** The implementation adopted a hybrid approach. A physical template file [embedded_frame.html](../tests/fixtures/embedded_frame.html) is maintained as the structural source of truth for the frame, but it is loaded in-memory and served virtually on `/test-embedded-frame.html` via the network router, keeping it on the same origin/port dynamically to bypass cross-origin iframe blocks.
- **Considered:** Storing a physical `tests/fixtures/embedded_frame.html` file
in the repository to act as the container for iframe tests.
- **Rejected because:** Storing a physical, standalone test-only wrapper file
can create environment port synchronization issues and static asset mapping
overhead.
- **Actual Implementation Note:** The implementation adopted a hybrid approach.
A physical template file
[embedded_frame.html](../tests/fixtures/embedded_frame.html) is maintained as
the structural source of truth for the frame, but it is loaded in-memory and
served virtually on `/test-embedded-frame.html` via the network router,
keeping it on the same origin/port dynamically to bypass cross-origin iframe
blocks.
### D. Retaining `start-server-and-test` for Dev Server Bootstrapping
* **Considered:** Continuing to rely on `start-server-and-test` or a custom bash script to verify when port `3000` is responsive before running tests.
* **Rejected because:** Playwright's native `webServer` orchestrator is superior, highly optimized, and self-contained. It handles port polling, processes recycling on failures, and performs automatic process cleanups upon exit natively. Keeping `start-server-and-test` adds unnecessary external library dependencies and script complexity.
- **Considered:** Continuing to rely on `start-server-and-test` or a custom bash
script to verify when port `3000` is responsive before running tests.
- **Rejected because:** Playwright's native `webServer` orchestrator is
superior, highly optimized, and self-contained. It handles port polling,
processes recycling on failures, and performs automatic process cleanups upon
exit natively. Keeping `start-server-and-test` adds unnecessary external
library dependencies and script complexity.
### E. Initial Multi-Browser & Multi-Device Targets
* **Considered:** Configuring Chromium, Firefox, WebKit, and mobile emulation from Day One.
* **Rejected because:** The immediate goal is to achieve full, stable parity with the existing single-browser Cypress suite. Adding multiple engines right away increases execution time and CI resource consumption. Multi-browser testing can be easily enabled later by adjusting the configuration in `playwright.config.ts`.
- **Considered:** Configuring Chromium, Firefox, WebKit, and mobile emulation
from Day One.
- **Rejected because:** The immediate goal is to achieve full, stable parity
with the existing single-browser Cypress suite. Adding multiple engines right
away increases execution time and CI resource consumption. Multi-browser
testing can be easily enabled later by adjusting the configuration in
`playwright.config.ts`.
## 4. Detailed Implementation Plan
This section defines the granular step-by-step instructions and enumerates **every single file** that will be created, modified, or deleted to ensure a flawless migration, including complete, copy-pasteable code contents for all configurations and test specifications matching the actual implementation.
This section defines the granular step-by-step instructions and enumerates
**every single file** that will be created, modified, or deleted to ensure a
flawless migration, including complete, copy-pasteable code contents for all
configurations and test specifications matching the actual implementation.
### A. Enumeration of Files
#### 1. Files to [DELETE]
* **[cypress.config.ts](../cypress.config.ts)**
* *Rationale:* Complete removal of Cypress configurations; no longer needed.
* **[cypress/e2e/chart_view.cy.js](../cypress/e2e/chart_view.cy.js)**
* *Rationale:* Outdated JavaScript test spec. Replaced by type-safe `tests/chart_view.spec.ts`.
* **[cypress/e2e/embedded.cy.js](../cypress/e2e/embedded.cy.js)**
* *Rationale:* Outdated JavaScript test spec. Replaced by type-safe `tests/embedded.spec.ts`.
* **[cypress/e2e/intro.cy.js](../cypress/e2e/intro.cy.js)**
* *Rationale:* Outdated JavaScript test spec. Replaced by type-safe `tests/intro.spec.ts`.
* **[cypress/e2e/search.cy.js](../cypress/e2e/search.cy.js)**
* *Rationale:* Outdated JavaScript test spec. Replaced by type-safe `tests/search.spec.ts`.
* **[cypress/e2e/webmcp.cy.js](../cypress/e2e/webmcp.cy.js)**
* *Rationale:* Outdated JavaScript test spec. Replaced by type-safe `tests/webmcp.spec.ts`.
* **`cypress/e2e/README.md`**
* *Rationale:* Legacy test documentation that is no longer applicable.
- **[cypress.config.ts](../cypress.config.ts)**
- _Rationale:_ Complete removal of Cypress configurations; no longer needed.
- **[cypress/e2e/chart_view.cy.js](../cypress/e2e/chart_view.cy.js)**
- _Rationale:_ Outdated JavaScript test spec. Replaced by type-safe
`tests/chart_view.spec.ts`.
- **[cypress/e2e/embedded.cy.js](../cypress/e2e/embedded.cy.js)**
- _Rationale:_ Outdated JavaScript test spec. Replaced by type-safe
`tests/embedded.spec.ts`.
- **[cypress/e2e/intro.cy.js](../cypress/e2e/intro.cy.js)**
- _Rationale:_ Outdated JavaScript test spec. Replaced by type-safe
`tests/intro.spec.ts`.
- **[cypress/e2e/search.cy.js](../cypress/e2e/search.cy.js)**
- _Rationale:_ Outdated JavaScript test spec. Replaced by type-safe
`tests/search.spec.ts`.
- **[cypress/e2e/webmcp.cy.js](../cypress/e2e/webmcp.cy.js)**
- _Rationale:_ Outdated JavaScript test spec. Replaced by type-safe
`tests/webmcp.spec.ts`.
- **`cypress/e2e/README.md`**
- _Rationale:_ Legacy test documentation that is no longer applicable.
#### 2. Files to [MODIFY]
* **[package.json](../package.json)**
* *Rationale:* Uninstall `cypress` and `start-server-and-test` devDependencies. Install `@playwright/test` and `@types/node`. Add the silent `"preview": "vite preview"` script, replace the `cy:*` test scripts with Playwright E2E execution targets (`"test:e2e": "playwright test"` and `"test:e2e:ui": "playwright test --ui"`), and update the `"prettier"` and `"lint"` scripts to format and lint both the `src/` and `tests/` directories.
* **[jest.config.ts](../jest.config.ts)**
* *Rationale:* Add the `roots` configuration property to isolate Jest unit testing to the `src/` directory, preventing Jest from scanning or executing Playwright specs inside the `tests/` folder.
* **[.gitignore](../.gitignore)**
* *Rationale:* Ignore locally generated Playwright E2E testing artifacts (`playwright-report/`, `test-results/`, and `.playwright/`) to maintain a clean working tree.
* **[PROJECT_STRUCTURE.md](../PROJECT_STRUCTURE.md)**
* *Rationale:* Replace references to Cypress (`cypress/`, `cypress.config.ts`) with Playwright (`tests/`, `playwright.config.ts`) to keep the repository structural documentation fully accurate.
* **[.github/workflows/node.js.yml](../.github/workflows/node.js.yml)**
* *Rationale:* Replace `npm run cy:start-and-run` with cached Playwright execution. E2E tests are executed across all matrix Node environments to maximize testing coverage and parity across runtimes.
* **[.github/workflows/deploy-gh-pages.yml](../.github/workflows/deploy-gh-pages.yml)**
* *Rationale:* Remove testing and browser-setup steps entirely to speed up the deployment flow and separate deploy actions from regular test gating.
* **[.github/workflows/deploy-wikitree-apps.yml](../.github/workflows/deploy-wikitree-apps.yml)**
* *Rationale:* Remove testing and browser-setup steps entirely to speed up the deployment flow and separate deploy actions from regular test gating.
- **[package.json](../package.json)**
- _Rationale:_ Uninstall `cypress` and `start-server-and-test`
devDependencies. Install `@playwright/test` and `@types/node`. Add the
silent `"preview": "vite preview"` script, replace the `cy:*` test scripts
with Playwright E2E execution targets (`"test:e2e": "playwright test"` and
`"test:e2e:ui": "playwright test --ui"`), and update the `"prettier"` and
`"lint"` scripts to format and lint both the `src/` and `tests/`
directories.
- **[jest.config.ts](../jest.config.ts)**
- _Rationale:_ Add the `roots` configuration property to isolate Jest unit
testing to the `src/` directory, preventing Jest from scanning or executing
Playwright specs inside the `tests/` folder.
- **[.gitignore](../.gitignore)**
- _Rationale:_ Ignore locally generated Playwright E2E testing artifacts
(`playwright-report/`, `test-results/`, and `.playwright/`) to maintain a
clean working tree.
- **[PROJECT_STRUCTURE.md](../PROJECT_STRUCTURE.md)**
- _Rationale:_ Replace references to Cypress (`cypress/`, `cypress.config.ts`)
with Playwright (`tests/`, `playwright.config.ts`) to keep the repository
structural documentation fully accurate.
- **[.github/workflows/node.js.yml](../.github/workflows/node.js.yml)**
- _Rationale:_ Replace `npm run cy:start-and-run` with cached Playwright
execution. E2E tests are executed across all matrix Node environments to
maximize testing coverage and parity across runtimes.
- **[.github/workflows/deploy-gh-pages.yml](../.github/workflows/deploy-gh-pages.yml)**
- _Rationale:_ Remove testing and browser-setup steps entirely to speed up the
deployment flow and separate deploy actions from regular test gating.
- **[.github/workflows/deploy-wikitree-apps.yml](../.github/workflows/deploy-wikitree-apps.yml)**
- _Rationale:_ Remove testing and browser-setup steps entirely to speed up the
deployment flow and separate deploy actions from regular test gating.
#### 3. Files to [NEW]
* **[playwright.config.ts](../playwright.config.ts)**
* *Rationale:* Central configuration for Playwright. Configures parallel execution, defines a single Desktop Chrome (Chromium) project, sets the base URL and locale to `'en-US'`, and manages the local dev or preview servers dynamically on port 3000 with strict port enforcement.
* **`tests/tsconfig.json`**
* *Rationale:* Custom localized compiler configuration for E2E tests that extends the root `tsconfig.json` but includes NodeJS environment types explicitly and isolates global types from Jest, keeping E2E environments perfectly isolated. It sets `"noEmit": true` because E2E tests do not need to output compiled JS files.
* **`tests/global.d.ts`**
* *Rationale:* Custom global type declaration file for E2E tests to safely declare `__registeredTools` on the `Window` interface without TypeScript compiler warnings. Redundant overrides for `Navigator` are omitted because the test suite inherits it from the application's core WebMCP declarations.
* **`tests/helpers.ts`**
* *Rationale:* Shared test utilities to encapsulate wildcard route mocking for family tree fetching (`setupGedcomRoute`) and hermetic context routing (`setupHermeticEnvironment`). This avoids code duplication across spec files.
* **`tests/fixtures/embedded_frame.html`**
* *Rationale:* Physical template wrapper file defining the iframe and message-passing structure for embedded view E2E verification.
* **`src/datasource/testdata/test.ged`**
* *Rationale:* Instead of checking in a duplicate fixture file, we directly read the existing version-controlled test GEDCOM dataset located at `src/datasource/testdata/test.ged` inside the Playwright interceptors. This eliminates file duplication, reduces repository footprint, and ensures a single source of truth.
* **`tests/intro.spec.ts`**
* *Rationale:* Type-safe TS spec checking landing page layout, menu items, and basic static DOM presence.
* **`tests/chart_view.spec.ts`**
* *Rationale:* Type-safe TS spec checking interactive tree navigation, relying on Playwright's auto-waiting to settle D3 transitions, drawer details panels, and routing interception to block analytics/third-party APIs.
* **`tests/search.spec.ts`**
* *Rationale:* Type-safe TS spec checking the search autocompletion (using robust, user-facing locators like `page.getByPlaceholder('Search for people')` to target the input, and Playwright's auto-waiting to handle search debouncing).
* **`tests/webmcp.spec.ts`**
* *Rationale:* Type-safe TS spec verifying WebMCP tools. Emulates out-of-process tool executions inside `navigator.modelContext` by evaluating execution blocks within the browser context, using polling assertions to avoid React `useEffect` mount race conditions.
* **`tests/embedded.spec.ts`**
* *Rationale:* Type-safe TS spec verifying iframe embedded views using a virtually served template file `tests/fixtures/embedded_frame.html` executing the bidirectional postMessage handshake (`ready` / `gedcom`) matching production.
- **[playwright.config.ts](../playwright.config.ts)**
- _Rationale:_ Central configuration for Playwright. Configures parallel
execution, defines a single Desktop Chrome (Chromium) project, sets the base
URL and locale to `'en-US'`, and manages the local dev or preview servers
dynamically on port 3000 with strict port enforcement.
- **`tests/tsconfig.json`**
- _Rationale:_ Custom localized compiler configuration for E2E tests that
extends the root `tsconfig.json` but includes NodeJS environment types
explicitly and isolates global types from Jest, keeping E2E environments
perfectly isolated. It sets `"noEmit": true` because E2E tests do not need
to output compiled JS files.
- **`tests/global.d.ts`**
- _Rationale:_ Custom global type declaration file for E2E tests to safely
declare `__registeredTools` on the `Window` interface without TypeScript
compiler warnings. Redundant overrides for `Navigator` are omitted because
the test suite inherits it from the application's core WebMCP declarations.
- **`tests/helpers.ts`**
- _Rationale:_ Shared test utilities to encapsulate wildcard route mocking for
family tree fetching (`setupGedcomRoute`) and hermetic context routing
(`setupHermeticEnvironment`). This avoids code duplication across spec
files.
- **`tests/fixtures/embedded_frame.html`**
- _Rationale:_ Physical template wrapper file defining the iframe and
message-passing structure for embedded view E2E verification.
- **`src/datasource/testdata/test.ged`**
- _Rationale:_ Instead of checking in a duplicate fixture file, we directly
read the existing version-controlled test GEDCOM dataset located at
`src/datasource/testdata/test.ged` inside the Playwright interceptors. This
eliminates file duplication, reduces repository footprint, and ensures a
single source of truth.
- **`tests/intro.spec.ts`**
- _Rationale:_ Type-safe TS spec checking landing page layout, menu items, and
basic static DOM presence.
- **`tests/chart_view.spec.ts`**
- _Rationale:_ Type-safe TS spec checking interactive tree navigation, relying
on Playwright's auto-waiting to settle D3 transitions, drawer details
panels, and routing interception to block analytics/third-party APIs.
- **`tests/search.spec.ts`**
- _Rationale:_ Type-safe TS spec checking the search autocompletion (using
robust, user-facing locators like
`page.getByPlaceholder('Search for people')` to target the input, and
Playwright's auto-waiting to handle search debouncing).
- **`tests/webmcp.spec.ts`**
- _Rationale:_ Type-safe TS spec verifying WebMCP tools. Emulates
out-of-process tool executions inside `navigator.modelContext` by evaluating
execution blocks within the browser context, using polling assertions to
avoid React `useEffect` mount race conditions.
- **`tests/embedded.spec.ts`**
- _Rationale:_ Type-safe TS spec verifying iframe embedded views using a
virtually served template file `tests/fixtures/embedded_frame.html`
executing the bidirectional postMessage handshake (`ready` / `gedcom`)
matching production.
### B. Step-by-Step Execution Plan & Complete File Contents
#### Step 1: Dependency Purge & Clean Break
1. Remove legacy Cypress and server-tester modules from `package.json`:
`npm uninstall cypress start-server-and-test`
2. Delete the `cypress.config.ts` configuration and the recursive `cypress/` folder from disk.
2. Delete the `cypress.config.ts` configuration and the recursive `cypress/`
folder from disk.
3. Install Playwright Test Framework and Node environment types:
`npm install -D @playwright/test @types/node`
4. Download the local browser binaries required for Playwright execution:
`npx playwright install chromium`
5. Update the `"prettier"` script in `package.json` to cover both `src/` and `tests/`:
`"prettier": "prettier --write \"{src,tests}/**/*.{ts,tsx,json}\""`
6. Modify `jest.config.ts` to isolate Jest testing to the `src/` directory and prevent test scanning collisions:
5. Update the `"prettier"` script in `package.json` to cover both `src/` and
`tests/`: `"prettier": "prettier --write \"{src,tests}/**/*.{ts,tsx,json}\""`
6. Modify `jest.config.ts` to isolate Jest testing to the `src/` directory and
prevent test scanning collisions:
```typescript
// Add this property to the exported config object in jest.config.ts
roots: ["<rootDir>/src"],
```
7. Update the repository structural documentation [PROJECT_STRUCTURE.md](../PROJECT_STRUCTURE.md) to replace Cypress details with Playwright details.
7. Update the repository structural documentation
[PROJECT_STRUCTURE.md](../PROJECT_STRUCTURE.md) to replace Cypress details
with Playwright details.
#### Step 2: Script Alignment & Silent Configs
1. Update the `package.json` scripts:
* Add `"preview": "vite preview"` to support serving the production build in CI.
* Replace `cy:*` script targets with Playwright E2E commands:
* `"test:e2e": "playwright test"`
* `"test:e2e:ui": "playwright test --ui"`
2. Author `playwright.config.ts` to orchestrate the `webServer` dynamically based on execution context, and poll port `3000`.
**Key Configuration Details for [playwright.config.ts](../playwright.config.ts):**
* **Test Directory**: Target `./tests` folder.
* **Parallelism & CI Tuning**: Enable fully parallel execution (`fullyParallel: true`), disable `forbidOnly` locally but enforce it in CI, and configure retries (2 in CI, 0 locally).
* **Base Configuration**: Set the `baseURL` to `http://localhost:3000`, force the locale to `'en-US'` to ensure consistent translation keys across all runs, and capture traces on first retry.
* **Browsers**: Set up a single project using standard Chromium devices (`Desktop Chrome`).
* **Orchestrated WebServer**: Configure `webServer` to run the Vite dev server (`npx vite --no-open --port 3000 --strictPort`) locally or Vite preview (`npx vite preview --port 3000 --strictPort`) in CI, targeting port `3000` with strict port enforcement and reusing any existing local server instance only in local mode.
1. Update the `package.json` scripts:
- Add `"preview": "vite preview"` to support serving the production build in
CI.
- Replace `cy:*` script targets with Playwright E2E commands:
- `"test:e2e": "playwright test"`
- `"test:e2e:ui": "playwright test --ui"`
2. Author `playwright.config.ts` to orchestrate the `webServer` dynamically
based on execution context, and poll port `3000`.
**Key Configuration Details for
[playwright.config.ts](../playwright.config.ts):**
- **Test Directory**: Target `./tests` folder.
- **Parallelism & CI Tuning**: Enable fully parallel execution
(`fullyParallel: true`), disable `forbidOnly` locally but enforce it in CI,
and configure retries (2 in CI, 0 locally).
- **Base Configuration**: Set the `baseURL` to `http://localhost:3000`, force
the locale to `'en-US'` to ensure consistent translation keys across all runs,
and capture traces on first retry.
- **Browsers**: Set up a single project using standard Chromium devices
(`Desktop Chrome`).
- **Orchestrated WebServer**: Configure `webServer` to run the Vite dev server
(`npx vite --no-open --port 3000 --strictPort`) locally or Vite preview
(`npx vite preview --port 3000 --strictPort`) in CI, targeting port `3000`
with strict port enforcement and reusing any existing local server instance
only in local mode.
#### Step 3: Establish Test Directories & Compile Settings
1. Create the folder path `tests/` and its subfolder `tests/fixtures/`.
2. Author `tests/tsconfig.json` to isolate test typings from the main application and Jest unit tests:
* **Inheritance & Target**: Extends the root `tsconfig.json`, setting `module` and `moduleResolution` to `NodeNext` and targeting `ES2022`.
* **Isolated Types**: Excludes Jest/app global typings and explicitly pulls in only the `"node"` types.
* **No Output**: Sets `"noEmit": true` as E2E specs do not need compilation output.
3. Author `tests/global.d.ts` to provide TypeScript type definitions for mocked window objects:
* **Type Extension**: Declares `__registeredTools?` on the global `Window` interface to prevent TypeScript compilation errors during WebMCP mocks.
4. Author `tests/helpers.ts` to provide reusable mock setups and routing interceptions:
* **Hermetic Routing**: Implements a `setupHermeticEnvironment(context)` helper that intercepts tracking services (`**/*google-analytics.com/**`, `**/*googletagmanager.com/**`) and embeds baseline web fonts to guarantee deterministic and fast test execution.
* **GEDCOM Mocks**: Implements a `setupGedcomRoute(context)` helper that reads the version-controlled local dataset (`src/datasource/testdata/test.ged`) and routes all requests matching `**/family.ged` to be fulfilled with it, serving a `200 OK` response with CORS enablement headers (`Access-Control-Allow-Origin: *`).
5. Author the physical template wrapper file `tests/fixtures/embedded_frame.html` for testing embedded iframe communications:
* **Structure**: Defines a standard wrapper document housing an iframe that points to the app's embedded route: `/#/view?embedded=true&handleCors=false`.
* **Bidirectional Handshake**: Contains script block executing a `fetch()` to get the GEDCOM content. It listens for a `'ready'` postMessage from the child iframe, and posts the raw GEDCOM content back with a `'gedcom'` message once the handshake is complete.
2. Author `tests/tsconfig.json` to isolate test typings from the main
application and Jest unit tests:
- **Inheritance & Target**: Extends the root `tsconfig.json`, setting
`module` and `moduleResolution` to `NodeNext` and targeting `ES2022`.
- **Isolated Types**: Excludes Jest/app global typings and explicitly pulls
in only the `"node"` types.
- **No Output**: Sets `"noEmit": true` as E2E specs do not need compilation
output.
3. Author `tests/global.d.ts` to provide TypeScript type definitions for mocked
window objects:
- **Type Extension**: Declares `__registeredTools?` on the global `Window`
interface to prevent TypeScript compilation errors during WebMCP mocks.
4. Author `tests/helpers.ts` to provide reusable mock setups and routing
interceptions:
- **Hermetic Routing**: Implements a `setupHermeticEnvironment(context)`
helper that intercepts tracking services (`**/*google-analytics.com/**`,
`**/*googletagmanager.com/**`) and embeds baseline web fonts to guarantee
deterministic and fast test execution.
- **GEDCOM Mocks**: Implements a `setupGedcomRoute(context)` helper that
reads the version-controlled local dataset
(`src/datasource/testdata/test.ged`) and routes all requests matching
`**/family.ged` to be fulfilled with it, serving a `200 OK` response with
CORS enablement headers (`Access-Control-Allow-Origin: *`).
5. Author the physical template wrapper file
`tests/fixtures/embedded_frame.html` for testing embedded iframe
communications:
- **Structure**: Defines a standard wrapper document housing an iframe that
points to the app's embedded route:
`/#/view?embedded=true&handleCors=false`.
- **Bidirectional Handshake**: Contains script block executing a `fetch()` to
get the GEDCOM content. It listens for a `'ready'` postMessage from the
child iframe, and posts the raw GEDCOM content back with a `'gedcom'`
message once the handshake is complete.
#### Step 4: Spec Translation & Spec Designs
##### 1. Intro Test (`tests/intro.spec.ts`)
Checks the landing page layout, menu items, and basic static DOM presence:
* **Setup**: Leverages `beforeEach` to configure hermetic routes using the `setupHermeticEnvironment` helper, then loads the index page (`/`).
* **Assertions**:
* Verifies that the main intro landing text content (specifically checking for the presence of `'Examples'`) is visible on the page.
* Asserts that core action buttons in the menu (exact text `'Open file'` and `'Load from URL'`) are properly rendered and visible to the user.
- **Setup**: Leverages `beforeEach` to configure hermetic routes using the
`setupHermeticEnvironment` helper, then loads the index page (`/`).
- **Assertions**:
- Verifies that the main intro landing text content (specifically checking for
the presence of `'Examples'`) is visible on the page.
- Asserts that core action buttons in the menu (exact text `'Open file'` and
`'Load from URL'`) are properly rendered and visible to the user.
##### 2. Chart View Test (`tests/chart_view.spec.ts`)
Checks tree navigation, settles transitions, details panels, and routing interception:
* **Setup**: Employs `beforeEach` to configure route intercepts via `setupGedcomRoute` and navigates to the app passing a mock URL parameter (`/#/view?url=https%3A%2F%2Fexample.org%2Ffamily.ged`).
* **Assertions**:
* Verifies that the viewer loads and renders the chart nodes, asserting that the name `'Bonifacy'` is visible inside the `#content` container.
* Tests chart interactive capabilities by clicking an individual node (e.g., `'Radobod'`) with `{force: true}` to navigate, asserting that a child node (e.g., `'Chike'`) animates and renders.
* Verifies side-drawer details rendering by asserting that person-specific data (e.g., `'a random note'`) is visible.
Checks tree navigation, settles transitions, details panels, and routing
interception:
- **Setup**: Employs `beforeEach` to configure route intercepts via
`setupGedcomRoute` and navigates to the app passing a mock URL parameter
(`/#/view?url=https%3A%2F%2Fexample.org%2Ffamily.ged`).
- **Assertions**:
- Verifies that the viewer loads and renders the chart nodes, asserting that
the name `'Bonifacy'` is visible inside the `#content` container.
- Tests chart interactive capabilities by clicking an individual node (e.g.,
`'Radobod'`) with `{force: true}` to navigate, asserting that a child node
(e.g., `'Chike'`) animates and renders.
- Verifies side-drawer details rendering by asserting that person-specific
data (e.g., `'a random note'`) is visible.
##### 3. Search Test (`tests/search.spec.ts`)
Checks search input, suggestion popups, debouncing, and navigation updates:
* **Setup**: Prepares GEDCOM routing via `setupGedcomRoute` and loads the chart view.
* **Assertions**:
* Targets the search input using the user-facing selector `page.getByPlaceholder('Search for people')`.
* Fills in search queries (e.g., `'chik'`), verifies the autocomplete results panel (`.results`) debounces and renders the target suggestion (`'Chike'`).
* Triggers selection by pressing `'Enter'` and asserts that the main chart view successfully shifts focus and renders the target person's node.
- **Setup**: Prepares GEDCOM routing via `setupGedcomRoute` and loads the chart
view.
- **Assertions**:
- Targets the search input using the user-facing selector
`page.getByPlaceholder('Search for people')`.
- Fills in search queries (e.g., `'chik'`), verifies the autocomplete results
panel (`.results`) debounces and renders the target suggestion (`'Chike'`).
- Triggers selection by pressing `'Enter'` and asserts that the main chart
view successfully shifts focus and renders the target person's node.
##### 4. WebMCP Test (`tests/webmcp.spec.ts`)
Bridges out-of-process serialization, checks tool registrations, and asserts detail updates:
* **Setup**: Configures GEDCOM intercepts. Prior to navigation, it injects an early script (`page.addInitScript`) that mocks `navigator.modelContext.registerTool` and `navigator.modelContext.unregisterTool` APIs, populating tool metadata in a global `window.__registeredTools` list.
* **Assertions**:
* Verifies all core WebMCP tools (`get_selected_person`, `search_indi`, `inspect_indi`, `focus_indi`, `find_relationship_path`, `get_ancestors`, `get_descendants`) register correctly. Uses a polling assertion (`page.waitForFunction`) to safely wait for the React hook mounting cycle.
* Tests interactive integration by evaluating a script in the browser context that calls the registered `focus_indi` tool callback with target identifier (`{id: 'I21'}`), then asserts that the UI automatically updates the viewer focus to `'Chike'`.
Bridges out-of-process serialization, checks tool registrations, and asserts
detail updates:
- **Setup**: Configures GEDCOM intercepts. Prior to navigation, it injects an
early script (`page.addInitScript`) that mocks
`navigator.modelContext.registerTool` and
`navigator.modelContext.unregisterTool` APIs, populating tool metadata in a
global `window.__registeredTools` list.
- **Assertions**:
- Verifies all core WebMCP tools (`get_selected_person`, `search_indi`,
`inspect_indi`, `focus_indi`, `find_relationship_path`, `get_ancestors`,
`get_descendants`) register correctly. Uses a polling assertion
(`page.waitForFunction`) to safely wait for the React hook mounting cycle.
- Tests interactive integration by evaluating a script in the browser context
that calls the registered `focus_indi` tool callback with target identifier
(`{id: 'I21'}`), then asserts that the UI automatically updates the viewer
focus to `'Chike'`.
##### 5. Embedded Test (`tests/embedded.spec.ts`)
Serves local wrapper page template virtually and handles complete bidirectional postMessage handshake:
* **Setup**: Prepares standard GEDCOM routing. Reads the physical `embedded_frame.html` template from disk and registers a route interceptor serving it dynamically at `/test-embedded-frame.html` so both frames stay on the same origin.
* **Assertions**:
* Navigates to `/test-embedded-frame.html`.
* Targets the inner iframe using `page.frameLocator('#topolaFrame')`.
* Verifies that the bidirectional message passing functions correctly by asserting that the inner iframe successfully receives and renders the root family chart nodes (asserting the presence of `'Bonifacy'`).
Serves local wrapper page template virtually and handles complete bidirectional
postMessage handshake:
- **Setup**: Prepares standard GEDCOM routing. Reads the physical
`embedded_frame.html` template from disk and registers a route interceptor
serving it dynamically at `/test-embedded-frame.html` so both frames stay on
the same origin.
- **Assertions**:
- Navigates to `/test-embedded-frame.html`.
- Targets the inner iframe using `page.frameLocator('#topolaFrame')`.
- Verifies that the bidirectional message passing functions correctly by
asserting that the inner iframe successfully receives and renders the root
family chart nodes (asserting the presence of `'Bonifacy'`).
#### Step 5: CI Pipeline Alignment
Modify the GitHub Actions YAML scripts in `.github/workflows/` to replace all legacy Cypress execution steps. E2E testing runs on all Node environments in the pipeline matrix for comprehensive validation.
Modify the GitHub Actions YAML scripts in `.github/workflows/` to replace all
legacy Cypress execution steps. E2E testing runs on all Node environments in the
pipeline matrix for comprehensive validation.
##### 1. Key Steps for `node.js.yml`
* **Prettier & Lint Checks**: Run formatting checks across both the application and the new tests using `npx prettier --check "{src,tests}/**/*.{ts,tsx,json}"` and run `npm run lint`.
* **Build & Test**: Run standard `npm run build` and unit tests via `npm test`.
* **E2E Spec Typecheck**: Run `npx tsc -p tests/tsconfig.json --noEmit` to guarantee complete E2E spec compilation/type correctness.
* **Playwright Browser Caching**: Extract the current Playwright version and cache the browser binaries (`~/.cache/ms-playwright`) dynamically using `actions/cache@v4` to prevent redundant downloads.
* **Execution**: Install necessary OS libraries using `npx playwright install-deps chromium`, install the browser binary, and run the test runner with `npm run test:e2e`.
* **Artifact Storage**: Archive the resulting `playwright-report/` directory on failure or completion using `actions/upload-artifact@v4` with a 30-day retention period.
- **Prettier & Lint Checks**: Run formatting checks across both the application
and the new tests using
`npx prettier --check "{src,tests}/**/*.{ts,tsx,json}"` and run
`npm run lint`.
- **Build & Test**: Run standard `npm run build` and unit tests via `npm test`.
- **E2E Spec Typecheck**: Run `npx tsc -p tests/tsconfig.json --noEmit` to
guarantee complete E2E spec compilation/type correctness.
- **Playwright Browser Caching**: Extract the current Playwright version and
cache the browser binaries (`~/.cache/ms-playwright`) dynamically using
`actions/cache@v4` to prevent redundant downloads.
- **Execution**: Install necessary OS libraries using
`npx playwright install-deps chromium`, install the browser binary, and run
the test runner with `npm run test:e2e`.
- **Artifact Storage**: Archive the resulting `playwright-report/` directory on
failure or completion using `actions/upload-artifact@v4` with a 30-day
retention period.
##### 2. Key Steps for `deploy-gh-pages.yml` & `deploy-wikitree-apps.yml`
* No testing steps are run within the deploy workflows. They are fully streamlined to execute checkouts, build production assets, and deploy immediately, leaving gating assertions to the primary merge-to-master CI loop (`node.js.yml`).
- No testing steps are run within the deploy workflows. They are fully
streamlined to execute checkouts, build production assets, and deploy
immediately, leaving gating assertions to the primary merge-to-master CI loop
(`node.js.yml`).
## 5. Future Considerations
### A. Visual Regression (Screenshot) Testing
While the initial migration focuses on full parity with standard Cypress text-assertion specs, Topola Viewer is a highly visual, SVG-driven charting engine. In a future iteration, we should introduce Playwright visual snapshot/regression testing using `expect(page.locator('#content svg')).toHaveScreenshot('default-chart.png')`. This will automatically catch layout regressions, overlaps, and styled box positioning issues that DOM text assertions cannot detect.
While the initial migration focuses on full parity with standard Cypress
text-assertion specs, Topola Viewer is a highly visual, SVG-driven charting
engine. In a future iteration, we should introduce Playwright visual
snapshot/regression testing using
`expect(page.locator('#content svg')).toHaveScreenshot('default-chart.png')`.
This will automatically catch layout regressions, overlaps, and styled box
positioning issues that DOM text assertions cannot detect.
### B. File Upload Flow Testing
To ensure users can reliably upload their family trees locally, a future iteration should introduce E2E tests for local file ingestion:
* **Single File Upload (.ged)**: Simulating selection and upload of `.ged` text files using Playwright's `setInputFiles()` or `filechooser` events, verifying that the application correctly transitions to the visual chart view.
* **Multi-File / Image Upload**: Verifying that uploading a `.ged` file along with associated `.jpg` or `.png` image assets resolves and binds photos to individuals in the chart without errors.
* **Fixture Management**: This will require checking in small, 1x1 pixel dummy images to act as local mock visual resources.
To ensure users can reliably upload their family trees locally, a future
iteration should introduce E2E tests for local file ingestion:
- **Single File Upload (.ged)**: Simulating selection and upload of `.ged` text
files using Playwright's `setInputFiles()` or `filechooser` events, verifying
that the application correctly transitions to the visual chart view.
- **Multi-File / Image Upload**: Verifying that uploading a `.ged` file along
with associated `.jpg` or `.png` image assets resolves and binds photos to
individuals in the chart without errors.
- **Fixture Management**: This will require checking in small, 1x1 pixel dummy
images to act as local mock visual resources.
### C. CI/CD Pipeline Optimization (Shared E2E Gating Job)
Currently, the manual deployment workflow `deploy-everywhere.yml` triggers both `deploy-gh-pages.yml` and `deploy-wikitree-apps.yml` in parallel, which duplicates E2E test execution and results in running the slow Playwright test runner twice simultaneously. A future optimization should restructure the workflows to execute the E2E validation once as a gating pre-deploy job (`needs: e2e-validation`), or rely strictly on the main merge-to-master CI pipeline validation, ensuring manual deployments are fast, clean, and lightweight.
Currently, the manual deployment workflow `deploy-everywhere.yml` triggers both
`deploy-gh-pages.yml` and `deploy-wikitree-apps.yml` in parallel, which
duplicates E2E test execution and results in running the slow Playwright test
runner twice simultaneously. A future optimization should restructure the
workflows to execute the E2E validation once as a gating pre-deploy job
(`needs: e2e-validation`), or rely strictly on the main merge-to-master CI
pipeline validation, ensuring manual deployments are fast, clean, and
lightweight.

View File

@@ -3,34 +3,33 @@
## 1. Problem Statement
Topola Viewer is deployed to two environments — GitHub Pages and
apps.wikitree.com — and depends on external services outside of our control:
the WikiTree API and a third-party CORS proxy (`topolaproxy.bieda.it`). Any of
these moving parts can break independently of our code: the WikiTree API can
change its response schema or rate-limit requests, the CORS proxy can go down
or change its URL scheme, and a deployment can silently introduce a routing or
build issue that only manifests in production. Our existing test suite is
hermetic — it mocks all network calls — so it verifies code correctness but
cannot detect when the live, deployed system stops working end-to-end. We need
lightweight smoke tests ("probers") that run against the live deployed URLs to
catch real-world breakage, both immediately after each deployment and on a
daily schedule.
apps.wikitree.com — and depends on external services outside of our control: the
WikiTree API and a third-party CORS proxy (`topolaproxy.bieda.it`). Any of these
moving parts can break independently of our code: the WikiTree API can change
its response schema or rate-limit requests, the CORS proxy can go down or change
its URL scheme, and a deployment can silently introduce a routing or build issue
that only manifests in production. Our existing test suite is hermetic — it
mocks all network calls — so it verifies code correctness but cannot detect when
the live, deployed system stops working end-to-end. We need lightweight smoke
tests ("probers") that run against the live deployed URLs to catch real-world
breakage, both immediately after each deployment and on a daily schedule.
## 2. The Technical Plan
The prober system consists of four independent smoke tests, each targeting a
specific combination of deployment target and data path. Three of the four
tests launch a real browser and navigate to a live deployed URL. The fourth
pulls the Docker image published to GHCR, runs it locally, and
verifies that the containerized application starts and renders data. All tests
verify that the chart renders, the side panel shows the expected person's name,
and no error message is displayed.
specific combination of deployment target and data path. Three of the four tests
launch a real browser and navigate to a live deployed URL. The fourth pulls the
Docker image published to GHCR, runs it locally, and verifies that the
containerized application starts and renders data. All tests verify that the
chart renders, the side panel shows the expected person's name, and no error
message is displayed.
The four probers are:
1. **WikiTree direct API prober** — Loads a known WikiTree profile
(`Skłodowska-2`) from the app deployed on `apps.wikitree.com`. This
exercises the direct WikiTree API path (no CORS proxy) and confirms the
WikiTree deployment is healthy.
(`Skłodowska-2`) from the app deployed on `apps.wikitree.com`. This exercises
the direct WikiTree API path (no CORS proxy) and confirms the WikiTree
deployment is healthy.
2. **GitHub Pages GEDCOM prober** — Loads a GEDCOM file from a raw GitHub URL
through the app on `pewu.github.io`. Because the app is not on the
@@ -44,12 +43,12 @@ The four probers are:
default. This confirms the CORS proxy is reachable from the WikiTree
deployment.
4. **Docker container prober** — Pulls the Docker image published to GHCR
by `deploy-docker.yml`, runs it locally with the test GEDCOM file mounted
via `STATIC_URL`, and verifies that the application renders the chart.
This exercises the Docker build path (multi-stage `Dockerfile`, Caddy
server configuration, static URL template injection) and confirms the
published container image starts and serves data correctly.
4. **Docker container prober** — Pulls the Docker image published to GHCR by
`deploy-docker.yml`, runs it locally with the test GEDCOM file mounted via
`STATIC_URL`, and verifies that the application renders the chart. This
exercises the Docker build path (multi-stage `Dockerfile`, Caddy server
configuration, static URL template injection) and confirms the published
container image starts and serves data correctly.
Each prober is a standalone GitHub Actions workflow that can be triggered in
three ways: automatically after a deploy finishes, on a daily schedule, or
@@ -110,21 +109,21 @@ flowchart TD
Each prober is a small Playwright test spec. Three specs run against live
deployed URLs; the Docker prober spec runs against a local Docker container
started by the workflow. The specs live in a separate `tests/probers/`
directory with their own Playwright configuration
(`playwright.prober.config.ts`) so they are completely isolated from the
existing hermetic test suite. Note: because the existing `playwright.config.ts`
uses `testDir: './tests'` and Playwright searches recursively, the e2e project
in the existing config must add `testIgnore: ['*_visual.spec.ts', 'probers/**']`
to prevent prober specs from being picked up by the regular CI test run
(`npm run test:e2e` or `npm run test:visual`). The prober config does not start
a local dev server — each spec navigates to a full absolute URL (or
`localhost:8080` for the Docker prober). A successful prober means a user can
load the app and see data; a failure means something in the chain is broken
and triggers an email notification. Note: GitHub Actions only sends email
notifications if the user has explicitly enabled email notifications in their
GitHub notification settings (Settings → Notifications → Email). If email
notifications are disabled, failures are only visible in the Actions UI.
started by the workflow. The specs live in a separate `tests/probers/` directory
with their own Playwright configuration (`playwright.prober.config.ts`) so they
are completely isolated from the existing hermetic test suite. Note: because the
existing `playwright.config.ts` uses `testDir: './tests'` and Playwright
searches recursively, the e2e project in the existing config must add
`testIgnore: ['*_visual.spec.ts', 'probers/**']` to prevent prober specs from
being picked up by the regular CI test run (`npm run test:e2e` or
`npm run test:visual`). The prober config does not start a local dev server —
each spec navigates to a full absolute URL (or `localhost:8080` for the Docker
prober). A successful prober means a user can load the app and see data; a
failure means something in the chain is broken and triggers an email
notification. Note: GitHub Actions only sends email notifications if the user
has explicitly enabled email notifications in their GitHub notification settings
(Settings → Notifications → Email). If email notifications are disabled,
failures are only visible in the Actions UI.
## 3. Alternatives Considered & Rejected
@@ -133,143 +132,142 @@ explicitly rejected. They are documented here to prevent future re-litigation
and to serve as guardrails against scope creep.
### Alternative A: Unit-level API integration tests against the live
WikiTree API
* **Considered:** Writing tests that call the raw `wikitree-js` library
- **Considered:** Writing tests that call the raw `wikitree-js` library
functions directly against the live WikiTree API, verifying response schemas
and field presence.
* **Why Rejected:** This tests the `wikitree-js` dependency, not our code. Our
existing Jest unit tests already cover our transformation logic using
mocked API responses. The goal of probers is to verify the full
end-to-end chain — browser, deployed app, network, API, proxy — not to
re-verify API response shapes. Adding a separate layer of API-level
integration tests would duplicate coverage without catching deployment or
proxy issues.
- **Why Rejected:** This tests the `wikitree-js` dependency, not our code. Our
existing Jest unit tests already cover our transformation logic using mocked
API responses. The goal of probers is to verify the full end-to-end chain —
browser, deployed app, network, API, proxy — not to re-verify API response
shapes. Adding a separate layer of API-level integration tests would duplicate
coverage without catching deployment or proxy issues.
### Alternative B: Testing the CORS proxy on apps.wikitree.com via the
WikiTree data path
* **Considered:** Forcing the WikiTree API calls through the CORS proxy when
the app is deployed on `apps.wikitree.com`, to test the proxy from that
- **Considered:** Forcing the WikiTree API calls through the CORS proxy when the
app is deployed on `apps.wikitree.com`, to test the proxy from that domain.
- **Why Rejected:** The app hardcodes `handleCors` based on hostname — on
`apps.wikitree.com`, WikiTree API calls always go direct (no proxy). There is
no URL parameter to override this for the WikiTree data source. Forcing the
proxy path would require a code change for test-only purposes, which is not
justified. Instead, the CORS proxy is tested on `apps.wikitree.com` through
the GEDCOM-from-URL path, which uses the proxy by default regardless of
domain.
* **Why Rejected:** The app hardcodes `handleCors` based on hostname — on
`apps.wikitree.com`, WikiTree API calls always go direct (no proxy). There
is no URL parameter to override this for the WikiTree data source. Forcing
the proxy path would require a code change for test-only purposes, which is
not justified. Instead, the CORS proxy is tested on `apps.wikitree.com`
through the GEDCOM-from-URL path, which uses the proxy by default
regardless of domain.
### Alternative C: Monolithic prober workflow with multiple jobs
* **Considered:** A single `prober.yml` workflow containing all four prober
- **Considered:** A single `prober.yml` workflow containing all four prober
tests as separate jobs within it.
* **Why Rejected:** Separate workflow files give finer-grained control in the
- **Why Rejected:** Separate workflow files give finer-grained control in the
GitHub Actions UI — each prober can be triggered, re-run, or inspected
independently. They also allow each prober to declare a targeted `needs`
dependency on only the relevant deploy job (e.g., the WikiTree prober
depends on `deploy-wikitree-apps`, not `deploy-gh-pages`). A monolithic
workflow would couple all probers to the same trigger and make partial
failures harder to manage.
dependency on only the relevant deploy job (e.g., the WikiTree prober depends
on `deploy-wikitree-apps`, not `deploy-gh-pages`). A monolithic workflow would
couple all probers to the same trigger and make partial failures harder to
manage.
### Alternative D: Probers that depend on all deploys finishing
* **Considered:** Making all four probers wait for all of `deploy-gh-pages`,
`deploy-wikitree-apps`, and `deploy-docker` to complete before running
any of them.
* **Why Rejected:** This unnecessarily delays probers whose target has
already been deployed. The WikiTree probers only need the WikiTree deploy
to finish; the GitHub Pages prober only needs the GitHub Pages deploy.
Coupling them to all deploys adds latency without benefit, and means a
failure in one deploy would block probers for the other.
- **Considered:** Making all four probers wait for all of `deploy-gh-pages`,
`deploy-wikitree-apps`, and `deploy-docker` to complete before running any of
them.
- **Why Rejected:** This unnecessarily delays probers whose target has already
been deployed. The WikiTree probers only need the WikiTree deploy to finish;
the GitHub Pages prober only needs the GitHub Pages deploy. Coupling them to
all deploys adds latency without benefit, and means a failure in one deploy
would block probers for the other.
### Alternative E: Unconditional sleep before every prober run
* **Considered:** Always waiting 3 minutes at the start of every prober run,
- **Considered:** Always waiting 3 minutes at the start of every prober run,
regardless of trigger source.
* **Why Rejected:** The sleep is only necessary after a deploy, to allow
GitHub Pages or WikiTree to propagate the new version. For daily scheduled
runs and manual triggers, there is no recent deploy to wait for, so the
sleep wastes 3 minutes. Instead, a `wait_for_propagation` input flag is
passed as `true` only when the prober is invoked from the deploy workflow.
- **Why Rejected:** The sleep is only necessary after a deploy, to allow GitHub
Pages or WikiTree to propagate the new version. For daily scheduled runs and
manual triggers, there is no recent deploy to wait for, so the sleep wastes 3
minutes. Instead, a `wait_for_propagation` input flag is passed as `true` only
when the prober is invoked from the deploy workflow.
### Alternative F: Correctness assertions against specific WikiTree profile
data
* **Considered:** Asserting detailed data fields (e.g., specific birth dates,
parent IDs, spouse counts) from the `Skłodowska-2` WikiTree profile to
verify data correctness.
* **Why Rejected:** Probers are smoke tests — their job is to verify "does
the pipe work?", not "is the data correct?". Data correctness is already
verified by the hermetic test suite with controlled fixtures. Coupling
probers to specific WikiTree profile data creates fragility: if anyone
edits the WikiTree profile, the prober would break even though the system
is healthy. Probers assert only that the expected person's name appears in
the chart and side panel, and that no error is displayed.
- **Considered:** Asserting detailed data fields (e.g., specific birth dates,
parent IDs, spouse counts) from the `Skłodowska-2` WikiTree profile to verify
data correctness.
- **Why Rejected:** Probers are smoke tests — their job is to verify "does the
pipe work?", not "is the data correct?". Data correctness is already verified
by the hermetic test suite with controlled fixtures. Coupling probers to
specific WikiTree profile data creates fragility: if anyone edits the WikiTree
profile, the prober would break even though the system is healthy. Probers
assert only that the expected person's name appears in the chart and side
panel, and that no error is displayed.
## 4. Detailed Implementation Plan
This section enumerates every file that will be created or modified, in
the order they should be implemented, along with the rationale for each
change. The implementation is divided into five steps.
This section enumerates every file that will be created or modified, in the
order they should be implemented, along with the rationale for each change. The
implementation is divided into five steps.
### Step 1: Prober Playwright configuration
**Create:** `playwright.prober.config.ts`
A separate Playwright configuration file dedicated to prober tests. This
file is distinct from the existing `playwright.config.ts` and serves a
different purpose: it does not start a local dev server, does not define
visual regression projects, and runs only against live deployed URLs.
A separate Playwright configuration file dedicated to prober tests. This file is
distinct from the existing `playwright.config.ts` and serves a different
purpose: it does not start a local dev server, does not define visual regression
projects, and runs only against live deployed URLs.
Rationale for key configuration decisions:
* **No `webServer`** — The existing config starts a Vite dev/preview server
on `localhost:3000`. The prober config does not use Playwright's
`webServer` feature. Live-URL probers navigate to full absolute URLs;
the Docker prober's workflow starts the container externally (via
`docker run`) before the test runs, so Playwright connects to
`localhost:8080` without a `webServer` definition.
* **`testDir: './tests/probers'`** — Prober specs are isolated in their own
directory. Additionally, the existing `playwright.config.ts` e2e project
must add `testIgnore: ['*_visual.spec.ts', 'probers/**']` to prevent
prober specs from being discovered by the regular CI test run, since
Playwright searches `testDir` recursively.
* **`fullyParallel: false`** — Tests run sequentially to avoid hammering the
live WikiTree API and CORS proxy with concurrent requests, which could
trigger rate-limiting.
* **`retries: 2`** — The WikiTree API and CORS proxy can have transient
- **No `webServer`** — The existing config starts a Vite dev/preview server on
`localhost:3000`. The prober config does not use Playwright's `webServer`
feature. Live-URL probers navigate to full absolute URLs; the Docker prober's
workflow starts the container externally (via `docker run`) before the test
runs, so Playwright connects to `localhost:8080` without a `webServer`
definition.
- **`testDir: './tests/probers'`** — Prober specs are isolated in their own
directory. Additionally, the existing `playwright.config.ts` e2e project must
add `testIgnore: ['*_visual.spec.ts', 'probers/**']` to prevent prober specs
from being discovered by the regular CI test run, since Playwright searches
`testDir` recursively.
- **`fullyParallel: false`** — Tests run sequentially to avoid hammering the
live WikiTree API and CORS proxy with concurrent requests, which could trigger
rate-limiting.
- **`retries: 2`** — The WikiTree API and CORS proxy can have transient
failures. Two retries (same as the existing CI config) provides a buffer
against flakiness without masking persistent failures.
* **`timeout: 120000`** — The WikiTree API prober makes multiple sequential
API calls (ancestors, descendants, relatives) that can take over 30
seconds under load. The default 30s timeout is too short for live API
probers; 120 seconds provides adequate headroom.
* **`reporter: [['html', {open: 'never'}], ['list']]`** — Generates an HTML
report for upload as a workflow artifact, plus list output for console
logs. Without this, no HTML report is produced and there is nothing to
upload.
* **`forbidOnly: true`** — Since probers always run in CI, `forbidOnly`
should be set to `true` to prevent `test.only` from accidentally blocking
all other prober specs. (The existing config uses `forbidOnly:
!!process.env.CI`, which achieves the same effect when `CI` is set, but
probers should enforce this unconditionally.)
* **Single project named `prober` using `devices['Desktop Chrome']`** — No
need for separate e2e/visual projects. All prober specs are smoke tests.
The project must explicitly use `devices['Desktop Chrome']` to ensure a
desktop viewport, because the side panel visibility depends on
`window.matchMedia('(max-width: 767px)')` (see the `getShowSidePanel` function in
`src/util/url_args.ts`).
Without an explicit device, Playwright's default viewport may be too
narrow, causing the side panel to be hidden and the `.details` assertion
to fail.
* **No `expect.toHaveScreenshot`** — Probers do not do visual regression
- **`timeout: 120000`** — The WikiTree API prober makes multiple sequential API
calls (ancestors, descendants, relatives) that can take over 30 seconds under
load. The default 30s timeout is too short for live API probers; 120 seconds
provides adequate headroom.
- **`reporter: [['html', {open: 'never'}], ['list']]`** — Generates an HTML
report for upload as a workflow artifact, plus list output for console logs.
Without this, no HTML report is produced and there is nothing to upload.
- **`forbidOnly: true`** — Since probers always run in CI, `forbidOnly` should
be set to `true` to prevent `test.only` from accidentally blocking all other
prober specs. (The existing config uses `forbidOnly: !!process.env.CI`, which
achieves the same effect when `CI` is set, but probers should enforce this
unconditionally.)
- **Single project named `prober` using `devices['Desktop Chrome']`** — No need
for separate e2e/visual projects. All prober specs are smoke tests. The
project must explicitly use `devices['Desktop Chrome']` to ensure a desktop
viewport, because the side panel visibility depends on
`window.matchMedia('(max-width: 767px)')` (see the `getShowSidePanel` function
in `src/util/url_args.ts`). Without an explicit device, Playwright's default
viewport may be too narrow, causing the side panel to be hidden and the
`.details` assertion to fail.
- **No `expect.toHaveScreenshot`** — Probers do not do visual regression
testing; that is handled by the existing visual test project.
* **`trace: 'on-first-retry'`, `screenshot: 'only-on-failure'`,
`video: 'on-first-retry'`** — For live-URL probers where failures are hard
to reproduce, trace files, failure screenshots, and retry videos are
essential for debugging.
* **`locale: 'en-US'`** — Forces consistent rendering and translation keys,
- **`trace: 'on-first-retry'`, `screenshot: 'only-on-failure'`,
`video: 'on-first-retry'`** — For live-URL probers where failures are hard to
reproduce, trace files, failure screenshots, and retry videos are essential
for debugging.
- **`locale: 'en-US'`** — Forces consistent rendering and translation keys,
matching the existing CI config. Without this, the app renders in the CI
runner's default locale, which is non-deterministic.
@@ -280,65 +278,65 @@ targets a different URL and asserts a different expected name.
**Create:** `tests/probers/wikitree.spec.ts`
* **Target URL:**
- **Target URL:**
`https://apps.wikitree.com/apps/wiech13/topola-viewer/#/view?source=wikitree&indi=Sk%C5%82odowska-2`
(URL-encoded `Skłodowska-2` to avoid encoding ambiguity with the non-ASCII
character `ł` in source code).
* **Expected name:** `Skłodowska` (from the WikiTree profile
`Skłodowska-2` Marie Skłodowska-Curie). The chart displays
`LastNameAtBirth`, which is `Skłodowska` for this profile.
* **What it exercises:** WikiTree direct API (no CORS proxy), WikiTree
- **Expected name:** `Skłodowska` (from the WikiTree profile `Skłodowska-2`
Marie Skłodowska-Curie). The chart displays `LastNameAtBirth`, which is
`Skłodowska` for this profile.
- **What it exercises:** WikiTree direct API (no CORS proxy), WikiTree
deployment.
* **Note:** Does not use `standalone=true` in the URL. The app defaults to
- **Note:** Does not use `standalone=true` in the URL. The app defaults to
standalone mode when not embedded and no static URL is set (see
`src/util/url_args.ts`).
**Create:** `tests/probers/gh-pages-gedcom.spec.ts`
* **Target URL:**
- **Target URL:**
`https://pewu.github.io/topola-viewer/#/view?url=https://raw.githubusercontent.com/PeWu/topola-viewer/master/src/datasource/testdata/test.ged&indi=I1`
* **Expected name:** `Bonifacy` (individual `@I1@` in `test.ged`, line 16:
- **Expected name:** `Bonifacy` (individual `@I1@` in `test.ged`, line 16:
`1 NAME Bonifacy /Gibbs/`).
* **What it exercises:** GitHub Pages deployment, CORS proxy
(`topolaproxy.bieda.it`), GEDCOM-from-URL loading. The app uses the CORS
proxy by default for GEDCOM URLs (`handleCors` defaults to `true` — see
- **What it exercises:** GitHub Pages deployment, CORS proxy
(`topolaproxy.bieda.it`), GEDCOM-from-URL loading. The app uses the CORS proxy
by default for GEDCOM URLs (`handleCors` defaults to `true` — see
`src/util/url_args.ts:156`).
**Create:** `tests/probers/wikitree-cors-gedcom.spec.ts`
* **Target URL:**
- **Target URL:**
`https://apps.wikitree.com/apps/wiech13/topola-viewer/#/view?url=https://raw.githubusercontent.com/PeWu/topola-viewer/master/src/datasource/testdata/test.ged&indi=I1`
* **Expected name:** `Bonifacy` (same as above).
* **What it exercises:** WikiTree deployment, CORS proxy from the WikiTree
domain. Even on `apps.wikitree.com`, GEDCOM-from-URL uses the CORS proxy
by default (the `handleCors` hostname check in `src/datasource/wikitree_api.ts`
- **Expected name:** `Bonifacy` (same as above).
- **What it exercises:** WikiTree deployment, CORS proxy from the WikiTree
domain. Even on `apps.wikitree.com`, GEDCOM-from-URL uses the CORS proxy by
default (the `handleCors` hostname check in `src/datasource/wikitree_api.ts`
only affects WikiTree API calls, not GEDCOM URL fetches in
`src/datasource/load_data.ts`). Note: probers do not block Google Analytics
scripts, so live-URL prober runs generate real analytics events on each
run. This is intentional — the prober tests the unmodified deployed app,
and blocking analytics would not reflect the real user experience.
scripts, so live-URL prober runs generate real analytics events on each run.
This is intentional — the prober tests the unmodified deployed app, and
blocking analytics would not reflect the real user experience.
**Create:** `tests/probers/docker.spec.ts`
* **Target URL:** `http://localhost:8080/` (local Docker container).
* **Expected name:** `Bonifacy` (same GEDCOM test file, mounted into the
- **Target URL:** `http://localhost:8080/` (local Docker container).
- **Expected name:** `Bonifacy` (same GEDCOM test file, mounted into the
container via `STATIC_URL=test.ged`).
* **What it exercises:** Published Docker image from GHCR (multi-stage
`Dockerfile` build output, Caddy server configuration, static URL
template injection (`{{ env "STATIC_URL" }}` in `index.html`)), and app
rendering with a pre-loaded GEDCOM.
* **Note:** The workflow pulls the Docker image published to GHCR
- **What it exercises:** Published Docker image from GHCR (multi-stage
`Dockerfile` build output, Caddy server configuration, static URL template
injection (`{{ env "STATIC_URL" }}` in `index.html`)), and app rendering with
a pre-loaded GEDCOM.
- **Note:** The workflow pulls the Docker image published to GHCR
(`ghcr.io/pewu/topola-viewer:latest`), runs it with
`docker run -p 8080:8080 -e STATIC_URL=test.ged`, mounts
`src/datasource/testdata/test.ged` into the container, and points
Playwright at `localhost:8080`. The app loads in non-standalone mode
(because `staticUrl` is set) and navigates directly to the chart view
(see `app.tsx` routing logic). The Docker image does not include Google
credentials (`VITE_GOOGLE_CLIENT_ID`, `VITE_GOOGLE_API_KEY`), so the
Google Drive integration is non-functional in the containerized app.
This is acceptable for the prober, which only tests chart rendering.
Note: the Docker prober tests the image published to GHCR by
`deploy-docker.yml`, ensuring the published artifact is functional.
`src/datasource/testdata/test.ged` into the container, and points Playwright
at `localhost:8080`. The app loads in non-standalone mode (because `staticUrl`
is set) and navigates directly to the chart view (see `app.tsx` routing
logic). The Docker image does not include Google credentials
(`VITE_GOOGLE_CLIENT_ID`, `VITE_GOOGLE_API_KEY`), so the Google Drive
integration is non-functional in the containerized app. This is acceptable for
the prober, which only tests chart rendering. Note: the Docker prober tests
the image published to GHCR by `deploy-docker.yml`, ensuring the published
artifact is functional.
**Shared test structure** (in `tests/probers/helpers.ts`, called by each spec):
@@ -368,40 +366,39 @@ Playwright's `getByTestId` searches the entire document, so it matches the
Selectors are derived from the source code:
* `#content` — main container, visible when chart state is `SHOWING_CHART`
(see the `renderMainArea` function in `src/pages/view_page.tsx`).
* `#chart` — SVG group inside the chart (see `src/chart.tsx`).
* `div.details` — side panel Details tab content (see the `Details` component in
- `#content` — main container, visible when chart state is `SHOWING_CHART` (see
the `renderMainArea` function in `src/pages/view_page.tsx`).
- `#chart` — SVG group inside the chart (see `src/chart.tsx`).
- `div.details` — side panel Details tab content (see the `Details` component in
`src/sidepanel/details/details.tsx`).
* `.ui.error.message` — fatal error replacing the chart (see
`src/components/error_display.tsx`, rendered when state is `ERROR`). The `ui` and
`message` classes are added by Semantic UI React's `<Message>`
component; the `error` class comes from the custom `className="error"`
prop in `ErrorMessage`. The resulting DOM element is
`<div class="ui negative message error">`, so the selector
`.ui.error.message` matches it.
* `.ui.errorPopup.message` — dismissable popup error (see
- `.ui.error.message` — fatal error replacing the chart (see
`src/components/error_display.tsx`, rendered when state is `ERROR`). The `ui`
and `message` classes are added by Semantic UI React's `<Message>` component;
the `error` class comes from the custom `className="error"` prop in
`ErrorMessage`. The resulting DOM element is
`<div class="ui negative message error">`, so the selector `.ui.error.message`
matches it.
- `.ui.errorPopup.message` — dismissable popup error (see
`src/components/error_display.tsx`). As above, `ui` and `message` come from
Semantic UI React's `<Message>`, and `errorPopup` comes from the
custom `className="errorPopup"` prop. Note: `ErrorPopup` uses Semantic
UI React's `<Portal>`, which renders its content at `document.body`
level, not inside `#content` in the DOM. When the popup is closed
(`open={false}`), the Portal renders nothing, so this assertion
verifies absence rather than visibility.
Semantic UI React's `<Message>`, and `errorPopup` comes from the custom
`className="errorPopup"` prop. Note: `ErrorPopup` uses Semantic UI React's
`<Portal>`, which renders its content at `document.body` level, not inside
`#content` in the DOM. When the popup is closed (`open={false}`), the Portal
renders nothing, so this assertion verifies absence rather than visibility.
The side panel is expanded by default on desktop viewports (the prober project
uses `devices['Desktop Chrome']`). The `getShowSidePanel` function in
`src/util/url_args.ts` returns `true` on non-mobile screens, so the `div.details`
container is visible without any URL parameters.
`src/util/url_args.ts` returns `true` on non-mobile screens, so the
`div.details` container is visible without any URL parameters.
All prober selectors use `data-testid` attributes (e.g., `data-testid="content"`,
`data-testid="chart"`, `data-testid="details"`, `data-testid="error-message"`,
`data-testid="error-popup"`) rather than CSS classes or element IDs. This makes
selectors resilient to CSS class refactors and Semantic UI React internal
changes. The `data-testid` attributes are added to the source components
alongside existing IDs and classes. A shared helper (`tests/probers/helpers.ts`)
encapsulates the prober flow and selector logic, eliminating duplication across
spec files.
All prober selectors use `data-testid` attributes (e.g.,
`data-testid="content"`, `data-testid="chart"`, `data-testid="details"`,
`data-testid="error-message"`, `data-testid="error-popup"`) rather than CSS
classes or element IDs. This makes selectors resilient to CSS class refactors
and Semantic UI React internal changes. The `data-testid` attributes are added
to the source components alongside existing IDs and classes. A shared helper
(`tests/probers/helpers.ts`) encapsulates the prober flow and selector logic,
eliminating duplication across spec files.
### Step 3: Prober GitHub Actions workflows
@@ -413,16 +410,15 @@ permissions:
actions: write
```
All prober workflows should use `actions/checkout@v4` (not v2, which is
used by some older deploy workflows).
All prober workflows should use `actions/checkout@v4` (not v2, which is used by
some older deploy workflows).
All prober workflows should set `timeout-minutes: 15` on each job to prevent
hanging runs from consuming runner minutes (default GitHub Actions timeout is
6 hours).
hanging runs from consuming runner minutes (default GitHub Actions timeout is 6
hours).
All prober workflows should define a `concurrency` group to prevent
overlapping runs (e.g., a deploy-triggered run overlapping with a
schedule-triggered run):
All prober workflows should define a `concurrency` group to prevent overlapping
runs (e.g., a deploy-triggered run overlapping with a schedule-triggered run):
```yaml
concurrency:
@@ -430,43 +426,41 @@ concurrency:
cancel-in-progress: false
```
`cancel-in-progress: false` ensures a deploy-triggered run is not cancelled
by a scheduled run — both complete independently.
`cancel-in-progress: false` ensures a deploy-triggered run is not cancelled by a
scheduled run — both complete independently.
Four reusable workflow files, one per prober. The three live-URL probers
are identical in structure — only the name and artifact name differ. The
Docker prober has a different structure (it builds and runs the container
before testing).
Four reusable workflow files, one per prober. The three live-URL probers are
identical in structure — only the name and artifact name differ. The Docker
prober has a different structure (it builds and runs the container before
testing).
**Create:** `.github/workflows/prober-wikitree.yml`
* **Triggers:** `workflow_call` (with `wait_for_propagation` input),
`workflow_dispatch` (with `wait_for_propagation` input), `schedule`
(daily at `0 5 * * *` UTC = ~6:00/7:00 CET).
* **Depends on (when called from deploy):** `deploy-wikitree-apps.yml`
only.
* **Artifact name:** `prober-report-wikitree`.
- **Triggers:** `workflow_call` (with `wait_for_propagation` input),
`workflow_dispatch` (with `wait_for_propagation` input), `schedule` (daily at
`0 5 * * *` UTC = ~6:00/7:00 CET).
- **Depends on (when called from deploy):** `deploy-wikitree-apps.yml` only.
- **Artifact name:** `prober-report-wikitree`.
**Create:** `.github/workflows/prober-gh-pages.yml`
* Same structure.
* **Depends on (when called from deploy):** `deploy-gh-pages.yml` only.
* **Artifact name:** `prober-report-gh-pages`.
- Same structure.
- **Depends on (when called from deploy):** `deploy-gh-pages.yml` only.
- **Artifact name:** `prober-report-gh-pages`.
**Create:** `.github/workflows/prober-wikitree-cors.yml`
* Same structure.
* **Depends on (when called from deploy):** `deploy-wikitree-apps.yml`
only.
* **Artifact name:** `prober-report-wikitree-cors`.
- Same structure.
- **Depends on (when called from deploy):** `deploy-wikitree-apps.yml` only.
- **Artifact name:** `prober-report-wikitree-cors`.
**Create:** `.github/workflows/prober-docker.yml`
* **Triggers:** `workflow_call`, `workflow_dispatch`, `schedule`
(daily at `0 5 * * *` UTC).
* **Depends on (when called from deploy):** `deploy-docker.yml` only.
* **Artifact name:** `prober-report-docker`.
* **No `wait_for_propagation` input** — The Docker container is available
- **Triggers:** `workflow_call`, `workflow_dispatch`, `schedule` (daily at
`0 5 * * *` UTC).
- **Depends on (when called from deploy):** `deploy-docker.yml` only.
- **Artifact name:** `prober-report-docker`.
- **No `wait_for_propagation` input** — The Docker container is available
immediately after startup; no propagation delay is needed.
**Shared workflow structure** (live-URL probers):
@@ -497,7 +491,7 @@ before testing).
**Docker prober workflow structure** (different from live-URL probers):
```
````
1. Checkout repository (actions/checkout@v4).
2. Pull Docker image: docker pull ghcr.io/pewu/topola-viewer:latest
(Pull the image published by deploy-docker.yml. This tests the actual
@@ -527,27 +521,26 @@ before testing).
sleep 1
done
curl -sf -o /dev/null http://localhost:8080/
```
The final `curl` ensures the workflow fails with a clear error if
the container never became ready. This prevents a race condition
where the test runs before Caddy is ready to serve requests. This
step runs after Node/Playwright setup so the container doesn't sit
idle during dependency installation.
The Docker spec also includes a guard that checks if localhost:8080
is reachable before running the prober. If the container is not
running (e.g., when running probers locally without Docker), the
test is skipped with a helpful message instead of failing with a
confusing ECONNREFUSED error.
10. Run: npx playwright test --config=playwright.prober.config.ts docker.spec.ts
11. Upload Playwright HTML report as artifact (if: always()). Set
`retention-days: 30` (same as live-URL probers).
12. Stop and remove container (if: always()): docker stop
topola-prober-${{ github.run_id }} 2>/dev/null || true; docker rm
topola-prober-${{ github.run_id }} 2>/dev/null || true
(The if: always() ensures cleanup runs even on failure. The
2>/dev/null and trailing true prevent errors if the container was
never started, e.g., pull failed at step 2.)
```
````
The final `curl` ensures the workflow fails with a clear error if the container
never became ready. This prevents a race condition where the test runs before
Caddy is ready to serve requests. This step runs after Node/Playwright setup so
the container doesn't sit idle during dependency installation. The Docker spec
also includes a guard that checks if localhost:8080 is reachable before running
the prober. If the container is not running (e.g., when running probers locally
without Docker), the test is skipped with a helpful message instead of failing
with a confusing ECONNREFUSED error. 10. Run: npx playwright test
--config=playwright.prober.config.ts docker.spec.ts 11. Upload Playwright HTML
report as artifact (if: always()). Set `retention-days: 30` (same as live-URL
probers). 12. Stop and remove container (if: always()): docker stop
topola-prober-${{ github.run_id }} 2>/dev/null || true; docker rm
topola-prober-${{ github.run_id }}
2>/dev/null || true (The if: always() ensures cleanup runs even on failure. The
2>/dev/null and trailing true prevent errors if the container was never started,
e.g., pull failed at step 2.)
````
**`wait_for_propagation` input flag** (live-URL probers only):
@@ -580,7 +573,7 @@ jobs:
deploy-docker:
uses: ./.github/workflows/deploy-docker.yml
secrets: inherit
```
````
After changes:
@@ -622,34 +615,34 @@ jobs:
Rationale for dependency mapping:
* `prober-wikitree` needs `deploy-wikitree-apps` — it tests the WikiTree
- `prober-wikitree` needs `deploy-wikitree-apps` — it tests the WikiTree
deployment.
* `prober-gh-pages` needs `deploy-gh-pages` — it tests the GitHub Pages
- `prober-gh-pages` needs `deploy-gh-pages` — it tests the GitHub Pages
deployment.
* `prober-wikitree-cors` needs `deploy-wikitree-apps` — it tests the
WikiTree deployment (with CORS proxy).
* `prober-docker` needs `deploy-docker` — it tests the Docker image
published to GHCR by `deploy-docker.yml` (Dockerfile, Caddy config, app
startup). It does not pass `wait_for_propagation` because the container is
available immediately after `docker run`.
* If a prober fails, the `deploy-everywhere` workflow is marked as failed
(red X), triggering an email notification (if GitHub email notifications
are enabled — see note in Section 2).
* Note: Individual deploy workflows (`deploy-gh-pages.yml`,
- `prober-wikitree-cors` needs `deploy-wikitree-apps` — it tests the WikiTree
deployment (with CORS proxy).
- `prober-docker` needs `deploy-docker` — it tests the Docker image published to
GHCR by `deploy-docker.yml` (Dockerfile, Caddy config, app startup). It does
not pass `wait_for_propagation` because the container is available immediately
after `docker run`.
- If a prober fails, the `deploy-everywhere` workflow is marked as failed (red
X), triggering an email notification (if GitHub email notifications are
enabled — see note in Section 2).
- Note: Individual deploy workflows (`deploy-gh-pages.yml`,
`deploy-wikitree-apps.yml`, `deploy-docker.yml`) also support
`workflow_dispatch`. If a deploy is triggered directly (instead of
through `deploy-everywhere.yml`), no probers run because probers are only
called from `deploy-everywhere.yml`. To ensure probers always run after a
deploy, always trigger deploys through `deploy-everywhere.yml`.
`workflow_dispatch`. If a deploy is triggered directly (instead of through
`deploy-everywhere.yml`), no probers run because probers are only called from
`deploy-everywhere.yml`. To ensure probers always run after a deploy, always
trigger deploys through `deploy-everywhere.yml`.
### Step 5: Update supporting files
**Modify:** `playwright.config.ts`
Add `testIgnore: ['*_visual.spec.ts', 'probers/**']` to the e2e project to
prevent prober specs in `tests/probers/` from being discovered by the regular
CI e2e test run. Without this, `npm run test:e2e` would try to execute
prober specs against the local dev server, causing failures.
prevent prober specs in `tests/probers/` from being discovered by the regular CI
e2e test run. Without this, `npm run test:e2e` would try to execute prober specs
against the local dev server, causing failures.
**Modify:** `package.json`
@@ -662,8 +655,7 @@ Add a `test:probers` script for running probers locally during development:
**Modify:** `tests/tsconfig.json`
Add `probers/` to the `include` array so prober specs are type-checked by
`tsc -p tests/tsconfig.json --noEmit` (which runs in CI via
`node.js.yml`).
`tsc -p tests/tsconfig.json --noEmit` (which runs in CI via `node.js.yml`).
Current state:
@@ -675,35 +667,34 @@ Current state:
```
The existing `./**/*.ts` glob already includes `tests/probers/` — no
modification needed. The `./**/*.d.ts` glob covers type declaration files
and does not affect prober spec discovery.
modification needed. The `./**/*.d.ts` glob covers type declaration files and
does not affect prober spec discovery.
**Modify:** `.github/workflows/README.md`
Add entries for the four new prober workflows to the file registry, e.g.:
```markdown
- [prober-wikitree.yml](prober-wikitree.yml): Reusable prober that
smoke-tests the WikiTree direct API path on the live WikiTree deployment.
Runs daily and after deploy.
- [prober-gh-pages.yml](prober-gh-pages.yml): Reusable prober that
smoke-tests the GitHub Pages deployment with GEDCOM-from-URL through the
CORS proxy. Runs daily and after deploy.
- [prober-wikitree-cors.yml](prober-wikitree-cors.yml): Reusable prober
that smoke-tests the CORS proxy from the WikiTree deployment with
GEDCOM-from-URL. Runs daily and after deploy.
- [prober-docker.yml](prober-docker.yml): Reusable prober that
smoke-tests the published Docker image from GHCR (Dockerfile, Caddy
config, app startup) by pulling and running it locally. Runs daily and
- [prober-wikitree.yml](prober-wikitree.yml): Reusable prober that smoke-tests
the WikiTree direct API path on the live WikiTree deployment. Runs daily and
after deploy.
- [prober-gh-pages.yml](prober-gh-pages.yml): Reusable prober that smoke-tests
the GitHub Pages deployment with GEDCOM-from-URL through the CORS proxy. Runs
daily and after deploy.
- [prober-wikitree-cors.yml](prober-wikitree-cors.yml): Reusable prober that
smoke-tests the CORS proxy from the WikiTree deployment with GEDCOM-from-URL.
Runs daily and after deploy.
- [prober-docker.yml](prober-docker.yml): Reusable prober that smoke-tests the
published Docker image from GHCR (Dockerfile, Caddy config, app startup) by
pulling and running it locally. Runs daily and after deploy.
```
**Create:** `tests/probers/README.md`
Document the prober test directory, explaining that these are live smoke
tests (not hermetic), how to run them locally (`npm run test:probers`), and
that they require network access to external services (WikiTree API, CORS
proxy, GitHub raw URLs).
Document the prober test directory, explaining that these are live smoke tests
(not hermetic), how to run them locally (`npm run test:probers`), and that they
require network access to external services (WikiTree API, CORS proxy, GitHub
raw URLs).
**Modify:** `PROJECT_STRUCTURE.md`
@@ -715,15 +706,15 @@ Add entries for the new `tests/probers/` directory and
Add an entry for this design document to the registry:
```markdown
* **[PROBERS_DESIGN.md](PROBERS_DESIGN.md)**: Live prober smoke tests
against deployed GitHub Pages, WikiTree URLs, and local Docker container,
covering WikiTree API, CORS proxy, GEDCOM-from-URL, and Docker build paths.
- **[PROBERS_DESIGN.md](PROBERS_DESIGN.md)**: Live prober smoke tests against
deployed GitHub Pages, WikiTree URLs, and local Docker container, covering
WikiTree API, CORS proxy, GEDCOM-from-URL, and Docker build paths.
```
### Summary of all files
| File | Action | Purpose |
|---|---|---|
| -------------------------------------------- | ------ | ------------------------------------------------------------------- |
| `playwright.prober.config.ts` | Create | Separate Playwright config for probers (no local server, live URLs) |
| `playwright.config.ts` | Modify | Add `testIgnore` for `probers/**` to e2e project |
| `package.json` | Modify | Add `test:probers` script |
@@ -747,28 +738,27 @@ Add an entry for this design document to the registry:
| `docs/README.md` | Modify | Add prober design doc to registry |
| `PROJECT_STRUCTURE.md` | Modify | Add prober directory and config file |
## 5. Future Considerations
### WikiTree Login Flow Prober
The current WikiTree prober tests the unauthenticated API path (loading a
public profile without an authcode). A future prober could test the
authenticated login flow — logging in with an authcode and verifying that
private profiles are accessible. This would require obtaining a dedicated
test account on wikitree.com and storing the authcode as a GitHub Actions
secret. This is deferred because it adds complexity (secret management,
authcode expiry, test account maintenance) and the unauthenticated path
already covers the most common deployment scenario.
The current WikiTree prober tests the unauthenticated API path (loading a public
profile without an authcode). A future prober could test the authenticated login
flow — logging in with an authcode and verifying that private profiles are
accessible. This would require obtaining a dedicated test account on
wikitree.com and storing the authcode as a GitHub Actions secret. This is
deferred because it adds complexity (secret management, authcode expiry, test
account maintenance) and the unauthenticated path already covers the most common
deployment scenario.
### Google Drive Integration Prober
A prober for the Google Drive integration (loading a GEDCOM file from
Google Drive) is not included. Google's OAuth flow is designed for human
interaction and includes bot detection (CAPTCHA, device verification) that
would likely prevent automated login. Additionally, the Google Drive
integration requires `VITE_GOOGLE_CLIENT_ID` and `VITE_GOOGLE_API_KEY`
secrets, which are not available in the prober environment. A possible
workaround would be to use a pre-authorized service account or a long-lived
refresh token stored as a secret, but this is complex and fragile. This is
deferred until a reliable automation approach is identified.
A prober for the Google Drive integration (loading a GEDCOM file from Google
Drive) is not included. Google's OAuth flow is designed for human interaction
and includes bot detection (CAPTCHA, device verification) that would likely
prevent automated login. Additionally, the Google Drive integration requires
`VITE_GOOGLE_CLIENT_ID` and `VITE_GOOGLE_API_KEY` secrets, which are not
available in the prober environment. A possible workaround would be to use a
pre-authorized service account or a long-lived refresh token stored as a secret,
but this is complex and fragile. This is deferred until a reliable automation
approach is identified.

View File

@@ -1,16 +1,32 @@
# Topola Viewer Feature Designs
This directory contains technical design documents for some of the new features and architectural updates of the Topola Viewer.
This directory contains technical design documents for some of the new features
and architectural updates of the Topola Viewer.
These documents are inspired by the design methodology described in the blog post [Elephants, Goldfish and the New Golden Age of Software Engineering](https://drensin.medium.com/elephants-goldfish-and-the-new-golden-age-of-software-engineering-c33641a48874) by Dave Rensin.
These documents are inspired by the design methodology described in the blog
post
[Elephants, Goldfish and the New Golden Age of Software Engineering](https://drensin.medium.com/elephants-goldfish-and-the-new-golden-age-of-software-engineering-c33641a48874)
by Dave Rensin.
While new features *may* follow this methodology to ensure robust design validation and context safety before writing code, doing so is optional.
While new features _may_ follow this methodology to ensure robust design
validation and context safety before writing code, doing so is optional.
## Design Documents Registry
* **[DOCKER_DESIGN.md](DOCKER_DESIGN.md)**: Docker container packaging, lightweight Caddy web server configuration, and GitHub Actions publishing pipelines.
* **[IMMEDIATE_FAMILY_SECTION_DESIGN.md](IMMEDIATE_FAMILY_SECTION_DESIGN.md)**: Side panel block consolidating parents, spouses, and children for efficient off-screen tree navigation.
* **[PLAYWRIGHT_DESIGN.md](PLAYWRIGHT_DESIGN.md)**: Playwright E2E testing architecture, Vite development/preview server lifecycle integration, tracking blocker interceptors, and embedded iframe communication.
* **[SCREENSHOT_TESTS_DESIGN.md](SCREENSHOT_TESTS_DESIGN.md)**: Pixel-perfect visual regression testing infrastructure, animation stabilization, sandbox environment/DOM sanitization, and isolated Playwright projects.
* **[WEBMCP_DESIGN.md](WEBMCP_DESIGN.md)**: Model Context Protocol (MCP) bridge and TS tool registration for AI agent interaction.
* **[PROBERS_DESIGN.md](PROBERS_DESIGN.md)**: Live prober smoke tests against deployed GitHub Pages, WikiTree URLs, and local Docker container, covering WikiTree API, CORS proxy, GEDCOM-from-URL, and Docker build paths.
- **[DOCKER_DESIGN.md](DOCKER_DESIGN.md)**: Docker container packaging,
lightweight Caddy web server configuration, and GitHub Actions publishing
pipelines.
- **[IMMEDIATE_FAMILY_SECTION_DESIGN.md](IMMEDIATE_FAMILY_SECTION_DESIGN.md)**:
Side panel block consolidating parents, spouses, and children for efficient
off-screen tree navigation.
- **[PLAYWRIGHT_DESIGN.md](PLAYWRIGHT_DESIGN.md)**: Playwright E2E testing
architecture, Vite development/preview server lifecycle integration, tracking
blocker interceptors, and embedded iframe communication.
- **[SCREENSHOT_TESTS_DESIGN.md](SCREENSHOT_TESTS_DESIGN.md)**: Pixel-perfect
visual regression testing infrastructure, animation stabilization, sandbox
environment/DOM sanitization, and isolated Playwright projects.
- **[WEBMCP_DESIGN.md](WEBMCP_DESIGN.md)**: Model Context Protocol (MCP) bridge
and TS tool registration for AI agent interaction.
- **[PROBERS_DESIGN.md](PROBERS_DESIGN.md)**: Live prober smoke tests against
deployed GitHub Pages, WikiTree URLs, and local Docker container, covering
WikiTree API, CORS proxy, GEDCOM-from-URL, and Docker build paths.

View File

@@ -2,142 +2,308 @@
## 1. Problem Statement
Topola Viewer is a highly interactive, visual genealogy exploration tool that renders family trees using complex SVG layouts and D3 configurations. As the codebase evolves, minor updates to CSS styles, React components, or underlying layout algorithms can easily introduce subtle visual regressions—such as overlapping text labels, misaligned parent-child connector lines, or broken formatting in the side panels—that standard text-based DOM tests cannot detect. To prevent these visual bugs from reaching production, we are introducing an automated screenshot (visual regression) testing suite using Playwright. This testing suite will capture pixel-perfect snapshots of critical interface states, automatically flag unintended visual changes, and guarantee a consistently polished, premium user experience across all releases.
Topola Viewer is a highly interactive, visual genealogy exploration tool that
renders family trees using complex SVG layouts and D3 configurations. As the
codebase evolves, minor updates to CSS styles, React components, or underlying
layout algorithms can easily introduce subtle visual regressions—such as
overlapping text labels, misaligned parent-child connector lines, or broken
formatting in the side panels—that standard text-based DOM tests cannot detect.
To prevent these visual bugs from reaching production, we are introducing an
automated screenshot (visual regression) testing suite using Playwright. This
testing suite will capture pixel-perfect snapshots of critical interface states,
automatically flag unintended visual changes, and guarantee a consistently
polished, premium user experience across all releases.
## 2. The Technical Plan
To consistently verify the user interface without introducing complex setups, the screenshot testing framework is built on a local-first, self-contained execution model. It operates by launching a virtual web browser, running the Topola Viewer application inside it, and checking it against stored master images (baselines).
To consistently verify the user interface without introducing complex setups,
the screenshot testing framework is built on a local-first, self-contained
execution model. It operates by launching a virtual web browser, running the
Topola Viewer application inside it, and checking it against stored master
images (baselines).
This setup consists of four major parts working in harmony:
1. **The Test Orchestrator (Playwright):** This acts as the central manager. It starts our local web server, launches virtual browser instances, automates user actions (such as clicking buttons or navigation links), captures the screenshots, and does the pixel-by-pixel comparison against our baseline master images.
2. **The Local Web Server:** A background web server hosting the Topola Viewer application code. It serves the frontend interface directly to the virtual browser so that the test context runs identically to our actual user deployments.
3. **The Network Traffic Controller (Route Interceptor):** An in-memory network router managed by the orchestrator. When the browser attempts to download a genealogy file (e.g., `family.ged`) or load a person's photo, the router intercepts that request and immediately answers it with tiny, predefined test datasets (fixtures). This guarantees that the test runs completely offline, remains blazingly fast, and has absolute visual predictability.
4. **The Environment Sanitizer:** A tiny automated script executed directly inside the browser window right before a screenshot is snapped. Its only job is to locate and overwrite dynamic or shifting text elements (like Git commit hashes or changelog dates) with fixed placeholders, ensuring they do not trigger false test failures.
1. **The Test Orchestrator (Playwright):** This acts as the central manager. It
starts our local web server, launches virtual browser instances, automates
user actions (such as clicking buttons or navigation links), captures the
screenshots, and does the pixel-by-pixel comparison against our baseline
master images.
2. **The Local Web Server:** A background web server hosting the Topola Viewer
application code. It serves the frontend interface directly to the virtual
browser so that the test context runs identically to our actual user
deployments.
3. **The Network Traffic Controller (Route Interceptor):** An in-memory network
router managed by the orchestrator. When the browser attempts to download a
genealogy file (e.g., `family.ged`) or load a person's photo, the router
intercepts that request and immediately answers it with tiny, predefined test
datasets (fixtures). This guarantees that the test runs completely offline,
remains blazingly fast, and has absolute visual predictability.
4. **The Environment Sanitizer:** A tiny automated script executed directly
inside the browser window right before a screenshot is snapped. Its only job
is to locate and overwrite dynamic or shifting text elements (like Git commit
hashes or changelog dates) with fixed placeholders, ensuring they do not
trigger false test failures.
## 3. Alternatives Considered & Rejected
To prevent future developer friction, avoid redundant debugging cycles, and establish firm design guardrails, the following technical alternatives were evaluated and rejected:
To prevent future developer friction, avoid redundant debugging cycles, and
establish firm design guardrails, the following technical alternatives were
evaluated and rejected:
### Alternative A: Global Animation Freezing (`freeze=true` Query Parameter)
* **Considered:** Forcing Topola's SVG engine to completely freeze all animations globally in E2E tests to prevent visual capturing mismatches.
* **Why Rejected:** Topola Viewer's initial chart mounting is entirely static; D3 transitions are only triggered during interactive navigation (e.g., clicking to shift focus to a child node). Since the target snapshots capture the initial mount of a chart or isolated panel element, introducing a complex global animation freezing hook is redundant. Instead, utilizing Playwright's standard auto-waiting mechanism (which pauses until the SVG is fully loaded and stationary) provides flawless, stable captures naturally.
- **Considered:** Forcing Topola's SVG engine to completely freeze all
animations globally in E2E tests to prevent visual capturing mismatches.
- **Why Rejected:** Topola Viewer's initial chart mounting is entirely static;
D3 transitions are only triggered during interactive navigation (e.g.,
clicking to shift focus to a child node). Since the target snapshots capture
the initial mount of a chart or isolated panel element, introducing a complex
global animation freezing hook is redundant. Instead, utilizing Playwright's
standard auto-waiting mechanism (which pauses until the SVG is fully loaded
and stationary) provides flawless, stable captures naturally.
### Alternative B: Monolithic Reference GEDCOM File (`rich_details.ged`)
* **Considered:** Maintaining a single, massive master `.ged` file containing a wide collection of custom individuals (with complex names, nested attributes, attached photos, and custom events) to serve all tests.
* **Why Rejected:** Monolithic test fixtures introduce severe coupling and high maintenance overhead. If a developer tweaks a birth record to debug an event-layout test, it can unintentionally shift elements in unrelated parts of the tree, failing baselines for name-formatting or photo rendering. Instead, creating microscopic, ad-hoc GEDCOM strings inline within each test case guarantees complete visual isolation, makes test intents instantly readable, and speeds up parsing.
- **Considered:** Maintaining a single, massive master `.ged` file containing a
wide collection of custom individuals (with complex names, nested attributes,
attached photos, and custom events) to serve all tests.
- **Why Rejected:** Monolithic test fixtures introduce severe coupling and high
maintenance overhead. If a developer tweaks a birth record to debug an
event-layout test, it can unintentionally shift elements in unrelated parts of
the tree, failing baselines for name-formatting or photo rendering. Instead,
creating microscopic, ad-hoc GEDCOM strings inline within each test case
guarantees complete visual isolation, makes test intents instantly readable,
and speeds up parsing.
### Alternative C: Build-Time Environment Variable Overrides (Git SHA/Time)
* **Considered:** Overriding `VITE_GIT_TIME` and `VITE_GIT_SHA` at build time specifically for E2E testing.
* **Why Rejected:** In production gating pipelines (such as GitHub Actions), the application is built and packaged into production-ready assets before the E2E job begins execution. Re-compiling Vite assets solely to inject static E2E values is slow, resource-intensive, and violates the rule of testing the exact binary that will be deployed. Instead, executing an in-browser DOM override (`page.evaluate`) right before screenshot execution is lightweight, self-contained, and requires zero alterations to the build flow or production bundle.
- **Considered:** Overriding `VITE_GIT_TIME` and `VITE_GIT_SHA` at build time
specifically for E2E testing.
- **Why Rejected:** In production gating pipelines (such as GitHub Actions), the
application is built and packaged into production-ready assets before the E2E
job begins execution. Re-compiling Vite assets solely to inject static E2E
values is slow, resource-intensive, and violates the rule of testing the exact
binary that will be deployed. Instead, executing an in-browser DOM override
(`page.evaluate`) right before screenshot execution is lightweight,
self-contained, and requires zero alterations to the build flow or production
bundle.
### Alternative D: Strict Pixel-Perfect Matching (Zero-Tolerance)
* **Considered:** Requiring absolute, 100% visual equivalence with zero pixel mismatch allowed.
* **Why Rejected:** Slight discrepancies in font rendering, subpixel antialiasing, and color blending are unavoidable across different operating systems (macOS developers vs. Linux CI agents). Enforcing zero-tolerance leads to extremely brittle tests that fail constantly due to harmless system-level rendering differences. Instead, setting relaxed thresholds (`maxDiffPixelRatio: 0.05` and `threshold: 0.2`) filters out system noise while aggressively catching genuine layout bugs, overlapping elements, and formatting failures.
- **Considered:** Requiring absolute, 100% visual equivalence with zero pixel
mismatch allowed.
- **Why Rejected:** Slight discrepancies in font rendering, subpixel
antialiasing, and color blending are unavoidable across different operating
systems (macOS developers vs. Linux CI agents). Enforcing zero-tolerance leads
to extremely brittle tests that fail constantly due to harmless system-level
rendering differences. Instead, setting relaxed thresholds
(`maxDiffPixelRatio: 0.05` and `threshold: 0.2`) filters out system noise
while aggressively catching genuine layout bugs, overlapping elements, and
formatting failures.
## 4. Detailed Implementation Plan
This section defines the granular, step-by-step implementation steps and enumerates every file that will be created or modified to complete this visual regression framework.
This section defines the granular, step-by-step implementation steps and
enumerates every file that will be created or modified to complete this visual
regression framework.
### A. Enumeration of Files
#### 1. Files to [MODIFY]
* **[playwright.config.ts](../playwright.config.ts)**
* *Rationale:* Isolate visual regression tests into a separate Playwright project (separate from standard functional E2E tests). This allows applying dedicated visual settings (like viewport locking, automatic scrollbar hiding, and custom screenshot mismatch thresholds) exclusively to visual tests without polluting standard E2E runs. Threshold settings are configured globally under `expect.toHaveScreenshot`.
* **[package.json](../package.json)**
* *Rationale:* Add dedicated npm script commands to target the standard E2E project (`--project=e2e`) and the isolated visual testing project (`--project=visual`), preventing slow screenshot tests from bloating standard developer verification cycles.
- **[playwright.config.ts](../playwright.config.ts)**
- _Rationale:_ Isolate visual regression tests into a separate Playwright
project (separate from standard functional E2E tests). This allows applying
dedicated visual settings (like viewport locking, automatic scrollbar
hiding, and custom screenshot mismatch thresholds) exclusively to visual
tests without polluting standard E2E runs. Threshold settings are configured
globally under `expect.toHaveScreenshot`.
- **[package.json](../package.json)**
- _Rationale:_ Add dedicated npm script commands to target the standard E2E
project (`--project=e2e`) and the isolated visual testing project
(`--project=visual`), preventing slow screenshot tests from bloating
standard developer verification cycles.
#### 2. Files to [NEW]
* **`tests/helpers.ts`**
* *Rationale:* Provide shared E2E/visual testing helper utilities. Features `setupHermeticEnvironment()` to abort external tracking requests and embed local fonts (ensuring offline hermetic execution) and `setupGedcomRoute()` to serve a standard mock `.ged` dataset.
* **`tests/intro_visual.spec.ts`**
* *Rationale:* Verify the landing page layout, copy block positions, and logo alignments. Employs an in-browser DOM script to overwrite dynamic footer versioning and dynamic changelog blocks prior to capture, ensuring baseline immunity.
* **`tests/charts_visual.spec.ts`**
* *Rationale:* Verify chart canvas boundaries, nodes, colors, and connections. Iterates over three of the supported layouts (`Hourglass`, `Relatives`, `Donatso`) using a simple tree, and captures screenshots of the stabilized D3 canvas.
* **`tests/details_visual.spec.ts`**
* *Rationale:* Verify details panel formats, image margins, fact headers, and sources. Defines tiny, ad-hoc mock GEDCOM inline strings for individual edge cases (long multi-part names, attached images, nested events) and serves pre-existing photo assets (e.g. `docker/examples/photos/photos/I1.jpg`) to render photos without broken image layouts.
* **`tests/config_visual.spec.ts`**
* *Rationale:* Verify the visual synchronization between Side Panel settings checkboxes/radio inputs and the SVG canvas. Captures full-viewport screenshots (at 1280x720) across the three curated configuration combinations.
- **`tests/helpers.ts`**
- _Rationale:_ Provide shared E2E/visual testing helper utilities. Features
`setupHermeticEnvironment()` to abort external tracking requests and embed
local fonts (ensuring offline hermetic execution) and `setupGedcomRoute()`
to serve a standard mock `.ged` dataset.
- **`tests/intro_visual.spec.ts`**
- _Rationale:_ Verify the landing page layout, copy block positions, and logo
alignments. Employs an in-browser DOM script to overwrite dynamic footer
versioning and dynamic changelog blocks prior to capture, ensuring baseline
immunity.
- **`tests/charts_visual.spec.ts`**
- _Rationale:_ Verify chart canvas boundaries, nodes, colors, and connections.
Iterates over three of the supported layouts (`Hourglass`, `Relatives`,
`Donatso`) using a simple tree, and captures screenshots of the stabilized
D3 canvas.
- **`tests/details_visual.spec.ts`**
- _Rationale:_ Verify details panel formats, image margins, fact headers, and
sources. Defines tiny, ad-hoc mock GEDCOM inline strings for individual edge
cases (long multi-part names, attached images, nested events) and serves
pre-existing photo assets (e.g. `docker/examples/photos/photos/I1.jpg`) to
render photos without broken image layouts.
- **`tests/config_visual.spec.ts`**
- _Rationale:_ Verify the visual synchronization between Side Panel settings
checkboxes/radio inputs and the SVG canvas. Captures full-viewport
screenshots (at 1280x720) across the three curated configuration
combinations.
### B. Step-by-Step Execution Plan
#### Step 1: Visual Project Isolation & Script Provisioning
1. Open `playwright.config.ts` and configure separate projects within the projects array:
* Define an `e2e` project using desktop Chrome settings that matches all `.spec.ts` files (`testMatch`) but excludes `*_visual.spec.ts` files (`testIgnore`).
* Define a dedicated `visual` project that matches only `*_visual.spec.ts` files (`testMatch`), and locks the browser viewport to a width of `1280` and height of `720` pixels in the `use` configuration.
2. Configure custom visual expectation thresholds globally under `expect.toHaveScreenshot` (specifically setting `maxDiffPixelRatio` to `0.05`, `threshold` to `0.2`, and `animations` to `'disabled'`).
3. Open `package.json` and update the scripts to target standard and visual projects respectively:
* `"test:e2e": "playwright test --project=e2e"` to run functional E2E tests exclusively.
* `"test:visual": "playwright test --project=visual"` to run visual regression tests exclusively.
* `"test:visual:update": "playwright test --project=visual --update-snapshots"` to automatically regenerate baseline reference files.
1. Open `playwright.config.ts` and configure separate projects within the
projects array:
- Define an `e2e` project using desktop Chrome settings that matches all
`.spec.ts` files (`testMatch`) but excludes `*_visual.spec.ts` files
(`testIgnore`).
- Define a dedicated `visual` project that matches only `*_visual.spec.ts`
files (`testMatch`), and locks the browser viewport to a width of `1280`
and height of `720` pixels in the `use` configuration.
2. Configure custom visual expectation thresholds globally under
`expect.toHaveScreenshot` (specifically setting `maxDiffPixelRatio` to
`0.05`, `threshold` to `0.2`, and `animations` to `'disabled'`).
3. Open `package.json` and update the scripts to target standard and visual
projects respectively:
- `"test:e2e": "playwright test --project=e2e"` to run functional E2E tests
exclusively.
- `"test:visual": "playwright test --project=visual"` to run visual
regression tests exclusively.
- `"test:visual:update": "playwright test --project=visual --update-snapshots"`
to automatically regenerate baseline reference files.
#### Step 2: Landing Page Visual Validation Spec (`tests/intro_visual.spec.ts`)
1. Define a test block marked with the `@visual` tag, utilizing `setupHermeticEnvironment` helper in `beforeEach`.
1. Define a test block marked with the `@visual` tag, utilizing
`setupHermeticEnvironment` helper in `beforeEach`.
2. Instruct the browser to navigate to the root path `/`.
3. Right before assertion, trigger `page.evaluate` to clean dynamic elements:
* Target the `.version` class element and set `.innerText = "version: 2026-01-01 00:00 (testcommit)"`.
* Target the changelog element (the container immediately following the "What's new" heading) and replace its HTML with a static placeholder change entry.
- Target the `.version` class element and set
`.innerText = "version: 2026-01-01 00:00 (testcommit)"`.
- Target the changelog element (the container immediately following the
"What's new" heading) and replace its HTML with a static placeholder change
entry.
4. Snap the screenshot using `expect(page).toHaveScreenshot('intro-page.png')`.
#### Step 3: Core SVG Canvas Layouts Spec (`tests/charts_visual.spec.ts`)
1. Set up a `beforeEach` block that initializes `setupGedcomRoute(context)` from `helpers.ts` to intercept `**/family.ged` requests and fulfill them with raw GEDCOM test data.
2. Write tests with the `@visual` tag iterating through the 3 supported layouts (`hourglass`, `relatives`, `donatso`):
* Set browser route to `/#/view?url=https://example.org/family.ged&view=[hourglass|relatives|donatso]`.
* Determine the appropriate container selector: `#dotatsoSvgContainer` if the view is `donatso`, otherwise `#svgContainer`.
* Locate the container element, and call `locator.waitFor()` to ensure the element is fully attached and visible.
* Wait for D3 rendering and layout stabilization using a brief layout-specific timeout (`waitTime`: `500ms` for hourglass/relatives, `1500ms` for donatso).
* Capture the isolated canvas screenshot: `expect(container).toHaveScreenshot('chart-[type].png')`.
1. Set up a `beforeEach` block that initializes `setupGedcomRoute(context)` from
`helpers.ts` to intercept `**/family.ged` requests and fulfill them with raw
GEDCOM test data.
2. Write tests with the `@visual` tag iterating through the 3 supported layouts
(`hourglass`, `relatives`, `donatso`):
- Set browser route to
`/#/view?url=https://example.org/family.ged&view=[hourglass|relatives|donatso]`.
- Determine the appropriate container selector: `#dotatsoSvgContainer` if the
view is `donatso`, otherwise `#svgContainer`.
- Locate the container element, and call `locator.waitFor()` to ensure the
element is fully attached and visible.
- Wait for D3 rendering and layout stabilization using a brief
layout-specific timeout (`waitTime`: `500ms` for hourglass/relatives,
`1500ms` for donatso).
- Capture the isolated canvas screenshot:
`expect(container).toHaveScreenshot('chart-[type].png')`.
#### Step 4: Details Panel Layouts Spec (`tests/details_visual.spec.ts`)
1. Set up a `beforeEach` block to establish hermetic routes via `setupHermeticEnvironment(context)`.
2. Define isolated test blocks with the `@visual` tag, each loading its own dedicated inline micro-GEDCOM dataset:
* **Complex Names Test:**
* Mock `**/family.ged` with a GEDCOM string containing prefix/suffix/rufname tags.
* Navigate to the view route with `sidePanel=true`, locate the side panel container `#sidebar`.
* Assert sidebar visual representation: `expect(page.locator('#sidebar')).toHaveScreenshot('details-complex-name.png')`.
* **Image / Photo Rendering Test:**
* Mock `**/family.ged` containing an `OBJE` tag pointing to a photo path (e.g. `photos/I1.jpg`).
* Intercept requests for `**/photos/I1.jpg` and fulfill the request by serving the project asset `docker/examples/photos/photos/I1.jpg`.
* Navigate, wait for the image load handler to complete (`img.waitFor({state: 'visible'})` and checking `image.complete` status).
* Assert sidebar visual representation: `expect(page.locator('#sidebar')).toHaveScreenshot('details-photo-render.png')`.
* **Custom Facts & Citations Test:**
* Mock `**/family.ged` containing complex nested fact (`FACT`), source (`SOUR`), and note (`NOTE`) trees.
* Select the individual and wait for `#sidebar` to load.
* Assert sidebar visual representation: `expect(page.locator('#sidebar')).toHaveScreenshot('details-events-sources.png')`.
* **Immediate Family Rendering Test:**
* Mock `**/family.ged` containing an individual with explicit parental links (`FAMC`) and multi-partner spousal families (`FAMS`) to display biological parents, spouses, and chronologically sorted children blocks.
* Select the individual and wait for `#sidebar` to load.
* Assert sidebar visual representation: `expect(page.locator('#sidebar')).toHaveScreenshot('details-immediate-family.png')`.
1. Set up a `beforeEach` block to establish hermetic routes via
`setupHermeticEnvironment(context)`.
2. Define isolated test blocks with the `@visual` tag, each loading its own
dedicated inline micro-GEDCOM dataset:
- **Complex Names Test:**
- Mock `**/family.ged` with a GEDCOM string containing
prefix/suffix/rufname tags.
- Navigate to the view route with `sidePanel=true`, locate the side panel
container `#sidebar`.
- Assert sidebar visual representation:
`expect(page.locator('#sidebar')).toHaveScreenshot('details-complex-name.png')`.
- **Image / Photo Rendering Test:**
- Mock `**/family.ged` containing an `OBJE` tag pointing to a photo path
(e.g. `photos/I1.jpg`).
- Intercept requests for `**/photos/I1.jpg` and fulfill the request by
serving the project asset `docker/examples/photos/photos/I1.jpg`.
- Navigate, wait for the image load handler to complete
(`img.waitFor({state: 'visible'})` and checking `image.complete` status).
- Assert sidebar visual representation:
`expect(page.locator('#sidebar')).toHaveScreenshot('details-photo-render.png')`.
- **Custom Facts & Citations Test:**
- Mock `**/family.ged` containing complex nested fact (`FACT`), source
(`SOUR`), and note (`NOTE`) trees.
- Select the individual and wait for `#sidebar` to load.
- Assert sidebar visual representation:
`expect(page.locator('#sidebar')).toHaveScreenshot('details-events-sources.png')`.
- **Immediate Family Rendering Test:**
- Mock `**/family.ged` containing an individual with explicit parental
links (`FAMC`) and multi-partner spousal families (`FAMS`) to display
biological parents, spouses, and chronologically sorted children blocks.
- Select the individual and wait for `#sidebar` to load.
- Assert sidebar visual representation:
`expect(page.locator('#sidebar')).toHaveScreenshot('details-immediate-family.png')`.
#### Step 5: Configurations Integration Spec (`tests/config_visual.spec.ts`)
1. Define a test block tagged `@visual` with a locked browser window viewport size of `1280x720` via `playwright.config.ts`.
2. In `beforeEach`, mock `**/family.ged` using `setupGedcomRoute(context)`, load `/view?sidePanel=true`, wait for `#sidebar` and `#content` to be visible, and click the "Settings" tab (`await page.getByText('Settings', {exact: true}).click();`) to expose config fields.
1. Define a test block tagged `@visual` with a locked browser window viewport
size of `1280x720` via `playwright.config.ts`.
2. In `beforeEach`, mock `**/family.ged` using `setupGedcomRoute(context)`, load
`/view?sidePanel=true`, wait for `#sidebar` and `#content` to be visible, and
click the "Settings" tab
(`await page.getByText('Settings', {exact: true}).click();`) to expose config
fields.
3. Assert the **Default Configuration (State 1)**:
* Verify that both the checkbox states and the corresponding generation-colored SVG boxes are in alignment.
* Assert the entire integrated screen: `expect(page).toHaveScreenshot('config-state-default.png')`.
4. Automate panel clicks: Scope locators using `page.locator('form.details .item')` to target the "Colors" and "IDs" section items. Select the "by sex" color radio button and select the "hide" IDs option.
5. Wait for updates (`page.waitForTimeout(300)`) and assert the **Sex Colors & No IDs Configuration (State 2)**:
* Assert the entire integrated screen: `expect(page).toHaveScreenshot('config-state-gender-no-ids.png')`.
6. Automate panel clicks: Scope locators using `page.locator('form.details .item')` to target the "Colors" and "Sex" section items. Select the "none" color radio button and select the "hide" sex option.
7. Wait for updates (`page.waitForTimeout(300)`) and assert the **Minimalist Configuration (State 3)**:
* Assert the entire integrated screen: `expect(page).toHaveScreenshot('config-state-minimalist.png')`.
- Verify that both the checkbox states and the corresponding
generation-colored SVG boxes are in alignment.
- Assert the entire integrated screen:
`expect(page).toHaveScreenshot('config-state-default.png')`.
4. Automate panel clicks: Scope locators using
`page.locator('form.details .item')` to target the "Colors" and "IDs" section
items. Select the "by sex" color radio button and select the "hide" IDs
option.
5. Wait for updates (`page.waitForTimeout(300)`) and assert the **Sex Colors &
No IDs Configuration (State 2)**:
- Assert the entire integrated screen:
`expect(page).toHaveScreenshot('config-state-gender-no-ids.png')`.
6. Automate panel clicks: Scope locators using
`page.locator('form.details .item')` to target the "Colors" and "Sex" section
items. Select the "none" color radio button and select the "hide" sex option.
7. Wait for updates (`page.waitForTimeout(300)`) and assert the **Minimalist
Configuration (State 3)**:
- Assert the entire integrated screen:
`expect(page).toHaveScreenshot('config-state-minimalist.png')`.
## 5. CI/CD Pipeline Integration
To ensure that no visual regressions are introduced into the master branch, the visual testing suite is integrated into the GitHub Actions CI/CD workflow ([node.js.yml](../.github/workflows/node.js.yml)) alongside existing tests.
To ensure that no visual regressions are introduced into the master branch, the
visual testing suite is integrated into the GitHub Actions CI/CD workflow
([node.js.yml](../.github/workflows/node.js.yml)) alongside existing tests.
### Pipeline Configuration
Visual tests run sequentially after standard E2E tests. The workflow executes the following steps:
1. **Install Dependencies:** Resolves Node.js package dependencies and installs/caches Playwright browser binaries (specifically `chromium`).
Visual tests run sequentially after standard E2E tests. The workflow executes
the following steps:
1. **Install Dependencies:** Resolves Node.js package dependencies and
installs/caches Playwright browser binaries (specifically `chromium`).
2. **Run E2E Tests:** Runs standard functional tests using `npm run test:e2e`.
3. **Run Visual Tests:** Runs visual regression tests using `npm run test:visual`.
3. **Run Visual Tests:** Runs visual regression tests using
`npm run test:visual`.
### Playwright HTML Reports in CI
To prevent test reports from overwriting each other, the HTML reports for the different testing suites are output to distinct subdirectories within the `playwright-report` folder:
* **E2E Tests:** Saved to `playwright-report/e2e` by setting the `PLAYWRIGHT_HTML_REPORT` environment variable.
* **Visual Tests:** Saved to `playwright-report/visual` by setting the `PLAYWRIGHT_HTML_REPORT` environment variable.
Both reports are bundled and uploaded as a single workflow artifact (`playwright-report-${{ matrix.node-version }}`) on completion, allowing easy review of failures.
To prevent test reports from overwriting each other, the HTML reports for the
different testing suites are output to distinct subdirectories within the
`playwright-report` folder:
- **E2E Tests:** Saved to `playwright-report/e2e` by setting the
`PLAYWRIGHT_HTML_REPORT` environment variable.
- **Visual Tests:** Saved to `playwright-report/visual` by setting the
`PLAYWRIGHT_HTML_REPORT` environment variable.
Both reports are bundled and uploaded as a single workflow artifact
(`playwright-report-${{ matrix.node-version }}`) on completion, allowing easy
review of failures.

View File

@@ -2,32 +2,102 @@
## Business Problem
When exploring large family trees in Topola Genealogy, users frequently need to locate specific individuals quickly using the search feature. Currently, accessing the search box requires moving the hand to the mouse or trackpad, navigating the cursor to the top bar, and clicking the input field, which breaks the flow of keyboard-driven navigation. This document proposes introducing a global keyboard shortcut (the `/` key) to instantly focus the search input, allowing users to search without manual mouse interaction. By streamlining this transition, the application provides a faster, more accessible, and premium keyboard-centric workflow for power users navigating complex genealogical data.
When exploring large family trees in Topola Genealogy, users frequently need to
locate specific individuals quickly using the search feature. Currently,
accessing the search box requires moving the hand to the mouse or trackpad,
navigating the cursor to the top bar, and clicking the input field, which breaks
the flow of keyboard-driven navigation. This document proposes introducing a
global keyboard shortcut (the `/` key) to instantly focus the search input,
allowing users to search without manual mouse interaction. By streamlining this
transition, the application provides a faster, more accessible, and premium
keyboard-centric workflow for power users navigating complex genealogical data.
---
## Technical Plan
The shortcut mechanism is designed to be lightweight, modular, and resilient against responsive layout duplication. Rather than introducing complex third-party shortcut libraries, the feature leverages standard web browser event handling, custom React hooks, standard accessibility attributes, and a centralized input registry.
The shortcut mechanism is designed to be lightweight, modular, and resilient
against responsive layout duplication. Rather than introducing complex
third-party shortcut libraries, the feature leverages standard web browser event
handling, custom React hooks, standard accessibility attributes, and a
centralized input registry.
### Major Components and Workflow
1. **The Encapsulated Hook (`useSearchShortcut`):** The keyboard shortcut logic is encapsulated within a custom hook `useSearchShortcut` (`src/menu/use_search_shortcut.ts`). Rather than being called by individual `SearchBar` components, the hook is invoked once at the layout level in `TopBar` (`src/menu/top_bar.tsx`). Individual `SearchBar` instances register their underlying `<input>` DOM elements in a centralized module-level registry (`registeredSearchInputs` `Set`). This ensures a single `window` `keydown` event listener manages focus across all responsive search bar instances.
2. **The Modifier Guard:** To prevent conflicts with system-level and browser-level shortcuts (such as `Cmd + /` for help or extensions, or `Ctrl + /` for toggling comments), the listener ignores keydown events with `Cmd/Meta`. It also ignores `Ctrl` or `Alt` unless both are pressed simultaneously (`Ctrl + Alt`), allowing `AltGr` combinations on international keyboards to function properly.
3. **The IME Composition & Default Prevented Guard:** To avoid hijacking keystrokes when international users are typing using an Input Method Editor (IME) for CJK (Chinese, Japanese, Korean) languages or when another event handler has already intercepted the keypress, the listener ignores keydown events when `event.isComposing === true` or `event.defaultPrevented === true`.
4. **The Smart Filter (Collision, Repeat & Modal Guards):** Before taking action, the listener verifies `event.key === '/'` and checks three conditions:
* **Collision Guard (`isTextEditable`):** It extracts the event path (supporting Shadow DOM encapsulation via `event.composedPath()`) and traverses all nodes in the path to check for `<input>`, `<textarea>`, `<select>`, `<button>`, elements with `contenteditable` active, or elements with `role="textbox"`, `role="searchbox"`, `role="spinbutton"`, or `role="combobox"`. This guarantees that typing `/` or pressing `/` while focused inside form controls, rich text editors, or ARIA fields does not trigger the shortcut.
* **Repeat Guard:** It checks if the key event is repeated (`event.repeat === true`). If so, it ignores it to prevent key-repeat cycles.
* **Modal Guard:** It inspects both the main `document` and the event target's `getRootNode()` for active modal dialogs (`dialog[open], .ui.modal.visible.active`) and ignores any modal with `aria-hidden="true"`. It verifies visibility (`checkVisibility() || getBoundingClientRect().width > 0`) without forcing synchronous layout recalculations (simple dropdown menus are not blocked). If an active visible modal is open, the shortcut is ignored to prevent stealing focus.
If all guards pass and a visible search input is found, the listener intercepts the keypress (using `event.preventDefault()`) and initiates the focus transition.
5. **The Centralized Search Input Registry:** Because `@artsy/fresnel` mounts both the desktop and mobile top bar menus in the DOM simultaneously, two separate instances of the `SearchBar` component exist concurrently.
To avoid duplicate IDs in the DOM, `id="search"` is removed from the component. When mounted, each `SearchBar` registers its underlying HTML `<input>` element in a module-level `Set` (`registeredSearchInputs`) via `registerSearchInput` / `unregisterSearchInput`. When the shortcut is triggered, `useSearchShortcut` iterates through the registered inputs and identifies the active instance by checking if it is visible in the viewport (`input.checkVisibility() || input.getBoundingClientRect().width > 0`). Once found, it focuses and selects it:
* Focus is requested with `{ preventScroll: true }` to avoid jarring page shifts.
* `select()` is called on the HTML `<input>` element to highlight all existing text, allowing the user to immediately overwrite it with a new query.
6. **The Translation Engine (Full Placeholder Localization):** To keep translation keys clean and avoid runtime string concatenation issues across different languages, two complete placeholder strings are defined: `"menu.search.placeholder"` (`"Search for people"`) and `"menu.search.placeholder_with_shortcut"` (`"Search for people (press '/')"`).
* On desktop (where `hideShortcutHint` is false or omitted), `SearchBar` uses `placeholder_with_shortcut`.
* On mobile (where `hideShortcutHint` is true), `SearchBar` uses `placeholder`, saving screen space on touch devices.
7. **Accessibility (a11y):** The search input is decorated with the standard `aria-keyshortcuts="/"` attribute to declare the global shortcut to screen readers and assistive technologies.
1. **The Encapsulated Hook (`useSearchShortcut`):** The keyboard shortcut logic
is encapsulated within a custom hook `useSearchShortcut`
(`src/menu/use_search_shortcut.ts`). Rather than being called by individual
`SearchBar` components, the hook is invoked once at the layout level in
`TopBar` (`src/menu/top_bar.tsx`). Individual `SearchBar` instances register
their underlying `<input>` DOM elements in a centralized module-level
registry (`registeredSearchInputs` `Set`). This ensures a single `window`
`keydown` event listener manages focus across all responsive search bar
instances.
2. **The Modifier Guard:** To prevent conflicts with system-level and
browser-level shortcuts (such as `Cmd + /` for help or extensions, or
`Ctrl + /` for toggling comments), the listener ignores keydown events with
`Cmd/Meta`. It also ignores `Ctrl` or `Alt` unless both are pressed
simultaneously (`Ctrl + Alt`), allowing `AltGr` combinations on
international keyboards to function properly.
3. **The IME Composition & Default Prevented Guard:** To avoid hijacking
keystrokes when international users are typing using an Input Method Editor
(IME) for CJK (Chinese, Japanese, Korean) languages or when another event
handler has already intercepted the keypress, the listener ignores keydown
events when `event.isComposing === true` or
`event.defaultPrevented === true`.
4. **The Smart Filter (Collision, Repeat & Modal Guards):** Before taking
action, the listener verifies `event.key === '/'` and checks three
conditions:
- **Collision Guard (`isTextEditable`):** It extracts the event path
(supporting Shadow DOM encapsulation via `event.composedPath()`) and
traverses all nodes in the path to check for `<input>`, `<textarea>`,
`<select>`, `<button>`, elements with `contenteditable` active, or
elements with `role="textbox"`, `role="searchbox"`, `role="spinbutton"`,
or `role="combobox"`. This guarantees that typing `/` or pressing `/`
while focused inside form controls, rich text editors, or ARIA fields does
not trigger the shortcut.
- **Repeat Guard:** It checks if the key event is repeated
(`event.repeat === true`). If so, it ignores it to prevent key-repeat
cycles.
- **Modal Guard:** It inspects both the main `document` and the event
target's `getRootNode()` for active modal dialogs
(`dialog[open], .ui.modal.visible.active`) and ignores any modal with
`aria-hidden="true"`. It verifies visibility
(`checkVisibility() || getBoundingClientRect().width > 0`) without forcing
synchronous layout recalculations (simple dropdown menus are not blocked).
If an active visible modal is open, the shortcut is ignored to prevent
stealing focus. If all guards pass and a visible search input is found,
the listener intercepts the keypress (using `event.preventDefault()`) and
initiates the focus transition.
5. **The Centralized Search Input Registry:** Because `@artsy/fresnel` mounts
both the desktop and mobile top bar menus in the DOM simultaneously, two
separate instances of the `SearchBar` component exist concurrently. To avoid
duplicate IDs in the DOM, `id="search"` is removed from the component. When
mounted, each `SearchBar` registers its underlying HTML `<input>` element in
a module-level `Set` (`registeredSearchInputs`) via `registerSearchInput` /
`unregisterSearchInput`. When the shortcut is triggered, `useSearchShortcut`
iterates through the registered inputs and identifies the active instance by
checking if it is visible in the viewport
(`input.checkVisibility() || input.getBoundingClientRect().width > 0`). Once
found, it focuses and selects it:
- Focus is requested with `{ preventScroll: true }` to avoid jarring page
shifts.
- `select()` is called on the HTML `<input>` element to highlight all
existing text, allowing the user to immediately overwrite it with a new
query.
6. **The Translation Engine (Full Placeholder Localization):** To keep
translation keys clean and avoid runtime string concatenation issues across
different languages, two complete placeholder strings are defined:
`"menu.search.placeholder"` (`"Search for people"`) and
`"menu.search.placeholder_with_shortcut"`
(`"Search for people (press '/')"`).
- On desktop (where `hideShortcutHint` is false or omitted), `SearchBar`
uses `placeholder_with_shortcut`.
- On mobile (where `hideShortcutHint` is true), `SearchBar` uses
`placeholder`, saving screen space on touch devices.
7. **Accessibility (a11y):** The search input is decorated with the standard
`aria-keyshortcuts="/"` attribute to declare the global shortcut to screen
readers and assistive technologies.
### Component Interaction Diagram
@@ -50,12 +120,24 @@ graph TD
## Alternatives Considered
### 1. Per-Component Event Listeners in `SearchBar`
* **Alternative:** Invoke `useSearchShortcut` inside each `SearchBar` instance so that each component attaches its own `window` `keydown` event listener.
* **Why Rejected:** Attaching multiple window event listeners concurrently (one for desktop, one for mobile) can lead to duplicate event handling or race conditions. Instead, `useSearchShortcut` is called once at the layout level in `TopBar`, while individual `SearchBar` components register their DOM `<input>` nodes in a shared module-level `Set` (`registeredSearchInputs`).
- **Alternative:** Invoke `useSearchShortcut` inside each `SearchBar` instance
so that each component attaches its own `window` `keydown` event listener.
- **Why Rejected:** Attaching multiple window event listeners concurrently (one
for desktop, one for mobile) can lead to duplicate event handling or race
conditions. Instead, `useSearchShortcut` is called once at the layout level in
`TopBar`, while individual `SearchBar` components register their DOM `<input>`
nodes in a shared module-level `Set` (`registeredSearchInputs`).
### 2. Using React Refs to Focus the Input
* **Alternative:** Pass a React `ref` to `<Search>` and call `focus()` or `getBoundingClientRect()` directly on it.
* **Why Rejected:** Semantic UI React's `Search` / `Input` components are class components or wrappers where refs do not consistently return the raw HTML `<input>` DOM node required for calling `select()`. Querying the wrapper element (`wrapperRef.current.querySelector('input')`) reliably targets the underlying HTML input element for registration.
- **Alternative:** Pass a React `ref` to `<Search>` and call `focus()` or
`getBoundingClientRect()` directly on it.
- **Why Rejected:** Semantic UI React's `Search` / `Input` components are class
components or wrappers where refs do not consistently return the raw HTML
`<input>` DOM node required for calling `select()`. Querying the wrapper
element (`wrapperRef.current.querySelector('input')`) reliably targets the
underlying HTML input element for registration.
---
@@ -64,34 +146,63 @@ graph TD
### Affected Files and Rationales
#### 1. [use_search_shortcut.ts](file:///home/pwiech/personal/github/topola-viewer/src/menu/use_search_shortcut.ts)
* **Rationale:** New custom hook and registry file. Implements `registerSearchInput` / `unregisterSearchInput` to manage mounted search input instances in a module-level `Set`. Implements `isTextEditable(event)` to guard against input hijacking across standard form controls (`<input>`, `<textarea>`, `<select>`, `<button>`) and ARIA text fields (`textbox`, `searchbox`, `spinbutton`, `combobox`) by traversing all nodes in the composed path. Implements `isModalActive(event)` to verify active visible modals (`dialog[open], .ui.modal.visible.active`) across main document and Shadow DOM roots (ignoring `aria-hidden="true"`). Implements `useSearchShortcut()` to attach a single window `keydown` listener (matching `event.key === '/'` with `AltGr` support), iterate registered inputs to identify the visible instance, intercept events with `preventDefault()`, and focus/select the search input.
- **Rationale:** New custom hook and registry file. Implements
`registerSearchInput` / `unregisterSearchInput` to manage mounted search input
instances in a module-level `Set`. Implements `isTextEditable(event)` to guard
against input hijacking across standard form controls (`<input>`,
`<textarea>`, `<select>`, `<button>`) and ARIA text fields (`textbox`,
`searchbox`, `spinbutton`, `combobox`) by traversing all nodes in the composed
path. Implements `isModalActive(event)` to verify active visible modals
(`dialog[open], .ui.modal.visible.active`) across main document and Shadow DOM
roots (ignoring `aria-hidden="true"`). Implements `useSearchShortcut()` to
attach a single window `keydown` listener (matching `event.key === '/'` with
`AltGr` support), iterate registered inputs to identify the visible instance,
intercept events with `preventDefault()`, and focus/select the search input.
#### 2. [search.tsx](file:///home/pwiech/personal/github/topola-viewer/src/menu/search.tsx)
* **Rationale:** Remove `id="search"` to comply with HTML specs. Register and unregister the underlying `<input>` DOM node in `registeredSearchInputs` upon mount/unmount. Select between `"menu.search.placeholder"` and `"menu.search.placeholder_with_shortcut"` based on the `hideShortcutHint` prop. Add `aria-keyshortcuts="/"` to the input shorthand.
- **Rationale:** Remove `id="search"` to comply with HTML specs. Register and
unregister the underlying `<input>` DOM node in `registeredSearchInputs` upon
mount/unmount. Select between `"menu.search.placeholder"` and
`"menu.search.placeholder_with_shortcut"` based on the `hideShortcutHint`
prop. Add `aria-keyshortcuts="/"` to the input shorthand.
#### 3. [top_bar.tsx](file:///home/pwiech/personal/github/topola-viewer/src/menu/top_bar.tsx)
* **Rationale:** Invoke `useSearchShortcut()` at the layout level. Pass `hideShortcutHint={true}` to the mobile `SearchBar` instance rendered inside `mobileMenus()`.
- **Rationale:** Invoke `useSearchShortcut()` at the layout level. Pass
`hideShortcutHint={true}` to the mobile `SearchBar` instance rendered inside
`mobileMenus()`.
#### 4. Localized Translation Files:
* [de.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/de.json)
* [pl.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/pl.json)
* [fr.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/fr.json)
* [it.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/it.json)
* [ru.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/ru.json)
* [bg.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/bg.json)
* [cs.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/cs.json)
* **Rationale:** Define `"menu.search.placeholder_with_shortcut"` with localized translations across all supported languages.
- [de.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/de.json)
- [pl.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/pl.json)
- [fr.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/fr.json)
- [it.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/it.json)
- [ru.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/ru.json)
- [bg.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/bg.json)
- [cs.json](file:///home/pwiech/personal/github/topola-viewer/src/translations/cs.json)
- **Rationale:** Define `"menu.search.placeholder_with_shortcut"` with localized
translations across all supported languages.
#### 5. [search.spec.ts](file:///home/pwiech/personal/github/topola-viewer/tests/search.spec.ts)
* **Rationale:** Playwright E2E test suite validating:
* Shortcut focus and text selection upon pressing `/`.
* Viewport switching where search query is preserved when resizing between desktop and mobile.
* Collision safety when typing `/` into active modal inputs (e.g., "Load from URL").
* Modal safety preventing focus theft when a modal is open and blurred.
* Form control protection preventing focus theft when focused on buttons.
* Modifier key exclusion (`Ctrl+/`, `Alt+/`, `Meta+/`).
* Landing page safety where no search bar is rendered.
* Question mark safety ensuring pressing `?` (`Shift+/`) does not trigger the shortcut.
* Dropdown menu safety allowing the shortcut to focus the search bar while a dropdown menu is open.
* Combobox wrapper interaction where clicking inside the search wrapper and pressing `/` focuses the input.
* AltGr compatibility ensuring international keyboard combinations (`Ctrl+Alt+/`) trigger the shortcut.
- **Rationale:** Playwright E2E test suite validating:
- Shortcut focus and text selection upon pressing `/`.
- Viewport switching where search query is preserved when resizing between
desktop and mobile.
- Collision safety when typing `/` into active modal inputs (e.g., "Load from
URL").
- Modal safety preventing focus theft when a modal is open and blurred.
- Form control protection preventing focus theft when focused on buttons.
- Modifier key exclusion (`Ctrl+/`, `Alt+/`, `Meta+/`).
- Landing page safety where no search bar is rendered.
- Question mark safety ensuring pressing `?` (`Shift+/`) does not trigger the
shortcut.
- Dropdown menu safety allowing the shortcut to focus the search bar while a
dropdown menu is open.
- Combobox wrapper interaction where clicking inside the search wrapper and
pressing `/` focuses the input.
- AltGr compatibility ensuring international keyboard combinations
(`Ctrl+Alt+/`) trigger the shortcut.

View File

@@ -2,186 +2,381 @@
## Problem
When users upload genealogy data using GEDCOM ZIP (GDZ) files or by selecting local images alongside a GEDCOM file, the images display correctly on the family tree chart nodes. However, these same photos, as well as any other uploaded document attachments, fail to render in the details side panel and events list. This occurs because the side panel reads directly from the raw GEDCOM entries, which only contain relative local file paths that the browser cannot resolve. The goal of this feature is to pass the mapped local object URLs to the details panel and resolve these paths dynamically, ensuring a consistent and complete viewing experience for all uploaded media.
When users upload genealogy data using GEDCOM ZIP (GDZ) files or by selecting
local images alongside a GEDCOM file, the images display correctly on the family
tree chart nodes. However, these same photos, as well as any other uploaded
document attachments, fail to render in the details side panel and events list.
This occurs because the side panel reads directly from the raw GEDCOM entries,
which only contain relative local file paths that the browser cannot resolve.
The goal of this feature is to pass the mapped local object URLs to the details
panel and resolve these paths dynamically, ensuring a consistent and complete
viewing experience for all uploaded media.
## The Technical Plan
To make uploaded images and files visible in the side panel, we will connect the loaded browser URL mapping to the components that display individual details and event lists.
To make uploaded images and files visible in the side panel, we will connect the
loaded browser URL mapping to the components that display individual details and
event lists.
The plan involves four major areas:
1. **Data Model Augmentation**: We will store the temporary file mapping (`images` map) in the main `TopolaData` container so it is available alongside the parsed GEDCOM data.
2. **Propagating the Mapping**: The main `App` component holds the loaded `TopolaData`. We will pass the `images` map as a prop to the `SidePanel` component, which will in turn pass it down to `Details` and `Events`.
3. **Resolving Relative Paths**: We will introduce a utility function that looks up relative paths or filenames in the `images` map. If a relative path is referenced in the GEDCOM, we will replace it with the resolved browser URL.
4. **Permissive File Extraction**: We will modify the parser utility to stop filtering out non-web paths (paths that do not start with `http`), allowing the application to process relative file paths.
1. **Data Model Augmentation**: We will store the temporary file mapping
(`images` map) in the main `TopolaData` container so it is available
alongside the parsed GEDCOM data.
2. **Propagating the Mapping**: The main `App` component holds the loaded
`TopolaData`. We will pass the `images` map as a prop to the `SidePanel`
component, which will in turn pass it down to `Details` and `Events`.
3. **Resolving Relative Paths**: We will introduce a utility function that looks
up relative paths or filenames in the `images` map. If a relative path is
referenced in the GEDCOM, we will replace it with the resolved browser URL.
4. **Permissive File Extraction**: We will modify the parser utility to stop
filtering out non-web paths (paths that do not start with `http`), allowing
the application to process relative file paths.
## Alternatives Considered
### 1. Pre-resolving URLs Directly in the Raw GEDCOM Data Model
We considered modifying the parsed `GedcomData` object to replace relative file paths with `blob:` URLs at load time, similar to how we pre-process `chartData`.
* **Why Rejected**: `GedcomData` is designed to be a raw, immutable representation of the source GEDCOM file structure. Directly mutating this tree could break features that expect raw tag data (such as exports, raw view logs, or metadata parsing). Keeping the `images` resolver map separate maintains a clean separation of concerns and preserves the integrity of the original GEDCOM entries.
We considered modifying the parsed `GedcomData` object to replace relative file
paths with `blob:` URLs at load time, similar to how we pre-process `chartData`.
- **Why Rejected**: `GedcomData` is designed to be a raw, immutable
representation of the source GEDCOM file structure. Directly mutating this
tree could break features that expect raw tag data (such as exports, raw view
logs, or metadata parsing). Keeping the `images` resolver map separate
maintains a clean separation of concerns and preserves the integrity of the
original GEDCOM entries.
### 2. Base64-Encoding and Persisting Images in Session Storage
We considered serializing the local file contents as Base64 strings so they could be saved in `sessionStorage` and survive page reloads.
* **Why Rejected**: Browser `sessionStorage` is typically limited to 5MB. Genealogy ZIP archives can easily contain tens or hundreds of megabytes of media files, which would instantly exceed this quota and cause the application to crash. The performance cost of encoding and decoding large files in session storage is also prohibitive.
We considered serializing the local file contents as Base64 strings so they
could be saved in `sessionStorage` and survive page reloads.
- **Why Rejected**: Browser `sessionStorage` is typically limited to 5MB.
Genealogy ZIP archives can easily contain tens or hundreds of megabytes of
media files, which would instantly exceed this quota and cause the application
to crash. The performance cost of encoding and decoding large files in session
storage is also prohibitive.
### 3. Rendering Placeholders for Unresolved Local Media References
We considered rendering warning cards or broken image placeholders for relative paths that cannot be resolved (i.e., when a GEDCOM lists a photo that wasn't included in the uploaded files/ZIP).
* **Why Rejected**: GEDCOM files created by desktop software often contain references to hundreds of local photos stored on the user's computer. Since users typically upload only the GEDCOM file (or a zip containing a subset of files), displaying error panels or placeholders for every missing image would clutter the UI. Returning `null` to gracefully hide missing media keeps the side panel clean and focused on available information.
We considered rendering warning cards or broken image placeholders for relative
paths that cannot be resolved (i.e., when a GEDCOM lists a photo that wasn't
included in the uploaded files/ZIP).
- **Why Rejected**: GEDCOM files created by desktop software often contain
references to hundreds of local photos stored on the user's computer. Since
users typically upload only the GEDCOM file (or a zip containing a subset of
files), displaying error panels or placeholders for every missing image would
clutter the UI. Returning `null` to gracefully hide missing media keeps the
side panel clean and focused on available information.
### 4. Storing both full relative path and base filename in the `images` map
We considered storing files under both their full path (e.g. `photos/person.jpg`) and their base filename (e.g. `person.jpg`) inside the `images` map to handle cases where the GEDCOM file does not preserve the folder name.
* **Why Rejected**: We assume the GEDCOM file references files preserving the folder name as exported by the source application. Storing both keys could also lead to collisions if two different folders contain files with the same name.
We considered storing files under both their full path (e.g.
`photos/person.jpg`) and their base filename (e.g. `person.jpg`) inside the
`images` map to handle cases where the GEDCOM file does not preserve the folder
name.
- **Why Rejected**: We assume the GEDCOM file references files preserving the
folder name as exported by the source application. Storing both keys could
also lead to collisions if two different folders contain files with the same
name.
## Detailed Implementation
This section lists every file that will be created or changed, the step-by-step changes, and the technical rationale.
This section lists every file that will be created or changed, the step-by-step
changes, and the technical rationale.
### 1. [gedcom_util.ts](../src/util/gedcom_util.ts)
#### Proposed Changes:
* Update `TopolaData` interface to add `images?: Map<string, string>;`.
* In `convertGedcom`, return the input `images` map in the output object.
* Remove `entry.data.startsWith('http')` check inside `findFileEntry` and ensure `entry.data` is a non-empty string before running predicate checks to avoid crashes.
* Add `.webp` to the `IMAGE_EXTENSIONS` list to support modern image formats.
* Update `isImageFile` to strip query parameters (`?`) and hashes (`#`) from the path/URL before verifying the extension.
* Centralize filename extraction inside `getFileName`: if `TITL` or `FORM` is missing, extract the filename from `fileEntry.data` (normalizing backslashes to forward slashes and stripping query parameters or hashes first).
* Implement and export `isBrowserLoadable(url: string): boolean` which returns `true` if the URL starts with `http://`, `https://`, `blob:`, `data:`, or `//`.
* Implement and export `resolveFileUrl(url: string, images?: Map<string, string>): string`. This function:
1. Returns the input URL immediately if it is browser-loadable (checked via `isBrowserLoadable`).
- Update `TopolaData` interface to add `images?: Map<string, string>;`.
- In `convertGedcom`, return the input `images` map in the output object.
- Remove `entry.data.startsWith('http')` check inside `findFileEntry` and ensure
`entry.data` is a non-empty string before running predicate checks to avoid
crashes.
- Add `.webp` to the `IMAGE_EXTENSIONS` list to support modern image formats.
- Update `isImageFile` to strip query parameters (`?`) and hashes (`#`) from the
path/URL before verifying the extension.
- Centralize filename extraction inside `getFileName`: if `TITL` or `FORM` is
missing, extract the filename from `fileEntry.data` (normalizing backslashes
to forward slashes and stripping query parameters or hashes first).
- Implement and export `isBrowserLoadable(url: string): boolean` which returns
`true` if the URL starts with `http://`, `https://`, `blob:`, `data:`, or
`//`.
- Implement and export
`resolveFileUrl(url: string, images?: Map<string, string>): string`. This
function:
1. Returns the input URL immediately if it is browser-loadable (checked via
`isBrowserLoadable`).
2. Replaces all backslashes (`\`) with forward slashes (`/`).
3. Checks the `images` map for the normalized lowercase path first, then for the lowercase base filename.
4. Returns the mapped `blob:` URL if found; otherwise, returns the original normalized URL.
* *Robustness check*: The function will safely check if `images` is a valid `Map` (using `images instanceof Map`) to avoid runtime errors when data is loaded from session storage.
* Refactor `getImageFileEntry` and `getNonImageFileEntry` to `getImageFileEntries` and `getNonImageFileEntries` to return all matching file entries within an `OBJE` tag, supporting multimedia objects with multiple file attachments.
* Update `filterImage` inside `gedcom_util.ts` to resolve the URL using the new `resolveFileUrl` helper, ensuring case-insensitive lookups for chart node images.
3. Checks the `images` map for the normalized lowercase path first, then for
the lowercase base filename.
4. Returns the mapped `blob:` URL if found; otherwise, returns the original
normalized URL.
- _Robustness check_: The function will safely check if `images` is a valid
`Map` (using `images instanceof Map`) to avoid runtime errors when data is
loaded from session storage.
- Refactor `getImageFileEntry` and `getNonImageFileEntry` to
`getImageFileEntries` and `getNonImageFileEntries` to return all matching file
entries within an `OBJE` tag, supporting multimedia objects with multiple file
attachments.
- Update `filterImage` inside `gedcom_util.ts` to resolve the URL using the new
`resolveFileUrl` helper, ensuring case-insensitive lookups for chart node
images.
#### Rationale:
Extending the model interface `TopolaData` binds the lifetime of the `images` map to the active loaded dataset. Removing `startsWith('http')` allows extracting relative paths. Centralizing the loadability check, URL decoding, case normalization, and base filename extraction helpers avoids code duplication. Supporting multiple file entries per `OBJE` record aligns with the GEDCOM multimedia specifications.
Extending the model interface `TopolaData` binds the lifetime of the `images`
map to the active loaded dataset. Removing `startsWith('http')` allows
extracting relative paths. Centralizing the loadability check, URL decoding,
case normalization, and base filename extraction helpers avoids code
duplication. Supporting multiple file entries per `OBJE` record aligns with the
GEDCOM multimedia specifications.
### 2. [load_data.ts](../src/datasource/load_data.ts)
#### Proposed Changes:
* In `prepareData`, pass the `images` map parameter to `convertGedcom` and return it inside the resulting data object.
* Ensure that the `images` map is *not* stored in `sessionStorage` by explicitly deleting or omitting the `images` key before calling `JSON.stringify(data)` in `prepareData`. This guarantees that `data.images` will be `undefined` (and not a plain empty object `{}`) when restored from `sessionStorage`.
* In `loadGedzip`, store the unzipped files in the `images` map using lowercase keys and normalizing backslashes to forward slashes to ensure case-insensitive lookup.
* Wrap the unzipping/extraction loop in `loadGedzip` in a `try-catch` block. If extraction fails or no GEDCOM file is found, revoke all `blob:` URLs created so far in the loop.
* Implement `try-catch` cleanup blocks in both `loadGedcom` and `loadFromUrl` to ensure that if file loading or parsing throws an error, all `blob:` URLs in the `images` map are immediately revoked to prevent memory leaks.
- In `prepareData`, pass the `images` map parameter to `convertGedcom` and
return it inside the resulting data object.
- Ensure that the `images` map is _not_ stored in `sessionStorage` by explicitly
deleting or omitting the `images` key before calling `JSON.stringify(data)` in
`prepareData`. This guarantees that `data.images` will be `undefined` (and not
a plain empty object `{}`) when restored from `sessionStorage`.
- In `loadGedzip`, store the unzipped files in the `images` map using lowercase
keys and normalizing backslashes to forward slashes to ensure case-insensitive
lookup.
- Wrap the unzipping/extraction loop in `loadGedzip` in a `try-catch` block. If
extraction fails or no GEDCOM file is found, revoke all `blob:` URLs created
so far in the loop.
- Implement `try-catch` cleanup blocks in both `loadGedcom` and `loadFromUrl` to
ensure that if file loading or parsing throws an error, all `blob:` URLs in
the `images` map are immediately revoked to prevent memory leaks.
#### Rationale:
Connects the ingestion pipeline to the `TopolaData` model. Deleting `images` prior to `sessionStorage` serialization prevents restoring a broken plain object `{}`. Adding try-catch blocks to cleanup `images` on loading or parsing failures prevents memory leaks from unrevoked `blob:` URLs.
Connects the ingestion pipeline to the `TopolaData` model. Deleting `images`
prior to `sessionStorage` serialization prevents restoring a broken plain object
`{}`. Adding try-catch blocks to cleanup `images` on loading or parsing failures
prevents memory leaks from unrevoked `blob:` URLs.
### 3. [side-panel.tsx](../src/sidepanel/side-panel.tsx)
#### Proposed Changes:
* Locate `<Details>` rendering block inside the `tabs` definition.
* Pass the map to the details panel: `images={data.images}`.
- Locate `<Details>` rendering block inside the `tabs` definition.
- Pass the map to the details panel: `images={data.images}`.
#### Rationale:
Acts as the intermediary component that forwards data properties from the root application state to the details view pane.
Acts as the intermediary component that forwards data properties from the root
application state to the details view pane.
### 4. [details.tsx](../src/sidepanel/details/details.tsx)
#### Proposed Changes:
* Update the `Props` interface for `Details` to accept `images?: Map<string, string>;`.
* Pass `props.images` to `imageDetails` and `fileDetails` (using inline arrow functions inside `getSectionForEachMatchingEntry` and `combineAllMatchingEntriesIntoSingleSection`), and `<Events ... images={props.images} />`.
* In `imageDetails`, retrieve all image file entries using `getImageFileEntries`. For each, resolve the URL using `resolveFileUrl(..., images)` and render `WrappedImage` with the resolved URL and its filename.
* In `fileDetails`, retrieve all non-image file entries using `getNonImageFileEntries`. For each, resolve the URL using `resolveFileUrl(..., images)` and pass the resolved URLs to `AdditionalFiles`.
- Update the `Props` interface for `Details` to accept
`images?: Map<string, string>;`.
- Pass `props.images` to `imageDetails` and `fileDetails` (using inline arrow
functions inside `getSectionForEachMatchingEntry` and
`combineAllMatchingEntriesIntoSingleSection`), and
`<Events ... images={props.images} />`.
- In `imageDetails`, retrieve all image file entries using
`getImageFileEntries`. For each, resolve the URL using
`resolveFileUrl(..., images)` and render `WrappedImage` with the resolved URL
and its filename.
- In `fileDetails`, retrieve all non-image file entries using
`getNonImageFileEntries`. For each, resolve the URL using
`resolveFileUrl(..., images)` and pass the resolved URLs to `AdditionalFiles`.
#### Rationale:
Directly resolves image and non-image document URLs from the loaded files list. Iterating over all resolved files from `getImageFileEntries` and `getNonImageFileEntries` supports multiple attachments per object record.
Directly resolves image and non-image document URLs from the loaded files list.
Iterating over all resolved files from `getImageFileEntries` and
`getNonImageFileEntries` supports multiple attachments per object record.
### 5. [events.tsx](../src/sidepanel/details/events.tsx)
#### Proposed Changes:
* Update the `Props` interface for `Events` to accept `images?: Map<string, string>;`.
* Pass `images` to `toEvent` -> `toIndiEvent` & `toFamilyEvents` -> `eventImages` & `eventFiles`.
* In `eventImages(entry, gedcom, images)`, extract all image files using `getImageFileEntries`. For each, resolve the URL using `resolveFileUrl` and add to the returned array.
* In `eventFiles(entry, gedcom, images)`, extract all non-image files using `getNonImageFileEntries`. For each, resolve the URL using `resolveFileUrl` and add to the returned array.
- Update the `Props` interface for `Events` to accept
`images?: Map<string, string>;`.
- Pass `images` to `toEvent` -> `toIndiEvent` & `toFamilyEvents` ->
`eventImages` & `eventFiles`.
- In `eventImages(entry, gedcom, images)`, extract all image files using
`getImageFileEntries`. For each, resolve the URL using `resolveFileUrl` and
add to the returned array.
- In `eventFiles(entry, gedcom, images)`, extract all non-image files using
`getNonImageFileEntries`. For each, resolve the URL using `resolveFileUrl` and
add to the returned array.
#### Rationale:
Allows life events to load, resolve, and render all associated media files, supporting multiple files and case-insensitive resolution.
Allows life events to load, resolve, and render all associated media files,
supporting multiple files and case-insensitive resolution.
### 6. [additional-files.tsx](../src/sidepanel/details/additional-files.tsx)
#### Proposed Changes:
* Map file extensions to Semantic UI icons (e.g. `file pdf outline` for PDF, `file image outline` for images, etc.) to improve the design aesthetics.
* Add the `download` attribute to the `<a>` tag for `blob:` URLs to trigger a file download instead of triggering browser security blocks on top-level navigations:
- Map file extensions to Semantic UI icons (e.g. `file pdf outline` for PDF,
`file image outline` for images, etc.) to improve the design aesthetics.
- Add the `download` attribute to the `<a>` tag for `blob:` URLs to trigger a
file download instead of triggering browser security blocks on top-level
navigations:
```typescript
download={file.url.startsWith('blob:') ? file.filename || true : undefined}
```
* Update `AdditionalFiles` component to check if the file is browser-loadable using `isBrowserLoadable(file.url)`. For non-loadable URLs:
- Update `AdditionalFiles` component to check if the file is browser-loadable
using `isBrowserLoadable(file.url)`. For non-loadable URLs:
- Do not render the `<a>` link (thus preventing download/navigation actions).
- Render the filename as grayed-out plain text with an italicized `(File not uploaded)` helper message.
- Render the filename as grayed-out plain text with an italicized
`(File not uploaded)` helper message.
#### Rationale:
Implements premium file icon layouts based on file types. Adding the `download` attribute is critical to bypass browser-level blocks on opening `blob:` URLs in new tabs. Checking loadability prevents users from navigating to broken local paths.
Implements premium file icon layouts based on file types. Adding the `download`
attribute is critical to bypass browser-level blocks on opening `blob:` URLs in
new tabs. Checking loadability prevents users from navigating to broken local
paths.
### 7. [wrapped-image.tsx](../src/sidepanel/details/wrapped-image.tsx)
#### Proposed Changes:
* Update `WrappedImage` to check if `props.url` is browser-loadable using `isBrowserLoadable`. If not, immediately render a clean gray card placeholder (displaying the image icon, the title or filename, and a "File not uploaded" badge) without rendering the `<img>` tag or trying to load the image.
- Update `WrappedImage` to check if `props.url` is browser-loadable using
`isBrowserLoadable`. If not, immediately render a clean gray card placeholder
(displaying the image icon, the title or filename, and a "File not uploaded"
badge) without rendering the `<img>` tag or trying to load the image.
#### Rationale:
Prevents browser 404/CORS console errors when attempting to load raw local relative paths as images, presenting a consistent and clean "File not uploaded" message to the user instead.
Prevents browser 404/CORS console errors when attempting to load raw local
relative paths as images, presenting a consistent and clean "File not uploaded"
message to the user instead.
### 8. [app.tsx](../src/app.tsx)
#### Proposed Changes:
* Add a `useEffect` cleanup hook in the `App` component that monitors changes to the active `data` state. When `data` changes, it iterates over the old `data.images` map and calls `URL.revokeObjectURL(url)` for all `blob:` URLs.
* In the path routing `useEffect` of the `App` component, if the path is not `/view`, explicitly reset the `data` state to `undefined`. This triggers the cleanup hook to revoke all active `blob:` URLs when leaving the chart viewer (e.g. going back to the landing page).
- Add a `useEffect` cleanup hook in the `App` component that monitors changes to
the active `data` state. When `data` changes, it iterates over the old
`data.images` map and calls `URL.revokeObjectURL(url)` for all `blob:` URLs.
- In the path routing `useEffect` of the `App` component, if the path is not
`/view`, explicitly reset the `data` state to `undefined`. This triggers the
cleanup hook to revoke all active `blob:` URLs when leaving the chart viewer
(e.g. going back to the landing page).
#### Rationale:
Prevents browser memory leaks from unrevoked object URLs when users upload and switch between different genealogy files, or navigate away from the viewer.
Prevents browser memory leaks from unrevoked object URLs when users upload and
switch between different genealogy files, or navigate away from the viewer.
### 9. [upload_menu.tsx](../src/menu/upload_menu.tsx)
#### Proposed Changes:
* Update the file input `accept` attribute to allow selecting `.webp` images.
* Remove the local `isImageFileName` helper function and import `isImageFile` from `gedcom_util.ts` to ensure consistent file extension support.
* Convert individually uploaded filenames to lowercase when storing them as keys in the `images` map to ensure case-insensitive matching:
- Update the file input `accept` attribute to allow selecting `.webp` images.
- Remove the local `isImageFileName` helper function and import `isImageFile`
from `gedcom_util.ts` to ensure consistent file extension support.
- Convert individually uploaded filenames to lowercase when storing them as keys
in the `images` map to ensure case-insensitive matching:
```typescript
images.set(file.name.toLowerCase(), URL.createObjectURL(file));
```
#### Rationale:
Extends modern image format support and unifies the image extension logic across the codebase. Storing keys as lowercase enables case-insensitive lookups for individually uploaded files.
Extends modern image format support and unifies the image extension logic across
the codebase. Storing keys as lowercase enables case-insensitive lookups for
individually uploaded files.
### 10. [Translation JSON Files](../src/translations/)
#### Proposed Changes:
* Add the localization key `"media.not_uploaded"` to all translation files (`bg.json`, `cs.json`, `de.json`, `fr.json`, `it.json`, `pl.json`, `ru.json`).
- Add the localization key `"media.not_uploaded"` to all translation files
(`bg.json`, `cs.json`, `de.json`, `fr.json`, `it.json`, `pl.json`, `ru.json`).
#### Rationale:
Ensures the new "File not uploaded" text badge is localized in all supported languages of the application.
Ensures the new "File not uploaded" text badge is localized in all supported
languages of the application.
## Verification Plan
We will verify the changes using automated unit tests, end-to-end regression checks, and manual visual testing.
We will verify the changes using automated unit tests, end-to-end regression
checks, and manual visual testing.
### Automated Unit Tests
We will run and add unit tests to ensure correct data resolution:
1. **Existing Test Verification**: Run `npm test` to verify all current unit tests pass.
2. **`resolveFileUrl` Unit Tests**: Add tests in `src/util/gedcom_util.spec.ts` to verify:
- Resolving a relative path that matches a key in the `images` map returns the correct `blob:` URL.
- Resolving a filename that matches a key in the `images` map returns the correct `blob:` URL.
- Resolving a URL that doesn't exist in the `images` map returns the original URL unchanged.
3. **`findFileEntry` Permissive Extract**: Add tests in `src/util/gedcom_util.spec.ts` to verify that `findFileEntry` matches and returns local relative `FILE` entries (e.g., `images/photo.jpg`), which were previously filtered out.
4. **Session cache recovery verification**: Add a test that verifies reloading the page correctly loads data from `sessionStorage` when `data.images` is `undefined`, ensuring no crashes occur.
1. **Existing Test Verification**: Run `npm test` to verify all current unit
tests pass.
2. **`resolveFileUrl` Unit Tests**: Add tests in `src/util/gedcom_util.spec.ts`
to verify:
- Resolving a relative path that matches a key in the `images` map returns
the correct `blob:` URL.
- Resolving a filename that matches a key in the `images` map returns the
correct `blob:` URL.
- Resolving a URL that doesn't exist in the `images` map returns the original
URL unchanged.
3. **`findFileEntry` Permissive Extract**: Add tests in
`src/util/gedcom_util.spec.ts` to verify that `findFileEntry` matches and
returns local relative `FILE` entries (e.g., `images/photo.jpg`), which were
previously filtered out.
4. **Session cache recovery verification**: Add a test that verifies reloading
the page correctly loads data from `sessionStorage` when `data.images` is
`undefined`, ensuring no crashes occur.
### Playwright Visual & E2E Tests
We will add automated verification of the upload flow and visual rendering:
1. **Add visual test**: Create a new test case in `tests/details_visual.spec.ts`:
1. **Add visual test**: Create a new test case in
`tests/details_visual.spec.ts`:
- Start at `/`.
- Setup Playwright's `filechooser` listener.
- Trigger the file upload menu, select and upload the `src/datasource/testdata/test.gdz` file.
- Trigger the file upload menu, select and upload the
`src/datasource/testdata/test.gdz` file.
- Click the node for the individual that contains the photo (e.g., Radobod).
- Wait for the side panel to open and the image to load.
- Match sidebar screenshot against a visual regression baseline (`details-uploaded-gdz-photo.png`).
2. **Add missing file placeholder test**: Verify that if a linked attachment is missing, a "File not uploaded" label or placeholder card renders in the details pane, matching a baseline screenshot.
- Match sidebar screenshot against a visual regression baseline
(`details-uploaded-gdz-photo.png`).
2. **Add missing file placeholder test**: Verify that if a linked attachment is
missing, a "File not uploaded" label or placeholder card renders in the
details pane, matching a baseline screenshot.
3. Run `npm run test:visual` to confirm tests execute and pass.
### Manual / Visual Verification
We will manually verify the visual rendering on a local development server:
1. Run `npm run dev` to launch the local application.
2. Prepare a test GDZ file: Zip the contents of the `docker/examples/photos` directory (which contains `family.ged` referencing relative photo paths under a `photos` subfolder).
2. Prepare a test GDZ file: Zip the contents of the `docker/examples/photos`
directory (which contains `family.ged` referencing relative photo paths under
a `photos` subfolder).
3. Upload the resulting GDZ file to the application.
4. Verify that:
- The primary individual's photo displays correctly in the side panel Details tab.
- Any documents or images linked to events (such as birth or marriage events) are rendered correctly in the event info cards.
- GEDCOM files referencing missing images render "File not uploaded" placeholders gracefully instead of breaking or showing broken image icons.
- The primary individual's photo displays correctly in the side panel Details
tab.
- Any documents or images linked to events (such as birth or marriage events)
are rendered correctly in the event info cards.
- GEDCOM files referencing missing images render "File not uploaded"
placeholders gracefully instead of breaking or showing broken image icons.
## Future Considerations
### IndexedDB for Upload Persistence
Currently, we reject persisting images in `sessionStorage` due to the 5MB size limit. In the future, we could consider using **IndexedDB** to store the zipped `.gdz` archives or the extracted file blobs. Since IndexedDB supports much larger storage quotas (typically hundreds of megabytes or more), this would allow uploaded genealogy trees and their media attachments to persist across page reloads and browser restarts, providing a much smoother user experience.
Currently, we reject persisting images in `sessionStorage` due to the 5MB size
limit. In the future, we could consider using **IndexedDB** to store the zipped
`.gdz` archives or the extracted file blobs. Since IndexedDB supports much
larger storage quotas (typically hundreds of megabytes or more), this would
allow uploaded genealogy trees and their media attachments to persist across
page reloads and browser restarts, providing a much smoother user experience.

View File

@@ -1,14 +1,33 @@
# WebMCP Interface Design
## The Problem
Exploring large genealogy charts can be overwhelming and time-consuming because users have to manually search, click, and scroll through hundreds of interconnected family branches just to find simple answers. To make this experience intuitive and modern, we are adding an interactive AI layer that acts as a research copilot directly inside the browser. This will allow users to effortlessly ask questions like "how is John related to Mary" or command the map to focus on specific relatives using simple natural conversation. Ultimately, this makes genealogy research accessible to everyone, letting users engage with their ancestry without wrestling with complex navigation controls.
Exploring large genealogy charts can be overwhelming and time-consuming because
users have to manually search, click, and scroll through hundreds of
interconnected family branches just to find simple answers. To make this
experience intuitive and modern, we are adding an interactive AI layer that acts
as a research copilot directly inside the browser. This will allow users to
effortlessly ask questions like "how is John related to Mary" or command the map
to focus on specific relatives using simple natural conversation. Ultimately,
this makes genealogy research accessible to everyone, letting users engage with
their ancestry without wrestling with complex navigation controls.
## System Architecture (How it works)
To make this feature work, we bridge three simple components together to let the map and the Assistant communicate smoothly:
1. **The Visual Chart (Topola core):** This is what you see on your screen. It draws the family members, sets up transitions, and tracks who you are currently looking at.
2. **The Assistant Adapter (The Bridge):** A singleton instance class (`WebMcpBridge`) instantiated once in `App.tsx` and running in the background. It acts as a continuous translator, giving the assistant access to read and move the active user chart while preventing disconnected side variables and state memory leaks.
3. **The AI Command Registry (WebMCP):** The external plug that allows the browser AI to issue predefined commands (such as "Focus on Sarah" or "Get direct descendants") to the Assistant Adapter.
To make this feature work, we bridge three simple components together to let the
map and the Assistant communicate smoothly:
1. **The Visual Chart (Topola core):** This is what you see on your screen. It
draws the family members, sets up transitions, and tracks who you are
currently looking at.
2. **The Assistant Adapter (The Bridge):** A singleton instance class
(`WebMcpBridge`) instantiated once in `App.tsx` and running in the
background. It acts as a continuous translator, giving the assistant access
to read and move the active user chart while preventing disconnected side
variables and state memory leaks.
3. **The AI Command Registry (WebMCP):** The external plug that allows the
browser AI to issue predefined commands (such as "Focus on Sarah" or "Get
direct descendants") to the Assistant Adapter.
```mermaid
graph LR
@@ -17,100 +36,128 @@ graph LR
Bridge <--> Assistant["AI Browser Assistant"]
```
This setup lets the AI control the tree smoothly without rebuilding the viewer from scratch. It behaves much like a second pair of hands working beside you on the same controls.
This setup lets the AI control the tree smoothly without rebuilding the viewer
from scratch. It behaves much like a second pair of hands working beside you on
the same controls.
## Rejected Alternatives (Design Guardrails)
To ensure consistent development and avoid repeating past defaults, here are the alternate designs considered but discarded during early setup:
* **Pushing real-time events for layout clicks:** We evaluated building a reactive push model that constantly updates the AI on every user click in real-time. We rejected this in favor of passive standard queries (`get_selected_person()`) because constant pushes can confuse the assistant and bloat the UI event stream.
* **A single combined Focus & Details tool:** We considered having one command perform both view manipulation and metadata inspection. We split these into isolated fetch commands (`inspect_indi`) and camera viewport shifts (`focus_indi`) so reading relatives stays fast and doesn't accidentally jerk the user's screen viewpoint.
* **Relying solely on recursive tool loops for deep trees:** Initially, direct single-node queries were considered enough for relationships. We rejected leaving the AI to fetch single nodes repeatedly in favor of generational acceleration commands (`get_ancestors` bounded to 5 generations ceiling) to protect interactive performance latency.
* **Internal fuzzy date parser algorithms:** We decided against writing standard regex parser logic for partial or approximate genealogy records (e.g., `"ABT 1750"`, `"BEFORE 1800"`). Instead, raw in-flight text string dumps allow standard conversational LLMs to contextualize approximation by themselves.
* **Module-scoped global state variables:** Rejected in favor of a single static bridge instance (Singleton pattern) initialized inside the React application frame. This prevents loose standard closures and allows isolated updates for new file uploads.
* **Custom synchronous DOM events for state queries:** Considered dispatching events from tools and capturing them inside React components. Rejected due to overhead constraints and event emitter latency.
To ensure consistent development and avoid repeating past defaults, here are the
alternate designs considered but discarded during early setup:
- **Pushing real-time events for layout clicks:** We evaluated building a
reactive push model that constantly updates the AI on every user click in
real-time. We rejected this in favor of passive standard queries
(`get_selected_person()`) because constant pushes can confuse the assistant
and bloat the UI event stream.
- **A single combined Focus & Details tool:** We considered having one command
perform both view manipulation and metadata inspection. We split these into
isolated fetch commands (`inspect_indi`) and camera viewport shifts
(`focus_indi`) so reading relatives stays fast and doesn't accidentally jerk
the user's screen viewpoint.
- **Relying solely on recursive tool loops for deep trees:** Initially, direct
single-node queries were considered enough for relationships. We rejected
leaving the AI to fetch single nodes repeatedly in favor of generational
acceleration commands (`get_ancestors` bounded to 5 generations ceiling) to
protect interactive performance latency.
- **Internal fuzzy date parser algorithms:** We decided against writing standard
regex parser logic for partial or approximate genealogy records (e.g.,
`"ABT 1750"`, `"BEFORE 1800"`). Instead, raw in-flight text string dumps allow
standard conversational LLMs to contextualize approximation by themselves.
- **Module-scoped global state variables:** Rejected in favor of a single static
bridge instance (Singleton pattern) initialized inside the React application
frame. This prevents loose standard closures and allows isolated updates for
new file uploads.
- **Custom synchronous DOM events for state queries:** Considered dispatching
events from tools and capturing them inside React components. Rejected due to
overhead constraints and event emitter latency.
## Exposed MCP Tools
To enable smooth AI communication, the following tools are exposed to the LLM. Returned individual details are structured into three level tiers:
To enable smooth AI communication, the following tools are exposed to the LLM.
Returned individual details are structured into three level tiers:
- **`IndiReference`:**
* **`IndiReference`:**
```json
{
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" }
"id": {"type": "string"},
"name": {"type": "string"}
},
"required": ["id", "name"]
}
```
* **`BasicIndi`:**
- **`BasicIndi`:**
```json
{
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"id": {"type": "string"},
"name": {"type": "string"},
"birth": {
"type": "object",
"properties": {
"date": { "type": "string" },
"place": { "type": "string" }
"date": {"type": "string"},
"place": {"type": "string"}
}
},
"death": {
"type": "object",
"properties": {
"date": { "type": "string" },
"place": { "type": "string" }
"date": {"type": "string"},
"place": {"type": "string"}
}
},
"mother": { "$ref": "#/definitions/IndiReference" },
"father": { "$ref": "#/definitions/IndiReference" }
"mother": {"$ref": "#/definitions/IndiReference"},
"father": {"$ref": "#/definitions/IndiReference"}
},
"required": ["id", "name"]
}
```
* **`FullIndi`:**
- **`FullIndi`:**
```json
{
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"id": {"type": "string"},
"name": {"type": "string"},
"birth": {
"type": "object",
"properties": {
"date": { "type": "string" },
"place": { "type": "string" }
"date": {"type": "string"},
"place": {"type": "string"}
}
},
"death": {
"type": "object",
"properties": {
"date": { "type": "string" },
"place": { "type": "string" }
"date": {"type": "string"},
"place": {"type": "string"}
}
},
"mother": { "$ref": "#/definitions/BasicIndi" },
"father": { "$ref": "#/definitions/BasicIndi" },
"mother": {"$ref": "#/definitions/BasicIndi"},
"father": {"$ref": "#/definitions/BasicIndi"},
"children": {
"type": "array",
"items": { "$ref": "#/definitions/BasicIndi" }
"items": {"$ref": "#/definitions/BasicIndi"}
},
"spouses": {
"type": "array",
"items": {
"type": "object",
"properties": {
"spouse": { "$ref": "#/definitions/BasicIndi" },
"spouse": {"$ref": "#/definitions/BasicIndi"},
"marriage": {
"type": "object",
"properties": {
"date": { "type": "string" },
"place": { "type": "string" }
"date": {"type": "string"},
"place": {"type": "string"}
}
}
},
@@ -123,187 +170,293 @@ To enable smooth AI communication, the following tools are exposed to the LLM. R
```
### 1. `get_selected_person`
Returns the individual currently selected in the browser viewport. This corresponds to the person displayed in the side panel, which is not necessarily the focused person.
* **Request Schema:**
Returns the individual currently selected in the browser viewport. This
corresponds to the person displayed in the side panel, which is not necessarily
the focused person.
- **Request Schema:**
```json
{ "type": "object", "properties": {} }
{"type": "object", "properties": {}}
```
* **Response Schema:** `FullIndi`
- **Response Schema:** `FullIndi`
### 2. `search_indi`
Searches the genealogy index for individuals by name. Returns at most 20 results to maintain fast performance and reasonable payload sizes.
* **Request Schema:**
Searches the genealogy index for individuals by name. Returns at most 20 results
to maintain fast performance and reasonable payload sizes.
- **Request Schema:**
```json
{
"type": "object",
"properties": {
"query": { "type": "string", "description": "The name of the person to search for." }
"query": {
"type": "string",
"description": "The name of the person to search for."
}
},
"required": ["query"]
}
```
* **Response Schema:** Array of `BasicIndi` (maximum 20 items).
- **Response Schema:** Array of `BasicIndi` (maximum 20 items).
### 3. `inspect_indi`
Fetches isolated detailed information for a specific individual by ID pointer.
* **Request Schema:**
- **Request Schema:**
```json
{
"type": "object",
"properties": {
"id": { "type": "string", "description": "The pointer ID of the individual." }
"id": {
"type": "string",
"description": "The pointer ID of the individual."
}
},
"required": ["id"]
}
```
* **Response Schema:** `FullIndi`
- **Response Schema:** `FullIndi`
### 4. `focus_indi`
Instructs the Topola viewer camera view to center on and focus a specific relative node. This will also update the side panel to show the focused person.
> [!NOTE]
> Changing focus prompts a full redesign layout sweep to center that person. This creates high UX layout jitter if the assistant uses it repetitively.
Instructs the Topola viewer camera view to center on and focus a specific
relative node. This will also update the side panel to show the focused person.
* **Request Schema:**
> [!NOTE] Changing focus prompts a full redesign layout sweep to center that
> person. This creates high UX layout jitter if the assistant uses it
> repetitively.
- **Request Schema:**
```json
{
"type": "object",
"properties": {
"id": { "type": "string", "description": "The pointer ID to focus." }
"id": {"type": "string", "description": "The pointer ID to focus."}
},
"required": ["id"]
}
```
* **Response Schema:**
- **Response Schema:**
```json
{ "type": "object", "properties": { "status": { "type": "string" } } }
{"type": "object", "properties": {"status": {"type": "string"}}}
```
### 5. `find_relationship_path`
Traverses the internal graph model to find relative step paths between two individuals. The following links should be traversed: parent, child, spouse, sibling.
* **Request Schema:**
Traverses the internal graph model to find relative step paths between two
individuals. The following links should be traversed: parent, child, spouse,
sibling.
- **Request Schema:**
```json
{
"type": "object",
"properties": {
"source": { "type": "string", "description": "Start individual ID pointer" },
"target": { "type": "string", "description": "End individual ID pointer" }
"source": {
"type": "string",
"description": "Start individual ID pointer"
},
"target": {"type": "string", "description": "End individual ID pointer"}
},
"required": ["source", "target"]
}
```
* **Response Schema:** Array of `BasicIndi` establishing the sequence.
- **Response Schema:** Array of `BasicIndi` establishing the sequence.
### 6. `get_ancestors`
Traverses upwards up to bounded ceiling generations.
* **Request Schema:**
- **Request Schema:**
```json
{
"type": "object",
"properties": {
"id": { "type": "string", "description": "Target individual ID" },
"generations": { "type": "number", "description": "Depth bound" }
"id": {"type": "string", "description": "Target individual ID"},
"generations": {"type": "number", "description": "Depth bound"}
},
"required": ["id"]
}
```
* **Response Schema:** Array of `BasicIndi`
- **Response Schema:** Array of `BasicIndi`
### 7. `get_descendants`
Traverses downwards up to bounded ceiling generations.
* **Request Schema:**
- **Request Schema:**
```json
{
"type": "object",
"properties": {
"id": { "type": "string", "description": "Target individual ID" },
"generations": { "type": "number", "description": "Depth bound" }
"id": {"type": "string", "description": "Target individual ID"},
"generations": {"type": "number", "description": "Depth bound"}
},
"required": ["id"]
}
```
* **Response Schema:** Array of `BasicIndi`
- **Response Schema:** Array of `BasicIndi`
## Constraints and Assumptions
* **Family Structure Assumptions:** The WebMCP integration assumes simplified family structures (e.g., single set of biological parents per individual). Multiple marriages are fully supported, consistent with the core Topola Viewer app design.
* **Privacy Boundaries:** The WebMCP tools must strictly follow the privacy constraints of the Topola Viewer. For instance, private profiles (such as WikiTree restricted profiles) must be filtered and hidden from the AI assistant and its tool responses.
* **Cycle Protection:** All graph traversal algorithms (e.g., finding ancestors and descendants) must implement internal cycle protection (e.g., tracking visited node pointers) to avoid endless iteration loops caused by standard pedigree collapse.
* **Legacy Replacement:** WebMCP tool creation must strictly overwrite and replace the existing tools previously defined in `src/webmcp.ts` to avoid duplicate hooks.
* **Relationship Pathfinder:** Topola viewer does not have prebuilt pathfinder operators. `find_relationship_path` must be manually implemented from scratch using a standard Breadth-First Search (BFS) algorithm traversing parent, child, spouse, and sibling links.
* **Index searching reuse:** The tool `search_indi` must borrow standard `buildSearchIndex` from `src/menu/search_index.ts` already powering the top search UI instead of deploying newly written independent fuzzy match loops.
* **Data Format Standards:** WebMCP tool integration consumes Topola's pre-parsed core JSON formats (`JsonGedcomData`, `JsonIndi`) instead of low-level raw GEDCOM pointer lines to enforce implementation consistency and operational efficiency.
- **Family Structure Assumptions:** The WebMCP integration assumes simplified
family structures (e.g., single set of biological parents per individual).
Multiple marriages are fully supported, consistent with the core Topola Viewer
app design.
- **Privacy Boundaries:** The WebMCP tools must strictly follow the privacy
constraints of the Topola Viewer. For instance, private profiles (such as
WikiTree restricted profiles) must be filtered and hidden from the AI
assistant and its tool responses.
- **Cycle Protection:** All graph traversal algorithms (e.g., finding ancestors
and descendants) must implement internal cycle protection (e.g., tracking
visited node pointers) to avoid endless iteration loops caused by standard
pedigree collapse.
- **Legacy Replacement:** WebMCP tool creation must strictly overwrite and
replace the existing tools previously defined in `src/webmcp.ts` to avoid
duplicate hooks.
- **Relationship Pathfinder:** Topola viewer does not have prebuilt pathfinder
operators. `find_relationship_path` must be manually implemented from scratch
using a standard Breadth-First Search (BFS) algorithm traversing parent,
child, spouse, and sibling links.
- **Index searching reuse:** The tool `search_indi` must borrow standard
`buildSearchIndex` from `src/menu/search_index.ts` already powering the top
search UI instead of deploying newly written independent fuzzy match loops.
- **Data Format Standards:** WebMCP tool integration consumes Topola's
pre-parsed core JSON formats (`JsonGedcomData`, `JsonIndi`) instead of
low-level raw GEDCOM pointer lines to enforce implementation consistency and
operational efficiency.
## Detailed Implementation Plan
This section lists the exact files to be created or modified to execute this design successfully.
This section lists the exact files to be created or modified to execute this
design successfully.
#### 1. [Modify] [webmcp.ts](../src/webmcp.ts)
* **Rationale:** Serves as the core integration plug for the experimental WebMCP browser assistant setup and standard operational in-memory state cache.
* **Action steps:**
* Define isolated state stores for current `selection`, `detailIndi`, and `loadedGedcomData`.
* Refactor current custom callbacks to register the complete tools collection blueprint (`search_indi`, `inspect_indi`, `focus_indi`, `get_ancestors`, `get_descendants`, `find_relationship_path`) into the `navigator.modelContext` array hook.
* Expose default standard state setters for Topola view adapter.
* Implement conversion and response helpers (`toMcpResponse`, `textMcpResponse`, `toBasicIndi`, `toFullIndi`) to standardise in-transit JSON streams.
- **Rationale:** Serves as the core integration plug for the experimental WebMCP
browser assistant setup and standard operational in-memory state cache.
- **Action steps:**
- Define isolated state stores for current `selection`, `detailIndi`, and
`loadedGedcomData`.
- Refactor current custom callbacks to register the complete tools collection
blueprint (`search_indi`, `inspect_indi`, `focus_indi`, `get_ancestors`,
`get_descendants`, `find_relationship_path`) into the
`navigator.modelContext` array hook.
- Expose default standard state setters for Topola view adapter.
- Implement conversion and response helpers (`toMcpResponse`,
`textMcpResponse`, `toBasicIndi`, `toFullIndi`) to standardise in-transit
JSON streams.
### 2. [NEW] [webmcp_definitions.ts](../src/webmcp_definitions.ts)
* **Rationale:** Keeps standard LLM tool definition blueprints separate from the execution bridge to avoid bloat and single interface monolithic designs.
- **Rationale:** Keeps standard LLM tool definition blueprints separate from the
execution bridge to avoid bloat and single interface monolithic designs.
### 3. [NEW] [webmcp_types.ts](../src/webmcp_types.ts)
* **Rationale:** Defines ambient `navigator.modelContext` parameters and concrete structural bridge types cleanly.
- **Rationale:** Defines ambient `navigator.modelContext` parameters and
concrete structural bridge types cleanly.
### 4. [Modify] [app.tsx](../src/app.tsx)
* **Rationale:** The top-level state component for Topola Viewer. It holds the interactive chart state and needs standard side effect hooks to update the WebMCP context on active selections.
* **Action steps:**
* Initialize WebMCP Bridge securely using `useState(() => new WebMcpBridge())` avoiding loose disconnected singleton memory leaks.
* Add standard `React.useEffect` hook to monitor active viewport selection changes and feed them into the WebMCP in-transit state.
* Expose selection and inspection callbacks handlers to the bridge hook preset.
- **Rationale:** The top-level state component for Topola Viewer. It holds the
interactive chart state and needs standard side effect hooks to update the
WebMCP context on active selections.
- **Action steps:**
- Initialize WebMCP Bridge securely using `useState(() => new WebMcpBridge())`
avoiding loose disconnected singleton memory leaks.
- Add standard `React.useEffect` hook to monitor active viewport selection
changes and feed them into the WebMCP in-transit state.
- Expose selection and inspection callbacks handlers to the bridge hook
preset.
### 5. [Modify] [gedcom_util.ts](../src/util/gedcom_util.ts)
* **Rationale:** Handles core conversion formulas from raw gedcom pointers to JSON objects. Houses newly proposed BFS algorithms avoiding visual rendering components dependency.
* **Action steps:**
* Implement standard Breadth-First Search (BFS) method for isolated `find_relationship_path` relative footprint.
* Draft flat array collection algorithms (bounded up to preset depth ceiling) for ancestors and descendants generation list.
- **Rationale:** Handles core conversion formulas from raw gedcom pointers to
JSON objects. Houses newly proposed BFS algorithms avoiding visual rendering
components dependency.
- **Action steps:**
- Implement standard Breadth-First Search (BFS) method for isolated
`find_relationship_path` relative footprint.
- Draft flat array collection algorithms (bounded up to preset depth ceiling)
for ancestors and descendants generation list.
### 6. [Modify] [gedcom_util.spec.ts](../src/util/gedcom_util.spec.ts)
* **Rationale:** Standard isolated unit test suite. It must accommodate boundary tests for newly added generic algorithms.
* **Action steps:**
* Add unit test cases for `find_relationship_path` with disconnected and connected multi relationships.
* Add test vectors for `get_ancestors` boundary ceilings (e.g., 5 generations) and cycles control.
- **Rationale:** Standard isolated unit test suite. It must accommodate boundary
tests for newly added generic algorithms.
- **Action steps:**
- Add unit test cases for `find_relationship_path` with disconnected and
connected multi relationships.
- Add test vectors for `get_ancestors` boundary ceilings (e.g., 5 generations)
and cycles control.
### 7. [New] [webmcp.cy.js](../cypress/e2e/webmcp.cy.js)
* **Rationale:** Formatted test files acting as automated integration coverage. Leverages Cypress stubs for isolated web tools inspection.
* **Action steps:**
* Mock `navigator.modelContext` using `cy.visit` on before preset lifecycle hooks.
* Trigger tool actions and check default DOM element shifts in simulated Topola frames.
- **Rationale:** Formatted test files acting as automated integration coverage.
Leverages Cypress stubs for isolated web tools inspection.
- **Action steps:**
- Mock `navigator.modelContext` using `cy.visit` on before preset lifecycle
hooks.
- Trigger tool actions and check default DOM element shifts in simulated
Topola frames.
## Testing Strategy
To ensure the robustness and correctness of the WebMCP integration, we will employ a multi-tiered testing approach spanning unit, integration, and manual end-to-end tests.
To ensure the robustness and correctness of the WebMCP integration, we will
employ a multi-tiered testing approach spanning unit, integration, and manual
end-to-end tests.
### Unit Tests
* **Graph Traversal Algorithms:**
* Test `find_relationship_path` with multiple scenarios:
* Direct descendants (e.g., Parent to Child).
* Sibling and cousin relationships.
* Pedigree collapse (cycles in the family tree).
* Unrelated individuals (should return an empty list or appropriate error).
* Test `get_ancestors` and `get_descendants` with generation bounds (e.g., limit = 5) and deep pedigree setups to verify the boundary ceilings and internal cycle protection.
* **Search indexing:** Test `search_indi` to verify it delegates correctly to the core search index and correctly limits the size of the response payload to at most 20 items.
- **Graph Traversal Algorithms:**
- Test `find_relationship_path` with multiple scenarios:
- Direct descendants (e.g., Parent to Child).
- Sibling and cousin relationships.
- Pedigree collapse (cycles in the family tree).
- Unrelated individuals (should return an empty list or appropriate error).
- Test `get_ancestors` and `get_descendants` with generation bounds (e.g.,
limit = 5) and deep pedigree setups to verify the boundary ceilings and
internal cycle protection.
- **Search indexing:** Test `search_indi` to verify it delegates correctly to
the core search index and correctly limits the size of the response payload to
at most 20 items.
### Integration Tests
* Standard UI integration tests are implemented using **Cypress**.
* The `navigator.modelContext` can be stubbed using Cypress `onBeforeLoad` hook to verify standard registration callbacks on application setup frame.
* The integration suite tests:
* Core tool callbacks correctly transition the React internal viewport selection.
* Application changes in selected person correctly propagate into in-transit operational state without rendering glitches.
- Standard UI integration tests are implemented using **Cypress**.
- The `navigator.modelContext` can be stubbed using Cypress `onBeforeLoad` hook
to verify standard registration callbacks on application setup frame.
- The integration suite tests:
- Core tool callbacks correctly transition the React internal viewport
selection.
- Application changes in selected person correctly propagate into in-transit
operational state without rendering glitches.
### Manual Verification
* Because interactive tools are bound to the experimental WebMCP protocol, manual verification can be accelerated using the **Model Context Tool Inspector Chrome Extension**. This grants operational developers a dashboard panel to trigger and fire tools independently inside standard dev viewports.
- Because interactive tools are bound to the experimental WebMCP protocol,
manual verification can be accelerated using the **Model Context Tool
Inspector Chrome Extension**. This grants operational developers a dashboard
panel to trigger and fire tools independently inside standard dev viewports.
### Files Created or Modified for Testing
* **[Modify] [gedcom_util.spec.ts](../src/util/gedcom_util.spec.ts)**
* **Rationale:** Contains existing unit tests for GEDCOM data structures. It will be extended to verify the newly introduced relationship finding and bounded graph traversal algorithms without visual overhead.
* **[New] [webmcp.cy.js](../cypress/e2e/webmcp.cy.js)**
* **Rationale:** Will act as the dedicated automated integration suite for the WebMCP feature. It will stub `navigator.modelContext` to verify correct tool registration and that standard execution callbacks successfully sync back layout and selection changes inside the Topola visual DOM.
- **[Modify] [gedcom_util.spec.ts](../src/util/gedcom_util.spec.ts)**
- **Rationale:** Contains existing unit tests for GEDCOM data structures. It
will be extended to verify the newly introduced relationship finding and
bounded graph traversal algorithms without visual overhead.
- **[New] [webmcp.cy.js](../cypress/e2e/webmcp.cy.js)**
- **Rationale:** Will act as the dedicated automated integration suite for the
WebMCP feature. It will stub `navigator.modelContext` to verify correct tool
registration and that standard execution callbacks successfully sync back
layout and selection changes inside the Topola visual DOM.
## Future Considerations
* **AI Canvas & Camera controls:** Exposing interactive UI commands such as canvas zoom and shifting the chart views (e.g., hourglass, donatso) could be added in future increments as additional tool blueprints.
- **AI Canvas & Camera controls:** Exposing interactive UI commands such as
canvas zoom and shifting the chart views (e.g., hourglass, donatso) could be
added in future increments as additional tool blueprints.

View File

@@ -91,7 +91,7 @@
"start": "vite",
"build": "tsc && vite build",
"test": "jest",
"prettier": "prettier --write \"{src,tests}/**/*.{ts,tsx,json}\"",
"prettier": "prettier --write \"{src,tests,docs}/**/*.{ts,tsx,json,md}\" \"*.md\"",
"lint": "eslint src tests --ext .ts,.tsx",
"predeploy": "npm run build",
"deploy": "gh-pages -d dist",

View File

@@ -2,7 +2,16 @@ const config = {
bracketSpacing: false,
endOfLine: 'lf',
singleQuote: true,
printWidth: 80,
plugins: ['prettier-plugin-organize-imports'],
overrides: [
{
files: '*.md',
options: {
proseWrap: 'always',
},
},
],
};
export default config;

View File

@@ -1,39 +1,65 @@
# Source Directory (`src`)
This directory contains the main source code for the Topola Viewer application. It includes the application entry point, main layout components, chart rendering logic, data source adapters, menus, side panel, translations, and utilities.
This directory contains the main source code for the Topola Viewer application.
It includes the application entry point, main layout components, chart rendering
logic, data source adapters, menus, side panel, translations, and utilities.
## Subdirectories
* **[datasource](datasource)**: Implements data sources for loading GEDCOM files, URLs, WikiTree API data, and embedded data.
* **[menu](menu)**: Contains components for the top navigation bar, search bar, and various menus (upload, URL, WikiTree).
* **[sidepanel](sidepanel)**: Contains components for the side panel, which shows individual details and chart configuration settings.
* **[translations](translations)**: Contains JSON files with translations for supported languages.
* **[util](util)**: Contains utility functions for dates, GEDCOM data, analytics, and responsive design.
- **[datasource](datasource)**: Implements data sources for loading GEDCOM
files, URLs, WikiTree API data, and embedded data.
- **[menu](menu)**: Contains components for the top navigation bar, search bar,
and various menus (upload, URL, WikiTree).
- **[sidepanel](sidepanel)**: Contains components for the side panel, which
shows individual details and chart configuration settings.
- **[translations](translations)**: Contains JSON files with translations for
supported languages.
- **[util](util)**: Contains utility functions for dates, GEDCOM data,
analytics, and responsive design.
## Files
* **[app.tsx](app.tsx)**: The main application component that manages state, routing, data loading orchestration, and high-level layout.
* **[changelog.tsx](changelog.tsx)**: Component that fetches and displays the recent changes in a modal dialog when the app is updated.
* **[chart.tsx](chart.tsx)**: The primary chart rendering component using D3 and the `topola` library. It handles zoom, pan, and export actions.
* **[donatso-chart.tsx](donatso-chart.tsx)**: An alternative chart view utilizing the `family-chart` library.
* **[index.tsx](index.tsx)**: The entry point of the application. It sets up polyfills, internationalization, and renders the app root.
* **[intro.tsx](intro.tsx)**: The landing page component shown when no data is loaded. It provides instructions, examples, and privacy information.
* **[webmcp.ts](webmcp.ts)**: Model Context Protocol (MCP) implementation to expose viewer state and action handlers for dynamic AI model interactions and tool definitions.
* **[webmcp_definitions.ts](webmcp_definitions.ts)**: Defines custom schemas and descriptions for various registration tools (such as person search, ancestry detail fetch, navigation triggers, etc.) exposed through the WebMCP protocol.
* **[webmcp_types.ts](webmcp_types.ts)**: Common TypeScript type annotations and structures used within the WebMCP engine layer.
- **[app.tsx](app.tsx)**: The main application component that manages state,
routing, data loading orchestration, and high-level layout.
- **[changelog.tsx](changelog.tsx)**: Component that fetches and displays the
recent changes in a modal dialog when the app is updated.
- **[chart.tsx](chart.tsx)**: The primary chart rendering component using D3 and
the `topola` library. It handles zoom, pan, and export actions.
- **[donatso-chart.tsx](donatso-chart.tsx)**: An alternative chart view
utilizing the `family-chart` library.
- **[index.tsx](index.tsx)**: The entry point of the application. It sets up
polyfills, internationalization, and renders the app root.
- **[intro.tsx](intro.tsx)**: The landing page component shown when no data is
loaded. It provides instructions, examples, and privacy information.
- **[webmcp.ts](webmcp.ts)**: Model Context Protocol (MCP) implementation to
expose viewer state and action handlers for dynamic AI model interactions and
tool definitions.
- **[webmcp_definitions.ts](webmcp_definitions.ts)**: Defines custom schemas and
descriptions for various registration tools (such as person search, ancestry
detail fetch, navigation triggers, etc.) exposed through the WebMCP protocol.
- **[webmcp_types.ts](webmcp_types.ts)**: Common TypeScript type annotations and
structures used within the WebMCP engine layer.
## Assets
* **[index.css](index.css)**: Global CSS styles for the application.
* **[topola.jpg](topola.jpg)**: Image asset for the Topola logo.
- **[index.css](index.css)**: Global CSS styles for the application.
- **[topola.jpg](topola.jpg)**: Image asset for the Topola logo.
## Type Definitions
* **[family-chart.d.ts](family-chart.d.ts)**: Type definitions for the `family-chart` library.
* **[imports.d.ts](imports.d.ts)**: Type definitions for non-code assets (e.g., CSS imports).
* **[javascript-natural-sort.d.ts](javascript-natural-sort.d.ts)**: Type definitions for the `javascript-natural-sort` library.
* **[lunr-languages.d.ts](lunr-languages.d.ts)**: Type definitions for `lunr-languages` plugins.
* **[parse-gedcom.d.ts](parse-gedcom.d.ts)**: Type definitions for the `parse-gedcom` library.
* **[react-app-env.d.ts](react-app-env.d.ts)**: Type definitions for React application environment.
* **[react-linkify.d.ts](react-linkify.d.ts)**: Type definitions for the `react-linkify` library.
* **[vite-env.d.ts](vite-env.d.ts)**: Type definitions for Vite environment variables.
- **[family-chart.d.ts](family-chart.d.ts)**: Type definitions for the
`family-chart` library.
- **[imports.d.ts](imports.d.ts)**: Type definitions for non-code assets (e.g.,
CSS imports).
- **[javascript-natural-sort.d.ts](javascript-natural-sort.d.ts)**: Type
definitions for the `javascript-natural-sort` library.
- **[lunr-languages.d.ts](lunr-languages.d.ts)**: Type definitions for
`lunr-languages` plugins.
- **[parse-gedcom.d.ts](parse-gedcom.d.ts)**: Type definitions for the
`parse-gedcom` library.
- **[react-app-env.d.ts](react-app-env.d.ts)**: Type definitions for React
application environment.
- **[react-linkify.d.ts](react-linkify.d.ts)**: Type definitions for the
`react-linkify` library.
- **[vite-env.d.ts](vite-env.d.ts)**: Type definitions for Vite environment
variables.

View File

@@ -1,19 +1,40 @@
# Data Sources
This directory contains the implementation of various data sources supported by Topola Viewer. It handles loading genealogical data from different origins and transforming it into a unified format used by the application.
This directory contains the implementation of various data sources supported by
Topola Viewer. It handles loading genealogical data from different origins and
transforming it into a unified format used by the application.
## Purpose
The main purpose of this directory is to abstract away the details of where the genealogical data comes from. Whether it is a file uploaded by the user, a URL pointing to a GEDCOM file, data fetched from the WikiTree API, or data received from a parent window in embedded mode, this directory provides a consistent interface ([DataSource](data_source.ts#L19)) to the rest of the application.
The main purpose of this directory is to abstract away the details of where the
genealogical data comes from. Whether it is a file uploaded by the user, a URL
pointing to a GEDCOM file, data fetched from the WikiTree API, or data received
from a parent window in embedded mode, this directory provides a consistent
interface ([DataSource](data_source.ts#L19)) to the rest of the application.
## Files
- [data_source.ts](data_source.ts): Defines the core abstractions for data sources, including the [DataSourceEnum](data_source.ts#L5) and the [DataSource](data_source.ts#L19) interface.
- [load_data.ts](load_data.ts): Contains helper functions for loading data from files and URLs, handling zip files (GEDZIP), and implements [UploadedDataSource](load_data.ts#L155) and [GedcomUrlDataSource](load_data.ts#L195).
- [embedded.ts](embedded.ts): Implements [EmbeddedDataSource](embedded.ts#L34) for cases where the viewer is embedded in an iframe and receives GEDCOM data via window messages.
- [gedcom_generator.ts](gedcom_generator.ts): Used to create a GEDCOM structure from internal JSON data. This is primarily used to generate a GEDCOM representation for data sources that do not natively provide it (like WikiTree), which is needed for the details panel.
- [wikitree.ts](wikitree.ts): Implements [WikiTreeDataSource](wikitree.ts#L62) and serves as the main entry point for loading data from WikiTree.
- [wikitree_api.ts](wikitree_api.ts): Handles the actual communication with the WikiTree API using the `wikitree-js` library, including caching and handling private profiles.
- [wikitree_transformer.ts](wikitree_transformer.ts): Transforms data fetched from the WikiTree API into Topola's internal data format.
- [load_data.spec.ts](load_data.spec.ts): Unit tests for the data loading functions in `load_data.ts`.
- [data_source.ts](data_source.ts): Defines the core abstractions for data
sources, including the [DataSourceEnum](data_source.ts#L5) and the
[DataSource](data_source.ts#L19) interface.
- [load_data.ts](load_data.ts): Contains helper functions for loading data from
files and URLs, handling zip files (GEDZIP), and implements
[UploadedDataSource](load_data.ts#L155) and
[GedcomUrlDataSource](load_data.ts#L195).
- [embedded.ts](embedded.ts): Implements [EmbeddedDataSource](embedded.ts#L34)
for cases where the viewer is embedded in an iframe and receives GEDCOM data
via window messages.
- [gedcom_generator.ts](gedcom_generator.ts): Used to create a GEDCOM structure
from internal JSON data. This is primarily used to generate a GEDCOM
representation for data sources that do not natively provide it (like
WikiTree), which is needed for the details panel.
- [wikitree.ts](wikitree.ts): Implements [WikiTreeDataSource](wikitree.ts#L62)
and serves as the main entry point for loading data from WikiTree.
- [wikitree_api.ts](wikitree_api.ts): Handles the actual communication with the
WikiTree API using the `wikitree-js` library, including caching and handling
private profiles.
- [wikitree_transformer.ts](wikitree_transformer.ts): Transforms data fetched
from the WikiTree API into Topola's internal data format.
- [load_data.spec.ts](load_data.spec.ts): Unit tests for the data loading
functions in `load_data.ts`.
- `testdata/`: A directory containing test files used by the unit tests.

View File

@@ -1,14 +1,27 @@
# Menu Directory
This directory contains React components that make up the top navigation bar and its associated menus and dialogs in Topola Viewer. It handles loading data from various sources, searching for individuals, and changing chart views.
This directory contains React components that make up the top navigation bar and
its associated menus and dialogs in Topola Viewer. It handles loading data from
various sources, searching for individuals, and changing chart views.
## Files
- [menu_item.tsx](menu_item.tsx): A utility component that renders as either a `Menu.Item` or a `Dropdown.Item` from `semantic-ui-react`, allowing menu items to be shared between desktop and mobile layouts.
- [search.tsx](search.tsx): The `SearchBar` component displayed in the top bar. It handles user input and displays search results.
- [search_index.ts](search_index.ts): Implements the search index using the `lunr` library. It creates a searchable index of individuals from the loaded genealogy data, supporting multilingual search and character normalization.
- [top_bar.tsx](top_bar.tsx): The main component for the application's top navigation bar. It handles responsive design for small and large screens and aggregates all the menus and the search bar.
- [upload_menu.tsx](upload_menu.tsx): The "Open file" menu item. It allows users to upload local GEDCOM files and images.
- [url_menu.tsx](url_menu.tsx): The "Load from URL" menu item and modal dialog. It allows users to load data from a URL, using a proxy to avoid CORS issues.
- [wikitree_menu.tsx](wikitree_menu.tsx): Menu items and modal dialogs for WikiTree integration, including loading by WikiTree ID and logging in.
- [wikitree.png](wikitree.png): Image asset for the WikiTree logo used in the menus.
- [menu_item.tsx](menu_item.tsx): A utility component that renders as either a
`Menu.Item` or a `Dropdown.Item` from `semantic-ui-react`, allowing menu items
to be shared between desktop and mobile layouts.
- [search.tsx](search.tsx): The `SearchBar` component displayed in the top bar.
It handles user input and displays search results.
- [search_index.ts](search_index.ts): Implements the search index using the
`lunr` library. It creates a searchable index of individuals from the loaded
genealogy data, supporting multilingual search and character normalization.
- [top_bar.tsx](top_bar.tsx): The main component for the application's top
navigation bar. It handles responsive design for small and large screens and
aggregates all the menus and the search bar.
- [upload_menu.tsx](upload_menu.tsx): The "Open file" menu item. It allows users
to upload local GEDCOM files and images.
- [url_menu.tsx](url_menu.tsx): The "Load from URL" menu item and modal dialog.
It allows users to load data from a URL, using a proxy to avoid CORS issues.
- [wikitree_menu.tsx](wikitree_menu.tsx): Menu items and modal dialogs for
WikiTree integration, including loading by WikiTree ID and logging in.
- [wikitree.png](wikitree.png): Image asset for the WikiTree logo used in the
menus.

View File

@@ -1,18 +1,36 @@
# Side Panel Directory
This directory contains components and logic for the side panel of the Topola Viewer application. The side panel is used to display detailed information about a selected individual and to configure chart settings.
This directory contains components and logic for the side panel of the Topola
Viewer application. The side panel is used to display detailed information about
a selected individual and to configure chart settings.
## Purpose
The purpose of this directory is to provide a collapsible side panel that offers:
1. **Detailed Information**: Displays comprehensive data about a selected person, including their name, images, events, facts, notes, and sources, extracted from the loaded GEDCOM file.
2. **Chart Configuration**: Provides controls for users to customize the appearance of the genealogical chart (e.g., color schemes, showing/hiding IDs, and sex indicators).
The purpose of this directory is to provide a collapsible side panel that
offers:
This directory acts as a container for these features, organizing them into specific subdirectories:
- **[config](config/)**: Contains the UI and logic for managing chart display settings.
- **[details](details/)**: Contains components for rendering the detailed view of an individual's data.
- **[head](head/)**: Contains components to display metadata from the GEDCOM file header.
1. **Detailed Information**: Displays comprehensive data about a selected
person, including their name, images, events, facts, notes, and sources,
extracted from the loaded GEDCOM file.
2. **Chart Configuration**: Provides controls for users to customize the
appearance of the genealogical chart (e.g., color schemes, showing/hiding
IDs, and sex indicators).
This directory acts as a container for these features, organizing them into
specific subdirectories:
- **[config](config/)**: Contains the UI and logic for managing chart display
settings.
- **[details](details/)**: Contains components for rendering the detailed view
of an individual's data.
- **[head](head/)**: Contains components to display metadata from the GEDCOM
file header.
## Files in this Directory
- [side-panel.tsx](side-panel.tsx): The main React component that implements the side panel. It manages the layout, switching between expanded and collapsed states. When expanded, it displays a tabbed interface with "Info" (details) and "Settings" (configuration) panes. When collapsed in a mobile view, it shows a minimal view of the selected person's name. It also includes the button to toggle the panel's expanded state.
- [side-panel.tsx](side-panel.tsx): The main React component that implements the
side panel. It manages the layout, switching between expanded and collapsed
states. When expanded, it displays a tabbed interface with "Info" (details)
and "Settings" (configuration) panes. When collapsed in a mobile view, it
shows a minimal view of the selected person's name. It also includes the
button to toggle the panel's expanded state.

View File

@@ -1,20 +1,28 @@
# Chart Configuration
This directory contains the logic and UI for configuring the display of the genealogical chart.
This directory contains the logic and UI for configuring the display of the
genealogical chart.
## Purpose
The purpose of this directory is to manage the user-configurable settings for the chart visualization. This includes:
- **Colors**: How individuals in the chart are colored (none, by generation, or by sex).
The purpose of this directory is to manage the user-configurable settings for
the chart visualization. This includes:
- **Colors**: How individuals in the chart are colored (none, by generation, or
by sex).
- **IDs**: Whether to show or hide person IDs.
- **Sex**: Whether to show or hide indicators for sex.
It provides the data structures for the configuration, default values, functions to serialize/deserialize the configuration to/from URL query parameters, and the UI component for the side panel.
It provides the data structures for the configuration, default values, functions
to serialize/deserialize the configuration to/from URL query parameters, and the
UI component for the side panel.
## Files
- [config.tsx](config.tsx): The main file containing:
- `ChartColors`, `Ids`, and `Sex` enums defining the available options.
- `Config` interface defining the configuration state.
- `argsToConfig` and `configToArgs` functions for mapping the configuration to and from URL query arguments.
- `ConfigPanel` React component providing the UI controls (radio buttons) for these settings in the side panel.
- `argsToConfig` and `configToArgs` functions for mapping the configuration to
and from URL query arguments.
- `ConfigPanel` React component providing the UI controls (radio buttons) for
these settings in the side panel.

View File

@@ -1,18 +1,33 @@
# Details Directory
This directory contains React components for displaying detailed information about a selected individual (person) in the side panel of the Topola Viewer application. The details are extracted from a parsed GEDCOM file.
This directory contains React components for displaying detailed information
about a selected individual (person) in the side panel of the Topola Viewer
application. The details are extracted from a parsed GEDCOM file.
## Files
- [additional-files.tsx](additional-files.tsx): Displays a list of links to non-image files associated with an entry.
- [collapsed-details.tsx](collapsed-details.tsx): Displays a minimal, vertical view of details (person's name) when the side panel is collapsed.
- [details.tsx](details.tsx): The main component that orchestrates the display of all details for an individual, including name, images, events, facts, notes, and sources.
- [event-extras.tsx](event-extras.tsx): Displays additional content for events (images, notes, sources, files) in a tabbed interface.
- [events.tsx](events.tsx): Handles the logic and display for all events related to an individual, sorting them by date and grouping them by life stages.
- [immediate-family.tsx](immediate-family.tsx): A dedicated side panel module for grouping and displaying parents, spouses, and children as rapid-navigation links.
- [linkify-new-tab.tsx](linkify-new-tab.tsx): A helper component that wraps content and makes URLs clickable, opening them in a new tab.
- [multiline-text.tsx](multiline-text.tsx): Helper component to display multi-line text with linkified URLs.
- [person-link.tsx](person-link.tsx): Component that renders a clickable link to navigate to a person's profile view.
- [additional-files.tsx](additional-files.tsx): Displays a list of links to
non-image files associated with an entry.
- [collapsed-details.tsx](collapsed-details.tsx): Displays a minimal, vertical
view of details (person's name) when the side panel is collapsed.
- [details.tsx](details.tsx): The main component that orchestrates the display
of all details for an individual, including name, images, events, facts,
notes, and sources.
- [event-extras.tsx](event-extras.tsx): Displays additional content for events
(images, notes, sources, files) in a tabbed interface.
- [events.tsx](events.tsx): Handles the logic and display for all events related
to an individual, sorting them by date and grouping them by life stages.
- [immediate-family.tsx](immediate-family.tsx): A dedicated side panel module
for grouping and displaying parents, spouses, and children as rapid-navigation
links.
- [linkify-new-tab.tsx](linkify-new-tab.tsx): A helper component that wraps
content and makes URLs clickable, opening them in a new tab.
- [multiline-text.tsx](multiline-text.tsx): Helper component to display
multi-line text with linkified URLs.
- [person-link.tsx](person-link.tsx): Component that renders a clickable link to
navigate to a person's profile view.
- [sources.tsx](sources.tsx): Displays a list of sources cited for an entry.
- [translated-tag.tsx](translated-tag.tsx): Component to translate GEDCOM tags into human-readable labels.
- [wrapped-image.tsx](wrapped-image.tsx): Component to display images with loading placeholders, error fallback, and a click-to-enlarge modal.
- [translated-tag.tsx](translated-tag.tsx): Component to translate GEDCOM tags
into human-readable labels.
- [wrapped-image.tsx](wrapped-image.tsx): Component to display images with
loading placeholders, error fallback, and a click-to-enlarge modal.

View File

@@ -1,16 +1,21 @@
# Head Directory
This directory contains components responsible for displaying information from the header of the loaded GEDCOM file in the side panel.
This directory contains components responsible for displaying information from
the header of the loaded GEDCOM file in the side panel.
## Files
### [head.tsx](head.tsx)
Defines the `SourceHead` component. This component displays metadata about the data source, extracted from the GEDCOM header (`HEAD` record). It includes information such as:
Defines the `SourceHead` component. This component displays metadata about the
data source, extracted from the GEDCOM header (`HEAD` record). It includes
information such as:
- The software name that generated the file (`SOUR`)
- File creation date (`DATE`)
- Original file name (`FILE`)
- Submitter's name and contact details (phone, email, address) (`SUBM`)
- Copyright information (`COPR`)
The component conditionally renders these details only if they are present in the data.
The component conditionally renders these details only if they are present in
the data.

View File

@@ -1,20 +1,25 @@
# Translations
This directory contains translation files for the Topola Viewer application. These files are used to support multiple languages in the user interface.
This directory contains translation files for the Topola Viewer application.
These files are used to support multiple languages in the user interface.
## Files
Each file in this directory corresponds to a specific language and contains a JSON object with translation keys and their corresponding localized strings.
Each file in this directory corresponds to a specific language and contains a
JSON object with translation keys and their corresponding localized strings.
* [bg.json](bg.json): Bulgarian translations.
* [cs.json](cs.json): Czech translations.
* [de.json](de.json): German translations.
* [fr.json](fr.json): French translations.
* [it.json](it.json): Italian translations.
* [pl.json](pl.json): Polish translations.
* [ru.json](ru.json): Russian translations.
* [sv.json](sv.json): Swedish translations.
- [bg.json](bg.json): Bulgarian translations.
- [cs.json](cs.json): Czech translations.
- [de.json](de.json): German translations.
- [fr.json](fr.json): French translations.
- [it.json](it.json): Italian translations.
- [pl.json](pl.json): Polish translations.
- [ru.json](ru.json): Russian translations.
- [sv.json](sv.json): Swedish translations.
## Usage
These files are loaded by the internationalization (i18n) framework used in the project to display the UI in the user's preferred language. English strings are used as defaults directly in the source code. The translation json files are loaded in [../index.tsx](src/index.tsx).
These files are loaded by the internationalization (i18n) framework used in the
project to display the UI in the user's preferred language. English strings are
used as defaults directly in the source code. The translation json files are
loaded in [../index.tsx](src/index.tsx).

View File

@@ -1,17 +1,35 @@
# Utilities (`src/util`)
This directory contains various utility functions, hooks, and classes used across the application. These utilities handle date calculations, GEDCOM data processing, internationalization of errors, analytics, and responsive design setup.
This directory contains various utility functions, hooks, and classes used
across the application. These utilities handle date calculations, GEDCOM data
processing, internationalization of errors, analytics, and responsive design
setup.
## Files
- **[age_util.ts](age_util.ts)**: Utilities for calculating and formatting age based on birth and death dates. It handles exact dates, ranges, and qualifiers, and supports localization.
- **[age_util.ts](age_util.ts)**: Utilities for calculating and formatting age
based on birth and death dates. It handles exact dates, ranges, and
qualifiers, and supports localization.
- **[age_util.spec.ts](age_util.spec.ts)**: Unit tests for `age_util.ts`.
- **[analytics.ts](analytics.ts)**: Provides the `analyticsEvent` function to send events to Google Analytics using `gtag`.
- **[analytics_noop.ts](analytics_noop.ts)**: A no-op implementation of `analyticsEvent`, used when analytics are disabled.
- **[date_util.ts](date_util.ts)**: Utilities for formatting and comparing dates, handling `DateOrRange` objects from the `topola` library, and supporting localization.
- **[error.ts](error.ts)**: Defines the `TopolaError` class, extending `Error` to include an error code and arguments used for internationalized error messages.
- **[error_i18n.ts](error_i18n.ts)**: Provides `getI18nMessage` to return translated messages for `TopolaError` instances.
- **[gedcom_util.ts](gedcom_util.ts)**: A comprehensive utility file for handling GEDCOM data. It includes functions for parsing and converting GEDCOM files, normalizing data (sorting children and spouses), dereferencing entries, and extracting specific data like names and sources.
- **[gedcom_util.spec.ts](gedcom_util.spec.ts)**: Unit tests for `gedcom_util.ts`.
- **[media.ts](media.ts)**: Sets up responsive design breakpoints (small and large) using the `@artsy/fresnel` library.
- **[previous-hook.ts](previous-hook.ts)**: A custom React hook (`usePrevious`) that returns the value of a variable from the previous render.
- **[analytics.ts](analytics.ts)**: Provides the `analyticsEvent` function to
send events to Google Analytics using `gtag`.
- **[analytics_noop.ts](analytics_noop.ts)**: A no-op implementation of
`analyticsEvent`, used when analytics are disabled.
- **[date_util.ts](date_util.ts)**: Utilities for formatting and comparing
dates, handling `DateOrRange` objects from the `topola` library, and
supporting localization.
- **[error.ts](error.ts)**: Defines the `TopolaError` class, extending `Error`
to include an error code and arguments used for internationalized error
messages.
- **[error_i18n.ts](error_i18n.ts)**: Provides `getI18nMessage` to return
translated messages for `TopolaError` instances.
- **[gedcom_util.ts](gedcom_util.ts)**: A comprehensive utility file for
handling GEDCOM data. It includes functions for parsing and converting GEDCOM
files, normalizing data (sorting children and spouses), dereferencing entries,
and extracting specific data like names and sources.
- **[gedcom_util.spec.ts](gedcom_util.spec.ts)**: Unit tests for
`gedcom_util.ts`.
- **[media.ts](media.ts)**: Sets up responsive design breakpoints (small and
large) using the `@artsy/fresnel` library.
- **[previous-hook.ts](previous-hook.ts)**: A custom React hook (`usePrevious`)
that returns the value of a variable from the previous render.

View File

@@ -1,54 +1,101 @@
# End-to-End & Visual Testing Suite (`tests`)
This directory houses all automated test specifications and validation fixtures for validating the **Topola Genealogy Viewer** application. The tests are powered by the modern Playwright framework to offer robust, type-safe end-to-end (E2E) user flows and pixel-perfect visual regression validations.
This directory houses all automated test specifications and validation fixtures
for validating the **Topola Genealogy Viewer** application. The tests are
powered by the modern Playwright framework to offer robust, type-safe end-to-end
(E2E) user flows and pixel-perfect visual regression validations.
## Test Design Reference Documents
Before writing new or adjusting existing tests, developers should review their corresponding systems design guidelines under the `docs/` directory to align with established conventions and paradigms:
* **[PLAYWRIGHT_DESIGN.md](../docs/PLAYWRIGHT_DESIGN.md)**: Explains overall Playwright orchestration, server hooks, mock GEDCOM network intercept routines, tracking blockages, and in-memory iframe environments.
* **[SCREENSHOT_TESTS_DESIGN.md](../docs/SCREENSHOT_TESTS_DESIGN.md)**: Outlines visual testing strategies, viewport locks, dynamic footer/changelog sanitization overrides in virtual DOMs, threshold sensitivities, and local baseline update policies.
Before writing new or adjusting existing tests, developers should review their
corresponding systems design guidelines under the `docs/` directory to align
with established conventions and paradigms:
- **[PLAYWRIGHT_DESIGN.md](../docs/PLAYWRIGHT_DESIGN.md)**: Explains overall
Playwright orchestration, server hooks, mock GEDCOM network intercept
routines, tracking blockages, and in-memory iframe environments.
- **[SCREENSHOT_TESTS_DESIGN.md](../docs/SCREENSHOT_TESTS_DESIGN.md)**: Outlines
visual testing strategies, viewport locks, dynamic footer/changelog
sanitization overrides in virtual DOMs, threshold sensitivities, and local
baseline update policies.
---
## Test Specs Registry
Tests are strategically partitioned into standard functional E2E specs and isolated visual regression specs (which are tagged and grouped into targeted Playwright projects).
Tests are strategically partitioned into standard functional E2E specs and
isolated visual regression specs (which are tagged and grouped into targeted
Playwright projects).
### 1. Functional E2E Specs
Verify functional correctness, route parameters, drawer side-panels, dynamic autocomplete searching, and external/bidirectional event loops.
* **[chart_view.spec.ts](chart_view.spec.ts)**: Asserts core tree rendering from mock network parameters, navigation shifts, transition handling, and card node interaction.
* **[embedded.spec.ts](embedded.spec.ts)**: Tests embedded iframe operations using virtual server postMessage listeners to execute proper parent-child synchronizations.
* **[intro.spec.ts](intro.spec.ts)**: Checks base landing layouts, instructions panels, menu existence, and responsive header navigation buttons.
* **[search.spec.ts](search.spec.ts)**: Targets autocompletion components using robust search locators to verify input debouncing and focus updates.
* **[upload.spec.ts](upload.spec.ts)**: Validates uploading raw GEDCOM files from the local filesystem, asserting navigation transitions, state parsing, and virtual tree renderings.
* **[webmcp.spec.ts](webmcp.spec.ts)**: Evaluates out-of-process browser calls from Model Context Protocol tool registrations and action assertions.
Verify functional correctness, route parameters, drawer side-panels, dynamic
autocomplete searching, and external/bidirectional event loops.
- **[chart_view.spec.ts](chart_view.spec.ts)**: Asserts core tree rendering from
mock network parameters, navigation shifts, transition handling, and card node
interaction.
- **[embedded.spec.ts](embedded.spec.ts)**: Tests embedded iframe operations
using virtual server postMessage listeners to execute proper parent-child
synchronizations.
- **[intro.spec.ts](intro.spec.ts)**: Checks base landing layouts, instructions
panels, menu existence, and responsive header navigation buttons.
- **[search.spec.ts](search.spec.ts)**: Targets autocompletion components using
robust search locators to verify input debouncing and focus updates.
- **[upload.spec.ts](upload.spec.ts)**: Validates uploading raw GEDCOM files
from the local filesystem, asserting navigation transitions, state parsing,
and virtual tree renderings.
- **[webmcp.spec.ts](webmcp.spec.ts)**: Evaluates out-of-process browser calls
from Model Context Protocol tool registrations and action assertions.
### 2. Visual Regression / Screenshot Specs
Capture pixel-perfect layout comparisons against preserved snapshots to detect subtle structural overlaps or rendering discrepancies.
* **[charts_visual.spec.ts](charts_visual.spec.ts)**: Loops through multiple layouts (`Hourglass`, `Relatives`, `Donatso`) using a local GEDCOM dataset to ensure aesthetic layout alignments.
* **[config_visual.spec.ts](config_visual.spec.ts)**: Asserts full-window UI synchronization states while settings choices (like colored genders, hidden node IDs, or compact profiles) are toggled.
* **[details_visual.spec.ts](details_visual.spec.ts)**: Checks that intricate information templates (e.g., complex multiline fact headers, embedded picture borders, and source list grids) are structured beautifully under edge cases.
* **[intro_visual.spec.ts](intro_visual.spec.ts)**: Validates static landing layouts. Pre-evaluates dynamic sections (e.g. version/commit logs) to replace them with fixed test markers for baseline durability.
Capture pixel-perfect layout comparisons against preserved snapshots to detect
subtle structural overlaps or rendering discrepancies.
- **[charts_visual.spec.ts](charts_visual.spec.ts)**: Loops through multiple
layouts (`Hourglass`, `Relatives`, `Donatso`) using a local GEDCOM dataset to
ensure aesthetic layout alignments.
- **[config_visual.spec.ts](config_visual.spec.ts)**: Asserts full-window UI
synchronization states while settings choices (like colored genders, hidden
node IDs, or compact profiles) are toggled.
- **[details_visual.spec.ts](details_visual.spec.ts)**: Checks that intricate
information templates (e.g., complex multiline fact headers, embedded picture
borders, and source list grids) are structured beautifully under edge cases.
- **[intro_visual.spec.ts](intro_visual.spec.ts)**: Validates static landing
layouts. Pre-evaluates dynamic sections (e.g. version/commit logs) to replace
them with fixed test markers for baseline durability.
---
## Supporting Infrastructure
To enable lightweight, reproducible, and offline executions, tests rely on the following configuration, definitions, and test data structures:
To enable lightweight, reproducible, and offline executions, tests rely on the
following configuration, definitions, and test data structures:
* **[fixtures/embedded_frame.html](fixtures/embedded_frame.html)**: HTML template for serving cross-origin iframe wrapper mockups virtually to the browser container.
* **[global.d.ts](global.d.ts)**: TypeScript declarations defining window overrides (like AI registration pointers `window.__registeredTools`) to bypass compiler warnings.
* **[helpers.ts](helpers.ts)**: Unified routing utilities:
* `setupHermeticEnvironment()`: Intercepts and halts metrics/analytical queries and embeds local baseline fonts during spec executions.
* `setupGedcomRoute()`: Re-routes standard genealogy payload network paths directly to load standard local datasets on-the-fly (`src/datasource/testdata/test.ged`).
* **[tsconfig.json](tsconfig.json)**: Typecheck preferences custom to the Playwright runner environment to avoid compilation type collisions.
* **`[spec_name]-snapshots/`**: Directory structure containing expected baseline references and image comparison files.
- **[fixtures/embedded_frame.html](fixtures/embedded_frame.html)**: HTML
template for serving cross-origin iframe wrapper mockups virtually to the
browser container.
- **[global.d.ts](global.d.ts)**: TypeScript declarations defining window
overrides (like AI registration pointers `window.__registeredTools`) to bypass
compiler warnings.
- **[helpers.ts](helpers.ts)**: Unified routing utilities:
- `setupHermeticEnvironment()`: Intercepts and halts metrics/analytical
queries and embeds local baseline fonts during spec executions.
- `setupGedcomRoute()`: Re-routes standard genealogy payload network paths
directly to load standard local datasets on-the-fly
(`src/datasource/testdata/test.ged`).
- **[tsconfig.json](tsconfig.json)**: Typecheck preferences custom to the
Playwright runner environment to avoid compilation type collisions.
- **`[spec_name]-snapshots/`**: Directory structure containing expected baseline
references and image comparison files.
---
## Running the Tests
Automated scripts to run and update tests are integrated into standard workspace npm configurations:
Automated scripts to run and update tests are integrated into standard workspace
npm configurations:
```bash
# Run standard functional E2E tests (fast verification)

View File

@@ -10,14 +10,14 @@ WikiTree API, CORS proxy, and GitHub raw URLs.
npm run test:probers
```
This uses the separate `playwright.prober.config.ts` configuration, which
does not start a local dev server. Each spec navigates to a full absolute
URL (or `localhost:8080` for the Docker prober).
This uses the separate `playwright.prober.config.ts` configuration, which does
not start a local dev server. Each spec navigates to a full absolute URL (or
`localhost:8080` for the Docker prober).
## What They Test
| Spec | Target | Exercises |
| --- | --- | --- |
| ------------------------------ | ------------------- | ---------------------------------------------------- |
| `gh-pages-gedcom.spec.ts` | `pewu.github.io` | GitHub Pages deployment, CORS proxy, GEDCOM-from-URL |
| `wikitree.spec.ts` | `apps.wikitree.com` | WikiTree direct API, WikiTree deployment |
| `wikitree-cors-gedcom.spec.ts` | `apps.wikitree.com` | WikiTree deployment, CORS proxy via GEDCOM-from-URL |
@@ -27,4 +27,5 @@ URL (or `localhost:8080` for the Docker prober).
- Probers run sequentially (`fullyParallel: false`) to avoid rate-limiting.
- Each prober has its own GitHub Actions workflow for independent triggering.
- Probers do not block Google Analytics, so live runs generate real analytics events.
- Probers do not block Google Analytics, so live runs generate real analytics
events.