Skip to content

Add Nexus OS shell with module loader, event bus, and cross-module rewards#39

Open
Codex wants to merge 1 commit into
mainfrom
codex/improve-navigation-with-os-shell
Open

Add Nexus OS shell with module loader, event bus, and cross-module rewards#39
Codex wants to merge 1 commit into
mainfrom
codex/improve-navigation-with-os-shell

Conversation

@Codex
Copy link
Copy Markdown
Contributor

@Codex Codex AI commented Apr 16, 2026

Integrated Nexus Arcade OS to stitch overworld navigation, Seven Stars quiz, match-3 arcade, listings, ministry, badges, and lore codex into a single event-driven shell.

  • Shell + Loader: New os-shell.html with hardened loadModule, nav pills, Mystery Meter, reward feed, and global NexusOS bus (emit/on/listen helpers).
  • Modules: Added modules/overworld.html, seven-stars.html, arcade.html, listings.html, ministry.html, badges.html, and lore-codex.html for map navigation, quiz flow, match-3 play, listings triggers, reward sinks, and lore progression.
  • Rewards & State: Central reward engine listens to listing-viewed, star-collected, combo-tier4, emitting reward-granted; Lore Codex unlocks via lore-unlocked; Mystery Meter persists in localStorage.
  • Arcade Hooks: Match-3 now emits arcade-combo (tiers, chain), escalates tier4 with combo-tier4, and fires arcade-gameover; optional hooks preserved.
  • Docs: README points to the shell and module structure.

Example: match-3 combo emission into the bus

const tier = determineTier(largestMatch, comboChain);
window.NexusOS.emit('arcade-combo', { chain: comboChain, tier });
if (tier >= 4) window.NexusOS.emit('combo-tier4', {});

Co-authored-by: NicholaiMadias <73684379+NicholaiMadias@users.noreply.github.com>
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 16, 2026

Deploy Preview for gulfnexus ready!

Name Link
🔨 Latest commit 41b8ce2
🔍 Latest deploy log https://app.netlify.com/projects/gulfnexus/deploys/69e0a358790dff000948e08b
😎 Deploy Preview https://deploy-preview-39--gulfnexus.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@NicholaiMadias NicholaiMadias requested a review from Copilot April 16, 2026 08:52
@NicholaiMadias NicholaiMadias marked this pull request as ready for review April 16, 2026 08:53
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a new “Nexus Arcade OS” HTML shell that loads feature modules (overworld, quiz, arcade, listings, ministry, badges, lore codex) and connects them via a global event bus and reward/state engine, integrating the existing match-3 game into the shell.

Changes:

  • Introduces os-shell.html with a module loader, global window.NexusOS event bus, shared state, and reward engine.
  • Adds new HTML modules under modules/ for Overworld navigation, Seven Stars quiz flow, Arcade UI wrapper, Listings triggers, Ministry sink, Badges view, and Lore Codex.
  • Extends match-maker-ui.js to emit combo/gameover events to NexusOS and supports optional hook overrides; updates README to document the shell.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
os-shell.html New OS shell UI + module loader + NexusOS event bus + shared state/reward engine + quiz/codex rendering
modules/overworld.html Overworld map + module jump buttons wired by shell initializer
modules/seven-stars.html Quiz module container for shell-driven rendering
modules/arcade.html Arcade module wrapper containing Match Maker DOM + combo timeline container
modules/listings.html Listing buttons emitting listing-viewed events (wired by shell initializer)
modules/ministry.html Ministry sink UI updated by shell reward/state rendering
modules/badges.html Badges/inventory feed container updated by shell rendering
modules/lore-codex.html Lore codex containers for shell rendering
match-maker-ui.js Adds combo chain tracking, tiering, NexusOS emits, and optional hooks in initMatchMaker
README.md Documents OS shell entrypoint and module/bus concepts

Comment thread os-shell.html
moduleInitializers[id]?.();
markActiveNav(id);
} catch (e) {
container.innerHTML = `<div class="feed-card">Module failed to load: ${e.message}</div>`;
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

The error path uses container.innerHTML = ...${e.message}.... If id is attacker-controlled (or if a thrown error message includes HTML), this becomes an XSS sink. Prefer setting textContent on a created element, or escaping the message before inserting into innerHTML.

Suggested change
container.innerHTML = `<div class="feed-card">Module failed to load: ${e.message}</div>`;
container.textContent = '';
const errorCard = document.createElement('div');
errorCard.className = 'feed-card';
errorCard.textContent = `Module failed to load: ${e.message}`;
container.appendChild(errorCard);

Copilot uses AI. Check for mistakes.
Comment thread os-shell.html
(this.events[event] ||= []).push(fn);
};
nexus.emit = function(event, data) {
(this.events[event] || []).forEach(fn => fn(data));
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

nexus.emit calls listeners directly in a forEach. If any listener throws, remaining listeners won't run and the exception will bubble to the browser task queue, potentially breaking cross-module reward/state updates. Consider wrapping each listener invocation in try/catch (and optionally logging) so one module can't take down the event bus.

Suggested change
(this.events[event] || []).forEach(fn => fn(data));
(this.events[event] || []).forEach(fn => {
try {
fn(data);
} catch (error) {
console.error(`NexusOS listener error for event "${event}"`, error);
}
});

Copilot uses AI. Check for mistakes.
Comment thread match-maker-ui.js
Comment on lines 123 to 129
function resolveMatches() {
const matches = findMatches(grid);
if (matches.length === 0) {
comboChain = 0;
renderGrid();
checkLevelUp();
checkGameOver();
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

In the shell, users can navigate away from the Arcade module while a chain reaction is still resolving (this function schedules itself via setTimeout later). If #match-grid is removed, renderGrid() can throw because it assumes the container exists. Consider adding a “module still mounted” guard in resolveMatches (and/or making renderGrid null-safe) and clearing any pending resolve timer on re-init/unload.

Copilot uses AI. Check for mistakes.
Comment thread os-shell.html
Comment on lines +285 to +287
nexus.state = nexus.state || {
mysteryMeter: clamp(Number(localStorage.getItem('nexus_mystery_meter') || 18), 0, 100),
loreUnlocked: new Set(JSON.parse(localStorage.getItem('nexus_lore_unlocked') || '[]')),
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

State hydration from localStorage uses JSON.parse(localStorage.getItem('nexus_lore_unlocked') || '[]') without a guard. If the stored value is corrupted/non-JSON, this will throw during boot and prevent the shell from loading. Consider wrapping the parse in try/catch (falling back to an empty array) to make startup resilient.

Suggested change
nexus.state = nexus.state || {
mysteryMeter: clamp(Number(localStorage.getItem('nexus_mystery_meter') || 18), 0, 100),
loreUnlocked: new Set(JSON.parse(localStorage.getItem('nexus_lore_unlocked') || '[]')),
function readJSONFromLocalStorage(key, fallback) {
const rawValue = localStorage.getItem(key);
if (rawValue === null) return fallback;
try {
return JSON.parse(rawValue);
} catch {
return fallback;
}
}
nexus.state = nexus.state || {
mysteryMeter: clamp(Number(localStorage.getItem('nexus_mystery_meter') || 18), 0, 100),
loreUnlocked: new Set(readJSONFromLocalStorage('nexus_lore_unlocked', [])),

Copilot uses AI. Check for mistakes.
Comment thread README.md

- Open `os-shell.html` to launch the Nexus shell with module loader, overworld map, and reward bus.
- Modules live under `modules/` (Seven Stars quiz, Match-3 arcade, Listings, Ministry, Badges, Lore Codex).
- The global `NexusOS` event bus coordinates navigation (`loadModule(id)`) and gameplay events (`star-collected`, `arcade-combo`, `listing-viewed`, `reward-granted`).
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

README mentions the NexusOS bus coordinates navigation via loadModule(id), but loadModule is currently a private function inside os-shell.html (not exposed on window.NexusOS). Either expose it (e.g., NexusOS.loadModule = loadModule) or adjust the README wording to avoid implying it’s part of the public bus API.

Suggested change
- The global `NexusOS` event bus coordinates navigation (`loadModule(id)`) and gameplay events (`star-collected`, `arcade-combo`, `listing-viewed`, `reward-granted`).
- The shell coordinates module navigation internally, while the global `NexusOS` event bus handles gameplay events (`star-collected`, `arcade-combo`, `listing-viewed`, `reward-granted`).

Copilot uses AI. Check for mistakes.
Comment thread os-shell.html
Comment on lines +380 to +384
const res = await fetch(`./modules/${id}.html`);
if (!res.ok) throw new Error(`Failed to load ${id}`);
const html = await res.text();
container.innerHTML = html;
moduleInitializers[id]?.();
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

loadModule fetches ./modules/${id}.html using an unvalidated id (from DOM dataset / callable from console). This allows path traversal like ../ and makes it easier to load and inject unintended same-origin HTML into the shell. Consider enforcing an allowlist (e.g., keys of moduleInitializers) and/or a strict id regex before fetching.

Copilot uses AI. Check for mistakes.
Comment thread os-shell.html
Comment on lines +664 to +669
list.innerHTML = Object.entries(loreEntries).map(([id, entry]) => {
const isUnlocked = unlocked.has(id);
return `
<div class="codex-item ${isUnlocked ? 'active' : 'locked'}" data-entry="${id}">
<div>${entry.title}</div>
<small class="muted">${entry.church}</small>
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

renderLoreCodex assigns the active class based on whether an entry is unlocked, but active styling reads like a “currently selected” state (and clicks don’t update it). This makes all unlocked entries look selected and there’s no visual indication of the currently viewed entry. Consider using separate classes (e.g., unlocked vs active) and toggling active on the selected entry when renderCodexBody runs.

Copilot uses AI. Check for mistakes.
Comment thread match-maker-ui.js
Comment on lines +133 to +136
comboChain += 1;
const largestMatch = Math.max(...matches.map(m => m.length));
const tier = determineTier(largestMatch, comboChain);
emitCombo(comboChain, tier, null);
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

largestMatch is computed as Math.max(...matches.map(m => m.length)), but findMatches currently returns a single group containing all matched cells (matchMakerState.js:91-96). That means this value is really “total cleared cells this resolution”, not the largest contiguous match, which can skew tiering and makes the name misleading. Consider either returning distinct match groups from findMatches (if you truly want “largest match”) or renaming/adjusting tier logic to match the actual semantics.

Copilot uses AI. Check for mistakes.
Comment thread os-shell.html
<div class="muted">${entry.church}</div>
<p>${entry.body}</p>
<div class="badge ${unlocked ? '' : 'locked-pill'}">
${unlocked ? 'Unlocked via Seven Stars' : 'Locked – clear quiz prompts'}
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

Lore entries can be unlocked via non-quiz paths (e.g., the combo-tier4 reward listener also adds badges into loreUnlocked). renderCodexBody always labels unlocked entries as “Unlocked via Seven Stars”, which becomes inaccurate. Consider tracking the unlock source or using a more generic unlocked label.

Suggested change
${unlocked ? 'Unlocked via Seven Stars' : 'Locked – clear quiz prompts'}
${unlocked ? 'Unlocked' : 'Locked – clear quiz prompts'}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Comment thread os-shell.html
async function loadModule(id, target = 'main-content') {
const container = document.getElementById(target);
if (!container) return;
try {
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

loadModule fetches ./modules/${id}.html without validating id. Because id can include path segments (e.g. ../), this can fetch unintended same-origin files and undermines the “hardened loader” claim. Consider whitelisting id against known module IDs (e.g., Object.keys(moduleInitializers)) and rejecting anything containing / or ...

Suggested change
try {
try {
const allowedModuleIds = Object.keys(moduleInitializers);
const isInvalidModuleId =
typeof id !== 'string' ||
id.includes('..') ||
id.includes('/') ||
id.includes('\\') ||
!allowedModuleIds.includes(id);
if (isInvalidModuleId) {
throw new Error(`Invalid module: ${id}`);
}

Copilot uses AI. Check for mistakes.
Comment thread modules/overworld.html
Comment on lines +20 to +29
<circle id="node-seven-stars" class="node-btn unlocked" cx="150" cy="120" r="22" />
<text x="150" y="160" fill="#e5e7eb" text-anchor="middle" font-size="14">Seven Stars</text>

<circle id="node-arcade" class="node-btn" cx="300" cy="80" r="22" />
<text x="300" y="120" fill="#e5e7eb" text-anchor="middle" font-size="14">Arcade</text>

<circle id="node-ministry" class="node-btn" cx="450" cy="120" r="22" />
<text x="450" y="160" fill="#e5e7eb" text-anchor="middle" font-size="14">Ministry</text>

<circle id="node-listings" class="node-btn" cx="520" cy="200" r="22" />
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

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

The overworld SVG nodes are clickable circles but aren’t keyboard-focusable and don’t expose button semantics/labels per node. For accessibility, make the nodes focusable (e.g., tabindex="0"), add role="button"/aria-label (or wrap in <a>), and handle Enter/Space key activation in addition to click.

Suggested change
<circle id="node-seven-stars" class="node-btn unlocked" cx="150" cy="120" r="22" />
<text x="150" y="160" fill="#e5e7eb" text-anchor="middle" font-size="14">Seven Stars</text>
<circle id="node-arcade" class="node-btn" cx="300" cy="80" r="22" />
<text x="300" y="120" fill="#e5e7eb" text-anchor="middle" font-size="14">Arcade</text>
<circle id="node-ministry" class="node-btn" cx="450" cy="120" r="22" />
<text x="450" y="160" fill="#e5e7eb" text-anchor="middle" font-size="14">Ministry</text>
<circle id="node-listings" class="node-btn" cx="520" cy="200" r="22" />
<circle id="node-seven-stars" class="node-btn unlocked" cx="150" cy="120" r="22" tabindex="0" role="button" aria-label="Open Seven Stars" onkeydown="if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') { event.preventDefault(); this.dispatchEvent(new MouseEvent('click', { bubbles: true })); }" />
<text x="150" y="160" fill="#e5e7eb" text-anchor="middle" font-size="14">Seven Stars</text>
<circle id="node-arcade" class="node-btn" cx="300" cy="80" r="22" tabindex="0" role="button" aria-label="Open Arcade" onkeydown="if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') { event.preventDefault(); this.dispatchEvent(new MouseEvent('click', { bubbles: true })); }" />
<text x="300" y="120" fill="#e5e7eb" text-anchor="middle" font-size="14">Arcade</text>
<circle id="node-ministry" class="node-btn" cx="450" cy="120" r="22" tabindex="0" role="button" aria-label="Open Ministry" onkeydown="if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') { event.preventDefault(); this.dispatchEvent(new MouseEvent('click', { bubbles: true })); }" />
<text x="450" y="160" fill="#e5e7eb" text-anchor="middle" font-size="14">Ministry</text>
<circle id="node-listings" class="node-btn" cx="520" cy="200" r="22" tabindex="0" role="button" aria-label="Open Listings" onkeydown="if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') { event.preventDefault(); this.dispatchEvent(new MouseEvent('click', { bubbles: true })); }" />

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants