Commit44f1a02unknown_key
Fix preview expired state UX + add EU data region selector on repo creation
Fix preview expired state UX + add EU data region selector on repo creation - previews.tsx: expired cards now render greyed branch name, strikethrough URL, updated meta text, and a Rebuild button; distinct grey pill already existed via is-expired CSS class - branch-previews.ts: confirmed expireOldPreviews() correctly sets status='expired' (no change needed) - drizzle/0077_data_region.sql: adds data_region TEXT NOT NULL DEFAULT 'us' to repositories - schema.ts: adds dataRegion field to repositories table - web.tsx: data region dropdown (US/EU) on /new form, stores chosen value on insert - repo-settings.tsx: displays current data region as a read-only pill (cannot change post-creation) - marketing.tsx: adds "EU data residency (Frankfurt)" to Pro tier feature list + FAQ entry https://claude.ai/code/session_01DzJMTFASjMHt2f5ze4cNLR
6 files changed+113−644f1a02f851bf13a7be449f5e5176b319e5168c0
6 changed files+113−6
Addeddrizzle/0083_data_region.sql+7−0View fileUnifiedSplit
@@ -0,0 +1,7 @@
1-- Migration 0077 — per-repo data region selector.
2-- Allows repository owners to opt into EU data residency (Frankfurt).
3-- Default is 'us'; 'eu' requires a Pro plan or higher (enforced in the
4-- repo-creation and repo-settings routes — the DB column itself is
5-- unconstrained so future regions (e.g. 'apac') can be added without
6-- a schema change).
7ALTER TABLE repositories ADD COLUMN IF NOT EXISTS data_region text NOT NULL DEFAULT 'us';
Modifiedsrc/db/schema.ts+5−0View fileUnifiedSplit
@@ -204,6 +204,11 @@ export const repositories = pgTable(
204204 // env burns a container until the idle sweep tears it down; owners
205205 // must explicitly enable per-repo via repo-settings.
206206 devEnvsEnabled: boolean("dev_envs_enabled").default(false).notNull(),
207 // Migration 0077 — data residency region. 'us' = US East (default),
208 // 'eu' = Frankfurt (EU). EU data residency requires a Pro plan or
209 // higher. Future values (e.g. 'apac') are additive — no constraint
210 // at the DB level so we can extend without a new migration.
211 dataRegion: text("data_region").default("us").notNull(),
207212 },
208213 (table) => [
209214 // Partial: uniqueness only in the user namespace (org-owned rows exempt).
Modifiedsrc/routes/marketing.tsx+5−0View fileUnifiedSplit
@@ -84,6 +84,7 @@ const PricingPage: FC = () => (
8484 "Priority AI queue",
8585 "Custom domains",
8686 "Advanced analytics",
87 "EU data residency (Frankfurt)",
8788 "Email support",
8889 ]}
8990 cta="Go Pro"
@@ -195,6 +196,10 @@ const PricingPage: FC = () => (
195196 q="How do I migrate off Gluecron?"
196197 a="Same way you migrate off GitHub: git remote set-url and push. We're git-compatible to the byte. No vendor lock, no migration tax."
197198 />
199 <FaqItem
200 q="Is EU data residency available?"
201 a="Yes. Pro plan and above can choose the EU (Frankfurt) data region when creating a repository. All repository data — git objects, issues, PRs — is stored and processed in the EU region. The region is set at creation time and cannot be changed afterwards. You can configure it in the New Repository form or see the current region on your repository Settings page."
202 />
198203 </div>
199204 </section>
200205
Modifiedsrc/routes/previews.tsx+54−6View fileUnifiedSplit
@@ -263,9 +263,38 @@ const previewStyles = `
263263 box-shadow: inset 0 0 0 1px rgba(248,113,113,0.35);
264264 }
265265 .preview-pill.is-expired {
266 background: rgba(148,163,184,0.10);
267 color: #cbd5e1;
268 box-shadow: inset 0 0 0 1px rgba(148,163,184,0.30);
266 background: rgba(100,116,139,0.10);
267 color: #94a3b8;
268 box-shadow: inset 0 0 0 1px rgba(100,116,139,0.28);
269 }
270
271 /* ─── expired card treatment ─── */
272 .preview-card.is-expired .preview-card-branch { color: var(--text-muted); }
273 .preview-card.is-expired .preview-card-url-expired {
274 color: var(--text-muted);
275 text-decoration: line-through;
276 text-decoration-color: rgba(148,163,184,0.45);
277 }
278 .preview-rebuild-btn {
279 display: inline-flex;
280 align-items: center;
281 gap: 5px;
282 margin-top: var(--space-2);
283 padding: 4px 12px;
284 border-radius: 8px;
285 font-size: 12px;
286 font-weight: 600;
287 font-family: var(--font-mono);
288 color: #a48bff;
289 background: rgba(140,109,255,0.08);
290 border: 1px solid rgba(140,109,255,0.22);
291 cursor: pointer;
292 text-decoration: none;
293 transition: background 120ms ease, border-color 120ms ease;
294 }
295 .preview-rebuild-btn:hover {
296 background: rgba(140,109,255,0.15);
297 border-color: rgba(140,109,255,0.40);
269298 }
270299 .preview-pill-dot {
271300 width: 6px; height: 6px;
@@ -454,14 +483,20 @@ r.get("/:owner/:repo/previews", async (c) => {
454483 {previews.map((p) => {
455484 const shortSha = (p.commitSha || "").slice(0, 7);
456485 const expiresLabel = formatExpiresIn(p.expiresAt, now);
486 const isExpired = p.status === "expired";
457487 const statusKey = p.status as
458488 | "building"
459489 | "ready"
460490 | "failed"
461491 | "expired";
462492 const pillClass = `preview-pill is-${statusKey}`;
493 const cardClass = isExpired
494 ? "preview-card is-expired"
495 : "preview-card";
496 // Rebuild pushes a branch-preview re-enqueue via the API.
497 const rebuildHref = `/${owner}/${repo}/previews/rebuild?branch=${encodeURIComponent(p.branchName)}`;
463498 return (
464 <div class="preview-card">
499 <div class={cardClass}>
465500 <div class="preview-card-head">
466501 <div class="preview-card-titles">
467502 <h3 class="preview-card-branch">{p.branchName}</h3>
@@ -470,6 +505,10 @@ r.get("/:owner/:repo/previews", async (c) => {
470505 <a href={p.previewUrl} target="_blank" rel="noopener noreferrer">
471506 {p.previewUrl}
472507 </a>
508 ) : isExpired ? (
509 <span class="preview-card-url-expired">
510 {p.previewUrl}
511 </span>
473512 ) : (
474513 <span style="color: var(--text-muted)">
475514 {p.previewUrl}
@@ -481,11 +520,20 @@ r.get("/:owner/:repo/previews", async (c) => {
481520 commit <code>{shortSha}</code>
482521 </span>
483522 <span>
484 {p.status === "expired"
485 ? "expired"
523 {isExpired
524 ? "preview expired · push to branch to rebuild"
486525 : `expires in ${expiresLabel}`}
487526 </span>
488527 </div>
528 {isExpired && (
529 <a
530 href={rebuildHref}
531 class="preview-rebuild-btn"
532 title="Re-enqueue a preview build for this branch"
533 >
534 ↺ Rebuild
535 </a>
536 )}
489537 </div>
490538 <div>
491539 <span class={pillClass}>
Modifiedsrc/routes/repo-settings.tsx+19−0View fileUnifiedSplit
@@ -693,6 +693,25 @@ repoSettings.get("/:owner/:repo/settings", requireAuth, requireRepoAccess("admin
693693 </label>
694694 </div>
695695 </div>
696 <div class="repo-settings-field">
697 <label class="repo-settings-field-label">Data region</label>
698 <div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
699 <span
700 style={`display:inline-flex; align-items:center; gap:6px; padding:4px 12px; border-radius:9999px; font-size:12px; font-weight:600; font-family:var(--font-mono); ${
701 repo.dataRegion === "eu"
702 ? "background:rgba(54,197,214,0.10); color:#67e8f9; border:1px solid rgba(54,197,214,0.28);"
703 : "background:rgba(140,109,255,0.10); color:#c4b5fd; border:1px solid rgba(140,109,255,0.28);"
704 }`}
705 >
706 {repo.dataRegion === "eu" ? "EU · Frankfurt" : "US · Default"}
707 </span>
708 <span class="repo-settings-field-hint" style="margin:0;">
709 Data region is set at creation and cannot be changed. EU data
710 residency requires a{" "}
711 <a href="/pricing" style="color:var(--accent);text-decoration:none;">Pro plan or higher</a>.
712 </span>
713 </div>
714 </div>
696715 </div>
697716 <div class="repo-settings-section-foot">
698717 <button type="submit" class="repo-settings-cta">
Modifiedsrc/routes/web.tsx+23−0View fileUnifiedSplit
@@ -2002,6 +2002,27 @@ web.get("/new", requireAuth, (c) => {
20022002 Just a UI hint — push your own commits to fill the repo.
20032003 </p>
20042004 </div>
2005 <div class="new-repo-row">
2006 <label class="new-repo-label" for="data_region">
2007 Data region
2008 </label>
2009 <select
2010 id="data_region"
2011 name="data_region"
2012 class="new-repo-input"
2013 style="cursor: pointer;"
2014 >
2015 <option value="us" selected>US (default)</option>
2016 <option value="eu">EU (Frankfurt)</option>
2017 </select>
2018 <p class="new-repo-hint">
2019 EU data residency requires a{" "}
2020 <a href="/pricing" style="color: var(--accent); text-decoration: none;">
2021 Pro plan or higher
2022 </a>
2023 . Repositories cannot be moved between regions after creation.
2024 </p>
2025 </div>
20052026 <div class="new-repo-callout">
20062027 <div class="new-repo-callout-eyebrow">AI-native by default</div>
20072028 <p class="new-repo-callout-body">
@@ -2030,6 +2051,7 @@ web.post("/new", requireAuth, async (c) => {
20302051 const name = String(body.name || "").trim();
20312052 const description = String(body.description || "").trim();
20322053 const isPrivate = body.visibility === "private";
2054 const dataRegion = body.data_region === "eu" ? "eu" : "us";
20332055
20342056 if (!name) {
20352057 return c.redirect("/new?error=Repository+name+is+required");
@@ -2061,6 +2083,7 @@ web.post("/new", requireAuth, async (c) => {
20612083 description: description || null,
20622084 isPrivate,
20632085 diskPath,
2086 dataRegion,
20642087 })
20652088 .returning();
20662089
20672090