SafeSight commited on
Commit
7a8f7d3
Β·
verified Β·
1 Parent(s): d108ae5

Upload 19 files

Browse files
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SafeSight on Hugging Face Spaces (Docker SDK)
2
+ FROM node:20-alpine
3
+
4
+ WORKDIR /app
5
+
6
+ # Install deps first for layer caching
7
+ COPY package.json ./
8
+ RUN npm install --omit=dev
9
+
10
+ # Copy the rest of the app
11
+ COPY . .
12
+
13
+ # HF Spaces expects the app to listen on port 7860
14
+ ENV PORT=7860
15
+ ENV NODE_ENV=production
16
+
17
+ # Persistent storage is mounted at /data by HF Spaces (paid persistent storage).
18
+ # The app encrypts everything it writes there.
19
+ VOLUME ["/data"]
20
+
21
+ EXPOSE 7860
22
+
23
+ CMD ["node", "src/server.js"]
package.json ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "safesight-web",
3
+ "version": "1.0.0",
4
+ "description": "SafeSight marketing site with Stripe billing, deployable to Hugging Face Spaces (Docker).",
5
+ "main": "src/server.js",
6
+ "type": "commonjs",
7
+ "scripts": {
8
+ "start": "node src/server.js"
9
+ },
10
+ "engines": { "node": ">=20" },
11
+ "dependencies": {
12
+ "ejs": "^3.1.10",
13
+ "express": "^4.19.2",
14
+ "stripe": "^16.12.0"
15
+ }
16
+ }
public/img/logo.svg ADDED
public/styles.css ADDED
@@ -0,0 +1,82 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root{
2
+ --bg:#0A1A3A; --bg-2:#0e2150; --ink:#F8FAFC; --muted:#a8b3d1;
3
+ --primary:#1E40AF; --primary-2:#3B82F6; --accent:#F97316; --card:#11244f;
4
+ --border:rgba(255,255,255,.08); --max:1100px;
5
+ }
6
+ *{box-sizing:border-box}
7
+ html,body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI",Inter,Roboto,sans-serif}
8
+ a{color:var(--primary-2);text-decoration:none}
9
+ a:hover{color:var(--accent)}
10
+ .container{max-width:var(--max);margin:0 auto;padding:0 24px}
11
+ .site-header{position:sticky;top:0;z-index:10;background:rgba(10,26,58,.85);backdrop-filter:blur(10px);border-bottom:1px solid var(--border)}
12
+ .nav{display:flex;align-items:center;justify-content:space-between;padding:14px 24px}
13
+ .brand{display:flex;align-items:center;gap:10px;color:var(--ink);font-weight:700}
14
+ nav a{margin-left:18px;color:var(--ink);font-weight:500}
15
+ .btn{display:inline-block;padding:10px 18px;border-radius:10px;font-weight:600;border:1px solid transparent;cursor:pointer}
16
+ .btn-primary{background:var(--accent);color:#1a1006}
17
+ .btn-primary:hover{filter:brightness(1.05);color:#1a1006}
18
+ .btn-outline{border-color:var(--border);color:var(--ink)}
19
+ .hero{padding:80px 0 60px;background:radial-gradient(800px 400px at 80% 0%, rgba(249,115,22,.15), transparent 60%), linear-gradient(180deg,var(--bg),var(--bg-2))}
20
+ .hero h1{font-size:clamp(2rem,5vw,3.4rem);line-height:1.1;margin:0 0 18px}
21
+ .hero p.lead{font-size:1.15rem;color:var(--muted);max-width:640px}
22
+ .cta{display:flex;gap:12px;margin-top:24px;flex-wrap:wrap}
23
+ section{padding:60px 0}
24
+ section h2{font-size:2rem;margin:0 0 24px}
25
+ .grid{display:grid;gap:20px}
26
+ .grid-3{grid-template-columns:repeat(auto-fit,minmax(260px,1fr))}
27
+ .card{background:var(--card);border:1px solid var(--border);border-radius:14px;padding:22px}
28
+ .card h3{margin:0 0 8px;color:var(--ink)}
29
+ .card p{margin:0;color:var(--muted)}
30
+ .stat{font-size:2.4rem;color:var(--accent);font-weight:800}
31
+ table{width:100%;border-collapse:collapse;background:var(--card);border-radius:12px;overflow:hidden}
32
+ th,td{padding:14px;border-bottom:1px solid var(--border);text-align:left}
33
+ th{background:rgba(255,255,255,.03)}
34
+ .price{display:flex;justify-content:center;gap:20px;flex-wrap:wrap}
35
+ .plan{background:var(--card);border:1px solid var(--border);border-radius:16px;padding:28px;min-width:260px;max-width:320px;flex:1}
36
+ .plan.featured{border-color:var(--accent)}
37
+ .plan .amount{font-size:2.4rem;font-weight:800}
38
+ .plan ul{padding-left:18px;color:var(--muted)}
39
+ .toggle{display:inline-flex;background:var(--card);border:1px solid var(--border);border-radius:999px;padding:4px;margin-bottom:24px}
40
+ .toggle button{border:0;background:transparent;color:var(--ink);padding:8px 16px;border-radius:999px;cursor:pointer;font-weight:600}
41
+ .toggle button.active{background:var(--accent);color:#1a1006}
42
+ .disclaimer{background:rgba(249,115,22,.08);border:1px solid rgba(249,115,22,.35);padding:14px;border-radius:10px;color:#fde4cf;font-size:.92rem}
43
+ .site-footer{padding:40px 0;border-top:1px solid var(--border);background:#06112a;margin-top:60px}
44
+ .footlinks{margin:16px 0}
45
+ .footlinks a{margin-right:16px}
46
+ .copy{color:var(--muted);font-size:.9rem;margin:0}
47
+ form input,form textarea{width:100%;padding:12px;border-radius:8px;border:1px solid var(--border);background:#0c1d44;color:var(--ink);margin-bottom:12px;font:inherit}
48
+ label{display:block;font-weight:600;margin-bottom:6px}
49
+ .notice{padding:12px;border-radius:10px;margin-bottom:20px}
50
+ .notice.success{background:rgba(34,197,94,.12);border:1px solid rgba(34,197,94,.4)}
51
+ .notice.warn{background:rgba(249,115,22,.12);border:1px solid rgba(249,115,22,.4)}
52
+
53
+ /* ---- hero phone ---- */
54
+ .hero-grid{display:grid;grid-template-columns:1fr;gap:40px;align-items:center}
55
+ @media (min-width:900px){.hero-grid{grid-template-columns:1.05fr .95fr}}
56
+ .chip{display:inline-flex;align-items:center;gap:6px;background:rgba(249,115,22,.12);color:var(--accent);border:1px solid rgba(249,115,22,.4);padding:6px 14px;border-radius:999px;font-weight:600;font-size:.9rem;margin-bottom:18px}
57
+ .alert-line{color:var(--accent);font-weight:500;margin:16px 0 0}
58
+ .hero-art{display:flex;justify-content:center}
59
+ .phone-frame{width:min(340px,80%);aspect-ratio:9/16;background:#f4efe6;border-radius:28px;padding:18px;box-shadow:0 30px 60px -20px rgba(0,0,0,.5);overflow:hidden}
60
+ .phone-screen{width:100%;height:100%;border-radius:22px;background:linear-gradient(160deg,#243b4a,#3a5b6d 60%,#a9b8a8);position:relative;overflow:hidden}
61
+ .phone-banner{position:absolute;top:38%;left:8%;right:18%;background:var(--accent);color:#fff;padding:14px 18px;border-radius:10px;font-weight:700;transform:rotate(-6deg);box-shadow:0 8px 20px rgba(0,0,0,.25)}
62
+
63
+ /* ---- buttons (contrast fix) ---- */
64
+ .btn-secondary{background:#fff;color:#0A1A3A;border:1px solid #fff;font-weight:700}
65
+ .btn-secondary:hover{background:#e8eefb;color:#0A1A3A}
66
+
67
+ /* ---- pricing alignment ---- */
68
+ .price{display:grid;grid-template-columns:1fr;gap:20px;align-items:stretch;max-width:880px;margin:0 auto}
69
+ @media (min-width:760px){.price{grid-template-columns:1fr 1fr}}
70
+ .plan{display:flex;flex-direction:column;max-width:none;min-width:0}
71
+ .plan h3{margin:0 0 6px;color:var(--ink);font-size:1.35rem}
72
+ .plan-sub{color:var(--muted);margin:0 0 22px;min-height:24px}
73
+ .plan .amount{font-size:2.6rem;font-weight:800;line-height:1;margin:0}
74
+ .amount-unit{font-size:1rem;color:var(--muted);font-weight:500;margin-left:4px}
75
+ .amount-equiv{color:var(--muted);font-size:.95rem;margin:8px 0 28px;min-height:22px}
76
+ .badge{display:inline-block;background:rgba(249,115,22,.15);color:var(--accent);font-size:.75rem;font-weight:700;padding:4px 10px;border-radius:999px;margin-left:8px;vertical-align:middle}
77
+ .features{list-style:none;padding:0;margin:0 0 24px;flex:1}
78
+ .features li{position:relative;padding:8px 0 8px 30px;color:var(--ink)}
79
+ .features li::before{position:absolute;left:0;top:8px;width:20px;height:20px;display:inline-flex;align-items:center;justify-content:center;font-weight:800;border-radius:50%;font-size:.85rem}
80
+ .features li.ok::before{content:"βœ“";color:var(--accent)}
81
+ .features li.no{color:var(--muted);text-decoration:line-through}
82
+ .features li.no::before{content:"βœ•";color:#94a3b8}
src/db.js ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Encrypted JSON store at /data/db.enc
3
+ *
4
+ * Cipher: AES-256-GCM
5
+ * - Symmetric AES with a 256-bit key is considered quantum-tolerant:
6
+ * Grover's algorithm only reduces effective security to 128 bits, which
7
+ * is the same level NIST targets for post-quantum symmetric primitives.
8
+ * - Each write uses a fresh random 12-byte IV and a 16-byte auth tag, so
9
+ * tampering with /data/db.enc is detected on the next read.
10
+ *
11
+ * Key: env var DB_ENCRYPTION_KEY = 64 hex chars (32 bytes). Set this manually
12
+ * in the Hugging Face Space "Secrets" panel. Generate with:
13
+ * node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
14
+ *
15
+ * On-disk layout (binary):
16
+ * [12 bytes IV][16 bytes auth tag][N bytes ciphertext of UTF-8 JSON]
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+ const crypto = require('crypto');
22
+
23
+ const DATA_DIR = '/data';
24
+ const DB_PATH = path.join(DATA_DIR, 'db.enc');
25
+
26
+ function getKey() {
27
+ const hex = process.env.DB_ENCRYPTION_KEY;
28
+ if (!hex || !/^[0-9a-fA-F]{64}$/.test(hex)) {
29
+ throw new Error(
30
+ 'DB_ENCRYPTION_KEY must be a 64-character hex string (32 bytes). ' +
31
+ 'Generate one with: node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'hex\'))"'
32
+ );
33
+ }
34
+ return Buffer.from(hex, 'hex');
35
+ }
36
+
37
+ function ensureDir() {
38
+ try {
39
+ fs.mkdirSync(DATA_DIR, { recursive: true });
40
+ } catch (e) {
41
+ // If /data is not mounted (running locally without persistence), fall back gracefully.
42
+ if (e.code !== 'EEXIST' && e.code !== 'EACCES') throw e;
43
+ }
44
+ }
45
+
46
+ function emptyState() {
47
+ return { customers: {}, events: [] };
48
+ }
49
+
50
+ function read() {
51
+ ensureDir();
52
+ if (!fs.existsSync(DB_PATH)) return emptyState();
53
+ const raw = fs.readFileSync(DB_PATH);
54
+ if (raw.length < 12 + 16 + 1) return emptyState();
55
+
56
+ const key = getKey();
57
+ const iv = raw.subarray(0, 12);
58
+ const tag = raw.subarray(12, 28);
59
+ const ct = raw.subarray(28);
60
+
61
+ const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
62
+ decipher.setAuthTag(tag);
63
+ const plain = Buffer.concat([decipher.update(ct), decipher.final()]);
64
+ return JSON.parse(plain.toString('utf8'));
65
+ }
66
+
67
+ function write(state) {
68
+ ensureDir();
69
+ const key = getKey();
70
+ const iv = crypto.randomBytes(12);
71
+ const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
72
+ const plain = Buffer.from(JSON.stringify(state), 'utf8');
73
+ const ct = Buffer.concat([cipher.update(plain), cipher.final()]);
74
+ const tag = cipher.getAuthTag();
75
+ const tmp = DB_PATH + '.tmp';
76
+ fs.writeFileSync(tmp, Buffer.concat([iv, tag, ct]), { mode: 0o600 });
77
+ fs.renameSync(tmp, DB_PATH);
78
+ }
79
+
80
+ function update(mutator) {
81
+ const state = read();
82
+ mutator(state);
83
+ write(state);
84
+ return state;
85
+ }
86
+
87
+ module.exports = { read, write, update };
src/server.js ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require('express');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const db = require('./db');
5
+
6
+ const app = express();
7
+ const PORT = process.env.PORT || 7860;
8
+
9
+ // ---------- Stripe (optional) ----------
10
+ // Set these as Hugging Face Space secrets:
11
+ // STRIPE_SECRET_KEY (sk_test_... or sk_live_...)
12
+ // STRIPE_PRICE_MONTHLY (price_... for the $4.99/mo plan)
13
+ // STRIPE_PRICE_ANNUAL (price_... for the $39.99/yr plan)
14
+ // STRIPE_WEBHOOK_SECRET (whsec_... from your endpoint)
15
+ // PUBLIC_URL (e.g. https://USERNAME-safesight.hf.space)
16
+ let stripe = null;
17
+ if (process.env.STRIPE_SECRET_KEY) {
18
+ stripe = require('stripe')(process.env.STRIPE_SECRET_KEY);
19
+ }
20
+
21
+ // ---------- View engine ----------
22
+ app.set('view engine', 'ejs');
23
+ app.set('views', path.join(__dirname, '..', 'views'));
24
+
25
+ // ---------- Static ----------
26
+ app.use('/static', express.static(path.join(__dirname, '..', 'public'), {
27
+ maxAge: '7d',
28
+ etag: true,
29
+ }));
30
+
31
+ // Webhook MUST receive the raw body before JSON parsing.
32
+ app.post('/stripe/webhook', express.raw({ type: 'application/json' }), (req, res) => {
33
+ if (!stripe) return res.status(503).send('Stripe not configured');
34
+ const sig = req.headers['stripe-signature'];
35
+ const secret = process.env.STRIPE_WEBHOOK_SECRET;
36
+ if (!secret) return res.status(503).send('Webhook secret not configured');
37
+
38
+ let event;
39
+ try {
40
+ event = stripe.webhooks.constructEvent(req.body, sig, secret);
41
+ } catch (err) {
42
+ return res.status(400).send(`Webhook Error: ${err.message}`);
43
+ }
44
+
45
+ try {
46
+ db.update((state) => {
47
+ state.events.push({ id: event.id, type: event.type, at: Date.now() });
48
+ if (state.events.length > 1000) state.events.splice(0, state.events.length - 1000);
49
+
50
+ if (event.type === 'checkout.session.completed') {
51
+ const s = event.data.object;
52
+ const customerId = s.customer;
53
+ if (customerId) {
54
+ state.customers[customerId] = {
55
+ ...(state.customers[customerId] || {}),
56
+ email: s.customer_details?.email || s.customer_email || null,
57
+ lastCheckout: s.id,
58
+ updatedAt: Date.now(),
59
+ subscriptionId: s.subscription || null,
60
+ status: 'active',
61
+ };
62
+ }
63
+ }
64
+ if (event.type === 'customer.subscription.updated' ||
65
+ event.type === 'customer.subscription.deleted' ||
66
+ event.type === 'customer.subscription.created') {
67
+ const sub = event.data.object;
68
+ const customerId = sub.customer;
69
+ if (customerId) {
70
+ state.customers[customerId] = {
71
+ ...(state.customers[customerId] || {}),
72
+ subscriptionId: sub.id,
73
+ status: sub.status,
74
+ currentPeriodEnd: sub.current_period_end,
75
+ updatedAt: Date.now(),
76
+ };
77
+ }
78
+ }
79
+ });
80
+ } catch (e) {
81
+ console.error('DB write failed:', e.message);
82
+ return res.status(500).send('storage error');
83
+ }
84
+
85
+ res.json({ received: true });
86
+ });
87
+
88
+ // JSON / form parsing for everything else.
89
+ app.use(express.json());
90
+ app.use(express.urlencoded({ extended: true }));
91
+
92
+ // ---------- Page metadata ----------
93
+ const SITE = {
94
+ name: 'SafeSight',
95
+ tagline: 'Stop distracted driving before it stops you.',
96
+ url: process.env.PUBLIC_URL || '',
97
+ };
98
+
99
+ function render(res, view, locals = {}) {
100
+ res.render(view, { SITE, page: view, query: {}, ...locals });
101
+ }
102
+
103
+ // ---------- Pages ----------
104
+ const pages = [
105
+ ['/', 'home', { title: 'SafeSight β€” Stop distracted driving', desc: 'On-device AI that warns you the moment your eyes leave the road.' }],
106
+ ['/how-it-works', 'how-it-works', { title: 'How SafeSight Works', desc: 'A 5-step look at on-device camera processing and instant alerts.' }],
107
+ ['/why-safesight', 'why-safesight', { title: 'Why SafeSight', desc: 'Privacy-first, real-time, and built for everyday drivers.' }],
108
+ ['/about', 'about', { title: 'About SafeSight', desc: 'Our mission to make every drive safer.' }],
109
+ ['/pricing', 'pricing', { title: 'Pricing β€” Monthly & Annual', desc: 'Simple plans. Cancel anytime.' }],
110
+ ['/download', 'download', { title: 'Download SafeSight', desc: 'Get SafeSight on iOS and Android.' }],
111
+ ['/contact', 'contact', { title: 'Contact SafeSight', desc: 'Questions, partnerships, and press.' }],
112
+ ['/privacy', 'privacy', { title: 'Privacy Policy', desc: 'How SafeSight handles your data.' }],
113
+ ['/terms', 'terms', { title: 'Terms of Service', desc: 'Terms, disclaimers, and liability.' }],
114
+ ];
115
+
116
+ for (const [route, view, meta] of pages) {
117
+ app.get(route, (req, res) => render(res, view, { ...meta, query: req.query }));
118
+ }
119
+
120
+ // ---------- Data deletion request ----------
121
+ app.post('/deletion-request', (req, res) => {
122
+ const email = String(req.body.email || '').trim().slice(0, 255);
123
+ const customerId = String(req.body.customerId || '').trim().slice(0, 64);
124
+ const reason = String(req.body.reason || '').trim().slice(0, 1000);
125
+ if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
126
+ return res.status(400).send('Valid email required.');
127
+ }
128
+ try {
129
+ db.update((state) => {
130
+ state.deletionRequests = state.deletionRequests || [];
131
+ state.deletionRequests.push({ email, customerId, reason, at: Date.now() });
132
+ if (state.deletionRequests.length > 5000) state.deletionRequests.shift();
133
+ });
134
+ } catch (e) {
135
+ console.error('DB write failed:', e.message);
136
+ return res.status(500).send('Storage error');
137
+ }
138
+ res.redirect(303, '/privacy?sent=1');
139
+ });
140
+
141
+ // ---------- Stripe checkout ----------
142
+ app.post('/checkout', async (req, res) => {
143
+ if (!stripe) return res.status(503).send('Payments are not configured yet. Set STRIPE_SECRET_KEY.');
144
+ const plan = req.body.plan === 'annual' ? 'annual' : 'monthly';
145
+ const priceId = plan === 'annual' ? process.env.STRIPE_PRICE_ANNUAL : process.env.STRIPE_PRICE_MONTHLY;
146
+ if (!priceId) return res.status(503).send(`Missing STRIPE_PRICE_${plan.toUpperCase()}`);
147
+
148
+ const base = process.env.PUBLIC_URL || `${req.protocol}://${req.get('host')}`;
149
+ try {
150
+ const session = await stripe.checkout.sessions.create({
151
+ mode: 'subscription',
152
+ line_items: [{ price: priceId, quantity: 1 }],
153
+ success_url: `${base}/pricing?status=success&session_id={CHECKOUT_SESSION_ID}`,
154
+ cancel_url: `${base}/pricing?status=cancelled`,
155
+ allow_promotion_codes: true,
156
+ });
157
+ res.redirect(303, session.url);
158
+ } catch (e) {
159
+ console.error(e);
160
+ res.status(500).send('Could not start checkout.');
161
+ }
162
+ });
163
+
164
+ // ---------- Misc ----------
165
+ app.get('/robots.txt', (req, res) => {
166
+ res.type('text/plain').send(`User-agent: *\nAllow: /\nSitemap: ${SITE.url || ''}/sitemap.xml\n`);
167
+ });
168
+
169
+ app.get('/sitemap.xml', (req, res) => {
170
+ const base = SITE.url || `${req.protocol}://${req.get('host')}`;
171
+ const urls = pages.map(([p]) => `<url><loc>${base}${p}</loc></url>`).join('');
172
+ res.type('application/xml').send(
173
+ `<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${urls}</urlset>`
174
+ );
175
+ });
176
+
177
+ app.get('/healthz', (req, res) => res.json({ ok: true }));
178
+
179
+ app.use((req, res) => {
180
+ res.status(404);
181
+ render(res, '404', { title: 'Not Found', desc: 'Page not found.' });
182
+ });
183
+
184
+ app.listen(PORT, '0.0.0.0', () => {
185
+ console.log(`SafeSight listening on http://0.0.0.0:${PORT}`);
186
+ if (!stripe) console.log('Stripe disabled (STRIPE_SECRET_KEY not set).');
187
+ });
views/404.ejs ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ <%- include('partials/head') %>
2
+ <%- include('partials/header') %>
3
+ <section class="hero"><div class="container" style="text-align:center">
4
+ <h1>404</h1><p class="lead" style="margin:0 auto">That page doesn't exist.</p>
5
+ <div class="cta" style="justify-content:center"><a href="/" class="btn btn-primary">Back home</a></div>
6
+ </div></section>
7
+ <%- include('partials/footer') %>
views/about.ejs ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <%- include('partials/head') %>
2
+ <%- include('partials/header') %>
3
+ <section class="hero"><div class="container"><h1>About SafeSight</h1><p class="lead">We're building the driver-attention layer for every vehicle, starting with the phone already in your pocket.</p></div></section>
4
+ <section><div class="container grid grid-3">
5
+ <div class="card"><h3>Mission</h3><p>Eliminate preventable crashes caused by distraction without sending anyone's face to the cloud.</p></div>
6
+ <div class="card"><h3>Approach</h3><p>Edge-only computer vision, audible/haptic feedback, and a privacy-first data model.</p></div>
7
+ <div class="card"><h3>Team</h3><p>Engineers and safety advocates who lost too many friends to "just one text".</p></div>
8
+ </div></div></section>
9
+ <section><div class="container grid grid-3">
10
+ <div class="card"><div class="stat">$8B</div><p>TAM β€” global driver-assistance software</p></div>
11
+ <div class="card"><div class="stat">$1.2B</div><p>SAM β€” phone-based attention monitoring</p></div>
12
+ <div class="card"><div class="stat">$60M</div><p>SOM β€” early-adopter rideshare & fleet drivers</p></div>
13
+ </div></section>
14
+ <%- include('partials/footer') %>
views/contact.ejs ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <%- include('partials/head') %>
2
+ <%- include('partials/header') %>
3
+ <section class="hero"><div class="container"><h1>Contact us</h1><p class="lead">Questions, partnerships, or press inquiries.</p></div></section>
4
+ <section><div class="container" style="max-width:560px">
5
+ <form method="POST" action="/contact" onsubmit="event.preventDefault(); this.querySelector('.notice')?.remove(); var d=document.createElement('div'); d.className='notice success'; d.textContent='Thanks β€” we\\'ll get back to you soon.'; this.prepend(d);">
6
+ <label>Name<input name="name" required></label>
7
+ <label>Email<input name="email" type="email" required></label>
8
+ <label>Message<textarea name="message" rows="5" required></textarea></label>
9
+ <button class="btn btn-primary" type="submit">Send</button>
10
+ </form>
11
+ </div></section>
12
+ <%- include('partials/footer') %>
views/download.ejs ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <%- include('partials/head') %>
2
+ <%- include('partials/header') %>
3
+ <section class="hero"><div class="container" style="text-align:center">
4
+ <h1>Get SafeSight</h1>
5
+ <p class="lead" style="margin:0 auto">Available on iOS and Android.</p>
6
+ <div class="cta" style="justify-content:center">
7
+ <a href="#" class="btn btn-primary" onclick="event.preventDefault()">Download for iOS</a>
8
+ <a href="#" class="btn btn-primary" onclick="event.preventDefault()">Download for Android</a>
9
+ </div>
10
+ <p style="color:var(--muted);margin-top:24px">App store links coming soon.</p>
11
+ </div></section>
12
+ <%- include('partials/footer') %>
views/home.ejs ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <%- include('partials/head') %>
2
+ <%- include('partials/header') %>
3
+ <section class="hero">
4
+ <div class="container hero-grid">
5
+ <div>
6
+ <span class="chip">πŸ›‘ Driver Safety App</span>
7
+ <h1>Eyes on the Road.<br><span style="color:var(--accent)">Always.</span></h1>
8
+ <p class="lead">SafeSight uses your device's camera to monitor driver focus and sends real-time alerts when your attention drifts. Drive safer, arrive alive.</p>
9
+ <p class="alert-line">⚠ 315,167 people injured in 2024 from driving distraction</p>
10
+ <div class="cta">
11
+ <a href="/download" class="btn btn-primary">Get the App β†’</a>
12
+ <a href="/how-it-works" class="btn btn-secondary">See how it works</a>
13
+ </div>
14
+ </div>
15
+ <div class="hero-art">
16
+ <div class="phone-frame">
17
+ <div class="phone-screen">
18
+ <div class="phone-banner">Eyes on the road!</div>
19
+ </div>
20
+ </div>
21
+ </div>
22
+ </div>
23
+ </section>
24
+ <section>
25
+ <div class="container grid grid-3">
26
+ <div class="card"><div class="stat">3,300+</div><p>U.S. deaths per year caused by distracted driving.</p></div>
27
+ <div class="card"><div class="stat">1 in 4</div><p>Crashes involve a driver using a phone behind the wheel.</p></div>
28
+ <div class="card"><div class="stat">5 sec</div><p>Average time eyes leave the road when texting β€” at 55 mph, that's a football field.</p></div>
29
+ </div>
30
+ </section>
31
+ <%- include('partials/footer') %>
views/how-it-works.ejs ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <%- include('partials/head') %>
2
+ <%- include('partials/header') %>
3
+ <section class="hero"><div class="container"><h1>How SafeSight works</h1><p class="lead">Five steps. Zero data leaving your phone.</p></div></section>
4
+ <section><div class="container grid grid-3">
5
+ <div class="card"><h3>1. Mount</h3><p>Place your phone in a dashboard mount so the front camera faces you.</p></div>
6
+ <div class="card"><h3>2. Calibrate</h3><p>A quick one-time calibration learns where "eyes on the road" looks like for your setup.</p></div>
7
+ <div class="card"><h3>3. Detect</h3><p>An on-device neural network analyzes head and eye position 30 times per second.</p></div>
8
+ <div class="card"><h3>4. Alert</h3><p>When your gaze drifts for more than a fraction of a second, SafeSight chirps and vibrates.</p></div>
9
+ <div class="card"><h3>5. Review</h3><p>After your trip, see a private summary of focus events β€” stored encrypted on your phone.</p></div>
10
+ </div></div></section>
11
+ <%- include('partials/footer') %>
views/partials/footer.ejs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <footer class="site-footer">
2
+ <div class="container">
3
+ <p class="disclaimer">
4
+ <strong>Safety disclaimer:</strong> SafeSight is a driver-assistance tool. It is
5
+ <strong>not</strong> responsible for any accidents, injuries, or damages that may
6
+ occur while driving. Drivers must remain attentive at all times, handle distractions
7
+ responsibly, and are fully responsible for the safe operation of their vehicle.
8
+ </p>
9
+ <div class="footlinks">
10
+ <a href="/privacy">Privacy</a>
11
+ <a href="/terms">Terms</a>
12
+ <a href="/contact">Contact</a>
13
+ </div>
14
+ <p class="copy">Β© <%= new Date().getFullYear() %> SafeSight</p>
15
+ </div>
16
+ </footer>
17
+ </body>
18
+ </html>
views/partials/head.ejs ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title><%= title %></title>
7
+ <meta name="description" content="<%= desc %>">
8
+ <link rel="icon" type="image/svg+xml" href="/static/img/logo.svg">
9
+ <meta property="og:title" content="<%= title %>">
10
+ <meta property="og:description" content="<%= desc %>">
11
+ <meta property="og:type" content="website">
12
+ <meta name="twitter:card" content="summary">
13
+ <link rel="stylesheet" href="/static/styles.css">
14
+ </head>
15
+ <body>
views/partials/header.ejs ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <header class="site-header">
2
+ <div class="container nav">
3
+ <a href="/" class="brand">
4
+ <img src="/static/img/logo.svg" alt="SafeSight logo" width="28" height="28">
5
+ <span>SafeSight</span>
6
+ </a>
7
+ <nav>
8
+ <a href="/how-it-works">How it works</a>
9
+ <a href="/why-safesight">Why SafeSight</a>
10
+ <a href="/pricing">Pricing</a>
11
+ <a href="/about">About</a>
12
+ <a href="/contact">Contact</a>
13
+ <a href="/download" class="btn btn-primary">Download</a>
14
+ </nav>
15
+ </div>
16
+ </header>
views/pricing.ejs ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <%- include('partials/head') %>
2
+ <%- include('partials/header') %>
3
+ <section class="hero"><div class="container" style="text-align:center">
4
+ <h1>Simple pricing</h1>
5
+ <p class="lead" style="margin:0 auto">Try free for 7 days. Cancel anytime.</p>
6
+ </div></section>
7
+ <section><div class="container">
8
+ <script>
9
+ (function(){
10
+ var p = new URLSearchParams(location.search);
11
+ if (p.get('status')==='success') document.write('<div class="notice success">Thanks! Your subscription is active.</div>');
12
+ if (p.get('status')==='cancelled') document.write('<div class="notice warn">Checkout cancelled β€” no charge was made.</div>');
13
+ })();
14
+ </script>
15
+ <div class="price">
16
+ <div class="plan">
17
+ <h3>Monthly</h3>
18
+ <p class="plan-sub">Pay as you go. Cancel anytime.</p>
19
+ <div class="amount">$4.99<span class="amount-unit">/mo</span></div>
20
+ <div class="amount-equiv">&nbsp;</div>
21
+ <ul class="features">
22
+ <li class="ok">Real-time gaze tracking</li>
23
+ <li class="ok">Instant distraction alerts</li>
24
+ <li class="ok">Drowsiness detection</li>
25
+ <li class="ok">Focus Score per trip</li>
26
+ <li class="no">Trip history &amp; trends</li>
27
+ <li class="ok">100% on-device privacy</li>
28
+ </ul>
29
+ <form method="POST" action="/checkout">
30
+ <input type="hidden" name="plan" value="monthly">
31
+ <button type="submit" class="btn btn-secondary" style="width:100%">Subscribe Monthly</button>
32
+ </form>
33
+ </div>
34
+ <div class="plan featured">
35
+ <h3>Annual <span class="badge">Best Value</span></h3>
36
+ <p class="plan-sub">Billed once per year. Biggest savings.</p>
37
+ <div class="amount">$39.99<span class="amount-unit">/yr</span></div>
38
+ <div class="amount-equiv">$3.33/mo equivalent</div>
39
+ <ul class="features">
40
+ <li class="ok">Real-time gaze tracking</li>
41
+ <li class="ok">Instant distraction alerts</li>
42
+ <li class="ok">Drowsiness detection</li>
43
+ <li class="ok">Focus Score per trip</li>
44
+ <li class="ok">Trip history &amp; trends</li>
45
+ <li class="ok">100% on-device privacy</li>
46
+ </ul>
47
+ <form method="POST" action="/checkout">
48
+ <input type="hidden" name="plan" value="annual">
49
+ <button type="submit" class="btn btn-primary" style="width:100%">Subscribe Annually</button>
50
+ </form>
51
+ </div>
52
+ </div>
53
+ <p style="text-align:center;color:var(--muted);margin-top:24px">All plans include a 7-day free trial. No credit card required to start.</p>
54
+ </div></section>
55
+ <%- include('partials/footer') %>
views/privacy.ejs ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <%- include('partials/head') %>
2
+ <%- include('partials/header') %>
3
+ <section class="hero"><div class="container"><h1>Privacy Policy</h1></div></section>
4
+ <section><div class="container" style="max-width:760px">
5
+ <p>SafeSight is built privacy-first. The mobile app processes camera frames entirely on your device. We do not upload, store, or transmit video, audio, or images of you to any server.</p>
6
+ <h3>1. What we collect</h3>
7
+ <p>Only the minimum required to operate billing: your email and Stripe customer ID. Billing records are stored encrypted at rest using AES-256-GCM on persistent storage with a key only the operator controls.</p>
8
+ <h3>2. What we never collect</h3>
9
+ <p>Camera frames, biometric data, location, contacts, or any in-vehicle imagery.</p>
10
+ <h3>3. Third parties</h3>
11
+ <p>Stripe processes payments. Their privacy policy applies to payment data.</p>
12
+ <h3>4. Cookies</h3>
13
+ <p>We use only essential cookies required for checkout sessions.</p>
14
+ <h3>5. Children</h3>
15
+ <p>SafeSight is not directed to children under 13.</p>
16
+ <h3>6. Your rights</h3>
17
+ <p>You can request deletion of your account and associated data at any time using the form below. We will process deletion requests within 30 days.</p>
18
+
19
+ <% const sent = (typeof query !== 'undefined' && query.sent) ? true : false; %>
20
+ <script>
21
+ (function(){
22
+ var p = new URLSearchParams(location.search);
23
+ if (p.get('sent')==='1') document.write('<div class="notice success">Request received. We will process it within 30 days.</div>');
24
+ })();
25
+ </script>
26
+
27
+ <form method="POST" action="/deletion-request" class="card" style="margin-top:16px">
28
+ <label for="dr-email">Email on the account</label>
29
+ <input id="dr-email" name="email" type="email" required maxlength="255" placeholder="you@example.com">
30
+
31
+ <label for="dr-customer">Stripe customer ID (optional)</label>
32
+ <input id="dr-customer" name="customerId" type="text" maxlength="64" placeholder="cus_...">
33
+
34
+ <label for="dr-reason">Reason (optional)</label>
35
+ <textarea id="dr-reason" name="reason" rows="4" maxlength="1000" placeholder="Anything you'd like us to know"></textarea>
36
+
37
+ <button type="submit" class="btn btn-primary">Request data deletion</button>
38
+ </form>
39
+ </div></section>
40
+ <%- include('partials/footer') %>
views/terms.ejs ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <%- include('partials/head') %>
2
+ <%- include('partials/header') %>
3
+ <section class="hero"><div class="container"><h1>Terms of Service</h1></div></section>
4
+ <section><div class="container" style="max-width:760px">
5
+ <div class="disclaimer" style="margin-bottom:24px">
6
+ <strong>Safety disclaimer.</strong> SafeSight is a driver-assistance tool only. It is
7
+ <strong>not</strong> a substitute for attentive driving and is <strong>not responsible</strong>
8
+ for any accidents, injuries, property damage, or losses that may occur while using the
9
+ product. Drivers must remain alert at all times, handle distractions responsibly, comply
10
+ with all traffic laws, and are <strong>fully responsible</strong> for the safe operation
11
+ of their vehicle. By using SafeSight you acknowledge and accept this disclaimer.
12
+ </div>
13
+ <h3>Service</h3>
14
+ <p>SafeSight provides software that warns drivers when their eyes appear to leave the road. Detection is probabilistic and may produce false positives or miss events.</p>
15
+ <h3>Subscription & billing</h3>
16
+ <p>Plans renew automatically until cancelled. You may cancel at any time; access continues through the end of the paid period.</p>
17
+ <h3>Limitation of liability</h3>
18
+ <p>To the maximum extent permitted by law, SafeSight and its operators are not liable for any direct, indirect, incidental, consequential, or punitive damages arising from use of the product, including any vehicle accident.</p>
19
+ <h3>Changes</h3>
20
+ <p>We may update these terms. Continued use constitutes acceptance.</p>
21
+ </div></section>
22
+ <%- include('partials/footer') %>
views/why-safesight.ejs ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <%- include('partials/head') %>
2
+ <%- include('partials/header') %>
3
+ <section class="hero"><div class="container"><h1>Why SafeSight is different</h1><p class="lead">Real-time, private, and built around how people actually drive.</p></div></section>
4
+ <section><div class="container">
5
+ <table>
6
+ <thead><tr><th></th><th>SafeSight</th><th>Typical "safe driving" apps</th></tr></thead>
7
+ <tbody>
8
+ <tr><td>Detects eyes off road in real time</td><td>βœ”</td><td>✘</td></tr>
9
+ <tr><td>Processing happens on-device</td><td>βœ”</td><td>✘ (uploaded to cloud)</td></tr>
10
+ <tr><td>Works without an internet connection</td><td>βœ”</td><td>✘</td></tr>
11
+ <tr><td>No video or audio leaves your phone</td><td>βœ”</td><td>✘</td></tr>
12
+ <tr><td>Alerts within ~200 ms</td><td>βœ”</td><td>Post-trip reports only</td></tr>
13
+ </tbody>
14
+ </table>
15
+ </div></section>
16
+ <%- include('partials/footer') %>