0
2
grain runs on a Rust server modelled on knotless, without hatk
.knotless/workflows/check.yml+7−1
| @@ -1,11 +1,17 @@ | ||
# Formatting, lint and types, as `npm run check` runs them: vp check, then | ||
# svelte-check. The packages are kept between runs and installed again only | ||
# when the lockfile changes (the npm-ci shared step). | ||
# | ||
# The Node side's: at cutover rust.yml is renamed over it, and what of it | ||
# checks web/ moves to a web.yml of its own, as knotless's does. | ||
image: node:25-slim | ||
runners: [runner.knotless.dev] | ||
# A pre-flight: the merge queue takes a pull into a batch only once this | ||
# has passed at its head, so one that does not build fails no batch. | ||
queue: first | ||
on: | ||
push: { refs: [refs/heads/main] } | ||
pull: { base: [refs/heads/main] } | ||
pull: { base: [refs/heads/main, refs/heads/feature/*] } | ||
cache: [node_modules] | ||
steps: | ||
- uses: chadtmiller.com/steps/npm-ci@1 | ||
.knotless/workflows/deploy.yml+25−5
| @@ -1,6 +1,12 @@ | ||
# Deploys main to the Hetzner host once this commit's test and check have | ||
# passed. The appview image is built from this checkout on the host, over | ||
# SSH, as a candidate, grain:build-<commit>, while the checks run; once | ||
# Deploys main to the Hetzner host once this commit's checks all pass: | ||
# check and test, the Node app's, rust and harness, the Rust | ||
# workspace's, and lexicons, so that no part can fail and still deploy. | ||
# Until cutover the Node app is what deploys; at cutover rust.yml and | ||
# harness.yml become check.yml and test.yml, and the list below is | ||
# `check test lexicons`. | ||
# | ||
# The appview image is built from this checkout on the host, over SSH, as | ||
# a candidate, grain:build-<commit>, while the checks run; once | ||
# they pass it becomes grain:current and the grain service is recreated | ||
# from it, leaving the rest of the stack as it is. grain-deploy on the | ||
# host rebuilds the whole stack by hand. | ||
| @@ -38,12 +44,26 @@ steps: | ||
timeout: 30 | ||
run: | | ||
DOCKER_HOST="ssh://$GRAIN_HOST" docker build -t "grain:build-$KNOTLESS_RUN_SHA" . | ||
- name: checks | ||
- name: checks done | ||
timeout: 30 | ||
run: | | ||
# 25 minutes over all five, and never a failure: the next step | ||
# judges. Its checks can wait for a job behind other runs, so it | ||
# waits for them over two steps, each under the runner's 30 minutes | ||
# a step, as knotless's deploy does. | ||
repo="knotless://$KNOTLESS_RUN_REPO" | ||
end=$(($(date +%s) + 1500)) | ||
for wf in check test rust harness lexicons; do | ||
left=$((end - $(date +%s))) | ||
[ "$left" -gt 0 ] || break | ||
knotless checks "$repo" --ref "$KNOTLESS_RUN_REF" --commit "$KNOTLESS_RUN_SHA" --workflow "$wf" --wait "$left" || true | ||
done | ||
- name: checks pass | ||
timeout: 30 | ||
run: | | ||
repo="knotless://$KNOTLESS_RUN_REPO" | ||
end=$(($(date +%s) + 1500)) | ||
for wf in test check; do | ||
for wf in check test rust harness lexicons; do | ||
left=$((end - $(date +%s))) | ||
[ "$left" -gt 0 ] || left=1 | ||
out=$(knotless checks "$repo" --ref "$KNOTLESS_RUN_REF" --commit "$KNOTLESS_RUN_SHA" --workflow "$wf" --wait "$left") | ||
.knotless/workflows/e2e.ymlnew+152−0
| @@ -0,0 +1,152 @@ | ||
# The pages used as people use them, as `just e2e` runs them: grain-serve | ||
# and the web's build together, in Chromium, logging in through the PDS's | ||
# own approval page, then each write the pages make, each read back on the | ||
# page and from the PDS (harness/e2e/flows.spec.mjs). Then, over the | ||
# spaces alpha, what an account on a PDS with spaces finds: the Groups tab | ||
# and the groups page (spaces.spec.mjs). rust.yml and harness.yml check grain-serve, check.yml | ||
# and test.yml the web; this, that the two work together. Each flow that | ||
# fails leaves the page as it was then among the check's outputs, as a | ||
# picture, beside what the servers said. | ||
# | ||
# Node, the packages, Chromium, the libraries it needs, the alpha and the | ||
# build are kept between runs as main's runs, or a feature/ branch's, | ||
# leave them: nothing is fetched again until what it is made from changes. | ||
image: rust:1.98 | ||
runners: [runner.knotless.dev] | ||
# Every push to main or a feature/ branch, and pulls into either that | ||
# change the pages, the server they log in to and write through, the lexicons | ||
# between them, the harness they run over, or this file. A pull changing | ||
# none passes at once, noted as skipped. | ||
on: | ||
push: { refs: [refs/heads/main, refs/heads/feature/*] } | ||
pull: | ||
base: [refs/heads/main, refs/heads/feature/*] | ||
paths: | ||
[ | ||
web/**, | ||
lexicons/**, | ||
crates/grain/**, | ||
crates/grain-serve/**, | ||
Cargo.toml, | ||
Cargo.lock, | ||
package.json, | ||
package-lock.json, | ||
svelte.config.js, | ||
vite.config.ts, | ||
hatk.generated.client.ts, | ||
harness/e2e/**, | ||
harness/pds.mjs, | ||
harness/tls-proxy.mjs, | ||
harness/package.json, | ||
harness/package-lock.json, | ||
.knotless/workflows/e2e.yml, | ||
] | ||
cache: | ||
[ | ||
target, | ||
/usr/local/cargo/registry, | ||
/usr/local/rustup, | ||
/opt/node, | ||
/opt/browsers, | ||
/opt/chromium-debs, | ||
/opt/atproto, | ||
node_modules, | ||
harness/node_modules, | ||
] | ||
env: | ||
PLAYWRIGHT_BROWSERS_PATH: /opt/browsers | ||
# The spaces alpha the spaces flows run over, as harness.yml pins it: | ||
# the two move together. | ||
ATPROTO_COMMIT: 11c5d98cd50261c5e1201fedc5c6c05a7b8d1ccf | ||
ALPHA_BUILD: "1" | ||
steps: | ||
# Node into /opt/node, and the web's and the harness's packages when | ||
# their lockfiles change, each kept in the cache. | ||
- uses: chadtmiller.com/steps/node@1 | ||
with: { version: 24.22.1 } | ||
- uses: chadtmiller.com/steps/npm-ci@1 | ||
name: web packages | ||
- uses: chadtmiller.com/steps/npm-ci@1 | ||
name: harness | ||
with: { dir: harness } | ||
- name: chromium | ||
run: | | ||
export PATH=/opt/node/bin:$PATH | ||
cd harness | ||
# The browser, into /opt/browsers once for each Playwright version. | ||
npx playwright install chromium | ||
# The libraries it needs, which `install-deps` would fetch with apt on | ||
# every run: their .debs are kept, fetched again only when this | ||
# Playwright or this Debian asks for others, and installed offline. | ||
pkgs=$(npx playwright install-deps --dry-run chromium | sed -n "s/.*--no-install-recommends \([^\"']*\).*/\1/p") | ||
[ -n "$pkgs" ] || { echo "no packages in: $(npx playwright install-deps --dry-run chromium)"; exit 1; } | ||
debs=/opt/chromium-debs | ||
want="$(. /etc/os-release && echo "$VERSION_ID") $pkgs" | ||
if [ "$(cat $debs/.want 2>/dev/null)" != "$want" ]; then | ||
mkdir -p $debs && rm -rf "${debs:?}"/* && mkdir -p $debs/partial | ||
apt-get update -qq | ||
apt-get install -y -qq --no-install-recommends --download-only -o Dir::Cache::archives=$debs $pkgs | ||
echo "$want" > $debs/.want | ||
fi | ||
dpkg -i $debs/*.deb > /dev/null | ||
- name: spaces alpha | ||
run: | | ||
[ "$(cat /opt/atproto/.built 2>/dev/null)" = "$ATPROTO_COMMIT $ALPHA_BUILD" ] && exit 0 | ||
export PATH=/opt/node/bin:$PATH COREPACK_ENABLE_DOWNLOAD_PROMPT=0 COREPACK_HOME=/opt/atproto/.corepack | ||
mkdir -p /opt/atproto && find /opt/atproto -mindepth 1 -delete | ||
cd /opt/atproto | ||
git init -q . && git fetch -q --depth 1 https://github.com/bluesky-social/atproto.git "$ATPROTO_COMMIT" && git checkout -q FETCH_HEAD | ||
mkdir .bin && corepack enable --install-directory /opt/atproto/.bin | ||
export PATH=/opt/atproto/.bin:$PATH | ||
pnpm install --frozen-lockfile --ignore-scripts | ||
pnpm run build:tooling --force | ||
pnpm --filter '@atproto/dev-env^...' --filter @atproto/dev-env run --if-present prebuild | ||
pnpm exec tsc --build packages/dev-env/tsconfig.build.json | ||
# The PDS serves the OAuth sign-in pages from this bundle. | ||
pnpm --filter @atproto/oauth-provider-ui run build:ui | ||
echo "$ATPROTO_COMMIT $ALPHA_BUILD" > /opt/atproto/.built | ||
- name: sqlite | ||
run: | | ||
# The one install script the alpha's PDS needs, which the install | ||
# above skips: SQLite's native module, as better-sqlite3 builds it. | ||
export PATH=/opt/node/bin:$PATH | ||
cd "$(dirname "$(node -p "require.resolve('better-sqlite3/package.json', { paths: ['/opt/atproto/packages/pds'] })")")" | ||
[ -f build/Release/better_sqlite3.node ] && exit 0 | ||
npm run install | ||
- name: build | ||
run: | | ||
export PATH=/opt/node/bin:$PATH | ||
cargo build --locked -p grain-serve --bin grain-serve | ||
npm run build | ||
- name: flows | ||
run: | | ||
export PATH=/opt/node/bin:$PATH | ||
rm -rf target/e2e | ||
status=0 | ||
(cd harness && npx playwright test -c e2e/playwright.config.mjs) || status=$? | ||
# Then the spaces ones, over a world of their own on the alpha. | ||
cp -r target/e2e/logs target/e2e/logs-public 2>/dev/null || true | ||
(cd harness && GRAIN_DEV_ENV=/opt/atproto/packages/dev-env npx playwright test -c e2e/playwright.config.mjs) || status=$? | ||
if [ $status -ne 0 ]; then | ||
# What the servers said, the last of it, for the flows that failed. | ||
# What each failed page held, and the browser said. | ||
for context in target/e2e/*/*/error-context.md; do | ||
[ -f "$context" ] && { echo "--- $context"; head -c 6000 "$context"; echo; } | ||
done | ||
for log in target/e2e/logs-public/*.log target/e2e/logs/*.log; do | ||
[ -f "$log" ] && { echo "--- $log"; grep -v '"url.path":"/\(_app\|atmo\)/' "$log" | tail -n 80 | cut -c1-600; } | ||
done | ||
fi | ||
# The page each failed flow ended on, named for the flow, and the logs. | ||
if [ -n "${KNOTLESS_RUN_OUTPUTS:-}" ] && [ -d target/e2e ]; then | ||
for shot in target/e2e/*/*/test-failed-*.png; do | ||
[ -f "$shot" ] && cp "$shot" "$KNOTLESS_RUN_OUTPUTS/$(basename "$(dirname "$shot")").png" | ||
done | ||
for log in target/e2e/logs-public/*.log; do | ||
[ -f "$log" ] && cp "$log" "$KNOTLESS_RUN_OUTPUTS/public-$(basename "$log")" | ||
done | ||
for log in target/e2e/logs/*.log; do | ||
[ -f "$log" ] && cp "$log" "$KNOTLESS_RUN_OUTPUTS/spaces-$(basename "$log")" | ||
done | ||
fi | ||
exit $status | ||
.knotless/workflows/harness.ymlnew+107−0
| @@ -0,0 +1,107 @@ | ||
# The Rust tests against the reference PDS, under the coverage gate, as | ||
# `just cov` runs them (harness/cov.mjs): every test binary, instrumented, | ||
# against @atproto/pds and a PLC directory from harness/pds.mjs, failing | ||
# under harness/cov.json's lines or functions. Then that every XRPC answer | ||
# grain-serve's tests received is as its lexicon says (harness/answers.mjs), | ||
# and that the check would name a wrong one (harness/answers-test.sh); and | ||
# that no query they ran scans the network at prod's size | ||
# (harness/plans.mjs). | ||
# | ||
# The test binaries named `spaces` run against the reference PDS on the | ||
# permissioned spaces alpha, built from an atproto commit, as knotless's | ||
# tests are: npm's alpha builds name their dependencies by workspace, so | ||
# they do not install. | ||
# | ||
# Node, the harness's packages, the alpha's build, cargo-llvm-cov and the | ||
# build are kept between runs; each is made again only when what it is | ||
# made from changes. So are the test runs that passed, in /opt/testcache: | ||
# a test binary the same byte for byte as one that passed, against the | ||
# same harness, is not run again, and its profile counts (harness/cov.mjs). | ||
# | ||
# At cutover this becomes test.yml, as knotless's is, over the Node one, | ||
# and rust.yml check.yml (see there). | ||
image: rust:1.98 | ||
runners: [runner.knotless.dev] | ||
# A pre-flight, as check is: the merge queue takes a pull into a batch only | ||
# once this has passed at its head, so one whose tests fail on their own | ||
# costs no batch and no halving. | ||
queue: first | ||
# Pushes to main and to a feature/ branch, which keeps caches of its own | ||
# for the pulls into it: a branch keeps them only where both push and | ||
# pull name it. | ||
on: | ||
push: { refs: [refs/heads/main, refs/heads/feature/*] } | ||
pull: { base: [refs/heads/main, refs/heads/feature/*] } | ||
cache: | ||
[ | ||
target, | ||
/usr/local/cargo/registry, | ||
/usr/local/rustup, | ||
/opt/node, | ||
/opt/tools, | ||
/opt/atproto, | ||
harness/node_modules, | ||
/opt/testcache, | ||
] | ||
env: | ||
# The spaces alpha the tests of spaces run against, as knotless's do. | ||
ATPROTO_COMMIT: 11c5d98cd50261c5e1201fedc5c6c05a7b8d1ccf | ||
# Bumped when the steps that build the alpha change, to build it again. | ||
ALPHA_BUILD: "1" | ||
steps: | ||
# Node into /opt/node, the harness's packages when their lockfile | ||
# changes, and cargo-llvm-cov into /opt/tools, each kept in the cache. | ||
- uses: chadtmiller.com/steps/node@1 | ||
with: { version: 24.22.1 } | ||
- uses: chadtmiller.com/steps/npm-ci@1 | ||
name: harness | ||
with: { dir: harness, omit: dev } | ||
- uses: chadtmiller.com/steps/cargo-llvm-cov@1 | ||
with: { version: 0.9.1 } | ||
- name: spaces alpha | ||
run: | | ||
[ "$(cat /opt/atproto/.built 2>/dev/null)" = "$ATPROTO_COMMIT $ALPHA_BUILD" ] && exit 0 | ||
export PATH=/opt/node/bin:$PATH COREPACK_ENABLE_DOWNLOAD_PROMPT=0 COREPACK_HOME=/opt/atproto/.corepack | ||
mkdir -p /opt/atproto && find /opt/atproto -mindepth 1 -delete | ||
cd /opt/atproto | ||
git init -q . && git fetch -q --depth 1 https://github.com/bluesky-social/atproto.git "$ATPROTO_COMMIT" && git checkout -q FETCH_HEAD | ||
mkdir .bin && corepack enable --install-directory /opt/atproto/.bin | ||
export PATH=/opt/atproto/.bin:$PATH | ||
pnpm install --frozen-lockfile --ignore-scripts | ||
pnpm run build:tooling --force | ||
pnpm --filter '@atproto/dev-env^...' --filter @atproto/dev-env run --if-present prebuild | ||
pnpm exec tsc --build packages/dev-env/tsconfig.build.json | ||
# The PDS serves the OAuth sign-in pages from this bundle. | ||
pnpm --filter @atproto/oauth-provider-ui run build:ui | ||
echo "$ATPROTO_COMMIT $ALPHA_BUILD" > /opt/atproto/.built | ||
- name: sqlite | ||
run: | | ||
# The one install script the alpha's PDS needs, which the install | ||
# above skips: SQLite's native module, as better-sqlite3 builds it. | ||
export PATH=/opt/node/bin:$PATH | ||
cd "$(dirname "$(node -p "require.resolve('better-sqlite3/package.json', { paths: ['/opt/atproto/packages/pds'] })")")" | ||
[ -f build/Release/better_sqlite3.node ] && exit 0 | ||
npm run install | ||
- name: test | ||
run: | | ||
export PATH=/opt/node/bin:$PATH GRAIN_DEV_ENV=/opt/atproto/packages/dev-env | ||
# Half the host's cores, as two test runs go side by side, over two | ||
# stock PDSes, as one PDS is one node process, and the alpha beside | ||
# them, all at once: each binary against the stock PDS but those | ||
# named `spaces`, which go against the alpha (harness/cov.mjs). | ||
COV_STORE=/opt/testcache COV_JOBS=$(( $(nproc) / 2 )) COV_HARNESSES=2 COV_TOOLCHAIN=$(rustup show active-toolchain | cut -d' ' -f1) \ | ||
node harness/cov.mjs | ||
- name: answers | ||
run: | | ||
# Every XRPC answer grain-serve's tests received, fresh or kept with | ||
# a stored run, as its lexicon says. | ||
export PATH=/opt/node/bin:$PATH | ||
harness/answers-test.sh | ||
node harness/answers.mjs target/llvm-cov-target/cov-report/answers | ||
- name: plans | ||
run: | | ||
# No query the tests ran scans a table that grows with the network, | ||
# or sorts many of its rows, as SQLite would plan it at prod's size, | ||
# unless its SQL says so. | ||
export PATH=/opt/node/bin:$PATH | ||
node harness/plans.mjs target/llvm-cov-target/cov-report/plans | ||
.knotless/workflows/lexicons.ymlnew+30−0
| @@ -0,0 +1,30 @@ | ||
# The lexicons, as `just lex` checks them: goat's lint, what it does not | ||
# see, and that they only grow from main's (harness/lexicons.mjs), over | ||
# grain's own under lexicons/social/grain, less what the old server alone | ||
# serves. goat is kept between runs. | ||
# | ||
# The XRPC answers are held to the lexicons by harness.yml, over what | ||
# grain-serve's tests received. | ||
image: node:24.22.1 | ||
runners: [runner.knotless.dev] | ||
# Every push to main, and pulls into it or a feature/ branch, at their | ||
# heads merged with their base, that change the lexicons, their check or | ||
# this file. A pull changing none passes at once, noted as skipped. | ||
on: | ||
push: { refs: [refs/heads/main] } | ||
pull: | ||
base: [refs/heads/main, refs/heads/feature/*] | ||
paths: [lexicons/**, harness/lexicons.mjs, .knotless/workflows/lexicons.yml] | ||
cache: [/opt/goat] | ||
steps: | ||
- name: goat | ||
run: | | ||
mkdir -p /opt/goat | ||
[ -x /opt/goat/goat ] || curl -fsSL https://github.com/bluesky-social/goat/releases/download/v0.2.6/goat_Linux_x86_64.tar.gz | tar -xz -C /opt/goat goat | ||
/opt/goat/goat --version | ||
- name: lexicons | ||
run: | | ||
# The checkout is another user's: git reads where it left main only | ||
# once told it may. | ||
git config --global --add safe.directory "$PWD" | ||
PATH=/opt/goat:$PATH node harness/lexicons.mjs | ||
.knotless/workflows/publish-lexicons.yml+4−3
| @@ -1,5 +1,6 @@ | ||
# Publishes grain's lexicons as atproto resolves them, once main's test | ||
# and check pass at a commit that changes them, with the publish-lexicons | ||
# Publishes grain's lexicons as atproto resolves them, once main's checks | ||
# all pass (check, test, rust, harness and lexicons, as deploy.yml waits on) | ||
# at a commit that changes them, with the publish-lexicons | ||
# shared step: a com.atproto.lexicon.schema record in grain.social's repo | ||
# for each lexicon whose authority is in the grain.social zone, and the | ||
# `_lexicon.<authority>` TXT records naming that account. The rest of | ||
| @@ -41,7 +42,7 @@ steps: | ||
- name: checks | ||
uses: chadtmiller.com/steps/wait-for-checks@2 | ||
on: { refs: [refs/heads/main] } | ||
with: { workflows: test check } | ||
with: { workflows: check test rust harness lexicons } | ||
- name: publish | ||
uses: chadtmiller.com/steps/publish-lexicons@1 | ||
on: { refs: [refs/heads/main] } | ||
.knotless/workflows/rust.ymlnew+41−0
| @@ -0,0 +1,41 @@ | ||
# The Rust workspace: formatting and clippy with warnings denied, as `just | ||
# lint` runs them; harness.yml runs the tests. Kept apart from check.yml | ||
# and test.yml, which run the Node side of the repository. | ||
# | ||
# At cutover this becomes check.yml, as knotless's is, and harness.yml | ||
# test.yml: the Node check.yml and test.yml are deleted, this file is | ||
# renamed over check.yml, and the lists of checks deploy.yml and | ||
# publish-lexicons.yml wait on go back to `check test` (and lexicons). | ||
# | ||
# The build, the registry and the toolchain's components are kept between | ||
# runs, so a push builds only what it changed. | ||
image: rust:1.98 | ||
runners: [runner.knotless.dev] | ||
# A pre-flight: the merge queue takes a pull into a batch only once this | ||
# has passed at its head, so one that does not build fails no batch. | ||
queue: first | ||
# Pushes to main and to a feature/ branch, which keeps caches of its own | ||
# for the pulls into it: a branch keeps them only where both push and | ||
# pull name it. | ||
on: | ||
push: { refs: [refs/heads/main, refs/heads/feature/*] } | ||
pull: { base: [refs/heads/main, refs/heads/feature/*] } | ||
cache: [target, /usr/local/cargo/registry, /usr/local/rustup] | ||
steps: | ||
# grain-import, the one-shot cutover from the old server's database, and | ||
# grain-serve's login carryover, which reads the cookies it set until a | ||
# date after cutover, are the only Rust that may name it: the rest | ||
# describes grain on its own terms. Both are deleted after cutover. The pattern is bracketed so that this file does not match it. | ||
# git grep exits 1 when it finds nothing, and more when it cannot search. | ||
- name: the old server is named only in grain-import | ||
run: | | ||
git config --global --add safe.directory "$PWD" | ||
rc=0 | ||
git grep -in 'h[a]tk' -- crates harness .knotless/workflows/rust.yml ':(exclude)crates/grain-import' ':(exclude)crates/grain-serve/src/login/carryover.rs' || rc=$? | ||
case $rc in | ||
1) exit 0 ;; | ||
0) echo "named outside crates/grain-import, above"; exit 1 ;; | ||
*) echo "git grep could not search the checkout"; exit 2 ;; | ||
esac | ||
# cargo fmt --check, then clippy over the workspace, warnings denied. | ||
- uses: chadtmiller.com/steps/rust-lint@1 | ||
.knotless/workflows/test.yml+7−1
| @@ -2,11 +2,17 @@ | ||
# themselves without SPACES_LIVE=1, and the Playwright tests in test/browser | ||
# are not run here. The packages are kept between runs and installed again | ||
# only when the lockfile changes (the npm-ci shared step). | ||
# | ||
# The Node side's: at cutover harness.yml is renamed over it. | ||
image: node:25-slim | ||
runners: [runner.knotless.dev] | ||
# A pre-flight, as check is: the merge queue takes a pull into a batch only | ||
# once this has passed at its head, so one whose tests fail on their own | ||
# costs no batch and no halving. | ||
queue: first | ||
on: | ||
push: { refs: [refs/heads/main] } | ||
pull: { base: [refs/heads/main] } | ||
pull: { base: [refs/heads/main, refs/heads/feature/*] } | ||
cache: [node_modules] | ||
steps: | ||
- uses: chadtmiller.com/steps/npm-ci@1 | ||
.knotless/uses.lock+12−0
| @@ -1,4 +1,12 @@ | ||
# Written by `knotless uses update`. Commit it: runs read it. | ||
chadtmiller.com/steps/cargo-llvm-cov@1: | ||
version: 1.0.0 | ||
commit: 686cdbb38978730bb1ba5d1c999967e48497ce15 | ||
release: at://did:plc:bcgltzqazw5tb6k2g3ttenbj/dev.knotless.release/3mza7iq7tundv.tbnmqntp7ufhx7yd6i3rdqewys.sjjb7q6l3fsl3spvqsuzdoe73w | ||
chadtmiller.com/steps/node@1: | ||
version: 1.0.0 | ||
commit: 686cdbb38978730bb1ba5d1c999967e48497ce15 | ||
release: at://did:plc:bcgltzqazw5tb6k2g3ttenbj/dev.knotless.release/3mza7iq7tundv.qespbvwvof6xwjdaedpzq3koia.sjjb7q6l3fsl3spvqsuzdoe73w | ||
chadtmiller.com/steps/npm-ci@1: | ||
version: 1.0.0 | ||
commit: 686cdbb38978730bb1ba5d1c999967e48497ce15 | ||
| @@ -7,6 +15,10 @@ chadtmiller.com/steps/publish-lexicons@1: | ||
version: 1.0.0 | ||
commit: cc436177fab7461a33e84ec8876a2d66656a5c09 | ||
release: at://did:plc:bcgltzqazw5tb6k2g3ttenbj/dev.knotless.release/3mza7iq7tundv.ez5tgppxbpaxxrztjj3cnlyyhc.sjjb7q6l3fsl3spvqsuzdoe73w | ||
chadtmiller.com/steps/rust-lint@1: | ||
version: 1.0.0 | ||
commit: 686cdbb38978730bb1ba5d1c999967e48497ce15 | ||
release: at://did:plc:bcgltzqazw5tb6k2g3ttenbj/dev.knotless.release/3mza7iq7tundv.zvxvlxa6fe7s4pnwsnmgmm63aj.sjjb7q6l3fsl3spvqsuzdoe73w | ||
chadtmiller.com/steps/wait-for-checks@2: | ||
version: 2.0.0 | ||
commit: d5b057a2314833fdee9f600c7872db2916b0d0aa | ||
app/lib/utils/records.tsdeleted+0−120
| @@ -1,120 +0,0 @@ | ||
import { callXrpc } from "$hatk/client"; | ||
import { nextTid } from "./tid.ts"; | ||
| ||
/** One `dev.hatk.applyWrites#create` entry with a client-minted rkey. */ | ||
export interface CreateWrite { | ||
$type: "dev.hatk.applyWrites#create"; | ||
collection: string; | ||
rkey: string; | ||
value: Record<string, unknown>; | ||
} | ||
| ||
/** One `dev.hatk.applyWrites#update` entry. The record must already exist. */ | ||
export interface UpdateWrite { | ||
$type: "dev.hatk.applyWrites#update"; | ||
collection: string; | ||
rkey: string; | ||
value: Record<string, unknown>; | ||
} | ||
| ||
export interface DeleteWrite { | ||
$type: "dev.hatk.applyWrites#delete"; | ||
collection: string; | ||
rkey: string; | ||
} | ||
| ||
export type Write = CreateWrite | UpdateWrite | DeleteWrite; | ||
| ||
export function createWrite( | ||
collection: string, | ||
rkey: string, | ||
value: Record<string, unknown>, | ||
): CreateWrite { | ||
return { $type: "dev.hatk.applyWrites#create", collection, rkey, value }; | ||
} | ||
| ||
export function updateWrite( | ||
collection: string, | ||
rkey: string, | ||
value: Record<string, unknown>, | ||
): UpdateWrite { | ||
return { $type: "dev.hatk.applyWrites#update", collection, rkey, value }; | ||
} | ||
| ||
export function deleteWrite(collection: string, rkey: string): DeleteWrite { | ||
return { $type: "dev.hatk.applyWrites#delete", collection, rkey }; | ||
} | ||
| ||
export function atUri(did: string, collection: string, rkey: string): string { | ||
return `at://${did}/${collection}/${rkey}`; | ||
} | ||
| ||
export function rkeyOf(uri: string): string { | ||
return uri.split("/").pop()!; | ||
} | ||
| ||
/** | ||
* Send every write in a single atomic PDS transaction. | ||
* | ||
* One request instead of one per record: a 10-photo gallery used to be ~31 | ||
* sequential `createRecord` calls, each its own signed repo commit and firehose | ||
* event, which is where the minute-plus publish time came from. It also means a | ||
* failure can no longer leave a half-written gallery behind. | ||
*/ | ||
export async function applyWrites(writes: Write[]): Promise<void> { | ||
if (writes.length === 0) return; | ||
await callXrpc("dev.hatk.applyWrites", { writes } as never); | ||
} | ||
| ||
/** True if the gallery is already in the index. Any failure reads as "no". */ | ||
export async function galleryExists(galleryUri: string): Promise<boolean> { | ||
try { | ||
const res = await callXrpc("social.grain.unspecced.getGallery", { gallery: galleryUri }); | ||
return !!(res as { gallery?: unknown })?.gallery; | ||
} catch { | ||
return false; | ||
} | ||
} | ||
| ||
/** Run `fn` over `items`, at most `limit` at a time, preserving input order. */ | ||
export async function mapWithConcurrency<T, R>( | ||
items: T[], | ||
limit: number, | ||
fn: (item: T, index: number) => Promise<R>, | ||
): Promise<R[]> { | ||
const results: R[] = []; | ||
let next = 0; | ||
const workers = Array.from({ length: Math.min(limit, items.length) }, async () => { | ||
while (next < items.length) { | ||
const index = next++; | ||
results[index] = await fn(items[index], index); | ||
} | ||
}); | ||
await Promise.all(workers); | ||
return results; | ||
} | ||
| ||
/** | ||
* Blobs can't ride along in applyWrites — the PDS takes raw bytes — so they | ||
* stay one request each. Overlapping a few keeps a full gallery from being a | ||
* long chain of serial uploads, while staying gentle enough on a phone's | ||
* connection and memory. | ||
*/ | ||
const UPLOAD_CONCURRENCY = 3; | ||
| ||
export function dataUrlToBlob(dataUrl: string): Blob { | ||
const base64 = dataUrl.split(",")[1]; | ||
const binary = Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)); | ||
return new Blob([binary], { type: "image/jpeg" }); | ||
} | ||
| ||
/** Upload each data URL as a blob and return the blob refs in input order. */ | ||
export async function uploadPhotoBlobs(dataUrls: string[]): Promise<unknown[]> { | ||
return mapWithConcurrency(dataUrls, UPLOAD_CONCURRENCY, async (dataUrl) => { | ||
const result = await callXrpc("dev.hatk.uploadBlob", dataUrlToBlob(dataUrl) as never); | ||
return (result as { blob: unknown }).blob; | ||
}); | ||
} | ||
| ||
/** Mint a fresh record key. Re-exported so callers only import from one place. */ | ||
export { nextTid }; | ||
app/lib/auth.tsdeleted+0−31
app/lib/feed.tsdeleted+0−5
app/lib/labels.tsdeleted+0−95
app/routes/.well-known/did.json/+server.tsdeleted+0−22
app/routes/oauth/callback/+page.sveltedeleted+0−11
app/routes/private/[authority]/[skey]/+page.sveltedeleted+0−309
app/routes/private/create/+page.sveltedeleted+0−274
app/routes/+layout.server.tsdeleted+0−32
crates/grain/src/bin/grain.rsnew+8−0
crates/grain/src/repo/car.rsnew+168−0
crates/grain/src/repo/mod.rsnew+229−0
crates/grain/src/repo/mst.rsnew+264−0
crates/grain/src/repo/star.rsnew+118−0
crates/grain/src/repo/tests.rsnew+439−0
439 lines changed: not loaded until asked for.
crates/grain/src/auth.rsnew+284−0
crates/grain/src/base32.rsnew+81−0
crates/grain/src/cli.rsnew+347−0
crates/grain/src/crypto.rsnew+359−0
crates/grain/src/drisl.rsnew+173−0
crates/grain/src/env.rsnew+294−0
crates/grain/src/guard.rsnew+180−0
crates/grain/src/identity.rsnew+717−0
717 lines changed: not loaded until asked for.
crates/grain/src/jetstream.rsnew+428−0
428 lines changed: not loaded until asked for.
crates/grain/src/keys.rsnew+100−0
crates/grain/src/labels.rsnew+245−0
crates/grain/src/lexicon.rsnew+1319−0
1319 lines changed: not loaded until asked for.
crates/grain/src/lib.rsnew+26−0
crates/grain/src/moderation.rsnew+296−0
crates/grain/src/oauth.rsnew+1438−0
1438 lines changed: not loaded until asked for.
crates/grain/src/session.rsnew+370−0
crates/grain/src/space.rsnew+789−0
789 lines changed: not loaded until asked for.
crates/grain/src/store.rsnew+119−0
crates/grain/src/telemetry.rsnew+883−0
883 lines changed: not loaded until asked for.
crates/grain/src/xrpc.rsnew+479−0
479 lines changed: not loaded until asked for.
crates/grain/tests/common/mod.rsnew+260−0
crates/grain/tests/cli.rsnew+39−0
crates/grain/tests/oauth.rsnew+255−0
crates/grain/tests/pds.rsnew+237−0
crates/grain/tests/repo.rsnew+145−0
crates/grain/tests/spaces.rsnew+144−0
crates/grain/Cargo.tomlnew+32−0
crates/grain-import/src/lib.rsnew+994−0
994 lines changed: not loaded until asked for.
crates/grain-import/src/main.rsnew+9−0
crates/grain-import/src/tests.rsnew+1096−0
1096 lines changed: not loaded until asked for.
crates/grain-import/tests/main.rsnew+43−0
crates/grain-import/Cargo.tomlnew+20−0
crates/grain-mcp/src/lib.rsnew+498−0
498 lines changed: not loaded until asked for.
crates/grain-mcp/src/main.rsnew+18−0
crates/grain-mcp/tests/mcp.rsnew+352−0
crates/grain-mcp/Cargo.tomlnew+22−0
crates/grain-serve/fonts/Inter-Regular.ttfnew+0−0
A binary file, not shown.
crates/grain-serve/fonts/NotoEmoji-Regular.ttfnew+0−0
A binary file, not shown.
crates/grain-serve/fonts/NotoSansJP-Regular.ttfnew+0−0
A binary file, not shown.
crates/grain-serve/fonts/OFL.txtnew+95−0
crates/grain-serve/fonts/Syne-ExtraBold.ttfnew+0−0
A binary file, not shown.
crates/grain-serve/src/classify/classifiers.rsnew+471−0
471 lines changed: not loaded until asked for.
crates/grain-serve/src/classify/clef.rsnew+305−0
crates/grain-serve/src/classify/mod.rsnew+396−0
crates/grain-serve/src/index/backfill.rsnew+147−0
crates/grain-serve/src/index/classify.rsnew+158−0
crates/grain-serve/src/index/graph.rsnew+35−0
crates/grain-serve/src/index/mod.rsnew+609−0
609 lines changed: not loaded until asked for.
crates/grain-serve/src/index/moderation.rsnew+418−0
418 lines changed: not loaded until asked for.
crates/grain-serve/src/index/plan.rsnew+59−0
crates/grain-serve/src/index/plans.rsnew+595−0
595 lines changed: not loaded until asked for.
crates/grain-serve/src/index/preferences.rsnew+138−0
crates/grain-serve/src/index/push.rsnew+153−0
crates/grain-serve/src/index/records.rsnew+345−0
crates/grain-serve/src/index/schema.rsnew+531−0
531 lines changed: not loaded until asked for.
crates/grain-serve/src/index/sessions.rsnew+204−0
crates/grain-serve/src/index/spaces.rsnew+76−0
crates/grain-serve/src/index/stories.rsnew+75−0
crates/grain-serve/src/index/tests.rsnew+750−0
750 lines changed: not loaded until asked for.
crates/grain-serve/src/login/carryover.rsnew+250−0
crates/grain-serve/src/login/profile.rsnew+129−0
crates/grain-serve/src/og/collage.rsnew+404−0
404 lines changed: not loaded until asked for.
crates/grain-serve/src/og/draw.rsnew+420−0
420 lines changed: not loaded until asked for.
crates/grain-serve/src/og/mod.rsnew+580−0
580 lines changed: not loaded until asked for.
crates/grain-serve/src/push/apns.rsnew+185−0
crates/grain-serve/src/push/fcm.rsnew+223−0
crates/grain-serve/src/push/mod.rsnew+201−0
crates/grain-serve/src/push/send.rsnew+258−0
crates/grain-serve/src/push/test-rsa-key.pemnew+28−0
crates/grain-serve/src/push/tests.rsnew+1051−0
1051 lines changed: not loaded until asked for.
crates/grain-serve/src/read/actor.rsnew+273−0
crates/grain-serve/src/read/camera.rsnew+300−0
crates/grain-serve/src/read/comment.rsnew+113−0
crates/grain-serve/src/read/feed.rsnew+242−0
crates/grain-serve/src/read/foryou.rsnew+199−0
crates/grain-serve/src/read/gallery.rsnew+78−0
crates/grain-serve/src/read/graph.rsnew+231−0
crates/grain-serve/src/read/location.rsnew+580−0
580 lines changed: not loaded until asked for.
crates/grain-serve/src/read/mod.rsnew+335−0
crates/grain-serve/src/read/moderation.rsnew+63−0
crates/grain-serve/src/read/notification.rsnew+418−0
418 lines changed: not loaded until asked for.
crates/grain-serve/src/read/place.rsnew+394−0
crates/grain-serve/src/read/search.rsnew+848−0
848 lines changed: not loaded until asked for.
crates/grain-serve/src/read/sitemap.rsnew+134−0
crates/grain-serve/src/read/story.rsnew+130−0
crates/grain-serve/src/read/tests.rsnew+87−0
crates/grain-serve/src/read/views.rsnew+575−0
575 lines changed: not loaded until asked for.
crates/grain-serve/src/spaces/blob.rsnew+105−0
crates/grain-serve/src/spaces/group.rsnew+864−0
864 lines changed: not loaded until asked for.
crates/grain-serve/src/spaces/mod.rsnew+367−0
crates/grain-serve/src/spaces/pool.rsnew+724−0
724 lines changed: not loaded until asked for.
crates/grain-serve/src/spaces/support.rsnew+344−0
crates/grain-serve/src/accounts.rsnew+382−0
crates/grain-serve/src/backfill.rsnew+589−0
589 lines changed: not loaded until asked for.
crates/grain-serve/src/cdn.rsnew+161−0
crates/grain-serve/src/did.rsnew+69−0
crates/grain-serve/src/gallery.rsnew+120−0
crates/grain-serve/src/graph.rsnew+80−0
crates/grain-serve/src/jetstream.rsnew+269−0
crates/grain-serve/src/lib.rsnew+688−0
688 lines changed: not loaded until asked for.
crates/grain-serve/src/login.rsnew+824−0
824 lines changed: not loaded until asked for.
crates/grain-serve/src/main.rsnew+68−0
crates/grain-serve/src/moderation.rsnew+631−0
631 lines changed: not loaded until asked for.
crates/grain-serve/src/notification.rsnew+55−0
crates/grain-serve/src/pages.rsnew+97−0
crates/grain-serve/src/preferences.rsnew+286−0
crates/grain-serve/src/status.rsnew+393−0
crates/grain-serve/src/story.rsnew+59−0
crates/grain-serve/src/sync.rsnew+328−0
crates/grain-serve/src/telemetry.rsnew+214−0
crates/grain-serve/src/viewer.rsnew+122−0
crates/grain-serve/src/write.rsnew+661−0
661 lines changed: not loaded until asked for.
crates/grain-serve/src/xrpc.rsnew+79−0
crates/grain-serve/tests/common/mod.rsnew+178−0
crates/grain-serve/tests/host/mod.rsnew+565−0
565 lines changed: not loaded until asked for.
crates/grain-serve/tests/web/mod.rsnew+228−0
crates/grain-serve/tests/backfill.rsnew+582−0
582 lines changed: not loaded until asked for.
crates/grain-serve/tests/classify.rsnew+815−0
815 lines changed: not loaded until asked for.
crates/grain-serve/tests/cli.rsnew+309−0
crates/grain-serve/tests/cross_post.rsnew+138−0
crates/grain-serve/tests/deletes.rsnew+472−0
472 lines changed: not loaded until asked for.
crates/grain-serve/tests/gone.rsnew+211−0
crates/grain-serve/tests/graph.rsnew+251−0
crates/grain-serve/tests/health.rsnew+190−0
crates/grain-serve/tests/index_records.rsnew+407−0
407 lines changed: not loaded until asked for.
crates/grain-serve/tests/jetstream.rsnew+667−0
667 lines changed: not loaded until asked for.
crates/grain-serve/tests/login.rsnew+852−0
852 lines changed: not loaded until asked for.
crates/grain-serve/tests/main.rsnew+168−0
crates/grain-serve/tests/moderation.rsnew+760−0
760 lines changed: not loaded until asked for.
crates/grain-serve/tests/notifications.rsnew+786−0
786 lines changed: not loaded until asked for.
crates/grain-serve/tests/og.rsnew+348−0
crates/grain-serve/tests/pages.rsnew+163−0
crates/grain-serve/tests/push.rsnew+151−0
crates/grain-serve/tests/read.rsnew+1925−0
1925 lines changed: not loaded until asked for.
crates/grain-serve/tests/sitemap.rsnew+175−0
crates/grain-serve/tests/spaces.rsnew+1055−0
1055 lines changed: not loaded until asked for.
crates/grain-serve/tests/stories.rsnew+225−0
crates/grain-serve/tests/write.rsnew+475−0
475 lines changed: not loaded until asked for.
crates/grain-serve/Cargo.tomlnew+38−0
deploy/deploy.shnew+178−0
deploy/docker-compose.ymlnew+168−0
deploy/Dockerfilenew+55−0
deploy/Dockerfile.dockerignorenew+17−0
deploy/local.env.examplenew+17−0
deploy/logs.shnew+57−0
deploy/README.mdnew+119−0
deploy/rollout.shnew+118−0
docs/observability.mdnew+77−0
harness/answers-test/lexicons/getThing.jsonnew+21−0
harness/answers-test/right/answers.jsonlnew+4−0
harness/answers-test/wrong/answers.jsonlnew+5−0
harness/e2e/flows.spec.mjsnew+256−0
harness/e2e/pages.mjsnew+142−0
harness/e2e/playwright.config.mjsnew+39−0
harness/e2e/setup.mjsnew+184−0
harness/e2e/spaces.spec.mjsnew+51−0
harness/.node-versionnew+1−0
harness/answers-test.shnew+21−0
harness/answers.mjsnew+100−0
harness/authorize.mjsnew+159−0
harness/cov.jsonnew+1−0
harness/cov.mjsnew+673−0
673 lines changed: not loaded until asked for.
harness/lexicons.mjsnew+334−0
harness/package-lock.jsonnew+9488−0
A large diff, not shown.
harness/package.jsonnew+13−0
harness/pds.mjsnew+201−0
harness/plans.mjsnew+56−0
harness/quarantine.jsonnew+1−0
harness/tls-proxy.mjsnew+29−0
lexicons/com/atproto/admin/defs.jsonnew+47−0
lexicons/com/atproto/admin/getSubjectStatus.jsonnew+43−0
lexicons/com/atproto/admin/updateSubjectStatus.jsonnew+56−0
lexicons/com/atproto/label/queryLabels.jsonnew+47−0
lexicons/com/atproto/moderation/createReport.jsonnew+78−0
lexicons/com/atproto/moderation/defs.json+56−8
lexicons/social/grain/actor/defs.json+108−2
lexicons/social/grain/actor/getFavorites.jsonnew+33−0
lexicons/social/grain/actor/getPreferences.jsonnew+20−0
lexicons/social/grain/actor/getProfile.jsonnew+31−0
lexicons/social/grain/actor/getSuggestedFollows.jsonnew+30−0
lexicons/social/grain/actor/putPreferences.jsonnew+24−0
lexicons/social/grain/actor/searchActors.jsonnew+33−0
lexicons/social/grain/actor/searchActorsTypeahead.jsonnew+30−0
lexicons/social/grain/admin/actOnReview.jsonnew+33−0
lexicons/social/grain/admin/addRepos.jsonnew+33−0
lexicons/social/grain/admin/applyLabel.jsonnew+29−0
lexicons/social/grain/admin/defs.jsonnew+163−0
lexicons/social/grain/admin/getInfo.jsonnew+29−0
lexicons/social/grain/admin/getRepo.jsonnew+20−0
lexicons/social/grain/admin/getReport.jsonnew+24−0
lexicons/social/grain/admin/getReviewQueue.jsonnew+46−0
lexicons/social/grain/admin/getReviewStats.jsonnew+32−0
lexicons/social/grain/admin/getReviewSubject.jsonnew+50−0
lexicons/social/grain/admin/listLabelDefinitions.jsonnew+23−0
lexicons/social/grain/admin/listReports.jsonnew+38−0
lexicons/social/grain/admin/listRepos.jsonnew+34−0
lexicons/social/grain/admin/negateLabel.jsonnew+29−0
lexicons/social/grain/admin/removeRepos.jsonnew+33−0
lexicons/social/grain/admin/resolveReport.jsonnew+26−0
lexicons/social/grain/admin/resyncRepos.jsonnew+32−0
lexicons/social/grain/admin/searchRecords.jsonnew+32−0
lexicons/social/grain/admin/startScan.jsonnew+43−0
lexicons/social/grain/comment/comment.json+1−0
lexicons/social/grain/comment/getThread.jsonnew+38−0
lexicons/social/grain/favorite/favorite.json+1−0
lexicons/social/grain/feed/defs.jsonnew+22−0
lexicons/social/grain/feed/describeFeeds.jsonnew+28−0
lexicons/social/grain/feed/getFeed.jsonnew+47−0
lexicons/social/grain/gallery/defs.json+49−0
lexicons/social/grain/gallery/deleteGallery.jsonnew+33−0
lexicons/social/grain/gallery/gallery.json+1−0
lexicons/social/grain/gallery/getCameras.jsonnew+22−0
lexicons/social/grain/gallery/getFavorites.jsonnew+33−0
lexicons/social/grain/gallery/getGallery.jsonnew+33−0
lexicons/social/grain/gallery/getLocations.jsonnew+31−0
lexicons/social/grain/gallery/item.json+1−0
lexicons/social/grain/gallery/searchGalleries.jsonnew+33−0
lexicons/social/grain/graph/block.json+1−0
lexicons/social/grain/graph/defs.jsonnew+66−0
lexicons/social/grain/graph/follow.json+1−0
lexicons/social/grain/graph/getBlocks.jsonnew+30−0
lexicons/social/grain/graph/getFollowers.jsonnew+33−0
lexicons/social/grain/graph/getFollowing.jsonnew+33−0
lexicons/social/grain/graph/getKnownFollowers.jsonnew+30−0
lexicons/social/grain/graph/getMutes.jsonnew+30−0
lexicons/social/grain/group/createComment.jsonnew+41−0
lexicons/social/grain/group/createGallery.jsonnew+57−0
lexicons/social/grain/group/createGroup.jsonnew+54−0
lexicons/social/grain/group/defs.json+125−0
lexicons/social/grain/group/deleteComment.jsonnew+28−0
lexicons/social/grain/group/deleteGallery.jsonnew+38−0
lexicons/social/grain/group/getGallery.jsonnew+52−0
lexicons/social/grain/group/getGroup.jsonnew+19−0
lexicons/social/grain/group/getGroupHost.jsonnew+24−0
lexicons/social/grain/group/getSubmissions.jsonnew+33−0
lexicons/social/grain/group/joinGroup.jsonnew+31−0
lexicons/social/grain/group/leaveGroup.jsonnew+22−0
lexicons/social/grain/group/listFeed.jsonnew+37−0
lexicons/social/grain/group/listGalleries.jsonnew+35−0
lexicons/social/grain/group/listGroups.jsonnew+32−0
lexicons/social/grain/group/putFavorite.jsonnew+40−0
lexicons/social/grain/moderation/defs.jsonnew+14−0
lexicons/social/grain/moderation/describeLabels.jsonnew+28−0
lexicons/social/grain/notification/defs.jsonnew+45−0
lexicons/social/grain/notification/getUnreadCount.jsonnew+18−0
lexicons/social/grain/notification/listNotifications.jsonnew+36−0
lexicons/social/grain/notification/registerPush.jsonnew+35−0
lexicons/social/grain/notification/unregisterPush.jsonnew+35−0
lexicons/social/grain/notification/updateSeen.jsonnew+22−0
lexicons/social/grain/photo/photo.json+1−0
lexicons/social/grain/repo/applyWrites.jsonnew+102−0
lexicons/social/grain/repo/createRecord.jsonnew+43−0
lexicons/social/grain/repo/defs.jsonnew+15−0
lexicons/social/grain/repo/deleteRecord.jsonnew+39−0
lexicons/social/grain/repo/putRecord.jsonnew+44−0
lexicons/social/grain/repo/uploadBlob.jsonnew+25−0
lexicons/social/grain/server/defs.jsonnew+17−0
lexicons/social/grain/server/deleteAccount.jsonnew+64−0
lexicons/social/grain/server/getSitemap.jsonnew+35−0
lexicons/social/grain/server/getSpaceSupport.jsonnew+39−0
lexicons/social/grain/story/defs.json+18−0
lexicons/social/grain/story/getArchive.jsonnew+33−0
lexicons/social/grain/story/getAuthors.jsonnew+23−0
lexicons/social/grain/story/getStories.jsonnew+28−0
lexicons/social/grain/story/getStory.jsonnew+22−0
lexicons/social/grain/story/markViewed.jsonnew+30−0
lexicons/social/grain/story/story.json+1−0
lexicons/social/grain/sync/indexRecords.jsonnew+52−0
lexicons/social/grain/basePermissions.json+1−0
lexicons/README.mdnew+145−0
marketing/capture-og.mjs+2−2
server/helpers/cameraName.ts+1−1
test/web/auth.test.tsnew+144−0
test/web/grainServe.test.tsnew+97−0
test/web/preferences.test.tsnew+131−0
test/web/writes.test.tsnew+103−0
test/web/xrpc.test.tsnew+149−0
test/galleryDetail.test.ts+1−1
test/resolveRouteActor.test.ts+5−5
test/storyViews.test.ts+8−7
test/tid.test.ts+1−1
app/lib/actions/infinite-scroll.ts → web/src/lib/actions/infinite-scroll.tsrenamed+0−0
Moved without changes.
app/lib/components/atoms/Avatar.svelte → web/src/lib/components/atoms/Avatar.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/AvatarLightbox.svelte → web/src/lib/components/atoms/AvatarLightbox.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/BskyIcon.svelte → web/src/lib/components/atoms/BskyIcon.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/Button.svelte → web/src/lib/components/atoms/Button.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/Checkbox.svelte → web/src/lib/components/atoms/Checkbox.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/ContentWarningPicker.svelte → web/src/lib/components/atoms/ContentWarningPicker.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/ExifInfo.svelte → web/src/lib/components/atoms/ExifInfo.svelterenamed+1−1
app/lib/components/atoms/Facepile.svelte → web/src/lib/components/atoms/Facepile.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/Field.svelte → web/src/lib/components/atoms/Field.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/FileDropZone.svelte → web/src/lib/components/atoms/FileDropZone.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/Input.svelte → web/src/lib/components/atoms/Input.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/LocationInput.svelte → web/src/lib/components/atoms/LocationInput.svelterenamed+1−1
app/lib/components/atoms/LocationMapBanner.svelte → web/src/lib/components/atoms/LocationMapBanner.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/Modal.svelte → web/src/lib/components/atoms/Modal.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/NotificationItem.svelte → web/src/lib/components/atoms/NotificationItem.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/OGMeta.svelte → web/src/lib/components/atoms/OGMeta.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/OverflowMenu.svelte → web/src/lib/components/atoms/OverflowMenu.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/PinButton.svelte → web/src/lib/components/atoms/PinButton.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/RichText.svelte → web/src/lib/components/atoms/RichText.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/RichTextarea.svelte → web/src/lib/components/atoms/RichTextarea.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/SelectCheck.svelte → web/src/lib/components/atoms/SelectCheck.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/SettingsGroup.svelte → web/src/lib/components/atoms/SettingsGroup.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/SettingsToggleRow.svelte → web/src/lib/components/atoms/SettingsToggleRow.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/Skeleton.svelte → web/src/lib/components/atoms/Skeleton.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/Spinner.svelte → web/src/lib/components/atoms/Spinner.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/Textarea.svelte → web/src/lib/components/atoms/Textarea.svelterenamed+0−0
Moved without changes.
app/lib/components/atoms/Toast.svelte → web/src/lib/components/atoms/Toast.svelterenamed+0−0
Moved without changes.
app/lib/components/molecules/AvatarCrop.svelte → web/src/lib/components/molecules/AvatarCrop.svelterenamed+0−0
Moved without changes.
app/lib/components/molecules/BrowseTile.svelte → web/src/lib/components/molecules/BrowseTile.svelterenamed+0−0
Moved without changes.
app/lib/components/molecules/Comment.svelte → web/src/lib/components/molecules/Comment.svelterenamed+9−7
app/lib/components/molecules/DetailHeader.svelte → web/src/lib/components/molecules/DetailHeader.svelterenamed+0−0
Moved without changes.
app/lib/components/molecules/FavedByFollowing.svelte → web/src/lib/components/molecules/FavedByFollowing.svelterenamed+0−0
Moved without changes.
app/lib/components/molecules/FavoriteButton.svelte → web/src/lib/components/molecules/FavoriteButton.svelterenamed+15−12
app/lib/components/molecules/FeedTabs.svelte → web/src/lib/components/molecules/FeedTabs.svelterenamed+0−0
Moved without changes.
app/lib/components/molecules/FollowButton.svelte → web/src/lib/components/molecules/FollowButton.svelterenamed+4−5
app/lib/components/molecules/GalleryCard.svelte → web/src/lib/components/molecules/GalleryCard.svelterenamed+15−16
app/lib/components/molecules/GalleryCardSkeleton.svelte → web/src/lib/components/molecules/GalleryCardSkeleton.svelterenamed+0−0
Moved without changes.
app/lib/components/molecules/GalleryMedia.svelte → web/src/lib/components/molecules/GalleryMedia.svelterenamed+1−1
app/lib/components/molecules/GalleryMenu.svelte → web/src/lib/components/molecules/GalleryMenu.svelterenamed+4−3
app/lib/components/molecules/GallerySectionRow.svelte → web/src/lib/components/molecules/GallerySectionRow.svelterenamed+2−1
app/lib/components/molecules/IosAppBanner.svelte → web/src/lib/components/molecules/IosAppBanner.svelterenamed+0−0
Moved without changes.
app/lib/components/molecules/LocationsMap.svelte → web/src/lib/components/molecules/LocationsMap.svelterenamed+0−0
Moved without changes.
app/lib/components/molecules/MobileBottomBar.svelte → web/src/lib/components/molecules/MobileBottomBar.svelterenamed+0−0
Moved without changes.
app/lib/components/molecules/MobileTopBar.svelte → web/src/lib/components/molecules/MobileTopBar.svelterenamed+0−0
Moved without changes.
app/lib/components/molecules/PageHeading.svelte → web/src/lib/components/molecules/PageHeading.svelterenamed+0−0
Moved without changes.
app/lib/components/molecules/ProfileCard.svelte → web/src/lib/components/molecules/ProfileCard.svelterenamed+0−0
Moved without changes.
app/lib/components/molecules/ProfilePopover.svelte → web/src/lib/components/molecules/ProfilePopover.svelterenamed+2−1
app/lib/components/molecules/PublicTopBar.svelte → web/src/lib/components/molecules/PublicTopBar.svelterenamed+0−0
Moved without changes.
app/lib/components/molecules/PullToRefresh.svelte → web/src/lib/components/molecules/PullToRefresh.svelterenamed+0−0
Moved without changes.
app/lib/components/molecules/ReportButton.svelte → web/src/lib/components/molecules/ReportButton.svelterenamed+8−6
app/lib/components/molecules/SpacesRequired.svelte → web/src/lib/components/molecules/SpacesRequired.svelterenamed+0−0
Moved without changes.
app/lib/components/molecules/StoryArchive.svelte → web/src/lib/components/molecules/StoryArchive.svelterenamed+0−0
Moved without changes.
app/lib/components/molecules/StoryCreate.svelte → web/src/lib/components/molecules/StoryCreate.svelterenamed+3−3
app/lib/components/molecules/StoryStrip.svelte → web/src/lib/components/molecules/StoryStrip.svelterenamed+0−0
Moved without changes.
app/lib/components/organisms/AuthBar.svelte → web/src/lib/components/organisms/AuthBar.svelterenamed+0−0
Moved without changes.
app/lib/components/organisms/CommentSheet.svelte → web/src/lib/components/organisms/CommentSheet.svelterenamed+30−22
app/lib/components/organisms/FeedList.svelte → web/src/lib/components/organisms/FeedList.svelterenamed+1−1
app/lib/components/organisms/GalleryGrid.svelte → web/src/lib/components/organisms/GalleryGrid.svelterenamed+2−1
app/lib/components/organisms/LoginModal.svelte → web/src/lib/components/organisms/LoginModal.svelterenamed+12−3
app/lib/components/organisms/LoginWall.svelte → web/src/lib/components/organisms/LoginWall.svelterenamed+0−0
Moved without changes.
app/lib/components/organisms/MobileDrawer.svelte → web/src/lib/components/organisms/MobileDrawer.svelterenamed+0−0
Moved without changes.
app/lib/components/organisms/MobileSearch.svelte → web/src/lib/components/organisms/MobileSearch.svelterenamed+0−0
Moved without changes.
app/lib/components/organisms/SearchFlyout.svelte → web/src/lib/components/organisms/SearchFlyout.svelterenamed+3−2
app/lib/components/organisms/Sidebar.svelte → web/src/lib/components/organisms/Sidebar.svelterenamed+0−0
Moved without changes.
app/lib/components/organisms/StoryViewer.svelte → web/src/lib/components/organisms/StoryViewer.svelterenamed+4−4
app/lib/components/organisms/SuggestedFollows.svelte → web/src/lib/components/organisms/SuggestedFollows.svelterenamed+4−2
app/lib/components/templates/Shell.svelte → web/src/lib/components/templates/Shell.svelterenamed+0−0
Moved without changes.
web/src/lib/server/grainServe.tsnew+90−0
app/lib/utils/bsky-post.ts → web/src/lib/utils/bsky-post.tsrenamed+3−3
app/lib/utils/cameraName.ts → web/src/lib/utils/cameraName.tsrenamed+0−0
Moved without changes.
app/lib/utils/formatLocation.ts → web/src/lib/utils/formatLocation.tsrenamed+1−1
app/lib/utils/h3.ts → web/src/lib/utils/h3.tsrenamed+0−0
Moved without changes.
app/lib/utils/image-resize.ts → web/src/lib/utils/image-resize.tsrenamed+0−0
Moved without changes.
app/lib/utils/instagram-import.ts → web/src/lib/utils/instagram-import.tsrenamed+0−0
Moved without changes.
app/lib/utils/nominatim.ts → web/src/lib/utils/nominatim.tsrenamed+0−0
Moved without changes.
web/src/lib/utils/records.tsnew+171−0
app/lib/utils/rich-text.ts → web/src/lib/utils/rich-text.tsrenamed+0−0
Moved without changes.
app/lib/utils/tid.ts → web/src/lib/utils/tid.tsrenamed+0−0
Moved without changes.
web/src/lib/xrpc/index.tsnew+138−0
app/lib/accounts.ts → web/src/lib/accounts.tsrenamed+25−44
app/lib/actor.ts → web/src/lib/actor.tsrenamed+1−1
web/src/lib/auth.tsnew+120−0
app/lib/basemap.ts → web/src/lib/basemap.tsrenamed+0−0
Moved without changes.
web/src/lib/feed.tsnew+46−0
web/src/lib/labels.tsnew+186−0
app/lib/mutations.ts → web/src/lib/mutations.tsrenamed+33−14
app/lib/notifications.ts → web/src/lib/notifications.tsrenamed+0−0
Moved without changes.
web/src/lib/pds.tsnew+47−0
app/lib/preferences.ts → web/src/lib/preferences.tsrenamed+13−10
web/src/lib/preferenceSync.tsnew+149−0
app/lib/queries.ts → web/src/lib/queries.tsrenamed+173−136
app/lib/seo.ts → web/src/lib/seo.tsrenamed+4−2
app/lib/sitemap.ts → web/src/lib/sitemap.tsrenamed+0−0
Moved without changes.
app/lib/spaceBlob.ts → web/src/lib/spaceBlob.tsrenamed+0−0
Moved without changes.
app/lib/stores.ts → web/src/lib/stores.tsrenamed+0−0
Moved without changes.
app/lib/stories.ts → web/src/lib/stories.tsrenamed+6−3
app/lib/theme.ts → web/src/lib/theme.tsrenamed+2−2
app/lib/utils.ts → web/src/lib/utils.tsrenamed+0−0
Moved without changes.
app/routes/.well-known/apple-app-site-association/+server.ts → web/src/routes/.well-known/apple-app-site-association/+server.tsrenamed+0−0
Moved without changes.
app/routes/camera/[name]/+page.svelte → web/src/routes/camera/[name]/+page.svelterenamed+0−0
Moved without changes.
app/routes/camera/[name]/+page.ts → web/src/routes/camera/[name]/+page.tsrenamed+0−0
Moved without changes.
app/routes/cameras/+page.svelte → web/src/routes/cameras/+page.svelterenamed+0−0
Moved without changes.
app/routes/cameras/+page.ts → web/src/routes/cameras/+page.tsrenamed+0−0
Moved without changes.
app/routes/create/+page.server.ts → web/src/routes/create/+page.server.tsrenamed+0−0
Moved without changes.
app/routes/create/+page.svelte → web/src/routes/create/+page.svelterenamed+1−1
app/routes/embed/gallery/[actor]/[rkey]/+server.ts → web/src/routes/embed/gallery/[actor]/[rkey]/+server.tsrenamed+0−0
Moved without changes.
app/routes/explore/+page.svelte → web/src/routes/explore/+page.svelterenamed+0−0
Moved without changes.
app/routes/explore/+page.ts → web/src/routes/explore/+page.tsrenamed+0−0
Moved without changes.
app/routes/feeds/following/+page.svelte → web/src/routes/feeds/following/+page.svelterenamed+0−0
Moved without changes.
app/routes/feeds/following/+page.ts → web/src/routes/feeds/following/+page.tsrenamed+0−0
Moved without changes.
app/routes/feeds/for-you/+page.svelte → web/src/routes/feeds/for-you/+page.svelterenamed+0−0
Moved without changes.
app/routes/feeds/for-you/+page.ts → web/src/routes/feeds/for-you/+page.tsrenamed+0−0
Moved without changes.
app/routes/feeds/groups/+page.svelte → web/src/routes/feeds/groups/+page.svelterenamed+3−3
app/routes/feeds/recent/+page.svelte → web/src/routes/feeds/recent/+page.svelterenamed+0−0
Moved without changes.
app/routes/feeds/+page.svelte → web/src/routes/feeds/+page.svelterenamed+0−0
Moved without changes.
app/routes/group/[did]/create/+page.svelte → web/src/routes/group/[did]/create/+page.svelterenamed+7−5
app/routes/group/[did]/+page.svelte → web/src/routes/group/[did]/+page.svelterenamed+1−1
app/routes/group/[did]/+page.ts → web/src/routes/group/[did]/+page.tsrenamed+0−0
Moved without changes.
app/routes/groups/new/+page.svelte → web/src/routes/groups/new/+page.svelterenamed+0−0
Moved without changes.
app/routes/groups/+page.svelte → web/src/routes/groups/+page.svelterenamed+0−0
Moved without changes.
app/routes/groups/+page.ts → web/src/routes/groups/+page.tsrenamed+0−0
Moved without changes.
app/routes/hashtags/[tag]/+page.svelte → web/src/routes/hashtags/[tag]/+page.svelterenamed+0−0
Moved without changes.
app/routes/hashtags/[tag]/+page.ts → web/src/routes/hashtags/[tag]/+page.tsrenamed+0−0
Moved without changes.
app/routes/location/[h3]/+page.svelte → web/src/routes/location/[h3]/+page.svelterenamed+0−0
Moved without changes.
app/routes/location/[h3]/+page.ts → web/src/routes/location/[h3]/+page.tsrenamed+0−0
Moved without changes.
app/routes/locations/+page.svelte → web/src/routes/locations/+page.svelterenamed+0−0
Moved without changes.
app/routes/locations/+page.ts → web/src/routes/locations/+page.tsrenamed+0−0
Moved without changes.
app/routes/notifications/+page.svelte → web/src/routes/notifications/+page.svelterenamed+3−2
app/routes/profile/[actor]/followers/+page.svelte → web/src/routes/profile/[actor]/followers/+page.svelterenamed+0−0
Moved without changes.
app/routes/profile/[actor]/followers/+page.ts → web/src/routes/profile/[actor]/followers/+page.tsrenamed+0−0
Moved without changes.
app/routes/profile/[actor]/following/+page.svelte → web/src/routes/profile/[actor]/following/+page.svelterenamed+0−0
Moved without changes.
app/routes/profile/[actor]/following/+page.ts → web/src/routes/profile/[actor]/following/+page.tsrenamed+0−0
Moved without changes.
app/routes/profile/[actor]/gallery/[rkey]/edit/+page.svelte → web/src/routes/profile/[actor]/gallery/[rkey]/edit/+page.svelterenamed+5−4
app/routes/profile/[actor]/gallery/[rkey]/edit/+page.ts → web/src/routes/profile/[actor]/gallery/[rkey]/edit/+page.tsrenamed+0−0
Moved without changes.
app/routes/profile/[actor]/gallery/[rkey]/favorited-by/+page.svelte → web/src/routes/profile/[actor]/gallery/[rkey]/favorited-by/+page.svelterenamed+0−0
Moved without changes.
app/routes/profile/[actor]/gallery/[rkey]/favorited-by/+page.ts → web/src/routes/profile/[actor]/gallery/[rkey]/favorited-by/+page.tsrenamed+0−0
Moved without changes.
app/routes/profile/[actor]/gallery/[rkey]/+page.svelte → web/src/routes/profile/[actor]/gallery/[rkey]/+page.svelterenamed+3−3
app/routes/profile/[actor]/gallery/[rkey]/+page.ts → web/src/routes/profile/[actor]/gallery/[rkey]/+page.tsrenamed+0−0
Moved without changes.
app/routes/profile/[actor]/known-followers/+page.svelte → web/src/routes/profile/[actor]/known-followers/+page.svelterenamed+0−0
Moved without changes.
app/routes/profile/[actor]/known-followers/+page.ts → web/src/routes/profile/[actor]/known-followers/+page.tsrenamed+0−0
Moved without changes.
app/routes/profile/[actor]/story/[rkey]/+page.svelte → web/src/routes/profile/[actor]/story/[rkey]/+page.svelterenamed+1−1
app/routes/profile/[actor]/story/[rkey]/+page.ts → web/src/routes/profile/[actor]/story/[rkey]/+page.tsrenamed+0−0
Moved without changes.
app/routes/profile/[actor]/+page.svelte → web/src/routes/profile/[actor]/+page.svelterenamed+3−2
app/routes/profile/[actor]/+page.ts → web/src/routes/profile/[actor]/+page.tsrenamed+0−0
Moved without changes.
app/routes/search/+page.svelte → web/src/routes/search/+page.svelterenamed+0−0
Moved without changes.
app/routes/settings/account/+page.svelte → web/src/routes/settings/account/+page.svelterenamed+21−63
app/routes/settings/appearance/+page.svelte → web/src/routes/settings/appearance/+page.svelterenamed+0−0
Moved without changes.
app/routes/settings/import/+page.svelte → web/src/routes/settings/import/+page.svelterenamed+1−0
app/routes/settings/moderation/blocked/+page.svelte → web/src/routes/settings/moderation/blocked/+page.svelterenamed+0−0
Moved without changes.
app/routes/settings/moderation/muted/+page.svelte → web/src/routes/settings/moderation/muted/+page.svelterenamed+0−0
Moved without changes.
app/routes/settings/moderation/+page.svelte → web/src/routes/settings/moderation/+page.svelterenamed+0−0
Moved without changes.
app/routes/settings/notifications/+page.svelte → web/src/routes/settings/notifications/+page.svelterenamed+4−4
app/routes/settings/profile/+page.svelte → web/src/routes/settings/profile/+page.svelterenamed+6−7
app/routes/settings/profile/+page.ts → web/src/routes/settings/profile/+page.tsrenamed+0−0
Moved without changes.
app/routes/settings/upload-defaults/+page.svelte → web/src/routes/settings/upload-defaults/+page.svelterenamed+0−0
Moved without changes.
app/routes/settings/+layout.server.ts → web/src/routes/settings/+layout.server.tsrenamed+0−0
Moved without changes.
app/routes/settings/+layout.svelte → web/src/routes/settings/+layout.svelterenamed+0−0
Moved without changes.
app/routes/settings/+layout.ts → web/src/routes/settings/+layout.tsrenamed+0−0
Moved without changes.
app/routes/settings/+page.svelte → web/src/routes/settings/+page.svelterenamed+0−0
Moved without changes.
app/routes/sitemap/[kind]-[page].xml/+server.ts → web/src/routes/sitemap/[kind]-[page].xml/+server.tsrenamed+8−6
app/routes/sitemap.xml/+server.ts → web/src/routes/sitemap.xml/+server.tsrenamed+10−3
app/routes/support/child-safety/+page.svelte → web/src/routes/support/child-safety/+page.svelterenamed+0−0
Moved without changes.
app/routes/support/community-guidelines/+page.svelte → web/src/routes/support/community-guidelines/+page.svelterenamed+0−0
Moved without changes.
app/routes/support/copyright/+page.svelte → web/src/routes/support/copyright/+page.svelterenamed+0−0
Moved without changes.
app/routes/support/privacy/+page.svelte → web/src/routes/support/privacy/+page.svelterenamed+0−0
Moved without changes.
app/routes/support/terms/+page.svelte → web/src/routes/support/terms/+page.svelterenamed+0−0
Moved without changes.
app/routes/+error.svelte → web/src/routes/+error.svelterenamed+0−0
Moved without changes.
web/src/routes/+layout.server.tsnew+27−0
app/routes/+layout.svelte → web/src/routes/+layout.svelterenamed+16−31
app/routes/+layout.ts → web/src/routes/+layout.tsrenamed+0−0
Moved without changes.
app/routes/+page.svelte → web/src/routes/+page.svelterenamed+0−0
Moved without changes.
app/routes/+page.ts → web/src/routes/+page.tsrenamed+0−0
Moved without changes.
app/app.css → web/src/app.cssrenamed+0−0
Moved without changes.
app/app.html → web/src/app.htmlrenamed+0−0
Moved without changes.
app/error.html → web/src/error.htmlrenamed+0−0
Moved without changes.
web/src/hooks.server.tsnew+18−0
app/service-worker.js → web/src/service-worker.jsrenamed+0−0
Moved without changes.
static/atmo/atmo-anisota.jpg → web/static/atmo/atmo-anisota.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-beacon-bits.jpg → web/static/atmo/atmo-beacon-bits.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-blacksky.jpg → web/static/atmo/atmo-blacksky.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-blento.jpg → web/static/atmo/atmo-blento.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-bluesky.jpg → web/static/atmo/atmo-bluesky.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-eurosky.jpg → web/static/atmo/atmo-eurosky.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-flashes.jpg → web/static/atmo/atmo-flashes.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-gander.jpg → web/static/atmo/atmo-gander.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-germ.jpg → web/static/atmo/atmo-germ.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-leaflet.jpg → web/static/atmo/atmo-leaflet.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-northsky.jpg → web/static/atmo/atmo-northsky.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-offprint.jpg → web/static/atmo/atmo-offprint.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-pckt.jpg → web/static/atmo/atmo-pckt.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-plyr.jpg → web/static/atmo/atmo-plyr.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-popfeed.jpg → web/static/atmo/atmo-popfeed.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-semble.jpg → web/static/atmo/atmo-semble.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-skylight.jpg → web/static/atmo/atmo-skylight.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-slices.jpg → web/static/atmo/atmo-slices.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-spark.png → web/static/atmo/atmo-spark.pngrenamed+0−0
Moved without changes.
static/atmo/atmo-stream-place.jpg → web/static/atmo/atmo-stream-place.jpgrenamed+0−0
Moved without changes.
static/atmo/atmo-tangled.jpg → web/static/atmo/atmo-tangled.jpgrenamed+0−0
Moved without changes.
web/static/app-version.jsonnew+10−0
static/favicon.png → web/static/favicon.pngrenamed+0−0
Moved without changes.
static/germ-logo.png → web/static/germ-logo.pngrenamed+0−0
Moved without changes.
static/icon-192.png → web/static/icon-192.pngrenamed+0−0
Moved without changes.
static/icon-512.png → web/static/icon-512.pngrenamed+0−0
Moved without changes.
static/manifest.json → web/static/manifest.jsonrenamed+0−0
Moved without changes.
static/og-default.jpg → web/static/og-default.jpgrenamed+0−0
Moved without changes.
static/robots.txt → web/static/robots.txtrenamed+0−0
Moved without changes.
.gitignore+4−0
AGENTS.md+173−6
Cargo.locknew+3277−0
A large diff, not shown.
Cargo.tomlnew+87−0
hatk.config.ts+2−2
hatk.generated.client.ts+3−3
hatk.generated.ts+557−175
732 lines changed: not loaded until asked for.
Justfilenew+123−0
justfiledeleted+0−24
package-lock.json+1228−3
1231 lines changed: not loaded until asked for.