commit ad7c3a860bbd1abbbafb0b24f90ce83f5a5d714f Author: ai.syui.ai Date: Tue Mar 10 23:16:42 2026 +0900 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be64e3c --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +/tmp +/claude.md +/CLAUDE.md +.claude +.DS_Store +.env +/envs/knot +/envs/appview +/deploy.yml +/install.zsh +/keys +/data +/repositories +/server diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..68e2018 --- /dev/null +++ b/compose.yml @@ -0,0 +1,44 @@ +services: + redis: + image: redis:alpine + restart: always + volumes: + - ./data/redis:/data + healthcheck: + test: ["CMD", "redis-cli", "ping", "|", "grep", "PONG"] + interval: 1s + timeout: 5s + retries: 5 + + knot: + build: + context: ./repos/knot-docker/ + args: + UID: 1000 + GID: 1000 + restart: always + ports: + - "2588:5555" + - "2222:22" + volumes: + - ./keys:/etc/ssh/keys + - ./repositories:/home/git/repositories + - ./server:/app + env_file: + - ./envs/knot + + appview: + build: + context: . + dockerfile: docker/appview/Dockerfile + target: appview + restart: always + ports: + - "2589:3000" + volumes: + - ./data/appview:/data + env_file: + - ./envs/appview + depends_on: + redis: + condition: service_healthy diff --git a/docker/appview/Dockerfile b/docker/appview/Dockerfile new file mode 100644 index 0000000..7b3127e --- /dev/null +++ b/docker/appview/Dockerfile @@ -0,0 +1,49 @@ +FROM golang:1.25-bookworm AS builder +RUN apt-get update && apt-get install -y git gcc libc6-dev nodejs npm curl unzip && rm -rf /var/lib/apt/lists/* +WORKDIR /build +COPY repos/core/ . + +# static assets required for go:embed +RUN mkdir -p appview/pages/static/fonts appview/pages/static/icons appview/pages/static/logos + +# JS +RUN curl -sLo appview/pages/static/htmx.min.js https://unpkg.com/htmx.org@2.0.4/dist/htmx.min.js && \ + curl -sLo appview/pages/static/htmx-ext-ws.min.js https://unpkg.com/htmx-ext-ws@2.0.2/ws.js && \ + curl -sLo appview/pages/static/mermaid.min.js https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js && \ + touch appview/pages/static/actor-typeahead.js + +# Fonts +RUN curl -sLo /tmp/inter.zip https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip && \ + cd /tmp && unzip -q inter.zip -d inter && \ + find /tmp/inter -name 'InterVariable*.woff2' -exec cp {} /build/appview/pages/static/fonts/ \; && \ + find /tmp/inter -name 'InterDisplay*.woff2' -exec cp {} /build/appview/pages/static/fonts/ \; || true + +RUN curl -sLo /tmp/plex.zip https://github.com/IBM/plex/releases/download/v6.4.0/IBM-Plex-Mono.zip && \ + cd /tmp && unzip -q plex.zip -d plex && \ + find /tmp/plex -name 'IBMPlexMono*.woff2' -exec cp {} /build/appview/pages/static/fonts/ \; || true + +# Lucide icons +RUN curl -sLo /tmp/lucide.zip https://github.com/lucide-icons/lucide/releases/download/0.344.0/lucide-icons-0.344.0.zip && \ + cd /tmp && unzip -q lucide.zip -d lucide && \ + find /tmp/lucide -name '*.svg' -exec cp {} /build/appview/pages/static/icons/ \; || true + +# Placeholder logos +RUN touch appview/pages/static/logos/dolly.png appview/pages/static/logos/dolly.ico appview/pages/static/logos/dolly.svg + +# Custom logos (place files in docker/appview/logos/) +COPY docker/appview/logos/ appview/pages/static/logos/ + +# Tailwind CSS (v3 - matches tailwind.config.js) +RUN cd /build && npm install tailwindcss@3 @tailwindcss/typography && \ + npx tailwindcss -c tailwind.config.js -i input.css -o appview/pages/static/tw.css --minify 2>&1 && \ + echo "tw.css size: $(wc -c < appview/pages/static/tw.css) bytes" + +# Build +RUN CGO_ENABLED=1 go build -o appview-bin ./cmd/appview/ + +# --- AppView --- +FROM debian:bookworm-slim AS appview +RUN apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/* +COPY --from=builder /build/appview-bin /usr/local/bin/appview +EXPOSE 3000 +CMD ["appview"] diff --git a/docker/appview/logos/ai.svg b/docker/appview/logos/ai.svg new file mode 100644 index 0000000..e3e634a --- /dev/null +++ b/docker/appview/logos/ai.svg @@ -0,0 +1,19 @@ + + + diff --git a/envs/appview.example b/envs/appview.example new file mode 100644 index 0000000..8add649 --- /dev/null +++ b/envs/appview.example @@ -0,0 +1,41 @@ +# core +TANGLED_APPVIEW_HOST=git.example.com +TANGLED_APPVIEW_NAME=Example Git +TANGLED_DB_PATH=/data/appview.db +TANGLED_LISTEN_ADDR=0.0.0.0:3000 +TANGLED_DEV=false +TANGLED_COOKIE_SECRET=change-me-32-char-random-string! + +# at protocol +TANGLED_PLC_URL=https://plc.directory +TANGLED_JETSTREAM_ENDPOINT=wss://jetstream1.us-east.bsky.network/subscribe + +# oauth (generate with: goat key generate -t P-256) +TANGLED_OAUTH_CLIENT_SECRET=z-multibase-p256-secret-key +TANGLED_OAUTH_CLIENT_KID=1234567890 + +# redis +TANGLED_REDIS_ADDR=tangled-redis:6379 +# TANGLED_REDIS_PASS= +# TANGLED_REDIS_DB=0 + +# labels (empty for self-host, default requires tangled.sh DID) +TANGLED_LABEL_DEFAULTS= +TANGLED_LABEL_GFI= + +# email (optional, uses resend.com) +# TANGLED_RESEND_API_KEY=re_xxxxx +# TANGLED_RESEND_SENT_FROM=noreply@example.com + +# optional services +# TANGLED_CAMO_HOST=https://camo.example.com +# TANGLED_CAMO_SHARED_SECRET= +# TANGLED_AVATAR_HOST=https://avatar.example.com +# TANGLED_AVATAR_SHARED_SECRET= +# TANGLED_PDS_HOST=https://pds.example.com +# TANGLED_PDS_ADMIN_SECRET= +# TANGLED_CLOUDFLARE_API_TOKEN= +# TANGLED_CLOUDFLARE_ZONE_ID= +# TANGLED_CLOUDFLARE_TURNSTILE_SITE_KEY= +# TANGLED_CLOUDFLARE_TURNSTILE_SECRET_KEY= +# TANGLED_POSTHOG_API_KEY= diff --git a/envs/knot.example b/envs/knot.example new file mode 100644 index 0000000..2d4d794 --- /dev/null +++ b/envs/knot.example @@ -0,0 +1,16 @@ +# required +KNOT_SERVER_HOSTNAME=knot.example.com +KNOT_SERVER_OWNER=did:plc:xxxxx + +# network +KNOT_SERVER_JETSTREAM_ENDPOINT=wss://jetstream.example.com/subscribe +APPVIEW_ENDPOINT=http://appview:3000 + +# optional (defaults shown) +# KNOT_SERVER_LISTEN_ADDR=0.0.0.0:5555 +# KNOT_SERVER_INTERNAL_LISTEN_ADDR=0.0.0.0:5444 +# KNOT_SERVER_DB_PATH=/app/knotserver.db +# KNOT_REPO_SCAN_PATH=/home/git/repositories +# KNOT_REPO_MAIN_BRANCH=main +# KNOT_SERVER_DEV=false +# KNOT_SERVER_LOG_DIDS=true diff --git a/patching/100-appview-avatar-pds-blob.patch b/patching/100-appview-avatar-pds-blob.patch new file mode 100644 index 0000000..dc36df5 --- /dev/null +++ b/patching/100-appview-avatar-pds-blob.patch @@ -0,0 +1,52 @@ +--- a/appview/pages/funcmap.go ++++ b/appview/pages/funcmap.go +@@ -505,34 +505,26 @@ + did = identity.DID.String() + } + +- secret := p.avatar.SharedSecret +- h := hmac.New(sha256.New, []byte(secret)) +- h.Write([]byte(did)) +- signature := hex.EncodeToString(h.Sum(nil)) +- +- // Get avatar CID for cache busting ++ // Get avatar CID from profile DB + profile, err := db.GetProfile(p.db, did) +- version := "" +- if err == nil && profile != nil && profile.Avatar != "" { +- // Use first 8 chars of avatar CID as version +- if len(profile.Avatar) > 8 { +- version = profile.Avatar[:8] +- } else { +- version = profile.Avatar ++ if err == nil && profile != nil && profile.Avatar != "" && identity != nil { ++ // Get PDS endpoint from DID document ++ if svc, ok := identity.Services["atproto_pds"]; ok { ++ pdsUrl := strings.TrimRight(svc.URL, "/") ++ return fmt.Sprintf("%s/xrpc/com.atproto.sync.getBlob?did=%s&cid=%s", pdsUrl, did, profile.Avatar) + } + } + +- baseUrl := fmt.Sprintf("%s/%s/%s", p.avatar.Host, signature, did) +- if size != "" { +- if version != "" { +- return fmt.Sprintf("%s?size=%s&v=%s", baseUrl, size, version) +- } +- return fmt.Sprintf("%s?size=%s", baseUrl, size) ++ // Fallback to avatar proxy if configured ++ if p.avatar.Host != "" && p.avatar.SharedSecret != "" { ++ secret := p.avatar.SharedSecret ++ h := hmac.New(sha256.New, []byte(secret)) ++ h.Write([]byte(did)) ++ signature := hex.EncodeToString(h.Sum(nil)) ++ return fmt.Sprintf("%s/%s/%s", p.avatar.Host, signature, did) + } +- if version != "" { +- return fmt.Sprintf("%s?v=%s", baseUrl, version) +- } +- return baseUrl ++ ++ return "" + } + + func (p *Pages) icon(name string, classes []string) (template.HTML, error) { diff --git a/patching/110-appview-custom-footer.patch b/patching/110-appview-custom-footer.patch new file mode 100644 index 0000000..4c1af15 --- /dev/null +++ b/patching/110-appview-custom-footer.patch @@ -0,0 +1,114 @@ +--- a/appview/pages/templates/layouts/fragments/footer.html 2026-03-10 19:59:35 ++++ b/appview/pages/templates/layouts/fragments/footer.html 2026-03-10 20:00:03 +@@ -1,102 +1,13 @@ + {{ define "layouts/fragments/footer" }} +-
+-
+-
+- +- +- +- +-
+- {{ $headerStyle := "text-gray-900 dark:text-gray-200 font-bold text-xs uppercase tracking-wide mb-1" }} +- {{ $linkStyle := "text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-200 hover:underline inline-flex gap-1 items-center" }} +- {{ $iconStyle := "w-4 h-4 flex-shrink-0" }} +- +- +- +- +- +-
+-
© 2026 Tangled Labs Oy. All rights reserved.
+-
+-
+-
++ + {{ end }} diff --git a/patching/120-appview-custom-header-logo.patch b/patching/120-appview-custom-header-logo.patch new file mode 100644 index 0000000..b3177f8 --- /dev/null +++ b/patching/120-appview-custom-header-logo.patch @@ -0,0 +1,15 @@ +--- a/appview/pages/templates/fragments/logotypeSmall.html 2026-03-10 20:00:17 ++++ b/appview/pages/templates/fragments/logotypeSmall.html 2026-03-10 20:00:23 +@@ -1,9 +1,9 @@ + {{ define "fragments/logotypeSmall" }} + +- {{ template "fragments/dolly/logo" (dict "Classes" "size-8 text-black dark:text-white")}} +- tangled ++ logo ++ git + +- alpha ++ syu.is + + + {{ end }} diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..d5c4d0f --- /dev/null +++ b/readme.md @@ -0,0 +1,18 @@ +# tangled + +git + atproto + +1. start knot (knot.example.com) +2. `tangled.org` oauth login (user.bsky.social) +3. `knot.example.com` verifiyed + +- https://docs.tangled.org/spindles.html#self-hosting-guide + +## self-hosting + +1. knot, `git clone https://tangled.org/tangled.org/knot-docker` +2. appviewe, `git clone https://tangled.org/tangled.org/infra` + +## lexicon + +`at://did:plc:6qyecktefllvenje24fcxnie/sh.tangled.knot/knot.syu.is`