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:
parent
cb9d62fb5b
commit
728f7784d1
5 changed files with 419 additions and 0 deletions
57
www/analytics.sh
Executable file
57
www/analytics.sh
Executable 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
36
www/forgejo.nginx.conf
Normal 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
202
www/index.html
Normal 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
35
www/repo-stats.sh
Executable 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
89
www/update-costs.sh
Executable 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue