Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
4ef36b3
Add focal and passive modes with data manager
spwashi Jun 18, 2025
39154b8
Merge pull request #1 from factshift/codex/update-websocket-container…
spwashi Jun 18, 2025
eceb1a8
Refactor websocket container to generic stream handler
spwashi Jun 18, 2025
e37f8c5
Merge pull request #2 from factshift/codex/refactor-websocket-handler…
spwashi Jun 18, 2025
b605a68
refactor: extract stream mode handlers
spwashi Jun 18, 2025
78cdebf
Merge pull request #3 from factshift/codex/enhance-stream-modes-with-…
spwashi Jun 18, 2025
2315644
feat: hydrate ui
spwashi Jun 18, 2025
52181f7
Merge pull request #4 from factshift/codex/normalize-ui-initializatio…
spwashi Jun 18, 2025
c062364
Move refactored modules back under js directory
spwashi Jun 18, 2025
df7c97b
Merge pull request #5 from factshift/codex/list-all-features
spwashi Jun 18, 2025
5ac74a3
Add init steps helper and documentation
spwashi Jun 18, 2025
e526427
Merge pull request #6 from factshift/codex/add-addinitsteps-helper-to…
spwashi Jun 18, 2025
b84336b
fix: add newline at end of files
spwashi Jun 18, 2025
6e95dbe
Merge pull request #7 from factshift/codex/ensure-newline-at-end-of-f…
spwashi Jun 18, 2025
75b87f5
Add project README
spwashi Jun 18, 2025
e2a6650
Merge pull request #8 from factshift/codex/create-readme.md-with-proj…
spwashi Jun 18, 2025
5509f6e
Add fallback UI for stream modes
spwashi Jun 18, 2025
99911fa
Merge pull request #10 from factshift/codex/extend-modeconfig-with-fa…
spwashi Jun 18, 2025
f1fd0d2
fix service worker version constants
spwashi Jun 18, 2025
f6aa77c
Merge pull request #11 from factshift/codex/update-service-worker.js-…
spwashi Jun 18, 2025
c8e67e6
docs: switch to yarn
spwashi Jun 18, 2025
399f7e5
Merge pull request #12 from factshift/codex/decide-between-npm-and-yarn
spwashi Jun 18, 2025
0596a89
feat: close live data sources on cleanup
spwashi Jun 18, 2025
a0b2a7f
Merge pull request #13 from factshift/codex/extend-livedatasource-wit…
spwashi Jun 18, 2025
bcc14f8
refactor: centralize init step definitions
spwashi Jun 18, 2025
369f3f9
Merge pull request #14 from factshift/codex/update-init-pipeline.js-a…
spwashi Jun 18, 2025
1ab3cce
Add QA docs for Ace editor styling
spwashi Jun 18, 2025
c371f2d
Merge pull request #15 from factshift/codex/add-or-remove-ace-editor-…
spwashi Jun 18, 2025
9bb938b
Detect duplicate and circular init steps
spwashi Jun 18, 2025
efe2286
Merge pull request #16 from factshift/codex/modify-init-pipeline.js-t…
spwashi Jun 18, 2025
1c9ebe6
refactor: strongly type init pipeline
spwashi Jun 18, 2025
a665223
Merge pull request #17 from factshift/codex/extend-step-object-with-p…
spwashi Jun 18, 2025
fe56131
refactor: move input and stream modes
spwashi Jun 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Factshift Web Client

This repository contains the front‑end code for **app.factshift.com**. The project is built with [Vite](https://vitejs.dev/) and uses a variety of client‑side libraries to power a real‑time stream interface.

## Installation

Use `yarn` to install dependencies:

```bash
yarn install
```

## Development

Start a local development server:

```bash
yarn start
```

## Building

Create a production build:

```bash
yarn build
```

Preview the built files locally:

```bash
yarn serve
```

## Initialization

For details on how the initialization pipeline works, see [docs/init-pipeline.md](docs/init-pipeline.md).

## QA

Quality assurance checklists live under [docs/qa](docs/qa). See the
[Ace Editor Styling QA Checklist](docs/qa/ace-editor-style.md) for
testing the new code editor styles.

39 changes: 39 additions & 0 deletions docs/api/modes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Stream Mode Web APIs

This document outlines the WebSocket message formats understood by each stream mode.

## Boon
Messages are JSON objects describing a "boon" node.
Example sent from the client:
```json
{
"id": 123,
"name": "Example Boon",
"x": 0,
"y": 0,
"z": 0,
"boonhonk": {"description": "text", "level": 1, "is_active": true},
"image_id": null
}
```
The server echoes a similar structure when broadcasting boon updates.

## Boof
Boof messages contain textual content. Clients may send plain text or a JSON object `{ "content": "text" }` and receive a similar structure from the server.

## Focal
Used to update the focal point of the simulation.
```json
{
"type": "focal-point",
"position": {"x": 10, "y": 20},
"bounds": {"x1": 0, "y1": 0, "x2": 20, "y2": 40}
}
```

## Passive
Passive mode receives streamed updates but does not send data. Any JSON object broadcast by the server will be displayed.

## Other Modes
Bane, Bone, Bonk, Honk and Lore share the same pattern as Passive mode. They may be extended later with specific message formats.

44 changes: 44 additions & 0 deletions docs/init-pipeline.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Initialization Pipeline

The bootstrap folder exposes a small pipeline utility for organizing startup routines.

```javascript
import { addInitSteps, runInitPipeline, InitPhase } from '../src/js/bootstrap/init-pipeline';
import { stepsById } from '../src/js/bootstrap/init-steps';

addInitSteps([
{ ...stepsById.analytics, phase: InitPhase.BOOT },
{ ...stepsById.parameters, phase: InitPhase.BOOT },
{ ...stepsById.ui, phase: InitPhase.UI, dependsOn: [stepsById.parameters] },
]);

await runInitPipeline();
```

`addInitSteps` accepts either an object mapping step ids to functions or an array of step objects. Each step object has the shape `{id, init, priority?, dependsOn?, phase?}`. The `dependsOn` list may contain step ids or references to other step objects. Steps are sorted before execution first by their phase, then by priority and finally their declared dependencies.

### Example with Priorities and Dependencies

```javascript
addInitSteps([
{ id: 'a', init: initA, priority: 0 },
{ id: 'b', init: initB, priority: 10 },
{ id: 'c', init: initC, dependsOn: ['a', 'b'] },
]);
```

Here `c` runs after `a` and `b` despite not specifying a priority. Lower priority values run earlier.

Duplicate step ids are not allowed and will throw an error when added. The
pipeline also detects circular dependencies between steps and throws an error if
a cycle is found during sorting.

### Phases

Steps can optionally specify a `phase` that groups them into broad buckets of work.
Phases are defined in the exported `InitPhase` enum with the values `BOOT`, `UI` and `DEFERRED`.
The sorter runs all `boot` phase steps before `ui` steps, followed by `deferred` steps.
Any step without a known phase is treated as `deferred`.

Phases are useful for keeping lightweight initialization logic (`boot`) separate
from user interface startup (`ui`) or work that can be postponed (`deferred`).
5 changes: 5 additions & 0 deletions docs/qa/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# QA Documentation

This folder contains checklists and notes for quality assurance testers.

- [Ace Editor Styling QA Checklist](ace-editor-style.md)
11 changes: 11 additions & 0 deletions docs/qa/ace-editor-style.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Ace Editor Styling QA Checklist

This document lists the steps for verifying that Ace editor instances are styled consistently throughout the app.

1. Navigate to any feature that displays a code editor.
2. Confirm that the editor is inside a container with the `.code-editor` class.
3. Verify the container has a thin border and uses the same background color as the main menu.
4. Ensure code text appears in a monospace font and is legible.
5. Resize the editor to check that width and height remain consistent.

Report any styling deviations to the development team.
11 changes: 11 additions & 0 deletions docs/websocket-notes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Stream Container Notes

This interface exposes nine narrative modes: **Boon**, **Bane**, **Bone**, **Bonk**, **Honk**, **Boof**, **Lore**, **Focal**, and **Passive**.

Each mode's settings are stored locally in `localStorage` and may be transmitted to a backend when the `Send to Backend` button is pressed. To hook in real-time updates, listen to stream messages of the form `{type: '<mode>-settings', data: {...}}` and update the UI using the `handleStreamMessage` method of each handler.

The container uses a `DataManager` abstraction to source data either from static arrays or from a live WebSocket feed. Extend `modeConfig` in `stream-container.js` to point a mode at a live URL when the backend is available.

`LiveDataSource` now exposes a `close()` method. `ModeHandler.cleanup()` invokes this when present so switching modes or stream strategies cleans up the previous connection and callbacks.

For detailed message formats see [Stream Mode Web APIs](api/modes.md).
12 changes: 12 additions & 0 deletions docs/websocket-options-sample.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<title>WebSocket Options Panel Sample</title>
<link rel="stylesheet" href="../src/stylesheets/interaction/stream-container.scss">
</head>
<body>
<div id="main-stream-container">
<spwashi-stream-container></spwashi-stream-container>
</div>
</body>
</html>
2 changes: 1 addition & 1 deletion public/v0.0.1/service-worker.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const appVersion = 'v0.0.2-alpha';
const appVersion = 'v0.0.1';
const cacheName = `serviceworker@${appVersion}`;

const staticAssets = [
Expand Down
2 changes: 1 addition & 1 deletion public/v0.0.2-alpha/service-worker.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const appVersion = 'v0.0.1';
const appVersion = 'v0.0.2-alpha';
const cacheName = `serviceworker@${appVersion}`;

const staticAssets =
Expand Down
2 changes: 1 addition & 1 deletion src/_spwashi@/scripts/env/augmentations.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {boonConcept, boonNode} from "../../../js/modes/spw/commands/boon";
import {boonConcept, boonNode} from "../../../js/modes/input/spw/commands/boon";

boonConcept['@node'] = (index, batchSize) => {
const currentDayIndex = (new Date()).getDay();
Expand Down
4 changes: 2 additions & 2 deletions src/_spwashi@/scripts/loop/body.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {mapNodes, pushNode} from "../../../js/simulation/nodes/data/operate";
import {clearActiveNodes} from "../../../js/init/hotkeys/handlers/clear-active-nodes";
import {clearActiveNodes} from "../../../js/ui/hotkeys/handlers/clear-active-nodes";
import {gameState} from "../state/state";
import {processSpwInput} from "../../../js/modes/spw/process-spw-input";
import {processSpwInput} from "../../../js/modes/input/spw/process-spw-input";
import {pushLink} from "../../../js/simulation/edges/data/pushLink";
import {mainLoop} from "./head";

Expand Down
6 changes: 3 additions & 3 deletions src/_spwashi@/scripts/spwashi.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import "../../stylesheets/main.scss";
import "../../js/ui/styles/main.scss";
import "../styles/_spwashi@.scss";
import {app} from '../../js'
import {processSpwInput} from "../../js/modes/spw/process-spw-input";
import {app} from '../../js/main.js'
import {processSpwInput} from "../../js/modes/input/spw/process-spw-input";
import {mainLoop} from "./loop/head";
import {gameState} from "./state/state";
import "./env/augmentations";
Expand Down
10 changes: 5 additions & 5 deletions src/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<link rel="icon" href="/v0.0.1/favicon/16.png">
<link rel="manifest" href="/manifest.json">
<link href="stylesheets/main.scss" rel="stylesheet"/>
<link href="stylesheets/form-factors/style.css" rel="stylesheet"/>
<link href="js/ui/styles/main.scss" rel="stylesheet"/>
<link href="js/ui/styles/form-factors/style.css" rel="stylesheet"/>
<script type="module">
import {app} from "./js";
import {app} from "./js/main.js";

app()
</script>
</head>
<body data-mode="nothing" data-interface-depth="standard">
<button id="main-wonder-button">?</button>
<h1>&lt;concept&gt;</h1>
<section id="main-websocket-container">
<section id="main-stream-container">
<script src="https://unpkg.com/htmx.org@1.9.2"></script>
<spwashi-websocket-container></spwashi-websocket-container>
<spwashi-stream-container></spwashi-stream-container>
</section>
<a id="title-md5"></a>
<main>
Expand Down
13 changes: 13 additions & 0 deletions src/js/bootstrap/hydrate-ui.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import {initFocalSquare} from "../ui/components/focal-point";
import {initH1} from "../ui/components/h1";
import {initUi} from "./ui";
import {initStreamContainer} from "../ui/components/stream-container";
import {initStreamConfig} from "../ui/components/stream-config";

export function hydrateUi(mode = window.spwashi.initialMode) {
initFocalSquare();
initH1();
initUi(mode);
initStreamContainer();
initStreamConfig();
}
91 changes: 91 additions & 0 deletions src/js/bootstrap/init-pipeline.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
export const InitPhase = {
BOOT: 'boot',
UI: 'ui',
DEFERRED: 'deferred',
};

const initSteps = [];
const map = new Map();

export function addInitStep(step) {
if (!step) return;
const { id, init, priority = 0, phase, dependsOn = [] } = step;
if (!id || typeof init !== 'function') {
throw new Error('init step requires {id, init}');
}
if (map.has(id)) {
throw new Error(`init step with id "${id}" already exists`);
}
const entry = { id, init, priority, phase, dependsOn };
map.set(id, entry);
initSteps.push(entry);
}

export function addInitSteps(steps) {
if (!steps) return;
if (Array.isArray(steps)) {
steps.forEach(step => {
if (Array.isArray(step)) {
const [id, init] = step;
addInitStep({ id, init });
} else {
addInitStep(step);
}
});
} else {
Object.entries(steps).forEach(([id, init]) => addInitStep({ id, init }));
}
}

export function sortSteps(steps) {
const stepsMap = new Map(steps.map(s => [s.id, s]));
const phaseOrder = [InitPhase.BOOT, InitPhase.UI, InitPhase.DEFERRED];
const phaseRank = phase => {
const idx = phaseOrder.indexOf(phase);
return idx === -1 ? phaseOrder.length : idx;
};
const ordered = [];
const visited = new Set();
const visiting = new Set();
const path = [];

function visit(step) {
if (visited.has(step.id)) return;
if (visiting.has(step.id)) {
const cycleStart = path.indexOf(step.id);
const cyclePath = path.slice(cycleStart).concat(step.id).join(' -> ');
throw new Error(`circular dependency detected: ${cyclePath}`);
}
visiting.add(step.id);
path.push(step.id);
(step.dependsOn || []).forEach(dep => {
const depId = typeof dep === 'string' ? dep : dep.id;
const depStep = stepsMap.get(depId);
if (depStep) visit(depStep);
});
visiting.delete(step.id);
path.pop();
visited.add(step.id);
ordered.push(step);
}

const sorted = [...steps].sort((a, b) => {
const phaseDiff = phaseRank(a.phase) - phaseRank(b.phase);
if (phaseDiff !== 0) return phaseDiff;
return (a.priority ?? 0) - (b.priority ?? 0);
});
sorted.forEach(visit);
return ordered;
}

export async function runInitPipeline() {
const ordered = sortSteps(initSteps);
for (const { init } of ordered) {
await init();
}
}

export function clearInitPipeline() {
initSteps.length = 0;
map.clear();
}
Loading