feat(web): GitHub Pages fragment-mode deploy (closes #75)#77
Conversation
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>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
WalkthroughAdds 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. ChangesGitHub Pages Fragment-Mode Deployment & Fragment UI
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json,!package-lock.json
📒 Files selected for processing (8)
.github/workflows/pages.ymlpackages/web/__tests__/share-url.test.tspackages/web/package.jsonpackages/web/src/AppFragment.tsxpackages/web/src/components/FlowEditorModal.tsxpackages/web/src/lib/share-url.tspackages/web/src/main.tsxpackages/web/vite.config.ts
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>
|
Both addressed in c783395.
Resolution: drop the named 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. |
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.
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-drivenbase—VITE_BASE='/OpenHop/'for the Pages build,'/'for dev / docker /npx openhop demo(unchanged)..github/workflows/pages.yml: build@openhop/webwithVITE_BASE=/OpenHop/andVITE_FRAGMENT_MODE=1, upload viaactions/upload-pages-artifact, deploy viaactions/deploy-pages. Concurrency cap of 1 per group so a fresh deploy never lands on top of an in-flight one.Runtime
main.tsxroutes to one of two app shells based onimport.meta.env.VITE_FRAGMENT_MODE. Server bundle (default) unchanged; fragment bundle gets a strippedAppFragment.src/AppFragment.tsx: reads YAML out oflocation.hash(decoded via lz-string) → renders via existingFlowCanvas/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 oflz-string'scompressToEncodedURIComponent(URL-safe, ~3× denser than plain base64 on YAML, decompresses tonullon bad input).FlowEditorModalgains an optionalmode: '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 updateslocation.hashso refresh / back / forward / bookmark all work without a server.What this PR does NOT do
Settings → Pages → Source: GitHub Actions. After that, every push to master deploys automatically.homepageUrl. Audit's B1 follow-up — once Pages is live:Test plan
npm test --workspaces→ 235/235 (5 new inshare-url.test.ts: round-trip, compression density, empty fragment → null, garbage fragment → null, BASE_URL split for dev vs Pages)/) and Pages (VITE_BASE=/OpenHop/ VITE_FRAGMENT_MODE=1)typecheck/format:check/lintcleanlz-stringis dep-tree clean)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Bug Fixes
Tests
Chores