chore: update DPO contact information across legal pages

- Replace specific DPO name with generic privacy email across all legal templates
- Update DPA to clarify third-party services vs sub-processors distinction
- Add privacy policy and DPA cross-references in Terms
- Add intellectual property section to Terms
- Improve prompts UI with Yes/No buttons, section headers, and better visual hierarchy

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
James 2026-02-08 05:13:31 -05:00
parent 35e9e2a84b
commit 37b7602027
4 changed files with 131 additions and 29 deletions

View File

@ -119,8 +119,8 @@
<h3>Data Processor.</h3> <h3>Data Processor.</h3>
<p><span class="inou-brand">inou</span>. We store, encrypt, and transmit your data according to your instructions.</p> <p><span class="inou-brand">inou</span>. We store, encrypt, and transmit your data according to your instructions.</p>
<h3>Sub-processors.</h3> <h3>Third-party services.</h3>
<p>Third-party services you explicitly connect to your account, such as AI assistants. We do not use sub-processors for storage or core functionality.</p> <p>You may connect external services to your account, such as AI assistants. These services operate as independent controllers or as processors engaged directly by you — not as our sub-processors. We do not engage sub-processors for storage or core functionality.</p>
</div> </div>
<div class="dpa-card"> <div class="dpa-card">
@ -238,7 +238,7 @@
<div class="dpa-card"> <div class="dpa-card">
<h2>Contact</h2> <h2>Contact</h2>
<p>Data Protection Officer: Johan Jongsma</p> <p>Data Protection Officer: <a href="mailto:privacy@inou.com">privacy@inou.com</a></p>
<p>Questions about data processing: <a href="mailto:privacy@inou.com">privacy@inou.com</a></p> <p>Questions about data processing: <a href="mailto:privacy@inou.com">privacy@inou.com</a></p>
<p>This agreement was last updated on February 8, 2026.</p> <p>This agreement was last updated on February 8, 2026.</p>
</div> </div>

View File

@ -234,7 +234,7 @@
<p>We comply with <strong>FADP</strong> (Swiss data protection), <strong>GDPR</strong> (European data protection), and <strong>HIPAA</strong> (US medical privacy) standards. Regardless of where you live, you get our highest level of protection.</p> <p>We comply with <strong>FADP</strong> (Swiss data protection), <strong>GDPR</strong> (European data protection), and <strong>HIPAA</strong> (US medical privacy) standards. Regardless of where you live, you get our highest level of protection.</p>
<p>We may update this policy. Registered users will be notified by email of material changes. Continued use after changes constitutes acceptance.</p> <p>We may update this policy. Registered users will be notified by email of material changes. Continued use after changes constitutes acceptance.</p>
<p>Regardless of your jurisdiction, you may request access to your data, correction of inaccuracies, or complete deletion of your account. We will respond within 30 days.</p> <p>Regardless of your jurisdiction, you may request access to your data, correction of inaccuracies, or complete deletion of your account. We will respond within 30 days.</p>
<p>Our Data Protection Officer is Johan Jongsma. For all privacy and data protection inquiries, contact <a href="mailto:privacy@inou.com">privacy@inou.com</a>.</p> <p>Data Protection Officer: <a href="mailto:privacy@inou.com">privacy@inou.com</a></p>
<p>This policy was last updated on February 8, 2026.</p> <p>This policy was last updated on February 8, 2026.</p>
</div> </div>

View File

@ -24,6 +24,9 @@
<div class="prompt-list"> <div class="prompt-list">
{{/* 1. FREEFORM CARD - Always visible */}} {{/* 1. FREEFORM CARD - Always visible */}}
<div style="padding: 12px 20px; background: #f8fafc; border-bottom: 1px solid var(--border);">
<span style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b;">Add New Entry</span>
</div>
{{range .DuePrompts}} {{range .DuePrompts}}
{{if .IsFreeform}} {{if .IsFreeform}}
<div class="prompt-item prompt-freeform" data-prompt-id="{{.ID}}"> <div class="prompt-item prompt-freeform" data-prompt-id="{{.ID}}">
@ -49,6 +52,9 @@
{{end}} {{end}}
{{/* 2. PENDING CARDS - Due but not filled yet */}} {{/* 2. PENDING CARDS - Due but not filled yet */}}
<div style="padding: 12px 20px; background: #f8fafc; border-bottom: 1px solid var(--border); margin-top: 24px;">
<span style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b;">Due Now</span>
</div>
{{range .DuePrompts}} {{range .DuePrompts}}
{{if not .IsFreeform}} {{if not .IsFreeform}}
{{if not .HasResponse}} {{if not .HasResponse}}
@ -81,11 +87,11 @@
<button type="button" class="btn-save" onclick="saveItem(this.closest('.prompt-item'))">Save</button> <button type="button" class="btn-save" onclick="saveItem(this.closest('.prompt-item'))">Save</button>
</div> </div>
{{else if eq .Type "checkbox"}} {{else if eq .Type "checkbox"}}
<label class="prompt-checkbox"> <div class="prompt-buttons">
<input type="checkbox" name="field_{{.Key}}" value="1"> <button type="button" class="prompt-btn" data-field="{{.Key}}" data-value="yes" onclick="selectYesNo(this)">Yes</button>
<span class="prompt-checkbox-box"></span> <button type="button" class="prompt-btn" data-field="{{.Key}}" data-value="no" onclick="selectYesNo(this)">No</button>
<span class="prompt-checkbox-label">{{if .Label}}{{.Label}}{{else}}Yes{{end}}</span> <input type="hidden" name="field_{{.Key}}" value="">
</label> </div>
{{end}} {{end}}
{{end}} {{end}}
{{end}} {{end}}
@ -98,6 +104,11 @@
{{end}} {{end}}
{{/* 3. FILLED CARDS - Entries from today */}} {{/* 3. FILLED CARDS - Entries from today */}}
{{if .Entries}}
<div style="padding: 12px 20px; background: #f8fafc; border-bottom: 1px solid var(--border); margin-top: 24px;">
<span style="font-size: 0.75rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: #64748b;">Completed Today</span>
</div>
{{end}}
{{range .Entries}} {{range .Entries}}
<div class="prompt-item prompt-filled" data-entry-id="{{.ID}}"> <div class="prompt-item prompt-filled" data-entry-id="{{.ID}}">
<a href="#" class="prompt-dismiss" onclick="deleteEntry('{{.ID}}'); return false;" title="Delete">✕</a> <a href="#" class="prompt-dismiss" onclick="deleteEntry('{{.ID}}'); return false;" title="Delete">✕</a>
@ -430,6 +441,35 @@
font-size: 0.9rem; font-size: 0.9rem;
color: var(--text-muted); color: var(--text-muted);
} }
.prompt-buttons {
display: flex;
gap: 8px;
}
.prompt-btn {
padding: 10px 24px;
border: 2px solid #ddd;
border-radius: 6px;
background: white;
font-size: 0.95rem;
font-weight: 500;
color: var(--text);
cursor: pointer;
transition: all 0.15s ease;
min-width: 80px;
}
.prompt-btn:hover {
border-color: var(--accent);
background: #fff8f6;
}
.prompt-btn.selected {
background: var(--accent);
border-color: var(--accent);
color: white;
}
.prompt-btn:focus {
outline: none;
box-shadow: 0 0 0 2px rgba(198, 93, 7, 0.2);
}
.prompt-scale { .prompt-scale {
display: flex; display: flex;
gap: 8px; gap: 8px;
@ -521,24 +561,43 @@
font-style: italic; font-style: italic;
} }
.prompt-freeform { .prompt-freeform {
background: #fefce8; background: #fff;
border-left: 4px solid #eab308; border-left: 3px solid #94a3b8;
} }
.prompt-pending { .prompt-pending {
background: #fff; background: #fff;
border-left: 4px solid var(--accent); border-left: 3px solid var(--accent);
} }
.prompt-filled { .prompt-filled {
background: #f0fdf4; background: #fafafa;
border-left: 4px solid #16a34a; border-left: 3px solid #64748b;
} }
.prompt-preview { .prompt-preview {
opacity: 0.6; opacity: 0.5;
} }
.prompt-preview input[disabled] { .prompt-preview input[disabled] {
cursor: not-allowed; cursor: not-allowed;
background: #f9fafb; background: #f9fafb;
} }
.prompt-question {
font-size: 1rem;
font-weight: 600;
color: #1e293b;
}
.prompt-category {
display: inline-block;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #64748b;
margin-right: 12px;
}
.prompt-due {
font-size: 0.8rem;
color: #64748b;
font-weight: 500;
}
.entry-readonly .prompt-field-row { .entry-readonly .prompt-field-row {
display: flex; display: flex;
align-items: center; align-items: center;
@ -653,6 +712,10 @@ document.querySelectorAll('.prompt-item').forEach(item => {
cb.addEventListener('change', () => saveItem(item)); cb.addEventListener('change', () => saveItem(item));
}); });
item.querySelectorAll('.prompt-btn').forEach(btn => {
btn.addEventListener('click', () => selectYesNo(btn));
});
item.querySelectorAll('.prompt-scale-btn').forEach(btn => { item.querySelectorAll('.prompt-scale-btn').forEach(btn => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
const field = btn.dataset.field; const field = btn.dataset.field;
@ -723,6 +786,25 @@ function editPrompt(btn) {
item.querySelector('.prompt-form input:not([type=hidden]), .prompt-form textarea')?.focus(); item.querySelector('.prompt-form input:not([type=hidden]), .prompt-form textarea')?.focus();
} }
function selectYesNo(btn) {
const item = btn.closest('.prompt-item');
const field = btn.dataset.field;
const value = btn.dataset.value;
// Deselect siblings
btn.parentElement.querySelectorAll('.prompt-btn').forEach(b => b.classList.remove('selected'));
// Select this one
btn.classList.add('selected');
// Update hidden input
const hidden = item.querySelector('input[name="field_' + field + '"]');
if (hidden) hidden.value = value;
// Auto-save
saveItem(item);
}
function addNewPromptCard(prompt) { function addNewPromptCard(prompt) {
const container = document.querySelector('.prompts-section'); const container = document.querySelector('.prompts-section');
if (!container) return; if (!container) return;
@ -737,10 +819,11 @@ function addNewPromptCard(prompt) {
prompt.input_config.fields.forEach(field => { prompt.input_config.fields.forEach(field => {
if (field.type === 'checkbox') { if (field.type === 'checkbox') {
fieldsHtml += ` fieldsHtml += `
<label class="prompt-checkbox"> <div class="prompt-buttons">
<input type="checkbox" name="field_${field.key}" value="1"> <button type="button" class="prompt-btn" data-field="${field.key}" data-value="yes" onclick="selectYesNo(this)">Yes</button>
<span class="prompt-checkbox-box"></span> <button type="button" class="prompt-btn" data-field="${field.key}" data-value="no" onclick="selectYesNo(this)">No</button>
</label>`; <input type="hidden" name="field_${field.key}" value="">
</div>`;
} else if (field.type === 'number') { } else if (field.type === 'number') {
const step = field.step || (field.datatype === 'float' ? '0.1' : '1'); const step = field.step || (field.datatype === 'float' ? '0.1' : '1');
const min = field.min !== undefined ? `min="${field.min}"` : ''; const min = field.min !== undefined ? `min="${field.min}"` : '';
@ -820,7 +903,7 @@ function addNewPromptCard(prompt) {
async function saveItem(item) { async function saveItem(item) {
const form = item.querySelector('.prompt-form'); const form = item.querySelector('.prompt-form');
const promptId = form.dataset.promptId; const promptId = form.dataset.promptId;
const inputs = form.querySelectorAll('input:not([type=hidden]), textarea'); const inputs = form.querySelectorAll('input, textarea');
const response = {}; const response = {};
let hasValue = false; let hasValue = false;
@ -828,6 +911,8 @@ async function saveItem(item) {
let responseRaw = ''; let responseRaw = '';
inputs.forEach(input => { inputs.forEach(input => {
if (!input.name) return; // Skip inputs without names
const key = input.name.replace('field_', '').replace('response_raw', 'raw'); const key = input.name.replace('field_', '').replace('response_raw', 'raw');
if (input.type === 'checkbox') { if (input.type === 'checkbox') {
if (input.checked) { if (input.checked) {
@ -836,7 +921,13 @@ async function saveItem(item) {
displayValue = '✓'; displayValue = '✓';
responseRaw = 'yes'; responseRaw = 'yes';
} }
} else if (input.value) { } else if (input.type === 'hidden' && input.value && input.name.startsWith('field_')) {
// Handle hidden inputs from Yes/No buttons
response[key] = input.value;
hasValue = true;
displayValue = input.value === 'yes' ? '✓' : '✗';
responseRaw = input.value;
} else if (input.value && input.type !== 'hidden') {
response[key] = input.value; response[key] = input.value;
hasValue = true; hasValue = true;
displayValue += (displayValue ? ' / ' : '') + input.value; displayValue += (displayValue ? ' / ' : '') + input.value;
@ -974,11 +1065,11 @@ function addPendingCard(prompt) {
</div>`; </div>`;
} else if (field.type === 'checkbox') { } else if (field.type === 'checkbox') {
fieldsHtml = ` fieldsHtml = `
<label class="prompt-checkbox"> <div class="prompt-buttons">
<input type="checkbox" name="field_${field.key}" value="1"> <button type="button" class="prompt-btn" data-field="${field.key}" data-value="yes" onclick="selectYesNo(this)">Yes</button>
<span class="prompt-checkbox-box"></span> <button type="button" class="prompt-btn" data-field="${field.key}" data-value="no" onclick="selectYesNo(this)">No</button>
<span class="prompt-checkbox-label">Yes</span> <input type="hidden" name="field_${field.key}" value="">
</label>`; </div>`;
} }
} }

View File

@ -121,12 +121,17 @@
<h2>Our responsibilities</h2> <h2>Our responsibilities</h2>
<h3>What we provide.</h3> <h3>What we provide.</h3>
<p>We will store your data securely using FIPS 140-3 validated encryption, make it available to you through the platform, and transmit it to third-party services you explicitly authorize. We will notify you of material changes to these terms or our privacy practices.</p> <p>We will store your data securely using FIPS 140-3 validated encryption, make it available to you through the platform, and transmit it to third-party services you explicitly authorize. For details on how we protect your data, see our <a href="/security">security page</a>. We will notify you of material changes to these terms or our privacy practices.</p>
<h3>What we don't guarantee.</h3> <h3>What we don't guarantee.</h3>
<p>We aim for continuous availability but cannot guarantee it. The service may be temporarily unavailable for maintenance, updates, or circumstances beyond our control. We are not liable for decisions you or others make based on data viewed through the platform.</p> <p>We aim for continuous availability but cannot guarantee it. The service may be temporarily unavailable for maintenance, updates, or circumstances beyond our control. We are not liable for decisions you or others make based on data viewed through the platform.</p>
</div> </div>
<div class="terms-card">
<h2>Privacy and data processing</h2>
<p>Your use of <span class="inou-brand">inou</span> is subject to our <a href="/privacy-policy">Privacy Policy</a> and <a href="/legal/dpa">Data Processing Agreement</a>, which describe what data we collect, how we use it, and your rights. By using the service, you acknowledge and agree to those terms.</p>
</div>
<div class="terms-card"> <div class="terms-card">
<h2>Acceptable use</h2> <h2>Acceptable use</h2>
@ -137,6 +142,12 @@
<p>We may suspend or terminate your account. In cases of illegal activity, we will cooperate with law enforcement.</p> <p>We may suspend or terminate your account. In cases of illegal activity, we will cooperate with law enforcement.</p>
</div> </div>
<div class="terms-card">
<h2>Intellectual property</h2>
<p>Your data is yours. You retain all rights to the health data, files, and information you upload. We claim no ownership over your content.</p>
<p>The <span class="inou-brand">inou</span> platform — its software, design, branding, and documentation — is our property. These terms grant you a personal, non-exclusive, non-transferable license to use the service. They do not grant you rights to our code, design, or brand.</p>
</div>
<div class="terms-card"> <div class="terms-card">
<h2>Payment</h2> <h2>Payment</h2>
@ -175,7 +186,7 @@
<div class="terms-card"> <div class="terms-card">
<h2>Changes</h2> <h2>Changes</h2>
<p>We may update these terms. Registered users will be notified by email of material changes. Continued use after changes constitutes acceptance.</p> <p>We may update these terms. Registered users will be notified by email of material changes. Continued use after changes constitutes acceptance.</p>
<p>Questions: <a href="mailto:privacy@inou.com">privacy@inou.com</a></p> <p>Data Protection Officer: <a href="mailto:privacy@inou.com">privacy@inou.com</a></p>
<p>Last updated: February 8, 2026.</p> <p>Last updated: February 8, 2026.</p>
</div> </div>