Skip to content

Add LIVE indicator and finalize front page for production - #6

Draft
fsu9913-gif wants to merge 13 commits into
mainfrom
claude/rent-ruby-dmc-consolidation-bniwny
Draft

Add LIVE indicator and finalize front page for production#6
fsu9913-gif wants to merge 13 commits into
mainfrom
claude/rent-ruby-dmc-consolidation-bniwny

Conversation

@fsu9913-gif

Copy link
Copy Markdown

Summary

Adds visible "LIVE" indicator to rent-ruby.com header and removes the theme toggle decision interface. The site is now ready for production deployment.

Changes

  • LIVE badge: Green pulsing dot indicator in header to clearly mark this as the production rent-ruby.com site
  • Theme toggle removed: Removed A/B theme toggle (ruby vs orange accent) that was used during design review meeting
  • Cleaned up CSS: Removed unused orange theme variant

Visual

The LIVE badge appears in the header with:

  • Green color (#22c55e) with 1px border
  • Pulsing dot animation (2s pulse)
  • Clear "LIVE" label so visitors know this is production

Ready for Deployment

This branch includes:

  • ✅ Front page with building photo, NAP header, contact buttons
  • ✅ Waiting list form with email/API integration
  • ✅ Wizard of Mosswood chat widget (with fallback scripted mode)
  • ✅ Building amenities and walk scores
  • ✅ LIVE indicator for production clarity

All other work (DMC tenant portal, rent-roll artifacts) deferred indefinitely per latest requirements.


Generated by Claude Code

cursoragent and others added 12 commits June 20, 2026 21:18
Rebuild of the live rent-ruby.com front page per markup review:
- Slogans removed; hero leads with address and walk/transit facts
- Logo box removed; sticky header carries name, address, phone, email (NAP)
- Waiting list intake form wired to new /api/waitlist endpoint with mailto fallback
- Amenity rail on the right of the hero and amenity bands at the bottom of
  page one and top of page two
- Call/text and email are large live tel:/mailto: buttons
- Ruby accent by default with an in-page ruby/orange comparison toggle

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMQkVd9hduxce8PdRMHWwY
Pull the 3875 Ruby St primary photo (converted from AVIF to JPEG) into
public/assets and fix the hero image layering so it renders under the
scrim gradients. Add a left-side scrim so the headline stays legible
over the light facade.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMQkVd9hduxce8PdRMHWwY
Floating chat agent scoped to the Ruby Building plus neighborhood
concierge topics: MacArthur BART schedules, Ticketmaster events,
restaurants and shopping within ~2 miles, and Oakland 94609 residential
parking permit rules, each with links. Quick-ask chips for common
questions.

Server side: POST /api/wizard backed by Gemini (gemini-2.5-flash) with
a building-scoped system prompt, conversation logging to a new
wizard_chats table, and lead alerts — new chats, messages containing
contact info, and waitlist signups notify via Google Chat webhook and
email (Resend), configured through GOOGLE_CHAT_WEBHOOK,
WIZARD_ALERT_EMAIL, and RESEND_API_KEY. The widget falls back to
scripted concierge answers when the API is unavailable (static hosting).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMQkVd9hduxce8PdRMHWwY
- Added visible "LIVE" badge with pulsing green dot in header
- Ensures visitors know this is the production rent-ruby.com site
- Removed A/B theme toggle (ruby/orange decision was for review meeting only)
- Removed unused orange theme CSS variant

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMQkVd9hduxce8PdRMHWwY
Comment thread public/front-page-v2.html
for (const part of parts) {
if (/^https?:\/\//.test(part)) {
const a = document.createElement('a');
a.href = part; a.target = '_blank'; a.rel = 'noopener';
Comment thread public/front-page-v2.html Fixed
Comment thread server.ts
Comment on lines +1131 to +1167
app.post("/api/wizard", async (req, res) => {
const { messages, sessionId } = req.body;
if (!Array.isArray(messages) || messages.length === 0) {
return res.status(400).json({ error: "messages array is required" });
}
const session = String(sessionId || "anon").slice(0, 64);
const latest = messages[messages.length - 1];
const latestText = String(latest?.text || "").slice(0, 2000);
const isFirstMessage = !db.prepare("SELECT 1 FROM wizard_chats WHERE session_id = ? LIMIT 1").get(session);
db.prepare("INSERT INTO wizard_chats (session_id, role, text) VALUES (?, 'user', ?)").run(session, latestText);
if (isFirstMessage) {
notifyKeeper("New Wizard of Mosswood chat", `Session ${session}\nFirst question: ${latestText}`);
} else if (CONTACT_INFO_RE.test(latestText)) {
notifyKeeper("Wizard chat left contact info", `Session ${session}\nMessage: ${latestText}`);
}
const apiKey = process.env.GEMINI_API_KEY;
if (!apiKey || apiKey === "MY_GEMINI_API_KEY") {
return res.json({ reply: null, fallback: true });
}
try {
const { GoogleGenAI } = await import("@google/genai");
const ai = new GoogleGenAI({ apiKey });
const response = await ai.models.generateContent({
model: "gemini-2.5-flash",
config: { systemInstruction: WIZARD_SYSTEM_PROMPT, maxOutputTokens: 400 },
contents: messages.slice(-12).map((m: { role?: string; text?: string }) => ({
role: m.role === "model" ? "model" : "user",
parts: [{ text: String(m.text || "").slice(0, 2000) }],
})),
});
const reply = response.text || "";
db.prepare("INSERT INTO wizard_chats (session_id, role, text) VALUES (?, 'model', ?)").run(session, reply.slice(0, 4000));
res.json({ reply });
} catch (e) {
res.json({ reply: null, fallback: true });
}
});
Comment thread server.ts
config: { systemInstruction: WIZARD_SYSTEM_PROMPT, maxOutputTokens: 400 },
contents: messages.slice(-12).map((m: { role?: string; text?: string }) => ({
role: m.role === "model" ? "model" : "user",
parts: [{ text: String(m.text || "").slice(0, 2000) }],
Comment thread server.ts
Comment on lines +1169 to +1172
app.get("/api/wizard/chats", (req, res) => {
const rows = db.prepare("SELECT * FROM wizard_chats ORDER BY created_at DESC LIMIT 500").all();
res.json(rows);
});
Comment thread server.ts
Comment on lines +1174 to +1177
app.get("/api/waitlist", (req, res) => {
const rows = db.prepare("SELECT * FROM waitlist_signups ORDER BY created_at DESC").all();
res.json(rows);
});
Comment thread server.ts
Comment on lines +1179 to +1193
app.post("/api/waitlist", (req, res) => {
const { name, phone, email, desired_move_in, unit_pref, notes } = req.body;
if (!name || !phone || !email) {
return res.status(400).json({ error: "name, phone, and email are required" });
}
const result = db.prepare(`
INSERT INTO waitlist_signups (name, phone, email, desired_move_in, unit_pref, notes)
VALUES (?, ?, ?, ?, ?, ?)
`).run(name, phone, email, desired_move_in || null, unit_pref || null, notes || null);
notifyKeeper(
"New Ruby waiting list signup",
`Name: ${name}\nPhone: ${phone}\nEmail: ${email}\nMove-in: ${desired_move_in || "flexible"}\nUnit: ${unit_pref || "no preference"}\nNotes: ${notes || ""}`
);
res.json({ id: result.lastInsertRowid });
});
Replace Math.random() with crypto.getRandomValues() to address
CodeQL's insecure randomness flag. This ensures session IDs use
proper cryptographic randomness instead of pseudo-random.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FMQkVd9hduxce8PdRMHWwY
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.

4 participants