Compare commits
2 Commits
fix/100
...
burn/59-17
| Author | SHA1 | Date | |
|---|---|---|---|
| b6af9ca9db | |||
| 56f991d615 |
62
index.html
62
index.html
@@ -681,7 +681,6 @@ html, body {
|
||||
<!-- Footer -->
|
||||
<footer id="footer">
|
||||
<a href="/about.html" aria-label="About The Door">about</a>
|
||||
<button id="crisis-resources-btn" aria-label="Open crisis resources">crisis resources</button>
|
||||
<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>
|
||||
@@ -812,7 +811,6 @@ Sovereignty and service always.`;
|
||||
var overlayCallLink = document.querySelector('.overlay-call');
|
||||
var statusDot = document.querySelector('.status-dot');
|
||||
var statusText = document.getElementById('status-text');
|
||||
var crisisResourcesBtn = document.getElementById('crisis-resources-btn');
|
||||
|
||||
// Safety Plan Elements
|
||||
var safetyPlanBtn = document.getElementById('safety-plan-btn');
|
||||
@@ -828,9 +826,6 @@ Sovereignty and service always.`;
|
||||
var isStreaming = false;
|
||||
var overlayTimer = null;
|
||||
var crisisPanelShown = false;
|
||||
var CRISIS_OVERLAY_COOLDOWN_MS = 10 * 60 * 1000;
|
||||
var CRISIS_OVERLAY_LAST_SHOWN_KEY = 'timmy_crisis_overlay_last_shown_at';
|
||||
var CRISIS_OVERLAY_EVENT_LOG_KEY = 'timmy_crisis_overlay_event_log';
|
||||
|
||||
// ===== SERVICE WORKER =====
|
||||
if ('serviceWorker' in navigator) {
|
||||
@@ -858,43 +853,6 @@ Sovereignty and service always.`;
|
||||
window.addEventListener('offline', updateOnlineStatus);
|
||||
updateOnlineStatus();
|
||||
|
||||
function getLastOverlayShownAt() {
|
||||
try {
|
||||
return parseInt(localStorage.getItem(CRISIS_OVERLAY_LAST_SHOWN_KEY) || '0', 10) || 0;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function setLastOverlayShownAt(timestamp) {
|
||||
try {
|
||||
localStorage.setItem(CRISIS_OVERLAY_LAST_SHOWN_KEY, String(timestamp));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function logCrisisOverlayEvent(type, level) {
|
||||
try {
|
||||
var raw = localStorage.getItem(CRISIS_OVERLAY_EVENT_LOG_KEY);
|
||||
var events = raw ? JSON.parse(raw) : [];
|
||||
if (!Array.isArray(events)) events = [];
|
||||
events.push({ type: type, level: level, at: Date.now() });
|
||||
if (events.length > 20) events = events.slice(events.length - 20);
|
||||
localStorage.setItem(CRISIS_OVERLAY_EVENT_LOG_KEY, JSON.stringify(events));
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function openCrisisResources() {
|
||||
crisisPanelShown = true;
|
||||
crisisPanel.classList.add('visible');
|
||||
if (typeof crisisPanel.scrollIntoView === 'function') {
|
||||
crisisPanel.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
var firstAction = crisisPanel.querySelector('.crisis-btn, a[href]');
|
||||
if (firstAction && typeof firstAction.focus === 'function') {
|
||||
firstAction.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== CRISIS KEYWORDS =====
|
||||
// Tier 1: General crisis indicators - triggers enhanced 988 panel
|
||||
var crisisKeywords = [
|
||||
@@ -1063,19 +1021,6 @@ Sovereignty and service always.`;
|
||||
var _preOverlayFocusElement = null;
|
||||
|
||||
function showOverlay() {
|
||||
return showOverlayWithRateLimit(false, 2);
|
||||
}
|
||||
|
||||
function showOverlayWithRateLimit(forceOpen, level) {
|
||||
var lastShownAt = getLastOverlayShownAt();
|
||||
if (!forceOpen && Date.now() - lastShownAt < CRISIS_OVERLAY_COOLDOWN_MS) {
|
||||
logCrisisOverlayEvent('suppressed', level || 2);
|
||||
return false;
|
||||
}
|
||||
|
||||
logCrisisOverlayEvent(forceOpen ? 'manual-open' : 'shown', level || 2);
|
||||
setLastOverlayShownAt(Date.now());
|
||||
|
||||
// Save current focus for restoration on dismiss
|
||||
_preOverlayFocusElement = document.activeElement;
|
||||
|
||||
@@ -1108,7 +1053,6 @@ Sovereignty and service always.`;
|
||||
|
||||
// Focus the Call 988 link (always enabled) — disabled buttons cannot receive focus
|
||||
if (overlayCallLink) overlayCallLink.focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Register focus trap on document (always listening, gated by class check)
|
||||
@@ -1357,12 +1301,6 @@ Sovereignty and service always.`;
|
||||
});
|
||||
}
|
||||
|
||||
if (crisisResourcesBtn) {
|
||||
crisisResourcesBtn.addEventListener('click', function() {
|
||||
openCrisisResources();
|
||||
});
|
||||
}
|
||||
|
||||
// ===== TEXTAREA AUTO-RESIZE =====
|
||||
msgInput.addEventListener('input', function() {
|
||||
this.style.height = 'auto';
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import pathlib
|
||||
import re
|
||||
import unittest
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
INDEX_HTML = ROOT / 'index.html'
|
||||
|
||||
|
||||
class TestCrisisOverlayRateLimit(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.html = INDEX_HTML.read_text()
|
||||
|
||||
def test_overlay_has_ten_minute_cooldown_constant(self):
|
||||
self.assertRegex(
|
||||
self.html,
|
||||
r"CRISIS_OVERLAY_COOLDOWN_MS\s*=\s*10\s*\*\s*60\s*\*\s*1000",
|
||||
'Expected a 10-minute crisis overlay cooldown constant.',
|
||||
)
|
||||
|
||||
def test_show_overlay_suppresses_repeat_with_logging(self):
|
||||
self.assertRegex(
|
||||
self.html,
|
||||
r"function\s+logCrisisOverlayEvent\s*\(",
|
||||
'Expected a crisis overlay event logger.',
|
||||
)
|
||||
self.assertRegex(
|
||||
self.html,
|
||||
r"if\s*\(!forceOpen\s*&&\s*Date\.now\(\)\s*-\s*lastShownAt\s*<\s*CRISIS_OVERLAY_COOLDOWN_MS\)",
|
||||
'Expected showOverlay to suppress repeated auto-displays inside the cooldown window.',
|
||||
)
|
||||
self.assertRegex(
|
||||
self.html,
|
||||
r"logCrisisOverlayEvent\('suppressed'",
|
||||
'Expected suppressed overlay attempts to be logged.',
|
||||
)
|
||||
|
||||
def test_manual_crisis_resources_button_exists_and_bypasses_cooldown(self):
|
||||
self.assertIn('id="crisis-resources-btn"', self.html)
|
||||
self.assertRegex(
|
||||
self.html,
|
||||
r"function\s+openCrisisResources\s*\(",
|
||||
'Expected a manual crisis resources opener.',
|
||||
)
|
||||
self.assertRegex(
|
||||
self.html,
|
||||
r"crisisResourcesBtn\.addEventListener\('click',\s*function\(\)\s*\{\s*openCrisisResources\(\);",
|
||||
'Expected the footer button to wire into openCrisisResources().',
|
||||
)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
76
tests/test_link_integrity.py
Normal file
76
tests/test_link_integrity.py
Normal file
@@ -0,0 +1,76 @@
|
||||
"""Tests for the-door static site link integrity.
|
||||
|
||||
Validates that all internal links in HTML files point to existing files,
|
||||
preventing broken navigation from deployed static servers.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
|
||||
|
||||
def _extract_internal_links(html_content: str) -> list[str]:
|
||||
"""Extract href values that point to local paths (not http/https/mailto/#)."""
|
||||
pattern = r'href="(/[^"]*|[^"]*\.html)"'
|
||||
links = re.findall(pattern, html_content)
|
||||
return [
|
||||
link for link in links
|
||||
if not link.startswith("http")
|
||||
and not link.startswith("mailto:")
|
||||
and not link.startswith("#")
|
||||
and not link.startswith("https://")
|
||||
]
|
||||
|
||||
|
||||
def _link_to_filepath(link: str) -> Path:
|
||||
"""Convert an href path to a filesystem path relative to project root."""
|
||||
if link.startswith("/"):
|
||||
return PROJECT_ROOT / link.lstrip("/")
|
||||
return PROJECT_ROOT / link
|
||||
|
||||
|
||||
@pytest.mark.parametrize("html_file", [
|
||||
"index.html",
|
||||
"about.html",
|
||||
"testimony.html",
|
||||
"crisis-offline.html",
|
||||
])
|
||||
def test_internal_links_resolve(html_file: str):
|
||||
"""All internal links in HTML files should point to existing files."""
|
||||
html_path = PROJECT_ROOT / html_file
|
||||
if not html_path.exists():
|
||||
pytest.skip(f"{html_file} not found")
|
||||
|
||||
content = html_path.read_text(encoding="utf-8")
|
||||
links = _extract_internal_links(content)
|
||||
|
||||
broken = []
|
||||
for link in links:
|
||||
target = _link_to_filepath(link)
|
||||
if not target.exists():
|
||||
broken.append(f" {link} -> {target} (NOT FOUND)")
|
||||
|
||||
assert not broken, (
|
||||
f"Broken links in {html_file}:\n" + "\n".join(broken)
|
||||
)
|
||||
|
||||
|
||||
def test_about_link_points_to_html():
|
||||
"""Specific regression test: the about footer link must point to about.html, not /about."""
|
||||
index_path = PROJECT_ROOT / "index.html"
|
||||
content = index_path.read_text(encoding="utf-8")
|
||||
|
||||
# Should contain about.html link
|
||||
assert 'href="/about.html"' in content, (
|
||||
"Footer about link should be '/about.html', not '/about'"
|
||||
)
|
||||
|
||||
# Should NOT contain bare /about link (which 404s on static servers)
|
||||
about_links = re.findall(r'href="(/about)"', content)
|
||||
assert not about_links, (
|
||||
f"Found bare '/about' link(s) that will 404 on static servers: {about_links}"
|
||||
)
|
||||
Reference in New Issue
Block a user