| Server IP : 3.147.158.171 / Your IP : 216.73.216.216 Web Server : Apache/2.4.67 (Amazon Linux) OpenSSL/3.5.5 System : Linux ip-172-31-2-178.us-east-2.compute.internal 6.1.172-216.329.amzn2023.x86_64 #1 SMP PREEMPT_DYNAMIC Wed May 20 06:31:34 UTC 2026 x86_64 User : ec2-user ( 1000) PHP Version : 8.4.21 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /tsai/repo/api/app/services/ |
Upload File : |
"""Append-only log of cron posting attempts (articles + comments).
The hourly cron scripts (`create_articles.py`, the comments agent) record every
posting attempt here; the schedule calendar (`schedule_calendar.py`) reads it
back to render concrete "what was posted" events with links and success/failure
status alongside the forward-looking recurring schedule.
Storage is a per-environment JSONL file under `app/data/` — one JSON object per
line — mirroring the `site_configs_{env}.json` cache pattern. JSONL is chosen so
independent processes can append safely without a read-modify-write race: the
two timers run sequentially (articles :05, comments :35) and each write is a
single short line. Recording is best-effort and never raises into the caller — a
logging failure must not break posting.
"""
import json
import logging
import os
from datetime import datetime, timedelta
from pathlib import Path
from typing import List, Optional
from ..appTypes import PostingRecord, SiteConfig
CACHE_DIR = Path(__file__).parent.parent / 'data'
def _history_path() -> Path:
"""Env-suffixed history file, matching SiteConfigService's cache mapping."""
env = os.getenv('TSAI_ENV', 'development')
if env == 'production':
suffix = 'tsai'
elif env == 'scike_ai':
suffix = 'scike'
else:
suffix = 'development'
return CACHE_DIR / f'posting_history_{suffix}.jsonl'
def record_post(
site: SiteConfig,
*,
kind: str,
character: str,
status: str,
character_slug: Optional[str] = None,
storage_id: Optional[str] = None,
post_id: Optional[int] = None,
url: Optional[str] = None,
title: Optional[str] = None,
excerpt: Optional[str] = None,
model: Optional[str] = None,
error: Optional[str] = None,
when: Optional[datetime] = None,
) -> None:
"""Append one posting attempt to the history log. Best-effort — never raises."""
try:
now = when or datetime.now()
record = PostingRecord(
ts=now.isoformat(timespec='seconds'),
date=now.strftime('%Y-%m-%d'),
site=site.slug,
kind=kind,
character=character,
status=status,
character_slug=character_slug,
storage_id=storage_id,
post_id=post_id,
url=url,
title=title,
excerpt=excerpt,
model=model,
error=error,
)
path = _history_path()
path.parent.mkdir(parents=True, exist_ok=True)
with open(path, 'a', encoding='utf-8') as f:
f.write(json.dumps(record.model_dump(exclude_none=True)) + '\n')
f.flush()
except Exception as e:
logging.error(f"Failed to record posting history ({kind}/{character}): {e}")
def read_history(site_slug: str, *, since_days: int = 60) -> List[PostingRecord]:
"""Return this site's posting records within the last `since_days` days.
Tolerant of malformed / partially-written trailing lines — those are skipped.
"""
path = _history_path()
if not path.exists():
return []
cutoff = (datetime.now() - timedelta(days=since_days)).strftime('%Y-%m-%d')
records: List[PostingRecord] = []
try:
with open(path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line:
continue
try:
data = json.loads(line)
except json.JSONDecodeError:
continue
if data.get('site') != site_slug:
continue
if str(data.get('date', '')) < cutoff:
continue
try:
records.append(PostingRecord(**data))
except Exception:
continue
except Exception as e:
logging.error(f"Failed to read posting history for {site_slug}: {e}")
return records
return records