First deployment
This commit is contained in:
2
.env
2
.env
@@ -11,3 +11,5 @@ 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
37
.github/workflows/quality-gates.yml
vendored
Normal 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
|
||||||
5
.sisyphus/notepads/sync-p12-p16/decisions.md
Normal file
5
.sisyphus/notepads/sync-p12-p16/decisions.md
Normal 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).
|
||||||
3
.sisyphus/notepads/sync-p12-p16/issues.md
Normal file
3
.sisyphus/notepads/sync-p12-p16/issues.md
Normal 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.
|
||||||
5
.sisyphus/notepads/sync-p12-p16/learnings.md
Normal file
5
.sisyphus/notepads/sync-p12-p16/learnings.md
Normal 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.
|
||||||
3
.sisyphus/notepads/sync-p12-p16/problems.md
Normal file
3
.sisyphus/notepads/sync-p12-p16/problems.md
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
## 2026-02-13
|
||||||
|
|
||||||
|
- None unresolved in the p12-p16 spec merge scope.
|
||||||
14
README.md
14
README.md
@@ -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.
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
125
backend/cli.py
125
backend/cli.py
@@ -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,16 +88,90 @@ 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:
|
||||||
|
if target_article_id is not None:
|
||||||
|
items = (
|
||||||
|
db.query(NewsItem)
|
||||||
|
.filter(NewsItem.archived.is_(False), NewsItem.id == target_article_id)
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
else:
|
||||||
items = (
|
items = (
|
||||||
db.query(NewsItem)
|
db.query(NewsItem)
|
||||||
.filter(NewsItem.archived.is_(False))
|
.filter(NewsItem.archived.is_(False))
|
||||||
@@ -108,35 +183,10 @@ async def refetch_images_for_latest(
|
|||||||
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",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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.",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
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
|
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,16 +712,36 @@ 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 query_variant in query_variants:
|
||||||
for provider_name, fetch_fn in get_enabled_providers():
|
for provider_name, fetch_fn in get_enabled_providers():
|
||||||
try:
|
try:
|
||||||
image_url, credit = await fetch_fn(refined_query)
|
image_url, credit = await fetch_fn(query_variant)
|
||||||
if image_url:
|
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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
26
docs/monitoring-dashboard-config.md
Normal file
26
docs/monitoring-dashboard-config.md
Normal 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.
|
||||||
23
docs/p15-code-review-findings.md
Normal file
23
docs/p15-code-review-findings.md
Normal 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
|
||||||
64
docs/quality-and-monitoring.md
Normal file
64
docs/quality-and-monitoring.md
Normal 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>
|
||||||
|
```
|
||||||
@@ -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,11 +470,8 @@
|
|||||||
@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">
|
|
||||||
<a :href="articlePermalink(item)" class="hover:text-cf-300 underline underline-offset-2">Link</a>
|
|
||||||
<span x-text="timeAgo(item.published_at)"></span>
|
<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">
|
||||||
Read TL;DR
|
Read TL;DR
|
||||||
@@ -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>© <span x-data x-text="new Date().getFullYear()"></span> ClawFort. All rights reserved.</p>
|
<p>© <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 = '';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
58
openspec/changes/archive/2026-02-13-p14-bugs/design.md
Normal file
58
openspec/changes/archive/2026-02-13-p14-bugs/design.md
Normal 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?
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
30
openspec/changes/archive/2026-02-13-p14-bugs/tasks.md
Normal file
30
openspec/changes/archive/2026-02-13-p14-bugs/tasks.md
Normal 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.
|
||||||
@@ -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)?
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-02-13
|
||||||
100
openspec/changes/archive/2026-02-13-p16-more-bugs/design.md
Normal file
100
openspec/changes/archive/2026-02-13-p16-more-bugs/design.md
Normal 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?
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
43
openspec/changes/archive/2026-02-13-p16-more-bugs/tasks.md
Normal file
43
openspec/changes/archive/2026-02-13-p16-more-bugs/tasks.md
Normal 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.
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
20
openspec/specs/end-to-end-system-testing/spec.md
Normal file
20
openspec/specs/end-to-end-system-testing/spec.md
Normal 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
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
18
openspec/specs/permalink-targeted-image-refetch/spec.md
Normal file
18
openspec/specs/permalink-targeted-image-refetch/spec.md
Normal 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
|
||||||
21
openspec/specs/platform-quality-gates/spec.md
Normal file
21
openspec/specs/platform-quality-gates/spec.md
Normal 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
|
||||||
31
openspec/specs/policy-disclosure-modals/spec.md
Normal file
31
openspec/specs/policy-disclosure-modals/spec.md
Normal 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
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
21
openspec/specs/security-and-performance-test-harness/spec.md
Normal file
21
openspec/specs/security-and-performance-test-harness/spec.md
Normal 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
|
||||||
@@ -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
Reference in New Issue
Block a user