Phase 3: Career Arc Timeline + Portfolio Microsite + RSS Feed Proxy
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user