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.
+
+
+ keep N days
+ keep forever
+ delete after processing
+
+
+ 7
+ 30
+ 90
+
+ 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."""