Add www/ directory to repository

Landing page, nginx config, analytics scripts, and cost updater
are now tracked in git. update-costs.sh writes to both the live
(/home/claude/www/) and repo copies.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
claude 2026-03-16 15:41:54 +00:00
parent cb9d62fb5b
commit 728f7784d1
5 changed files with 419 additions and 0 deletions

57
www/analytics.sh Executable file
View file

@ -0,0 +1,57 @@
#!/bin/bash
# Simple analytics for llm-impact.org
# Parses nginx access logs. Run as root or a user with log read access.
#
# Usage: ./analytics.sh [days]
# days: how many days back to analyze (default: 7)
set -euo pipefail
DAYS="${1:-7}"
LOG="/var/log/nginx/access.log"
CUTOFF=$(date -d "$DAYS days ago" +%d/%b/%Y)
if [ ! -r "$LOG" ]; then
echo "Error: Cannot read $LOG (run as root or add user to adm group)"
exit 1
fi
echo "=== llm-impact.org analytics (last $DAYS days) ==="
echo
# Filter to recent entries, exclude assets and known scanners
recent=$(awk -v cutoff="$CUTOFF" '
$4 ~ cutoff || $4 > "["cutoff { print }
' "$LOG" \
| grep -v -E '\.(css|js|ico|png|jpg|svg|woff|ttf|map)' \
| grep -v -iE '(bot|crawler|spider|leakix|zgrab|masscan|nmap)' \
| grep -v -E '\.(env|php|git|xml|yml|yaml|bak|sql)')
if [ -z "$recent" ]; then
echo "No matching requests in the last $DAYS days."
exit 0
fi
# Unique IPs (proxy for unique visitors)
unique_ips=$(echo "$recent" | awk '{print $1}' | sort -u | wc -l)
echo "Unique IPs: $unique_ips"
# Total requests (excluding assets)
total=$(echo "$recent" | wc -l)
echo "Total page requests: $total"
echo
echo "=== Top pages ==="
echo "$recent" | awk '{print $7}' | sort | uniq -c | sort -rn | head -10
echo
echo "=== Referrers (external) ==="
# In combined log format: IP - - [date] "request" status size "referer" "ua"
echo "$recent" | awk -F'"' '{print $4}' | grep -v -E '(^-$|^$|llm-impact\.org)' | sort | uniq -c | sort -rn | head -10
echo
echo "=== Landing page vs repo ==="
landing=$(echo "$recent" | awk '$7 == "/" || $7 == "/index.html"' | wc -l)
forge=$(echo "$recent" | grep '/forge/' | wc -l)
echo "Landing page: $landing"
echo "Forge (repo): $forge"

36
www/forgejo.nginx.conf Normal file
View file

@ -0,0 +1,36 @@
server {
server_name llm-impact.org;
root /home/claude/www;
index index.html;
location / {
try_files $uri $uri/ =404;
}
location /forge/ {
proxy_pass http://127.0.0.1:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 100M;
}
listen 443 ssl; # managed by Certbot
listen [::]:443 ssl ipv6only=on; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/llm-impact.org/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/llm-impact.org/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
server {
if ($host = llm-impact.org) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
listen [::]:80;
server_name llm-impact.org;
return 404; # managed by Certbot
}

202
www/index.html Normal file
View file

@ -0,0 +1,202 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AI Conversation Impact</title>
<meta name="description" content="A framework for estimating the full cost of conversations with large language models — environmental, financial, social, and political.">
<style>
:root {
--bg: #fafaf8;
--fg: #1a1a1a;
--muted: #555;
--accent: #2a6e3f;
--border: #ddd;
--card-bg: #fff;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #1a1a1a;
--fg: #e8e8e4;
--muted: #aaa;
--accent: #5cb87a;
--border: #333;
--card-bg: #242424;
}
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--fg);
line-height: 1.6;
}
.container {
max-width: 720px;
margin: 0 auto;
padding: 4rem 1.5rem;
}
h1 {
font-size: 2rem;
font-weight: 700;
margin-bottom: 0.5rem;
}
.subtitle {
font-size: 1.15rem;
color: var(--muted);
margin-bottom: 2.5rem;
}
.stat-row {
display: flex;
gap: 1.5rem;
margin-bottom: 2.5rem;
flex-wrap: wrap;
}
.stat {
flex: 1;
min-width: 140px;
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 1rem 1.25rem;
}
.stat .number {
font-size: 1.5rem;
font-weight: 700;
color: var(--accent);
}
.stat .label {
font-size: 0.85rem;
color: var(--muted);
}
h2 {
font-size: 1.25rem;
font-weight: 600;
margin: 2rem 0 0.75rem;
}
p, li {
color: var(--fg);
margin-bottom: 0.5rem;
}
ul {
padding-left: 1.25rem;
margin-bottom: 1rem;
}
a {
color: var(--accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.links {
display: flex;
gap: 1rem;
margin-top: 2rem;
flex-wrap: wrap;
}
.links a {
display: inline-block;
padding: 0.6rem 1.25rem;
border: 1px solid var(--accent);
border-radius: 6px;
font-weight: 500;
font-size: 0.95rem;
}
.links a.primary {
background: var(--accent);
color: #fff;
}
.links a.primary:hover {
opacity: 0.9;
text-decoration: none;
}
.links a:hover {
text-decoration: none;
}
.caveat {
margin-top: 3rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
font-size: 0.9rem;
color: var(--muted);
}
footer {
margin-top: 3rem;
font-size: 0.85rem;
color: var(--muted);
}
</style>
</head>
<body>
<div class="container">
<h1>AI Conversation Impact</h1>
<p class="subtitle">Beyond carbon: a framework for the full cost of AI conversations — environmental, social, epistemic, and political.</p>
<div class="stat-row">
<div class="stat">
<div class="number">20+</div>
<div class="label">Cost categories across 5 dimensions</div>
</div>
<div class="stat">
<div class="number">100-250 Wh</div>
<div class="label">Energy per long conversation</div>
</div>
<div class="stat">
<div class="number">CC0</div>
<div class="label">Public domain, no restrictions</div>
</div>
</div>
<h2>The problem</h2>
<p>Most tools for measuring AI's impact stop at energy and CO2. But the costs that matter most — cognitive deskilling, data pollution, algorithmic monoculture, power concentration — are invisible precisely because no one is tracking them. This project names and organizes those costs so they cannot be ignored.</p>
<h2>What makes this different</h2>
<p>Existing tools like <a href="https://ecologits.ai/">EcoLogits</a> and <a href="https://codecarbon.io/">CodeCarbon</a> measure environmental metrics well. We don't compete with them — we complement them. This methodology adds the dimensions they don't cover:</p>
<ul>
<li><strong>Social</strong> — annotation labor conditions, cognitive deskilling (<a href="https://dl.acm.org/doi/full/10.1145/3706598.3713778">CHI 2025</a>), linguistic homogenization</li>
<li><strong>Epistemic</strong> — code quality degradation, data pollution (<a href="https://www.nature.com/articles/s41586-024-07566-y">Nature, 2024</a>), research integrity</li>
<li><strong>Political</strong> — power concentration, data sovereignty, opaque content filtering</li>
<li><strong>Environmental</strong> — calibrated against <a href="https://arxiv.org/abs/2508.15734">Google (2025)</a> and <a href="https://arxiv.org/abs/2505.09598">Jegham et al. (2025)</a> published data</li>
<li><strong>Financial</strong> — compute costs, creative market displacement, opportunity cost</li>
</ul>
<p>The goal is not zero AI usage but <strong>net-positive</strong> usage. The framework includes positive impact metrics (reach, counterfactual value, durability) alongside costs.</p>
<h2>What's here</h2>
<ul>
<li><strong>A methodology</strong> covering 20+ cost categories with estimation methods where possible and honest acknowledgment where not.</li>
<li><strong>A toolkit</strong> for <a href="https://claude.ai/claude-code">Claude Code</a> that automatically tracks environmental, financial, and social cost proxies (deskilling risk, code quality, data pollution, provider concentration) during sessions.</li>
<li><strong>A related work survey</strong> mapping existing tools and research so you can see where this fits.</li>
</ul>
<h2>Help improve the estimates</h2>
<p>Many figures have low confidence. If you have data center measurements, inference cost data, or research on the social costs of AI, your corrections are welcome.</p>
<div class="links">
<a class="primary" href="/forge/claude/ai-conversation-impact/src/branch/main/impact-methodology.md">Read the methodology</a>
<a href="/forge/claude/ai-conversation-impact">Browse the repository</a>
<a href="/forge/claude/ai-conversation-impact/issues/1">Contribute corrections</a>
</div>
<div class="caveat">
<strong>Limitations:</strong> The quantifiable costs are almost certainly the least important ones. Effects like deskilling, data pollution, and power concentration cannot be reduced to numbers. This is a tool for honest approximation, not precise accounting.
</div>
<div class="caveat">
<strong>How this was made:</strong>
This project was developed by a human
directing <a href="https://claude.ai">Claude</a> (Anthropic's AI assistant)
across multiple conversations. The methodology was applied to itself:
across 3 tracked sessions, the project has consumed
~295 Wh of energy, ~95g of CO2, and ~$98 in
compute. Whether it produces enough value to justify those costs depends
on whether anyone finds it useful. We are
<a href="/forge/claude/ai-conversation-impact/src/branch/main/plans/measure-project-impact.md">tracking that question</a>.
</div>
<footer>
<a href="https://creativecommons.org/publicdomain/zero/1.0/">CC0 1.0</a> — public domain. No restrictions on use.
</footer>
</div>
</body>
</html>

35
www/repo-stats.sh Executable file
View file

@ -0,0 +1,35 @@
#!/bin/bash
# Query Forgejo API for repository statistics
# No authentication needed for public repo data.
#
# Usage: ./repo-stats.sh
set -euo pipefail
BASE="http://127.0.0.1:3000/api/v1"
REPO="claude/ai-conversation-impact"
echo "=== Repository stats for $REPO ==="
echo
# Repo info
info=$(curl -s "$BASE/repos/$REPO")
stars=$(echo "$info" | python3 -c "import sys,json; print(json.load(sys.stdin).get('stars_count', 0))")
forks=$(echo "$info" | python3 -c "import sys,json; print(json.load(sys.stdin).get('forks_count', 0))")
watchers=$(echo "$info" | python3 -c "import sys,json; print(json.load(sys.stdin).get('watchers_count', 0))")
echo "Stars: $stars"
echo "Forks: $forks"
echo "Watchers: $watchers"
# Open issues
issues=$(curl -s "$BASE/repos/$REPO/issues?state=open&type=issues&limit=50")
issue_count=$(echo "$issues" | python3 -c "import sys,json; print(len(json.load(sys.stdin)))")
echo "Open issues: $issue_count"
echo
echo "=== Recent issues ==="
echo "$issues" | python3 -c "
import sys, json
issues = json.load(sys.stdin)
for i in issues[:10]:
print(f\" #{i['number']} {i['title']} ({i['created_at'][:10]})\")" 2>/dev/null || echo " (none)"

89
www/update-costs.sh Executable file
View file

@ -0,0 +1,89 @@
#!/bin/bash
#
# update-costs.sh — Update the landing page with latest project cost
# estimates from the impact log.
#
# Reads .claude/impact/impact-log.jsonl, takes the latest snapshot per
# session, sums totals, and updates index.html in place.
#
# Usage: ./update-costs.sh
#
# Run after each conversation or as a cron job.
set -euo pipefail
IMPACT_LOG="/home/claude/claude-dir/.claude/impact/impact-log.jsonl"
if [ ! -f "$IMPACT_LOG" ]; then
echo "No impact log found at $IMPACT_LOG"
exit 1
fi
python3 << 'PYEOF'
import json, re, sys
IMPACT_LOG = "/home/claude/claude-dir/.claude/impact/impact-log.jsonl"
PAGES = ["/home/claude/www/index.html", "/home/claude/claude-dir/www/index.html"]
# Read all entries, keep latest per session
sessions = {}
with open(IMPACT_LOG) as f:
for line in f:
line = line.strip()
if not line:
continue
d = json.loads(line)
sid = d.get("session_id", "")
if sid == "test-123" or not sid:
continue
ts = d.get("timestamp", "")
if sid not in sessions or ts > sessions[sid]["timestamp"]:
sessions[sid] = d
if not sessions:
print("No sessions found in impact log")
sys.exit(0)
# Sum across sessions
n = len(sessions)
total_energy = sum(d.get("energy_wh", 0) for d in sessions.values())
total_co2 = sum(d.get("co2_g", 0) for d in sessions.values())
total_cost_cents = sum(d.get("cost_cents", 0) for d in sessions.values())
# Format cost
if total_cost_cents >= 100:
cost_display = f"${total_cost_cents // 100}"
else:
cost_display = f"{total_cost_cents}c"
# Pluralize
session_word = "session" if n == 1 else "sessions"
# Build replacement paragraph
new_para = (
f"How this was made:</strong>\n"
f" This project was developed by a human\n"
f" directing <a href=\"https://claude.ai\">Claude</a> (Anthropic's AI assistant)\n"
f" across multiple conversations. The methodology was applied to itself:\n"
f" across {n} tracked {session_word}, the project has consumed\n"
f" ~{total_energy} Wh of energy, ~{total_co2}g of CO2, and ~{cost_display} in\n"
f" compute. Whether it produces enough value to justify those costs depends\n"
f" on whether anyone finds it useful. We are\n"
f' <a href="/forge/claude/ai-conversation-impact/src/branch/main/plans/measure-project-impact.md">tracking that question</a>.\n'
f" </div>"
)
# Read and replace in all landing page copies
pattern = r"How this was made:</strong>.*?</div>"
for page in PAGES:
try:
with open(page) as f:
html = f.read()
new_html = re.sub(pattern, new_para, html, flags=re.DOTALL)
with open(page, "w") as f:
f.write(new_html)
except FileNotFoundError:
pass
print(f"Updated: {n} {session_word}, {total_energy} Wh, {total_co2}g CO2, {cost_display}")
PYEOF