init
This commit is contained in:
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal 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
44
compose.yml
Normal 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
49
docker/appview/Dockerfile
Normal 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"]
|
||||
19
docker/appview/logos/ai.svg
Normal file
19
docker/appview/logos/ai.svg
Normal 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
41
envs/appview.example
Normal 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
16
envs/knot.example
Normal 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
|
||||
52
patching/100-appview-avatar-pds-blob.patch
Normal file
52
patching/100-appview-avatar-pds-blob.patch
Normal 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) {
|
||||
114
patching/110-appview-custom-footer.patch
Normal file
114
patching/110-appview-custom-footer.patch
Normal 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">© 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">© 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 }}
|
||||
15
patching/120-appview-custom-header-logo.patch
Normal file
15
patching/120-appview-custom-header-logo.patch
Normal 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
18
readme.md
Normal 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`
|
||||
Reference in New Issue
Block a user