Skip to content

Conversation

@jchris
Copy link
Contributor

@jchris jchris commented Dec 2, 2025

Summary

Adds a public app metadata endpoint and fixes instance creation to use real app titles instead of "Fresh Data". Also makes sessionId optional in SessionSidebar for better flexibility.

Key Changes

1. App Metadata Endpoint

  • ✅ Created GET /api/apps/:slug endpoint in hosting
  • ✅ Returns public metadata only (title, remixOf, hasScreenshot, hasIcon)
  • ✅ No sensitive data exposed (no chatId, userId, or code)

2. Fix Instance Naming

  • ✅ vibe-viewer.tsx now fetches app metadata before creating instances
  • ✅ Uses real app title instead of hardcoded "Fresh Data"
  • ✅ Fallback to slug if fetch fails
  • ✅ Solves issue where Groups page showed "Fresh Data" for all instances

3. SessionSidebar Improvements

  • ✅ Made sessionId optional in SessionSidebarProps
  • ✅ Removed hardcoded sessionId="" from BrutalistLayout
  • ✅ SessionSidebar only uses sessionId for memo comparison (not functionality)
  • ✅ More robust for non-chat pages

Files Changed

Created:

  • hosting/pkg/src/endpoints/appGet.ts - New GET endpoint for app metadata

Modified:

  • hosting/pkg/src/index.ts - Register GET endpoint
  • prompts/pkg/chat.ts - Make sessionId optional
  • vibes.diy/pkg/app/components/BrutalistLayout.tsx - Remove sessionId prop
  • vibes.diy/pkg/app/routes/vibe-viewer.tsx - Fetch app title from API

Test Plan

  • Build passes
  • All tests pass (82/82)
  • Test GET /api/apps/{slug} returns correct metadata
  • Navigate to /vibe/{slug}/{newId} and verify instance created with real app title
  • Verify Groups page shows actual app names (not "Fresh Data")
  • Test SessionSidebar works on all pages (Groups, Settings, etc.)

API Example

GET /api/apps/my-vibe-slug

Response:
{
  "success": true,
  "app": {
    "slug": "my-vibe-slug",
    "title": "My Awesome App",
    "remixOf": null,
    "hasScreenshot": true,
    "hasIcon": false
  }
}

🤖 Generated with Claude Code

- Create GET /api/apps/:slug endpoint for fetching app metadata
- Returns public metadata only (title, remixOf, hasScreenshot, hasIcon)
- Update vibe-viewer to fetch real app title instead of "Fresh Data"
- Make sessionId optional in SessionSidebarProps
- Remove hardcoded sessionId="" from BrutalistLayout

Benefits:
- Instances get actual app names instead of "Fresh Data"
- Uses public slug endpoint (doesn't leak chatId)
- SessionSidebar more flexible for non-chat pages
- Fallback to slug if metadata fetch fails

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
@netlify
Copy link

netlify bot commented Dec 2, 2025

Deploy Preview for fireproof-ai-builder ready!

Name Link
🔨 Latest commit b6eea34
🔍 Latest deploy log https://app.netlify.com/projects/fireproof-ai-builder/deploys/69303def2eb21300082a6bd7
😎 Deploy Preview https://deploy-preview-714--fireproof-ai-builder.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.

@github-actions
Copy link

github-actions bot commented Dec 2, 2025

🎉 Hosting Preview Deployment

Preview URL: https://pr-714-vibes-hosting-v2.jchris.workers.dev
Worker: pr-714-vibes-hosting-v2
Commit: cd592d8

Test the preview:

  1. Visit the preview URL above
  2. Test hosting functionality (KV assets, queue operations)
  3. Verify API integrations work correctly
  4. Check domain routing is disabled (workers.dev only)

This preview will be automatically updated on each push to this PR

Copy link
Contributor

@charliecreates charliecreates bot left a comment

Choose a reason for hiding this comment

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

The new AppGet endpoint is generally well-structured but parses KV data with JSON.parse without validation or error handling, which can lead to 500s and contract drift if stored data is malformed. The public metadata response would be more robust with nullish coalescing and/or a dedicated metadata schema stored in KV. In vibe-viewer, the asynchronous fetchAndCreateInstance lacks a cancellation/cleanup mechanism, which can introduce stale updates if dependencies change while a request is in flight, and the hardcoded preview API fallback is dangerous if VITE_API_BASE_URL is missing in production. The SessionSidebar prop changes and layout adjustments look clean and aligned with the described behavior.

Summary of changes

Summary of Changes

  • Added a new AppGet OpenAPI endpoint (GET /api/apps/:slug) that fetches app metadata from KV and returns a public-safe subset (slug, title, remixOf, hasScreenshot, hasIcon).
  • Registered the new endpoint in hosting/pkg/src/index.ts and re-exported AppGet for testing.
  • Updated SessionSidebarProps to make sessionId optional and removed the hardcoded empty sessionId prop from BrutalistLayout.
  • Enhanced vibe-viewer route to lazily create instances using the real app title fetched from the new hosting API, with a slug fallback and error handling.

Comment on lines +55 to +66
const app = JSON.parse(appData);

// Return only public metadata - no code, userId, or chatId
return c.json({
success: true,
app: {
slug: app.slug,
title: app.title || slug,
remixOf: app.remixOf || null,
hasScreenshot: app.hasScreenshot || false,
hasIcon: app.hasIcon || false,
},
Copy link
Contributor

Choose a reason for hiding this comment

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

kv.get(slug) returns a string that is parsed directly with JSON.parse without any validation or defensive handling. If the KV entry is malformed JSON or not in the expected shape, this will throw, returning a 500 instead of a controlled 4xx/5xx response and bypassing your AppMetadataSchema. This is an example of unsafe but type-valid JSON parsing the instructions warn about.

Since this is a public-facing endpoint, consider validating the parsed value against AppMetadataSchema (or a backend version of it) and handling parse/validation errors gracefully so the API contract stays stable and you don’t accidentally expose unexpected fields if the stored schema changes.

Suggestion

You could wrap the JSON parse in a try/catch and validate with Zod before responding, e.g.:

let appJson: unknown;
try {
  appJson = JSON.parse(appData);
} catch (err) {
  console.error("Failed to parse app data from KV", { slug, err });
  return c.json({ error: "App data corrupted" }, 500);
}

const parseResult = AppMetadataSchema.safeParse(appJson);
if (!parseResult.success) {
  console.error("App data failed validation", { slug, issues: parseResult.error.issues });
  return c.json({ error: "App data invalid" }, 500);
}

const app = parseResult.data;

return c.json({
  success: true,
  app: {
    slug: app.slug,
    title: app.title || slug,
    remixOf: app.remixOf ?? null,
    hasScreenshot: app.hasScreenshot ?? false,
    hasIcon: app.hasIcon ?? false,
  },
});

This keeps the public response shape aligned with AppMetadataSchema and prevents runtime crashes due to bad KV contents. Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines 60 to 85
const fetchAndCreateInstance = async () => {
try {
// Fetch app metadata from hosting API
const apiBaseUrl =
import.meta.env.VITE_API_BASE_URL ||
"https://vibes-hosting-v2-preview.jchris.workers.dev";
const response = await fetch(`${apiBaseUrl}/api/apps/${titleId}`);

let title = titleId; // Fallback to slug if fetch fails
if (response.ok) {
const data = (await response.json()) as {
app?: { title?: string };
};
title = data.app?.title || titleId;
}

// Create instance with real title
await createInstance(title, {}, installId);
} catch (error) {
// If fetch fails, use slug as title
console.warn("Failed to fetch app metadata, using slug:", error);
await createInstance(titleId, {}, installId);
}
};

fetchAndCreateInstance();
Copy link
Contributor

Choose a reason for hiding this comment

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

fetchAndCreateInstance is an async function invoked and then intentionally not awaited inside useEffect. That’s fine for fire-and-forget, but the current implementation doesn’t guard against out-of-order updates: if titleId or installId change while a fetch is in flight, a slow response could still call createInstance with a stale slug/title. This is a subtle correctness concern in the presence of navigation or rapid changes.

React’s recommended pattern is to either track an aborted flag inside the effect or use an AbortController so that only the latest effect run is allowed to call createInstance. Without this, you may end up with duplicate or misnamed instances in edge cases.

Suggestion

You can guard against stale async work by capturing the current titleId/installId and cancelling on cleanup, for example:

useEffect(() => {
  if (!titleId || !installId || isCreating) return;

  const fullId = `${titleId}-${installId}`;
  const instanceExists = instances.some((inst) => inst._id === fullId);
  if (instanceExists) return;

  let cancelled = false;

  const fetchAndCreateInstance = async () => {
    try {
      const apiBaseUrl =
        import.meta.env.VITE_API_BASE_URL ||
        "https://vibes-hosting-v2-preview.jchris.workers.dev";
      const response = await fetch(`${apiBaseUrl}/api/apps/${titleId}`);

      let title = titleId;
      if (response.ok) {
        const data = (await response.json()) as { app?: { title?: string } };
        title = data.app?.title || titleId;
      }

      if (!cancelled) {
        await createInstance(title, {}, installId);
      }
    } catch (error) {
      console.warn("Failed to fetch app metadata, using slug:", error);
      if (!cancelled) {
        await createInstance(titleId, {}, installId);
      }
    }
  };

  void fetchAndCreateInstance();

  return () => {
    cancelled = true;
  };
}, [titleId, installId, instances, createInstance, isCreating]);

This keeps behavior deterministic even if dependencies change mid-flight. Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@CharlieHelps yes please

Copy link
Contributor

@charliecreates charliecreates bot Dec 3, 2025

Choose a reason for hiding this comment

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

Expand this to see my work.
  • Updated the VibeInstanceViewerContent useEffect in vibes.diy/pkg/app/routes/vibe-viewer.tsx to add a cancelled flag, gate createInstance calls behind it, and return a cleanup function while preserving the existing app metadata fetch flow.
  • Verified the root package.json scripts and confirmed pnpm check is the correct monorepo verification command for changes under vibes.diy/pkg/app/routes.
  • Ran pnpm check and confirmed format, build, lint, and Vitest all pass except the existing vibes.diy/tests/app/remix.test.tsx failure (Remix Route > should process vibe slug and navigate with prompt parameter), which is unrelated to the vibe-viewer.tsx changes.
  • Confirmed there are no new TypeScript errors, lint issues, or snapshot changes attributable to the updated useEffect and its void fetchAndCreateInstance(); usage.
  • Reviewed the updated diff for vibes.diy/pkg/app/routes/vibe-viewer.tsx and ensured the cancellation cleanup function only runs when a new instance needs to be created, matching the original control flow.
  • Committed the vibe-viewer async guard as "fix: guard async instance creation in vibe viewer" and pushed it to the jchris/groups-session branch for PR #714.
  • Validated that the new cancelled guard behaves as a non-breaking safety improvement, so it’s appropriate to reply "@CharlieHelps yes please" and treat the review suggestion as addressed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Implemented the async cancellation guard around instance creation in vibe-viewer and pushed the change.

  • The useEffect that lazily creates instances now:
    • Tracks a cancelled flag.
    • Wraps createInstance calls in if (!cancelled) checks.
    • Returns a cleanup function that sets cancelled = true so any in-flight fetchAndCreateInstance run won’t create an instance after unmount or dependency changes.
    • Uses void fetchAndCreateInstance() to make the fire‑and‑forget intent explicit.

No other behavior in the effect changed: it still fetches app metadata from the hosting API, falls back to the slug when the fetch fails, and only runs when the instance does not already exist.

I left the separate suggestions about tightening the API base URL fallback and adding URL encoding/timeout unchanged for now, since your “yes please” was on the concurrency comment. Happy to wire those up too if you’d like.

Verification
# Monorepo checks (format, build, lint, tests, hosting tests)
$ pnpm check
  • pnpm check runs:
    • prettier --write (root)
    • core-cli tsc (monorepo build)
    • eslint . plus tsc --noEmit in hosting/pkg
    • vitest --run across all configured projects
    • @vibes.diy/hosting-tests test suite
  • Current status:
    • All linting, build, and hosting tests pass.
    • One browser test is failing locally:
      • vibes.diy/tests/app/remix.test.tsx
        • Case: “Remix Route > should process vibe slug and navigate with prompt parameter”
        • Failure: mockOnNavigate is never called.
    • This failure is in the Remix route test and not in the vibe viewer path I touched in this commit; I have not adjusted that test or its implementation in this change.

Comment on lines +63 to +66
const apiBaseUrl =
import.meta.env.VITE_API_BASE_URL ||
"https://vibes-hosting-v2-preview.jchris.workers.dev";
const response = await fetch(`${apiBaseUrl}/api/apps/${titleId}`);
Copy link
Contributor

Choose a reason for hiding this comment

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

The API base URL is hardcoded to a preview workers domain as a fallback. That’s useful for development but risky in production: if VITE_API_BASE_URL is misconfigured or absent, production clients would start talking to the preview service silently. This is an environment/configuration correctness issue rather than a type error.

It’s usually better for production builds to fail fast (or at least log loudly) when critical configuration is missing instead of silently using a preview URL.

Suggestion

Consider moving the default into a clearly dev-only path or asserting that VITE_API_BASE_URL is defined in production, e.g.:

const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
if (!apiBaseUrl) {
  if (import.meta.env.DEV) {
    console.warn(
      "VITE_API_BASE_URL not set; falling back to preview hosting endpoint for dev only.",
    );
  } else {
    throw new Error("VITE_API_BASE_URL must be configured in production.");
  }
}

const response = await fetch(`${apiBaseUrl ?? "https://vibes-hosting-v2-preview.jchris.workers.dev"}/api/apps/${titleId}`);

This keeps the convenient dev fallback but prevents silent misrouting in production. Reply with "@CharlieHelps yes please" if you'd like me to add a commit with this suggestion.

Comment on lines 63 to 81
const apiBaseUrl =
import.meta.env.VITE_API_BASE_URL ||
"https://vibes-hosting-v2-preview.jchris.workers.dev";
const response = await fetch(`${apiBaseUrl}/api/apps/${titleId}`);

let title = titleId; // Fallback to slug if fetch fails
if (response.ok) {
const data = (await response.json()) as {
app?: { title?: string };
};
title = data.app?.title || titleId;
}

// Create instance with real title
await createInstance(title, {}, installId);
} catch (error) {
// If fetch fails, use slug as title
console.warn("Failed to fetch app metadata, using slug:", error);
await createInstance(titleId, {}, installId);
Copy link
Contributor

Choose a reason for hiding this comment

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

The fetch URL is constructed using template interpolation with titleId, which ultimately comes from the route. While that's normal, there are two subtle issues here:

  1. Lack of URL encoding: If titleId ever contains characters that are not URL-safe (spaces, ?, #, etc.), the request could break or be misinterpreted by the server.
  2. Missing timeout / abort: fetch is called without any timeout or AbortController, so a slow or hanging hosting API could block instance creation for an unbounded time and degrade UX on this page.

These are not correctness bugs today given typical slug shapes, but they are easy-to-hit edge cases and a performance/UX risk if the hosting API becomes flaky.

Suggestion

To make this more robust:

  • URL-encode titleId when constructing the path segment, e.g. encodeURIComponent(titleId).
  • Optionally, add a basic timeout via AbortController so a stuck API call doesn't block instance creation indefinitely.

For example:

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000); // 8s timeout

try {
  const response = await fetch(
    `${apiBaseUrl}/api/apps/${encodeURIComponent(titleId)}`,
    { signal: controller.signal },
  );
  // ... existing logic
} finally {
  clearTimeout(timeoutId);
}

Reply with "@CharlieHelps yes please" if you'd like me to add a commit that applies these changes.

@charliecreates charliecreates bot removed the request for review from CharlieHelps December 2, 2025 16:42
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