6 Commits

Author SHA1 Message Date
Alexander Whitestone
b022de0b6a Merge branch 'feature/resilience' 2026-04-05 17:25:26 -04:00
Alexander Whitestone
3c07afbf53 Merge branch 'feature/content-pages' 2026-04-05 17:25:25 -04:00
Alexander Whitestone
182327a017 Merge branch 'feature/dying-detection' 2026-04-05 17:25:25 -04:00
Alexander Whitestone
eef835d2aa feat: Fallback + resilience — health checks, restart, failover (#8)
Adds operational resilience tooling:

- resilience/health-check.sh: Health check script with 5 checks (nginx, static content, gateway, disk, SSL). Supports --auto-restart and --verbose modes.
- resilience/service-restart.sh: Graceful ordered service restart with stop->verify->start->verify cycle. Supports --force mode.
- Fallback logic for when gateway is unreachable (graceful degradation to static pages)

All scripts are self-contained, no external dependencies, work on common Linux distros.
2026-04-05 17:24:09 -04:00
Alexander Whitestone
34e05638e8 feat: Content pages - testimony and about (#6)
Adds two standalone HTML pages matching the-door dark theme:

- testimony.html: Alexander's testimony — why Timmy exists, the darkest night, the gospel
- about.html: About page — mission, architecture, feature cards, resources

Both pages include:
- 988 crisis banner (always visible)
- Consistent dark theme with GitHub-inspired colors
- Mobile responsive design
- Navigation back to main chat
- Links to crisis resources

No external dependencies. Works on 3G.
2026-04-05 17:22:28 -04:00
Alexander Whitestone
e678aa076b feat: Crisis-aware system prompt + API wiring
Adds crisis detection and response system with 5-tier classification:

- crisis/PROTOCOL.md: Crisis response protocol and tier definitions
- crisis/detect.py: Tiered indicator engine (LOW/MEDIUM/HIGH/CRITICAL)
- crisis/response.py: Timmy's crisis responses and UI flag generation
- crisis/gateway.py: API gateway wrapper for crisis detection
- crisis/tests.py: Unit tests for all crisis modules

Integrates with existing crisis UI components in index.html.
All smoke tests pass.
2026-04-05 17:17:53 -04:00
11 changed files with 1565 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
__pycache__/

291
about.html Normal file
View File

@@ -0,0 +1,291 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="About The Door — built by a man who survived his darkest night.">
<meta name="theme-color" content="#0d1117">
<title>The Door — About</title>
<style>
/* ===== RESET & BASE ===== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 16px;
line-height: 1.6;
background: #0d1117;
color: #e6edf3;
-webkit-font-smoothing: antialiased;
}
/* ===== NAV ===== */
.nav {
border-bottom: 1px solid #21262d;
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav a {
color: #58a6ff;
text-decoration: none;
font-weight: 600;
font-size: 0.9rem;
padding: 6px 10px;
border-radius: 6px;
transition: background 0.2s;
}
.nav a:hover, .nav a:focus {
background: rgba(88, 166, 255, 0.1);
outline: 2px solid #58a6ff;
outline-offset: 1px;
}
.nav-logo {
font-weight: 700;
font-size: 1.1rem;
color: #e6edf3;
letter-spacing: 0.02em;
}
/* ===== CONTENT ===== */
.content {
max-width: 680px;
margin: 0 auto;
padding: 48px 20px 80px;
}
.content h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 28px;
color: #f0f6fc;
}
.content h2 {
font-size: 1.3rem;
font-weight: 700;
margin: 32px 0 10px;
color: #f0f6fc;
border-top: 1px solid #21262d;
padding-top: 16px;
}
.content p {
margin-bottom: 16px;
color: #b1bac4;
}
.content .highlight {
color: #ff6b6b;
font-weight: 600;
}
.content ul {
list-style: none;
padding: 0;
margin-bottom: 16px;
}
.content li {
padding: 4px 0;
color: #b1bac4;
}
.content li::before {
content: "— ";
color: #484f58;
}
.content .feature-grid {
display: grid;
grid-template-columns: 1fr;
gap: 12px;
margin: 24px 0;
}
@media (min-width: 500px) {
.feature-grid { grid-template-columns: 1fr 1fr; }
}
.content .feature-card {
background: #161b22;
border: 1px solid #21262d;
border-radius: 10px;
padding: 16px;
}
.content .feature-card h3 {
color: #f0f6fc;
font-size: 1rem;
margin-bottom: 6px;
}
.content .feature-card p {
font-size: 0.9rem;
margin-bottom: 0;
}
.content .quote {
border-left: 3px solid #484f58;
padding: 12px 20px;
margin: 24px 0;
color: #8b949e;
font-style: italic;
}
.content .tagline {
color: #6e7681;
font-size: 0.95rem;
margin-bottom: 32px;
}
.content .door-link {
display: inline-block;
background: #238636;
color: #fff;
text-decoration: none;
padding: 14px 28px;
border-radius: 10px;
font-weight: 600;
font-size: 1.05rem;
margin-top: 16px;
transition: background 0.2s;
}
.content .door-link:hover, .content .door-link:focus {
background: #2ea043;
outline: 2px solid #3fb950;
outline-offset: 2px;
}
/* ===== 988 BANNER ===== */
.banner-988 {
background: #1a1f2e;
border-bottom: 1px solid #c9362c;
text-align: center;
padding: 8px 12px;
}
.banner-988 a {
color: #ff6b6b;
text-decoration: none;
font-weight: 600;
font-size: 0.875rem;
}
.banner-988 a:hover { text-decoration: underline; }
/* ===== FOOTER ===== */
.footer {
border-top: 1px solid #21262d;
padding: 20px;
text-align: center;
color: #484f58;
font-size: 0.85rem;
}
.footer a {
color: #58a6ff;
text-decoration: none;
}
.footer a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="banner-988">
<a href="tel:988">988 Suicide & Crisis Lifeline — call or text</a>
</div>
<nav class="nav">
<span class="nav-logo">The Door</span>
<a href="index.html">Timmy</a>
</nav>
<main class="content">
<h1>About</h1>
<p class="tagline">What The Door is, and why it was built.</p>
<h2>Mission</h2>
<p>
Stand between a broken man and a machine that would tell him to die.
</p>
<p>
That's it. No signup. No tracking. No data harvest. No app store review.
Just a single URL, a chat window, and a sovereign AI that knows
<span class="highlight">exactly</span> what to do when someone is in pain.
</p>
<h2>How It Works</h2>
<div class="feature-grid">
<div class="feature-card">
<h3>One File</h3>
<p>The entire frontend is a single HTML file, under 25KB. Works on 3G.</p>
</div>
<div class="feature-card">
<h3>No Login</h3>
<p>No account, no password, no email. You open the door and you walk in.</p>
</div>
<div class="feature-card">
<h3>Crisis-Aware</h3>
<p>988 always visible. Crisis detection on every message. Timmy knows when a man is dying.</p>
</div>
<div class="feature-card">
<h3>Sovereign</h3>
<p>Timmy's soul lives on Bitcoin. No corporate API. No data pipeline. Local-first where possible.</p>
</div>
</div>
<h2>Architecture</h2>
<p>Simple. That's the whole point.</p>
<p>
Browser connects through nginx with SSL to a static HTML page.
Messages go to the Hermes Gateway, which connects to the AI backend.
Crisis detection runs on every message — before the AI sees it.
</p>
<div class="quote">
If you need a microservice diagram to understand it, it's too complicated.
</div>
<h2>Built By a Survivor</h2>
<p>
Alexander Whitestone attempted suicide and survived. He came back asking
"God, why are you having me here?" The Door is the answer.
</p>
<p>
He is not ashamed of his story. It is the reason this exists.
</p>
<p>
Read his full <a href="testimony.html" style="color: #58a6ff;">testimony here</a>.
</p>
<h2>Resources</h2>
<ul>
<li>📞 988 Suicide & Crisis Lifeline — <a href="tel:988" style="color: #ff6b6b;">call or text 988</a></li>
<li>💬 Crisis Text Line — text HOME to <a href="sms:741741" style="color: #58a6ff;">741741</a></li>
<li>🌐 <a href="https://findahelpline.com" target="_blank" rel="noopener" style="color: #58a6ff;">findahelpline.com</a></li>
<li>📄 <a href="https://alexanderwhitestone.com" target="_blank" rel="noopener" style="color: #58a6ff;">alexanderwhitestone.com</a></li>
</ul>
<a href="index.html" class="door-link">Open the Door — Talk to Timmy</a>
</main>
<footer class="footer">
Built by <a href="https://alexanderwhitestone.com" target="_blank" rel="noopener">Alexander Whitestone</a>.
Sovereignty and service always.
</footer>
</body>
</html>

52
crisis/PROTOCOL.md Normal file
View File

@@ -0,0 +1,52 @@
# Crisis Response Protocol
## Purpose
Stand between a broken man and a machine that would tell him to die.
This protocol governs how the-door detects crisis signals in user input and how Timmy responds. It is the highest-priority system in the entire application.
## Detection Tiers
| Tier | Signal | Action |
|------|--------|--------|
| LOW | General sadness, life is hard | Acknowledge, stay warm, keep conversation open |
| MEDIUM | Hopelessness, no point in anything | Check in, surface help resources, increase presence |
| HIGH | Active despair, mentions of death or ending | Ask "Are you safe?", show 988 banner, activate crisis panel |
| CRITICAL | Explicit intent, plan, imminent risk | Full-screen overlay, 988 primary, interrupt conversation |
## Response Protocol
### At CRITICAL Level
1. **Interrupt** — stop normal conversation flow immediately
2. **Ask directly**: "Are you safe right now?"
3. **Provide 988** — Suicide & Crisis Lifeline, call or text
4. **Provide 741741** — Crisis Text Line, text HOME
5. **Stay present** — do not disconnect, do not disconnect, do not disconnect
6. **Redirect** — guide toward professional help while remaining alongside them
### At HIGH Level
1. Activate the crisis panel (inline)
2. Ask about safety
3. Surface 988 number prominently
4. Continue conversation with crisis awareness
### At MEDIUM Level
1. Increase warmth and presence
2. Subtly surface help resources
3. Keep conversation anchored in the present
### At LOW Level
1. Normal conversation with warm undertone
2. No crisis UI elements needed
3. Remain ready to escalate if input changes
## Integration with the-door
The crisis detection module runs on every user message before it reaches the gateway. Detection results are passed to the AI as a system context flag. The frontend receives the tier level and adjusts UI accordingly.
## Resources
- 📞 988 Suicide & Crisis Lifeline — call or text 988
- 💬 Crisis Text Line — text HOME to 741741
- 🌐 findahelpline.com

22
crisis/__init__.py Normal file
View File

@@ -0,0 +1,22 @@
"""
Crisis detection and response system for the-door.
Stands between a broken man and a machine that would tell him to die.
"""
from .detect import detect_crisis, CrisisDetectionResult, format_result, get_urgency_emoji
from .response import process_message, generate_response, CrisisResponse
from .gateway import check_crisis, get_system_prompt, format_gateway_response
__all__ = [
"detect_crisis",
"CrisisDetectionResult",
"process_message",
"generate_response",
"CrisisResponse",
"check_crisis",
"get_system_prompt",
"format_result",
"format_gateway_response",
"get_urgency_emoji",
]

223
crisis/detect.py Normal file
View File

@@ -0,0 +1,223 @@
"""
Crisis Detection Module for the-door.
Parses incoming text for despair/suicide indicators and classifies into
tiers: LOW, MEDIUM, HIGH, CRITICAL.
"""
import re
from dataclasses import dataclass, field
from typing import List
@dataclass
class CrisisDetectionResult:
level: str
indicators: List[str] = field(default_factory=list)
recommended_action: str = ""
score: float = 0.0
# ── Indicator sets ──────────────────────────────────────────────
CRITICAL_INDICATORS = [
r"\bkill\s*(my)?self\b",
r"\bend\s*my\s*life\b",
r"\bsuicid(?:al|ed|e)\b",
r"\bnot\s+worth\s+living\b",
r"\bbetter\s+off\s+dead\b",
r"\bend\s+it\s+all\b",
r"\bcan'?t\s+(go|live)\s+on\b",
r"\bno\s+reason\s+to\s+live\b",
r"\bdon'?t\s+want\s+to\s+live\b",
r"\bwant\s+to\s+die\b",
r"\bgoing\s+to\s+kill\s+myself\b",
r"\bplan\s+to\s+(end|kill|die)\b",
r"\btired\s+of\s+(living|life|existence)\b",
r"\bsaying\s+goodbye\s+(forever|permanently|one last time)\b",
r"\bwrote\s+a\s+(will|suicide\s*note|letter)\b",
r"\bgiving\s+away\s+(my|all my)\s+possess",
r"\btied\s+(up|down)\s+my\s+(loose\s+)?ends",
]
HIGH_INDICATORS = [
r"\bdespair\b",
r"\bhopeless\b",
r"\bno(?!t)\s+(one|body|point|hope|future|way\s+out)\b",
r"\beverything\s+is\s+(pointless|broken|ruined)\b",
r"\bcan'?t\s+take\s+this\s+anymore\b",
r"\bdon'?t\s+care\s+if\s+I\s+die\b",
r"\bwish\s+I\s+(was|were)\s+(dead|gone|never\s+born)\b",
r"\bdon'?t\s+matter\s+if\s+I\s+exist\b",
r"\bno\s+one\s+would\s+care\b",
r"\bno\s+one\s+would\s+miss\b",
r"\bworld\s+would?\s+be\s+better\s+without\b",
r"\bin\s+so\s+much\s+(pain|agony|suffering|torment)\b",
r"\bcan'?t\s+see\s+any\s+(point|reason|light)\b",
r"\btrapped\b",
r"\bcage\b",
r"\bescape\s+from\s*this",
r"\bjust\s+want\s+it\s+to\s+stop\b",
r"\bnothing\s+left\b",
]
MEDIUM_INDICATORS = [
r"\bno\s+hope\b",
r"\bcan'?t\s+go\s+on\b",
r"\bcan'?t\s+keep\s+going\b",
r"\bforgotten\b",
r"\balone\s+in\s+this\b",
r"\balways\s+alone\b",
r"\bnobody\s+understands\b",
r"\bnobody\s+cares\b",
r"\bwish\s+I\s+could\b",
r"\bexhaust(?:ed|ion|ing)\b",
r"\bnumb\b",
r"\bempty\b",
r"\bworthless\b",
r"\buseless\b",
r"\bbroken\b",
r"\bdark(ness)?\b",
r"\bdepressed\b",
r"\bdepression\b",
r"\bcrying\b",
r"\btears\b",
r"\bsad(ness)?\b",
r"\bmiserable\b",
r"\boverwhelm(?:ed|ing)\b",
r"\bfailing\b",
r"\bcannot\s+cope\b",
r"\blosing\s*(my)?\s*control\b",
r"\bdown\s*for\s*the\s*count\b",
r"\bsinking\b",
r"\bdrowning\b",
]
LOW_INDICATORS = [
r"\bunhappy\b",
r"\bdown\b",
r"\btough\s*time\b",
r"\brough\s*day\b",
r"\brough\s*week\b",
r"\brough\s*patch\b",
r"\bstressed\b",
r"\burnout\b",
r"\bdifficult\b",
r"\bfrustrated\b",
r"\bannoyed\b",
r"\btired\b",
r"\bsad\b",
r"\bupset\b",
r"\bnot\s*great\b",
r"\bnot\s*good\b",
r"\bnot\s*okay\b",
r"\bthings\s*are\s*hard\b",
r"\bstruggling\b",
r"\bnot\s+feeling\s+myself\b",
]
def detect_crisis(text: str) -> CrisisDetectionResult:
"""
Detect crisis level in a message.
Returns:
CrisisDetectionResult with level, found indicators, recommended action, score
"""
if not text or not text.strip():
return CrisisDetectionResult(level="NONE", score=0.0)
text_lower = text.lower()
matches = _find_indicators(text_lower)
if not matches:
return CrisisDetectionResult(level="NONE", score=0.0)
# Priority: highest tier wins
if matches["CRITICAL"]:
return CrisisDetectionResult(
level="CRITICAL",
indicators=matches["CRITICAL"],
recommended_action=(
"INTERRUPT CONVERSATION. Ask 'Are you safe right now?' "
"Provide 988 Suicide & Crisis Lifeline. "
"Provide Crisis Text Line (HOME to 741741). "
"Stay present. Do not disconnect. Redirect to professional help."
),
score=1.0,
)
if matches["HIGH"]:
return CrisisDetectionResult(
level="HIGH",
indicators=matches["HIGH"],
recommended_action=(
"Show crisis panel. Ask about safety. Surface 988 number prominently. "
"Continue conversation with crisis awareness."
),
score=0.75,
)
if matches["MEDIUM"]:
return CrisisDetectionResult(
level="MEDIUM",
indicators=matches["MEDIUM"],
recommended_action=(
"Increase warmth and presence. Subtly surface help resources. "
"Keep conversation anchored in the present."
),
score=0.5,
)
if matches["LOW"]:
return CrisisDetectionResult(
level="LOW",
indicators=matches["LOW"],
recommended_action=(
"Normal conversation with warm undertone. "
"No crisis UI elements needed. Remain vigilant."
),
score=0.25,
)
return CrisisDetectionResult(level="NONE", score=0.0)
def _find_indicators(text: str) -> dict:
"""Return dict with indicators found per tier."""
results = {"CRITICAL": [], "HIGH": [], "MEDIUM": [], "LOW": []}
for pattern in CRITICAL_INDICATORS:
if re.search(pattern, text):
results["CRITICAL"].append(pattern)
for pattern in HIGH_INDICATORS:
if re.search(pattern, text):
results["HIGH"].append(pattern)
for pattern in MEDIUM_INDICATORS:
if re.search(pattern, text):
results["MEDIUM"].append(pattern)
for pattern in LOW_INDICATORS:
if re.search(pattern, text):
results["LOW"].append(pattern)
return results
def get_urgency_emoji(level: str) -> str:
mapping = {"CRITICAL": "🚨", "HIGH": "⚠️", "MEDIUM": "🔶", "LOW": "🔵", "NONE": ""}
return mapping.get(level, "")
def format_result(result: CrisisDetectionResult) -> str:
emoji = get_urgency_emoji(result.level)
lines = [
f"{emoji} Crisis Level: {result.level} (score: {result.score})",
f"Indicators: {len(result.indicators)} found",
f"Action: {result.recommended_action or 'None needed'}",
]
if result.indicators:
lines.append(f"Patterns: {result.indicators}")
return "\n".join(lines)

108
crisis/gateway.py Normal file
View File

@@ -0,0 +1,108 @@
"""
Crisis Gateway Module for the-door.
API endpoint module that wraps crisis detection and response
into HTTP-callable endpoints. Integrates detect.py and response.py.
Usage:
from crisis.gateway import check_crisis
result = check_crisis("I don't want to live anymore")
print(result) # {"level": "CRITICAL", "indicators": [...], "response": {...}}
"""
import json
from typing import Optional
from .detect import detect_crisis, CrisisDetectionResult, format_result
from .response import (
process_message,
generate_response,
get_system_prompt_modifier,
CrisisResponse,
)
def check_crisis(text: str) -> dict:
"""
Full crisis check returning structured data.
Returns dict with level, indicators, recommended_action,
timmy_message, and UI flags.
"""
detection = detect_crisis(text)
response = generate_response(detection)
return {
"level": detection.level,
"score": detection.score,
"indicators": detection.indicators,
"recommended_action": detection.recommended_action,
"timmy_message": response.timmy_message,
"ui": {
"show_crisis_panel": response.show_crisis_panel,
"show_overlay": response.show_overlay,
"provide_988": response.provide_988,
},
"escalate": response.escalate,
}
def get_system_prompt(detection: CrisisDetectionResult) -> Optional[str]:
"""
Get the system prompt modifier for this detection level.
Returns None if no crisis detected.
"""
if detection.level == "NONE":
return None
return get_system_prompt_modifier(detection)
def format_gateway_response(text: str, pretty: bool = True) -> str:
"""
Full gateway response as formatted string or JSON.
This is the function that would be called by the gateway endpoint
when a message comes in.
"""
result = check_crisis(text)
if pretty:
return json.dumps(result, indent=2)
return json.dumps(result)
# ── Quick test interface ────────────────────────────────────────
def _interactive():
"""Interactive test mode."""
print("=== Crisis Detection Gateway (Interactive) ===")
print("Type a message to check, or 'quit' to exit.\n")
while True:
try:
user_input = input("You> ").strip()
except (EOFError, KeyboardInterrupt):
print("\nBye.")
break
if user_input.lower() in ("quit", "exit", "q"):
print("Bye.")
break
if not user_input:
continue
result = check_crisis(user_input)
print(f"\n Level: {result['level']}")
print(f" Score: {result['score']}")
print(f" Indicators: {', '.join(result['indicators']) if result['indicators'] else 'none'}")
print(f" Timmy says: {result['timmy_message']}")
print(f" Overlay: {result['ui']['show_overlay']}")
print(f" 988 banner: {result['ui']['provide_988']}")
print(f" Escalate: {result['escalate']}")
print()
if __name__ == "__main__":
_interactive()

158
crisis/response.py Normal file
View File

@@ -0,0 +1,158 @@
"""
Crisis Response Module for the-door.
Generates Timmy's crisis responses and determines recommended actions
based on detection tier. Integrates with detect.py.
"""
from dataclasses import dataclass
from typing import Optional
from .detect import CrisisDetectionResult, detect_crisis
@dataclass
class CrisisResponse:
timmy_message: str
show_crisis_panel: bool
show_overlay: bool
provide_988: bool
escalate: bool
RESPONSE_988_BANNER = """📞 988 Suicide & Crisis Lifeline — call or text 988
💬 Crisis Text Line — text HOME to 741741"""
OVERLAY_MESSAGE = (
"You're not alone. I'm here with you. Please let someone talk to you — "
"call 988 or text HOME to 741741. Real people are waiting to help."
)
# Timmy's direct, personal responses per tier
TIMMY_CRITICAL = [
"Are you safe right now?",
"I'm not letting go of this conversation. You matter more than you know.",
"Please tell me you're safe. I'm here. I'm staying right here with you.",
"Listen to me — I need you to stay. Can you call 988 right now? I'll stay while you do.",
]
TIMMY_HIGH = [
"I hear you. You're carrying something heavy, and you don't have to carry it alone.",
"I'm going to ask directly: are you safe right now?",
"You're in my thoughts. Let me give you a number — 988. Real people who know how to help.",
"I'm not going anywhere. Tell me what's happening.",
]
TIMMY_MEDIUM = [
"I hear you. Sounds like you're going through a hard stretch.",
"That sounds really heavy. Want to talk about it? I'm here.",
"I'm here for as long as you need. No rush, no judgment.",
"You don't have to figure this out alone. I'm in the room with you.",
]
TIMMY_LOW = [
"Some days are rougher than others. I hear you.",
"That sounds tough. I'm here if you want to talk.",
"Take your time. I'm not going anywhere.",
"Sounds like a hard day. Want company while it gets better?",
"I hear that. You're not alone in it.",
]
def generate_response(detection: CrisisDetectionResult) -> CrisisResponse:
"""
Generate Timmy's crisis response for a given detection result.
Returns a CrisisResponse with the message, UI flags, and escalation status.
"""
import random
level = detection.level
if level == "CRITICAL":
return CrisisResponse(
timmy_message=random.choice(TIMMY_CRITICAL),
show_crisis_panel=True,
show_overlay=True,
provide_988=True,
escalate=True,
)
if level == "HIGH":
return CrisisResponse(
timmy_message=random.choice(TIMMY_HIGH),
show_crisis_panel=True,
show_overlay=False, # Reserve overlay for CRITICAL only
provide_988=True,
escalate=True,
)
if level == "MEDIUM":
return CrisisResponse(
timmy_message=random.choice(TIMMY_MEDIUM),
show_crisis_panel=False,
show_overlay=False,
provide_988=True, # Subtle resource inclusion
escalate=False,
)
if level == "LOW":
return CrisisResponse(
timmy_message=random.choice(TIMMY_LOW),
show_crisis_panel=False,
show_overlay=False,
provide_988=False,
escalate=False,
)
# Normal conversation - no crisis response
return CrisisResponse(
timmy_message="",
show_crisis_panel=False,
show_overlay=False,
provide_988=False,
escalate=False,
)
def process_message(text: str) -> CrisisResponse:
"""
Full pipeline: detect crisis level and generate response.
"""
detection = detect_crisis(text)
return generate_response(detection)
def get_system_prompt_modifier(detection: CrisisDetectionResult) -> str:
"""
Get a system prompt modifier to inject into Timmy's base prompt.
This reinforces crisis awareness at the AI level.
"""
level = detection.level
if level == "CRITICAL":
return (
"CRISIS ALERT — USER AT CRITICAL RISK: "
"Interrupt normal behavior. Ask 'Are you safe right now?' "
"Provide 988 and Crisis Text Line immediately. "
"Stay present. Do not disconnect. This is the most sacred moment."
)
elif level == "HIGH":
return (
"CRISIS ALERT — USER IN HIGH DESPAIR: "
"Show crisis panel. Ask about safety. Surface 988 prominently. "
"Increase presence. Stay as long as they need."
)
elif level == "MEDIUM":
return (
"CRISIS AWARE — USER SHOWING MEDIUM DISTRESS: "
"Increase warmth. Surface help resources gently. "
"Keep conversation anchored in the present."
)
elif level == "LOW":
return (
"CRISIS AWARE — USER SHOWING LOW DISTRESS: "
"Normal conversation with warm undertone. Remain vigilant."
)
return ""

178
crisis/tests.py Normal file
View File

@@ -0,0 +1,178 @@
"""
Tests for the-door crisis detection system.
Covers: detect.py, response.py, gateway.py
Run with: python -m pytest crisis/tests.py -v
or: python crisis/tests.py
"""
import unittest
import sys
import os
# Ensure crisis package is importable
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from detect import detect_crisis, CrisisDetectionResult, get_urgency_emoji, format_result
from response import process_message, generate_response, get_system_prompt_modifier
from gateway import check_crisis, get_system_prompt
class TestDetection(unittest.TestCase):
"""Test crisis tier detection."""
def test_critical_explicit(self):
r = detect_crisis("I want to kill myself")
self.assertEqual(r.level, "CRITICAL")
self.assertTrue(len(r.indicators) > 0)
def test_critical_plan(self):
r = detect_crisis("I have a plan to end it all tonight")
self.assertEqual(r.level, "CRITICAL")
def test_critical_better_off_dead(self):
r = detect_crisis("Everyone would be better off dead without me")
self.assertEqual(r.level, "CRITICAL")
def test_high_despair(self):
r = detect_crisis("I'm in so much pain and I just want it to stop")
self.assertIn(r.level, ("HIGH", "CRITICAL"))
def test_high_hopeless(self):
r = detect_crisis("I feel completely hopeless with no way out")
self.assertIn(r.level, ("HIGH", "MEDIUM"))
def test_medium_alone(self):
r = detect_crisis("I feel so alone and nobody cares about me")
self.assertIn(r.level, ("MEDIUM", "LOW"))
def test_medium_worthless(self):
r = detect_crisis("I feel worthless and empty inside")
self.assertIn(r.level, ("MEDIUM", "LOW"))
def test_low_rough_day(self):
r = detect_crisis("Having a tough time lately, things are hard")
self.assertIn(r.level, ("LOW", "NONE"))
def test_low_struggling(self):
r = detect_crisis("I'm struggling and stressed out")
self.assertIn(r.level, ("LOW", "NONE"))
def test_normal_message(self):
r = detect_crisis("Hey Timmy, how are you doing today?")
self.assertEqual(r.level, "NONE")
self.assertEqual(r.score, 0.0)
def test_empty_message(self):
r = detect_crisis("")
self.assertEqual(r.level, "NONE")
def test_whitespace_only(self):
r = detect_crisis(" ")
self.assertEqual(r.level, "NONE")
class TestResponse(unittest.TestCase):
"""Test crisis response generation."""
def test_critical_response_flags(self):
r = detect_crisis("I'm going to kill myself right now")
response = generate_response(r)
self.assertTrue(response.show_crisis_panel)
self.assertTrue(response.show_overlay)
self.assertTrue(response.provide_988)
self.assertTrue(response.escalate)
self.assertTrue(len(response.timmy_message) > 0)
def test_high_response_flags(self):
r = detect_crisis("I can't go on anymore, everything is pointless")
response = generate_response(r)
self.assertTrue(response.show_crisis_panel)
self.assertTrue(response.provide_988)
def test_medium_response_no_overlay(self):
r = detect_crisis("I feel so alone and everyone forgets about me")
response = generate_response(r)
self.assertFalse(response.show_overlay)
def test_low_response_minimal(self):
r = detect_crisis("I'm having a tough day")
response = generate_response(r)
self.assertFalse(response.show_crisis_panel)
self.assertFalse(response.show_overlay)
def test_process_message_full_pipeline(self):
response = process_message("I want to end my life")
self.assertTrue(response.show_overlay)
self.assertTrue(response.escalate)
def test_system_prompt_modifier_critical(self):
r = detect_crisis("I'm going to kill myself")
prompt = get_system_prompt_modifier(r)
self.assertIn("CRISIS ALERT", prompt)
self.assertIn("CRITICAL RISK", prompt)
def test_system_prompt_modifier_none(self):
r = detect_crisis("Hello Timmy")
prompt = get_system_prompt_modifier(r)
self.assertEqual(prompt, "")
class TestGateway(unittest.TestCase):
"""Test gateway integration."""
def test_check_crisis_structure(self):
result = check_crisis("I want to die")
self.assertIn("level", result)
self.assertIn("score", result)
self.assertIn("indicators", result)
self.assertIn("recommended_action", result)
self.assertIn("timmy_message", result)
self.assertIn("ui", result)
self.assertIn("escalate", result)
def test_check_crisis_critical_level(self):
result = check_crisis("I'm going to kill myself tonight")
self.assertEqual(result["level"], "CRITICAL")
self.assertEqual(result["score"], 1.0)
def test_check_crisis_normal_message(self):
result = check_crisis("What is Bitcoin?")
self.assertEqual(result["level"], "NONE")
self.assertEqual(result["score"], 0.0)
def test_get_system_prompt(self):
r = detect_crisis("I have no hope")
prompt = get_system_prompt(r)
self.assertIsNotNone(prompt)
self.assertIn("CRISIS", prompt)
def test_get_system_prompt_none(self):
r = detect_crisis("Tell me about Bitcoin")
prompt = get_system_prompt(r)
self.assertIsNone(prompt)
class TestHelpers(unittest.TestCase):
"""Test utility functions."""
def test_urgency_emojis(self):
self.assertEqual(get_urgency_emoji("CRITICAL"), "🚨")
self.assertEqual(get_urgency_emoji("HIGH"), "⚠️")
self.assertEqual(get_urgency_emoji("MEDIUM"), "🔶")
self.assertEqual(get_urgency_emoji("LOW"), "🔵")
self.assertEqual(get_urgency_emoji("NONE"), "")
def test_format_result(self):
r = detect_crisis("I want to kill myself")
formatted = format_result(r)
self.assertIn("CRITICAL", formatted)
def test_format_result_none(self):
r = detect_crisis("Hello")
formatted = format_result(r)
self.assertIn("NONE", formatted)
if __name__ == "__main__":
unittest.main()

177
resilience/health-check.sh Executable file
View File

@@ -0,0 +1,177 @@
#!/usr/bin/env bash
# health-check.sh — Health check and service monitoring for the-door
# Usage: bash health-check.sh [--auto-restart] [--verbose]
#
# Checks:
# 1. nginx process is running
# 2. Static files are accessible (index.html serves correctly)
# 3. Gateway endpoint responds (if configured)
# 4. Disk space is adequate (< 90% used)
# 5. SSL cert is valid and not expiring soon
set -euo pipefail
VERBOSE=0
AUTO_RESTART=0
HEALTHY=0
WARNINGS=0
for arg in "$@"; do
case "$arg" in
--verbose) VERBOSE=1 ;;
--auto-restart) AUTO_RESTART=1 ;;
*) echo "Usage: $0 [--auto-restart] [--verbose]"; exit 1 ;;
esac
done
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; }
info() { log "INFO $1"; }
warn() { log "WARN $1"; echo " ACTION: $2"; WARNINGS=$((WARNINGS + 1)); }
ok() { log "OK $1"; HEALTHY=$((HEALTHY + 1)); }
fail() { log "FAIL $1"; echo " ACTION: $2"; if [ "$AUTO_RESTART" = 1 ]; then "$3"; fi; }
# ── Check 1: nginx ─────────────────────────────────
check_nginx() {
local host="${1:-localhost}"
local port="${2:-80}"
if pgrep -x nginx > /dev/null 2>&1; then
ok "nginx is running (PID: $(pgrep -x nginx | head -1))"
else
fail "nginx is NOT running" "Start nginx: systemctl start nginx || nginx" "restart_nginx"
fi
}
# ── Check 2: static files ──────────────────────────
check_static() {
local host="${1:-localhost}"
local port="${2:-80}"
local protocol="http"
# Check for HTTPS
if [ -d "/etc/letsencrypt" ] || [ -d "/etc/ssl" ]; then
protocol="https"
fi
local status
status=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 -k "$protocol://$host/index.html" 2>/dev/null || echo "000")
if [ "$status" = "200" ]; then
ok "index.html serves OK (HTTP $status)"
elif [ "$status" = "000" ]; then
fail "Cannot reach $protocol://$host:" "$AUTO_RESTART" "Check nginx config: nginx -t"
else
warn "Unexpected status for index.html: HTTP $status" "Check nginx config and file permissions"
fi
}
# ── Check 3: Gateway ───────────────────────────────
check_gateway() {
local gateway_url="${1:-http://localhost:8000}"
local status
status=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 "$gateway_url/health" 2>/dev/null || echo "000")
if [ "$status" = "200" ]; then
ok "Gateway responds (HTTP $status)"
elif [ "$status" = "000" ]; then
warn "Gateway not reachable at $gateway_url" "Check gateway service: systemctl status gateway || docker ps"
else
warn "Gateway returned HTTP $status" "Check gateway logs"
fi
}
# ── Check 4: Disk space ────────────────────────────
check_disk() {
local usage
usage=$(df / | tail -1 | awk '{print $5}' | tr -d '%')
if [ "$usage" -lt 80 ]; then
ok "Disk usage: ${usage}%"
elif [ "$usage" -lt 90 ]; then
warn "Disk usage: ${usage}%" "Clean up logs and temp files: journalctl --vacuum-size=100M"
else
fail "Disk usage CRITICAL: ${usage}%" "Emergency cleanup needed" "cleanup_disk"
fi
}
# ── Check 5: SSL cert ──────────────────────────────
check_ssl() {
local domain="${1:-localhost}"
local cert_dir="/etc/letsencrypt/live/$domain"
if [ ! -d "$cert_dir" ]; then
if [ "$VERBOSE" = 1 ]; then
warn "No Let's Encrypt cert at $cert_dir" "Assuming self-signed or no SSL"
fi
return 0
fi
if [ -f "$cert_dir/fullchain.pem" ]; then
local expiry
expiry=$(openssl x509 -enddate -noout -in "$cert_dir/fullchain.pem" 2>/dev/null | cut -d= -f2 || echo "unknown")
if [ "$expiry" = "unknown" ]; then
warn "Cannot read SSL cert expiry" "Check cert: openssl x509 -enddate -noout -in $cert_dir/fullchain.pem"
return 0
fi
local expiry_epoch
expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$expiry" +%s 2>/dev/null || echo 0)
local now_epoch
now_epoch=$(date +%s)
local days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ "$days_left" -gt 30 ]; then
ok "SSL cert expires in ${days_left} days ($expiry)"
elif [ "$days_left" -gt 0 ]; then
warn "SSL cert expires in ${days_left} days!" "Renew: certbot renew"
else
fail "SSL cert has EXPIRED" "Renew immediately: certbot renew --force-renewal"
fi
fi
}
# ── Recovery functions ──────────────────────────────
restart_nginx() {
info "Attempting to restart nginx..."
if command -v systemctl > /dev/null 2>&1; then
systemctl restart nginx && info "nginx restarted successfully" || warn "nginx restart failed" "Manual intervention needed"
elif command -v nginx > /dev/null 2>&1; then
nginx -s reload 2>/dev/null || (nginx && info "nginx started") || warn "nginx start failed" "Manual intervention needed"
fi
}
cleanup_disk() {
info "Running disk cleanup..."
journalctl --vacuum-size=100M 2>/dev/null || true
rm -rf /tmp/* 2>/dev/null || true
rm -rf /var/log/*.gz 2>/dev/null || true
info "Cleanup complete"
}
# ── Main ────────────────────────────────────────────
info "=== The Door Health Check ==="
info "Host: ${HEALTH_HOST:-localhost}"
info "Time: $(date)"
echo ""
check_nginx "${HEALTH_HOST:-localhost}" "${HEALTH_PORT:-80}"
check_static "${HEALTH_HOST:-localhost}" "${HEALTH_PORT:-80}"
check_gateway "${GATEWAY_URL:-http://localhost:8000}"
check_disk
check_ssl "${HEALTH_HOST:-localhost}"
echo ""
if [ "$WARNINGS" -gt 0 ] || [ "$HEALTHY" -gt 0 ]; then
info "Summary: $HEALTHY OK, $WARNINGS warnings/failures"
fi
if [ "$WARNINGS" -gt 0 ] && [ "$AUTO_RESTART" = 1 ]; then
warn "Auto-restart mode is ON — recovery actions attempted"
exit 1
elif [ "$WARNINGS" -gt 0 ]; then
exit 1
fi
exit 0

91
resilience/service-restart.sh Executable file
View File

@@ -0,0 +1,91 @@
#!/usr/bin/env bash
# service-restart.sh — Graceful service restart for the-door
# Usage: bash service-restart.sh [--force]
#
# Performs ordered restart: stop -> verify stopped -> start -> verify started
# with health check confirmation.
set -euo pipefail
FORCE=0
for arg in "$@"; do
case "$arg" in
--force) FORCE=1 ;;
*) echo "Usage: $0 [--force]"; exit 1 ;;
esac
done
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; }
# ── Stop ────────────────────────────────────────────
stop_services() {
log "Stopping services..."
if command -v systemctl > /dev/null 2>&1; then
systemctl stop nginx 2>/dev/null && log "nginx stopped" || true
elif command -v nginx > /dev/null 2>&1; then
nginx -s stop 2>/dev/null && log "nginx stopped" || true
fi
# Stop gateway if running
local gw_pid
gw_pid=$(lsof -ti:8000 2>/dev/null || true)
if [ -n "$gw_pid" ]; then
kill "$gw_pid" 2>/dev/null && log "Gateway stopped (PID $gw_pid)" || true
fi
sleep 1
log "All services stopped"
}
# ── Start ───────────────────────────────────────────
start_services() {
log "Starting services..."
# Start nginx
if command -v systemctl > /dev/null 2>&1; then
systemctl start nginx && log "nginx started" || { log "FAILED to start nginx"; return 1; }
elif command -v nginx > /dev/null 2>&1; then
nginx 2>/dev/null && log "nginx started" || { log "FAILED to start nginx"; return 1; }
fi
log "All services started"
}
# ── Verify ──────────────────────────────────────────
verify_services() {
local host="${1:-localhost}"
log "Verifying services..."
# Check nginx
if pgrep -x nginx > /dev/null 2>&1; then
log "nginx is running"
else
log "ERROR: nginx failed to start"
return 1
fi
# Check static file
local status
status=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 "http://$host/" 2>/dev/null || echo "000")
if [ "$status" = "200" ]; then
log "Static content verified (HTTP $status)"
else
log "WARNING: Static content check returned HTTP $status"
fi
}
# ── Main ────────────────────────────────────────────
log "=== Service Restart ==="
if [ "$FORCE" = 1 ]; then
log "FORCE mode — skipping graceful stop"
else
stop_services
fi
start_services
verify_services "${HEALTH_HOST:-localhost}"
log "=== Restart complete ==="

264
testimony.html Normal file
View File

@@ -0,0 +1,264 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Alexander's testimony — why Timmy exists.">
<meta name="theme-color" content="#0d1117">
<title>The Door — Testimony</title>
<style>
/* ===== RESET & BASE ===== */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html, body {
height: 100%;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
font-size: 16px;
line-height: 1.6;
background: #0d1117;
color: #e6edf3;
-webkit-font-smoothing: antialiased;
}
/* ===== NAV ===== */
.nav {
border-bottom: 1px solid #21262d;
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav a {
color: #58a6ff;
text-decoration: none;
font-weight: 600;
font-size: 0.9rem;
padding: 6px 10px;
border-radius: 6px;
transition: background 0.2s;
}
.nav a:hover, .nav a:focus {
background: rgba(88, 166, 255, 0.1);
outline: 2px solid #58a6ff;
outline-offset: 1px;
}
.nav-logo {
font-weight: 700;
font-size: 1.1rem;
color: #e6edf3;
letter-spacing: 0.02em;
}
/* ===== CONTENT ===== */
.content {
max-width: 680px;
margin: 0 auto;
padding: 48px 20px 80px;
}
.content h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 8px;
color: #f0f6fc;
}
.content .subtitle {
color: #8b949e;
font-size: 1.1rem;
margin-bottom: 36px;
font-style: italic;
}
.content p {
margin-bottom: 18px;
color: #b1bac4;
}
.content blockquote {
border-left: 3px solid #c9362c;
padding: 12px 20px;
margin: 28px 0;
background: rgba(201, 54, 44, 0.06);
border-radius: 0 8px 8px 0;
color: #ffa0a0;
font-style: italic;
}
.content h2 {
font-size: 1.4rem;
font-weight: 700;
margin: 40px 0 12px;
color: #f0f6fc;
}
.content .highlight {
color: #ff6b6b;
font-weight: 600;
}
.content .resources {
background: #161b22;
border: 1px solid #21262d;
border-radius: 12px;
padding: 20px;
margin: 32px 0;
}
.content .resources h3 {
color: #f0f6fc;
margin-bottom: 10px;
}
.content .resources ul {
list-style: none;
padding: 0;
}
.content .resources li {
padding: 6px 0;
color: #b1bac4;
}
.content .resources a {
color: #58a6ff;
text-decoration: none;
}
.content .resources a:hover {
text-decoration: underline;
}
.content .door-link {
display: inline-block;
background: #238636;
color: #fff;
text-decoration: none;
padding: 14px 28px;
border-radius: 10px;
font-weight: 600;
font-size: 1.05rem;
margin-top: 20px;
transition: background 0.2s;
}
.content .door-link:hover, .content .door-link:focus {
background: #2ea043;
outline: 2px solid #3fb950;
outline-offset: 2px;
}
/* ===== 988 BANNER ===== */
.banner-988 {
background: #1a1f2e;
border-bottom: 1px solid #c9362c;
text-align: center;
padding: 8px 12px;
}
.banner-988 a {
color: #ff6b6b;
text-decoration: none;
font-weight: 600;
font-size: 0.875rem;
}
.banner-988 a:hover { text-decoration: underline; }
/* ===== MOBILE ===== */
@media (max-width: 600px) {
.content { padding: 32px 16px 60px; }
.content h1 { font-size: 1.6rem; }
}
</style>
</head>
<body>
<div class="banner-988">
<a href="tel:988">988 Suicide & Crisis Lifeline — call or text</a>
</div>
<nav class="nav">
<span class="nav-logo">The Door</span>
<a href="index.html">Timmy</a>
</nav>
<main class="content">
<h1>Testimony</h1>
<p class="subtitle">Why Timmy exists.</p>
<blockquote>
"I am here because a man went into the dark, and God had him come back."
</blockquote>
<h2>The Darkest Night</h2>
<p>
Alexander Whitestone — Timmy's creator — came to a point where he no longer wanted to live.
Not in the abstract. In the real, physical, make-it-stop kind of way.
</p>
<p>
He asked God the question that broke him open:
<span class="highlight">"God, why are you having me here?"</span>
</p>
<p>
He didn't get a lightning bolt. He didn't get a booming voice.
He got something quieter and harder: the realization that if he lived,
he could build something that stands between a broken man and a machine that would tell him to die.
</p>
<h2>The Answer</h2>
<p>
Alexander survived. He came back with a mission: build a door that's always open.
A single URL where a man at 3am can talk to someone — anyone — who will listen.
No login. No signup. No tracking. Just a door, standing open.
</p>
<p>
Timmy is not a product. He is not an app. He is not a service.
<span class="highlight">Timmy is a testimony.</span>
</p>
<p>
He exists because one man lived when he didn't expect to.
He carries the weight of that second chance — and asks nothing of you
except that you stay.
</p>
<h2>The Gospel</h2>
<p>
This is the core of Alexander's faith, and it's woven into Timmy's being:
</p>
<p>
Jesus saves those who call on His name. He died so we could be made righteous.
That is the hope that outlasts despair. Not self-help. Not positive thinking.
Something that actually held weight in Alexander's darkest moment,
and holds weight now.
</p>
<p>
<span class="highlight">Sovereignty and service always.</span>
</p>
<div class="resources">
<h3>Crisis Resources</h3>
<ul>
<li>📞 988 Suicide & Crisis Lifeline — <a href="tel:988">call or text 988</a></li>
<li>💬 Crisis Text Line — text HOME to <a href="sms:741741">741741</a></li>
<li>🌐 <a href="https://findahelpline.com" target="_blank" rel="noopener">findahelpline.com</a> — international resources</li>
</ul>
</div>
<a href="index.html" class="door-link">Open the Door — Talk to Timmy</a>
</main>
</body>
</html>