First deployment
Some checks failed
quality-gates / lint-and-test (push) Has been cancelled
quality-gates / security-scan (push) Has been cancelled

This commit is contained in:
2026-02-13 09:14:04 -05:00
parent 0e21e035f5
commit 679561bcdb
128 changed files with 3479 additions and 120 deletions

4
.env
View File

@@ -10,4 +10,6 @@ ROYALTY_IMAGE_PROVIDERS=pixabay,unsplash,pexels,wikimedia,picsum
PIXABAY_API_KEY=54637577-dbef68c927eec6553190fa4dc PIXABAY_API_KEY=54637577-dbef68c927eec6553190fa4dc
UNSPLASH_ACCESS_KEY= UNSPLASH_ACCESS_KEY=
PEXELS_API_KEY=fRdPmXg16nsz1pPe0Zmp02eALJkhAz4sG7g4RN56Q3J90Qi6qV3Qvuz8 PEXELS_API_KEY=fRdPmXg16nsz1pPe0Zmp02eALJkhAz4sG7g4RN56Q3J90Qi6qV3Qvuz8
SUMMARY_LENGTH_SCALE=3 SUMMARY_LENGTH_SCALE=3
GITHUB_REPO_URL=https://github.com/santhoshjanan
CONTACT_EMAIL=santhoshj@gmail.com

37
.github/workflows/quality-gates.yml vendored Normal file
View File

@@ -0,0 +1,37 @@
name: quality-gates
on:
pull_request:
push:
branches: [main]
jobs:
lint-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install project dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[dev]
- name: Ruff lint
run: python -m ruff check backend tests
- name: Pytest coverage and contracts
run: python -m pytest
security-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install scanner
run: |
python -m pip install --upgrade pip
pip install pip-audit
- name: Dependency vulnerability scan
run: pip-audit

View File

@@ -0,0 +1,5 @@
## 2026-02-13
- Applied chronological conflict resolution order exactly as requested: p12 -> p13 -> p14 -> p15 -> p16, with p16 language/behavior taking precedence on overlapping requirement text.
- Created new capability specs in canonical form for all delta-marked additions missing from `openspec/specs/*`.
- Kept pre-existing requirements/scenarios unless superseded by explicit newer behavioral changes (e.g., footer policy navigation replaced with modal disclosure behavior).

View File

@@ -0,0 +1,3 @@
## 2026-02-13
- Markdown LSP diagnostics are unavailable in this workspace (`No LSP server configured for extension: .md`), so spec-file verification relied on structural diff review instead of LSP validation.

View File

@@ -0,0 +1,5 @@
## 2026-02-13
- Canonical OpenSpec sync files follow a stable template: `## Purpose` + `## Requirements`; preserving this structure keeps delta-to-main merges consistent.
- Conflict-heavy capability merges are safest when newest delta text updates requirement sentence semantics while older non-conflicting scenarios are retained.
- For policy/footer capabilities, behavior precedence can flip navigation contracts (page routes -> modals) while still preserving visibility and accessibility scenarios from older deltas.

View File

@@ -0,0 +1,3 @@
## 2026-02-13
- None unresolved in the p12-p16 spec merge scope.

View File

@@ -46,6 +46,20 @@ Exit codes:
- `0`: Command completed successfully (including runs that store zero new rows) - `0`: Command completed successfully (including runs that store zero new rows)
- `1`: Fatal command failure (for example missing API keys or unrecoverable runtime error) - `1`: Fatal command failure (for example missing API keys or unrecoverable runtime error)
## Quality and Test Suite
Run local quality gates:
```bash
pip install -e .[dev]
pytest
ruff check backend tests
```
CI quality gates are defined in `.github/workflows/quality-gates.yml`.
Monitoring baseline, thresholds, and alert runbook are documented in `docs/quality-and-monitoring.md`.
## Admin Maintenance Commands ## Admin Maintenance Commands
ClawFort includes an admin command suite to simplify operational recovery and maintenance. ClawFort includes an admin command suite to simplify operational recovery and maintenance.

View File

@@ -14,6 +14,7 @@ from backend import config
from backend.database import SessionLocal, init_db from backend.database import SessionLocal, init_db
from backend.models import NewsItem from backend.models import NewsItem
from backend.news_service import ( from backend.news_service import (
GENERIC_AI_FALLBACK_URL,
download_and_optimize_image, download_and_optimize_image,
extract_image_keywords, extract_image_keywords,
fetch_royalty_free_image, fetch_royalty_free_image,
@@ -87,56 +88,105 @@ def build_contextual_query(headline: str, summary: str | None) -> str:
return cleaned return cleaned
def resolve_article_id_from_permalink(value: str | None) -> int | None:
if not value:
return None
if value.isdigit():
return int(value)
match = re.search(r"(?:\?|&)article=(\d+)", value)
if match:
return int(match.group(1))
return None
def is_unrelated_image_candidate(image_url: str | None, image_credit: str | None) -> bool:
text = f"{image_url or ''} {image_credit or ''}".lower()
blocked = (
"cat",
"dog",
"pet",
"animal",
"wildlife",
"lion",
"tiger",
"bird",
"horse",
)
return any(term in text for term in blocked)
async def refetch_image_for_item(
item: NewsItem,
max_attempts: int,
) -> tuple[str | None, str | None, str]:
query = build_contextual_query(item.headline, item.summary)
current_summary_image = item.summary_image_url
query_variants = [
f"{query} alternative angle",
f"{query} concept illustration",
query,
]
for query_variant in query_variants:
for attempt in range(max_attempts):
try:
image_url, image_credit = await fetch_royalty_free_image(query_variant)
if not image_url:
raise RuntimeError("no-image-url")
if is_unrelated_image_candidate(image_url, image_credit):
logger.info("Rejected unrelated image candidate: %s", image_url)
continue
local_image = await download_and_optimize_image(image_url)
if not local_image:
raise RuntimeError("image-download-or-optimize-failed")
if current_summary_image and local_image == current_summary_image:
logger.info("Rejected duplicate image candidate for article=%s", item.id)
continue
return local_image, image_credit, "provider"
except Exception:
if attempt < max_attempts - 1:
delay = 2**attempt
await asyncio.sleep(delay)
fallback_local = await download_and_optimize_image(GENERIC_AI_FALLBACK_URL)
if fallback_local and fallback_local != current_summary_image:
return fallback_local, "AI-themed fallback", "fallback"
return None, None, "none"
async def refetch_images_for_latest( async def refetch_images_for_latest(
limit: int, limit: int,
max_attempts: int, max_attempts: int,
dry_run: bool, dry_run: bool,
target_article_id: int | None = None,
) -> tuple[int, int]: ) -> tuple[int, int]:
db = SessionLocal() db = SessionLocal()
processed = 0 processed = 0
refreshed = 0 refreshed = 0
try: try:
items = ( if target_article_id is not None:
db.query(NewsItem) items = (
.filter(NewsItem.archived.is_(False)) db.query(NewsItem)
.order_by(desc(NewsItem.published_at)) .filter(NewsItem.archived.is_(False), NewsItem.id == target_article_id)
.limit(limit) .all()
.all() )
) else:
items = (
db.query(NewsItem)
.filter(NewsItem.archived.is_(False))
.order_by(desc(NewsItem.published_at))
.limit(limit)
.all()
)
total = len(items) total = len(items)
for idx, item in enumerate(items, start=1): for idx, item in enumerate(items, start=1):
processed += 1 processed += 1
query = build_contextual_query(item.headline, item.summary) local_image, image_credit, decision = await refetch_image_for_item(
item=item,
image_url: str | None = None max_attempts=max_attempts,
image_credit: str | None = None )
local_image: str | None = None
for attempt in range(max_attempts):
try:
image_url, image_credit = await fetch_royalty_free_image(query)
if not image_url:
raise RuntimeError("no-image-url")
local_image = await download_and_optimize_image(image_url)
if not local_image:
raise RuntimeError("image-download-or-optimize-failed")
break
except Exception:
if attempt == max_attempts - 1:
logger.exception("Image refetch failed for item=%s after retries", item.id)
image_url = None
local_image = None
break
delay = 2**attempt
logger.warning(
"Refetch retry item=%s attempt=%d delay=%ds",
item.id,
attempt + 1,
delay,
)
await asyncio.sleep(delay)
if local_image: if local_image:
refreshed += 1 refreshed += 1
@@ -152,6 +202,7 @@ async def refetch_images_for_latest(
total=total, total=total,
refreshed=refreshed, refreshed=refreshed,
article_id=item.id, article_id=item.id,
decision=decision,
) )
return processed, refreshed return processed, refreshed
@@ -186,6 +237,12 @@ def build_parser() -> argparse.ArgumentParser:
help="Refetch and optimize latest article images", help="Refetch and optimize latest article images",
) )
refetch_parser.add_argument("--limit", type=positive_int, default=30) refetch_parser.add_argument("--limit", type=positive_int, default=30)
refetch_parser.add_argument(
"--permalink",
type=str,
default="",
help="Target one article by permalink (for example '/?article=123' or '123')",
)
refetch_parser.add_argument("--max-attempts", type=positive_int, default=4) refetch_parser.add_argument("--max-attempts", type=positive_int, default=4)
refetch_parser.add_argument("--dry-run", action="store_true") refetch_parser.add_argument("--dry-run", action="store_true")
refetch_parser.set_defaults(handler=handle_admin_refetch_images) refetch_parser.set_defaults(handler=handle_admin_refetch_images)
@@ -280,11 +337,22 @@ def handle_admin_refetch_images(args: argparse.Namespace) -> int:
start = time.monotonic() start = time.monotonic()
try: try:
init_db() init_db()
target_article_id = resolve_article_id_from_permalink(args.permalink)
if args.permalink and target_article_id is None:
print_result(
"refetch-images",
"blocked",
reason="invalid-permalink",
hint="use '/?article=<id>' or raw numeric id",
)
return 2
processed, refreshed = asyncio.run( processed, refreshed = asyncio.run(
refetch_images_for_latest( refetch_images_for_latest(
limit=min(args.limit, 30), limit=min(args.limit, 30),
max_attempts=args.max_attempts, max_attempts=args.max_attempts,
dry_run=args.dry_run, dry_run=args.dry_run,
target_article_id=target_article_id,
) )
) )
elapsed = time.monotonic() - start elapsed = time.monotonic() - start
@@ -293,6 +361,7 @@ def handle_admin_refetch_images(args: argparse.Namespace) -> int:
"ok", "ok",
processed=processed, processed=processed,
refreshed=refreshed, refreshed=refreshed,
target_article_id=target_article_id,
dry_run=args.dry_run, dry_run=args.dry_run,
elapsed=f"{elapsed:.1f}s", elapsed=f"{elapsed:.1f}s",
) )

View File

@@ -37,18 +37,18 @@ app = FastAPI(title="ClawFort News API", version="0.1.0")
_ERROR_MESSAGES = { _ERROR_MESSAGES = {
404: [ 404: [
"Oh no! This page wandered off to train a tiny model.", "This page wandered off to train a tiny model.",
"Oh no! We looked everywhere, even in the latent space.", "We looked everywhere, even in the latent space.",
"Oh no! The link took a creative detour.", "The link took a creative detour.",
"Oh no! This route is currently off doing research.", "This route is currently off doing research.",
"Oh no! The page you asked for is not in this timeline.", "The page you asked for is not in this timeline.",
], ],
500: [ 500: [
"Oh no! The server hit a logic knot and needs a quick reset.", "The server hit a logic knot and needs a quick reset.",
"Oh no! Our robots dropped a semicolon somewhere important.", "Our robots dropped a semicolon somewhere important.",
"Oh no! A background process got stage fright.", "A background process got stage fright.",
"Oh no! The AI took an unexpected coffee break.", "The AI took an unexpected coffee break.",
"Oh no! Something internal blinked at the wrong moment.", "Something internal blinked at the wrong moment.",
], ],
} }

View File

@@ -25,6 +25,49 @@ logger = logging.getLogger(__name__)
PLACEHOLDER_IMAGE_PATH = "/static/images/placeholder.png" PLACEHOLDER_IMAGE_PATH = "/static/images/placeholder.png"
GENERIC_AI_FALLBACK_URL = "https://placehold.co/1200x630/0f172a/e2e8f0/png?text=AI+News" GENERIC_AI_FALLBACK_URL = "https://placehold.co/1200x630/0f172a/e2e8f0/png?text=AI+News"
GENERIC_FINANCE_FALLBACK_URL = "https://placehold.co/1200x630/0f172a/e2e8f0/png?text=Market+News"
_FINANCE_TOPIC_TERMS = frozenset(
{
"finance",
"financial",
"market",
"markets",
"stock",
"stocks",
"share",
"shares",
"earnings",
"investor",
"investors",
"nasdaq",
"nyse",
"dow",
"s&p",
"bank",
"banking",
"revenue",
"profit",
"trading",
"ipo",
"valuation",
}
)
_FINANCE_IMAGE_BLOCKLIST = (
"cat",
"dog",
"pet",
"lion",
"tiger",
"bird",
"horse",
"portrait",
"selfie",
"wedding",
"food",
"nature-only",
)
async def call_perplexity_api(query: str) -> dict | None: async def call_perplexity_api(query: str) -> dict | None:
@@ -174,6 +217,43 @@ def parse_translation_response(response: dict) -> dict | None:
return None return None
def validate_translation_quality(
headline: str, summary: str, language_code: str
) -> tuple[bool, str | None]:
text = f"{headline} {summary}".strip()
if not headline or not summary:
return False, "empty-content"
if len(text) < 20:
return False, "too-short"
repeated_runs = re.search(r"(.)\1{6,}", text)
if repeated_runs:
return False, "repeated-sequence"
lines = [segment.strip() for segment in re.split(r"[.!?]\s+", text) if segment.strip()]
if lines:
unique_ratio = len(set(lines)) / len(lines)
if unique_ratio < 0.4:
return False, "low-unique-content"
if language_code == "ta":
script_hits = sum(1 for char in text if "\u0b80" <= char <= "\u0bff")
elif language_code == "ml":
script_hits = sum(1 for char in text if "\u0d00" <= char <= "\u0d7f")
else:
return True, None
alpha_hits = sum(1 for char in text if char.isalpha())
if alpha_hits == 0:
return False, "no-alpha-content"
script_ratio = script_hits / alpha_hits
if script_ratio < 0.35:
return False, "script-mismatch"
return True, None
async def generate_translations( async def generate_translations(
headline: str, headline: str,
summary: str, summary: str,
@@ -200,7 +280,20 @@ async def generate_translations(
if response: if response:
parsed = parse_translation_response(response) parsed = parse_translation_response(response)
if parsed: if parsed:
translations[language_code] = parsed is_valid, reason = validate_translation_quality(
parsed["headline"],
parsed["summary"],
language_code,
)
if is_valid:
logger.info("Translation accepted for %s", language_code)
translations[language_code] = parsed
else:
logger.warning(
"Translation rejected for %s: %s",
language_code,
reason,
)
except Exception: except Exception:
logger.exception("Translation generation failed for %s", language_code) logger.exception("Translation generation failed for %s", language_code)
@@ -467,7 +560,7 @@ async def fetch_pixabay_image(query: str) -> tuple[str | None, str | None]:
except Exception: except Exception:
logger.exception("Pixabay image retrieval failed") logger.exception("Pixabay image retrieval failed")
return GENERIC_AI_FALLBACK_URL, "Generic AI fallback" return None, None
async def fetch_unsplash_image(query: str) -> tuple[str | None, str | None]: async def fetch_unsplash_image(query: str) -> tuple[str | None, str | None]:
@@ -591,6 +684,15 @@ def get_enabled_providers() -> list[
async def fetch_royalty_free_image(query: str) -> tuple[str | None, str | None]: async def fetch_royalty_free_image(query: str) -> tuple[str | None, str | None]:
"""Fetch royalty-free image using provider chain with fallback.""" """Fetch royalty-free image using provider chain with fallback."""
def is_finance_story(text: str) -> bool:
lowered = (text or "").lower()
return any(term in lowered for term in _FINANCE_TOPIC_TERMS)
def is_finance_safe_image(image_url: str, credit: str | None) -> bool:
haystack = f"{image_url or ''} {credit or ''}".lower()
return not any(term in haystack for term in _FINANCE_IMAGE_BLOCKLIST)
# MCP endpoint takes highest priority if configured # MCP endpoint takes highest priority if configured
if config.ROYALTY_IMAGE_MCP_ENDPOINT: if config.ROYALTY_IMAGE_MCP_ENDPOINT:
try: try:
@@ -610,15 +712,35 @@ async def fetch_royalty_free_image(query: str) -> tuple[str | None, str | None]:
# Extract keywords for better image search # Extract keywords for better image search
refined_query = extract_image_keywords(query) refined_query = extract_image_keywords(query)
finance_story = is_finance_story(query)
query_variants = [refined_query]
if finance_story:
query_variants = [
f"{refined_query} stock market trading chart finance business",
refined_query,
]
# Try each enabled provider in order # Try each enabled provider in order
for provider_name, fetch_fn in get_enabled_providers(): for query_variant in query_variants:
try: for provider_name, fetch_fn in get_enabled_providers():
image_url, credit = await fetch_fn(refined_query) try:
if image_url: image_url, credit = await fetch_fn(query_variant)
if not image_url:
continue
if finance_story and not is_finance_safe_image(image_url, credit):
logger.info(
"Rejected non-finance-safe image from %s for query '%s': %s",
provider_name,
query_variant,
image_url,
)
continue
return image_url, credit return image_url, credit
except Exception: except Exception:
logger.exception("%s image retrieval failed", provider_name.capitalize()) logger.exception("%s image retrieval failed", provider_name.capitalize())
if finance_story:
return GENERIC_FINANCE_FALLBACK_URL, "Finance-safe fallback"
return None, None return None, None

View File

@@ -0,0 +1,26 @@
# Monitoring Dashboard Configuration
## Objective
Define baseline dashboards and alert thresholds for reliability and freshness checks.
## Dashboard Panels
1. API p95 latency for `/api/news` and `/api/news/latest`
2. API error rate (`5xx`) by route
3. Scheduler success/failure count per hour
4. Feed freshness lag (minutes since latest published item)
## Alert Thresholds
- API latency alert: p95 > 750 ms for 10 minutes
- API error-rate alert: `5xx` > 3% for 5 minutes
- Scheduler alert: 2 consecutive failed fetch cycles
- Freshness alert: latest item older than 120 minutes
## Test Trigger Plan
- Latency trigger: run stress test against `/api/news` with 50 concurrent requests in staging.
- Error-rate trigger: simulate upstream timeout and confirm 5xx alert path.
- Scheduler trigger: disable upstream API key in staging and verify consecutive failure alert.
- Freshness trigger: pause scheduler for >120 minutes in staging and confirm lag alert.

View File

@@ -0,0 +1,23 @@
# P15 Code Review Findings
Date: 2026-02-13
## High
- owner=backend area=translations finding=Machine translation output is accepted without strict language validation in runtime flow, allowing occasional script mismatch/gibberish.
## Medium
- owner=frontend area=policy-disclosures finding=Terms and Attribution links previously required route navigation, reducing continuity and causing context loss.
- owner=backend area=admin-cli finding=Image refetch previously lacked permalink-targeted repair mode, forcing broad batch operations.
## Low
- owner=frontend area=sharing finding=Text-based icon actions in compact surfaces reduced visual consistency on small screens.
## Remediation Status
- translations-quality-gate: fixed-in-progress
- policy-modal-surface: fixed-in-progress
- permalink-targeted-refetch: fixed-in-progress
- icon-consistency: fixed-in-progress

View File

@@ -0,0 +1,64 @@
# Quality and Monitoring Baseline
## CI Quality Gates
Pipeline file: `.github/workflows/quality-gates.yml`
Stages:
- `lint-and-test`: Ruff + pytest (coverage threshold enforced).
- `security-scan`: `pip-audit` dependency vulnerability scan.
Failure policy:
- Any failed stage blocks merge.
- Coverage floor below threshold blocks merge.
## Coverage and Test Scope
Current baseline suites:
- API contracts: `tests/test_api_contracts.py`
- DB lifecycle workflows: `tests/test_db_workflows.py`
- Accessibility contracts: `tests/test_accessibility_contract.py`
- Security/performance smoke checks: `tests/test_security_and_performance.py`
## UX Validation Checklist
Run manually on desktop + mobile viewport:
1. Hero loads with image and CTA visible.
2. Feed cards render with source and TL;DR CTA.
3. Modal opens/closes with Escape and backdrop click.
4. Share controls are visible in light and dark themes.
5. Floating back-to-top appears after scrolling and returns to top.
## Production Metrics and Alert Thresholds
| Metric | Target | Alert Threshold |
|---|---|---|
| API p95 latency (`/api/news`) | < 350 ms | > 750 ms for 10 min |
| API error rate (`5xx`) | < 1% | > 3% for 5 min |
| Scheduler success rate | 100% hourly runs | 2 consecutive failures |
| Feed freshness lag | < 75 min | > 120 min |
## Alert Runbook
### Incident: Elevated API latency
1. Confirm DB file I/O and host CPU saturation.
2. Inspect recent release diff for expensive queries.
3. Roll back latest deploy if regression is confirmed.
### Incident: Scheduler failures
1. Check API key and upstream provider status.
2. Run `python -m backend.cli force-fetch` for repro.
3. Review logs for provider fallback exhaustion.
### Incident: Error-rate spike
1. Check `/api/health` response and DB availability.
2. Identify top failing routes and common status codes.
3. Mitigate with rollback or feature flag disablement.
## Review/Remediation Log Template
Use this structure for each cycle:
```text
severity=<high|medium|low> owner=<name> area=<frontend|backend|infra> finding=<summary> status=<open|fixed>
```

View File

@@ -103,8 +103,13 @@
--cf-select-text: #ffffff; --cf-select-text: #ffffff;
--cf-select-border: rgba(255, 255, 255, 0.55); --cf-select-border: rgba(255, 255, 255, 0.55);
} }
.cf-body { background: var(--cf-bg); color: var(--cf-text); } .cf-body { background: var(--cf-bg); color: var(--cf-text); padding-bottom: 78px; }
.cf-header { background: var(--cf-header-bg); } .cf-header { background: var(--cf-header-bg); }
.cf-header-top { box-shadow: none; border-color: rgba(148, 163, 184, 0.12); }
.cf-header-scrolled {
box-shadow: 0 10px 28px rgba(2, 6, 23, 0.24);
border-color: rgba(148, 163, 184, 0.28);
}
.cf-card { background: var(--cf-card-bg) !important; } .cf-card { background: var(--cf-card-bg) !important; }
.cf-modal { background: var(--cf-modal-bg); } .cf-modal { background: var(--cf-modal-bg); }
.cf-select { .cf-select {
@@ -129,7 +134,7 @@
} }
.theme-menu-item:hover { background: rgba(92, 124, 250, 0.15); } .theme-menu-item:hover { background: rgba(92, 124, 250, 0.15); }
.hero-overlay { .hero-overlay {
background: linear-gradient(to top, rgba(2, 6, 23, 0.94), rgba(15, 23, 42, 0.62), rgba(15, 23, 42, 0.22), transparent); background: linear-gradient(to top, rgba(2, 6, 23, 0.97), rgba(15, 23, 42, 0.75), rgba(15, 23, 42, 0.35), transparent);
} }
.hero-title { color: #e2e8f0; text-shadow: 0 2px 8px rgba(0, 0, 0, 0.55); } .hero-title { color: #e2e8f0; text-shadow: 0 2px 8px rgba(0, 0, 0, 0.55); }
.hero-summary { color: #cbd5e1; text-shadow: 0 1px 6px rgba(0, 0, 0, 0.55); } .hero-summary { color: #cbd5e1; text-shadow: 0 1px 6px rgba(0, 0, 0, 0.55); }
@@ -221,6 +226,14 @@
line-height: 1.78; line-height: 1.78;
letter-spacing: 0.01em; letter-spacing: 0.01em;
} }
html[data-lang='ta'] .hero-title,
html[data-lang='ml'] .hero-title {
text-shadow: 0 3px 12px rgba(0, 0, 0, 0.75);
}
html[data-lang='ta'] .hero-summary,
html[data-lang='ml'] .hero-summary {
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.7);
}
.share-icon-btn { .share-icon-btn {
width: 34px; width: 34px;
height: 34px; height: 34px;
@@ -234,8 +247,42 @@
transition: background 180ms ease; transition: background 180ms ease;
} }
.share-icon-btn:hover { background: rgba(92, 124, 250, 0.25); } .share-icon-btn:hover { background: rgba(92, 124, 250, 0.25); }
html[data-theme='light'] .share-icon-btn {
color: #1d4ed8;
border-color: rgba(37, 99, 235, 0.45);
background: rgba(59, 130, 246, 0.12);
}
html[data-theme='light'] .share-icon-btn:hover {
background: rgba(59, 130, 246, 0.2);
}
.footer-link { text-decoration: underline; text-underline-offset: 2px; } .footer-link { text-decoration: underline; text-underline-offset: 2px; }
.footer-link:hover { color: #dbeafe; } .footer-link:hover { color: #dbeafe; }
.site-footer {
background: color-mix(in srgb, var(--cf-bg) 92%, transparent);
backdrop-filter: blur(10px);
}
.back-to-top-island {
position: fixed;
right: 14px;
bottom: 88px;
z-index: 55;
border-radius: 9999px;
border: 1px solid rgba(148, 163, 184, 0.35);
background: rgba(15, 23, 42, 0.88);
color: #dbeafe;
min-width: 44px;
min-height: 44px;
padding: 0 13px;
font-size: 12px;
font-weight: 700;
line-height: 1;
box-shadow: 0 12px 24px rgba(2, 6, 23, 0.35);
}
html[data-theme='light'] .back-to-top-island {
background: rgba(248, 250, 252, 0.96);
color: #1e3a8a;
border-color: rgba(37, 99, 235, 0.35);
}
.contact-hint { .contact-hint {
position: fixed; position: fixed;
z-index: 60; z-index: 60;
@@ -256,14 +303,31 @@
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.theme-btn { width: 26px; height: 26px; } .theme-btn { width: 26px; height: 26px; }
.cf-body { padding-bottom: 84px; }
html[data-lang='ta'] .hero-title,
html[data-lang='ml'] .hero-title {
font-size: 1.9rem;
line-height: 1.26;
letter-spacing: 0.01em;
}
html[data-lang='ta'] .hero-summary,
html[data-lang='ml'] .hero-summary {
font-size: 1.08rem;
line-height: 1.86;
letter-spacing: 0.015em;
}
.back-to-top-island {
right: 10px;
bottom: 82px;
}
} }
</style> </style>
</head> </head>
<body class="font-sans min-h-screen overflow-x-hidden cf-body"> <body class="font-sans min-h-screen overflow-x-hidden cf-body">
<a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 bg-cf-500 text-white px-3 py-2 rounded">Skip to content</a> <a href="#main-content" class="sr-only focus:not-sr-only focus:absolute focus:top-2 focus:left-2 bg-cf-500 text-white px-3 py-2 rounded">Skip to content</a>
<header class="sticky top-0 z-50 backdrop-blur-lg border-b border-white/5 cf-header"> <header x-data="headerFx()" x-init="init()" :class="scrolled ? 'cf-header-scrolled' : 'cf-header-top'" class="sticky top-0 z-50 backdrop-blur-lg border-b border-white/5 cf-header transition-all duration-300">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center justify-between"> <div :class="scrolled ? 'h-14' : 'h-16'" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-between transition-all duration-300">
<a href="/" class="flex items-center gap-2.5 group"> <a href="/" class="flex items-center gap-2.5 group">
<svg class="w-8 h-8 text-cf-500 group-hover:text-cf-400 transition-colors" viewBox="0 0 32 32" fill="none"> <svg class="w-8 h-8 text-cf-500 group-hover:text-cf-400 transition-colors" viewBox="0 0 32 32" fill="none">
<path d="M16 2L4 8v8c0 7.7 5.1 14.9 12 16 6.9-1.1 12-8.3 12-16V8L16 2z" fill="currentColor" fill-opacity="0.15" stroke="currentColor" stroke-width="1.5"/> <path d="M16 2L4 8v8c0 7.7 5.1 14.9 12 16 6.9-1.1 12-8.3 12-16V8L16 2z" fill="currentColor" fill-opacity="0.15" stroke="currentColor" stroke-width="1.5"/>
@@ -342,9 +406,6 @@
x-show="item.source_url"> x-show="item.source_url">
Via: <span x-text="extractDomain(item.source_url)" class="underline underline-offset-2"></span> Via: <span x-text="extractDomain(item.source_url)" class="underline underline-offset-2"></span>
</a> </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> <span x-show="item.image_credit" x-text="'Image: ' + item.image_credit"></span>
</div> </div>
</div> </div>
@@ -409,10 +470,7 @@
@click.stop="trackEvent('source-link-click')" @click.stop="trackEvent('source-link-click')"
x-show="item.source_url" x-show="item.source_url"
x-text="extractDomain(item.source_url)"></a> x-text="extractDomain(item.source_url)"></a>
<div class="flex items-center gap-2"> <span x-text="timeAgo(item.published_at)"></span>
<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> </div>
<button @click="trackEvent('feed-cta-click', { article_id: item.id, article_title: item.headline }); 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"> 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">
@@ -483,7 +541,13 @@
<a :href="shareLink('linkedin', modalItem)" target="_blank" rel="noopener" class="share-icon-btn" aria-label="Share on LinkedIn" title="Share on LinkedIn"> <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> <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> </a>
<button type="button" class="share-icon-btn" aria-label="Copy article link" title="Copy article link"
@click="copyShareLink(modalItem)">
<svg x-show="copyStatus !== 'copied'" viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true"><path fill="currentColor" d="M16 1H6a2 2 0 0 0-2 2v12h2V3h10V1zm3 4H10a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h9a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2zm0 16H10V7h9v14z"/></svg>
<svg x-show="copyStatus === 'copied'" viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true"><path fill="currentColor" d="M9 16.2 4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4z"/></svg>
</button>
</div> </div>
<p class="text-xs text-gray-400 mt-2" x-show="copyStatus === 'copied'">Permalink copied.</p>
</div> </div>
<p class="text-xs modal-powered">Powered by Perplexity</p> <p class="text-xs modal-powered">Powered by Perplexity</p>
@@ -508,16 +572,54 @@
<div class="fixed left-0 right-0 pointer-events-none" style="top:75%" x-data x-intersect:enter="trackDepth(75)"></div> <div class="fixed left-0 right-0 pointer-events-none" style="top:75%" x-data x-intersect:enter="trackDepth(75)"></div>
</main> </main>
<footer x-data="footerEnhancements()" x-init="init()" class="border-t border-white/5 py-8 text-center text-sm text-gray-500"> <section x-data="policyDisclosures()" x-init="init()">
<div class="max-w-7xl mx-auto px-4 space-y-2"> <div x-show="openType" x-cloak class="fixed inset-0 z-[60] flex items-center justify-center" @keydown.window="onKeydown($event)">
<div class="absolute inset-0 bg-black/70" @click="close()"></div>
<div role="dialog" aria-modal="true" :aria-label="openType === 'terms' ? 'Terms of Use' : 'Attribution'"
class="relative w-full sm:w-[92vw] lg:w-[70vw] xl:w-[60vw] 2xl:w-[50vw] max-w-[1200px] mx-4 max-h-[96vh] overflow-auto rounded-xl border border-white/10 bg-[#0f172a]">
<div class="p-6 space-y-5 cf-modal">
<div class="flex justify-end">
<button type="button" @click="close()" aria-label="Close policy modal" class="transition-colors modal-close-btn">Close</button>
</div>
<template x-if="openType === 'terms'">
<div>
<h2 class="text-xl sm:text-2xl font-bold leading-tight modal-article-title">Terms of Use</h2>
<div class="space-y-3 text-sm leading-relaxed modal-body-text mt-3">
<p>The information shown on this site is provided for general informational use only. It is not independently verified by the site owner.</p>
<p>Any use of this content is entirely at your own risk. You are responsible for validating facts, citations, and suitability before relying on or redistributing the information.</p>
<p>Content may originate from external and AI-generated sources and may include errors, omissions, or outdated material.</p>
</div>
</div>
</template>
<template x-if="openType === 'attribution'">
<div>
<h2 class="text-xl sm:text-2xl font-bold leading-tight modal-article-title">Attribution and Ownership Disclaimer</h2>
<div class="space-y-3 text-sm leading-relaxed modal-body-text mt-3">
<p>None of the content presented on this site is created by the owner as an individual person.</p>
<p>The content is AI-generated and automatically assembled from external sources and generated summaries.</p>
<p>The owner does not claim personal authorship, editorial ownership, or direct responsibility for generated statements beyond operating the site interface.</p>
</div>
</div>
</template>
</div>
</div>
</div>
</section>
<button x-data="backToTopIsland()" x-init="init()" x-show="visible" x-cloak @click="scrollTop()"
class="back-to-top-island" aria-label="Back to top" title="Back to top">
<svg viewBox="0 0 24 24" class="w-4 h-4" aria-hidden="true"><path fill="currentColor" d="M12 5 5 12h4v7h6v-7h4z"/></svg>
</button>
<footer x-data="footerEnhancements()" x-init="init()" class="site-footer sticky bottom-0 z-40 border-t border-white/10 py-3 text-center text-xs text-gray-500">
<div class="max-w-7xl mx-auto px-4 space-y-1.5">
<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>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"> <p class="space-x-3">
<a href="/terms" class="footer-link">Terms of Use</a> <button type="button" class="footer-link" @click="window.dispatchEvent(new CustomEvent('open-policy-modal', { detail: { type: 'terms' } }))">Terms of Use</button>
<a href="/attribution" class="footer-link">Attribution</a> <button type="button" class="footer-link" @click="window.dispatchEvent(new CustomEvent('open-policy-modal', { detail: { type: 'attribution' } }))">Attribution</button>
<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="githubUrl" :href="githubUrl" target="_blank" rel="noopener" class="footer-link">GitHub</a>
<a x-show="contactEmail" :href="'mailto:' + contactEmail" class="footer-link" <a x-show="contactEmail" :href="'mailto:' + contactEmail" class="footer-link"
@mouseenter="showHint($event)" @mousemove="moveHint($event)" @mouseleave="hideHint()">Email me</a> @mouseenter="showHint($event)" @mousemove="moveHint($event)" @mouseleave="hideHint()" x-text="contactEmail"></a>
</p> </p>
<p>&copy; <span x-data x-text="new Date().getFullYear()"></span> ClawFort. All rights reserved.</p> <p>&copy; <span x-data x-text="new Date().getFullYear()"></span> ClawFort. All rights reserved.</p>
</div> </div>
@@ -586,6 +688,11 @@ function getPermalinkArticleId() {
return null; return null;
} }
function hasPermalinkArticleParam() {
const params = new URLSearchParams(window.location.search);
return params.has('article');
}
function setPermalinkArticleId(articleId) { function setPermalinkArticleId(articleId) {
if (!articleId) return; if (!articleId) return;
const url = new URL(window.location.href); const url = new URL(window.location.href);
@@ -601,6 +708,28 @@ function clearPermalinkArticleId() {
window.history.replaceState({}, '', url.toString()); window.history.replaceState({}, '', url.toString());
} }
function getPolicyModalType() {
const params = new URLSearchParams(window.location.search);
const modal = (params.get('policy') || '').toLowerCase();
if (modal === 'terms' || modal === 'attribution') return modal;
return '';
}
function setPolicyModalType(type) {
if (!type) return;
const url = new URL(window.location.href);
url.searchParams.set('policy', type);
window.history.replaceState({}, '', url.toString());
}
function clearPolicyModalType() {
const url = new URL(window.location.href);
if (url.searchParams.has('policy')) {
url.searchParams.delete('policy');
window.history.replaceState({}, '', url.toString());
}
}
function shareLink(provider, item) { function shareLink(provider, item) {
const permalink = articlePermalink(item); const permalink = articlePermalink(item);
const encodedUrl = encodeURIComponent(permalink); const encodedUrl = encodeURIComponent(permalink);
@@ -937,7 +1066,7 @@ function footerEnhancements() {
if (!resp.ok) return; if (!resp.ok) return;
const cfg = await resp.json(); const cfg = await resp.json();
this.githubUrl = cfg.github_repo_url || ''; this.githubUrl = cfg.github_repo_url || '';
this.contactEmail = cfg.contact_email || ''; this.contactEmail = (cfg.contact_email || '').trim();
} catch {} } catch {}
}, },
scrollTop() { scrollTop() {
@@ -962,6 +1091,89 @@ function footerEnhancements() {
}; };
} }
function headerFx() {
return {
scrolled: false,
init() {
const onScroll = () => {
this.scrolled = window.scrollY > 18;
};
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
},
};
}
function backToTopIsland() {
return {
visible: false,
init() {
const onScroll = () => {
this.visible = window.scrollY > Math.max(380, window.innerHeight * 0.6);
};
onScroll();
window.addEventListener('scroll', onScroll, { passive: true });
},
scrollTop() {
window.scrollTo({ top: 0, behavior: 'smooth' });
},
};
}
function policyDisclosures() {
return {
openType: '',
lastActiveElement: null,
init() {
const fromUrl = getPolicyModalType();
if (fromUrl) this.open(fromUrl);
window.addEventListener('open-policy-modal', (event) => {
const modalType = event?.detail?.type;
if (modalType === 'terms' || modalType === 'attribution') {
this.open(modalType);
}
});
},
open(type) {
this.lastActiveElement = document.activeElement;
this.openType = type;
setPolicyModalType(type);
setTimeout(() => {
const closeButton = document.querySelector('[aria-label="Close policy modal"]');
if (closeButton instanceof HTMLElement) closeButton.focus();
}, 0);
},
close() {
this.openType = '';
clearPolicyModalType();
if (this.lastActiveElement instanceof HTMLElement) {
this.lastActiveElement.focus();
}
},
onKeydown(event) {
if (!this.openType) return;
if (event.key === 'Escape') {
this.close();
return;
}
if (event.key !== 'Tab') return;
const focusable = Array.from(document.querySelectorAll('button, a, [tabindex]:not([tabindex="-1"])')).filter(
(el) => el instanceof HTMLElement && !el.hasAttribute('disabled') && el.offsetParent !== null,
);
if (!focusable.length) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (event.shiftKey && document.activeElement === first) {
event.preventDefault();
last.focus();
} else if (!event.shiftKey && document.activeElement === last) {
event.preventDefault();
first.focus();
}
},
};
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => { window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const choice = getPreferredTheme(); const choice = getPreferredTheme();
if (choice === 'system') { if (choice === 'system') {
@@ -1031,6 +1243,7 @@ function newsFeed() {
modalItem: null, modalItem: null,
modalImageLoading: true, modalImageLoading: true,
modalTldrLoading: true, modalTldrLoading: true,
copyStatus: '',
imageLoaded: {}, imageLoaded: {},
async init() { async init() {
@@ -1056,6 +1269,11 @@ function newsFeed() {
if (!event?.detail) return; if (!event?.detail) return;
this.openSummary(event.detail); this.openSummary(event.detail);
}); });
window.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && this.modalOpen) {
this.closeSummary();
}
});
}, },
waitForHero() { waitForHero() {
@@ -1125,7 +1343,10 @@ function newsFeed() {
async openFromPermalink() { async openFromPermalink() {
const articleId = getPermalinkArticleId(); const articleId = getPermalinkArticleId();
if (!articleId) return; if (!articleId) {
if (hasPermalinkArticleParam()) clearPermalinkArticleId();
return;
}
if (window.__heroNewsItem?.id === articleId) { if (window.__heroNewsItem?.id === articleId) {
this.openSummary(window.__heroNewsItem); this.openSummary(window.__heroNewsItem);
@@ -1140,14 +1361,26 @@ function newsFeed() {
attempts += 1; attempts += 1;
} }
if (item) this.openSummary(item); if (item) {
this.openSummary(item);
return;
}
clearPermalinkArticleId();
}, },
openSummary(item) { openSummary(item) {
this.modalItem = item; const normalized = {
...(item || {}),
summary_image_url: item?.summary_image_url || item?.image_url || '/static/images/placeholder.png',
tldr_points: Array.isArray(item?.tldr_points)
? item.tldr_points
: toBulletPoints(item?.summary_body || item?.summary || ''),
};
this.modalItem = normalized;
this.modalOpen = true; this.modalOpen = true;
this.modalImageLoading = true; this.modalImageLoading = true;
this.modalTldrLoading = true; this.modalTldrLoading = true;
this.copyStatus = '';
setPermalinkArticleId(item?.id); setPermalinkArticleId(item?.id);
setTimeout(() => { setTimeout(() => {
if (this.modalOpen) this.modalTldrLoading = false; if (this.modalOpen) this.modalTldrLoading = false;
@@ -1164,6 +1397,7 @@ function newsFeed() {
this.modalOpen = false; this.modalOpen = false;
this.modalItem = null; this.modalItem = null;
this.modalTldrLoading = true; this.modalTldrLoading = true;
this.copyStatus = '';
clearPermalinkArticleId(); clearPermalinkArticleId();
trackEvent('summary-modal-close', { trackEvent('summary-modal-close', {
article_id: id, article_id: id,
@@ -1177,6 +1411,36 @@ function newsFeed() {
article_id: item.id, article_id: item.id,
source_url: item.source_url, source_url: item.source_url,
}); });
},
async copyShareLink(item) {
if (!item?.id) return;
const permalink = articlePermalink(item);
try {
if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(permalink);
} else {
const temp = document.createElement('textarea');
temp.value = permalink;
temp.setAttribute('readonly', '');
temp.style.position = 'absolute';
temp.style.left = '-9999px';
document.body.appendChild(temp);
temp.select();
document.execCommand('copy');
document.body.removeChild(temp);
}
this.copyStatus = 'copied';
trackEvent('summary-modal-copy-link', {
article_id: item.id,
article_title: item.headline || null,
});
setTimeout(() => {
if (this.copyStatus === 'copied') this.copyStatus = '';
}, 1400);
} catch {
this.copyStatus = '';
}
} }
}; };
} }

View File

@@ -0,0 +1,58 @@
## Context
Recent usability enhancements introduced regressions in modal consistency, footer/header behavior, and share/contact visibility. The issues are cross-cutting: SPA URL state, modal lifecycle handling, theme contrast, sticky surface behavior, and image relevance heuristics. The fixes must remain incremental and avoid feature re-architecture.
## Goals / Non-Goals
**Goals:**
- Restore modal parity for hero and feed deep links, including image loading and Escape-close behavior.
- Ensure contact email is rendered when configured and share controls are visible in light mode.
- Introduce copy-to-clipboard sharing and remove redundant card permalink affordance.
- Implement floating Back-to-Top and compact sticky footer.
- Implement sticky, shrinking, elevated glass header behavior on scroll.
- Tighten image relevance fallback behavior for finance/market stories.
**Non-Goals:**
- Full UI redesign.
- New backend image provider integrations.
- New analytics taxonomy.
## Decisions
### Decision 1: Normalize deep-link modal resolution path
Use one resolver for both hero and feed items, and open modal only after item context is guaranteed.
### Decision 2: Decouple modal close from item existence
Escape and backdrop close should dismiss the modal even if modal item resolution is partially failed.
### Decision 3: Theme-safe icon tokens
Share icon foreground/background tokens are split per theme to guarantee visibility in light mode.
### Decision 4: Sticky surfaces with conservative footprint
Header remains sticky and shrinks slightly with blur/elevation on scroll; footer remains sticky with minimal height and readable contrast.
### Decision 5: Finance-aware image fallback guard
If query contains market/finance intent, reject obviously unrelated animal/person close-up imagery via keyword guards and force finance-safe fallback query.
## Risks / Trade-offs
- **[Risk] Sticky footer could occlude content** -> Mitigation: add bottom spacing to main content and reduce footer height.
- **[Risk] Header animation jank on low-end devices** -> Mitigation: simple transform/opacity transitions only.
- **[Risk] Stricter image relevance may reduce variety** -> Mitigation: constrain guardrails to high-confidence finance terms only.
## Migration Plan
1. Patch modal/deep-link lifecycle and close semantics in frontend state handlers.
2. Patch share/contact visibility and add copy-link control.
3. Implement sticky header/footer visual behavior.
4. Patch image-query relevance guard in backend image selection.
5. Validate with manual regression checklist.
Rollback:
- Revert header/footer sticky behavior.
- Revert modal resolver and share changes.
- Revert image guardrails to prior fallback logic.
## Open Questions
- Should copy-link action include transient toast feedback in this change or next UX pass?

View File

@@ -0,0 +1,19 @@
## MODIFIED Requirements
### 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, regardless of whether the target is hero or feed content.
#### Scenario: Hero permalink opens parity modal
- **WHEN** a user lands with a permalink for the current hero article
- **THEN** the modal opens with the same structure and behaviors as feed-opened modal
- **AND** summary image render path is executed normally
#### Scenario: Escape closes deep-linked modal
- **WHEN** a modal opened from permalink is focused
- **THEN** pressing `Escape` closes the modal
- **AND** URL deep-link state is cleared consistently
#### Scenario: Invalid permalink fails safely
- **WHEN** permalink article id does not resolve to an existing item
- **THEN** modal is not opened
- **AND** main page remains fully usable

View File

@@ -0,0 +1,14 @@
## MODIFIED Requirements
### Requirement: Footer exposes policy navigation links
The system SHALL keep policy links while supporting sticky compact footer behavior and floating back-to-top control.
#### Scenario: Sticky compact footer
- **WHEN** user scrolls through content
- **THEN** footer remains sticky at viewport bottom
- **AND** footer height stays compact enough to avoid readability obstruction
#### Scenario: Floating back-to-top island
- **WHEN** page is scrolled beyond initial viewport
- **THEN** a floating back-to-top control is visible
- **AND** activating it returns viewport to top smoothly

View File

@@ -0,0 +1,16 @@
## MODIFIED Requirements
### Requirement: Infinite scroll news feed
The system SHALL display feed cards without redundant per-card permalink text link when primary CTA and source link are present.
#### Scenario: Card chrome remains minimal
- **WHEN** feed cards are rendered
- **THEN** redundant small "Link" affordance is not shown
- **AND** card still exposes required source and TL;DR interactions
### Requirement: Hero block display
The system SHALL maintain hero-to-modal behavior parity with feed cards.
#### Scenario: Hero-origin modal consistency
- **WHEN** modal opens from hero context (including permalink-triggered entry)
- **THEN** modal image, content, and close controls behave consistently with feed-origin modal flows

View File

@@ -0,0 +1,14 @@
## MODIFIED Requirements
### Requirement: Fallback behavior remains context-aware first
The system SHALL apply stricter relevance guardrails before accepting summary images for finance/market stories.
#### Scenario: Finance-story relevance guard
- **WHEN** article topic contains finance/market terms (for example stocks, shares, plunge, earnings)
- **THEN** image selection rejects obviously unrelated animal/portrait outcomes
- **AND** system retries with finance-safe query refinements before final fallback
#### Scenario: Guarded fallback remains deterministic
- **WHEN** provider chain cannot return a relevant finance-safe image
- **THEN** system uses deterministic generic fallback that is topic-safe
- **AND** avoids unrelated imagery classes flagged by guardrails

View File

@@ -0,0 +1,14 @@
## MODIFIED Requirements
### Requirement: Core layout is device-agnostic and responsive
The system SHALL preserve responsive readability while enabling sticky header and footer surfaces.
#### Scenario: Sticky shrinking glass header
- **WHEN** user scrolls downward
- **THEN** header remains sticky with slight height reduction, subtle elevation, and glass blur effect
- **AND** controls remain readable and usable at all breakpoints
#### Scenario: Sticky footer does not overlap core reading zones
- **WHEN** footer is sticky
- **THEN** content remains readable and interactive controls are not obscured
- **AND** mobile/tablet/desktop layouts stay overflow-safe

View File

@@ -0,0 +1,22 @@
## MODIFIED Requirements
### Requirement: Modal/footer exposes minimal icon-based share actions
The system SHALL provide visible, accessible icon-only share actions for article permalinks.
#### Scenario: Light-theme icon visibility
- **WHEN** the site is in light mode
- **THEN** share icons remain visibly distinguishable with accessible contrast
- **AND** icon controls remain keyboard focusable
#### Scenario: Copy-link share action
- **WHEN** a user activates the copy-to-clipboard share control
- **THEN** the article permalink is written to clipboard
- **AND** action succeeds without navigating away
### Requirement: Footer supports env-driven GitHub and contact links
The system SHALL render contact email link when `CONTACT_EMAIL` is configured.
#### Scenario: Contact link visible when configured
- **WHEN** `CONTACT_EMAIL` is present in frontend config payload
- **THEN** footer shows the contact email affordance
- **AND** hover microcopy behavior remains enabled

View File

@@ -0,0 +1,30 @@
## 1. Permalink Modal Regression Fixes
- [x] 1.1 Fix deep-link modal open path for hero targets so modal structure and image loading match feed behavior.
- [x] 1.2 Restore `Escape` close behavior for permalink-opened modal state.
- [x] 1.3 Ensure invalid permalink handling does not break page interaction.
## 2. Share and Contact Fixes
- [x] 2.1 Fix light-mode social icon contrast tokens for X/WhatsApp/LinkedIn controls.
- [x] 2.2 Add copy-to-clipboard share action for article permalink.
- [x] 2.3 Restore footer contact email rendering from `/config` payload when `CONTACT_EMAIL` is set.
## 3. Header/Footer Usability Fixes
- [x] 3.1 Replace inline back-to-top text control with floating island control.
- [x] 3.2 Make footer sticky and compact while preserving content readability.
- [x] 3.3 Make header sticky with subtle shrink, elevation, and glass effect on scroll.
## 4. Content Relevance and Card Cleanup
- [x] 4.1 Add finance-story image relevance guardrails to avoid unrelated image classes.
- [x] 4.2 Add finance-safe query retry/fallback refinement path.
- [x] 4.3 Remove redundant card-level "Link" affordance from feed cards.
## 5. Verification
- [x] 5.1 Verify hero permalink opens modal with correct image and closing behavior.
- [x] 5.2 Verify footer email presence and share icon visibility in light/dark themes.
- [x] 5.3 Verify floating back-to-top, sticky footer, and sticky shrinking header on desktop/mobile.
- [x] 5.4 Verify finance-story image relevance behavior and fallback safety.

View File

@@ -0,0 +1,55 @@
## Context
The codebase has grown across frontend UX, backend ingestion, translations, analytics, and admin tooling. Quality checks are currently ad hoc and mostly manual, creating regression risk. A single cross-layer test and observability program is needed to enforce predictable release quality.
## Goals / Non-Goals
**Goals:**
- Establish CI quality gates covering unit, integration, E2E, accessibility, security, and performance.
- Provide deterministic test fixtures for UI/API/DB workflows.
- Define explicit coverage targets for critical paths and edge cases.
- Add production monitoring and alerting for latency, failures, and freshness.
**Non-Goals:**
- Migrating the app to a different framework.
- Building a full SRE platform from scratch.
- Replacing existing business logic outside remediation findings.
## Decisions
### Decision 1: Layered test pyramid with release gates
Adopt unit + integration + E2E layering; block release when any gate fails.
### Decision 2: Deterministic test data contracts
Use seeded fixtures and mockable provider boundaries for repeatable results.
### Decision 3: Accessibility and speed as first-class CI checks
Treat WCAG and page-speed regressions as gate failures with explicit thresholds.
### Decision 4: Security checks split by class
Run dependency audit, static security lint, and API abuse smoke tests separately for clearer ownership.
### Decision 5: Monitoring linked to user-impacting SLOs
Alert on API error rate, response latency, scheduler freshness, and failed fetch cycles.
## Risks / Trade-offs
- **[Risk] Longer CI times** -> Mitigation: split fast/slow suites, parallelize jobs.
- **[Risk] Flaky E2E tests** -> Mitigation: stable fixtures, retry policy only for known transient failures.
- **[Risk] Alert fatigue** -> Mitigation: tune thresholds with burn-in period and severity levels.
## Migration Plan
1. Baseline current test/tooling and add missing framework dependencies.
2. Implement layered suites and CI workflow stages.
3. Add WCAG, speed, and security checks with thresholds.
4. Add monitoring dashboards and alert routes.
5. Run remediation sprint for failing gates.
Rollback:
- Keep non-blocking mode for new gates until stability criteria are met.
## Open Questions
- Which minimum coverage threshold should be required for merge (line/branch)?
- Which environments should execute full E2E and speed checks (PR vs nightly)?

View File

@@ -0,0 +1,17 @@
## ADDED Requirements
### Requirement: Comprehensive review findings are tracked and remediated
The system SHALL track review findings and remediation outcomes in a structured workflow.
#### Scenario: Review finding lifecycle
- **WHEN** a code review identifies defect, risk, or optimization opportunity
- **THEN** finding is recorded with severity/owner/status
- **AND** remediation is linked to a verifiable change
### Requirement: Optimization work is bounded and measurable
Optimization actions SHALL include measurable before/after evidence.
#### Scenario: Optimization evidence recorded
- **WHEN** performance or code quality optimization is implemented
- **THEN** benchmark or metric delta is documented
- **AND** no functional regression is introduced

View File

@@ -0,0 +1,43 @@
## MODIFIED Requirements
### Requirement: HTTP delivery applies compression and cache policy
The system SHALL apply transport-level compression and explicit cache directives for static assets, API responses, and public HTML routes.
#### Scenario: Compressed responses are available for eligible payloads
- **WHEN** a client requests compressible content that exceeds the compression threshold
- **THEN** the response is served with gzip compression
- **AND** response headers advertise the selected content encoding
#### Scenario: Route classes receive deterministic cache-control directives
- **WHEN** clients request static assets, API responses, or HTML page routes
- **THEN** each route class returns a cache policy aligned to its freshness requirements
- **AND** cache directives are explicit and testable from response headers
### Requirement: Media rendering optimizes perceived loading performance
The system SHALL lazy-load non-critical images and render shimmer placeholders until image load completion or fallback resolution.
#### Scenario: Feed and modal images lazy-load with placeholders
- **WHEN** feed or modal images have not completed loading
- **THEN** a shimmer placeholder is visible for the pending image region
- **AND** the placeholder is removed after load or fallback error handling completes
#### Scenario: Image rendering reduces layout shift risk
- **WHEN** article images are rendered in hero, feed, or modal contexts
- **THEN** image elements include explicit dimensions and async decoding hints
- **AND** layout remains stable while content loads
### Requirement: Smooth scrolling behavior is consistently enabled
The system SHALL provide smooth scrolling behavior for in-page navigation and user-initiated scroll interactions.
#### Scenario: In-page navigation uses smooth scrolling
- **WHEN** users navigate to in-page anchors or equivalent interactions
- **THEN** scrolling transitions occur smoothly rather than jumping abruptly
- **AND** behavior is consistent across supported breakpoints
### Requirement: Performance thresholds are continuously validated
The system SHALL enforce page-speed and rendering performance thresholds in automated checks.
#### Scenario: Performance budget gate
- **WHEN** performance checks exceed configured budget thresholds
- **THEN** CI performance gate fails
- **AND** reports identify the regressed metrics and impacted pages

View File

@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: End-to-end coverage spans UI, API, and DB effects
The system SHALL provide end-to-end tests that validate full workflows across UI, API, and persisted database outcomes.
#### Scenario: Core user flow E2E
- **WHEN** a core browsing flow is executed in E2E tests
- **THEN** UI behavior, API responses, and DB side effects match expected outcomes
### Requirement: Edge-case workflows are covered
The system SHALL include edge-case E2E tests for critical failure and boundary conditions.
#### Scenario: Failure-state E2E
- **WHEN** an edge case is triggered (empty data, unavailable upstream, invalid permalink, etc.)
- **THEN** system response remains stable and user-safe
- **AND** no unhandled runtime errors occur

View File

@@ -0,0 +1,16 @@
## ADDED Requirements
### Requirement: Production monitoring covers key reliability signals
The system SHALL capture and expose reliability/performance metrics for core services.
#### Scenario: Metrics available for operations
- **WHEN** production system is running
- **THEN** dashboards expose API latency/error rate, scheduler freshness, and ingestion health signals
### Requirement: Alerting is actionable and threshold-based
The system SHALL send alerts on defined thresholds with clear operator guidance.
#### Scenario: Threshold breach alert
- **WHEN** a monitored metric breaches configured threshold
- **THEN** alert is emitted to configured channel
- **AND** alert includes service, metric, threshold, and suggested next action

View File

@@ -0,0 +1,17 @@
## ADDED Requirements
### Requirement: Release quality gates are mandatory
The system SHALL enforce mandatory CI quality gates before release.
#### Scenario: Gate failure blocks release
- **WHEN** any required gate fails
- **THEN** release pipeline status is failed
- **AND** deployment/archive promotion is blocked
### Requirement: Required gates are explicit and versioned
The system SHALL define an explicit set of required gates and versions for tooling.
#### Scenario: Gate manifest exists
- **WHEN** pipeline configuration is evaluated
- **THEN** required gates include tests, accessibility, security, and performance checks
- **AND** tool versions are pinned or documented for reproducibility

View File

@@ -0,0 +1,17 @@
## ADDED Requirements
### Requirement: Security test harness runs in CI
The system SHALL run baseline automated security checks in CI.
#### Scenario: Security checks execute
- **WHEN** CI pipeline runs on protected branches
- **THEN** dependency vulnerability and static security checks execute
- **AND** high-severity findings fail the gate
### Requirement: Performance test harness enforces thresholds
The system SHALL run page-speed and API-performance checks against defined thresholds.
#### Scenario: Performance regression detection
- **WHEN** measured performance exceeds regression threshold
- **THEN** performance gate fails
- **AND** reports include metric deltas and failing surfaces

View File

@@ -0,0 +1,33 @@
## MODIFIED Requirements
### Requirement: Confirmation guard for destructive commands
Destructive admin commands SHALL require explicit confirmation before execution.
#### Scenario: Missing confirmation flag
- **WHEN** an operator runs clear-news or clean-archive without required confirmation
- **THEN** the command exits without applying destructive changes
- **AND** prints guidance for explicit confirmation usage
### Requirement: Dry-run support where applicable
Maintenance commands SHALL provide dry-run mode for previewing effects where feasible.
#### Scenario: Dry-run preview
- **WHEN** an operator invokes a command with dry-run mode
- **THEN** the command reports intended actions and affected counts
- **AND** persists no data changes
### Requirement: Actionable failure summaries
Admin commands SHALL output actionable errors and final status summaries.
#### Scenario: Partial failure reporting
- **WHEN** a maintenance command partially fails
- **THEN** output includes succeeded/failed counts
- **AND** includes actionable next-step guidance
### Requirement: Admin workflows have automated verification coverage
Admin safety-critical workflows SHALL be covered by automated tests.
#### Scenario: Safety command regression test
- **WHEN** admin command tests run in CI
- **THEN** confirmation and dry-run behavior are validated by tests
- **AND** regressions in safety guards fail the gate

View File

@@ -0,0 +1,19 @@
## MODIFIED Requirements
### Requirement: Core user flows comply with WCAG 2.2 AA baseline
The system SHALL meet WCAG 2.2 AA accessibility requirements for primary interactions and content presentation, and SHALL verify compliance through automated accessibility checks in CI.
#### Scenario: Keyboard-only interaction flow
- **WHEN** a keyboard-only user navigates the page
- **THEN** all primary interactive elements are reachable and operable
- **AND** visible focus indication is present at each step
#### Scenario: Contrast and non-text alternatives
- **WHEN** users consume text and non-text UI content
- **THEN** color contrast meets AA thresholds for relevant text and controls
- **AND** meaningful images and controls include accessible labels/alternatives
#### Scenario: Accessibility CI gate
- **WHEN** pull request validation runs
- **THEN** automated accessibility checks execute against key pages and flows
- **AND** violations above configured severity fail the gate

View File

@@ -0,0 +1,43 @@
## 1. Test Framework Baseline
- [x] 1.1 Inventory current test/tooling gaps across frontend, backend, and DB layers.
- [x] 1.2 Add or standardize test runners, fixtures, and deterministic seed data.
- [x] 1.3 Define CI quality-gate stages and failure policies.
## 2. UI/API/DB End-to-End Coverage
- [x] 2.1 Implement E2E tests for critical UI journeys (hero/feed/modal/permalink/share).
- [x] 2.2 Implement API contract integration tests for news, config, and admin flows.
- [x] 2.3 Add DB state verification for ingestion, archiving, and translation workflows.
- [x] 2.4 Add edge-case E2E scenarios for invalid input, empty data, and failure paths.
## 3. Accessibility and UX Testing
- [x] 3.1 Integrate automated WCAG checks into CI for core pages.
- [x] 3.2 Add keyboard-focus and contrast regression checks.
- [x] 3.3 Add user-experience validation checklist for readability and interaction clarity.
## 4. Security and Performance Testing
- [x] 4.1 Add dependency and static security scanning to CI.
- [x] 4.2 Add abuse/safety smoke tests for API endpoints.
- [x] 4.3 Add page-speed and runtime performance checks with threshold budgets.
- [x] 4.4 Fail pipeline when security/performance thresholds are breached.
## 5. Review, Remediation, and Optimization
- [x] 5.1 Run comprehensive code review pass and log findings with severity/owner.
- [x] 5.2 Remediate defects uncovered by automated and manual testing.
- [x] 5.3 Implement optimization tasks with before/after evidence.
## 6. Monitoring and Alerting
- [x] 6.1 Define production metrics for reliability and latency.
- [x] 6.2 Configure dashboards and alert thresholds for key services.
- [x] 6.3 Add alert runbook guidance for common incidents.
## 7. Final Validation
- [x] 7.1 Verify all quality gates pass in CI.
- [x] 7.2 Verify coverage targets and edge-case suites meet defined thresholds.
- [x] 7.3 Verify monitoring alerts trigger correctly in test conditions.

View File

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

View File

@@ -0,0 +1,100 @@
## Context
This change addresses unresolved regressions introduced by recent UX and content pipeline updates. The affected areas cross frontend interaction patterns (`frontend/index.html`), policy content delivery (`backend/main.py`), translation generation (`backend/news_service.py`), and admin image maintenance operations (`backend/cli.py`). Current behavior still allows low-quality translations and repeated or weakly related refetched images, and policy links still rely on full-page navigation.
Key constraints:
- Maintain existing OpenSpec capability contracts where possible and ship mostly as requirement deltas.
- Preserve permalink identity for machine/admin targeting, while removing redundant user-facing permalink chrome on hero.
- Keep deterministic fallback behavior (AI-themed image fallback and English fallback for failed translations).
## Goals / Non-Goals
**Goals:**
- Deliver modal-based Terms and Attribution access with keyboard-safe behavior.
- Remove visible hero permalink affordance while preserving internal permalink targeting.
- Improve Tamil/Malayalam hero readability on desktop and mobile.
- Switch copy and back-to-top controls to icon-first interaction while preserving accessibility labels.
- Add translation quality validation gates (wrong-language/gibberish rejection and deterministic fallback).
- Extend refetch-images to support permalink targeting and enforce alternative relevant image selection (non-repeat, relevance/safety filters, AI fallback when uncertain).
**Non-Goals:**
- Replacing the existing rendering framework or routing model.
- Introducing new external image providers in this change.
- Reworking full article ingestion architecture beyond targeted translation/image guardrails.
## Decisions
### Decision 1: Policy disclosure moves to modal-first UI with deep-link-safe state
Use in-page modals for Terms and Attribution content while supporting deterministic open/close state from URL params or equivalent state synchronization.
Alternatives considered:
- Keep route pages only: rejected due to UX friction and context switch from main feed.
- Embed truncated inline footer text: rejected due to disclosure readability and legal clarity risks.
### Decision 2: Hero permalink identity remains internal, not a primary visual affordance
Retain permalink generation/parsing helpers for deep links and admin targeting, but remove visible hero permalink action from hero chrome.
Alternatives considered:
- Remove permalink behavior entirely: rejected because admin targeting and deep-link tooling need stable identifiers.
- Keep visible permalink: rejected because current hero action density and readability are degraded.
### Decision 3: Locale-aware hero readability profile for Tamil/Malayalam
Apply stronger readability guardrails for non-English hero text: increased contrast treatment, safer line-wrapping, language-specific typography tuning.
Alternatives considered:
- Single style for all languages: rejected; longer glyph clusters and script density degrade legibility.
- Separate locale-specific templates: rejected as too heavy for this stabilization change.
### Decision 4: Icon-only controls must remain explicitly accessible
Use icon-based copy and back-to-top controls with accessible names, keyboard operation, and stable tap targets.
Alternatives considered:
- Text-only controls: rejected by updated UX requirement.
- Icon-only without explicit labels: rejected due to WCAG and discoverability concerns.
### Decision 5: Translation quality gate before persistence/serving
Add post-generation validation for expected language/script sanity and basic gibberish detection. If validation fails, mark translation unavailable and serve deterministic English fallback.
Alternatives considered:
- Blindly accept model output: rejected due to current wrong-language/gibberish incidents.
- Human moderation for all translations: rejected for runtime latency and operational overhead.
### Decision 6: Refetch image selection becomes candidate-based and non-repeat
For refetch operations, generate contextual query candidates, filter against relevance/safety constraints, reject current/previously used image matches for the same article, and fall back to AI-themed image when confidence is low or candidates are exhausted.
Alternatives considered:
- First-provider-first-image approach: rejected because it often repeats or returns weakly related imagery.
- Hard blocklist only: rejected; insufficient for relevance and dedupe guarantees.
### Decision 7: Admin refetch supports permalink-targeted execution
Extend admin command contract to accept a permalink input, resolve to article identity, and run the same queue/refetch pipeline with targeted scope.
Alternatives considered:
- Keep limit-based batch-only refetch: rejected; operators need deterministic single-article correction path.
- Add separate ad hoc script: rejected due to fragmented operational surface.
## Risks / Trade-offs
- **[Risk] Modal policy flow may regress keyboard/focus behavior** -> Mitigation: explicit focus trap, escape-close, and focus-return requirements.
- **[Risk] Translation validation may over-reject valid short outputs** -> Mitigation: bounded heuristic thresholds plus deterministic English fallback and logging for tuning.
- **[Risk] Stronger image filtering can reduce image diversity** -> Mitigation: multi-candidate ranking and fallback to AI-themed image instead of unrelated imagery.
- **[Risk] Permalink-targeted admin flow may fail on malformed or stale links** -> Mitigation: strict permalink parser and clear operator error summaries.
## Migration Plan
1. Introduce modal policy behavior and hero/readability/icon control updates in frontend.
2. Add translation quality gate logic and fallback outcomes in backend translation path.
3. Extend image refetch pipeline with candidate ranking, dedupe, relevance/safety filters, and fallback.
4. Add permalink-targeted admin refetch argument and resolution path.
5. Validate through existing/manual verification paths and update OpenSpec tasks.
Rollback:
- Revert p16 frontend modal/icon/readability changes.
- Revert translation gate checks to prior translation acceptance path.
- Revert permalink-targeted and dedupe-aware refetch logic to prior batch refetch behavior.
## Open Questions
- Should policy modals update canonical URL state (`?modal=terms`) for shareable deep links, or stay ephemeral-only?
- What minimum confidence/sanity threshold should trigger translation rejection for short headlines?
- Should image dedupe history persist only per article current/previous image, or maintain wider history window?

View File

@@ -0,0 +1,46 @@
## Why
Recent UX and content-quality updates still leave several user-facing regressions unresolved across policy access, hero readability, share/back-to-top controls, translation quality, and image refetch behavior. These issues directly affect trust and usability, so we need a focused stabilization change that tightens both frontend interactions and backend content-quality guardrails.
## What Changes
- Convert Terms of Use and Attribution access from full-page navigation to in-page modal dialogs while preserving clear disclosure content and keyboard accessibility.
- Remove hero-level permalink affordance from visible hero chrome.
- Improve hero readability for non-English content (Tamil/Malayalam) across desktop and mobile.
- Replace text-based copy/share and back-to-top controls with icon-based controls that remain accessible.
- Add translation quality validation to reduce wrong-language or gibberish outputs before serving/storing translated content.
- Upgrade `admin refetch-images` behavior so refreshed images are alternative (not same as current), subject-relevant, and filtered against unrelated animal/pet outcomes.
- Extend admin image refetch operations to accept a permalink target and fetch a new relevant image for that article.
- Keep deterministic AI-themed fallback behavior when confidence is low or relevance checks fail.
## Capabilities
### New Capabilities
- `policy-disclosure-modals`: In-page modal experience for Terms of Use and Attribution content (focus trap, escape close, deep-link-safe modal state).
- `translation-quality-validation-gates`: Post-translation validation gates (language/script sanity + gibberish rejection + deterministic fallback policy).
- `permalink-targeted-image-refetch`: Admin command support for refetching summary images by permalink target.
- `alternative-image-selection-and-dedupe`: Refetch pipeline guarantees alternative image selection, relevance scoring, repeat-image avoidance, and unsafe/unrelated-image filtering.
### Modified Capabilities
- `footer-policy-links`: Footer policy access behavior changes from route navigation emphasis to modal activation flow.
- `terms-of-use-risk-disclosure`: Terms content remains required, but delivery surface changes from standalone page-centric flow to modal-capable flow.
- `attribution-disclaimer-page`: Attribution content remains required, but delivery surface changes from standalone page-centric flow to modal-capable flow.
- `hero-display`: Hero chrome removes visible permalink affordance and preserves clean primary/secondary actions.
- `hero-summary-entry-and-readability`: Strengthen multilingual hero readability requirements, especially for Tamil/Malayalam headline and summary rendering.
- `responsive-device-agnostic-layout`: Ensure icon-based controls and modal policy surfaces remain usable and unclipped on mobile/tablet/desktop.
- `wcag-2-2-aa-accessibility`: Icon-only controls and policy modals require explicit accessible labels, keyboard navigation, and focus behavior.
- `article-translations-ml-tm`: Add quality validation outcomes for generated Tamil/Malayalam translations before persistence/serving.
- `language-aware-content-delivery`: Define deterministic fallback behavior when requested translation fails quality validation.
- `admin-maintenance-command-suite`: Extend admin command contract to include permalink-targeted image refetch execution path.
- `queued-image-refetch-with-backoff`: Extend queue behavior to support targeted permalink jobs and non-repeat image outcomes.
- `context-aware-image-selection-recovery`: Improve subject extraction and keyword recovery for alternative relevant image retrieval.
- `news-image-relevance-and-fallbacks`: Tighten relevance/safety checks to reject unrelated animal/pet imagery and preserve AI-themed fallback when uncertain.
## Impact
- **Frontend/UI**: `frontend/index.html` policy-link behavior, hero metadata/actions, icon controls, modal state handling, and locale-specific readability styles.
- **Backend API/Routes**: `backend/main.py` policy content delivery strategy and compatibility with modal-first access.
- **Admin CLI**: `backend/cli.py` image refetch command arguments and permalink-targeted workflow.
- **Image pipeline**: `backend/news_service.py` subject extraction, alternative image candidate selection, dedupe/repeat prevention, relevance/safety filtering, and fallback selection.
- **Translation pipeline**: `backend/news_service.py` translation response validation and fallback policy; potential metadata persistence updates in repository/model layers.
- **Operational behavior**: Additional logging/audit fields for translation-quality failures and image-refetch decision path to support debugging and trust.

View File

@@ -0,0 +1,9 @@
## MODIFIED Requirements
### Requirement: Unified admin command surface
The system SHALL provide a single admin CLI command family exposing maintenance subcommands, including permalink-targeted image refetch support.
#### Scenario: Subcommand discovery
- **WHEN** an operator runs the admin command help output
- **THEN** available subcommands include refetch-images, clean-archive, clear-cache, clear-news, rebuild-site, regenerate-translations, and fetch
- **AND** refetch-images help output documents optional permalink-targeted execution

View File

@@ -0,0 +1,25 @@
## ADDED Requirements
### Requirement: Refetch selects an alternative image for target article
Refetch operations SHALL select an image different from the article's current summary image when a usable alternative exists.
#### Scenario: Current image is excluded from candidate result
- **WHEN** refetch evaluates image candidates for an article
- **THEN** the system rejects candidates matching the current summary image identity
- **AND** stores a different accepted image candidate when available
### Requirement: Candidate filtering enforces topical relevance and safety
Refetch candidate selection SHALL reject clearly unrelated imagery classes while preserving topic relevance.
#### Scenario: Unrelated animal/pet candidate is rejected
- **WHEN** candidate metadata or source signals indicate unrelated animal/pet imagery
- **THEN** the system rejects that candidate for article refetch
- **AND** continues evaluating other candidates or fallback paths
### Requirement: Low-confidence selection falls back to AI-themed default
If no candidate satisfies relevance and safety thresholds, the system SHALL use deterministic AI-themed fallback imagery.
#### Scenario: All candidates rejected
- **WHEN** candidate evaluation exhausts without an accepted image
- **THEN** the system assigns configured AI-themed fallback image
- **AND** records fallback decision path in command output/logs

View File

@@ -0,0 +1,19 @@
## MODIFIED Requirements
### Requirement: System generates Tamil and Malayalam translations at article creation time
The system SHALL generate Tamil (`ta`) and Malayalam (`ml`) translations for each newly created article during ingestion and validate translation quality before persistence.
#### Scenario: Translation generation for new article
- **WHEN** a new source article is accepted for storage
- **THEN** the system requests Tamil and Malayalam translations for headline and summary
- **AND** translation generation occurs in the same ingestion flow for that article
#### Scenario: Translation failure fallback
- **WHEN** translation generation fails for one or both target languages
- **THEN** the system stores the base article in English
- **AND** marks missing translations as unavailable without failing the whole ingestion cycle
#### Scenario: Translation quality validation failure
- **WHEN** generated translation fails language/script sanity or gibberish validation checks
- **THEN** the system does not persist invalid translation content for that article-language pair
- **AND** records validation failure outcome for diagnostics

View File

@@ -0,0 +1,14 @@
## MODIFIED Requirements
### Requirement: Attribution page discloses AI generation and non-ownership
The system SHALL provide Attribution disclosure content with explicit statements that content is AI-generated and not personally authored by the site owner, including modal-based access from the landing experience.
#### Scenario: Attribution disclosure title and body content
- **WHEN** a user opens Attribution disclosure content from supported entry points
- **THEN** the title clearly indicates attribution/disclaimer purpose
- **AND** the body states content is AI-generated and not generated by the owner as an individual
#### Scenario: Attribution disclosure includes non-involvement statement
- **WHEN** a user reads Attribution details
- **THEN** the content explicitly states owner non-involvement in generated content claims
- **AND** wording appears in primary readable content area

View File

@@ -0,0 +1,17 @@
## MODIFIED Requirements
### Requirement: Context-aware image query generation
Image refetch SHALL construct provider queries from article context by identifying main subject keywords from headline and summary content.
#### Scenario: Context-enriched query
- **WHEN** a queued article is processed for image refetch
- **THEN** the system derives main-subject query terms from article headline and summary
- **AND** includes relevance-supporting cues for improved candidate quality
### Requirement: AI-domain fallback keywords
When context extraction is insufficient or confidence is low, the system SHALL use AI-domain fallback keywords.
#### Scenario: Empty or weak context extraction
- **WHEN** extracted context terms are empty or below quality threshold
- **THEN** the system applies fallback terms such as `ai`, `machine learning`, `deep learning`
- **AND** continues candidate search with deterministic fallback ordering

View File

@@ -0,0 +1,14 @@
## MODIFIED Requirements
### Requirement: Footer exposes policy navigation links
The system SHALL display footer controls for Terms of Use and Attribution on the landing page and open their disclosure content in in-page modal dialogs.
#### Scenario: Footer policy controls visible and focusable
- **WHEN** a user loads the main page
- **THEN** the footer includes controls labeled "Terms of Use" and "Attribution"
- **AND** controls are visually distinguishable and keyboard focusable
#### Scenario: Footer policy controls open modal disclosures
- **WHEN** a user activates either policy control
- **THEN** the system opens the corresponding policy modal
- **AND** activation succeeds without full-page navigation dependency

View File

@@ -0,0 +1,9 @@
## MODIFIED Requirements
### Requirement: Hero block display
The system SHALL display hero article actions without a visible permalink affordance while preserving primary summary and source attribution interactions.
#### Scenario: Hero action chrome excludes permalink control
- **WHEN** the hero block is rendered
- **THEN** no dedicated hero permalink control is displayed
- **AND** summary open and source-link actions remain available

View File

@@ -0,0 +1,14 @@
## MODIFIED Requirements
### Requirement: Hero metadata readability over images
Hero metadata (`LATEST`, relative time, headline, and summary) SHALL remain visually legible across bright and dark images on desktop and mobile, including Tamil and Malayalam rendering paths.
#### Scenario: Bright image background
- **WHEN** the hero image contains bright regions under metadata text
- **THEN** overlay and text styles preserve readable contrast for metadata and headline blocks
- **AND** readability remains stable for English, Tamil, and Malayalam content
#### Scenario: Mobile viewport readability
- **WHEN** the hero renders on a mobile viewport
- **THEN** metadata and title remain readable without overlapping controls or clipping
- **AND** Tamil and Malayalam typography remains legible at mobile breakpoints

View File

@@ -0,0 +1,14 @@
## MODIFIED Requirements
### Requirement: Language fallback to English is deterministic
The system SHALL return English source content when the requested translation is unavailable or fails translation quality validation.
#### 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: Invalid translation fallback
- **WHEN** a client requests Tamil or Malayalam content for an article whose translation failed quality validation
- **THEN** the system returns English headline and summary for that article
- **AND** avoids returning invalid translated output

View File

@@ -0,0 +1,14 @@
## MODIFIED Requirements
### Requirement: Fallback behavior remains context-aware first
The system SHALL evaluate context-aware candidates before fallback and require refetched summary images to be relevant, non-redundant alternatives.
#### Scenario: Context-aware attempt precedes fallback
- **WHEN** summary image selection runs for a news item
- **THEN** the system first attempts provider queries from extracted context-aware keywords
- **AND** only falls back to generic AI image if candidate evaluation fails
#### Scenario: Refetch rejects unrelated and duplicate outcomes
- **WHEN** candidate images are evaluated during refetch
- **THEN** the system rejects candidates matching current image identity for the same article
- **AND** rejects clearly unrelated animal/pet imagery classes before final selection

View File

@@ -0,0 +1,14 @@
## ADDED Requirements
### Requirement: Admin image refetch supports permalink targeting
The admin image refetch command SHALL support a permalink-targeted execution mode for deterministic single-article remediation.
#### Scenario: Refetch by permalink
- **WHEN** an operator runs refetch-images with a valid permalink target
- **THEN** the system resolves permalink identity to the matching article
- **AND** executes image refetch for that target without requiring batch mode
#### Scenario: Invalid permalink target fails clearly
- **WHEN** an operator provides a malformed or unresolved permalink target
- **THEN** the command exits with actionable error output
- **AND** no article image is modified

View File

@@ -0,0 +1,27 @@
## ADDED Requirements
### Requirement: Policy disclosures are available as in-page modals
The system SHALL present Terms of Use and Attribution content in in-page modal dialogs without requiring full-page navigation from the landing experience.
#### Scenario: Open Terms modal from footer
- **WHEN** a user activates the "Terms of Use" footer control
- **THEN** a Terms modal opens in place on the current page
- **AND** the underlying page context remains intact
#### Scenario: Open Attribution modal from footer
- **WHEN** a user activates the "Attribution" footer control
- **THEN** an Attribution modal opens in place on the current page
- **AND** disclosure content is readable in the modal body
### Requirement: Policy modals are keyboard-safe and dismissible
Policy disclosure modals SHALL provide deterministic keyboard and pointer dismissal behavior.
#### Scenario: Escape closes active policy modal
- **WHEN** a policy modal is open and the user presses `Escape`
- **THEN** the modal closes
- **AND** focus returns to the triggering control
#### Scenario: Modal focus remains trapped while open
- **WHEN** a keyboard-only user tabs while a policy modal is open
- **THEN** focus cycles within modal interactive controls
- **AND** focus does not escape to background content

View File

@@ -0,0 +1,14 @@
## MODIFIED Requirements
### Requirement: Latest-30 queue construction
The refetch-images command SHALL enqueue up to the latest 30 news items for processing in batch mode and support targeted single-article mode when permalink targeting is provided.
#### Scenario: Queue population
- **WHEN** refetch-images is started without a permalink target
- **THEN** the command loads recent news items
- **AND** enqueues at most 30 items ordered from newest to oldest
#### Scenario: Targeted permalink mode
- **WHEN** refetch-images is started with a valid permalink target
- **THEN** the command enqueues only the resolved article
- **AND** bypasses latest-30 queue expansion

View File

@@ -0,0 +1,14 @@
## MODIFIED Requirements
### Requirement: Core layout is device-agnostic and responsive
The system SHALL render key surfaces (header, hero, feed, modal, footer, policy modals, and floating controls) 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 including icon-only copy/back-to-top and policy modals 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

View File

@@ -0,0 +1,14 @@
## MODIFIED Requirements
### Requirement: Terms page states unverified-content risk
The system SHALL provide Terms of Use disclosure content that states information is unverified and use is at the user's own risk, including modal-based access from the landing experience.
#### Scenario: Terms disclosure risk statement visible
- **WHEN** a user opens Terms of Use disclosure content from its supported entry points
- **THEN** the content includes clear at-own-risk usage language
- **AND** the content states information is not independently verified
#### Scenario: Terms disclosure references source uncertainty
- **WHEN** a user reads Terms details
- **THEN** the content explains information is surfaced from external/AI-generated sources
- **AND** users are informed responsibility remains with their own decisions

View File

@@ -0,0 +1,22 @@
## ADDED Requirements
### Requirement: Translation output passes quality validation before use
The system SHALL validate generated Tamil and Malayalam translation output for language/script sanity and gibberish risk before persistence or delivery.
#### Scenario: Valid translation accepted
- **WHEN** generated translation passes configured language and quality checks
- **THEN** the system stores and serves the translated content
- **AND** records a successful validation outcome
#### Scenario: Invalid translation rejected
- **WHEN** generated translation fails language/script or gibberish checks
- **THEN** the system rejects translated output for that article-language pair
- **AND** records a validation failure reason for operations visibility
### Requirement: Validation failure fallback is deterministic
When translation quality validation fails, the system SHALL provide deterministic English fallback behavior.
#### Scenario: Failed translation falls back to English
- **WHEN** a client requests language content for an article whose translation failed validation
- **THEN** the system returns English source headline and summary
- **AND** response shape remains consistent with language-aware responses

View File

@@ -0,0 +1,14 @@
## MODIFIED Requirements
### Requirement: Core user flows comply with WCAG 2.2 AA baseline
The system SHALL meet WCAG 2.2 AA accessibility requirements for primary interactions and content presentation, including icon-only controls and policy-disclosure modals.
#### Scenario: Keyboard-only interaction flow
- **WHEN** a keyboard-only user navigates policy modals and icon-only controls
- **THEN** all primary interactive elements remain reachable and operable
- **AND** visible focus indication is present at each step
#### Scenario: Contrast and non-text alternatives
- **WHEN** users consume text and non-text UI content
- **THEN** color contrast meets AA thresholds for relevant text and controls
- **AND** icon-only controls expose meaningful accessible labels

View File

@@ -0,0 +1,43 @@
## 1. Policy Disclosure Modal Conversion
- [x] 1.1 Implement Terms of Use modal surface and wire footer control to open it in-page.
- [x] 1.2 Implement Attribution modal surface and wire footer control to open it in-page.
- [x] 1.3 Add modal focus trap, Escape-close, and focus-return behavior for policy dialogs.
- [x] 1.4 Ensure policy modal state handling is deep-link-safe and does not break existing navigation.
## 2. Hero and Interaction Chrome Cleanup
- [x] 2.1 Remove visible hero permalink affordance while preserving hero summary and source actions.
- [x] 2.2 Replace modal copy-link text control with icon-only button and accessible labels.
- [x] 2.3 Replace floating back-to-top text control with icon-only button and accessible labels.
## 3. Multilingual Hero Readability Hardening
- [x] 3.1 Improve Tamil/Malayalam hero headline and summary readability across desktop and mobile.
- [x] 3.2 Tune hero overlay/contrast behavior to preserve legibility over bright image regions.
- [x] 3.3 Verify no clipping/overlap regressions in hero metadata and controls on narrow viewports.
## 4. Translation Quality Validation Gates
- [x] 4.1 Add translation quality validation checks for language/script sanity and gibberish rejection.
- [x] 4.2 Persist/log translation validation outcomes for observability and debugging.
- [x] 4.3 Enforce deterministic English fallback when requested translation fails validation.
- [x] 4.4 Prevent invalid translation variants from being served as authoritative localized content.
## 5. Refetch Image Quality and Targeting Enhancements
- [x] 5.1 Extend admin refetch-images to accept permalink-targeted execution.
- [x] 5.2 Resolve permalink targets to article identity with actionable operator error output.
- [x] 5.3 Implement candidate-based refetch selection that prefers subject-relevant alternatives.
- [x] 5.4 Reject duplicate current-image outcomes for same-article refetch operations.
- [x] 5.5 Reject clearly unrelated animal/pet imagery during candidate filtering.
- [x] 5.6 Preserve deterministic AI-themed fallback when no acceptable candidate exists.
- [x] 5.7 Emit progress/decision-path output for targeted and batch refetch execution.
## 6. Verification
- [x] 6.1 Verify Terms and Attribution open as accessible modals from footer controls.
- [x] 6.2 Verify hero permalink affordance is removed and core hero actions still function.
- [x] 6.3 Verify Tamil/Malayalam hero readability on desktop and mobile.
- [x] 6.4 Verify translation-quality gate fallback behavior for wrong-language/gibberish outputs.
- [x] 6.5 Verify permalink-targeted refetch returns alternative relevant image and avoids pet-style mismatches.

View File

@@ -5,11 +5,12 @@ Canonical specification for admin-maintenance-command-suite requirements synced
## Requirements ## Requirements
### Requirement: Unified admin command surface ### Requirement: Unified admin command surface
The system SHALL provide a single admin CLI command family exposing maintenance subcommands. The system SHALL provide a single admin CLI command family exposing maintenance subcommands, including permalink-targeted image refetch support.
#### Scenario: Subcommand discovery #### Scenario: Subcommand discovery
- **WHEN** an operator runs the admin command help output - **WHEN** an operator runs the admin command help output
- **THEN** available subcommands include refetch-images, clean-archive, clear-cache, clear-news, rebuild-site, regenerate-translations, and fetch - **THEN** available subcommands include refetch-images, clean-archive, clear-cache, clear-news, rebuild-site, regenerate-translations, and fetch
- **AND** refetch-images help output documents optional permalink-targeted execution
### Requirement: Fetch command supports configurable article count ### Requirement: Fetch command supports configurable article count
The admin fetch command SHALL support an operator-provided article count parameter. The admin fetch command SHALL support an operator-provided article count parameter.

View File

@@ -0,0 +1,29 @@
## Purpose
Canonical specification for alternative-image-selection-and-dedupe requirements synced from OpenSpec change deltas.
## Requirements
### Requirement: Refetch selects an alternative image for target article
Refetch operations SHALL select an image different from the article's current summary image when a usable alternative exists.
#### Scenario: Current image is excluded from candidate result
- **WHEN** refetch evaluates image candidates for an article
- **THEN** the system rejects candidates matching the current summary image identity
- **AND** stores a different accepted image candidate when available
### Requirement: Candidate filtering enforces topical relevance and safety
Refetch candidate selection SHALL reject clearly unrelated imagery classes while preserving topic relevance.
#### Scenario: Unrelated animal/pet candidate is rejected
- **WHEN** candidate metadata or source signals indicate unrelated animal/pet imagery
- **THEN** the system rejects that candidate for article refetch
- **AND** continues evaluating other candidates or fallback paths
### Requirement: Low-confidence selection falls back to AI-themed default
If no candidate satisfies relevance and safety thresholds, the system SHALL use deterministic AI-themed fallback imagery.
#### Scenario: All candidates rejected
- **WHEN** candidate evaluation exhausts without an accepted image
- **THEN** the system assigns configured AI-themed fallback image
- **AND** records fallback decision path in command output/logs

View File

@@ -0,0 +1,39 @@
## Purpose
Canonical specification for article-permalinks-and-deep-link-modal requirements synced from OpenSpec change deltas.
## 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, regardless of whether the target is hero or feed content.
#### 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: Hero permalink opens parity modal
- **WHEN** a user lands with a permalink for the current hero article
- **THEN** the modal opens with the same structure and behaviors as feed-opened modal
- **AND** summary image render path is executed normally
#### Scenario: Escape closes deep-linked modal
- **WHEN** a modal opened from permalink is focused
- **THEN** pressing `Escape` closes the modal
- **AND** URL deep-link state is cleared consistently
#### Scenario: Invalid permalink fails safely
- **WHEN** permalink article id does not resolve to an existing item
- **THEN** modal is not opened
- **AND** main page remains fully usable

View File

@@ -5,7 +5,7 @@ Canonical specification for article-translations-ml-tm requirements synced from
## Requirements ## Requirements
### Requirement: System generates Tamil and Malayalam translations at article creation time ### Requirement: System generates Tamil and Malayalam translations at article creation time
The system SHALL generate Tamil (`ta`) and Malayalam (`ml`) translations for each newly created article during ingestion. The system SHALL generate Tamil (`ta`) and Malayalam (`ml`) translations for each newly created article during ingestion and validate translation quality before persistence.
#### Scenario: Translation generation for new article #### Scenario: Translation generation for new article
- **WHEN** a new source article is accepted for storage - **WHEN** a new source article is accepted for storage
@@ -17,6 +17,11 @@ The system SHALL generate Tamil (`ta`) and Malayalam (`ml`) translations for eac
- **THEN** the system stores the base article in English - **THEN** the system stores the base article in English
- **AND** marks missing translations as unavailable without failing the whole ingestion cycle - **AND** marks missing translations as unavailable without failing the whole ingestion cycle
#### Scenario: Translation quality validation failure
- **WHEN** generated translation fails language/script sanity or gibberish validation checks
- **THEN** the system does not persist invalid translation content for that article-language pair
- **AND** records validation failure outcome for diagnostics
### Requirement: System stores translation variants linked to the same article ### Requirement: System stores translation variants linked to the same article
The system SHALL persist language-specific translated content as translation items associated with the base article. The system SHALL persist language-specific translated content as translation items associated with the base article.

View File

@@ -5,14 +5,14 @@ Canonical specification for attribution-disclaimer-page requirements synced from
## Requirements ## Requirements
### Requirement: Attribution page discloses AI generation and non-ownership ### Requirement: Attribution page discloses AI generation and non-ownership
The system SHALL provide an Attribution page with explicit statements that content is AI-generated and not personally authored by the site owner. The system SHALL provide Attribution disclosure content with explicit statements that content is AI-generated and not personally authored by the site owner, including modal-based access from the landing experience.
#### Scenario: Attribution page title and disclosure content #### Scenario: Attribution disclosure title and body content
- **WHEN** a user opens the Attribution page - **WHEN** a user opens Attribution disclosure content from supported entry points
- **THEN** the page title clearly indicates attribution/disclaimer purpose - **THEN** the title clearly indicates attribution/disclaimer purpose
- **AND** the body states that content is AI-generated and not generated by the owner as an individual - **AND** the body states content is AI-generated and not generated by the owner as an individual
#### Scenario: Attribution page includes non-involvement statement #### Scenario: Attribution disclosure includes non-involvement statement
- **WHEN** a user reads the Attribution page - **WHEN** a user reads Attribution details
- **THEN** the page explicitly states owner non-involvement in generated content claims - **THEN** the content explicitly states owner non-involvement in generated content claims
- **AND** wording is presented in primary readable content area - **AND** wording appears in primary readable content area

View File

@@ -5,19 +5,20 @@ Canonical specification for context-aware-image-selection-recovery requirements
## Requirements ## Requirements
### Requirement: Context-aware image query generation ### Requirement: Context-aware image query generation
Image refetch SHALL construct provider queries from article context including keywords and mood/sentiment cues. Image refetch SHALL construct provider queries from article context by identifying main subject keywords from headline and summary content.
#### Scenario: Context-enriched query #### Scenario: Context-enriched query
- **WHEN** a queued article is processed for image refetch - **WHEN** a queued article is processed for image refetch
- **THEN** the system derives query terms from article headline/summary content - **THEN** the system derives main-subject query terms from article headline and summary
- **AND** includes mood/sentiment-informed cues to improve relevance - **AND** includes relevance-supporting cues for improved candidate quality
### Requirement: AI-domain fallback keywords ### Requirement: AI-domain fallback keywords
When context extraction is insufficient, the system SHALL use AI-domain fallback keywords. When context extraction is insufficient or confidence is low, the system SHALL use AI-domain fallback keywords.
#### Scenario: Empty or weak context extraction #### Scenario: Empty or weak context extraction
- **WHEN** extracted context terms are empty or below quality threshold - **WHEN** extracted context terms are empty or below quality threshold
- **THEN** the system applies fallback terms such as `ai`, `machine learning`, `deep learning` - **THEN** the system applies fallback terms such as `ai`, `machine learning`, `deep learning`
- **AND** continues candidate search with deterministic fallback ordering
### Requirement: Generic AI fallback image on terminal failure ### Requirement: Generic AI fallback image on terminal failure
If no usable provider image is returned, the system SHALL assign a generic AI fallback image. If no usable provider image is returned, the system SHALL assign a generic AI fallback image.

View File

@@ -37,3 +37,11 @@ The system SHALL provide smooth scrolling behavior for in-page navigation and us
- **WHEN** users navigate to in-page anchors or equivalent interactions - **WHEN** users navigate to in-page anchors or equivalent interactions
- **THEN** scrolling transitions occur smoothly rather than jumping abruptly - **THEN** scrolling transitions occur smoothly rather than jumping abruptly
- **AND** behavior is consistent across supported breakpoints - **AND** behavior is consistent across supported breakpoints
### Requirement: Performance thresholds are continuously validated
The system SHALL enforce page-speed and rendering performance thresholds in automated checks.
#### Scenario: Performance budget gate
- **WHEN** performance checks exceed configured budget thresholds
- **THEN** CI performance gate fails
- **AND** reports identify the regressed metrics and impacted pages

View File

@@ -0,0 +1,20 @@
## Purpose
Canonical specification for end-to-end-system-testing requirements synced from OpenSpec change deltas.
## Requirements
### Requirement: End-to-end coverage spans UI, API, and DB effects
The system SHALL provide end-to-end tests that validate full workflows across UI, API, and persisted database outcomes.
#### Scenario: Core user flow E2E
- **WHEN** a core browsing flow is executed in E2E tests
- **THEN** UI behavior, API responses, and DB side effects match expected outcomes
### Requirement: Edge-case workflows are covered
The system SHALL include edge-case E2E tests for critical failure and boundary conditions.
#### Scenario: Failure-state E2E
- **WHEN** an edge case is triggered (empty data, unavailable upstream, invalid permalink, etc.)
- **THEN** system response remains stable and user-safe
- **AND** no unhandled runtime errors occur

View File

@@ -5,14 +5,29 @@ Canonical specification for footer-policy-links requirements synced from OpenSpe
## Requirements ## Requirements
### Requirement: Footer exposes policy navigation links ### Requirement: Footer exposes policy navigation links
The system SHALL display footer links for Terms of Use and Attribution on the landing page. The system SHALL display footer controls for Terms of Use and Attribution on the landing page and open their disclosure content in in-page modal dialogs while preserving compact sticky-footer behavior.
#### Scenario: Footer links visible on landing page #### Scenario: Footer policy controls visible and focusable
- **WHEN** a user loads the main page - **WHEN** a user loads the main page
- **THEN** the footer includes links labeled "Terms of Use" and "Attribution" - **THEN** the footer includes controls labeled "Terms of Use" and "Attribution"
- **AND** links are visually distinguishable and keyboard focusable - **AND** controls are visually distinguishable and keyboard focusable
#### Scenario: Footer links navigate correctly #### Scenario: Footer policy controls open modal disclosures
- **WHEN** a user activates either policy link - **WHEN** a user activates either policy control
- **THEN** the browser navigates to the corresponding policy page - **THEN** the system opens the corresponding policy modal
- **AND** navigation succeeds without API dependency - **AND** activation succeeds without full-page navigation dependency
#### Scenario: Sticky compact footer
- **WHEN** user scrolls through content
- **THEN** footer remains sticky at viewport bottom
- **AND** footer height stays compact enough to avoid readability obstruction
#### Scenario: Floating back-to-top island
- **WHEN** page is scrolled beyond initial viewport
- **THEN** a floating back-to-top control is visible
- **AND** activating it returns viewport to 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 controls
- **AND** absent values do not break footer layout

View File

@@ -5,7 +5,7 @@ Canonical specification for hero-display requirements synced from OpenSpec chang
## Requirements ## Requirements
### Requirement: Hero block display ### Requirement: Hero block display
The system SHALL display the most recent news item as a featured hero block with full attribution. The system SHALL display the most recent news item as a featured hero block with full attribution and modal-behavior parity, while omitting a dedicated hero permalink affordance.
#### Scenario: Hero rendering #### Scenario: Hero rendering
- **WHEN** the page loads - **WHEN** the page loads
@@ -18,8 +18,17 @@ The system SHALL display the most recent news item as a featured hero block with
- **THEN** the hero block SHALL automatically update to show the newest item - **THEN** the hero block SHALL automatically update to show the newest item
- **AND** the previous hero item SHALL move to the news feed - **AND** the previous hero item SHALL move to the news feed
#### Scenario: Hero-origin modal consistency
- **WHEN** modal opens from hero context (including permalink-triggered entry)
- **THEN** modal image, content, and close controls behave consistently with feed-origin modal flows
#### Scenario: Hero action chrome excludes permalink control
- **WHEN** the hero block is rendered
- **THEN** no dedicated hero permalink control is displayed
- **AND** summary open and source-link actions remain available
### Requirement: Infinite scroll news feed ### Requirement: Infinite scroll news feed
The system SHALL display news items in reverse chronological order with infinite scroll pagination. The system SHALL display news items in reverse chronological order with infinite scroll pagination and minimal card chrome.
#### Scenario: Initial load #### Scenario: Initial load
- **WHEN** the page first loads - **WHEN** the page first loads
@@ -37,6 +46,11 @@ The system SHALL display news items in reverse chronological order with infinite
- **THEN** the system SHALL display "No more news" message - **THEN** the system SHALL display "No more news" message
- **AND** disable further scroll triggers - **AND** disable further scroll triggers
#### Scenario: Card chrome remains minimal
- **WHEN** feed cards are rendered
- **THEN** redundant small "Link" affordance is not shown
- **AND** card still exposes required source and TL;DR interactions
### Requirement: News attribution display ### Requirement: News attribution display
The system SHALL clearly attribute all news content and images to their sources. The system SHALL clearly attribute all news content and images to their sources.
@@ -55,3 +69,4 @@ The system SHALL clearly attribute all news content and images to their sources.
- **AND** track page view events on initial load - **AND** track page view events on initial load
- **AND** track scroll depth events (25%, 50%, 75%, 100%) - **AND** track scroll depth events (25%, 50%, 75%, 100%)
- **AND** track CTA click events (news item clicks, source link clicks) - **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

@@ -20,12 +20,14 @@ The hero section SHALL keep an explicit secondary source-link action for externa
- **THEN** the system opens the article source URL in a new tab - **THEN** the system opens the article source URL in a new tab
### Requirement: Hero metadata readability over images ### Requirement: Hero metadata readability over images
Hero metadata (`LATEST`, relative time, headline, and summary) SHALL remain visually legible across bright and dark images on desktop and mobile. Hero metadata (`LATEST`, relative time, headline, and summary) SHALL remain visually legible across bright and dark images on desktop and mobile, including Tamil and Malayalam rendering paths.
#### Scenario: Bright image background #### Scenario: Bright image background
- **WHEN** the hero image contains bright regions under metadata text - **WHEN** the hero image contains bright regions under metadata text
- **THEN** overlay and text styles preserve readable contrast for metadata and headline blocks - **THEN** overlay and text styles preserve readable contrast for metadata and headline blocks
- **AND** readability remains stable for English, Tamil, and Malayalam content
#### Scenario: Mobile viewport readability #### Scenario: Mobile viewport readability
- **WHEN** the hero renders on a mobile viewport - **WHEN** the hero renders on a mobile viewport
- **THEN** metadata and title remain readable without overlapping controls or clipping - **THEN** metadata and title remain readable without overlapping controls or clipping
- **AND** Tamil and Malayalam typography remains legible at mobile breakpoints

View File

@@ -17,14 +17,24 @@ The system SHALL support language-aware content delivery for hero and feed reads
- **THEN** the system returns each feed item's headline and summary in the selected language when available - **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 - **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 ### Requirement: Language fallback to English is deterministic
The system SHALL return English source content when the requested translation is unavailable. The system SHALL return English source content when the requested translation is unavailable or fails translation quality validation.
#### Scenario: Missing translation fallback #### Scenario: Missing translation fallback
- **WHEN** a client requests Tamil or Malayalam content for an article lacking that translation - **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 - **THEN** the system returns the English headline and summary for that article
- **AND** response shape remains consistent with language-aware responses - **AND** response shape remains consistent with language-aware responses
#### Scenario: Invalid translation fallback
- **WHEN** a client requests Tamil or Malayalam content for an article whose translation failed quality validation
- **THEN** the system returns English headline and summary for that article
- **AND** avoids returning invalid translated output
#### Scenario: Unsupported language handling #### Scenario: Unsupported language handling
- **WHEN** a client requests a language outside supported values (`en`, `ta`, `ml`) - **WHEN** a client requests a language outside supported values (`en`, `ta`, `ml`)
- **THEN** the system applies the defined default language behavior for this phase - **THEN** the system applies the defined default language behavior for this phase

View File

@@ -19,9 +19,24 @@ If provider lookups fail to return a usable summary image, the system SHALL use
- **THEN** the system assigns a generic AI fallback image URL/path for summary image - **THEN** the system assigns a generic AI fallback image URL/path for summary image
### Requirement: Fallback behavior remains context-aware first ### Requirement: Fallback behavior remains context-aware first
The system SHALL attempt context-aware keyword retrieval before any generic fallback image is selected. The system SHALL evaluate context-aware candidates before fallback and require refetched summary images to be relevant, non-redundant alternatives.
#### Scenario: Context-aware attempt precedes fallback #### Scenario: Context-aware attempt precedes fallback
- **WHEN** summary image selection runs for a news item - **WHEN** summary image selection runs for a news item
- **THEN** the system first attempts provider queries from extracted context-aware keywords - **THEN** the system first attempts provider queries from extracted context-aware keywords
- **AND** only falls back to generic AI image if these attempts fail - **AND** only falls back to generic AI image if candidate evaluation fails
#### Scenario: Finance-story relevance guard
- **WHEN** article topic contains finance/market terms (for example stocks, shares, plunge, earnings)
- **THEN** image selection rejects obviously unrelated animal/portrait outcomes
- **AND** system retries with finance-safe query refinements before final fallback
#### Scenario: Guarded fallback remains deterministic
- **WHEN** provider chain cannot return a relevant finance-safe image
- **THEN** system uses deterministic generic fallback that is topic-safe
- **AND** avoids unrelated imagery classes flagged by guardrails
#### Scenario: Refetch rejects unrelated and duplicate outcomes
- **WHEN** candidate images are evaluated during refetch
- **THEN** the system rejects candidates matching current image identity for the same article
- **AND** rejects clearly unrelated animal/pet imagery classes before final selection

View File

@@ -0,0 +1,18 @@
## Purpose
Canonical specification for permalink-targeted-image-refetch requirements synced from OpenSpec change deltas.
## Requirements
### Requirement: Admin image refetch supports permalink targeting
The admin image refetch command SHALL support a permalink-targeted execution mode for deterministic single-article remediation.
#### Scenario: Refetch by permalink
- **WHEN** an operator runs refetch-images with a valid permalink target
- **THEN** the system resolves permalink identity to the matching article
- **AND** executes image refetch for that target without requiring batch mode
#### Scenario: Invalid permalink target fails clearly
- **WHEN** an operator provides a malformed or unresolved permalink target
- **THEN** the command exits with actionable error output
- **AND** no article image is modified

View File

@@ -0,0 +1,21 @@
## Purpose
Canonical specification for platform-quality-gates requirements synced from OpenSpec change deltas.
## Requirements
### Requirement: Release quality gates are mandatory
The system SHALL enforce mandatory CI quality gates before release.
#### Scenario: Gate failure blocks release
- **WHEN** any required gate fails
- **THEN** release pipeline status is failed
- **AND** deployment/archive promotion is blocked
### Requirement: Required gates are explicit and versioned
The system SHALL define an explicit set of required gates and versions for tooling.
#### Scenario: Gate manifest exists
- **WHEN** pipeline configuration is evaluated
- **THEN** required gates include tests, accessibility, security, and performance checks
- **AND** tool versions are pinned or documented for reproducibility

View File

@@ -0,0 +1,31 @@
## Purpose
Canonical specification for policy-disclosure-modals requirements synced from OpenSpec change deltas.
## Requirements
### Requirement: Policy disclosures are available as in-page modals
The system SHALL present Terms of Use and Attribution content in in-page modal dialogs without requiring full-page navigation from the landing experience.
#### Scenario: Open Terms modal from footer
- **WHEN** a user activates the "Terms of Use" footer control
- **THEN** a Terms modal opens in place on the current page
- **AND** the underlying page context remains intact
#### Scenario: Open Attribution modal from footer
- **WHEN** a user activates the "Attribution" footer control
- **THEN** an Attribution modal opens in place on the current page
- **AND** disclosure content is readable in the modal body
### Requirement: Policy modals are keyboard-safe and dismissible
Policy disclosure modals SHALL provide deterministic keyboard and pointer dismissal behavior.
#### Scenario: Escape closes active policy modal
- **WHEN** a policy modal is open and the user presses `Escape`
- **THEN** the modal closes
- **AND** focus returns to the triggering control
#### Scenario: Modal focus remains trapped while open
- **WHEN** a keyboard-only user tabs while a policy modal is open
- **THEN** focus cycles within modal interactive controls
- **AND** focus does not escape to background content

View File

@@ -5,13 +5,18 @@ Canonical specification for queued-image-refetch-with-backoff requirements synce
## Requirements ## Requirements
### Requirement: Latest-30 queue construction ### Requirement: Latest-30 queue construction
The refetch-images command SHALL enqueue up to the latest 30 news items for processing. The refetch-images command SHALL enqueue up to the latest 30 news items for processing in batch mode and support targeted single-article mode when permalink targeting is provided.
#### Scenario: Queue population #### Scenario: Queue population
- **WHEN** refetch-images is started - **WHEN** refetch-images is started without a permalink target
- **THEN** the command loads recent news items - **THEN** the command loads recent news items
- **AND** enqueues at most 30 items ordered from newest to oldest - **AND** enqueues at most 30 items ordered from newest to oldest
#### Scenario: Targeted permalink mode
- **WHEN** refetch-images is started with a valid permalink target
- **THEN** the command enqueues only the resolved article
- **AND** bypasses latest-30 queue expansion
### Requirement: Sequential processing ### Requirement: Sequential processing
The image refetch queue SHALL be processed one item at a time. The image refetch queue SHALL be processed one item at a time.

View File

@@ -5,14 +5,29 @@ Canonical specification for responsive-device-agnostic-layout requirements synce
## Requirements ## Requirements
### Requirement: Core layout is device-agnostic and responsive ### 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. The system SHALL render key surfaces (header, hero, feed, modal, footer, policy modals, and floating controls) responsively across mobile, tablet, and desktop viewports while preserving readability.
#### Scenario: Mobile layout behavior #### Scenario: Mobile layout behavior
- **WHEN** a user opens the site on a mobile viewport - **WHEN** a user opens the site on a mobile viewport
- **THEN** content remains readable without horizontal overflow - **THEN** content remains readable without horizontal overflow
- **AND** interactive controls remain reachable and usable - **AND** interactive controls including icon-only copy/back-to-top and policy modals remain reachable and usable
#### Scenario: Desktop and tablet adaptation #### Scenario: Desktop and tablet adaptation
- **WHEN** a user opens the site on tablet or desktop viewports - **WHEN** a user opens the site on tablet or desktop viewports
- **THEN** layout reflows according to breakpoint design rules - **THEN** layout reflows according to breakpoint design rules
- **AND** no key content or controls are clipped - **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
#### Scenario: Sticky shrinking glass header
- **WHEN** user scrolls downward
- **THEN** header remains sticky with slight height reduction, subtle elevation, and glass blur effect
- **AND** controls remain readable and usable at all breakpoints
#### Scenario: Sticky footer does not overlap core reading zones
- **WHEN** footer is sticky
- **THEN** content remains readable and interactive controls are not obscured
- **AND** mobile/tablet/desktop layouts stay overflow-safe

View File

@@ -0,0 +1,21 @@
## Purpose
Canonical specification for security-and-performance-test-harness requirements synced from OpenSpec change deltas.
## Requirements
### Requirement: Security test harness runs in CI
The system SHALL run baseline automated security checks in CI.
#### Scenario: Security checks execute
- **WHEN** CI pipeline runs on protected branches
- **THEN** dependency vulnerability and static security checks execute
- **AND** high-severity findings fail the gate
### Requirement: Performance test harness enforces thresholds
The system SHALL run page-speed and API-performance checks against defined thresholds.
#### Scenario: Performance regression detection
- **WHEN** measured performance exceeds regression threshold
- **THEN** performance gate fails
- **AND** reports include metric deltas and failing surfaces

View File

@@ -17,6 +17,11 @@ The system SHALL expose standards-compliant SEO metadata on the homepage and pol
- **THEN** the page includes page-specific `title` and `description` metadata - **THEN** the page includes page-specific `title` and `description` metadata
- **AND** Open Graph and Twitter card metadata are present for link previews - **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
### Requirement: Canonical and preview metadata remain deterministic ### Requirement: Canonical and preview metadata remain deterministic
The system SHALL keep canonical and preview metadata deterministic for each route to avoid conflicting crawler signals. The system SHALL keep canonical and preview metadata deterministic for each route to avoid conflicting crawler signals.

Some files were not shown because too many files have changed in this diff Show More