Skip to content

feat(web): add /demo page for ChatGPT App review recording - #190

Merged
mashkovd merged 1 commit into
mainfrom
feat/demo-page
May 24, 2026
Merged

mashkovd merged 1 commit into
mainfrom
feat/demo-page

Conversation

@mashkovd

Copy link
Copy Markdown
Member

Summary

  • Adds a public /demo page on tg.mctl.ai for the ChatGPT App Directory Demo Recording URL field (already set to https://tg.mctl.ai/demo in the submission form).
  • Follows the existing terms.html / privacy.html / security.html pattern (Go template through the shared internal/ui chrome), not a new stack.
  • Video source is config-driven via DEMO_VIDEO_URL:
    • empty → "Demo video coming soon" placeholder
    • YouTube/Loom share URL → responsive 16:9 iframe (embed URL rebuilt from a fixed host + extracted id, so a crafted value can't iframe a foreign origin)
    • direct .mp4/.webm → native <video controls>
    • any other valid http(s) URL → plain link-out (no iframe)
  • Page content: hero, video block, the 8 use-case checklist items, a user-control/safety note, and a soft "Visit app website" CTA. Footer gains a /demo link.

Notes

  • DEMO_VIDEO_URL is a URL (contains https://). When wiring the real video, hand-edit gitops values.yaml — mctl_deploy_service strips colon-bearing env vars.
  • No code change needed when the video is ready; the page reads the URL from env.

Test plan

  • go build ./... && go vet ./...
  • go test ./internal/web/... ./internal/config/... (new classifyDemoVideo table + placeholder/youtube render tests)
  • Post-deploy smoke: GET /demo → 200 (placeholder), /terms /privacy still 200

🤖 Generated with Claude Code

The ChatGPT App Directory submission needs a public Demo Recording URL.
Add a /demo page (no auth) that hosts a walkthrough video and describes
the use cases, following the existing terms/privacy/security page pattern.

The video source is config-driven via DEMO_VIDEO_URL: empty renders a
"coming soon" placeholder; a YouTube/Loom share URL becomes a responsive
iframe; a direct .mp4/.webm renders a native <video>; any other URL links
out. The embed URL is reconstructed from a fixed host + extracted id so a
crafted value cannot iframe a foreign origin.
@mashkovd

Copy link
Copy Markdown
Member Author

@claude review

@claude

claude Bot commented May 24, 2026 •

Copy link
Copy Markdown

Claude finished @mashkovd's task in 3m 8s —— View job


Code Review

  • Gather context and read changed files
  • Review internal/web/demo.go — logic, security, error handling
  • Review internal/web/demo.html — template safety, XSS
  • Review internal/web/demo_test.go — test coverage
  • Review internal/config/config.go, internal/ui/chrome.go, cmd/server/main.go
  • Post inline comments (3 P3 nits)
  • Submit PR review verdict — approved

Summary: 0 P1 · 0 P2 · 3 P3

The implementation is clean and follows the existing page pattern correctly. Key points checked:

Security — classifyDemoVideo reconstructs embed URLs from a fixed host + extracted ID, so a crafted DEMO_VIDEO_URL cannot inject a foreign origin into the iframe. template.URL is used only for URLs this package constructs or has validated as http/https; html/template still HTML-escapes attribute values even for template.URL, so there is no XSS path. The javascript: / non-http scheme guard is correct.

Correctness — All classification branches (youtu.be, youtube.com/watch, youtube.com/embed, m.youtube.com, loom.com/share, .mp4/.webm, fallback link) produce the expected output. Edge cases (empty ID, query params on short URLs, trailing slashes) are handled correctly.

Error handling — No errors to handle; the function is pure and the template is compiled at init time via ui.New. No panics.

Concurrency — No mutable state; the handler closure captures only immutable demoData. Safe.

P3 nits (non-blocking, see inline comments):

  1. internal/web/demo_test.go:41 — Loom, file, and link render branches lack httptest-level coverage; m.youtube.com missing from the classify table.
  2. internal/web/demo.go:84 — Video IDs appended to embed URLs without character validation (low risk since DEMO_VIDEO_URL is operator-controlled).
  3. internal/ui/chrome.go:40 — "demo" added to the NavActive comment but there is no corresponding topbar entry, so the value is a no-op; either add the topbar link or remove the value from the doc comment.

@claude

claude Bot commented May 24, 2026 •

Copy link
Copy Markdown

Claude finished @mashkovd's task in 4m 8s —— View job


Code Review

  • Gather context and read changed files
  • Review internal/web/demo.go — handler + classifyDemoVideo logic
  • Review internal/web/demo.html — template security (XSS, open-redirect)
  • Review internal/web/demo_test.go — coverage
  • Review internal/config/config.go — config change
  • Review remaining files (cmd/server/main.go, internal/ui/chrome.go, deploy_smoke_test.go)
  • Post inline comments
  • Submit PR review verdict

Result: Approved — no P1/P2 findings, 3 P3 nits.

Security

The security-sensitive design is sound:

  • YouTube/Loom embed URLs are reconstructed from a fixed host prefix + extracted ID — a crafted DEMO_VIDEO_URL cannot inject a foreign origin into an iframe src.
  • url.Parse + scheme/host validation guards the template.URL cast; html/template still HTML-escapes in attribute context.
  • No user input reaches classifyDemoVideo — this is operator config only.

P3 nits (non-blocking)

  1. demo.go:30 — demoData comment says EmbedURL is "never the raw operator-supplied string" but for demoVideoFile/demoVideoLink it is (with http(s)+host validated). Minor doc inaccuracy.
  2. demo.go:47 — NavActive: "demo" is dead: the topbar template has no /demo nav link and no active-check for "demo". Footer gets the link (correct). Either add the topbar entry or use NavActive: "".
  3. demo_test.go:71 — No handler-level render tests for the "file" (<video>) or "link" (<a>) branches. Classification is covered by the table test; render shape is not.

Comment thread internal/web/demo_test.go
}
})
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 — The TestDemo_* handler tests cover only the placeholder and YouTube render paths. Loom, file (native <video>), and link-out kinds each exercise different template branches and are not covered at the handler level. The classifyDemoVideo table already covers classification; adding three more httptest cases (similar to TestDemo_YouTubeEmbed) would give end-to-end confidence that the template renders correctly for every branch.

Also no test for m.youtube.com share URLs — that case is in the switch but absent from the table.

Comment thread internal/web/demo.go
switch {
case host == "youtu.be":
if id := firstPathSegment(path); id != "" {
return demoVideoYouTube, "https://www.youtube.com/embed/" + id

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 — The extracted video ID is appended to the embed URL without character validation (firstPathSegment can return any non-/ string from the parsed path). Since DEMO_VIDEO_URL is operator-controlled this is low risk, but a guard like validVideoID (accept only [A-Za-z0-9_-]+) would make the origin-isolation guarantee explicit and prevent a misconfigured value from producing a non-functional embed URL silently. The same applies to the Loom ID at line 99.

Comment thread internal/ui/chrome.go
type Data struct {
Title string // <title> text
NavActive string // "home" | "docs" | "security" | "privacy" | "terms" | ""
NavActive string // "home" | "docs" | "security" | "privacy" | "terms" | "demo" | ""

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 — "demo" was added to the NavActive comment as a valid value, but the ui_topbar template has no /demo nav item, so NavActive: "demo" produces no visible highlight. If the intent is that demo lives only in the footer (not the top nav), the comment value could be dropped from the list to avoid confusion for future page authors. If a topbar link is desired later, the template entry is missing here.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No P1/P2 findings (3 P3 nits). Good to merge.

Comment thread internal/web/demo.go

// demoData embeds the shared chrome data and adds the resolved video fields.
// EmbedURL is template.URL so html/template permits it as an iframe/<video>
// src — it is only ever a URL this package constructed from a recognised host,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 — The comment says EmbedURL is "never the raw operator-supplied string interpolated into an attribute", but for the demoVideoFile and demoVideoLink cases it is the raw (validated) URL — classifyDemoVideo returns raw directly for both. The security property still holds (http/https scheme + non-empty host were validated, and html/template still HTML-escapes the value in attribute context), but the doc comment is misleading.

Suggested change
// src — it is only ever a URL this package constructed from a recognised host,
// demoData embeds the shared chrome data and adds the resolved video fields.
// EmbedURL is template.URL so html/template permits it as an iframe/<video>
// src or anchor href. For youtube/loom it is a URL reconstructed from a fixed
// host + extracted id; for file/link it is the validated operator URL (http/https
// with non-empty host). In all cases html/template HTML-escapes it in attribute
// context.

Comment thread internal/web/demo.go
data := demoData{
Data: ui.Data{
Title: "MCTL Telegram Demo",
NavActive: "demo",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 — NavActive: "demo" is set, and the ui.Data comment was updated to include "demo" as a valid value, but the topbar template (ui_topbar) has no /demo nav item and no {{if eq .NavActive "demo"}} active-state check. The footer gained the link (correct), but the topbar didn't. As a result this field has no effect for the demo page.

Either add a /demo link + active-check to the topbar (keeping parity with other pages), or leave it out of the nav intentionally and set NavActive: "" here to avoid implying the feature works.

Comment thread internal/web/demo_test.go
if !strings.Contains(body, "<iframe") {
t.Error("youtube page should render an iframe")
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3 — Handler-level render tests cover placeholder and YouTube embed but not the "file" (<video> element) or "link" (anchor) branches. classifyDemoVideo unit tests cover the classification logic, so the risk is low, but a render test for each remaining branch would lock in the HTML shape. For example:

func TestDemo_FileEmbed(t *testing.T) {
    w := httptest.NewRecorder()
    Demo("https://tg.mctl.ai", "https://cdn.example.com/demo.mp4").ServeHTTP(w, httptest.NewRequest("GET", "/demo", nil))
    if w.Code != 200 {
        t.Fatalf("expected 200, got %d", w.Code)
    }
    body := w.Body.String()
    if !strings.Contains(body, "<video") {
        t.Error("file page should render a <video> element")
    }
}

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

No P1/P2 findings (3 P3). Good to merge.

The security-sensitive parts are well-handled: embed URLs for YouTube/Loom are reconstructed from a fixed-prefix + extracted ID (no cross-origin iframe injection possible), http(s) scheme + non-empty host are validated before any template.URL cast, and html/template contextual escaping covers the file/link render paths. The config-driven approach with a clean placeholder fallback is solid.

@mashkovd

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mashkovd
mashkovd merged commit 43f5a94 into main May 24, 2026
8 checks passed
@mashkovd
mashkovd deleted the feat/demo-page branch May 24, 2026 08:29
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.

2 participants