blog/lifelab/the everything software for no one
the everything software for no one

With the advent of agentic coding I have finally finished every single idea I ever had. All software projects and experiences are just finished and of higher quality than if I were to do it myself. I finally made a malleable personal computing platform on the web, where each user can modify their own environment via in-browser live-scripting. You can make mini apps that have state, can query and update your notes.

... but why would you? Why would you constrain yourself to my set of APIs, when you can have a local database file (or, yuck, a bunch of markdown files) and just have claude make whatever interface you need on demand?

I feel the same tools, that enabled me to make my dream software (which I imagined people would take and personalize), also made the said software pointless. I enjoy trying to pierce together python scripts in a life dev environment, but even I will admit it is 1 claude prompt away.

Maybe the real software of future will be harnesses that can natively load claude and render react scripts and expose a memory.

P.S. I made an anime tracker that shows up upcoming and overdue shows I had on anilist. It demonstrates:

  1. two way sync between lifelab and anilist
  2. a secrets api in lifelab to hold api keys
  3. a way to make interactive buttons that modify block state
Anime Tracker
from Anime Tracker
Show code
import json
from datetime import datetime

TOKEN = nb.secret("ANILIST_TOKEN")
API = "https://graphql.anilist.co"
refresh = False
inputs = nb_ctx.get("inputs", {}) if "nb_ctx" in dir() else {}

# ── AniList ──────────────────────────────────────────────

def gql(query, variables=None):
    payload = json.dumps({"query": query, "variables": variables or {}})
    resp = nb.fetch(API, method="POST",
        headers={
            "Content-Type": "application/json",
            "Accept": "application/json",
            "User-Agent": "lifelab/1.0",
            "Authorization": f"Bearer {TOKEN}",
        },
        body=payload)
    raw = resp.get("body") or ""
    if not resp.get("ok"):
        raise Exception(f"AniList {resp.get('status')}: {raw}")
    data = json.loads(raw)
    if "errors" in data:
        raise Exception(data["errors"][0]["message"])
    return data["data"]

def fetch_watching():
    user_id = gql("{ Viewer { id } }")["Viewer"]["id"]
    data = gql("""
    query ($userId: Int) {
      MediaListCollection(userId: $userId, type: ANIME, status: CURRENT) {
        lists { entries {
          progress
          media {
            id
            title { romaji english }
            status episodes
            nextAiringEpisode { airingAt timeUntilAiring episode }
            siteUrl
          }
        }}
      }
    }""", {"userId": user_id})

    results = []
    for lst in data["MediaListCollection"]["lists"]:
        for entry in lst["entries"]:
            m = entry["media"]
            progress = entry.get("progress") or 0
            nxt = m.get("nextAiringEpisode")

            if m["status"] not in ("RELEASING", "FINISHED"):
                continue

            if nxt and nxt.get("episode"):
                latest = nxt["episode"] - 1
                airing_at = nxt.get("airingAt")
            elif m.get("episodes"):
                latest = m["episodes"]
                airing_at = None
            else:
                continue

            results.append({
                "id": m["id"],
                "title": m["title"].get("english") or m["title"]["romaji"],
                "progress": progress,
                "latest": latest,
                "behind": latest - progress,
                "airing_at": airing_at,
                "next_ep": nxt.get("episode") if nxt else None,
                "url": m["siteUrl"],
            })

    results.sort(key=lambda x: x["behind"], reverse=True)
    return results

# ── Handle bumps, then load (cached or fresh) ─────────

block = nb.current_block
block_id = block["id"] if block else None
cache = (block.get("metadata") or {}).get("_cache") if block else None

for key, val in inputs.items():
    if key.startswith("b:") and val:
        aid = int(key.split(":")[1])
        new_progress = int(key.split(":")[2]) + 1
        gql("""mutation ($mediaId: Int, $progress: Int) {
          SaveMediaListEntry(mediaId: $mediaId, progress: $progress) { id }
        }""", {"mediaId": aid, "progress": new_progress})
        # Update cache in-place so we don't need a full refetch
        if cache:
            for a in cache:
                if a["id"] == aid:
                    a["progress"] = new_progress
                    a["behind"] = a["latest"] - new_progress
                    break

if refresh or not cache:
    anime_list = fetch_watching()
    if block_id:
        nb.patch_metadata(block_id, {"_cache": anime_list})
else:
    anime_list = cache

# ── Visibility filter ───────────────────────────────────

now_ts = int(datetime.utcnow().timestamp())

visible = []
for a in anime_list:
    if a["behind"] > 0:
        visible.append(a)
    elif a.get("airing_at") and (a["airing_at"] - now_ts) <= 3 * 86400:
        a["_soon"] = True
        visible.append(a)

# ── Render ───────────────────────────────────────────────

# Always render something so we can test UI works at all

if visible:
    rows = []
    for a in visible:
        if a["behind"] > 0:
            rows.append(ui.row([
                ui.page_link(f"anime/{a['title']}", label=a['title']),
                ui.text(f"{a['progress']}/{a['latest']} ({a['behind']} ep behind)"),
                ui.button("+1", on_click=actions.rerun_with({f"b:{a['id']}:{a['progress']}": "true"})),
            ]))
        elif a.get("_soon"):
            secs = a["airing_at"] - now_ts
            d, h = secs // 86400, (secs % 86400) // 3600
            rows.append(ui.row([
                ui.page_link(f"anime/{a['title']}", label=a['title']),
                ui.badge(f"Ep {a['next_ep']} in {d}d {h}h", variant="warning"),
            ]))

    ui.column([
        *rows,
    ])