cleanyourtv-app
Clean Your TV를 위한 커뮤니티 제작 모바일 앱
저장소 정보는 아직 수집하지 않았습니다.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
메이커가 쓴 소개는 위의 한 줄이 전부입니다. 더 자세한 내용은 저장소 README에서 읽을 수 있습니다. 저장소 README ↗
README 전체를 불러오는 중입니다.
README 전문 보기 ↗SIGNAL scrapes insurance-trade, business, and geopolitical news, scores each article against a weighted-keyword framework, and publishes three daily views:
/) — the front door. Full executive intelligence
layout: 14 domain tiles, live ticker, hero cards, Executive Pulse and a
Featured Analysis. Five tiles are driven by the real scoring engine (Global,
Economy, AI, GCC, Insurance); the other nine are marked "coming soon" until a
scoring engine exists for them. Every content surface reads from
/api/command-center — nothing on the page is hardcoded copy./brief) — a five-lens executive brief (Macro & Geo-Political,
Regulatory & Compliance, Economic & Market, Digital/AI & Automation,
Operating Model & Talent). Compact, low-bandwidth: the view for mobile and travel./signals) — the same articles regrouped under six audience-facing
Signals (Global, Business, AI, GCC, Insurance, Executive), rendered in a
Bloomberg-terminal-style layout. Also compact and image-light.A companion script can also publish a condensed version of the Daily Brief to LinkedIn.
app/
analysis/
command_center.py Maps scored Signals onto the Command Center surfaces —
tiles, hero row, Featured Analysis and Executive Pulse.
Shared by the live API and the static build script.
scorer.py Keyword-weighted scoring engine for the five-lens Daily Brief
signals.py Weighted-evidence classifier for the six Signals (with
near-duplicate suppression and disambiguation rules)
synthesis.py Turns scored articles into the Daily Brief JSON structure
api/
routes.py Flask blueprint: /api/newsletter, /api/signals,
/api/command-center (live Command Center tiles)
scraper/
sources.py Registry of RSS/HTML sources, grouped by geography/beat
scraper.py Async fetch + parse (RSS and HTML fallback), NewsAPI
store.py Once-a-day on-disk cache shared by both views
static/ style.css (Daily Brief), signal.css (Signals terminal theme)
templates/ index.html, signals.html
main.py Flask app factory and routes
config/
config.py Flask config, reads secrets from environment variables
env_check.py Startup validation: fails fast if production is misconfigured
.github/workflows/
ci.yml Runs on every push/PR: install, compile check, import check, tests
scripts/
run_scraper.py Manual one-off scrape, for debugging sources
linkedin_auth.py One-time OAuth flow; writes a LinkedIn access token to .env
daily_linkedin_post.py Scrapes, synthesizes, and posts the brief to LinkedIn
docs/
SIGNAL_Pipeline_Pseudocode.docx Full scoring/classification pipeline reference
tests/ Unit tests for the scorer and scraper module
instance/ Runtime-only: the daily articles_cache.json (not committed)
scraper.py fetches every source in sources.py in parallel (RSS parsed as
XML, everything else as an HTML link scrape), plus NewsAPI if a key is set.store.py caches the result in instance/articles_cache.json for
CACHE_TTL_MINUTES (default 30). A background thread re-scrapes on that
same interval, so the cache is already warm when a reader arrives and
nobody waits on a scrape — set BACKGROUND_REFRESH=0 to disable it. If a
fresh scrape returns nothing, the previous cache is served instead of an
empty page.app/intelligence/freshness.py, tuned in
config/freshness.yaml) drops anything older than 7 days or carrying no
publication date before it can reach a page. Feeds do go stale upstream
while still returning HTTP 200 — moneycontrol.com served April 2024
articles for months — and without the gate those land on a page stamped
with today's date.synthesis.py (Daily Brief) and signals.py (Signals) each independently
score and bucket the same cached articles — two different lenses on one
dataset.python -m venv .venv
.venv\Scripts\activate # Windows
pip install -r requirements.txt
cp .env.example .env # then fill in the values you need
python -m app.main
Open http://127.0.0.1:5000/ for the Command Center; the nav links through to
/brief and /signals. http://127.0.0.1:5000/health returns {"status": "ok"}
for uptime checks.
Force a fresh scrape (bypassing the TTL) by appending ?refresh=1 to any API
endpoint. Set CACHE_TTL_MINUTES in .env to change how often the app
re-scrapes; the Command Center shows a "Sources checked N min ago" stamp so
the page states its own freshness.
See .env.example for the full list with descriptions. Summary:
| Variable | Required | Purpose |
|---|---|---|
APP_ENV | No (defaults to development) | Set to production on real deployments — see Security below |
SECRET_KEY | Yes, when APP_ENV=production | Flask session signing key. Dev has a safe, clearly-labeled fallback |
NEWS_API_KEY | No | Adds NewsAPI.org results to the scrape |
LINKEDIN_CLIENT_ID / LINKEDIN_CLIENT_SECRET | Only for LinkedIn posting | From your LinkedIn developer app |
LINKEDIN_ACCESS_TOKEN / LINKEDIN_PERSON_URN | Only for LinkedIn posting | Written automatically by linkedin_auth.py |
Never commit .env — it's git-ignored. .env.example documents the shape
without any real values. Every variable is validated once at startup by
config/env_check.py: missing optional variables log a warning naming the
feature they disable; a missing SECRET_KEY when APP_ENV=production raises
immediately and refuses to start, rather than silently running with an
insecure key.
python scripts/linkedin_auth.py # one-time browser authorization
python scripts/daily_linkedin_post.py --dry-run # preview without publishing
python scripts/daily_linkedin_post.py # publish
The LinkedIn access token expires roughly every 60 days; rerun
linkedin_auth.py when posting starts failing with an auth error.
python -m unittest discover -s tests -v
.github/workflows/ci.yml runs this, plus a dependency install and a compile
check across every tracked .py file, on every push and pull request.
/signals alone can be published to Vercel as a static site — this sidesteps
the serverless/local-cache mismatch described below entirely, because Vercel
never runs Python for it; it only serves pre-built files.
scripts/build_static_signals.py scrapes fresh, renders the real
signals.html template once, and writes public/index.html,
public/signals.json, and public/signal.css..github/workflows/build-signals.yml runs that script daily at 08:00 IST
(and on-demand via its "Run workflow" button). It commits public/ to the
working branch for history, then force-pushes just those three files to a
dedicated signals-deploy branch — that branch's root only ever
contains those three static files.That dedicated branch exists because Vercel auto-detects a Python project
from requirements.txt and .python-version anywhere it can see them and
tries to build one — even with Root Directory pointed at public/, in
practice this kept triggering. A branch that structurally cannot contain
those files sidesteps the detection entirely rather than fighting it.
To connect it: import the repo in the Vercel dashboard, choose Branch:
signals-deploy during import (not Main), leave Root Directory as the
default. Vercel will see only static HTML/CSS/JSON and deploy with zero
build step.
The page updates once a day on the schedule above; there is no live
?refresh=1 for this static build — use the workflow's manual "Run
workflow" button to force an early rebuild.
news-letter-cxo.vercel.app is wired to signals-deploy, so that branch's
root index.html is the Command Center; the Signals page is published
alongside it as signals.html, served at /signals via vercel.json
cleanUrls. (GitHub's default branch has no effect on what Vercel serves —
only the Vercel project's Production Branch does.)
The Daily Brief (/) and the LinkedIn script
still need the always-on Flask app described below.
This is a standard Flask app with two stateful assumptions that matter for
hosting: it writes a daily JSON cache to local disk (instance/), and the
scrape itself is a multi-minute background-style operation triggered by the
first request of the day. That combination fits a conventional always-on host
with persistent disk (a VPS, Render, Railway, Azure/AWS App Service, or a
Docker container with a mounted volume) far better than a stateless serverless
platform — on serverless, the local cache won't persist between invocations
and a cold-started function may not have time to complete a full scrape.
For an always-on host:
pip install -r requirements.txt
gunicorn "app.main:app" # or another production WSGI server
Set all required environment variables on the host (never hardcode them), and
mount/persist the instance/ directory so the daily cache survives restarts.