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