diff --git a/apps/home/workspace.html b/apps/home/workspace.html index 0aba03081..a2626ee8a 100644 --- a/apps/home/workspace.html +++ b/apps/home/workspace.html @@ -953,7 +953,6 @@

welcome to your home page

this is where your day comes together — narrative summaries, calendar events, tasks, routines, and the people in your network. as solstone captures and processes your day, sections will appear here automatically.

-

your recordings are kept for 7 days by default, then cleaned up after processing. you can change this anytime in settings.

check system health →
{% endif %} diff --git a/convey/root.py b/convey/root.py index 9c5e5138f..e7221796e 100644 --- a/convey/root.py +++ b/convey/root.py @@ -238,6 +238,19 @@ def init_finalize() -> Any: if gemini_key: config.setdefault("env", {})["GOOGLE_API_KEY"] = gemini_key config.setdefault("setup", {})["completed_at"] = now_ms() + retention_mode = data.get("retention_mode", "days") + retention_days = data.get("retention_days", 7) + if isinstance(retention_days, str): + try: + retention_days = int(retention_days) + except (ValueError, TypeError): + retention_days = 7 + config.setdefault("retention", {}).update( + { + "raw_media": retention_mode, + "raw_media_days": retention_days if retention_mode == "days" else None, + } + ) config_path = Path(get_journal()) / "config" / "journal.json" config_path.parent.mkdir(parents=True, exist_ok=True) diff --git a/convey/templates/init.html b/convey/templates/init.html index b57bb37b4..ba60cc618 100644 --- a/convey/templates/init.html +++ b/convey/templates/init.html @@ -59,6 +59,25 @@ .observer-empty { font-size: 0.85rem; color: #888; } .observer-empty a { color: #b06a1a; } .footer-note { font-size: 0.88rem; color: #767676; margin-top: 1.5rem; text-align: center; } + .settings-mode-btn { + padding: 0.5em 1em; border: 1px solid #ccc; border-radius: 4px; + background: #fff; color: #555; cursor: pointer; font-size: 0.85rem; + } + .settings-mode-btn:hover { border-color: #999; } + .settings-mode-btn:active:not(.active) { background: #f5f5f5; } + .settings-mode-btn.active { background: #E8923A; color: #fff; border-color: #E8923A; } + .settings-preset-btn { + padding: 0.35em 0.75em; border: 1px solid #ccc; border-radius: 4px; + background: #fff; color: #555; cursor: pointer; font-size: 0.85rem; + } + .settings-preset-btn:hover { border-color: #999; } + .settings-preset-btn.active { border-color: #E8923A; color: #E8923A; } + .retention-days-row { display: flex; align-items: center; gap: 0.5rem; margin-top: 0.5rem; } + .retention-days-row input[type="number"] { + width: 5em; padding: 0.35em 0.5em; border: 2px solid #ddd; border-radius: 8px; + font-size: 0.85rem; box-sizing: border-box; + } + .retention-days-row input[type="number"]:focus { outline: none; border-color: #E8923A; } @@ -131,8 +150,27 @@ +
+

5. your recordings

+

choose how long to keep raw audio and screen recordings. you can change this anytime in settings.

+
+
+ + + +
+
+ + + + + days +
+
+
+
-

5. get started

+

6. get started

solstone works best with a coding agent. open a terminal, navigate to your solstone directory, and ask it to help you set up:

cd {{ repo_path }} && claude "help me set up solstone"

cd {{ repo_path }} && codex "help me set up solstone"

@@ -180,7 +218,7 @@ let detectedTimezone = ''; function unlockSections() { - ['section-identity', 'section-provider', 'section-observers', 'section-finalize'].forEach(id => { + ['section-identity', 'section-provider', 'section-observers', 'section-recordings', 'section-finalize'].forEach(id => { const section = document.getElementById(id); section.classList.remove('disabled'); section.querySelectorAll('input[disabled]').forEach(inp => inp.disabled = false); @@ -250,6 +288,27 @@ } catch (err) {} } + function setRetentionMode(mode) { + document.querySelectorAll('#section-recordings .settings-mode-btn').forEach(btn => { + btn.classList.toggle('active', btn.dataset.mode === mode); + }); + document.getElementById('retention-days-field').style.display = mode === 'days' ? 'flex' : 'none'; + } + + function setRetentionDays(days) { + document.querySelectorAll('#section-recordings .settings-preset-btn').forEach(btn => { + btn.classList.toggle('active', parseInt(btn.dataset.days) === days); + }); + document.getElementById('retention-days-input').value = days; + } + + function setRetentionDaysCustom(value) { + const days = parseInt(value) || 7; + document.querySelectorAll('#section-recordings .settings-preset-btn').forEach(btn => { + btn.classList.toggle('active', parseInt(btn.dataset.days) === days); + }); + } + async function finalize() { try { const resp = await fetch('/init/finalize', { @@ -259,7 +318,9 @@ name: document.getElementById('name').value.trim(), preferred: document.getElementById('preferred').value.trim(), timezone: detectedTimezone, - gemini_key: document.getElementById('gemini-key').value.trim() + gemini_key: document.getElementById('gemini-key').value.trim(), + retention_mode: (document.querySelector('#section-recordings .settings-mode-btn.active') || {}).dataset?.mode || 'days', + retention_days: parseInt(document.getElementById('retention-days-input').value) || 7 }) }); const data = await resp.json(); diff --git a/tests/test_init.py b/tests/test_init.py index 3a2be42ae..a00b1cf33 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -247,6 +247,34 @@ class TestInitFinalize: resp = fresh_client.get("/init") assert resp.status_code == 302 + def test_finalize_with_retention_config(self, fresh_client, journal_copy): + """Finalize with explicit retention config writes correct values.""" + resp = fresh_client.post( + "/init/finalize", + json={ + "password": "securepass123", + "retention_mode": "processed", + "retention_days": 30, + }, + content_type="application/json", + ) + assert resp.status_code == 200 + config = _read_config(journal_copy) + assert config["retention"]["raw_media"] == "processed" + assert config["retention"]["raw_media_days"] is None + + def test_finalize_default_retention(self, fresh_client, journal_copy): + """Finalize without retention fields writes default (days/7).""" + resp = fresh_client.post( + "/init/finalize", + json={"password": "securepass123"}, + content_type="application/json", + ) + assert resp.status_code == 200 + config = _read_config(journal_copy) + assert config["retention"]["raw_media"] == "days" + assert config["retention"]["raw_media_days"] == 7 + class TestRemovedEndpoints: """Verify old endpoints no longer exist."""