1
0
This commit is contained in:
ai.syui.ai
2026-03-10 23:16:42 +09:00
commit ad7c3a860b
10 changed files with 382 additions and 0 deletions

14
.gitignore vendored Normal file
View File

@@ -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

44
compose.yml Normal file
View File

@@ -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

49
docker/appview/Dockerfile Normal file
View File

@@ -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"]

View File

@@ -0,0 +1,19 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="1024" height="1024">
<path fill-rule="evenodd" fill="#F6E717" d="
M 619,232
L 512,7
L 405,232
A 300,300 0 0,0 216,559
L 75,765
L 323,745
A 300,300 0 0,0 701,745
L 949,765
L 808,559
A 300,300 0 0,0 619,232
Z
M 512,337
A 175,175 0 0,0 512,687
A 175,175 0 0,0 512,337
Z
"/>
</svg>

After

Width:  |  Height:  |  Size: 413 B

41
envs/appview.example Normal file
View File

@@ -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=

16
envs/knot.example Normal file
View File

@@ -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

View File

@@ -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) {

View File

@@ -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" }}
-<div class="w-full p-8 bg-white dark:bg-gray-800">
- <div class="mx-auto px-4">
- <div class="flex flex-col text-gray-600 dark:text-gray-400 gap-8">
- <!-- Desktop layout: grid with 3 columns -->
- <div class="hidden lg:grid lg:grid-cols-[1fr_minmax(0,1024px)_1fr] lg:gap-8 lg:items-start">
- <!-- Left section -->
- <div>
- <a href="/" hx-boost="true" class="flex gap-2 font-semibold italic no-underline hover:no-underline">
- {{ template "fragments/logotypeSmall" }}
- </a>
- </div>
-
- {{ $headerStyle := "text-gray-900 dark:text-gray-200 font-bold text-sm 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" }}
-
- <!-- Center section with max-width -->
- <div class="grid grid-cols-4 gap-2">
- <div class="flex flex-col gap-1">
- <div class="{{ $headerStyle }}">legal</div>
- <a href="/terms" class="{{ $linkStyle }}">{{ i "file-text" $iconStyle }} terms of service</a>
- <a href="/privacy" class="{{ $linkStyle }}">{{ i "shield" $iconStyle }} privacy policy</a>
- </div>
-
- <div class="flex flex-col gap-1">
- <div class="{{ $headerStyle }}">resources</div>
- <a href="https://blog.tangled.org" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "book-open" $iconStyle }} blog</a>
- <a href="https://docs.tangled.org" class="{{ $linkStyle }}">{{ i "book" $iconStyle }} docs</a>
- <a href="https://tangled.org/@tangled.org/core" class="{{ $linkStyle }}">{{ i "code" $iconStyle }} source</a>
- <a href="https://tangled.org/brand" class="{{ $linkStyle }}">{{ i "paintbrush" $iconStyle }} brand</a>
- </div>
-
- <div class="flex flex-col gap-1">
- <div class="{{ $headerStyle }}">social</div>
- <a href="https://chat.tangled.org" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "message-circle" $iconStyle }} discord</a>
- <a href="https://web.libera.chat/#tangled" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "hash" $iconStyle }} irc</a>
- <a href="https://bsky.app/profile/tangled.org" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ template "user/fragments/bluesky" $iconStyle }} bluesky</a>
- </div>
-
- <div class="flex flex-col gap-1">
- <div class="{{ $headerStyle }}">contact</div>
- <a href="mailto:team@tangled.org" class="{{ $linkStyle }}">{{ i "mail" "w-4 h-4 flex-shrink-0" }} team@tangled.org</a>
- <a href="mailto:security@tangled.org" class="{{ $linkStyle }}">{{ i "shield-check" "w-4 h-4 flex-shrink-0" }} security@tangled.org</a>
- </div>
- </div>
-
- <!-- Right section -->
- <div class="text-right">
- <div class="text-xs">&copy; 2026 Tangled Labs Oy. All rights reserved.</div>
- </div>
- </div>
-
- <!-- Mobile layout: stacked -->
- <div class="lg:hidden flex flex-col gap-8">
- {{ $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" }}
-
- <div class="mb-4 md:mb-0">
- <a href="/" hx-boost="true" class="flex gap-2 font-semibold italic no-underline hover:no-underline">
- {{ template "fragments/logotypeSmall" }}
- </a>
- </div>
-
- <div class="grid grid-cols-1 sm:grid-cols-1 md:grid-cols-4 sm:gap-6 md:gap-2 gap-6">
- <div class="flex flex-col gap-1">
- <div class="{{ $headerStyle }}">legal</div>
- <a href="/terms" class="{{ $linkStyle }}">{{ i "file-text" $iconStyle }} terms of service</a>
- <a href="/privacy" class="{{ $linkStyle }}">{{ i "shield" $iconStyle }} privacy policy</a>
- </div>
-
- <div class="flex flex-col gap-1">
- <div class="{{ $headerStyle }}">resources</div>
- <a href="https://blog.tangled.org" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "book-open" $iconStyle }} blog</a>
- <a href="https://docs.tangled.org" class="{{ $linkStyle }}">{{ i "book" $iconStyle }} docs</a>
- <a href="https://tangled.org/@tangled.org/core" class="{{ $linkStyle }}">{{ i "code" $iconStyle }} source</a>
- <a href="https://tangled.org/brand" class="{{ $linkStyle }}">{{ i "paintbrush" $iconStyle }} brand</a>
- </div>
-
- <div class="flex flex-col gap-1">
- <div class="{{ $headerStyle }}">social</div>
- <a href="https://chat.tangled.org" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "message-circle" $iconStyle }} discord</a>
- <a href="https://web.libera.chat/#tangled" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ i "hash" $iconStyle }} irc</a>
- <a href="https://bsky.app/profile/tangled.org" class="{{ $linkStyle }}" target="_blank" rel="noopener noreferrer">{{ template "user/fragments/bluesky" $iconStyle }} bluesky</a>
- </div>
-
- <div class="flex flex-col gap-1">
- <div class="{{ $headerStyle }}">contact</div>
- <a href="mailto:team@tangled.org" class="{{ $linkStyle }}">{{ i "mail" "w-4 h-4 flex-shrink-0" }} team@tangled.org</a>
- <a href="mailto:security@tangled.org" class="{{ $linkStyle }}">{{ i "shield-check" "w-4 h-4 flex-shrink-0" }} security@tangled.org</a>
- </div>
- </div>
-
- <div class="text-center">
- <div class="text-xs">&copy; 2026 Tangled Labs Oy. All rights reserved.</div>
- </div>
- </div>
- </div>
+<div class="w-full py-6 bg-white dark:bg-gray-800">
+ <div class="mx-auto px-4 flex items-center justify-center gap-4 text-sm text-gray-500 dark:text-gray-400">
+ <a href="https://tangled.org/@tangled.org" class="hover:text-gray-900 dark:hover:text-gray-200 hover:underline inline-flex gap-1 items-center">
+ {{ i "code" "w-4 h-4" }} tangled
+ </a>
+ <span class="text-gray-300 dark:text-gray-600">|</span>
+ <a href="https://tangled.org/@tangled.org/core/blob/master/license" class="hover:text-gray-900 dark:hover:text-gray-200 hover:underline inline-flex gap-1 items-center">
+ {{ i "file-text" "w-4 h-4" }} license
+ </a>
</div>
</div>
{{ end }}

View File

@@ -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" }}
<span class="flex items-center gap-2">
- {{ template "fragments/dolly/logo" (dict "Classes" "size-8 text-black dark:text-white")}}
- <span class="font-bold text-xl not-italic">tangled</span>
+ <img src="/static/logos/ai.svg" alt="logo" class="size-8" />
+ <span class="font-bold text-xl not-italic">git</span>
<span class="font-normal not-italic text-xs rounded bg-gray-100 dark:bg-gray-700 px-1">
- alpha
+ syu.is
</span>
<span>
{{ end }}

18
readme.md Normal file
View File

@@ -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`