Phase 3: Career Arc Timeline + Portfolio Microsite + RSS Feed Proxy

This commit is contained in:
Tony0410
2026-07-20 21:30:11 +08:00
parent 5bb4b8705c
commit 2c918ae39c
3 changed files with 747 additions and 0 deletions

View File

@@ -12,6 +12,8 @@ import subprocess
import sys
import time
import traceback
import urllib.request
import xml.etree.ElementTree as ET
from datetime import datetime, timezone
from http.server import HTTPServer, SimpleHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
@@ -347,9 +349,65 @@ class DashboardHandler(SimpleHTTPRequestHandler):
})
# GET /ops — serve the ops timeline page
elif path == "/api/feed-proxy":
# Fetch multiple RSS feeds and return as JSON
qs = parse_qs(parsed.query)
# Support both ?urls=url1,url2 and multiple &urls=url1&urls=url2
raw_urls = qs.get("urls", [""])
if isinstance(raw_urls, list) and len(raw_urls) > 1:
# Multiple &urls= params — join them
all_urls = ",".join(raw_urls)
else:
all_urls = raw_urls[0] if isinstance(raw_urls, list) else raw_urls
if not all_urls:
return self._json_response({"error": "Provide ?urls=... (comma-separated URLs)"}, 400)
feed_urls = [u.strip() for u in all_urls.split(",") if u.strip()]
results = []
for fu in feed_urls:
try:
req = urllib.request.Request(fu, headers={
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
"Accept": "application/rss+xml, application/xml, text/xml, */*",
"Referer": "https://anthony.martinwa.org/",
})
with urllib.request.urlopen(req, timeout=15) as resp:
raw = resp.read()
root = ET.fromstring(raw)
# RSS 2.0 or Atom
ns = {"content": "http://purl.org/rss/1.0/modules/content/"}
items = []
# Try RSS 2.0
for item in root.iter("item"):
title = item.findtext("title", "")
link = item.findtext("link", "")
pubdate = item.findtext("pubDate", "")
desc = item.findtext("description", "")
# Strip HTML tags from description
desc_clean = re.sub(r"<[^>]+>", "", desc or "")
items.append({"title": title, "link": link, "date": pubdate[:25] if pubdate else "", "summary": desc_clean[:300]})
# Try Atom
if not items:
for entry in root.iter("{http://www.w3.org/2005/Atom}entry"):
title = entry.findtext("{http://www.w3.org/2005/Atom}title", "")
link_el = entry.find("{http://www.w3.org/2005/Atom}link")
link = link_el.get("href", "") if link_el is not None else ""
pubdate = entry.findtext("{http://www.w3.org/2005/Atom}published", "") or entry.findtext("{http://www.w3.org/2005/Atom}updated", "")
desc = entry.findtext("{http://www.w3.org/2005/Atom}summary", "")
items.append({"title": title, "link": link, "date": pubdate[:25], "summary": (desc or "")[:300]})
results.append({"url": fu, "items": items[:15], "count": len(items)})
except Exception as ex:
results.append({"url": fu, "error": str(ex)[:100], "items": []})
return self._json_response({"feeds": results})
elif path == "/ops":
self.path = "/ops.html"
return super().do_GET()
elif path == "/timeline":
self.path = "/timeline.html"
return super().do_GET()
elif path == "/portfolio":
self.path = "/portfolio.html"
return super().do_GET()
# Serve draft files (cover letters) — preserve extension for browser rendering
# Support both /drafts/ and /jobs/drafts/ paths (Tailscale Serve uses /jobs/drafts/ from prompt)