Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4989641b39 |
@@ -55,7 +55,7 @@ jobs:
|
||||
echo "$CHANGED"
|
||||
# Asset-only: files that don't require rebuilding the El binary.
|
||||
# migrations/, scripts/, tests/ are data/infra/test changes — no binary rebuild needed.
|
||||
NON_ASSET=$(echo "$CHANGED" | grep -v '^src/assets/' | grep -v '^src/shares/' | grep -v '^src/index\.html' | grep -v '^src/about\.html' | grep -v '^src/terms\.html' | grep -v '^src/enterprise-terms\.html' | grep -v '^src/llms\.txt' | grep -v '^migrations/' | grep -v '^scripts/' | grep -v '^tests/' | grep -v '^\.gitea/' | grep -v '^$' || true)
|
||||
NON_ASSET=$(echo "$CHANGED" | grep -v '^src/assets/' | grep -v '^src/shares/' | grep -v '^src/index\.html' | grep -v '^src/about\.html' | grep -v '^src/terms\.html' | grep -v '^src/privacy\.html' | grep -v '^src/enterprise-terms\.html' | grep -v '^src/llms\.txt' | grep -v '^migrations/' | grep -v '^scripts/' | grep -v '^tests/' | grep -v '^\.gitea/' | grep -v '^$' || true)
|
||||
if [ -z "$NON_ASSET" ] && [ "$CHANGED" != "unknown" ]; then
|
||||
echo "asset_only=true" >> "$GITHUB_OUTPUT"
|
||||
echo "=> Asset-only change detected, will use fast path"
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
- name: Touch HTML placeholder files
|
||||
# El binary regenerates these at startup via fs_write. They must exist
|
||||
# in the build context for Dockerfile COPY to succeed.
|
||||
run: touch src/index.html src/about.html src/terms.html src/enterprise-terms.html
|
||||
run: touch src/index.html src/about.html src/terms.html src/privacy.html src/enterprise-terms.html
|
||||
|
||||
# ── El SDK setup ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
+1
-1
@@ -96,7 +96,7 @@ RUN apt-get update \
|
||||
&& groupadd -r landing && useradd -r -g landing landing
|
||||
|
||||
COPY --from=builder /build/landing /usr/local/bin/landing
|
||||
COPY src/index.html /srv/landing/index.html
|
||||
COPY src/index.html src/about.html src/terms.html src/privacy.html src/enterprise-terms.html /srv/landing/
|
||||
COPY src/assets /srv/landing/assets
|
||||
|
||||
ENV LANDING_ROOT=/srv/landing
|
||||
|
||||
+2
-2
@@ -37,8 +37,8 @@ COPY src/llms.txt /srv/landing/llms.txt
|
||||
# rewrite them with the freshly-rendered page (extracted JS asset paths,
|
||||
# updated chat widget, etc.). Without this they stay as their COPY'd root-
|
||||
# owned shells and the served HTML never reflects post-COPY source edits.
|
||||
COPY src/about.html src/terms.html src/enterprise-terms.html src/index.html /srv/landing/
|
||||
RUN chown landing:landing /srv/landing/about.html /srv/landing/terms.html /srv/landing/enterprise-terms.html /srv/landing/index.html /srv/landing/llms.txt
|
||||
COPY src/about.html src/terms.html src/privacy.html src/enterprise-terms.html src/index.html /srv/landing/
|
||||
RUN chown landing:landing /srv/landing/about.html /srv/landing/terms.html /srv/landing/privacy.html /srv/landing/enterprise-terms.html /srv/landing/index.html /srv/landing/llms.txt
|
||||
|
||||
COPY dist/entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
// GET /api/founding-count → {"sold":N,"total":N,"remaining":N}
|
||||
// GET /assets/* → static files under $LANDING_ROOT/assets/
|
||||
// GET /brand/* → static files under $LANDING_ROOT/assets/brand/
|
||||
// GET /legal/privacy → privacy.html
|
||||
// GET /<other> → 404 JSON
|
||||
|
||||
// ── Path helpers ──────────────────────────────────────────────────────────────
|
||||
@@ -118,6 +119,9 @@ fn handle_request(method: String, path: String, body: String) -> String {
|
||||
if str_eq(clean, "/legal/terms") {
|
||||
return fs_read(root_dir() + "/terms.html")
|
||||
}
|
||||
if str_eq(clean, "/legal/privacy") {
|
||||
return fs_read(root_dir() + "/privacy.html")
|
||||
}
|
||||
if str_eq(clean, "/about") {
|
||||
return fs_read(root_dir() + "/about.html")
|
||||
}
|
||||
|
||||
+30
-1
@@ -651,7 +651,8 @@ fn account_roadmap_phase2() -> String {
|
||||
el_li("", "Process and knowledge packets") +
|
||||
el_li("", "Local inference via Ollama") +
|
||||
el_li("", "Additional connectors — GitHub, Notion, Linear, more") +
|
||||
el_li("", "Mobile companion app")
|
||||
el_li("", "Mobile companion app") +
|
||||
el_li("", "Family accounts")
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -691,6 +692,33 @@ fn account_roadmap_section() -> String {
|
||||
)
|
||||
}
|
||||
|
||||
fn account_family_section() -> String {
|
||||
let child_attest: String = "<label style=\"display:flex;gap:.5rem;align-items:flex-start;cursor:pointer;margin:.75rem 0\">" +
|
||||
"<input type=\"checkbox\" id=\"family-attest\" style=\"margin-top:.2rem;accent-color:var(--navy)\">" +
|
||||
"<span style=\"font-size:.8rem;color:var(--t2)\">I confirm this person is a minor under my care and is using this account under my supervision.</span>" +
|
||||
"</label>"
|
||||
el_div(
|
||||
"id=\"family-section\" style=\"display:none\"",
|
||||
el_div(
|
||||
"class=\"card-dark\"",
|
||||
el_div(
|
||||
"class=\"acct-section-header\"",
|
||||
el_p("class=\"card-label\"", "Family members") +
|
||||
el_p("style=\"font-size:.8125rem;font-weight:300;color:var(--t2);line-height:1.65\"", "Add up to 5 children to your Founding Member plan. $10/month per child.")
|
||||
) +
|
||||
el_div("id=\"family-list\"", "") +
|
||||
el_div(
|
||||
"id=\"add-child-form\"",
|
||||
el_input("email", "id=\"child-email\" placeholder=\"Child's email\" class=\"acct-input\"") +
|
||||
el_input("number", "id=\"child-dob-year\" placeholder=\"Birth year (e.g. 2012)\" min=\"1990\" max=\"2007\" class=\"acct-input\"") +
|
||||
child_attest +
|
||||
el_button("class=\"btn-primary\" onclick=\"addFamilyMember()\" style=\"padding:.75rem 1.5rem\"", "Add family member") +
|
||||
el_p("id=\"family-msg\" style=\"display:none;font-size:.8rem;margin-top:.5rem\"", "")
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
fn account_badge_section() -> String {
|
||||
el_div(
|
||||
"id=\"badge-section\" style=\"display:none\"",
|
||||
@@ -924,6 +952,7 @@ fn account_dashboard_section() -> String {
|
||||
account_plan_card() +
|
||||
account_api_keys_section() +
|
||||
account_roadmap_section() +
|
||||
account_family_section() +
|
||||
account_badge_section() +
|
||||
account_timeline_card() +
|
||||
account_download_card() +
|
||||
|
||||
+2
-2
@@ -129,8 +129,8 @@ fn comparison_rows() -> String {
|
||||
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"font-weight:500;color:var(--t2)\">$30/mo</span><br><span style=\"font-size:0.7rem\">Microsoft 365</span></td>" +
|
||||
"</tr>"
|
||||
let row10: String = "<tr>" +
|
||||
"<td style=\"padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500\">Free trial</td>" +
|
||||
"<td style=\"text-align:center;padding:1rem\"><span style=\"color:#008040;font-weight:700;font-size:1.1rem\">✓</span><br><span style=\"font-size:0.7rem;color:var(--t3);font-weight:400\">Full app, 14 days</span></td>" +
|
||||
"<td style=\"padding:1rem 1.5rem 1rem 0;color:var(--t1);font-weight:500\">Free tier</td>" +
|
||||
"<td style=\"text-align:center;padding:1rem\"><span style=\"color:#008040;font-weight:700;font-size:1.1rem\">✓</span><br><span style=\"font-size:0.7rem;color:var(--t3);font-weight:400\">Full app, forever</span></td>" +
|
||||
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">●</span><br><span style=\"font-size:0.7rem\">GPT-3.5 only</span></td>" +
|
||||
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">●</span><br><span style=\"font-size:0.7rem\">Limited</span></td>" +
|
||||
"<td style=\"text-align:center;padding:1rem;color:var(--t3)\"><span style=\"color:#D4A017\">●</span><br><span style=\"font-size:0.7rem\">Limited</span></td>" +
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// account-dashboard.el -- Account dashboard: session check, plan card.
|
||||
// Handles onAuthStateChange and plan card rendering.
|
||||
// account-dashboard.el -- Account dashboard: session check, plan card, family.
|
||||
// Handles onAuthStateChange, plan card rendering, family member management.
|
||||
// Compiled with: elc --target=js --bundle --minify --obfuscate
|
||||
//
|
||||
// Required globals: window.NEURON_CFG.supabase_url, window.NEURON_CFG.supabase_anon_key
|
||||
@@ -127,8 +127,77 @@ fn main() -> Void {
|
||||
|
||||
var roadmapSection = document.getElementById('roadmap-section');
|
||||
if (plan === 'founding' && roadmapSection) roadmapSection.style.display = '';
|
||||
|
||||
if (plan === 'founding') {
|
||||
var famSection = document.getElementById('family-section');
|
||||
if (famSection) famSection.style.display = 'block';
|
||||
var session = await sb.auth.getSession();
|
||||
var userEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
|
||||
if (userEmail) loadFamilyMembers(userEmail);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadFamilyMembers(parentEmail) {
|
||||
var r = await fetch('/api/family/members?parent_email=' + encodeURIComponent(parentEmail));
|
||||
var members = await r.json();
|
||||
var list = document.getElementById('family-list');
|
||||
if (!list) return;
|
||||
if (!members || !members.length) {
|
||||
list.innerHTML = '<p style=\"color:var(--t3);font-size:.875rem;margin-bottom:1rem\">No family members yet.</p>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = members.map(function(m) {
|
||||
return '<div style=\"display:flex;justify-content:space-between;align-items:center;padding:.75rem 0;border-bottom:1px solid var(--border)\">'
|
||||
+ '<div><p style=\"font-size:.875rem;color:var(--t1)\">' + m.child_email + '</p>'
|
||||
+ '<p style=\"font-size:.75rem;color:var(--t3);text-transform:uppercase;letter-spacing:.06em\">' + m.status + '</p></div>'
|
||||
+ '<button onclick=\"removeFamilyMember(\'' + m.child_email + '\')\" style=\"background:none;border:none;color:var(--t3);cursor:pointer;font-size:.75rem\">Remove</button>'
|
||||
+ '</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
window.addFamilyMember = async function() {
|
||||
var email = document.getElementById('child-email').value.trim();
|
||||
var year = document.getElementById('child-dob-year').value;
|
||||
var attest = document.getElementById('family-attest').checked;
|
||||
var msg = document.getElementById('family-msg');
|
||||
if (!email || !year || !attest) {
|
||||
msg.style.display='block'; msg.style.color='#c44'; msg.textContent='Please fill in all fields and confirm the attestation.'; return;
|
||||
}
|
||||
if (parseInt(year) < 2008) {
|
||||
msg.style.display='block'; msg.style.color='#c44'; msg.textContent='Child must be under 18. Birth year must be 2008 or later.'; return;
|
||||
}
|
||||
var session = await sb.auth.getSession();
|
||||
var parentEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
|
||||
var r = await fetch('/api/family/invite', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({parent_email: parentEmail, child_email: email, child_dob_year: parseInt(year), attested: true})
|
||||
});
|
||||
var d = await r.json();
|
||||
msg.style.display = 'block';
|
||||
if (d.ok) {
|
||||
msg.style.color = 'var(--navy)';
|
||||
msg.textContent = 'Invitation sent to ' + email + '. They will receive an email to set up their account.';
|
||||
document.getElementById('child-email').value = '';
|
||||
document.getElementById('child-dob-year').value = '';
|
||||
document.getElementById('family-attest').checked = false;
|
||||
loadFamilyMembers(parentEmail);
|
||||
} else {
|
||||
msg.style.color = '#c44';
|
||||
msg.textContent = d.error || 'Something went wrong.';
|
||||
}
|
||||
};
|
||||
|
||||
window.removeFamilyMember = async function(childEmail) {
|
||||
var session = await sb.auth.getSession();
|
||||
var parentEmail = session.data.session && session.data.session.user ? session.data.session.user.email : '';
|
||||
await fetch('/api/family/remove', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({parent_email: parentEmail, child_email: childEmail})
|
||||
});
|
||||
loadFamilyMembers(parentEmail);
|
||||
};
|
||||
|
||||
function renderUserChip(user) {
|
||||
var email = user.email || '';
|
||||
var avatarEl = document.getElementById('user-avatar-el');
|
||||
|
||||
+1
-1
@@ -55,7 +55,7 @@ fn local_first() -> String {
|
||||
let principle3: String = el_div(
|
||||
"class=\"lf-principle reveal\" style=\"transition-delay:240ms\"",
|
||||
el_p("class=\"lf-principle-label\"", "No ads. Ever.") +
|
||||
el_p("class=\"lf-principle-body\"", "Not during your trial. Not on paid. Not in any future version. Ads require surveillance. Surveillance requires your data. I'm not building that.")
|
||||
el_p("class=\"lf-principle-body\"", "Not on the free tier. Not on paid. Not in any future version. Ads require surveillance. Surveillance requires your data. I'm not building that.")
|
||||
)
|
||||
let principle4: String = el_div(
|
||||
"class=\"lf-principle reveal\" style=\"transition-delay:360ms\"",
|
||||
|
||||
+178
-2
@@ -82,6 +82,7 @@ from styles import { page_open, page_open_seo, page_close }
|
||||
from about import { about_page }
|
||||
from founding_badge import { founding_badge, founding_badge_css }
|
||||
from terms import { terms_page }
|
||||
from privacy import { privacy_page }
|
||||
from enterprise_terms import { enterprise_terms_page }
|
||||
from checkout import { checkout_page }
|
||||
from safety import { safety }
|
||||
@@ -582,6 +583,7 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
|
||||
+ " <url><loc>https://neurontechnologies.ai/</loc><changefreq>weekly</changefreq><priority>1.0</priority></url>\n"
|
||||
+ " <url><loc>https://neurontechnologies.ai/about</loc><changefreq>monthly</changefreq><priority>0.8</priority></url>\n"
|
||||
+ " <url><loc>https://neurontechnologies.ai/legal/terms</loc><changefreq>monthly</changefreq><priority>0.3</priority></url>\n"
|
||||
+ " <url><loc>https://neurontechnologies.ai/legal/privacy</loc><changefreq>monthly</changefreq><priority>0.3</priority></url>\n"
|
||||
+ " <url><loc>https://neurontechnologies.ai/legal/enterprise-terms</loc><changefreq>monthly</changefreq><priority>0.3</priority></url>\n"
|
||||
+ "</urlset>\n"
|
||||
return http_response(200, "{\"Content-Type\":\"application/xml; charset=utf-8\"}", sitemap_body)
|
||||
@@ -605,6 +607,15 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
|
||||
return "{\"__status__\":404,\"error\":\"not found\"}"
|
||||
}
|
||||
|
||||
// ── Privacy Policy ────────────────────────────────────────────────────────
|
||||
if str_eq(path, "/legal/privacy") {
|
||||
let privacy_path: String = state_get("__privacy_html_file__")
|
||||
if !str_eq(privacy_path, "") {
|
||||
return fs_read(privacy_path)
|
||||
}
|
||||
return "{\"__status__\":404,\"error\":\"not found\"}"
|
||||
}
|
||||
|
||||
// ── Enterprise Agreement ──────────────────────────────────────────────────
|
||||
if str_eq(path, "/legal/enterprise-terms") {
|
||||
let ent_path: String = state_get("__enterprise_terms_html_file__")
|
||||
@@ -628,7 +639,7 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
|
||||
"Founding Member Checkout — Neuron"
|
||||
} else {
|
||||
if str_eq(plan, "free") {
|
||||
"Start Your Free Trial — Neuron"
|
||||
"Get Started Free — Neuron"
|
||||
} else {
|
||||
"Professional Plan Checkout — Neuron"
|
||||
}
|
||||
@@ -637,7 +648,7 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
|
||||
"Secure your Founding Member spot. Pay once, $199 lifetime — Neuron inference included at launch, priced below the major APIs. First 1,000 only."
|
||||
} else {
|
||||
if str_eq(plan, "free") {
|
||||
"Start your 14-day free trial. No credit card required. Your AI that remembers you — runs on your machine, never resets."
|
||||
"Create your free Neuron account. No credit card required. Your AI that remembers you — runs on your machine, never resets."
|
||||
} else {
|
||||
"Subscribe to Neuron Professional for $19/month. The AI that remembers you — persistent memory, runs locally, bring your own API keys."
|
||||
}
|
||||
@@ -1991,6 +2002,164 @@ fn handle_request_inner(method: String, path: String, headers: Map, body: String
|
||||
return badge_css + badge_html
|
||||
}
|
||||
|
||||
// ── Family plan — POST /api/family/invite ────────────────────────────────
|
||||
if str_eq(path, "/api/family/invite") {
|
||||
if !str_eq(method, "POST") {
|
||||
return "{\"error\":\"POST required\"}"
|
||||
}
|
||||
let parent_email: String = json_get(body, "parent_email")
|
||||
let child_email: String = json_get(body, "child_email")
|
||||
let child_dob_year_str: String = json_get(body, "child_dob_year")
|
||||
let attested_str: String = json_get(body, "attested")
|
||||
if str_eq(parent_email, "") || str_eq(child_email, "") || str_eq(child_dob_year_str, "") {
|
||||
return "{\"error\":\"parent_email, child_email, and child_dob_year are required\"}"
|
||||
}
|
||||
if !str_eq(attested_str, "true") {
|
||||
return "{\"error\":\"attestation required\"}"
|
||||
}
|
||||
let child_dob_year: Int = str_to_int(child_dob_year_str)
|
||||
if child_dob_year < 2008 {
|
||||
return "{\"error\":\"Child must be under 18. Birth year must be 2008 or later.\"}"
|
||||
}
|
||||
// Check existing family member count
|
||||
let fam_sb_url: String = state_get("__supabase_project_url__")
|
||||
let fam_sb_key: String = state_get("__supabase_service_key__")
|
||||
if str_eq(fam_sb_key, "") {
|
||||
return "{\"error\":\"not configured\"}"
|
||||
}
|
||||
let pe_safe: String = str_replace(str_replace(parent_email, "\\", "\\\\"), "\"", "\\\"")
|
||||
let ce_safe: String = str_replace(str_replace(child_email, "\\", "\\\\"), "\"", "\\\"")
|
||||
let members_resp: String = http_get_auth(
|
||||
fam_sb_url + "/rest/v1/family_members?parent_email=eq." + parent_email + "&status=neq.cancelled&select=child_email",
|
||||
fam_sb_key
|
||||
)
|
||||
let member_count: Int = json_array_len(members_resp)
|
||||
if member_count >= 5 {
|
||||
return "{\"error\":\"Maximum of 5 family members reached.\"}"
|
||||
}
|
||||
// Check child not already added
|
||||
let dup_resp: String = http_get_auth(
|
||||
fam_sb_url + "/rest/v1/family_members?parent_email=eq." + parent_email + "&child_email=eq." + child_email + "&select=child_email",
|
||||
fam_sb_key
|
||||
)
|
||||
let dup_count: Int = json_array_len(dup_resp)
|
||||
if dup_count > 0 {
|
||||
return "{\"error\":\"This child email is already in your family plan.\"}"
|
||||
}
|
||||
// Create Stripe customer for child
|
||||
let fam_stripe_key: String = state_get("__stripe_secret_key__")
|
||||
if str_eq(fam_stripe_key, "") {
|
||||
return "{\"error\":\"Stripe not configured\"}"
|
||||
}
|
||||
let child_email_safe: String = str_replace(child_email, "@", "%40")
|
||||
let customer_body: String = "email=" + child_email_safe
|
||||
let customer_resp: String = http_post_auth("https://api.stripe.com/v1/customers", fam_stripe_key, customer_body)
|
||||
let child_customer_id: String = json_get(customer_resp, "id")
|
||||
if str_eq(child_customer_id, "") {
|
||||
return "{\"error\":\"Failed to create Stripe customer\"}"
|
||||
}
|
||||
// Create Stripe subscription for child
|
||||
let price_id: String = state_get("__stripe_price_family_child__")
|
||||
if str_eq(price_id, "") {
|
||||
return "{\"error\":\"Family child price not configured\"}"
|
||||
}
|
||||
let parent_email_safe: String = str_replace(parent_email, "@", "%40")
|
||||
let sub_body: String = "customer=" + child_customer_id
|
||||
+ "&items[0][price]=" + price_id
|
||||
+ "&trial_period_days=7"
|
||||
+ "&metadata[parent_email]=" + parent_email_safe
|
||||
+ "&metadata[type]=family_child"
|
||||
let sub_resp: String = http_post_auth("https://api.stripe.com/v1/subscriptions", fam_stripe_key, sub_body)
|
||||
let sub_id: String = json_get(sub_resp, "id")
|
||||
if str_eq(sub_id, "") {
|
||||
return "{\"error\":\"Failed to create Stripe subscription\"}"
|
||||
}
|
||||
// Generate invite token
|
||||
let ts_str: String = int_to_str(unix_timestamp())
|
||||
let email_prefix: String = str_slice(child_email, 0, 4)
|
||||
let invite_token: String = ts_str + email_prefix
|
||||
// Insert into family_members table
|
||||
let fam_row: String = "{\"parent_email\":\"" + pe_safe + "\",\"parent_user_id\":\"\",\"child_email\":\"" + ce_safe + "\",\"child_dob_year\":" + child_dob_year_str + ",\"status\":\"invited\",\"stripe_subscription_id\":\"" + sub_id + "\",\"invite_token\":\"" + invite_token + "\"}"
|
||||
let fam_insert_resp: String = supabase_insert(fam_sb_url, fam_sb_key, "family_members", fam_row)
|
||||
println("[family/invite] insert -> " + fam_insert_resp)
|
||||
// Send invite email
|
||||
let invite_text: String = "You have been invited to join Neuron by " + parent_email
|
||||
+ ". Visit https://neurontechnologies.ai/account to set up your account."
|
||||
let invite_send: String = send_email(
|
||||
"Neuron <no-reply@neurontechnologies.ai>",
|
||||
child_email,
|
||||
"You have been invited to Neuron",
|
||||
"",
|
||||
invite_text
|
||||
)
|
||||
println("[family/invite] " + invite_send)
|
||||
return "{\"ok\":true,\"invite_token\":\"" + invite_token + "\"}"
|
||||
}
|
||||
|
||||
// ── Family plan — GET /api/family/members ────────────────────────────────
|
||||
if str_starts_with(path, "/api/family/members") {
|
||||
let fml_sb_url: String = state_get("__supabase_project_url__")
|
||||
let fml_sb_key: String = state_get("__supabase_service_key__")
|
||||
if str_eq(fml_sb_key, "") {
|
||||
return "[]"
|
||||
}
|
||||
// Extract parent_email from query string
|
||||
let fml_parent_email: String = ""
|
||||
let q_idx: Int = str_index_of(path, "parent_email=")
|
||||
if q_idx >= 0 {
|
||||
let fml_parent_email = str_slice(path, q_idx + 13, str_len(path))
|
||||
}
|
||||
if str_eq(fml_parent_email, "") {
|
||||
return "{\"error\":\"parent_email required\"}"
|
||||
}
|
||||
let fml_resp: String = http_get_auth(
|
||||
fml_sb_url + "/rest/v1/family_members?parent_email=eq." + fml_parent_email + "&select=child_email,status,child_dob_year,created_at",
|
||||
fml_sb_key
|
||||
)
|
||||
if str_eq(fml_resp, "") {
|
||||
return "[]"
|
||||
}
|
||||
return fml_resp
|
||||
}
|
||||
|
||||
// ── Family plan — POST /api/family/remove ────────────────────────────────
|
||||
if str_eq(path, "/api/family/remove") {
|
||||
if !str_eq(method, "POST") {
|
||||
return "{\"error\":\"POST required\"}"
|
||||
}
|
||||
let rem_parent_email: String = json_get(body, "parent_email")
|
||||
let rem_child_email: String = json_get(body, "child_email")
|
||||
if str_eq(rem_parent_email, "") || str_eq(rem_child_email, "") {
|
||||
return "{\"error\":\"parent_email and child_email required\"}"
|
||||
}
|
||||
let rem_sb_url: String = state_get("__supabase_project_url__")
|
||||
let rem_sb_key: String = state_get("__supabase_service_key__")
|
||||
if str_eq(rem_sb_key, "") {
|
||||
return "{\"error\":\"not configured\"}"
|
||||
}
|
||||
// Fetch the subscription ID
|
||||
let sub_lookup: String = http_get_auth(
|
||||
rem_sb_url + "/rest/v1/family_members?parent_email=eq." + rem_parent_email + "&child_email=eq." + rem_child_email + "&select=stripe_subscription_id",
|
||||
rem_sb_key
|
||||
)
|
||||
let rem_sub_id: String = json_get(sub_lookup, "0.stripe_subscription_id")
|
||||
// Cancel Stripe subscription if present
|
||||
if !str_eq(rem_sub_id, "") {
|
||||
let rem_stripe_key: String = state_get("__stripe_secret_key__")
|
||||
if !str_eq(rem_stripe_key, "") {
|
||||
let cancel_resp: String = http_post_auth("https://api.stripe.com/v1/subscriptions/" + rem_sub_id + "/cancel", rem_stripe_key, "")
|
||||
println("[family/remove] cancel sub " + rem_sub_id + " -> " + cancel_resp)
|
||||
}
|
||||
}
|
||||
// Update status to cancelled
|
||||
let rpe_safe: String = str_replace(str_replace(rem_parent_email, "\\", "\\\\"), "\"", "\\\"")
|
||||
let rce_safe: String = str_replace(str_replace(rem_child_email, "\\", "\\\\"), "\"", "\\\"")
|
||||
let cancel_row: String = "{\"status\":\"cancelled\"}"
|
||||
supabase_insert(rem_sb_url, rem_sb_key, "family_members?parent_email=eq." + rem_parent_email + "&child_email=eq." + rem_child_email, cancel_row)
|
||||
println("[family/remove] cancelled " + rce_safe + " from " + rpe_safe)
|
||||
return "{\"ok\":true}"
|
||||
}
|
||||
|
||||
// ── API key provisioning — POST /api/api-keys ────────────────────────────
|
||||
// Returns user's stored provider keys (masked) for display on /account.
|
||||
// Body: { access_token: "<jwt>" }
|
||||
@@ -2187,6 +2356,7 @@ let stripe_key: String = env("STRIPE_SECRET_KEY")
|
||||
let stripe_pub_key: String = env("STRIPE_PUBLISHABLE_KEY")
|
||||
let stripe_price_founding: String = env("STRIPE_PRICE_FOUNDING")
|
||||
let stripe_price_professional: String = env("STRIPE_PRICE_PROFESSIONAL")
|
||||
let family_child_price: String = env("STRIPE_PRICE_FAMILY_CHILD")
|
||||
let stripe_webhook_secret: String = env("STRIPE_WEBHOOK_SECRET")
|
||||
let license_api_url: String = env("NEURON_LICENSE_API_URL")
|
||||
let resend_api_key: String = env("RESEND_API_KEY")
|
||||
@@ -2227,16 +2397,22 @@ let ent_terms_html_path: String = src_dir + "/enterprise-terms.html"
|
||||
fs_write(terms_html_path, terms_page())
|
||||
fs_write(ent_terms_html_path, enterprise_terms_page())
|
||||
|
||||
// Generate privacy policy HTML.
|
||||
let privacy_html_path: String = src_dir + "/privacy.html"
|
||||
fs_write(privacy_html_path, privacy_page())
|
||||
|
||||
// Register with El HTTP runtime.
|
||||
state_set("__html_file__", html_path)
|
||||
state_set("__about_html_file__", about_html_path)
|
||||
state_set("__terms_html_file__", terms_html_path)
|
||||
state_set("__privacy_html_file__", privacy_html_path)
|
||||
state_set("__enterprise_terms_html_file__", ent_terms_html_path)
|
||||
state_set("__src_dir__", src_dir)
|
||||
state_set("__stripe_secret_key__", stripe_key)
|
||||
state_set("__stripe_publishable_key__", stripe_pub_key)
|
||||
state_set("__stripe_price_founding__", stripe_price_founding)
|
||||
state_set("__stripe_price_professional__", stripe_price_professional)
|
||||
state_set("__stripe_price_family_child__", family_child_price)
|
||||
state_set("__license_api_url__", license_api_url)
|
||||
state_set("__resend_api_key__", resend_api_key)
|
||||
state_set("__supabase_anon_key__", supabase_anon_key)
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
// components/pricing.el - Three pricing cards: Free trial / Professional / Founding Member.
|
||||
// components/pricing.el - Three pricing cards: Free / Professional / Founding Member.
|
||||
// Accepts the founding counter values as parameters so main.el can inject
|
||||
// server-side rendered counts without JS.
|
||||
|
||||
@@ -89,14 +89,14 @@ fn pricing(sold: Int, total: Int) -> String {
|
||||
el_div(
|
||||
"class=\"pricing-price-row\"",
|
||||
el_span("class=\"pricing-price\"", "$0") +
|
||||
el_span("class=\"pricing-cadence\"", "for 14 days")
|
||||
el_span("class=\"pricing-cadence\"", "forever")
|
||||
) +
|
||||
el_p("class=\"pricing-tagline\"", "Try everything free for 14 days. No card required.") +
|
||||
el_p("class=\"pricing-tagline\"", "Start building your memory. No card required.") +
|
||||
el_ul("class=\"pricing-features\"", pricing_free_features()) +
|
||||
el_div("style=\"flex:1\"", "") +
|
||||
el_div(
|
||||
"class=\"pricing-cta pricing-cta-ghost\"",
|
||||
el_button("class=\"pricing-cta-ghost\" data-checkout=\"free\"", "Start free trial →")
|
||||
el_button("class=\"pricing-cta-ghost\" data-checkout=\"free\"", "Preorder free tier →")
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
// components/privacy.el - Consumer Privacy Policy page.
|
||||
// Returns complete HTML using the shared page shell from styles.el.
|
||||
// Mirrors terms.el. Adapted from docs/legal/privacy-policy-companion-DRAFT.md
|
||||
// (draft dated 2026-07-14). PENDING ATTORNEY SIGN-OFF before publication.
|
||||
|
||||
from styles import { page_open_seo, page_close }
|
||||
from nav import { nav }
|
||||
|
||||
extern fn el_div(attrs: String, children: String) -> String
|
||||
extern fn el_span(attrs: String, children: String) -> String
|
||||
extern fn el_h1(attrs: String, text: String) -> String
|
||||
extern fn el_h2(attrs: String, text: String) -> String
|
||||
extern fn el_p(attrs: String, children: String) -> String
|
||||
extern fn el_a(href: String, attrs: String, children: String) -> String
|
||||
extern fn el_strong(children: String) -> String
|
||||
|
||||
fn privacy_page() -> String {
|
||||
page_open_seo(
|
||||
"Privacy Policy — Neuron",
|
||||
"Read the Neuron Privacy Policy. Neuron is local-first: your memory and conversations stay on your device.",
|
||||
"/legal/privacy",
|
||||
"The Neuron Privacy Policy — local-first by design. Your memory and conversations stay on your device.",
|
||||
"false"
|
||||
) + nav() + privacy_body() + page_close()
|
||||
}
|
||||
|
||||
fn privacy_section_head(num: String, title: String) -> String {
|
||||
el_div(
|
||||
"style=\"display:flex;align-items:baseline;gap:1rem;margin-bottom:1.25rem;border-bottom:1px solid var(--border);padding-bottom:0.75rem\"",
|
||||
el_span("style=\"font-family:var(--body);font-size:0.75rem;font-weight:600;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy-65)\"", "§ " + num) +
|
||||
el_h2("style=\"font-family:var(--head);font-size:1.25rem;font-weight:600;color:var(--t1)\"", title)
|
||||
)
|
||||
}
|
||||
|
||||
fn privacy_p(text: String) -> String {
|
||||
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8;margin-bottom:1rem\"", text)
|
||||
}
|
||||
|
||||
fn privacy_p_last(text: String) -> String {
|
||||
el_p("style=\"font-family:var(--body);font-weight:300;font-size:0.9375rem;color:var(--t2);line-height:1.8\"", text)
|
||||
}
|
||||
|
||||
fn privacy_body() -> String {
|
||||
let back_link: String = el_div(
|
||||
"style=\"margin-bottom:3rem\"",
|
||||
el_a("/", "style=\"font-family:var(--body);font-size:0.75rem;font-weight:500;letter-spacing:0.15em;text-transform:uppercase;color:var(--navy);text-decoration:none\"", "← Neuron")
|
||||
)
|
||||
|
||||
let page_header: String = el_div(
|
||||
"style=\"margin-bottom:4rem;border-bottom:1px solid var(--border);padding-bottom:3rem\"",
|
||||
el_p("class=\"label\" style=\"margin-bottom:1rem\"", "Legal") +
|
||||
el_h1("style=\"font-family:var(--head);font-size:clamp(2rem,4vw,3rem);font-weight:600;color:var(--t1);margin-bottom:0.75rem;line-height:1.1\"", "Privacy Policy") +
|
||||
el_p("style=\"font-family:var(--body);font-size:0.875rem;color:var(--t3)\"", "Effective May 1, 2026 · Neuron, LLC")
|
||||
)
|
||||
|
||||
let s1: String = el_div(
|
||||
"style=\"margin-bottom:3rem\"",
|
||||
privacy_section_head("1", "Who We Are") +
|
||||
privacy_p("Neuron is made by Neuron Technologies / Neuron, LLC ("Neuron," "we," "us"). This policy explains what data Neuron handles, where it lives, and what - if anything - leaves your device.") +
|
||||
privacy_p_last("Neuron is an AI companion with persistent memory. Because it is designed to remember you across time and to feel warm and personal, we hold ourselves to a local-first standard: your relationship with Neuron stays on your machine.")
|
||||
)
|
||||
|
||||
let s2: String = el_div(
|
||||
"style=\"margin-bottom:3rem\"",
|
||||
privacy_section_head("2", "Our Core Principle - Local-First") +
|
||||
privacy_p_last("Your Neuron memory and knowledge graph are stored " + "<strong style=\"color:var(--t1);font-weight:500\">on your device</strong>" + " (in ~/.neuron) by a local engine running on your own computer. By default, your memories, notes, conversations, documents, and knowledge never leave your device. We cannot see them.")
|
||||
)
|
||||
|
||||
let s3: String = el_div(
|
||||
"style=\"margin-bottom:3rem\"",
|
||||
privacy_section_head("3", "What Data We Handle, and Where It Goes") +
|
||||
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">Your memory, conversations, and files.</strong> Stored locally on your device. Neuron's servers do not receive them.") +
|
||||
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">AI inference (bring-your-own-key).</strong> To answer you, the Neuron app on your device sends your conversation content directly to the AI provider you choose (for example, Anthropic) using your own API key. This content goes from your machine to that provider; it does not pass through Neuron's servers, and we do not store or train on it. That provider's privacy policy and terms govern how they handle it - you are using your own account with them.") +
|
||||
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">AI inference (future Neuron-hosted tiers).</strong> If you opt into a paid tier that uses Neuron-hosted models, conversation content needed for inference will be processed on Neuron-operated infrastructure. When that launches, we will not train on it, will not sell it, and will state retention plainly. This is forward-looking and not active today.") +
|
||||
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">License validation.</strong> On startup the app contacts our licensing server to verify your license status only. We do not transmit your memory or conversations during validation.") +
|
||||
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">Account & purchase data.</strong> If you create an account or buy a tier, we (and our payment processor and sign-in providers) process your email, authentication identity, and payment metadata. We do not receive full card numbers.") +
|
||||
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">Connectors & imports.</strong> When you connect a third-party service (GitHub, Notion, Linear, Google Drive) or import from another tool, you authorize those flows; the third party's terms govern their side. Imported content and on-device OCR results are stored locally.") +
|
||||
privacy_p_last("<strong style=\"color:var(--t1);font-weight:500\">Website.</strong> The marketing website uses standard anti-abuse and sign-in tooling (for example, Cloudflare Turnstile and OAuth sign-in). The desktop app does not send usage telemetry for ordinary local use.")
|
||||
)
|
||||
|
||||
let s4: String = el_div(
|
||||
"style=\"margin-bottom:3rem\"",
|
||||
privacy_section_head("4", "What We Do Not Do") +
|
||||
privacy_p_last("We do " + "<strong style=\"color:var(--t1);font-weight:500\">not</strong>" + " sell your data. We do " + "<strong style=\"color:var(--t1);font-weight:500\">not</strong>" + " train models on your conversations. We do " + "<strong style=\"color:var(--t1);font-weight:500\">not</strong>" + " read your memory or local files.")
|
||||
)
|
||||
|
||||
let s5: String = el_div(
|
||||
"style=\"margin-bottom:3rem\"",
|
||||
privacy_section_head("5", "Safety Features and Crisis Data") +
|
||||
privacy_p("If Neuron's safety features are active, distress detection runs on your device. If triggered, Neuron shows you crisis resources (such as 988) and, if you have set a personal safety contact, shows you how to reach that person. " + "<strong style=\"color:var(--t1);font-weight:500\">Neuron does not contact anyone on your behalf - it does not notify your safety contact, 988, 911, or anyone else. There is no automatic escalation.</strong>") +
|
||||
privacy_p("Your safety contact (a name, relationship, and phone or email you optionally enter) is stored locally on your device in ~/.neuron. It is surfaced only to you, only in a crisis moment, as a reminder of who you trust. It is not transmitted to us.") +
|
||||
privacy_p_last("Crisis-related conversation content is excluded from Neuron's persistent memory ("Hard Bell" does not retain it), so distressing moments are not resurfaced to you later. Neuron is not a crisis service and not a medical or clinical provider (see Terms § 11). We process only what is necessary for the feature to function on your device.")
|
||||
)
|
||||
|
||||
let s6: String = el_div(
|
||||
"style=\"margin-bottom:3rem\"",
|
||||
privacy_section_head("6", "Age and Minors") +
|
||||
privacy_p_last("<strong style=\"color:var(--t1);font-weight:500\">Neuron is intended for adults; you must be 18 or older to use it</strong>" + " (see Terms § 1). The app asks you to confirm your age before use. We do not knowingly collect personal information from anyone under 18. If we learn that someone under 18 has used Neuron, we will take reasonable steps to delete the information we hold and end that use.")
|
||||
)
|
||||
|
||||
let s7: String = el_div(
|
||||
"style=\"margin-bottom:3rem\"",
|
||||
privacy_section_head("7", "Your Rights and Choices") +
|
||||
privacy_p_last("Most of your data lives on your device and is under your direct control - you can access, correct, export, and delete it yourself. To delete the account, license, or payment records we hold, contact legal@neurontechnologies.ai. Depending on where you live, you may have GDPR or CCPA/CPRA rights, including the right to know, access, delete, correct, and opt out of the "sale" or "sharing" of your personal information - which we do not do.")
|
||||
)
|
||||
|
||||
let s8: String = el_div(
|
||||
"style=\"margin-bottom:3rem\"",
|
||||
privacy_section_head("8", "Data Retention") +
|
||||
privacy_p("<strong style=\"color:var(--t1);font-weight:500\">Local data</strong>" + " persists on your device until you delete it - you control it directly.") +
|
||||
privacy_p_last("<strong style=\"color:var(--t1);font-weight:500\">Account, license, and payment records</strong>" + " we hold are retained only as long as needed to provide the service, then deleted on account closure, subject to legal and accounting requirements.")
|
||||
)
|
||||
|
||||
let s9: String = el_div(
|
||||
"style=\"margin-bottom:3rem\"",
|
||||
privacy_section_head("9", "Security") +
|
||||
privacy_p_last("We use reasonable technical and organizational measures to protect the limited data we hold (account, license, and payment metadata). Because your memory and conversations stay on your device, the security of that device is also important - keep it protected.")
|
||||
)
|
||||
|
||||
let s10: String = el_div(
|
||||
"style=\"margin-bottom:3rem\"",
|
||||
privacy_section_head("10", "Changes to This Policy") +
|
||||
privacy_p_last("We will post material changes here and, where appropriate, notify you in-app.")
|
||||
)
|
||||
|
||||
let s11: String = el_div(
|
||||
"style=\"margin-bottom:3rem\"",
|
||||
privacy_section_head("11", "Contact") +
|
||||
privacy_p_last("legal@neurontechnologies.ai - Neuron Technologies / Neuron, LLC.")
|
||||
)
|
||||
|
||||
let footer_links: String = el_div(
|
||||
"style=\"margin-top:4rem;padding-top:2rem;border-top:1px solid var(--border);display:flex;gap:2rem;flex-wrap:wrap\"",
|
||||
el_a("/", "style=\"font-family:var(--body);font-size:0.8125rem;color:var(--navy);text-decoration:none\"", "← Home") +
|
||||
el_a("/legal/terms", "style=\"font-family:var(--body);font-size:0.8125rem;color:var(--navy);text-decoration:none\"", "Terms of Service →")
|
||||
)
|
||||
|
||||
el_div(
|
||||
"style=\"max-width:720px;margin:0 auto;padding:6rem 2.5rem 8rem\"",
|
||||
back_link +
|
||||
page_header +
|
||||
s1 + s2 + s3 + s4 + s5 + s6 + s7 + s8 + s9 + s10 + s11 +
|
||||
footer_links
|
||||
)
|
||||
}
|
||||
+2
-2
@@ -78,9 +78,9 @@ fn page_head() -> String {
|
||||
return page_head_base()
|
||||
+ page_seo_block(
|
||||
"Neuron — The AI That Remembers You",
|
||||
"Every AI resets when you close the tab. Neuron doesn't. Runs on your machine. Remembers everything. Start with a 14-day free trial — no credit card required.",
|
||||
"Every AI resets when you close the tab. Neuron doesn't. Runs on your machine. Remembers everything. Start free — no credit card required.",
|
||||
"/",
|
||||
"Every other AI forgets you. Neuron doesn't. Runs on your machine, builds a persistent memory over time, and gets sharper the longer you use it. 14-day free trial."
|
||||
"Every other AI forgets you. Neuron doesn't. Runs on your machine, builds a persistent memory over time, and gets sharper the longer you use it. Free tier available."
|
||||
)
|
||||
+ page_schema()
|
||||
}
|
||||
|
||||
+7
-18
@@ -59,6 +59,7 @@ fn terms_body() -> String {
|
||||
"style=\"margin-bottom:3rem\"",
|
||||
terms_section_head("1", "Using Neuron") +
|
||||
terms_p("Using Neuron means you've read these terms and agree to them. That's it. No buried consent, no dark patterns. If something here doesn't sit right with you, email us before using the product - we'll talk through it.") +
|
||||
terms_p("<strong style=\"color:var(--t1);font-weight:500\">You must be 18 or older to use Neuron.</strong> Neuron is intended for adults. By using it you represent that you are at least 18 years old and able to form a binding contract. The app asks you to confirm your age before you can use it, and we do not knowingly permit anyone under 18 to use Neuron.") +
|
||||
terms_p_last("These terms cover every version of Neuron: free, Professional, and Founding Member. Enterprise use has its own agreement. If you're deploying Neuron for an organization, that's the one you want.")
|
||||
)
|
||||
|
||||
@@ -136,26 +137,14 @@ fn terms_body() -> String {
|
||||
terms_p_last("Founding Member licenses are locked. I won't retroactively change what you signed up for.")
|
||||
)
|
||||
|
||||
let hard_bell_block: String = el_div(
|
||||
"style=\"margin:1.5rem 0;padding:1.5rem;border-left:3px solid rgba(0,82,160,.35);background:rgba(0,82,160,.04)\"",
|
||||
el_p("style=\"font-family:var(--body);font-weight:600;font-size:0.875rem;color:var(--t1);margin-bottom:0.875rem;letter-spacing:0.04em\"", "Hard Bell - Required acknowledgment for family accounts") +
|
||||
terms_p("Federal data shows approximately 89% of child maltreatment is perpetrated by a parent or caregiver (HHS, Child Maltreatment 2023). The person listed as the primary contact is not always a safe contact. This system is built with that reality in mind.") +
|
||||
el_p("style=\"font-family:var(--body);font-weight:400;font-size:0.875rem;color:var(--t1);line-height:1.8\"",
|
||||
"When you create a family account that includes a minor, you are agreeing unconditionally to the following: " +
|
||||
"<strong>if a safety signal is triggered by a child's account, the parent or guardian named on the family account will not be the first contact reached.</strong>" +
|
||||
" Emergency services and independently designated trusted contacts are notified first. This is not a setting that can be changed. By creating a family account, you accept this structure."
|
||||
)
|
||||
)
|
||||
|
||||
let s11: String = el_div(
|
||||
"style=\"margin-bottom:3rem\"",
|
||||
terms_section_head("11", "Children and Family Accounts") +
|
||||
terms_p("Children 13 and over may use Neuron independently. Children under 13 may only use Neuron as part of a family account established by a parent or legal guardian. By setting up a family account that includes a minor, the parent or guardian provides verifiable consent for the child's use and accepts these terms on the child's behalf.") +
|
||||
terms_p("When a child's Neuron instance is part of a family account, certain information - including usage activity and relevant context - may be shared from the child's local instance to the parent's local instance. This sharing happens device-to-device and does not pass through or get stored on Neuron's servers.") +
|
||||
hard_bell_block +
|
||||
el_p("style=\"font-family:var(--body);font-weight:500;font-size:0.9375rem;color:var(--t1);line-height:1.8;margin-bottom:0.75rem\"", "Mandatory reporting - applies to all accounts.") +
|
||||
terms_p("By creating any Neuron account, you acknowledge and agree that: " + "<strong style=\"color:var(--t1);font-weight:500\">if Neuron detects credible, specific indicators of intended harm to a person - including harm to the account holder themselves - emergency services or relevant authorities may be contacted.</strong>" + " This is not limited to family accounts. It applies to every user. This is not a privacy violation. It is the only honest answer to what a system that knows you this well is obligated to do when someone is in genuine danger. You cannot opt out of this. If you are unwilling to agree, do not create an account.") +
|
||||
terms_p_last("Parents control what is visible to them through their local instance and can revoke family account access at any time. If you believe a child is using Neuron outside of a family account, contact legal@neurontechnologies.ai and we will address it promptly.")
|
||||
terms_section_head("11", "Safety Features") +
|
||||
terms_p("Neuron is for adults 18 and older (see § 1). It is not a crisis line, not a medical or mental-health service, and not an emergency responder. In an emergency, contact 911 or your local emergency number, or call or text 988 (the Suicide & Crisis Lifeline) directly.") +
|
||||
terms_p("Neuron includes optional safety features intended to help if you are ever in distress. It is important that you understand exactly what they do and do not do.") +
|
||||
terms_p("<strong style=\"color:var(--t1);font-weight:500\">What they do.</strong> If Neuron's on-device distress detection is triggered, Neuron shows you crisis resources - prominently the 988 Suicide & Crisis Lifeline (call, text, or chat) - and, if you have chosen a personal safety contact, it shows you how to reach that person. You remain in control of whether and how you reach out.") +
|
||||
terms_p("<strong style=\"color:var(--t1);font-weight:500\">What they do not do. Neuron does not contact anyone on your behalf. It does not call, text, email, or otherwise notify your safety contact, 988, 911, or any other person or service. There is no automatic escalation.</strong> The safety contact you set is stored locally on your device and is surfaced only to you, as a reminder of who you trust - Neuron never reaches out to them for you.") +
|
||||
terms_p_last("These features are provided "AS IS." Distress detection is imperfect: it may fail to detect distress, may raise false alarms, and must not be relied upon in an emergency. To the maximum extent permitted by law, Neuron, LLC is not liable for any outcome relating to the presence, absence, accuracy, timing, or performance of any safety feature.")
|
||||
)
|
||||
|
||||
let s12: String = el_div(
|
||||
|
||||
Reference in New Issue
Block a user