diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
new file mode 100644
index 0000000..21cb5f0
--- /dev/null
+++ b/.github/workflows/nightly.yml
@@ -0,0 +1,37 @@
+name: Nightly Telemetry
+
+on:
+ schedule:
+ - cron: "17 6 * * *"
+ workflow_dispatch:
+
+concurrency:
+ group: demon-state
+ cancel-in-progress: false
+
+permissions:
+ contents: write
+
+jobs:
+ refresh:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Refresh feed, stats, featured cards, entropy field
+ env:
+ GITHUB_TOKEN: ${{ github.token }}
+ run: python3 engine/nightly.py
+
+ - name: Commit if changed
+ run: |
+ if git diff --quiet; then
+ echo "no changes"
+ exit 0
+ fi
+ git config user.name "github-actions[bot]"
+ git config user.email "41898470+github-actions[bot]@users.noreply.github.com"
+ git add README.md state/demon.json assets/entropy.svg
+ git commit -m "chore: nightly telemetry refresh"
+ git pull --rebase origin "${GITHUB_REF_NAME}"
+ git push
diff --git a/.github/workflows/snake.yml b/.github/workflows/snake.yml
index 81849ab..988375b 100644
--- a/.github/workflows/snake.yml
+++ b/.github/workflows/snake.yml
@@ -4,19 +4,15 @@ on:
schedule:
# Runs every 12 hours
- cron: "0 */12 * * *"
-
+
workflow_dispatch:
-
- push:
- branches:
- - main
jobs:
generate:
runs-on: ubuntu-latest
permissions:
contents: write
-
+
steps:
- name: Generate Snake
uses: Platane/snk/svg-only@v3
@@ -25,11 +21,11 @@ jobs:
outputs: |
dist/github-contribution-grid-snake.svg
dist/github-contribution-grid-snake-dark.svg?palette=github-dark
-
+
- name: Push to output branch
uses: crazy-max/ghaction-github-pages@v4
with:
target_branch: output
build_dir: dist
env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
\ No newline at end of file
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/engine/nightly.py b/engine/nightly.py
new file mode 100644
index 0000000..d4358b2
--- /dev/null
+++ b/engine/nightly.py
@@ -0,0 +1,210 @@
+#!/usr/bin/env python3
+"""Nightly telemetry refresh for the profile README.
+
+Updates the marker-fenced sections of README.md:
+ FEED — 4 newest Strange Quarks articles (RSS if populated, else the
+ server-rendered blog HTML)
+ STATS — telemetry row with live star/repo totals
+ FEATURED — star counts on the featured project cards
+and bumps the entropy field's chaos generation so the field differs daily.
+
+Every section is fail-safe: if a fetch or parse fails, that section is left
+untouched and the script still exits 0. Stdlib only.
+"""
+
+import datetime
+import html as htmllib
+import json
+import os
+import re
+import subprocess
+import sys
+import urllib.request
+import xml.etree.ElementTree as ET
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+README = os.path.join(ROOT, "README.md")
+STATE_PATH = os.path.join(ROOT, "state", "demon.json")
+
+SITE = "https://stevenmilanese.com"
+FEED_URL = f"{SITE}/feed.xml"
+BLOG_URL = f"{SITE}/blog"
+
+FEATURED_REPOS = [
+ ("anthropic-certs", "Verified Anthropic certifications — Claude, API, Claude Code CLI, MCP."),
+ ("slTrain", "A steam locomotive for your terminal, smoke trail included."),
+ ("meowchi-releases", "A desktop pet that evolves when you actually get work done."),
+ ("mpl", "Mathematics Programming Language — write the equation, run the equation."),
+]
+
+
+def fetch(url, accept=None):
+ req = urllib.request.Request(url, headers={"User-Agent": "developtheweb-nightly/1.0"})
+ if accept:
+ req.add_header("Accept", accept)
+ token = os.environ.get("GITHUB_TOKEN")
+ if token and url.startswith("https://api.github.com"):
+ req.add_header("Authorization", f"Bearer {token}")
+ with urllib.request.urlopen(req, timeout=30) as resp:
+ return resp.read().decode("utf-8", errors="replace")
+
+
+def gh_api(path):
+ return json.loads(fetch(f"https://api.github.com{path}", accept="application/vnd.github+json"))
+
+
+def replace_section(text, marker, body):
+ start, end = f"", f""
+ if start not in text or end not in text:
+ raise ValueError(f"missing {marker} markers")
+ pre = text.split(start)[0]
+ post = text.split(end)[1]
+ return f"{pre}{start}\n{body}\n{end}{post}"
+
+
+def articles_from_rss():
+ root = ET.fromstring(fetch(FEED_URL))
+ items = []
+ for item in root.iter("item"):
+ title = (item.findtext("title") or "").strip()
+ link = (item.findtext("link") or "").strip()
+ pub = (item.findtext("pubDate") or "").strip()
+ date = None
+ try:
+ date = datetime.datetime.strptime(pub[:16].strip(), "%a, %d %b %Y")
+ except ValueError:
+ pass
+ if title and link:
+ items.append({"title": title, "url": link, "date": date, "meta": ""})
+ return items
+
+
+def articles_from_html():
+ page = fetch(BLOG_URL)
+ skip = ("/blog/tag/", "/blog/author/")
+ anchors = [
+ m for m in re.finditer(r']+href="(/blog/[a-z0-9][a-z0-9-]*)"', page)
+ if not m.group(1).startswith(skip)
+ ]
+ seen, articles = set(), []
+ for idx, m in enumerate(anchors):
+ slug = m.group(1)
+ if slug in seen:
+ continue
+ seen.add(slug)
+ stop = len(page)
+ for nxt in anchors[idx + 1:]:
+ if nxt.group(1) != slug:
+ stop = nxt.start()
+ break
+ window = page[m.start():stop]
+ title_m = re.search(r'alt="([^"]+)"', window)
+ stripped = re.sub(r"<[^>]+>", " ", window)
+ stripped = htmllib.unescape(re.sub(r"", "", stripped))
+ date_m = re.search(r"([A-Z][a-z]+ \d{1,2}, \d{4})", stripped)
+ mins_m = re.search(r"(\d+)\s*min", stripped)
+ views_m = re.search(r"([\d,]+)\s*views", stripped)
+ if not (title_m and date_m):
+ continue
+ date = datetime.datetime.strptime(date_m.group(1), "%B %d, %Y")
+ meta = f"{mins_m.group(1)} min read" if mins_m else (
+ f"{views_m.group(1)} views" if views_m else ""
+ )
+ articles.append({
+ "title": htmllib.unescape(title_m.group(1)),
+ "url": f"{SITE}{slug}",
+ "date": date,
+ "meta": meta,
+ })
+ return articles
+
+
+def update_feed(text):
+ articles = []
+ try:
+ articles = articles_from_rss()
+ except Exception as exc:
+ print(f"feed: rss unusable ({exc}); falling back to html")
+ if not articles:
+ articles = articles_from_html()
+ dated = [a for a in articles if a["date"]]
+ dated.sort(key=lambda a: a["date"], reverse=True)
+ newest = dated[:4]
+ if len(newest) < 4:
+ raise ValueError(f"only {len(newest)} parseable articles")
+ rows = ["| Article | |", "|:---|---:|"]
+ for a in newest:
+ title = a["title"].replace("|", "\\|")
+ when = a["date"].strftime("%b %d, %Y").replace(" 0", " ")
+ meta = f"{when} · {a['meta']}" if a["meta"] else when
+ rows.append(f"| [{title}]({a['url']}) | {meta} |")
+ return replace_section(text, "FEED", "\n".join(rows))
+
+
+def update_stats(text):
+ repos = gh_api("/users/developtheweb/repos?per_page=100&type=owner")
+ public = [r for r in repos if not r["private"]]
+ stars = sum(r["stargazers_count"] for r in public)
+ line = (
+ "
★ {repo["stargazers_count"]}
{blurb}
\n' + f'