Compare commits

..

2 Commits

Author SHA1 Message Date
Alexander Whitestone
b4129bc873 fix: add chat-header safety plan access (#38)
All checks were successful
Sanity Checks / sanity-test (pull_request) Successful in 5s
Smoke Test / smoke (pull_request) Successful in 12s
2026-04-17 01:14:28 -04:00
Alexander Whitestone
e3bb6b86ee test: add chat-header safety plan regression (#38) 2026-04-17 00:59:21 -04:00
3 changed files with 85 additions and 134 deletions

124
GENOME.md
View File

@@ -1,124 +0,0 @@
# GENOME.md — the-door
> Codebase analysis generated 2026-04-13. Crisis intervention web app — a door that's always open.
## Project Overview
the-door is a single-URL crisis intervention web app. A man at 3am can talk to Timmy. No login. No signup. No tracking. Just a door that's always open.
**Mission**: Stand between a broken man and a machine that would tell him to die.
48 files. Static HTML frontend (<25KB, works on 3G). Python crisis detection backend. Safety-critical — a broken deployment could prevent someone from reaching the 988 Lifeline.
## Architecture
```
Browser → nginx (SSL) → index.html → /api/* proxy → Hermes Gateway
crisis/detect.py
988 Lifeline overlay
```
## Entry Points
- **index.html** — The entire frontend. One file. <25KB. Works on 3G.
- **system-prompt.txt** — Crisis-aware system prompt for the AI.
- **deploy/deploy.sh** — Deployment script for VPS.
- **deploy/playbook.yml** — Ansible playbook for deployment.
- **crisis/detect.py** — Core crisis detection module (canonical).
- **crisis_detector.py** — Legacy class API wrapper around detect.py.
- **crisis_responder.py** — Response formatting for crisis levels.
## Data Flow
```
User message → browser
index.html → client-side crisis keyword scan
/api/chat → Hermes Gateway
system-prompt.txt → injected into AI system prompt
crisis/detect.py → 5-tier classification (NONE/LOW/MEDIUM/HIGH/CRITICAL)
crisis/response.py → appropriate response with 988 Lifeline info
Response → browser → crisis overlay if HIGH/CRITICAL
```
## Key Abstractions
### Crisis Detection (crisis/detect.py)
Canonical detection module. Regex-based keyword matching across 4 tiers:
- CRITICAL: immediate self-harm risk (single match triggers)
- HIGH: strong despair signals (single match triggers)
- MEDIUM: distress signals (requires 2+ indicators)
- LOW: emotional difficulty (single match)
Design principles:
- Never computes the value of a human life
- Never suggests death is a solution
- Always errs on side of higher risk
### Crisis Profiles (crisis/profiles.py)
Compassion profiles that shape AI response tone based on crisis level.
### Session Tracker (crisis/session_tracker.py)
Tracks crisis interactions across sessions. Persistent state for ongoing support.
### Gateway (crisis/gateway.py)
HTTP gateway for crisis detection API. Endpoints for scanning text and getting responses.
### Offline Fallback (crisis-offline.html, sw.js)
Service worker caches crisis resources. When network is down, users still see 988 Lifeline info and crisis resources.
## File Types
| Type | Count | Purpose |
|------|-------|---------|
| .py | 16 | Crisis detection, response, tests |
| .html | 4 | Frontend, offline fallback, tests |
| .yml | 2 | CI workflows |
| .sh | 2 | Health check, service restart |
| .md | 5 | Documentation, safety audits |
## Test Coverage
### Existing Tests
- test_crisis_overlay_focus_trap.py — Accessibility: focus trap in crisis overlay
- test_dying_detection_deprecation.py — Legacy API deprecation
- test_false_positive_fixes.py — Crisis detection false positive resistance
- test_service_worker_offline.py — Offline fallback verification
- test_session_tracker.py — Session tracking persistence
- crisis/test_rescue.py — Rescue flow testing
- crisis/tests.py — Core crisis detection tests
### Coverage Gaps
- No integration tests for full browser → API → response → overlay flow
- No tests for system-prompt.txt injection into AI system prompt
- No load tests (what happens at 1000 concurrent crisis users?)
- No tests for deploy.sh idempotency
### Critical paths that need tests:
1. **Full crisis flow**: user message → detection → 988 overlay → response
2. **Offline fallback**: network down → service worker → cached crisis resources
3. **Deploy safety**: deploy.sh doesn't break running service
## Security Considerations
- **SAFETY-CRITICAL**: the-door serves users in crisis. Broken deployment could prevent someone from reaching 988 Lifeline.
- **PR safety**: the-door PRs NEVER auto-merge. Requires-human label on all PRs. (fleet-ops#183)
- **No authentication by design**: no login, no signup, no tracking. Privacy is a safety feature.
- **Rate limiting**: deploy/rate-limit.conf prevents abuse while allowing crisis access.
- **Offline resilience**: service worker ensures crisis resources available even without network.
- **System prompt is safety boundary**: system-prompt.txt defines the AI's crisis behavior. Changes require human review.
## Design Decisions
- **Single HTML file**: no build step, no framework, no dependencies. Works on 3G. Loads instantly.
- **Client-side detection first**: browser scans for crisis keywords before sending to server. Instant response for critical cases.
- **Server-side detection second**: crisis/detect.py provides deeper analysis with tiered classification.
- **Offline-first for crisis**: service worker caches crisis resources. Network failure doesn't block access to help.
- **No tracking**: privacy protects vulnerable users. No analytics, no cookies, no login.

View File

@@ -241,6 +241,48 @@ html, body {
opacity: 0.5;
}
/* ===== CHAT HEADER ===== */
#chat-header {
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 10px 12px;
border-bottom: 1px solid #21262d;
background: #11161d;
}
.chat-header-title {
font-size: 0.85rem;
color: #8b949e;
font-weight: 600;
letter-spacing: 0.02em;
}
#chat-safety-plan-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 12px;
min-height: 36px;
border: 1px solid #30363d;
border-radius: 999px;
background: transparent;
color: #c9d1d9;
font-size: 0.8rem;
font-weight: 600;
cursor: pointer;
}
#chat-safety-plan-btn:hover,
#chat-safety-plan-btn:focus {
border-color: #58a6ff;
background: rgba(88, 166, 255, 0.12);
outline: 2px solid #58a6ff;
outline-offset: 2px;
}
/* ===== CHAT AREA ===== */
#chat-area {
flex: 1;
@@ -649,6 +691,14 @@ html, body {
</div>
</div>
<div id="chat-header">
<div class="chat-header-title" aria-hidden="true">Conversation</div>
<button id="chat-safety-plan-btn" type="button" aria-label="Open My Safety Plan from chat header">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/></svg>
My Safety Plan
</button>
</div>
<!-- Chat messages -->
<div id="chat-area" role="log" aria-label="Chat messages" aria-live="polite" tabindex="0">
<!-- Messages inserted here -->
@@ -680,7 +730,7 @@ html, body {
<!-- Footer -->
<footer id="footer">
<a href="/about.html" aria-label="About The Door">about</a>
<a href="/about" aria-label="About The Door">about</a>
<button id="safety-plan-btn" aria-label="Open My Safety Plan">my safety plan</button>
<button id="clear-chat-btn" aria-label="Clear chat history">clear chat</button>
</footer>
@@ -814,6 +864,7 @@ Sovereignty and service always.`;
// Safety Plan Elements
var safetyPlanBtn = document.getElementById('safety-plan-btn');
var chatSafetyPlanBtn = document.getElementById('chat-safety-plan-btn');
var crisisSafetyPlanBtn = document.getElementById('crisis-safety-plan-btn');
var safetyPlanModal = document.getElementById('safety-plan-modal');
var closeSafetyPlan = document.getElementById('close-safety-plan');
@@ -1285,19 +1336,25 @@ Sovereignty and service always.`;
_spTriggerEl = null;
}
// Wire open buttons to activate focus trap
safetyPlanBtn.addEventListener('click', function() {
function openSafetyPlan(triggerEl) {
loadSafetyPlan();
safetyPlanModal.classList.add('active');
_activateSafetyPlanFocusTrap(safetyPlanBtn);
_activateSafetyPlanFocusTrap(triggerEl || document.activeElement);
}
// Wire open buttons to activate focus trap
safetyPlanBtn.addEventListener('click', function() {
openSafetyPlan(safetyPlanBtn);
});
chatSafetyPlanBtn.addEventListener('click', function() {
openSafetyPlan(chatSafetyPlanBtn);
});
// Crisis panel safety plan button (if crisis panel is visible)
if (crisisSafetyPlanBtn) {
crisisSafetyPlanBtn.addEventListener('click', function() {
loadSafetyPlan();
safetyPlanModal.classList.add('active');
_activateSafetyPlanFocusTrap(crisisSafetyPlanBtn);
openSafetyPlan(crisisSafetyPlanBtn);
});
}
@@ -1444,9 +1501,7 @@ Sovereignty and service always.`;
// Check for URL params (e.g., ?safetyplan=true for PWA shortcut)
var urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('safetyplan') === 'true') {
loadSafetyPlan();
safetyPlanModal.classList.add('active');
_activateSafetyPlanFocusTrap(safetyPlanBtn);
openSafetyPlan(chatSafetyPlanBtn || safetyPlanBtn);
// Clean up URL
window.history.replaceState({}, document.title, window.location.pathname);
}

View File

@@ -0,0 +1,20 @@
from pathlib import Path
INDEX = Path("index.html")
def test_chat_header_has_persistent_safety_plan_button():
html = INDEX.read_text()
assert 'id="chat-header"' in html
assert 'id="chat-safety-plan-btn"' in html
assert 'aria-label="Open My Safety Plan from chat header"' in html
assert 'My Safety Plan' in html
def test_chat_header_button_opens_existing_safety_plan_modal():
html = INDEX.read_text()
assert "var chatSafetyPlanBtn = document.getElementById('chat-safety-plan-btn');" in html
assert "chatSafetyPlanBtn.addEventListener('click'" in html
assert "function openSafetyPlan(triggerEl)" in html
assert "safetyPlanModal.classList.add('active');" in html
assert "openSafetyPlan(chatSafetyPlanBtn);" in html