usability enhancements

This commit is contained in:
2026-02-13 03:12:42 -05:00
parent bf4a40f533
commit 0e21e035f5
28 changed files with 904 additions and 15 deletions

View File

@@ -3,6 +3,8 @@ IMAGE_QUALITY=85
OPENROUTER_API_KEY=
UMAMI_SCRIPT_URL=
UMAMI_WEBSITE_ID=
GITHUB_REPO_URL=
CONTACT_EMAIL=
RETENTION_DAYS=30
ROYALTY_IMAGE_PROVIDER=picsum
ROYALTY_IMAGE_MCP_ENDPOINT=

View File

@@ -10,6 +10,8 @@ IMAGE_QUALITY = int(os.getenv("IMAGE_QUALITY", "85"))
RETENTION_DAYS = int(os.getenv("RETENTION_DAYS", "30"))
UMAMI_SCRIPT_URL = os.getenv("UMAMI_SCRIPT_URL", "")
UMAMI_WEBSITE_ID = os.getenv("UMAMI_WEBSITE_ID", "")
GITHUB_REPO_URL = os.getenv("GITHUB_REPO_URL", "")
CONTACT_EMAIL = os.getenv("CONTACT_EMAIL", "")
ROYALTY_IMAGE_MCP_ENDPOINT = os.getenv("ROYALTY_IMAGE_MCP_ENDPOINT", "")
ROYALTY_IMAGE_API_KEY = os.getenv("ROYALTY_IMAGE_API_KEY", "")
ROYALTY_IMAGE_PROVIDER = os.getenv("ROYALTY_IMAGE_PROVIDER", "picsum")

View File

@@ -1,13 +1,15 @@
import logging
import os
import random
from apscheduler.schedulers.background import BackgroundScheduler
from fastapi import Depends, FastAPI, Query, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from sqlalchemy.orm import Session
from starlette.exceptions import HTTPException as StarletteHTTPException
from backend import config
from backend.database import get_db, init_db
@@ -33,6 +35,77 @@ logger = logging.getLogger(__name__)
app = FastAPI(title="ClawFort News API", version="0.1.0")
_ERROR_MESSAGES = {
404: [
"Oh no! This page wandered off to train a tiny model.",
"Oh no! We looked everywhere, even in the latent space.",
"Oh no! The link took a creative detour.",
"Oh no! This route is currently off doing research.",
"Oh no! The page you asked for is not in this timeline.",
],
500: [
"Oh no! The server hit a logic knot and needs a quick reset.",
"Oh no! Our robots dropped a semicolon somewhere important.",
"Oh no! A background process got stage fright.",
"Oh no! The AI took an unexpected coffee break.",
"Oh no! Something internal blinked at the wrong moment.",
],
}
def _render_error_page(status_code: int) -> str:
message = random.choice(_ERROR_MESSAGES.get(status_code, _ERROR_MESSAGES[500]))
return f"""<!DOCTYPE html>
<html lang=\"en\">
<head>
<meta charset=\"UTF-8\" />
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />
<title>{status_code} - ClawFort</title>
<link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/images/favicon-ai.svg\" />
<style>
:root {{ color-scheme: dark; }}
body {{ margin:0; font-family: Inter, system-ui, sans-serif; background:#0f172a; color:#e2e8f0; }}
.wrap {{ min-height:100vh; display:grid; place-items:center; padding:24px; }}
.card {{ max-width:760px; width:100%; border:1px solid rgba(148,163,184,.25); border-radius:16px; background:#111827; padding:28px; }}
.code {{ font-size:72px; font-weight:900; line-height:1; color:#60a5fa; margin:0 0 12px; }}
.title {{ font-size:28px; font-weight:800; margin:0 0 8px; }}
.msg {{ color:#cbd5e1; font-size:17px; line-height:1.6; margin:0 0 16px; }}
a {{ color:#93c5fd; text-decoration:none; font-weight:700; }}
a:hover {{ color:#bfdbfe; }}
</style>
</head>
<body>
<main class=\"wrap\">
<section class=\"card\" role=\"status\" aria-live=\"polite\">
<p class=\"code\">{status_code}</p>
<h1 class=\"title\">Oh no!</h1>
<p class=\"msg\">{message}</p>
<a href=\"/\">Back to ClawFort</a>
</section>
</main>
</body>
</html>"""
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request: Request, exc: StarletteHTTPException):
if request.url.path.startswith("/api/"):
return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})
if exc.status_code == 404:
return HTMLResponse(_render_error_page(404), status_code=404)
return JSONResponse(status_code=exc.status_code, content={"detail": exc.detail})
@app.exception_handler(Exception)
async def unhandled_exception_handler(request: Request, exc: Exception):
logger.exception("Unhandled server error: %s", exc)
if request.url.path.startswith("/api/"):
return JSONResponse(status_code=500, content={"detail": "Internal Server Error"})
return HTMLResponse(_render_error_page(500), status_code=500)
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
@@ -216,6 +289,8 @@ async def serve_config() -> dict:
return {
"umami_script_url": config.UMAMI_SCRIPT_URL,
"umami_website_id": config.UMAMI_WEBSITE_ID,
"github_repo_url": config.GITHUB_REPO_URL,
"contact_email": config.CONTACT_EMAIL,
"supported_languages": config.SUPPORTED_LANGUAGES,
"default_language": "en",
}

View File

@@ -0,0 +1,13 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="AI favicon">
<defs>
<linearGradient id="g" x1="0" x2="1" y1="0" y2="1">
<stop offset="0" stop-color="#60a5fa"/>
<stop offset="1" stop-color="#22d3ee"/>
</linearGradient>
</defs>
<rect x="4" y="4" width="56" height="56" rx="14" fill="#0f172a"/>
<circle cx="32" cy="24" r="10" fill="none" stroke="url(#g)" stroke-width="4"/>
<path d="M18 44c4-7 8-10 14-10s10 3 14 10" fill="none" stroke="url(#g)" stroke-width="4" stroke-linecap="round"/>
<circle cx="26" cy="24" r="2.2" fill="#e2e8f0"/>
<circle cx="38" cy="24" r="2.2" fill="#e2e8f0"/>
</svg>

After

Width:  |  Height:  |  Size: 659 B

Binary file not shown.

View File

@@ -15,6 +15,7 @@
<meta name="twitter:description" content="Understand content attribution and ownership boundaries for ClawFort AI-generated summaries.">
<meta name="twitter:image" content="/static/images/placeholder.png">
<link rel="canonical" href="/attribution">
<link rel="icon" type="image/svg+xml" href="/static/images/favicon-ai.svg">
<title>Attribution and Ownership Disclaimer - ClawFort</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

View File

@@ -20,10 +20,11 @@
<link rel="canonical" href="/" id="canonical-link">
<link rel="dns-prefetch" href="//fonts.googleapis.com">
<link rel="dns-prefetch" href="//fonts.gstatic.com">
<title>ClawFort AI News</title>
<title>ClawFort AI News</title>
<link rel="icon" type="image/svg+xml" href="/static/images/favicon-ai.svg">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800;900&family=Noto+Sans+Tamil:wght@400;500;600;700&family=Noto+Sans+Malayalam:wght@400;500;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
@@ -36,7 +37,9 @@
700: '#4263eb', 900: '#1e2a5e', 950: '#0f172a'
}
},
fontFamily: { sans: ['Inter', 'system-ui', 'sans-serif'] }
fontFamily: {
sans: ['Inter', 'Noto Sans Tamil', 'Noto Sans Malayalam', 'system-ui', 'sans-serif']
}
}
}
}
@@ -208,6 +211,45 @@
border-color: #5c7cfa;
box-shadow: 0 0 0 1px #5c7cfa inset;
}
html[data-lang='ta'] .news-card-summary,
html[data-lang='ml'] .news-card-summary,
html[data-lang='ta'] .hero-summary,
html[data-lang='ml'] .hero-summary,
html[data-lang='ta'] .modal-body-text,
html[data-lang='ml'] .modal-body-text {
font-size: 1.02rem;
line-height: 1.78;
letter-spacing: 0.01em;
}
.share-icon-btn {
width: 34px;
height: 34px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
border: 1px solid rgba(148, 163, 184, 0.35);
background: rgba(92, 124, 250, 0.14);
color: #dbeafe;
transition: background 180ms ease;
}
.share-icon-btn:hover { background: rgba(92, 124, 250, 0.25); }
.footer-link { text-decoration: underline; text-underline-offset: 2px; }
.footer-link:hover { color: #dbeafe; }
.contact-hint {
position: fixed;
z-index: 60;
max-width: 280px;
background: rgba(15, 23, 42, 0.96);
color: #e2e8f0;
border: 1px solid rgba(148, 163, 184, 0.35);
border-radius: 10px;
padding: 8px 10px;
font-size: 12px;
line-height: 1.5;
pointer-events: none;
box-shadow: 0 8px 22px rgba(2, 6, 23, 0.45);
}
*:focus-visible {
outline: 2px solid #5c7cfa;
outline-offset: 2px;
@@ -291,7 +333,7 @@
<p class="text-base sm:text-lg max-w-3xl line-clamp-3 mb-4 hero-summary" x-text="item.summary"></p>
<div class="flex flex-wrap items-center gap-4 text-sm hero-meta">
<button class="px-3 py-1.5 rounded-md bg-cf-500/20 text-cf-300 hover:bg-cf-500/30 transition-colors"
@click="trackEvent('hero-cta-click', { article_id: item.id }); window.dispatchEvent(new CustomEvent('open-summary', { detail: item }))">
@click="trackEvent('hero-cta-click', { article_id: item.id, article_title: item.headline }); window.dispatchEvent(new CustomEvent('open-summary', { detail: item }))">
Read TL;DR
</button>
<a :href="item.source_url" target="_blank" rel="noopener"
@@ -300,6 +342,9 @@
x-show="item.source_url">
Via: <span x-text="extractDomain(item.source_url)" class="underline underline-offset-2"></span>
</a>
<a :href="articlePermalink(item)" class="hover:text-cf-300 transition-colors underline underline-offset-2">
Permalink
</a>
<span x-show="item.image_credit" x-text="'Image: ' + item.image_credit"></span>
</div>
</div>
@@ -364,9 +409,12 @@
@click.stop="trackEvent('source-link-click')"
x-show="item.source_url"
x-text="extractDomain(item.source_url)"></a>
<span x-text="timeAgo(item.published_at)"></span>
<div class="flex items-center gap-2">
<a :href="articlePermalink(item)" class="hover:text-cf-300 underline underline-offset-2">Link</a>
<span x-text="timeAgo(item.published_at)"></span>
</div>
</div>
<button @click="openSummary(item)"
<button @click="trackEvent('feed-cta-click', { article_id: item.id, article_title: item.headline }); openSummary(item)"
class="w-full text-center text-xs font-semibold rounded-md px-3 py-2 bg-cf-500/15 hover:bg-cf-500/25 transition-colors news-card-btn">
Read TL;DR
</button>
@@ -423,6 +471,21 @@
</a>
</div>
<div>
<h3 class="text-sm uppercase tracking-wide font-semibold mb-2 modal-section-title">Share</h3>
<div class="flex items-center gap-2">
<a :href="shareLink('x', modalItem)" target="_blank" rel="noopener" class="share-icon-btn" aria-label="Share on X" title="Share on X">
<svg viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true"><path fill="currentColor" d="M18.9 2H22l-6.8 7.8L23 22h-6.2l-4.9-6.4L6.2 22H3l7.3-8.4L1 2h6.3l4.4 5.8L18.9 2z"/></svg>
</a>
<a :href="shareLink('whatsapp', modalItem)" target="_blank" rel="noopener" class="share-icon-btn" aria-label="Share on WhatsApp" title="Share on WhatsApp">
<svg viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true"><path fill="currentColor" d="M20.5 3.5A11.8 11.8 0 0 0 12.1 0C5.5 0 .1 5.3.1 11.9c0 2.1.5 4.1 1.6 5.9L0 24l6.4-1.7a12 12 0 0 0 5.7 1.4h.1c6.6 0 11.9-5.3 11.9-11.9 0-3.2-1.3-6.2-3.6-8.3zM12.2 21.7h-.1a9.9 9.9 0 0 1-5-1.4l-.4-.2-3.8 1 1-3.7-.2-.4a9.8 9.8 0 0 1-1.5-5.2c0-5.4 4.4-9.8 9.9-9.8 2.6 0 5.1 1 6.9 2.9a9.7 9.7 0 0 1 2.8 6.9c0 5.4-4.4 9.9-9.8 9.9zm5.4-7.3c-.3-.1-1.8-.9-2.1-1-.3-.1-.5-.1-.7.2-.2.3-.8 1-1 1.2-.2.2-.3.2-.6.1-1.6-.8-2.6-1.5-3.7-3.3-.3-.5.3-.5 1-1.8.1-.2.1-.4 0-.6l-1-2.4c-.2-.6-.5-.5-.7-.5h-.6c-.2 0-.6.1-.9.4-.3.3-1.2 1.1-1.2 2.8 0 1.6 1.2 3.2 1.4 3.4.2.2 2.3 3.6 5.6 5 .8.3 1.4.5 1.9.7.8.3 1.5.2 2 .1.6-.1 1.8-.8 2-1.5.2-.7.2-1.4.1-1.5-.1-.1-.3-.2-.6-.3z"/></svg>
</a>
<a :href="shareLink('linkedin', modalItem)" target="_blank" rel="noopener" class="share-icon-btn" aria-label="Share on LinkedIn" title="Share on LinkedIn">
<svg viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true"><path fill="currentColor" d="M4.98 3.5a2.5 2.5 0 1 1 0 5.001A2.5 2.5 0 0 1 4.98 3.5zM3 9h4v12H3zM10 9h3.8v1.7h.1c.5-1 1.8-2.1 3.7-2.1 4 0 4.7 2.6 4.7 6V21h-4v-5.4c0-1.3 0-2.9-1.8-2.9s-2.1 1.4-2.1 2.8V21h-4V9z"/></svg>
</a>
</div>
</div>
<p class="text-xs modal-powered">Powered by Perplexity</p>
</div>
</div>
@@ -445,15 +508,20 @@
<div class="fixed left-0 right-0 pointer-events-none" style="top:75%" x-data x-intersect:enter="trackDepth(75)"></div>
</main>
<footer class="border-t border-white/5 py-8 text-center text-sm text-gray-500">
<footer x-data="footerEnhancements()" x-init="init()" class="border-t border-white/5 py-8 text-center text-sm text-gray-500">
<div class="max-w-7xl mx-auto px-4 space-y-2">
<p>Powered by <a href="https://www.perplexity.ai" target="_blank" rel="noopener" class="text-cf-400 hover:text-cf-300 transition-colors">Perplexity</a></p>
<p class="space-x-3">
<a href="/terms" class="underline hover:text-gray-300">Terms of Use</a>
<a href="/attribution" class="underline hover:text-gray-300">Attribution</a>
<a href="/terms" class="footer-link">Terms of Use</a>
<a href="/attribution" class="footer-link">Attribution</a>
<button type="button" class="footer-link" @click="scrollTop()">Back to Top</button>
<a x-show="githubUrl" :href="githubUrl" target="_blank" rel="noopener" class="footer-link">GitHub</a>
<a x-show="contactEmail" :href="'mailto:' + contactEmail" class="footer-link"
@mouseenter="showHint($event)" @mousemove="moveHint($event)" @mouseleave="hideHint()">Email me</a>
</p>
<p>&copy; <span x-data x-text="new Date().getFullYear()"></span> ClawFort. All rights reserved.</p>
</div>
<div x-show="hintVisible" x-cloak class="contact-hint" :style="`left:${hintX}px; top:${hintY}px`" x-text="hintText"></div>
</footer>
<div id="cookie-consent-banner" class="hidden fixed bottom-4 left-1/2 -translate-x-1/2 w-[95%] max-w-3xl z-50 rounded-lg border border-white/15 bg-slate-900/95 backdrop-blur p-4 transition-opacity duration-700 opacity-100">
@@ -506,7 +574,47 @@ function toAbsoluteUrl(url) {
function articlePermalink(item) {
if (!item?.id) return toAbsoluteUrl('/');
return toAbsoluteUrl(`/#news-${item.id}`);
return toAbsoluteUrl(`/?article=${item.id}`);
}
function getPermalinkArticleId() {
const params = new URLSearchParams(window.location.search);
const article = params.get('article');
if (article && /^\d+$/.test(article)) return Number(article);
const match = (window.location.hash || '').match(/^#news-(\d+)$/);
if (match) return Number(match[1]);
return null;
}
function setPermalinkArticleId(articleId) {
if (!articleId) return;
const url = new URL(window.location.href);
url.searchParams.set('article', String(articleId));
url.hash = '';
window.history.replaceState({}, '', url.toString());
}
function clearPermalinkArticleId() {
const url = new URL(window.location.href);
url.searchParams.delete('article');
if ((url.hash || '').startsWith('#news-')) url.hash = '';
window.history.replaceState({}, '', url.toString());
}
function shareLink(provider, item) {
const permalink = articlePermalink(item);
const encodedUrl = encodeURIComponent(permalink);
const encodedTitle = encodeURIComponent(item?.headline || 'ClawFort AI News');
if (provider === 'x') {
return `https://x.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`;
}
if (provider === 'whatsapp') {
return `https://wa.me/?text=${encodedTitle}%20${encodedUrl}`;
}
if (provider === 'linkedin') {
return `https://www.linkedin.com/sharing/share-offsite/?url=${encodedUrl}`;
}
return permalink;
}
function setMetaContent(selector, content, attribute = 'content') {
@@ -519,13 +627,13 @@ function syncSeoMeta(item) {
const canonical = toAbsoluteUrl(window.location.pathname || '/');
setMetaContent('#canonical-link', canonical, 'href');
setMetaContent('#meta-og-url', canonical);
document.title = 'ClawFort AI News';
if (!item) return;
const title = `${item.headline} | ClawFort`;
const title = 'ClawFort AI News';
const description = item.summary || 'Latest AI news updates from ClawFort.';
const image = toAbsoluteUrl(preferredImage(item));
document.title = title;
setMetaContent('meta[name="description"]', description);
setMetaContent('meta[property="og:title"]', title);
setMetaContent('meta[property="og:description"]', description);
@@ -741,6 +849,7 @@ function getPreferredLanguage() {
function setPreferredLanguage(language) {
const normalized = normalizeLanguage(language);
window._selectedLanguage = normalized;
document.documentElement.setAttribute('data-lang', normalized);
safeSetStorage('clawfort_language', normalized);
setCookie('clawfort_language', normalized);
const select = document.getElementById('language-select');
@@ -756,10 +865,103 @@ window._selectedLanguage = getPreferredLanguage();
window._themeChoice = getPreferredTheme();
window.__heroNewsItem = null;
window.__feedNewsItems = [];
document.documentElement.setAttribute('data-lang', window._selectedLanguage);
applyTheme(window._themeChoice);
syncSeoMeta(null);
syncStructuredData();
const CONTACT_HINTS = [
'If you have feedback, I would love to hear it.',
'Found a bug? Send a note and I will take a look.',
'Have an idea for ClawFort? Reach out anytime.',
'Suggestions are always welcome here.',
'Want a feature? Let me know by email.',
'Your feedback helps improve this project.',
'Questions, suggestions, or praise - all welcome.',
'Noticed something odd? Please report it.',
'Tell me what would make this site better.',
'Happy to hear your thoughts on UX and content.',
'If something feels off, I am listening.',
'Feature requests are open - send yours.',
'If you care about this project, drop a line.',
'Spotted a typo? I appreciate quick heads-up notes.',
'Your perspective can shape the next update.',
'If this helped you, tell me what to improve next.',
'Ideas are fuel. Share one when you can.',
'Small feedback can create big improvements.',
'If a page confused you, let me know why.',
'Product feedback is always in scope.',
'Have performance concerns? I want to hear them.',
'Your suggestions are part of the roadmap.',
'See something broken? I can fix it faster with details.',
'If a feature is missing, say the word.',
'Tell me how ClawFort could serve you better.',
'Think the UI can be clearer? Share your take.',
'Want better readability options? Send feedback.',
'If translation quality feels off, please report it.',
'Any accessibility concern is worth sharing.',
'If a link fails, I would appreciate the report.',
'Help make this news feed sharper with feedback.',
'Your comments help prioritize updates.',
'Share your favorite improvement idea.',
'If you have a workflow pain point, mention it.',
'Even one sentence of feedback helps.',
'Tell me which feature you use most.',
'If sharing feels clunky, let me know.',
'Want custom views? I am open to suggestions.',
'If something is hard to discover, please say so.',
'I appreciate concrete suggestions and examples.',
'If the mobile layout needs work, send details.',
'A quick email can spark the next fix.',
'Thanks for helping improve this project.',
'If this tool saves you time, tell me what else you need.',
'I welcome constructive feedback.',
'Have a better idea? I am curious.',
'If you found a regression, please report it.',
'Your use case matters - share it.',
'If something should be simpler, I want to know.',
'Feedback is always appreciated. Thank you.',
];
function footerEnhancements() {
return {
githubUrl: '',
contactEmail: '',
hintVisible: false,
hintText: '',
hintX: 16,
hintY: 16,
async init() {
try {
const resp = await fetch('/config');
if (!resp.ok) return;
const cfg = await resp.json();
this.githubUrl = cfg.github_repo_url || '';
this.contactEmail = cfg.contact_email || '';
} catch {}
},
scrollTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
},
randomHint() {
return CONTACT_HINTS[Math.floor(Math.random() * CONTACT_HINTS.length)];
},
showHint(event) {
this.hintText = this.randomHint();
this.hintVisible = true;
this.moveHint(event);
},
moveHint(event) {
if (!event) return;
this.hintX = Math.min(event.clientX + 16, window.innerWidth - 290);
this.hintY = Math.min(event.clientY + 18, window.innerHeight - 80);
},
hideHint() {
this.hintVisible = false;
},
};
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const choice = getPreferredTheme();
if (choice === 'system') {
@@ -834,6 +1036,7 @@ function newsFeed() {
async init() {
await this.waitForHero();
await this.loadMore();
await this.openFromPermalink();
this.initialLoading = false;
this.setupObserver();
window.addEventListener('language-changed', async () => {
@@ -845,6 +1048,7 @@ function newsFeed() {
window.__feedNewsItems = [];
await this.waitForHero();
await this.loadMore();
await this.openFromPermalink();
this.initialLoading = false;
});
@@ -919,23 +1123,52 @@ function newsFeed() {
this.observer.observe(this.$refs.sentinel);
},
async openFromPermalink() {
const articleId = getPermalinkArticleId();
if (!articleId) return;
if (window.__heroNewsItem?.id === articleId) {
this.openSummary(window.__heroNewsItem);
return;
}
let item = this.items.find(i => i.id === articleId);
let attempts = 0;
while (!item && this.hasMore && attempts < 5) {
await this.loadMore();
item = this.items.find(i => i.id === articleId);
attempts += 1;
}
if (item) this.openSummary(item);
},
openSummary(item) {
this.modalItem = item;
this.modalOpen = true;
this.modalImageLoading = true;
this.modalTldrLoading = true;
setPermalinkArticleId(item?.id);
setTimeout(() => {
if (this.modalOpen) this.modalTldrLoading = false;
}, 250);
trackEvent('summary-modal-open', { article_id: item.id });
trackEvent('summary-modal-open', {
article_id: item.id,
article_title: item.headline || null,
});
},
closeSummary() {
const id = this.modalItem ? this.modalItem.id : null;
const title = this.modalItem ? this.modalItem.headline : null;
this.modalOpen = false;
this.modalItem = null;
this.modalTldrLoading = true;
trackEvent('summary-modal-close', { article_id: id });
clearPermalinkArticleId();
trackEvent('summary-modal-close', {
article_id: id,
article_title: title,
});
},
trackSummarySource(item) {

View File

@@ -15,6 +15,7 @@
<meta name="twitter:description" content="Read ClawFort terms governing informational use of AI-generated and aggregated content.">
<meta name="twitter:image" content="/static/images/placeholder.png">
<link rel="canonical" href="/terms">
<link rel="icon" type="image/svg+xml" href="/static/images/favicon-ai.svg">
<title>Terms of Use - ClawFort</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

View File

@@ -0,0 +1,57 @@
## Context
Current Umami event payloads for hero/feed CTA clicks and summary modal lifecycle events are identifier-heavy (`article_id`) and not directly human-readable in dashboards. Analysts must resolve IDs back to content to understand behavior patterns, which slows ad-hoc exploration and increases reporting friction.
## Goals / Non-Goals
**Goals:**
- Add `article_title` to existing CTA and summary modal analytics payloads.
- Preserve existing event names and current fields for backward compatibility.
- Keep changes scoped to frontend event payload construction.
**Non-Goals:**
- Renaming event names or changing event timing semantics.
- Introducing new analytics providers or backend event pipelines.
- Reworking existing dashboard taxonomy outside payload enrichment.
## Decisions
### Decision 1: Additive payload enrichment only
**Choice:** Add `article_title` as an extra property while retaining `article_id` and existing metadata.
**Rationale:**
- Backward compatible for existing analytics queries.
- Immediate analyst readability improvement without migration burden.
### Decision 2: Use headline string from the in-memory item model
**Choice:** Populate `article_title` from current article object (`item.headline` / `modalItem.headline`).
**Rationale:**
- No extra network calls or state plumbing.
- Data already present where events are emitted.
### Decision 3: Preserve event contracts and dispatch points
**Choice:** Keep event names unchanged (`hero-cta-click`, `summary-modal-open`, `summary-modal-close`).
**Rationale:**
- Avoids dashboard/query breakage.
- Keeps this change low risk and auditable.
## Risks / Trade-offs
- **[Risk] Long titles inflate payload size** -> Mitigation: send raw headline as-is; acceptable for low-volume client events.
- **[Risk] Missing title on edge cases** -> Mitigation: include `article_id` as canonical fallback identifier.
- **[Trade-off] Same event schema differs across historical windows** -> Mitigation: additive field keeps historical queries valid while enabling richer future segmentation.
## Migration Plan
1. Update event emitters in `frontend/index.html` to include `article_title`.
2. Verify emitted payloads in browser devtools/Umami debug path.
3. Update analytics-tagging spec deltas and task checklist.
Rollback:
- Remove `article_title` field additions; existing events remain unchanged.
## Open Questions
- Do analysts also want `article_permalink` included now, or defer to future UX/permalink change?

View File

@@ -0,0 +1,21 @@
## MODIFIED Requirements
### Requirement: News attribution display
The system SHALL clearly attribute all news content and images to their sources.
#### Scenario: Source attribution
- **WHEN** displaying any news item
- **THEN** the system SHALL show the original source name and link
- **AND** display image credit if available
#### Scenario: Perplexity attribution
- **WHEN** displaying aggregated content
- **THEN** the system SHALL include "Powered by Perplexity" in the footer
#### Scenario: Analytics tracking
- **WHEN** Umami analytics is configured via `UMAMI_SCRIPT_URL` and `UMAMI_WEBSITE_ID`
- **THEN** the system SHALL inject Umami tracking script into page head
- **AND** track page view events on initial load
- **AND** track scroll depth events (25%, 50%, 75%, 100%)
- **AND** track CTA click events (news item clicks, source link clicks)
- **AND** CTA click payload includes both `article_id` and `article_title` when article context is available

View File

@@ -0,0 +1,24 @@
## MODIFIED Requirements
### Requirement: Modal interactions are tagged for analytics
The system SHALL emit Umami analytics events for summary modal open and close actions.
#### Scenario: Modal open event tagging
- **WHEN** a user opens the summary modal
- **THEN** the system emits a modal-open Umami event
- **AND** event payload includes article context identifier
- **AND** event payload includes `article_title` when available
#### Scenario: Modal close event tagging
- **WHEN** a user closes the summary modal
- **THEN** the system emits a modal-close Umami event
- **AND** event payload includes article context identifier when available
- **AND** event payload includes `article_title` when available
### Requirement: Source link-out interactions are tagged for analytics
The system SHALL emit Umami analytics events for source/citation link-outs from summary modal.
#### Scenario: Source link-out event tagging
- **WHEN** a user clicks source/citation link in summary modal
- **THEN** the system emits a link-out Umami event before or at navigation trigger
- **AND** event includes source URL or source identifier metadata

View File

@@ -0,0 +1,15 @@
## 1. Event Payload Enrichment
- [x] 1.1 Update hero/feed CTA tracking payloads in `frontend/index.html` to include `article_title` alongside `article_id`.
- [x] 1.2 Update summary modal open tracking payload to include `article_title`.
- [x] 1.3 Update summary modal close tracking payload to include `article_title` when available.
## 2. Compatibility and Safety
- [x] 2.1 Preserve existing event names (`hero-cta-click`, `summary-modal-open`, `summary-modal-close`) and existing fields.
- [x] 2.2 Ensure payload enrichment is additive and does not break tracking when title is missing.
## 3. Validation
- [x] 3.1 Verify events in browser devtools/Umami network payload include `article_title`.
- [x] 3.2 Verify existing dashboard queries based on `article_id` remain valid.

View File

@@ -0,0 +1,84 @@
## Context
The current site works functionally but has usability gaps: dynamic homepage title behavior can confuse context, discovery/sharing of individual articles is weak, error pages are plain, and footer affordances are limited. Readability needs incremental tuning, especially for Tamil and Malayalam text rendering. These issues span frontend state management, metadata behavior, footer UX, and static asset/routing support.
## Goals / Non-Goals
**Goals:**
- Stabilize homepage title for clarity and SEO consistency.
- Add back-to-top affordance and per-article permalink deep-linking that opens the correct modal.
- Add minimal icon-based sharing (X/WhatsApp/LinkedIn) for modal/permalink flows.
- Improve legibility via typography/color refinements, with explicit Tamil/Malayalam attention.
- Introduce branded AI favicon.
- Add expressive 404/500 pages with prominent code display and safe playful AI-style copy.
- Add env-driven GitHub/email footer links with randomized safe feedback tooltip microcopy.
**Non-Goals:**
- Rewriting router architecture or adding server-side rendering.
- Adding complex social auth or third-party share SDKs.
- Replacing existing analytics instrumentation.
- Building full i18n content moderation pipeline for tooltip text.
## Decisions
### Decision 1: Hash/query driven deep-link modal state
**Choice:** Use permalink token (`?article=<id>` or hash equivalent) and resolve on load to open matching modal.
**Rationale:**
- Works with existing SPA flow and avoids backend route explosion.
- Easy to share/copy and easy to close by clearing URL state.
### Decision 2: Keep sharing implementation lightweight
**Choice:** Construct provider share URLs client-side and use icon buttons without extra SDK dependencies.
**Rationale:**
- Minimal bundle impact.
- Predictable behavior with native share endpoints.
### Decision 3: Readability tuning via CSS tokens and language-aware classes
**Choice:** Adjust text size/line-height/color contrast in existing theme token system; apply optional class hooks for Tamil/Malayalam content rendering.
**Rationale:**
- Incremental and low-risk.
- Preserves current theme architecture.
### Decision 4: Error pages with controlled safe message pool
**Choice:** Use curated safe message templates (50+) and deterministic randomization per request/session.
**Rationale:**
- “Oh no!” playful tone without unsafe generation risks.
- No runtime LLM dependency required for error path.
### Decision 5: Footer extensibility via env-backed config
**Choice:** Expose `GITHUB_REPO_URL` and `CONTACT_EMAIL` through existing frontend config bootstrap and render conditional links.
**Rationale:**
- Keeps deployment-specific metadata configurable.
- Avoids hardcoded personal links.
## Risks / Trade-offs
- **[Risk] Deep-link to missing article id** -> Mitigation: fail gracefully (no modal open) and keep list view stable.
- **[Risk] Tooltip microcopy quality drift** -> Mitigation: curate fixed safe message corpus and review before release.
- **[Risk] Share URL encoding mistakes** -> Mitigation: centralized URL builder + encode all user-facing strings.
- **[Trade-off] Stable homepage title reduces article-specific title SEO** -> Mitigation: keep OG/Twitter/article-level metadata contextual in card/modal/share surfaces.
## Migration Plan
1. Add permalink generation, URL parsing, and modal open-on-load logic.
2. Add share icons/buttons and footer microinteraction links.
3. Apply readability and color token tweaks; verify Tamil/Malayalam rendering.
4. Add error page handlers/templates and safe message pool.
5. Add favicon asset and head reference.
6. Add env-config plumbing for GitHub/email footer links.
Rollback:
- Revert permalink and share UI additions.
- Revert title and readability token changes.
- Restore prior generic error page behavior.
## Open Questions
- Should permalink use numeric article id only, or slug+id hybrid for readability?
- Should contact tooltip randomization rotate per hover event or per page load?
- Should 404/500 messages be localized immediately or staged in English first?

View File

@@ -0,0 +1,36 @@
## Why
Several small UX and content-discovery issues are adding friction across navigation, sharing, readability, and trust signals. Addressing them together delivers a noticeable quality jump with low implementation risk.
## What Changes
- Set a stable homepage title (for example, `ClawFort AI News`) instead of changing it to the latest article headline.
- Add a footer `Back to Top` action for quick navigation.
- Add per-article permalinks and support deep-link behavior that opens the correct article modal on page load.
- Add minimal icon-only share actions for permalink sharing (`X`, `WhatsApp`, `LinkedIn`) in modal/footer share area.
- Improve readability with small typography/color updates, with extra attention to Tamil and Malayalam legibility.
- Add custom 404/500 pages that prominently show the error code and include safe, playful AI-generated “Oh no!” style messages.
- Add footer links driven by environment configuration for GitHub repo and contact email; include enhanced hover/near-cursor feedback messages from a randomized safe message pool.
- Add an AI-themed favicon asset and wire it into the site.
## Capabilities
### New Capabilities
- `article-permalinks-and-deep-link-modal`: Defines canonical per-article permalink structure and modal auto-open behavior from URL state.
- `share-and-contact-microinteractions`: Defines social share actions, configurable GitHub/email links, and randomized safe feedback microcopy for contact affordance.
- `error-pages-with-playful-ai-messaging`: Defines 404/500 UX with prominent status codes and policy-safe AI-style messaging.
- `site-branding-favicon`: Defines favicon asset requirements and integration.
### Modified Capabilities
- `seo-meta-and-social-tags`: Update homepage title behavior to remain stable and brand-oriented while preserving metadata quality.
- `summary-modal-experience`: Extend modal contract to support permalink-driven open state and share entry points.
- `responsive-device-agnostic-layout`: Apply small typography/color refinements for readability across devices.
- `language-aware-content-delivery`: Improve Tamil/Malayalam presentation quality in the existing delivery surfaces.
- `footer-policy-links`: Extend footer navigation with back-to-top, GitHub, and contact affordances.
## Impact
- **Frontend UI:** `frontend/index.html` (title policy, footer controls, permalink/deep-link modal behavior, share icons, readability tweaks, favicon link).
- **Backend/Config:** environment variables for GitHub URL and contact email exposure; possible API/config exposure if frontend config bootstrapping is needed.
- **Routing/Pages:** new error page templates/handlers for 404 and 500 responses.
- **Content/UX:** curated safe message pool for contact tooltip/hover microcopy and error-page “Oh no!” messaging.

View File

@@ -0,0 +1,25 @@
## ADDED Requirements
### Requirement: Each news item exposes a permalink
The system SHALL expose a stable, shareable permalink for each rendered news item.
#### Scenario: Per-item permalink rendering
- **WHEN** a news item is rendered in hero or feed context
- **THEN** the UI provides a permalink target tied to that article context
#### Scenario: Permalink copy/share usability
- **WHEN** a user uses share/copy affordances for an item
- **THEN** the resulting URL contains sufficient information to resolve that article on load
### Requirement: Deep-link loads article modal in open state
The system SHALL open the matching article modal when the page is loaded with a valid article permalink.
#### Scenario: Valid permalink opens modal
- **WHEN** a user lands on homepage with a permalink for an existing article
- **THEN** the corresponding article modal is opened automatically
- **AND** modal content matches the permalink target
#### Scenario: Invalid permalink fails safely
- **WHEN** a permalink references a missing or invalid article identifier
- **THEN** the page remains usable without hard failure
- **AND** modal is not opened with incorrect content

View File

@@ -0,0 +1,20 @@
## ADDED Requirements
### Requirement: Error pages prominently display status code
The system SHALL show prominent HTTP status code display on 404 and 500 pages.
#### Scenario: 404 rendering
- **WHEN** user visits a non-existent route
- **THEN** the page prominently displays `404`
#### Scenario: 500 rendering
- **WHEN** an internal server error page is rendered
- **THEN** the page prominently displays `500`
### Requirement: Error pages include safe playful AI-style messaging
The system SHALL render an "Oh no!" style playful message that is safe and policy-compliant.
#### Scenario: Safe message selection
- **WHEN** an error page is displayed
- **THEN** a playful message is selected from a curated safe message set
- **AND** message excludes profanity and discriminatory/abusive content

View File

@@ -0,0 +1,24 @@
## MODIFIED Requirements
### Requirement: Footer exposes policy navigation links
The system SHALL display footer links for Terms of Use and Attribution on the landing page.
#### Scenario: Footer links visible on landing page
- **WHEN** a user loads the main page
- **THEN** the footer includes links labeled "Terms of Use" and "Attribution"
- **AND** links are visually distinguishable and keyboard focusable
#### Scenario: Footer links navigate correctly
- **WHEN** a user activates either policy link
- **THEN** the browser navigates to the corresponding policy page
- **AND** navigation succeeds without API dependency
#### Scenario: Footer includes back-to-top action
- **WHEN** a user reaches lower sections of the page
- **THEN** footer exposes a "Back to Top" control
- **AND** activating it returns viewport to page top smoothly
#### Scenario: Footer includes optional GitHub and email links
- **WHEN** GitHub repository URL and/or contact email are configured
- **THEN** footer renders corresponding links without replacing policy links
- **AND** absent values do not break footer layout

View File

@@ -0,0 +1,32 @@
## MODIFIED Requirements
### Requirement: API supports language-aware content retrieval
The system SHALL support language-aware content delivery for hero and feed reads using selected language input.
#### Scenario: Language-specific latest article response
- **WHEN** a client requests latest article data with a supported language selection
- **THEN** the system returns headline and summary in the selected language when available
- **AND** includes the corresponding base article metadata and media attribution
#### Scenario: Language-specific paginated feed response
- **WHEN** a client requests paginated feed data with a supported language selection
- **THEN** the system returns each feed item's headline and summary in the selected language when available
- **AND** preserves existing pagination behavior and ordering semantics
#### Scenario: Tamil and Malayalam rendering quality support
- **WHEN** Tamil (`ta`) or Malayalam (`ml`) content is delivered to frontend surfaces
- **THEN** payload text preserves script fidelity and Unicode correctness
- **AND** frontend presentation hooks can apply readability-focused typography adjustments without changing response shape
### Requirement: Language fallback to English is deterministic
The system SHALL return English source content when the requested translation is unavailable.
#### Scenario: Missing translation fallback
- **WHEN** a client requests Tamil or Malayalam content for an article lacking that translation
- **THEN** the system returns the English headline and summary for that article
- **AND** response shape remains consistent with language-aware responses
#### Scenario: Unsupported language handling
- **WHEN** a client requests a language outside supported values (`en`, `ta`, `ml`)
- **THEN** the system applies the defined default language behavior for this phase
- **AND** avoids breaking existing consumers of news endpoints

View File

@@ -0,0 +1,19 @@
## MODIFIED Requirements
### Requirement: Core layout is device-agnostic and responsive
The system SHALL render key surfaces (header, hero, feed, modal, footer) responsively across mobile, tablet, and desktop viewports.
#### Scenario: Mobile layout behavior
- **WHEN** a user opens the site on a mobile viewport
- **THEN** content remains readable without horizontal overflow
- **AND** interactive controls remain reachable and usable
#### Scenario: Desktop and tablet adaptation
- **WHEN** a user opens the site on tablet or desktop viewports
- **THEN** layout reflows according to breakpoint design rules
- **AND** no key content or controls are clipped
#### Scenario: Readability-focused typography and contrast updates
- **WHEN** content is rendered in core reading surfaces
- **THEN** typography and color choices improve baseline readability
- **AND** updates remain compatible with responsive behavior across breakpoints

View File

@@ -0,0 +1,19 @@
## MODIFIED Requirements
### Requirement: Core SEO metadata is present on public pages
The system SHALL expose standards-compliant SEO metadata on the homepage and policy pages, including description, robots, canonical URL, and social preview metadata.
#### Scenario: Homepage metadata baseline exists
- **WHEN** a crawler or browser loads the homepage
- **THEN** the document includes `description`, `robots`, and canonical metadata
- **AND** Open Graph and Twitter card metadata fields are present with non-empty values
#### Scenario: Policy pages include indexable metadata
- **WHEN** a crawler loads `/terms` or `/attribution`
- **THEN** the page includes page-specific `title` and `description` metadata
- **AND** Open Graph and Twitter card metadata are present for link previews
#### Scenario: Homepage title remains stable and brand-oriented
- **WHEN** homepage content updates to newer articles
- **THEN** document title remains a stable brand title (for example `ClawFort AI News`)
- **AND** title does not switch to latest-article headline text

View File

@@ -0,0 +1,28 @@
## ADDED Requirements
### Requirement: Modal/footer exposes minimal icon-based share actions
The system SHALL provide icon-only social share actions for article permalinks on supported providers.
#### Scenario: Supported share providers
- **WHEN** share controls are rendered
- **THEN** icons for `X`, `WhatsApp`, and `LinkedIn` are displayed
- **AND** activating an icon opens provider share flow with the article permalink
### Requirement: Footer supports env-driven GitHub and contact links
The system SHALL conditionally render GitHub and contact-email links from environment-backed configuration.
#### Scenario: Config present
- **WHEN** GitHub URL and contact email are configured
- **THEN** footer renders both links as interactive controls
#### Scenario: Config absent
- **WHEN** either value is missing
- **THEN** corresponding footer control is hidden without breaking layout
### Requirement: Contact affordance provides randomized safe microcopy
The contact link SHALL present randomized, policy-safe helper messages to encourage feedback.
#### Scenario: Randomized helper tooltip
- **WHEN** user hovers or nears the contact affordance
- **THEN** a tooltip-style helper message is shown from a predefined safe message pool
- **AND** message language avoids profanity/offensive/racist/sexist/misogynistic content

View File

@@ -0,0 +1,13 @@
## ADDED Requirements
### Requirement: Site includes AI-themed favicon
The system SHALL include an AI-themed favicon asset and reference it from public pages.
#### Scenario: Favicon linked in document head
- **WHEN** a public page is loaded
- **THEN** a favicon link element resolves to the configured AI-themed icon asset
#### Scenario: Favicon asset is cacheable and valid
- **WHEN** browser requests favicon asset
- **THEN** asset responds successfully with a valid icon format
- **AND** can be cached using standard static-asset policy

View File

@@ -0,0 +1,30 @@
## MODIFIED Requirements
### Requirement: Summary is rendered in a modal dialog using standard template
The system SHALL render article summary content in a modal dialog using the required structure.
#### Scenario: Open summary modal
- **WHEN** a user triggers summary view for an article
- **THEN** a modal dialog opens and displays content in this order: relevant image, TL;DR bullets, summary body, source and citation, and "Powered by Perplexity"
- **AND** modal content corresponds to the selected article
#### Scenario: Close summary modal
- **WHEN** a user closes the modal via close control or backdrop interaction
- **THEN** the modal is dismissed cleanly
- **AND** user returns to previous feed context without page navigation
#### Scenario: Permalink-driven modal open
- **WHEN** page is loaded with a valid article permalink state
- **THEN** the modal opens for the linked article without requiring manual click
### Requirement: Modal content preserves source link-out behavior
The system SHALL provide source link-outs from the summary modal.
#### Scenario: Source link-out from modal
- **WHEN** a user clicks source/citation link in the modal
- **THEN** the original source opens in a new tab/window
- **AND** modal behavior remains stable for continued browsing
#### Scenario: Modal exposes share entry points
- **WHEN** a summary modal is open for an article
- **THEN** share controls for the article permalink are available from modal/footer share area

View File

@@ -0,0 +1,41 @@
## 1. Homepage Metadata and Navigation
- [x] 1.1 Set homepage title to stable brand title (e.g., `ClawFort AI News`) and prevent latest-article overrides.
- [x] 1.2 Add footer `Back to Top` control with smooth scroll behavior.
## 2. Permalinks and Deep-Link Modal
- [x] 2.1 Add per-article permalink generation for hero and feed items.
- [x] 2.2 Add URL parsing on load to detect permalink article target.
- [x] 2.3 Open matching article modal automatically when permalink target is valid.
- [x] 2.4 Handle invalid/missing permalink targets safely without UI breakage.
## 3. Sharing and Footer Contact UX
- [x] 3.1 Add icon-only share actions for `X`, `WhatsApp`, and `LinkedIn` using article permalinks.
- [x] 3.2 Add env-driven footer GitHub link (`GITHUB_REPO_URL`) and contact email link (`CONTACT_EMAIL`).
- [x] 3.3 Implement randomized safe tooltip microcopy pool (~50 messages) for contact affordance.
## 4. Readability Improvements
- [x] 4.1 Apply minor typography and color-contrast refinements across reading surfaces.
- [x] 4.2 Add targeted Tamil/Malayalam readability tuning (font size/line-height/weight adjustments as needed).
- [x] 4.3 Verify updates preserve responsive behavior across mobile/tablet/desktop.
## 5. Error Experience
- [x] 5.1 Implement custom 404 page with prominent status code and safe playful "Oh no!" message.
- [x] 5.2 Implement custom 500 page with prominent status code and safe playful "Oh no!" message.
- [x] 5.3 Add curated safe message set and deterministic/randomized selection strategy.
## 6. Branding Asset
- [x] 6.1 Add AI-themed favicon asset to static files with proper licensing/attribution notes if required.
- [x] 6.2 Wire favicon reference in public page heads.
## 7. Validation
- [x] 7.1 Verify permalink deep-link opens correct modal and share links include correct URL.
- [x] 7.2 Verify footer controls (policy links, back-to-top, GitHub, email) across breakpoints.
- [x] 7.3 Verify Tamil/Malayalam readability changes in actual rendered content.
- [x] 7.4 Verify 404/500 pages render status code + safe playful message.

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-13

View File

@@ -0,0 +1,34 @@
## Why
Several regressions and UX defects slipped in after recent enhancements, affecting modal behavior, sharing visibility, footer/header usability, and image relevance. Fixing these together will restore trust, readability, and navigation quality without introducing new product scope.
## What Changes
- Fix permalink-to-hero modal behavior so image rendering and keyboard close (`Escape`) work consistently.
- Restore footer contact email visibility and ensure env-driven contact link rendering is reliable.
- Fix light-theme contrast for social share icons and add a copy-to-clipboard action alongside X, WhatsApp, and LinkedIn.
- Replace inline footer "Back to Top" text button with a floating island-style control.
- Make footer persistently sticky and compact while preserving reading comfort.
- Make header persistently sticky with subtle shrink, elevation, and glass effect on scroll.
- Tighten article-image relevance to avoid clearly unrelated images for finance/market stories.
- Remove unnecessary per-card "Link" affordance from feed cards.
## Capabilities
### New Capabilities
- None.
### Modified Capabilities
- `article-permalinks-and-deep-link-modal`: Correct deep-link modal open-state parity for hero/feed targets and keyboard close behavior.
- `share-and-contact-microinteractions`: Fix share-icon visibility in light mode, add copy-link action, and restore contact-email presence.
- `footer-policy-links`: Update footer interaction model for sticky compact layout and floating back-to-top control.
- `responsive-device-agnostic-layout`: Apply sticky header/footer behavior and ensure readability is not degraded.
- `news-image-relevance-and-fallbacks`: Improve relevance guardrails to reduce obviously mismatched fallback/provider image outcomes.
- `hero-display`: Remove redundant card-level permalink text affordance where it harms clarity.
## Impact
- **Frontend UI:** `frontend/index.html` modal behaviors, keyboard handling, share controls, footer/header interactions, and card affordances.
- **Backend config/data:** contact link exposure verification through `/config` payload and env handling.
- **Image pipeline:** backend relevance and fallback heuristics in image selection paths.
- **UX quality:** stronger consistency across themes and navigation paths, especially for permalink and sharing flows.

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-02-13

View File

@@ -0,0 +1,36 @@
## Why
The platform needs stronger quality gates to ensure stable, predictable behavior across releases. A complete testing and review program will reduce regressions, improve confidence in deployments, and surface performance, accessibility, and security risks earlier.
## What Changes
- Introduce a unified automated test suite strategy covering unit, integration, and end-to-end paths.
- Add end-to-end test coverage for core UI flows, API contracts, and database state transitions.
- Add WCAG-focused accessibility checks and include them in quality gates.
- Add page speed and runtime performance checks with repeatable thresholds.
- Add baseline security testing (dependency, config, and common web vulnerability checks).
- Add user-experience validation scenarios for key journeys and failure states.
- Define comprehensive coverage expectations for critical features and edge cases.
- Add a structured code-review/remediation/optimization pass to resolve quality debt.
- Add performance monitoring and alerting requirements for production health visibility.
## Capabilities
### New Capabilities
- `platform-quality-gates`: Defines required CI quality gates and pass/fail criteria for release readiness.
- `end-to-end-system-testing`: Defines end-to-end testing coverage across UI, API, and database workflows.
- `security-and-performance-test-harness`: Defines security checks and page/runtime performance testing strategy.
- `observability-monitoring-and-alerting`: Defines performance monitoring signals, dashboards, and alerting thresholds.
- `code-review-remediation-workflow`: Defines structured remediation and optimization workflow after comprehensive review.
### Modified Capabilities
- `wcag-2-2-aa-accessibility`: Expand verification requirements to include automated accessibility testing in release gates.
- `delivery-and-rendering-performance`: Add enforceable page speed benchmarks and regression thresholds.
- `site-admin-safety-and-ergonomics`: Add operational verification requirements tied to maintenance command behaviors.
## Impact
- **Testing/Tooling:** New test suites, fixtures, and CI workflows for UI/API/DB/accessibility/security/performance.
- **Frontend/Backend:** Potential bug fixes and optimizations discovered during comprehensive test and review passes.
- **Operations:** Monitoring/alerting setup and documentation for performance and reliability signals.
- **Release Process:** Stronger quality gates before archive/release actions.