Skip to content

feat(web): GitHub Pages fragment-mode deploy (closes #75)#77

Merged
naorsabag merged 4 commits into
masterfrom
feat/web-pages-fragment-mode
May 5, 2026
Merged

feat(web): GitHub Pages fragment-mode deploy (closes #75)#77
naorsabag merged 4 commits into
masterfrom
feat/web-pages-fragment-mode

Conversation

@naorsabag
Copy link
Copy Markdown
Owner

@naorsabag naorsabag commented May 5, 2026

Summary

Server-less variant of the web bundle for GitHub Pages. Flows live in the URL hash, "Save" copies a sharable URL to the clipboard, no API server, no sidebar list. Closes #75.

https://naorsabag.github.io/OpenHop/#N4IgDgTgpgrhBOEAuBLAdgYwK...

The fragment is the entire flow's YAML, lz-compressed and URI-encoded. Refresh, new tab, bookmark, share via Slack/email — all preserve the URL, all preserve the flow. No server, no localStorage, no account.

Plumbing

  • vite.config.ts: env-driven baseVITE_BASE='/OpenHop/' for the Pages build, '/' for dev / docker / npx openhop demo (unchanged).
  • .github/workflows/pages.yml: build @openhop/web with VITE_BASE=/OpenHop/ and VITE_FRAGMENT_MODE=1, upload via actions/upload-pages-artifact, deploy via actions/deploy-pages. Concurrency cap of 1 per group so a fresh deploy never lands on top of an in-flight one.

Runtime

  • main.tsx routes to one of two app shells based on import.meta.env.VITE_FRAGMENT_MODE. Server bundle (default) unchanged; fragment bundle gets a stripped AppFragment.
  • src/AppFragment.tsx: reads YAML out of location.hash (decoded via lz-string) → renders via existing FlowCanvas / DataInspectionPanel. Header has + New flow / ✎ Edit / ▶ Play. No sidebar — Pages serves one flow per URL.
  • src/lib/share-url.ts: encode/decode helpers on top of lz-string's compressToEncodedURIComponent (URL-safe, ~3× denser than plain base64 on YAML, decompresses to null on bad input).
  • FlowEditorModal gains an optional mode: 'server' | 'fragment' prop. Server mode unchanged. Fragment mode flips the Save button to Copy share URL / Copying…, footer hint to ⌘/Ctrl-Enter to copy URL. AppFragment's save handler builds the share URL, writes to clipboard (with a graceful fallback toast for browsers that block the clipboard API), and updates location.hash so refresh / back / forward / bookmark all work without a server.
  • Corrupt fragment → red banner "this share link looks corrupted — start a new one" + the empty-state CTA. Locked in by tests.

What this PR does NOT do

  • Enable GitHub Pages on the repo. One-time manual flip in Settings → Pages → Source: GitHub Actions. After that, every push to master deploys automatically.
  • Repoint homepageUrl. Audit's B1 follow-up — once Pages is live:
    gh repo edit naorsabag/OpenHop --homepage https://naorsabag.github.io/OpenHop/

Test plan

  • npm test --workspaces235/235 (5 new in share-url.test.ts: round-trip, compression density, empty fragment → null, garbage fragment → null, BASE_URL split for dev vs Pages)
  • Both build variants succeed: default (/) and Pages (VITE_BASE=/OpenHop/ VITE_FRAGMENT_MODE=1)
  • typecheck / format:check / lint clean
  • Both audit gates green (lz-string is dep-tree clean)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Automated GitHub Pages build & publish workflow
    • Fragment-mode app shell for shareable client-side flows with in-app fragment editor
    • Shareable URL encoding/decoding with compression and copy-to-clipboard feedback
    • Mode-aware flow editor labels and footer hints
    • Configurable base path for static deploys
  • Bug Fixes

    • Fixed playback behavior when a flow cycle completes (sub-flow vs root)
  • Tests

    • Added tests for share URL encoding/decoding
  • Chores

    • Added lz-string dependency for compression

Adds a server-less variant of the web bundle: flows live in the URL
hash, "Save" copies a sharable URL to the clipboard, no API server,
no sidebar list. Issue #75.

Plumbing:
- vite.config.ts: env-driven `base` (`VITE_BASE='/OpenHop/'` for the
  Pages build, `'/'` for dev/docker so nothing else changes).
- .github/workflows/pages.yml: build @openhop/web with `VITE_BASE=
  /OpenHop/` and `VITE_FRAGMENT_MODE=1`, upload via
  actions/upload-pages-artifact, deploy via actions/deploy-pages.
  Concurrency cap of 1 per group so a fresh deploy never lands on top
  of an in-flight one. (Repo Settings → Pages → Source: GitHub
  Actions still has to be flipped on once manually.)

Runtime:
- main.tsx routes to one of two app shells based on
  `import.meta.env.VITE_FRAGMENT_MODE`. Server bundle (default)
  unchanged; fragment bundle gets a stripped App.
- src/AppFragment.tsx: reads the YAML out of `location.hash` (decoded
  via lz-string), renders via the existing FlowCanvas /
  DataInspectionPanel. Header has + New flow / ✎ Edit / ▶ Play. No
  sidebar — Pages shows one flow per URL.
- src/lib/share-url.ts: encode/decode helpers built on
  lz-string's `compressToEncodedURIComponent` (URL-safe by design,
  ~3× denser than plain base64 on YAML, decompresses to null on
  bad input).
- FlowEditorModal: optional `mode: 'server' | 'fragment'` prop.
  Server mode unchanged. Fragment mode flips the Save button to
  "Copy share URL" / "Copying…", footer hint to "⌘/Ctrl-Enter to
  copy URL". The save handler in AppFragment builds the share URL,
  writes to clipboard (with a fallback toast for browsers that
  block clipboard API), and updates `location.hash` so refresh /
  back / forward / bookmark all work without a server.
- Corrupt fragment → red banner "this share link looks corrupted —
  start a new one" + the empty-state CTA, per the design call we
  made up front.

Verified:
- shared 93/93, server 19/19, cli 83/83, web 40/40 (5 new in
  share-url.test.ts) = 235/235
- both build variants succeed: default (`/`) and Pages
  (`VITE_BASE=/OpenHop/ VITE_FRAGMENT_MODE=1`)
- typecheck / format / lint clean
- both audit gates green (lz-string is dep-tree clean)

What this PR does NOT do:
- enable GitHub Pages on the repo — that's a one-time manual flip in
  Settings → Pages → Source: GitHub Actions. After that, every push
  to master will deploy automatically.
- repoint `homepageUrl` to the Pages URL (audit's B1 follow-up).
  Once Pages is live, run:
    gh repo edit naorsabag/OpenHop --homepage https://naorsabag.github.io/OpenHop/

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 4bd41fd8-f289-4553-94d3-d2b93d5d6cf6

📥 Commits

Reviewing files that changed from the base of the PR and between b142f47 and c783395.

📒 Files selected for processing (1)
  • packages/web/src/main.tsx

Walkthrough

Adds a GitHub Pages deploy workflow and fragment-mode client UI: lz-string-based fragment encode/decode helpers, AppFragment app shell that reads/writes window.location.hash, FlowEditorModal fragment-mode UX, Vite base configuration for path deploys, lz-string dependency and tests for share URL behavior.

Changes

GitHub Pages Fragment-Mode Deployment & Fragment UI

Layer / File(s) Summary
Build & Deployment
.github/workflows/pages.yml
New GitHub Actions workflow: builds @openhop/web in fragment mode with VITE_BASE=/OpenHop/ and VITE_FRAGMENT_MODE="1", uploads packages/web/dist and deploys via actions/deploy-pages@v4.
Bundling Base Path
packages/web/vite.config.ts
Vite base now reads process.env.VITE_BASE ?? '/' to support path-based GitHub Pages asset paths.
Dependencies
packages/web/package.json
Added lz-string@^1.5.0 and @types/lz-string@^1.5.0.
Fragment encoding utilities
packages/web/src/lib/share-url.ts
New exports: encodeFragment(yamlText), `decodeFragment(fragment): string
Fragment-mode root & wiring
packages/web/src/main.tsx
Conditionally renders AppFragment when import.meta.env.VITE_FRAGMENT_MODE === '1', otherwise renders App.
Fragment App shell
packages/web/src/AppFragment.tsx
New AppFragment component: decodes window.location.hash, validates parsed YAML, manages fragment editor modal, copies built share URL to clipboard, updates window.location.hash, handles playback/drilldown/breadcrumb/back behavior, step inspection, and renders canvas/inspector/modal/toasts.
Editor modal fragment UX
packages/web/src/components/FlowEditorModal.tsx
Added optional prop `mode?: 'server'
Playback lifecycle
packages/web/src/hooks/useFlowAnimation.ts, packages/web/src/App.tsx
useFlowAnimation resets cycle state before invoking onCycleComplete; App distinguishes sub-flow vs root completion (pop sub-flow after delay; stop playback on root cycle complete).
Tests
packages/web/__tests__/share-url.test.ts
Vitest suite validating encodeFragment/decodeFragment round-trip, schema validation after decode, compression characteristic, null handling for empty/malformed fragments, and buildShareUrl composition for dev vs Pages paths.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant Browser
    participant GitHubPages
    participant AppFragment
    participant ShareURL
    participant FlowEditorModal
    participant FlowCanvas

    User->>GitHubPages: Request https://.../OpenHop/#<encoded>
    GitHubPages->>Browser: Serve index.html + assets
    Browser->>AppFragment: Load component
    AppFragment->>Browser: Read window.location.hash
    AppFragment->>ShareURL: decodeFragment(hash)
    ShareURL->>AppFragment: YAML text
    AppFragment->>AppFragment: parseFlowYaml & validate
    AppFragment->>FlowCanvas: Render flow

    User->>FlowCanvas: Click drilldown
    FlowCanvas->>AppFragment: onDrillDown(step)
    AppFragment->>AppFragment: Push stack, update view

    User->>AppFragment: Click "Edit"
    AppFragment->>FlowEditorModal: Open (mode='fragment')
    User->>FlowEditorModal: Edit YAML & Save
    FlowEditorModal->>ShareURL: buildShareUrl(yaml)
    ShareURL->>FlowEditorModal: full URL with `#fragment`
    FlowEditorModal->>Browser: Copy URL to clipboard
    FlowEditorModal->>AppFragment: onSave callback
    AppFragment->>Browser: Update window.location.hash
    Browser->>AppFragment: hashchange event
    AppFragment->>ShareURL: decodeFragment(newHash)
    ShareURL->>AppFragment: new YAML
    AppFragment->>FlowCanvas: Re-render with updated flow
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

Possibly related PRs

  • naorsabag/OpenHop#76: Overlaps with changes to packages/web/src/components/FlowEditorModal.tsx (modal/editor behavior) and is directly related.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(web): GitHub Pages fragment-mode deploy' clearly and concisely describes the main change—adding GitHub Pages fragment-mode deployment support for the web package.
Linked Issues check ✅ Passed The PR successfully implements all core coding requirements from issue #75: fragment-based URL encoding/decoding with lz-string, AppFragment component for client-side flow rendering, conditional rendering via VITE_FRAGMENT_MODE, GitHub Actions workflow for Pages deployment, and FlowEditorModal fragment-mode support with clipboard save.
Out of Scope Changes check ✅ Passed All code changes are directly scoped to the stated objective of enabling GitHub Pages fragment-mode deployment. Changes include the necessary plumbing (vite.config, workflow), runtime components (AppFragment, share-url helpers, main.tsx conditional), and supporting fixtures (tests, modal updates) without unrelated modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/web-pages-fragment-mode

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/web/src/main.tsx`:
- Around line 11-12: Add an ESLint disable for the react-refresh rule at the top
of this entry file so it won't require exported components: in
packages/web/src/main.tsx add a file-level comment disabling
react-refresh/only-export-components, leaving the existing constants
FRAGMENT_MODE and Root (and references to App and AppFragment) unchanged so Fast
Refresh linting is skipped for this entrypoint.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: e65f7cb2-df91-4f7e-a36a-82b19b009b2a

📥 Commits

Reviewing files that changed from the base of the PR and between f82f800 and 9c86ef0.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json, !package-lock.json
📒 Files selected for processing (8)
  • .github/workflows/pages.yml
  • packages/web/__tests__/share-url.test.ts
  • packages/web/package.json
  • packages/web/src/AppFragment.tsx
  • packages/web/src/components/FlowEditorModal.tsx
  • packages/web/src/lib/share-url.ts
  • packages/web/src/main.tsx
  • packages/web/vite.config.ts

Comment thread packages/web/src/main.tsx Outdated
naorsabag and others added 3 commits May 5, 2026 17:53
Before this, handleCycleComplete bailed early when not in a sub-flow,
so the root flow's animation would loop forever and the header
button stayed stuck on "⏸ Pause" — no way to step back to "▶ Play"
without manually clicking it. Sub-flow case unchanged: a child cycle
finishing still pops back to the parent, which then keeps playing
from `resumeFromStep` until the root finishes and now stops.

Same fix in App.tsx and AppFragment.tsx — both share the
play / drilldown state machine.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lean

Follow-up to 005df06. After "stop on root cycle complete" landed, clicking
Play again caused the button to flash from "▶ Play" → "⏸ Pause" → "▶ Play"
instantly without restarting the animation.

Cause: useFlowAnimation kept `stepIndexRef.current` pinned at the last
step when it fired onCycleComplete. On re-play, advanceStep ran with
`rawNext = stepIndexRef.current + 1 === steps.length` again, hit the
end-of-cycle branch, fired onCycleComplete a second time → setPlaying(
false) → flash back to paused.

Fix: when we detect end-of-cycle, reset stepIndexRef + node-progress refs
BEFORE firing the callback (or before falling through to the loop).
The next advanceStep call now sees `rawNext = 0`, plays step 0, and the
flow runs normally. The no-callback (auto-loop) path is unaffected — the
trailing `nextIdx = rawNext % steps.length = 0` and the explicit
`stepIndexRef.current = nextIdx` overwrite still produce the same end
state, just via the explicit reset rather than implicitly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…efresh

CI failure on b142f47: lint reported a `react-refresh/only-export-components`
error on the `const Root = FRAGMENT_MODE ? AppFragment : App` line. The
plugin requires component-shaped consts to be exported (so Fast Refresh
can swap them at edit time); main.tsx is the entry point and shouldn't
export anything.

Cleanest fix: drop the named `Root` const and inline the dispatch in
JSX. Same runtime behavior, no eslint-disable directive needed, no
extra file.

Closes the only outstanding CodeRabbit comment on #77.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@naorsabag
Copy link
Copy Markdown
Owner Author

Both addressed in c783395.

  • CI failure (build & test on node 20 / node 22)react-refresh/only-export-components flagged the const Root = FRAGMENT_MODE ? AppFragment : App line. The plugin requires component-shaped consts to be exported, but main.tsx is the entry point and shouldn't export.
  • CodeRabbit comment on main.tsx:12 — same root cause, same fix.

Resolution: drop the named Root const and inline the dispatch directly in JSX:

createRoot(...).render(
  <StrictMode>
    {import.meta.env.VITE_FRAGMENT_MODE === '1' ? <AppFragment /> : <App />}
  </StrictMode>
)

Same runtime behavior, no eslint-disable directive, no extra file. CI now green on c783395.

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.

feat(web): serve the UI as a static site on GitHub Pages (naorsabag.github.io/openhop/)

1 participant