Spaces:
Runtime error
Runtime error
space: add email signup for Microfactory updates
Browse files- FIELD-NOTES.md +17 -0
- README.md +11 -2
- app.py +17 -1
- core/signups.py +114 -0
FIELD-NOTES.md
CHANGED
|
@@ -109,8 +109,25 @@ refusing any prefix that isn't `<username>/`, etc.) so the next adapter is a
|
|
| 109 |
ten-minute job, not a half-day. Done means someone you've never met can pull
|
| 110 |
and run it in one line. Build the publishing in.
|
| 111 |
|
|
|
|
|
|
|
| 112 |
---
|
| 113 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 114 |
*Microfactory Node: 3D Printer runs fully local (Ollama / llama.cpp, Gemma E4B/E2B), falls back
|
| 115 |
to a deterministic advisor, and publishes both its lesson ledger and its live interaction log as open
|
| 116 |
datasets. It is the first node of the Microfactory, a network of small machines and the people who
|
|
|
|
| 109 |
ten-minute job, not a half-day. Done means someone you've never met can pull
|
| 110 |
and run it in one line. Build the publishing in.
|
| 111 |
|
| 112 |
+
Got it. To capture interest without pretending the product is finished, I added a simple email signup at the bottom of the Space. It is opt-in only: checkbox + email, clear privacy note, stored as a local JSONL and optionally synced to a private HF dataset when `HF_TOKEN` is set. No print data, no uploaded files, no third-party trackers. The same pattern as the field log, but for people instead of jobs.
|
| 113 |
+
|
| 114 |
---
|
| 115 |
|
| 116 |
+
### Build paper trail
|
| 117 |
+
|
| 118 |
+
Everything that is part of the judged artifact is linked from the Space README and mirrored in the
|
| 119 |
+
GitHub repo:
|
| 120 |
+
|
| 121 |
+
- **Model cards:** [`learn/finetune/MODEL_CARD.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/MODEL_CARD.md) · [`MODEL_CARD_QAT.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/MODEL_CARD_QAT.md)
|
| 122 |
+
- **Local run / publish:** [`SERVING.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/SERVING.md) · [`OLLAMA_PUBLISHING.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/OLLAMA_PUBLISHING.md)
|
| 123 |
+
- **Fine-tune pipeline:** [`learn/finetune/README.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/README.md) · [`PIPELINE.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/PIPELINE.md) · [`RUNBOOK.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/RUNBOOK.md) · [`BUDGET.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/BUDGET.md)
|
| 124 |
+
- **Session report + activity trace:** [`SESSION_REPORT.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/SESSION_REPORT.md) · [`activity.jsonl`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/learn/finetune/activity.jsonl)
|
| 125 |
+
- **Calibration:** [`sim/calibration/CALIBRATION-REPORT.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/sim/calibration/CALIBRATION-REPORT.md) · [`sim/calibration/README.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/sim/calibration/README.md)
|
| 126 |
+
- **Ingestion + assets + outputs:** [`ingest/README.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/ingest/README.md) · [`assets/screenshots/README.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/assets/screenshots/README.md) · [`dist/README.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/dist/README.md) · [`dist/deliberation/README.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/dist/deliberation/README.md)
|
| 127 |
+
|
| 128 |
+
Internal planning docs, raw workorders, and archived iterations stay in the GitHub repo but are
|
| 129 |
+
kept out of the Space so the public-facing card stays curated.
|
| 130 |
+
|
| 131 |
*Microfactory Node: 3D Printer runs fully local (Ollama / llama.cpp, Gemma E4B/E2B), falls back
|
| 132 |
to a deterministic advisor, and publishes both its lesson ledger and its live interaction log as open
|
| 133 |
datasets. It is the first node of the Microfactory, a network of small machines and the people who
|
README.md
CHANGED
|
@@ -141,9 +141,9 @@ Local dev uses [uv](https://docs.astral.sh/uv); the HF Space installs via `requi
|
|
| 141 |
**Layout:** `app.py` (Space entrypoint) + `test_core.py` at the root; the core library lives in
|
| 142 |
`core/`, runnable helpers in `scripts/` (run via `make <target>` or `uv run python -m scripts.<name>`),
|
| 143 |
and the simulator / learning-loop / ingestion packages in `sim/`, `learn/`, `ingest/`. See
|
| 144 |
-
`docs/RUNBOOK.md` for the full run-and-test order of operations.
|
| 145 |
|
| 146 |
-
**HF Space deployment** (source repo): `docs/reference/DEPLOYMENT.md` + `docs/RUNBOOK.md`
|
| 147 |
("Deploy to the Space") cover Space variables, the dual local/Space config, build strategy,
|
| 148 |
the `@spaces.GPU`-on-inference-only pattern, and field-log setup.
|
| 149 |
|
|
@@ -239,7 +239,16 @@ Storytelling is a judging principle, not a badge.
|
|
| 239 |
- **Field log (live):** [build-small-hackathon/chief-engineer-field-log](https://huggingface.co/datasets/build-small-hackathon/chief-engineer-field-log)
|
| 240 |
- **Demo video:** [<!-- TODO: add video URL after recording -->]()
|
| 241 |
- **Social post:** [<!-- TODO: add social post URL after publishing -->]()
|
|
|
|
| 242 |
- **How to use it (guided tour):** [`docs/RUNBOOK.md` §2](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/docs/RUNBOOK.md#2--use-the-tool-the-guided-tour--also-the-judges-tour)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 243 |
- **Source:** [kylebrodeur/microfactory-lab](https://github.com/kylebrodeur/microfactory-lab)
|
| 244 |
|
| 245 |
## License
|
|
|
|
| 141 |
**Layout:** `app.py` (Space entrypoint) + `test_core.py` at the root; the core library lives in
|
| 142 |
`core/`, runnable helpers in `scripts/` (run via `make <target>` or `uv run python -m scripts.<name>`),
|
| 143 |
and the simulator / learning-loop / ingestion packages in `sim/`, `learn/`, `ingest/`. See
|
| 144 |
+
[`docs/RUNBOOK.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/docs/RUNBOOK.md) for the full run-and-test order of operations.
|
| 145 |
|
| 146 |
+
**HF Space deployment** (source repo): [`docs/reference/DEPLOYMENT.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/docs/reference/DEPLOYMENT.md) + [`docs/RUNBOOK.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/docs/RUNBOOK.md)
|
| 147 |
("Deploy to the Space") cover Space variables, the dual local/Space config, build strategy,
|
| 148 |
the `@spaces.GPU`-on-inference-only pattern, and field-log setup.
|
| 149 |
|
|
|
|
| 239 |
- **Field log (live):** [build-small-hackathon/chief-engineer-field-log](https://huggingface.co/datasets/build-small-hackathon/chief-engineer-field-log)
|
| 240 |
- **Demo video:** [<!-- TODO: add video URL after recording -->]()
|
| 241 |
- **Social post:** [<!-- TODO: add social post URL after publishing -->]()
|
| 242 |
+
- **Stay in the loop:** an email signup lives at the bottom of the live Space. Opt-in only, clear privacy note, no third-party trackers.
|
| 243 |
- **How to use it (guided tour):** [`docs/RUNBOOK.md` §2](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/docs/RUNBOOK.md#2--use-the-tool-the-guided-tour--also-the-judges-tour)
|
| 244 |
+
- **Build story / field notes:** [`docs/writeup/06-FIELD-NOTES.md`](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/docs/writeup/06-FIELD-NOTES.md)
|
| 245 |
+
- **Build paper trail (on this Space):**
|
| 246 |
+
- **Model cards:** [`LoRA v2`](learn/finetune/MODEL_CARD.md) · [`LoRA v3 QAT`](learn/finetune/MODEL_CARD_QAT.md)
|
| 247 |
+
- **Local run / publish:** [`SERVING.md`](learn/finetune/SERVING.md) · [`OLLAMA_PUBLISHING.md`](learn/finetune/OLLAMA_PUBLISHING.md)
|
| 248 |
+
- **Fine-tune pipeline:** [`learn/finetune/README.md`](learn/finetune/README.md) · [`PIPELINE.md`](learn/finetune/PIPELINE.md) · [`RUNBOOK.md`](learn/finetune/RUNBOOK.md) · [`BUDGET.md`](learn/finetune/BUDGET.md)
|
| 249 |
+
- **Session report + activity trace:** [`SESSION_REPORT.md`](learn/finetune/SESSION_REPORT.md) · [`activity.jsonl`](learn/finetune/activity.jsonl)
|
| 250 |
+
- **Calibration:** [`sim/calibration/CALIBRATION-REPORT.md`](sim/calibration/CALIBRATION-REPORT.md) · [`sim/calibration/README.md`](sim/calibration/README.md)
|
| 251 |
+
- **Ingestion + assets:** [`ingest/README.md`](ingest/README.md) · [`assets/screenshots/README.md`](assets/screenshots/README.md) · [`dist/README.md`](dist/README.md) · [`dist/deliberation/README.md`](dist/deliberation/README.md)
|
| 252 |
- **Source:** [kylebrodeur/microfactory-lab](https://github.com/kylebrodeur/microfactory-lab)
|
| 253 |
|
| 254 |
## License
|
app.py
CHANGED
|
@@ -39,6 +39,7 @@ from core import field_log
|
|
| 39 |
from core import inspector
|
| 40 |
from core import llm
|
| 41 |
from core import seed_lessons
|
|
|
|
| 42 |
from core.theme import (
|
| 43 |
THEME, CSS, rule, command_bar, footer_bar, inspector_panel, icon, loader, tab_intro,
|
| 44 |
CLOCK_JS,
|
|
@@ -783,7 +784,7 @@ def run_print(state, iterations, nozzle, bed, fan, retract, progress=gr.Progress
|
|
| 783 |
+ (f"first clean print at **iteration {first}**, now **{traj[-1]:.2f}**."
|
| 784 |
if first else f"still improving — best **{max(traj):.2f}** after {len(traj)} runs.")
|
| 785 |
+ " The Engineer proposed; a separate simulated world reported the outcome; the **Inspector** "
|
| 786 |
-
"graded each run; the policy and ledger learned. *(Simulated — see SIMULATION.md.)*"
|
| 787 |
)
|
| 788 |
policy_html = (f"{before_html}<div style='text-align:center;color:var(--ao-orange);font-size:11px;"
|
| 789 |
f"letter-spacing:2px;'>{icon('arrow')} LEARNED</div>{policy_cell_html(after, key)}")
|
|
@@ -1144,6 +1145,18 @@ def build() -> gr.Blocks:
|
|
| 1144 |
footer = gr.HTML(footer_bar())
|
| 1145 |
privacy_line = gr.HTML(visible=field_log.is_active())
|
| 1146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1147 |
# ── wiring ──
|
| 1148 |
reset_outs = _reset_outputs(ledger_panel, node_cards, review_summary, p_curve, p_policy,
|
| 1149 |
p_log, p_headline, outcome_panel, results_group, real_log_msg,
|
|
@@ -1167,6 +1180,9 @@ def build() -> gr.Blocks:
|
|
| 1167 |
demo.load(None, None, None, js=CLOCK_JS)
|
| 1168 |
demo.load(lambda: field_log.privacy_notice() if field_log.is_active() else "",
|
| 1169 |
None, [privacy_line])
|
|
|
|
|
|
|
|
|
|
| 1170 |
roll_btn.click(randomize_sensors, None, sensor_outs).then(status_footer, foot_in, [footer])
|
| 1171 |
for c in (temp, humidity, bed_position, material):
|
| 1172 |
c.change(sync_readout, [temp, humidity, bed_position], [sensors_readout]).then(
|
|
|
|
| 39 |
from core import inspector
|
| 40 |
from core import llm
|
| 41 |
from core import seed_lessons
|
| 42 |
+
from core import signups
|
| 43 |
from core.theme import (
|
| 44 |
THEME, CSS, rule, command_bar, footer_bar, inspector_panel, icon, loader, tab_intro,
|
| 45 |
CLOCK_JS,
|
|
|
|
| 784 |
+ (f"first clean print at **iteration {first}**, now **{traj[-1]:.2f}**."
|
| 785 |
if first else f"still improving — best **{max(traj):.2f}** after {len(traj)} runs.")
|
| 786 |
+ " The Engineer proposed; a separate simulated world reported the outcome; the **Inspector** "
|
| 787 |
+
"graded each run; the policy and ledger learned. *(Simulated — see [SIMULATION.md](https://github.com/kylebrodeur/microfactory-lab/blob/main/chief-engineer/docs/reference/SIMULATION.md).)*"
|
| 788 |
)
|
| 789 |
policy_html = (f"{before_html}<div style='text-align:center;color:var(--ao-orange);font-size:11px;"
|
| 790 |
f"letter-spacing:2px;'>{icon('arrow')} LEARNED</div>{policy_cell_html(after, key)}")
|
|
|
|
| 1145 |
footer = gr.HTML(footer_bar())
|
| 1146 |
privacy_line = gr.HTML(visible=field_log.is_active())
|
| 1147 |
|
| 1148 |
+
# Email signup for Microfactory updates (opt-in, privacy-first)
|
| 1149 |
+
with gr.Group(elem_classes=["ce-card"]):
|
| 1150 |
+
gr.HTML(rule("STAY IN THE LOOP"))
|
| 1151 |
+
gr.HTML("<div class='ce-sub'>Get Microfactory updates: new nodes, build notes, and when this thing learns to stream g-code straight to the printer. One email. No spam. Unsub any time.</div>")
|
| 1152 |
+
with gr.Row():
|
| 1153 |
+
signup_email = gr.Textbox(placeholder="you@example.com", label="EMAIL", show_label=False,
|
| 1154 |
+
elem_classes=["ce-notes-inline"], scale=3)
|
| 1155 |
+
signup_consent = gr.Checkbox(label="Yes, email me Microfactory updates", scale=2)
|
| 1156 |
+
signup_btn = gr.Button("SUBSCRIBE", elem_classes=["ce-pillbtn"])
|
| 1157 |
+
signup_status = gr.HTML()
|
| 1158 |
+
signup_privacy = gr.HTML(signups.privacy_notice())
|
| 1159 |
+
|
| 1160 |
# ── wiring ──
|
| 1161 |
reset_outs = _reset_outputs(ledger_panel, node_cards, review_summary, p_curve, p_policy,
|
| 1162 |
p_log, p_headline, outcome_panel, results_group, real_log_msg,
|
|
|
|
| 1180 |
demo.load(None, None, None, js=CLOCK_JS)
|
| 1181 |
demo.load(lambda: field_log.privacy_notice() if field_log.is_active() else "",
|
| 1182 |
None, [privacy_line])
|
| 1183 |
+
signup_btn.click(signups.record_signup,
|
| 1184 |
+
[signup_email, signup_consent, gr.State("space" if signups.is_active() else "local")],
|
| 1185 |
+
[signup_status])
|
| 1186 |
roll_btn.click(randomize_sensors, None, sensor_outs).then(status_footer, foot_in, [footer])
|
| 1187 |
for c in (temp, humidity, bed_position, material):
|
| 1188 |
c.change(sync_readout, [temp, humidity, bed_position], [sensors_readout]).then(
|
core/signups.py
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Email signup for Microfactory updates.
|
| 2 |
+
|
| 3 |
+
Lightweight, privacy-first opt-in collection. Stores one JSONL row per signup:
|
| 4 |
+
{ts, email, consent, source, app_version}. Local runs write to data/signups.jsonl.
|
| 5 |
+
Space runs can additionally push to a private HF dataset via CommitScheduler if
|
| 6 |
+
SIGNUPS_DATASET and HF_TOKEN are set. Never writes without explicit consent.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
import json
|
| 12 |
+
import os
|
| 13 |
+
import re
|
| 14 |
+
import threading
|
| 15 |
+
from datetime import datetime, timezone
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
from typing import Any
|
| 18 |
+
|
| 19 |
+
SIGNUPS_FILE = Path(__file__).resolve().parent.parent / "data" / "signups.jsonl"
|
| 20 |
+
SIGNUPS_DATASET = os.environ.get("SIGNUPS_DATASET", "kylebrodeur/microfactory-signups").strip()
|
| 21 |
+
FLUSH_MINUTES = 5
|
| 22 |
+
|
| 23 |
+
_scheduler: Any = None
|
| 24 |
+
_lock = threading.Lock()
|
| 25 |
+
|
| 26 |
+
_EMAIL_RE = re.compile(r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$")
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def _get_scheduler():
|
| 30 |
+
"""Lazy-init CommitScheduler for the private signup dataset."""
|
| 31 |
+
global _scheduler
|
| 32 |
+
token = os.environ.get("HF_TOKEN", "").strip()
|
| 33 |
+
if not token:
|
| 34 |
+
return None
|
| 35 |
+
if not SIGNUPS_DATASET or "/" not in SIGNUPS_DATASET:
|
| 36 |
+
return None
|
| 37 |
+
if _scheduler is None:
|
| 38 |
+
with _lock:
|
| 39 |
+
if _scheduler is None:
|
| 40 |
+
try:
|
| 41 |
+
from huggingface_hub import CommitScheduler
|
| 42 |
+
except ImportError:
|
| 43 |
+
return None
|
| 44 |
+
SIGNUPS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
| 45 |
+
if not SIGNUPS_FILE.exists():
|
| 46 |
+
SIGNUPS_FILE.write_text("", encoding="utf-8")
|
| 47 |
+
_scheduler = CommitScheduler(
|
| 48 |
+
repo_id=SIGNUPS_DATASET,
|
| 49 |
+
repo_type="dataset",
|
| 50 |
+
folder_path=str(SIGNUPS_FILE.parent),
|
| 51 |
+
every=FLUSH_MINUTES,
|
| 52 |
+
token=token,
|
| 53 |
+
allow_patterns=["signups.jsonl"],
|
| 54 |
+
)
|
| 55 |
+
return _scheduler
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def is_active() -> bool:
|
| 59 |
+
"""True if signup syncing to HF is configured and active."""
|
| 60 |
+
return _get_scheduler() is not None
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def validate_email(email: str) -> str | None:
|
| 64 |
+
"""Return normalized email or None if invalid."""
|
| 65 |
+
if not email:
|
| 66 |
+
return None
|
| 67 |
+
email = email.strip().lower()
|
| 68 |
+
if not _EMAIL_RE.match(email):
|
| 69 |
+
return None
|
| 70 |
+
return email
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
def record_signup(email: str, consent: bool, source: str = "local") -> tuple[bool, str]:
|
| 74 |
+
"""Record one signup. Returns (ok, message).
|
| 75 |
+
|
| 76 |
+
Requires explicit consent. Email is validated. Writes locally always;
|
| 77 |
+
pushes to HF only when HF_TOKEN + SIGNUPS_DATASET are present.
|
| 78 |
+
"""
|
| 79 |
+
if not consent:
|
| 80 |
+
return False, "Please check the box to opt in before submitting."
|
| 81 |
+
normalized = validate_email(email)
|
| 82 |
+
if normalized is None:
|
| 83 |
+
return False, "Enter a valid email address."
|
| 84 |
+
|
| 85 |
+
row = {
|
| 86 |
+
"ts": datetime.now(timezone.utc).isoformat(),
|
| 87 |
+
"email": normalized,
|
| 88 |
+
"consent": True,
|
| 89 |
+
"source": source,
|
| 90 |
+
"app_version": os.environ.get("CHIEF_ENGINEER_VERSION", "0.1.0"),
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
try:
|
| 94 |
+
SIGNUPS_FILE.parent.mkdir(parents=True, exist_ok=True)
|
| 95 |
+
with _lock:
|
| 96 |
+
with SIGNUPS_FILE.open("a", encoding="utf-8") as f:
|
| 97 |
+
f.write(json.dumps(row, ensure_ascii=False) + "\n")
|
| 98 |
+
sched = _get_scheduler()
|
| 99 |
+
if sched is not None:
|
| 100 |
+
try:
|
| 101 |
+
sched.trigger()
|
| 102 |
+
except Exception:
|
| 103 |
+
pass
|
| 104 |
+
return True, "You're on the list. Microfactory updates will hit that inbox."
|
| 105 |
+
except Exception as e:
|
| 106 |
+
return False, f"Could not save signup: {e}"
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def privacy_notice() -> str:
|
| 110 |
+
return (
|
| 111 |
+
"<div class='ce-sub' style='font-size:10px;opacity:0.7;margin-top:4px;'>"
|
| 112 |
+
"📬 Microfactory updates: one email, no spam, unsub any time. "
|
| 113 |
+
"We store only your email and this timestamp. No print data, no uploaded files.</div>"
|
| 114 |
+
)
|