Contact email: a report's rev is its own change; another address starts fresh

- from_report applies its change and reads the id and rev together, so a
  removal made while that change is sending is newer than the report; the
  report's redaction window now starts before the address is saved.
- A report with a different address replaces the saved one with follow-up
  questions only: update notices aren't carried over to an address nobody
  agreed them for, and the form says so before sending.
- Settings refreshes after every report send, whatever the box shows by then.
- privacy.md: a report with follow-up ticked also saves and sends the address.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
saphidandClaude Opus 5.5 committed 2026-10-01 21:25:22 +10:00
1 parent 08d75e3ffb
commit 989962aecc
5 files changed
+72 -17

No files matched your search

+22 -10
View File
@@ -81,16 +81,19 @@ def flag(body, key):
def from_report(email):
"""Follow-up questions agreed to with a problem report: the address becomes the contact
email with that choice ticked (the update choice stays as it was), so it shows in Settings
and is removed the same way. Returns (contact id, rev) for the report to carry: a later
change from this copy has a higher rev, and the newest such change decides whether the
report's follow-up permission still stands, whatever the clocks say."""
s = load()
if s['email'].lower() != email.lower() or not s['followup']:
save({'email': s['email'] if s['email'].lower() == email.lower() else email,
'updates': s['updates'], 'followup': True})
email with that choice ticked, so it shows in Settings and is removed the same way. Update
notices stay on only for the same address: a different one replaces the old address with
follow-up questions only (the report form says so before sending). Returns (contact id,
rev) for the report to carry, read together with the change itself: a later change from
this copy has a higher rev, and the newest such change decides whether the report's
follow-up permission still stands, whatever the clocks say."""
with _lock:
s = load()
return s['id'], s['rev']
same = s['email'].lower() == email.lower()
changed, cid, rev = _apply({'email': s['email'] if same else email,
'updates': s['updates'] and same, 'followup': True})
_deliver(changed)
return cid, rev
def state():
@@ -177,6 +180,12 @@ def redact_removed(event, started):
def save(body):
"""Set, change or remove the address and the two choices. An address needs at least one
choice ticked; an empty address (or neither ticked) removes it and withdraws both."""
_deliver(_apply(body)[0])
return state()
def _apply(body):
"""save()'s change, kept here and waiting to send. Returns (changed, contact id, rev)."""
email = str(body.get('email') or '').strip()
updates, followup = flag(body, 'updates'), flag(body, 'followup')
if email and not valid_email(email):
@@ -203,9 +212,12 @@ def save(body):
_forget_locally(old)
except OSError:
pass
return changed, s['id'], s['rev']
def _deliver(changed):
if changed and not _send_pending(block=False):
_wake.set() # offline, or a send under way that will take this change with it
return state()
def prompt(body):
+1 -1
View File
@@ -116,6 +116,7 @@ def send(body):
contact = str(body.get('contact') or '').strip() if followup else ''
if followup and not frame_contact.valid_email(contact):
raise ValueError('add your email address for follow-up questions, or untick that box')
started = time.time() # a removal from now on (even while saving the address) is redacted from the log
# It becomes the contact email in Settings, where it's changed or removed like any other.
contact_id, contact_rev = frame_contact.from_report(contact) if followup else ('', 0)
ref = uuid.uuid4().hex[:8].upper()
@@ -127,7 +128,6 @@ def send(body):
# Its own random id: a report can carry contact details, so it isn't linked to this copy's analytics.
event = {'event': 'problem_report', 'distinct_id': str(uuid.uuid4()), 'uuid': str(uuid.uuid4()),
'timestamp': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()), 'properties': props}
started = time.time()
try:
frame_telemetry.post([event], timeout=30)
except frame_telemetry.SendError as e:
+13 -1
View File
@@ -1264,6 +1264,7 @@
<label class="popt"><input type="checkbox" id="bugFollowup"><b>The maintainer may contact me with follow-up questions</b>
<span class="sub">Optional. Your email address goes with this report only when this is ticked, and is kept as your contact email in Privacy &amp; updates, where you can remove it.</span></label>
<label class="field">Your email address<input type="email" id="bugContact" maxlength="254" placeholder="you@example.com" disabled></label>
<p class="hint" id="bugReplaces" role="status" hidden></p>
<label class="popt"><input type="checkbox" id="bugDiag" checked><b>Include diagnostics</b>
<span class="sub">Frame Control's version, your OS and the Frame's SteamOS build.</span></label>
<label class="popt"><input type="checkbox" id="bugLogs"><b>Also include recent activity and the server log</b>
@@ -4092,6 +4093,7 @@ function openBugReport() {
// A standing yes to follow-up questions (Privacy & updates) fills this in; it can be unticked.
$("bugFollowup").checked = contact.followup; $("bugContact").value = contact.followup ? contact.email : "";
$("bugContact").disabled = !contact.followup; $("bugContact").required = contact.followup;
bugReplaces();
$("bugDlg").showModal();
loadBugPreview();
}
@@ -4102,7 +4104,17 @@ $("bugFollowup").onchange = () => {
const on = $("bugFollowup").checked;
$("bugContact").disabled = !on; $("bugContact").required = on;
if (on && !$("bugContact").value) { $("bugContact").value = contact.email; $("bugContact").focus(); }
bugReplaces();
};
// Sending with another address replaces the saved one (ui/frame_contact.py from_report): say so first.
function bugReplaces() {
const email = $("bugContact").value.trim(), old = contact.email;
const replaces = $("bugFollowup").checked && email && old && email.toLowerCase() !== old.toLowerCase();
$("bugReplaces").hidden = !replaces;
$("bugReplaces").textContent = !replaces ? "" : `Sending replaces ${old} as your contact email${contact.updates
? ", and update notices stop until you turn them on again in Privacy & updates" : ""}.`;
}
$("bugContact").oninput = bugReplaces;
$("bugCancel").onclick = () => $("bugDlg").close();
$("bugCopy").onclick = async () => {
const { title, body } = bugReportText();
@@ -4125,7 +4137,7 @@ $("bugForm").onsubmit = async e => {
$("bugMsg").textContent = `Couldn't send it: ${err.message}. Try again later, or use Copy report.`;
$("bugSend").disabled = false;
}
if ($("bugFollowup").checked) api("/api/contact").then(renderContact).catch(() => {});
api("/api/contact").then(renderContact).catch(() => {}); // the report may have saved the address
};
if (window.frameApp && window.frameApp.onReportProblem) window.frameApp.onReportProblem(openBugReport);