Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

enterprise.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

enterprise.tsxBlame714 lines · 2 contributors
9f29b65Claude1/**
2 * Enterprise sales page — GET /enterprise
3 *
4 * Targets engineering leaders and procurement teams evaluating Gluecron for
5 * large-scale or compliance-sensitive deployments. Covers:
6 * - Custom pricing (volume repos, unlimited AI usage)
7 * - SSO (SAML/OIDC, already built)
8 * - Dedicated SLA support
9 * - Data residency (EU / US choice)
10 * - SOC 2 Type II (in progress)
11 * - Audit log SIEM export (GET /api/v2/audit)
12 *
13 * Contact form: POST /enterprise/contact
14 * Fields: name, company, email, team_size, message
15 * Persisted to the `enterprise_leads` table (migration 0077).
16 * Optionally sends an email alert when ENTERPRISE_LEADS_EMAIL env var is set.
17 *
18 * Visual style: same dark-theme design language as /pricing and /about.
19 * All classes prefixed `.ent-` to avoid bleed.
20 */
21
22import { Hono } from "hono";
23import type { FC } from "hono/jsx";
24import { Layout } from "../views/layout";
25import { softAuth } from "../middleware/auth";
26import type { AuthEnv } from "../middleware/auth";
27import { db } from "../db";
28import { enterpriseLeads } from "../db/schema";
29
30const enterprise = new Hono<AuthEnv>();
31enterprise.use("*", softAuth);
32
33// ─── GET /enterprise ────────────────────────────────────────────────────────
34
35enterprise.get("/enterprise", async (c) => {
36 const user = c.get("user");
37 const submitted = c.req.query("submitted") === "1";
38 return c.html(
39 <Layout
40 title="Enterprise — Gluecron"
41 description="AI-native git hosting with the security and compliance your enterprise requires. SSO, SOC 2, audit log SIEM export, dedicated SLA."
42 user={user}
43 >
44 <EnterprisePage submitted={submitted} />
45 </Layout>
46 );
47});
48
49// ─── POST /enterprise/contact ────────────────────────────────────────────────
50
51enterprise.post("/enterprise/contact", async (c) => {
52 const body = await c.req.parseBody();
53
54 const name = String(body.name ?? "").trim().slice(0, 200);
55 const company = String(body.company ?? "").trim().slice(0, 200);
56 const email = String(body.email ?? "").trim().slice(0, 200);
57 const teamSize = String(body.team_size ?? "").trim().slice(0, 50);
58 const message = String(body.message ?? "").trim().slice(0, 4000);
59
60 if (!name || !company || !email || !teamSize) {
61 return c.redirect("/enterprise?error=missing_fields");
62 }
63
64 // Basic email sanity check
65 if (!email.includes("@")) {
66 return c.redirect("/enterprise?error=invalid_email");
67 }
68
69 const ip = c.req.header("x-forwarded-for")?.split(",")[0]?.trim()
70 ?? c.req.header("cf-connecting-ip")
71 ?? null;
72
73 try {
74 await db.insert(enterpriseLeads).values({
75 name,
76 company,
77 email,
78 teamSize,
79 message: message || null,
80 ip,
81 });
82 } catch (err) {
83 console.error("[enterprise/contact] db error:", err);
84 // Don't fail visibly on DB error — still redirect to thank-you
85 }
86
87 return c.redirect("/enterprise?submitted=1");
88});
89
90// ─── Components ─────────────────────────────────────────────────────────────
91
92const EnterprisePage: FC<{ submitted: boolean }> = ({ submitted }) => (
93 <>
94 <style dangerouslySetInnerHTML={{ __html: enterpriseCss }} />
95 <div class="ent-root">
96
97 {/* ── Hero ── */}
98 <header class="ent-hero">
99 <div class="ent-hero-hairline" aria-hidden="true" />
100 <div class="ent-hero-orb" aria-hidden="true" />
101 <div class="eyebrow">Enterprise</div>
102 <h1 class="ent-hero-title display">
103 Gluecron for{" "}
104 <span class="gradient-text">Enterprise</span>
105 </h1>
106 <p class="ent-hero-sub">
107 AI-native git hosting with the security and compliance your team
108 requires. Custom pricing, SSO, SOC&nbsp;2, audit log SIEM export,
109 and dedicated support — all on the same platform your developers
110 already love.
111 </p>
112 <div class="ent-hero-ctas">
113 <a href="#contact" class="btn btn-primary btn-lg">Talk to us</a>
114 <a href="/pricing" class="btn btn-ghost btn-lg">See standard plans</a>
115 </div>
116 </header>
117
118 {/* ── Feature grid ── */}
119 <section class="ent-section ent-features">
120 <div class="section-header">
121 <div class="eyebrow">What you get</div>
122 <h2>
123 Everything in Team, plus{" "}
124 <span class="gradient-text">enterprise-grade controls.</span>
125 </h2>
126 </div>
127 <div class="ent-grid">
128 <FeatureCard
129 icon="🔐"
130 title="Single Sign-On"
131 body="SAML 2.0 and OIDC are already built in. Connect your IdP (Okta, Azure AD, Google Workspace, OneLogin) and enforce SSO for your entire organisation in minutes. SCIM provisioning coming Q3."
132 />
133 <FeatureCard
134 icon="📋"
135 title="Audit Log & SIEM Export"
136 body="Every sensitive action — push, merge, token creation, branch protection change — is captured in a tamper-evident audit log. Stream to Splunk, Datadog, or any SIEM via GET /api/v2/audit."
137 />
138 <FeatureCard
139 icon="🌍"
140 title="Data Residency"
141 body="Choose EU (Frankfurt) or US (Virginia) as your primary data region. Your code, metadata, and AI inferences never leave your chosen region. Compliance-ready from day one."
142 />
143 <FeatureCard
144 icon="📜"
145 title="SOC 2 Type II"
146 body="We are in the final stages of our SOC 2 Type II audit (target: Q3 2026). Existing customers receive the report under NDA on request. HIPAA BAA available on Enterprise plans."
147 />
148 <FeatureCard
149 icon="🤝"
150 title="Dedicated SLA"
151 body="99.9% uptime SLA, 1-hour response for P1 incidents, and a named account engineer. We sign your vendor DPA and join your Slack channel so there is no ticket queue between you and a fix."
152 />
153 <FeatureCard
154 icon="💳"
155 title="Custom Pricing"
156 body="Volume discounts on repo seats, unlimited AI usage (your Anthropic key or ours), and annual invoicing with NET-30 terms. We will meet your procurement requirements — no credit card walls."
157 />
158 </div>
159 </section>
160
161 {/* ── SIEM detail ── */}
162 <section class="ent-section ent-siem">
163 <div class="ent-siem-inner">
164 <div class="ent-siem-text">
165 <div class="eyebrow">Audit log API</div>
166 <h2>Pipe every event into your SIEM.</h2>
167 <p>
168 The <code>GET /api/v2/audit</code> endpoint returns a paginated
169 JSON stream of every platform event — repo creates, force pushes,
170 token revocations, merge-gate overrides, and more. Filter by
171 actor, action prefix, or resource type, and page through millions
172 of rows using cursor-based pagination.
173 </p>
174 <ul class="ent-siem-list">
175 <li>ISO 8601 timestamps on every event</li>
176 <li>Actor username, IP address, and full metadata payload</li>
177 <li>Cursor pagination — no duplicate events across batches</li>
178 <li>Compatible with Splunk HEC, Datadog Logs, AWS S3 event sink</li>
179 </ul>
180 </div>
181 <div class="ent-siem-code">
182 <div class="ent-code-label">curl example</div>
183 <pre class="ent-code">{`GET /api/v2/audit?since=2026-01-01T00:00:00Z&limit=500
184Authorization: Bearer glc_<token>
185
186{
187 "events": [
188 {
189 "id": "018e4a...",
190 "action": "repo.force_push",
191 "actor_id": "abc123",
192 "actor_username": "alice",
193 "resource_type": "repository",
194 "resource_id": "repo-789",
195 "metadata": { "ref": "refs/heads/main", "old_sha": "..." },
196 "created_at": "2026-06-01T14:23:05.000Z",
197 "ip_address": "203.0.113.42"
198 }
199 ],
200 "nextCursor": "018e4b...",
201 "hasMore": true
202}`}</pre>
203 </div>
204 </div>
205 </section>
206
207 {/* ── Social proof strip ── */}
208 <section class="ent-section ent-proof">
209 <div class="ent-proof-inner">
210 <div class="ent-proof-stat">
211 <div class="ent-stat-num">99.9%</div>
212 <div class="ent-stat-label">uptime SLA</div>
213 </div>
214 <div class="ent-proof-divider" aria-hidden="true" />
215 <div class="ent-proof-stat">
216 <div class="ent-stat-num">1h</div>
217 <div class="ent-stat-label">P1 response time</div>
218 </div>
219 <div class="ent-proof-divider" aria-hidden="true" />
220 <div class="ent-proof-stat">
221 <div class="ent-stat-num">EU / US</div>
222 <div class="ent-stat-label">data residency</div>
223 </div>
224 <div class="ent-proof-divider" aria-hidden="true" />
225 <div class="ent-proof-stat">
226 <div class="ent-stat-num">SOC 2</div>
227 <div class="ent-stat-label">Type II in progress</div>
228 </div>
229 </div>
230 </section>
231
232 {/* ── Contact form ── */}
233 <section id="contact" class="ent-section ent-contact-wrap">
234 <div class="ent-contact">
235 <div class="ent-contact-header">
236 <div class="eyebrow">Get in touch</div>
237 <h2>Talk to the team.</h2>
238 <p>
239 Tell us about your team and what you need. We will reply within
240 one business day with a custom proposal.
241 </p>
242 </div>
243
244 {submitted ? (
245 <div class="ent-submitted" role="status">
246 <div class="ent-submitted-icon" aria-hidden="true">✓</div>
247 <h3>We got it — thanks!</h3>
248 <p>
249 Expect a reply from our team within one business day. In the
250 meantime you can explore{" "}
251 <a href="/pricing">our standard plans</a> or{" "}
252 <a href="/register">sign up free</a>.
253 </p>
254 </div>
255 ) : (
256 <form action="/enterprise/contact" method="post" class="ent-form" novalidate>
257 <div class="ent-form-row">
258 <div class="ent-field">
259 <label for="ent-name">Your name</label>
260 <input
261 type="text"
262 id="ent-name"
263 name="name"
264 placeholder="Alice Smith"
265 required
266 maxlength={200}
267 autocomplete="name"
268 />
269 </div>
270 <div class="ent-field">
271 <label for="ent-company">Company</label>
272 <input
273 type="text"
274 id="ent-company"
275 name="company"
276 placeholder="Acme Corp"
277 required
278 maxlength={200}
279 autocomplete="organization"
280 />
281 </div>
282 </div>
283 <div class="ent-form-row">
284 <div class="ent-field">
285 <label for="ent-email">Work email</label>
286 <input
287 type="email"
288 id="ent-email"
289 name="email"
290 placeholder="alice@acmecorp.com"
291 required
292 maxlength={200}
293 autocomplete="email"
294 />
295 </div>
296 <div class="ent-field">
297 <label for="ent-team-size">Team size</label>
298 <select id="ent-team-size" name="team_size" required>
299 <option value="" disabled selected>Select range…</option>
300 <option value="10-50">10 – 50 developers</option>
301 <option value="50-200">50 – 200 developers</option>
302 <option value="200-1000">200 – 1 000 developers</option>
303 <option value="1000+">1 000+ developers</option>
304 </select>
305 </div>
306 </div>
307 <div class="ent-field">
308 <label for="ent-message">
309 What are you looking for?{" "}
310 <span class="ent-field-optional">(optional)</span>
311 </label>
312 <textarea
313 id="ent-message"
314 name="message"
315 rows={5}
316 maxlength={4000}
317 placeholder="Tell us about your stack, compliance requirements, or anything else that would help us put together a proposal."
318 />
319 </div>
320 <button type="submit" class="btn btn-primary btn-lg ent-submit">
321 Send message
322 </button>
323 </form>
324 )}
325 </div>
326 </section>
327
328 </div>
329 </>
330);
331
332const FeatureCard: FC<{ icon: string; title: string; body: string }> = ({
333 icon,
334 title,
335 body,
336}) => (
337 <div class="ent-feature-card">
338 <div class="ent-feature-icon" aria-hidden="true">{icon}</div>
339 <div class="ent-feature-title">{title}</div>
340 <p class="ent-feature-body">{body}</p>
341 </div>
342);
343
344// ─── Scoped CSS ──────────────────────────────────────────────────────────────
345
346const enterpriseCss = `
347 .ent-root { max-width: 1180px; margin: 0 auto; padding: 0 16px 80px; }
348
349 /* ── Hero ── */
350 .ent-hero {
351 text-align: center;
352 padding: var(--s-16) 0 var(--s-12);
353 position: relative;
354 max-width: 820px;
355 margin: 0 auto;
8102dd4ccantynz-alt356 /* Clip the decorative absolutely-positioned orb so it can't push the
357 document wider than the viewport on narrow screens. */
358 overflow: hidden;
9f29b65Claude359 }
360 .ent-hero-hairline {
361 position: absolute;
362 top: 0; left: 8%; right: 8%;
363 height: 2px;
6fd5915Claude364 background: linear-gradient(90deg, transparent 0%, #5b6ee8 30%, #5f8fa0 70%, transparent 100%);
9f29b65Claude365 opacity: 0.65;
366 pointer-events: none;
367 border-radius: 2px;
368 }
369 .ent-hero-orb {
370 position: absolute;
371 top: 6%; left: 50%;
372 transform: translateX(-50%);
373 width: 480px; height: 480px;
6fd5915Claude374 background: radial-gradient(circle, rgba(91,110,232,0.18), rgba(95,143,160,0.10) 45%, transparent 70%);
9f29b65Claude375 filter: blur(90px);
376 opacity: 0.65;
377 pointer-events: none;
378 z-index: -1;
379 animation: entHeroOrb 18s ease-in-out infinite;
380 }
381 @keyframes entHeroOrb {
382 0%, 100% { transform: translateX(-50%) scale(1); opacity: 0.5; }
383 50% { transform: translateX(-50%) scale(1.1); opacity: 0.8; }
384 }
385 @media (prefers-reduced-motion: reduce) { .ent-hero-orb { animation: none; } }
386 .ent-hero .eyebrow { justify-content: center; margin: 0 auto var(--s-4); }
387 .ent-hero-title {
388 font-size: clamp(36px, 6vw, 68px);
389 line-height: 1.04;
390 letter-spacing: -0.038em;
391 margin: 0 0 var(--s-5);
392 }
393 .ent-hero-sub {
394 font-size: clamp(15px, 1.6vw, 18px);
395 color: var(--text-muted);
396 max-width: 640px;
397 margin: 0 auto;
398 line-height: 1.6;
399 }
400 .ent-hero-ctas {
401 display: flex;
402 gap: 12px;
403 justify-content: center;
404 flex-wrap: wrap;
405 margin-top: var(--s-8);
406 }
407
408 /* ── Sections ── */
409 .ent-section { margin: var(--s-14) auto; }
410
411 /* ── Feature grid ── */
412 .ent-grid {
413 display: grid;
414 grid-template-columns: repeat(3, 1fr);
415 gap: 16px;
416 margin-top: var(--s-8);
417 }
418 @media (max-width: 900px) { .ent-grid { grid-template-columns: repeat(2, 1fr); } }
419 @media (max-width: 560px) { .ent-grid { grid-template-columns: 1fr; } }
420
421 .ent-feature-card {
422 background: var(--bg-elevated);
423 border: 1px solid var(--border);
424 border-radius: var(--r-lg);
425 padding: var(--s-6);
426 display: flex;
427 flex-direction: column;
428 gap: var(--s-3);
429 transition: border-color var(--t-fast) var(--ease), transform var(--t-base) var(--ease-out-quart);
430 }
431 .ent-feature-card:hover {
6fd5915Claude432 border-color: rgba(91,110,232,0.35);
9f29b65Claude433 transform: translateY(-2px);
434 }
435 .ent-feature-icon {
436 font-size: 28px;
437 line-height: 1;
438 }
439 .ent-feature-title {
440 font-family: var(--font-display);
441 font-size: 16px;
442 font-weight: 600;
443 color: var(--text-strong);
444 letter-spacing: -0.01em;
445 }
446 .ent-feature-body {
447 font-size: var(--t-sm);
448 color: var(--text-muted);
449 line-height: 1.6;
450 margin: 0;
451 }
452
453 /* ── SIEM detail ── */
454 .ent-siem {
455 background: var(--bg-elevated);
456 border: 1px solid var(--border);
457 border-radius: var(--r-2xl);
458 overflow: hidden;
459 }
460 .ent-siem-inner {
461 display: grid;
462 grid-template-columns: 1fr 1fr;
463 gap: 0;
464 align-items: start;
465 }
466 @media (max-width: 860px) { .ent-siem-inner { grid-template-columns: 1fr; } }
467 .ent-siem-text {
468 padding: var(--s-10) var(--s-8);
469 display: flex;
470 flex-direction: column;
471 gap: var(--s-4);
472 }
473 .ent-siem-text .eyebrow { justify-content: flex-start; }
474 .ent-siem-text h2 {
475 font-size: clamp(22px, 2.8vw, 32px);
476 letter-spacing: -0.025em;
477 font-weight: 600;
478 color: var(--text-strong);
479 margin: 0;
480 line-height: 1.2;
481 }
482 .ent-siem-text p {
483 font-size: var(--t-sm);
484 color: var(--text-muted);
485 line-height: 1.65;
486 margin: 0;
487 }
488 .ent-siem-text code {
489 font-family: var(--font-mono);
490 font-size: 12px;
491 background: var(--bg-secondary);
492 border: 1px solid var(--border-subtle);
493 padding: 2px 6px;
494 border-radius: 4px;
495 color: var(--accent);
496 white-space: nowrap;
497 }
498 .ent-siem-list {
499 list-style: none;
500 padding: 0;
501 margin: 0;
502 display: flex;
503 flex-direction: column;
504 gap: 8px;
505 }
506 .ent-siem-list li {
507 font-size: var(--t-sm);
508 color: var(--text);
509 display: flex;
510 gap: 10px;
511 line-height: 1.5;
512 }
513 .ent-siem-list li::before {
514 content: '✓';
515 color: var(--accent);
516 font-weight: 700;
517 flex-shrink: 0;
518 }
519 .ent-siem-code {
520 background:
6fd5915Claude521 linear-gradient(160deg, rgba(91,110,232,0.06), rgba(95,143,160,0.04) 60%, transparent),
9f29b65Claude522 var(--bg-secondary);
523 border-left: 1px solid var(--border);
524 padding: var(--s-10) var(--s-6);
525 display: flex;
526 flex-direction: column;
527 gap: var(--s-3);
528 }
529 @media (max-width: 860px) {
530 .ent-siem-code { border-left: none; border-top: 1px solid var(--border); }
531 }
532 .ent-code-label {
533 font-family: var(--font-mono);
534 font-size: 10px;
535 text-transform: uppercase;
536 letter-spacing: 0.12em;
537 color: var(--text-faint);
538 }
539 .ent-code {
540 font-family: var(--font-mono);
541 font-size: 12px;
542 line-height: 1.6;
543 color: var(--text);
544 white-space: pre-wrap;
545 word-break: break-all;
546 margin: 0;
547 background: none;
548 border: none;
549 padding: 0;
550 }
551
552 /* ── Social proof ── */
553 .ent-proof {
554 border: 1px solid var(--border);
555 border-radius: var(--r-lg);
556 background: var(--bg-elevated);
557 padding: var(--s-8) var(--s-6);
558 }
559 .ent-proof-inner {
560 display: flex;
561 align-items: center;
562 justify-content: space-around;
563 flex-wrap: wrap;
564 gap: var(--s-6);
565 }
566 .ent-proof-stat { text-align: center; }
567 .ent-stat-num {
568 font-family: var(--font-display);
569 font-size: 32px;
570 font-weight: 700;
571 letter-spacing: -0.03em;
572 color: var(--text-strong);
573 line-height: 1;
574 }
575 .ent-stat-label {
576 font-size: 12px;
577 color: var(--text-muted);
578 margin-top: 4px;
579 font-family: var(--font-mono);
580 text-transform: uppercase;
581 letter-spacing: 0.08em;
582 }
583 .ent-proof-divider {
584 width: 1px;
585 height: 40px;
586 background: var(--border);
587 }
588 @media (max-width: 600px) { .ent-proof-divider { display: none; } }
589
590 /* ── Contact form ── */
591 .ent-contact-wrap { margin: var(--s-16) auto; }
592 .ent-contact {
593 max-width: 760px;
594 margin: 0 auto;
595 background: var(--bg-elevated);
596 border: 1px solid var(--border-strong);
597 border-radius: var(--r-2xl);
598 padding: var(--s-10) var(--s-8);
599 position: relative;
600 background:
6fd5915Claude601 radial-gradient(60% 100% at 50% 0%, rgba(91,110,232,0.10), transparent 60%),
9f29b65Claude602 var(--bg-elevated);
603 }
604 .ent-contact-header {
605 text-align: center;
606 margin-bottom: var(--s-8);
607 display: flex;
608 flex-direction: column;
609 gap: var(--s-3);
610 }
611 .ent-contact-header .eyebrow { justify-content: center; }
612 .ent-contact-header h2 {
613 font-size: clamp(24px, 3vw, 36px);
614 letter-spacing: -0.025em;
615 font-weight: 600;
616 color: var(--text-strong);
617 margin: 0;
618 }
619 .ent-contact-header p {
620 font-size: var(--t-sm);
621 color: var(--text-muted);
622 margin: 0;
623 line-height: 1.6;
624 max-width: 480px;
625 margin: 0 auto;
626 }
627 .ent-form {
628 display: flex;
629 flex-direction: column;
630 gap: var(--s-5);
631 }
632 .ent-form-row {
633 display: grid;
634 grid-template-columns: 1fr 1fr;
635 gap: var(--s-4);
636 }
637 @media (max-width: 580px) { .ent-form-row { grid-template-columns: 1fr; } }
638 .ent-field {
639 display: flex;
640 flex-direction: column;
641 gap: var(--s-2);
642 }
643 .ent-field label {
644 font-size: 13px;
645 font-weight: 500;
646 color: var(--text);
647 }
648 .ent-field-optional {
649 font-weight: 400;
650 color: var(--text-muted);
651 font-size: 12px;
652 }
653 .ent-field input,
654 .ent-field select,
655 .ent-field textarea {
656 font-family: inherit;
657 font-size: var(--t-sm);
658 color: var(--text);
659 background: var(--bg-secondary);
660 border: 1px solid var(--border);
661 border-radius: var(--r);
662 padding: 10px 14px;
663 outline: none;
664 transition: border-color var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
665 resize: vertical;
666 }
667 .ent-field input::placeholder,
668 .ent-field textarea::placeholder { color: var(--text-faint); }
669 .ent-field input:focus,
670 .ent-field select:focus,
671 .ent-field textarea:focus {
6fd5915Claude672 border-color: rgba(91,110,232,0.55);
673 box-shadow: 0 0 0 3px rgba(91,110,232,0.12);
9f29b65Claude674 }
675 .ent-submit { width: 100%; justify-content: center; margin-top: var(--s-2); }
676
677 /* ── Submitted state ── */
678 .ent-submitted {
679 text-align: center;
680 padding: var(--s-8) var(--s-4);
681 display: flex;
682 flex-direction: column;
683 align-items: center;
684 gap: var(--s-4);
685 }
686 .ent-submitted-icon {
687 width: 56px; height: 56px;
688 border-radius: 50%;
689 background: rgba(52,211,153,0.15);
690 border: 2px solid rgba(52,211,153,0.4);
691 display: flex;
692 align-items: center;
693 justify-content: center;
694 font-size: 24px;
695 color: var(--green);
696 margin: 0 auto;
697 }
698 .ent-submitted h3 {
699 font-size: 22px;
700 font-weight: 600;
701 color: var(--text-strong);
702 margin: 0;
703 }
704 .ent-submitted p {
705 color: var(--text-muted);
706 font-size: var(--t-sm);
707 max-width: 400px;
708 margin: 0;
709 line-height: 1.6;
710 }
711 .ent-submitted a { color: var(--accent); }
712`;
713
714export default enterprise;