diff --git a/.gitea/workflows/stats.yml b/.gitea/workflows/stats.yml
new file mode 100644
index 0000000..499e88c
--- /dev/null
+++ b/.gitea/workflows/stats.yml
@@ -0,0 +1,37 @@
+name: Regenerate profile stats cards
+# Runs daily; recompute stats cards via the repo's own generate_stats.py
+# (stdlib-only, no external service) and commit them to this repo, where
+# README.md embeds them.
+
+on:
+ schedule:
+ - cron: "30 4 * * *"
+ workflow_dispatch: {}
+
+jobs:
+ stats:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout profile repo
+ uses: actions/checkout@v4
+
+ - name: Generate stats images
+ env:
+ GIT_URL: https://git.turbo-data.com
+ USER: doctasmooth
+ # Token with "read:user" + "read:repository" scopes; include private
+ # repos in the stats. Leave empty to only use public data.
+ ACCESS_TOKEN: ${{ secrets.STATS_READ_TOKEN }}
+ run: python3 generate_stats.py
+
+ - name: Commit updated cards
+ run: |
+ git config user.name "stats-bot"
+ git config user.email "stats-bot@users.noreply.git.turbo-data.com"
+ git add stats/
+ if [[ -n "$(git status --porcelain stats/)" ]]; then
+ git commit -m "chore: regenerate profile stats cards"
+ git push https://${{ secrets.STATS_COMMIT_TOKEN }}@git.turbo-data.com/doctasmooth/.profile.git HEAD:main
+ else
+ echo "Stats unchanged, nothing to commit"
+ fi
diff --git a/README.md b/README.md
index 4ebce99..7beb9fa 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,25 @@
-# .profile
+# doctasmooth
+
+
+
+
+
+
+
+
+## About
+
+Short bio / what I work on goes here.
+
+## Active projects
+
+- [repo-1](https://git.turbo-data.com/doctasmooth/repo-1) — one-line description
+- [repo-2](https://git.turbo-data.com/doctasmooth/repo-2) — one-line description
+
+## Contact
+
+- `@doctasmooth` on this instance
+- email / URL of your choice
diff --git a/generate_stats.py b/generate_stats.py
new file mode 100644
index 0000000..ab57ac0
--- /dev/null
+++ b/generate_stats.py
@@ -0,0 +1,251 @@
+#!/usr/bin/env python3
+"""Generate profile stats cards (SVG) for a Gitea/Forgejo user profile README.
+
+Stdlib only (no pip dependencies). Pulls public data from the Gitea API and
+renders two cards:
+
+ stats/overview.svg - repos, stars earned, forks, followers, stars given
+ stats/languages.svg - top languages bar + legend
+
+Configuration via environment variables:
+ GIT_URL base URL of the instance (default: https://git.turbo-data.com)
+ USER username to generate cards for
+ ACCESS_TOKEN optional API token ("read:user" + "read:repository" scopes);
+ omit if the profile is fully public
+ OUTPUT_DIR where to write the cards (default: stats)
+
+Cards use a transparent background and a neutral palette that stays readable
+in both Gitea light and dark themes.
+"""
+
+import json
+import os
+import sys
+import urllib.parse
+import urllib.request
+from datetime import datetime, timezone
+
+GIT_URL = os.environ.get("GIT_URL", "https://git.turbo-data.com").rstrip("/")
+USER = os.environ.get("USER", "") or sys.exit("USER env var not set")
+TOKEN = os.environ.get("ACCESS_TOKEN", "") # optional
+OUT = os.environ.get("OUTPUT_DIR", "stats")
+
+UA = "gitea-profile-stats/1.0"
+API = GIT_URL + "/api/v1"
+
+# Neutral palette readable on both light (#fff) and dark (#0d1117) backgrounds
+INK = "#768390" # secondary text
+INK_STRONG = "#a5acaf" # primary text
+ACCENT = "#4493f8"
+STROKE = "#768390"
+MAX_LANGS = 5
+
+# Small fallback language color table (used if remote colors fetch fails)
+FALLBACK_COLORS = {
+ "Python": "#3572A5", "TypeScript": "#3178c6", "JavaScript": "#f1e05a",
+ "Shell": "#89e051", "Go": "#00ADD8", "Rust": "#dea584", "C": "#555555",
+ "C++": "#f34b7d", "Java": "#b07219", "HTML": "#e34c26", "CSS": "#563d7c",
+ "SQL": "#e38c00", "Dockerfile": "#384d54", "Makefile": "#427819",
+ "JSON": "#404e5c", "Markdown": "#083fa1", "YAML": "#cb171e",
+ "Ruby": "#701516", "PHP": "#4F5D95", "Lua": "#000080",
+ "SCSS": "#c6538c", "Vue": "#41b883", "Dart": "#00B4AB", "Kotlin": "#A97BFF",
+}
+
+
+def api(path, params=None):
+ """GET {API}{path} -> (json_body, response_headers) or (None, None) on failure."""
+ url = f"{API}{path}"
+ if params:
+ url += "?" + urllib.parse.urlencode(params)
+ req = urllib.request.Request(url, headers={"User-Agent": UA})
+ if TOKEN:
+ req.add_header("Authorization", f"token {TOKEN}")
+ try:
+ with urllib.request.urlopen(req, timeout=15) as r:
+ return json.load(r), dict(r.headers)
+ except Exception: # noqa: BLE001 - some endpoints 401 without a token
+ return None, None
+
+
+def total_count(path, params=None):
+ """Count a list endpoint via X-Total-Count, using a cheap page-1 call.
+
+ Returns None when the endpoint is not available (e.g. 401 without a token).
+ """
+ _body, headers = api(path, dict(params or {}, limit=1))
+ if not headers:
+ return None
+ return int(headers.get("x-total-count", 0))
+
+
+def esc(s):
+ return (
+ str(s).replace("&", "&").replace("<", "<")
+ .replace(">", ">").replace('"', """)
+ )
+
+
+def fetch():
+ user, _ = api(f"/users/{urllib.parse.quote(USER)}")
+ repos, _ = api(f"/users/{urllib.parse.quote(USER)}/repos", {"limit": 50})
+
+ data = {
+ "name": user.get("full_name") or user.get("login", USER),
+ "login": user.get("login", USER),
+ "visit_year": user.get("created", "")[:4],
+ "repositories": user.get("public_repos", len(repos)),
+ "followers": total_count(f"/users/{urllib.parse.quote(USER)}/followers"),
+ "following": total_count(f"/users/{urllib.parse.quote(USER)}/following"),
+ }
+
+ stars = forks = 0
+ by_lang = {}
+ for r in repos:
+ stars += r.get("stars_count", 0) or 0
+ forks += r.get("forks_count", 0) or 0
+ lang = (r.get("language") or "").strip()
+ if lang:
+ weight = r.get("size") or 1 # primary-language weight by repo size
+ by_lang[lang] = by_lang.get(lang, 0) + weight
+ data.update(stars=stars, forks=forks, languages=by_lang,
+ starred=total_count(f"/users/{urllib.parse.quote(USER)}/starred"))
+ return data
+
+
+def colors_for(languages):
+ colors = {}
+ try:
+ req = urllib.request.Request(
+ "https://raw.githubusercontent.com/ozh/github-colors/master/colors.json",
+ headers={"User-Agent": UA},
+ )
+ with urllib.request.urlopen(req, timeout=10) as r:
+ colors = {k: "#" + v["color"] for k, v in json.load(r).items()
+ if k in languages}
+ except Exception: # noqa: BLE001 - offline instances still need cards
+ pass
+ autofill = [f"#{(abs(hash(l)) % 0xAAAAAA) + 0x555555:06x}"
+ for i, l in enumerate(languages)]
+ return {l: colors.get(l, FALLBACK_COLORS.get(l, c))
+ for l, c in zip(languages, autofill)}
+
+
+def svg_head(w, h):
+ return (f'"
+ )
+
+
+def draw_languages(d):
+ langs = sorted(d["languages"].items(), key=lambda kv: -kv[1])[:MAX_LANGS]
+ cells = []
+ if langs:
+ total = sum(v for _, v in langs)
+ colors = colors_for([l for l, _ in langs])
+ x = 26
+ bar_w = 428
+ segs = []
+ legend = []
+ for lang, v in langs:
+ share = v / total
+ w = bar_w * share if share > 0.02 else max(bar_w * share, 3)
+ segs.append(
+ f''
+ )
+ lx = 26 + ((len(legend) % 2) * 214)
+ ly = 106 + (len(legend) // 2) * 26
+ legend.append(
+ f''
+ f''
+ + esc(lang)
+ + f''
+ f"{share * 100:.1f}%"
+ )
+ x += w
+ label = f"Top languages"
+ footer_y = 186 if len(legend) > 3 else 146
+ cells = (
+ [label]
+ + [f''
+ for bar_x, bar_y in [(25.5, 65.5)]]
+ + segs
+ + legend
+ + [f''
+ + esc("updated " + datetime.now(timezone.utc).strftime("%Y-%m-%d"))
+ + ""]
+ )
+ height = footer_y + 16
+ else:
+ height = 100
+ cells = [
+ f'No public repositories '
+ f'with languages yet.',
+ ]
+ body = (
+ svg_head(480, height)
+ + f''
+ + "".join(cells)
+ + ""
+ )
+ return body
+
+
+def main():
+ os.makedirs(OUT, exist_ok=True)
+ d = fetch()
+ print(f"Loaded profile for {d['login']}: {d['repositories']} repos, "
+ f"{d['stars']} stars, {d['forks']} forks, {d['followers']} followers")
+ for name, svg in (("overview", draw_overview(d)),
+ ("languages", draw_languages(d))):
+ path = os.path.join(OUT, f"{name}.svg")
+ with open(path, "w", encoding="utf-8") as f:
+ f.write(svg)
+ print(f"wrote {path}")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/stats/languages.svg b/stats/languages.svg
new file mode 100644
index 0000000..3f940dc
--- /dev/null
+++ b/stats/languages.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/stats/overview.svg b/stats/overview.svg
new file mode 100644
index 0000000..4c1071d
--- /dev/null
+++ b/stats/overview.svg
@@ -0,0 +1 @@
+
\ No newline at end of file