commit e5d7d3eb6a0241c4007e5ce68b2eb585d29398fe
parent 9807c951f1c902c2c045c9c1833068a66e769c89
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Wed, 12 Aug 2026 09:41:48 +0200
Add static deployment site
Diffstat:
32 files changed, 1127 insertions(+), 407 deletions(-)
diff --git a/.dockerignore b/.dockerignore
@@ -0,0 +1,10 @@
+target
+src-tauri/target
+src-tauri/binaries
+.agents
+.codex
+.DS_Store
+
+# Keep git history in the build context. The static git browser and the
+# clonable dumb-HTTP repo are generated from this metadata.
+!.git
diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml
@@ -1,42 +0,0 @@
-name: Deploy GitHub Pages
-
-on:
- push:
- branches: ["main"]
- paths:
- - "www/**"
- - ".github/workflows/pages.yml"
- workflow_dispatch:
-
-permissions:
- contents: read
- pages: write
- id-token: write
-
-concurrency:
- group: "pages"
- cancel-in-progress: false
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v7
- - name: Setup Pages
- uses: actions/configure-pages@v6
- - name: Upload artifact
- uses: actions/upload-pages-artifact@v5
- with:
- path: www
-
- deploy:
- needs: build
- environment:
- name: github-pages
- url: ${{ steps.deployment.outputs.page_url }}
- runs-on: ubuntu-latest
- steps:
- - name: Deploy to GitHub Pages
- id: deployment
- uses: actions/deploy-pages@v5
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
@@ -1,194 +0,0 @@
-name: Build Release
-
-on:
- push:
- tags:
- - "v*"
- workflow_dispatch:
-
-permissions:
- contents: write
-
-jobs:
- build:
- name: Build ${{ matrix.platform }}
- runs-on: ${{ matrix.os }}
- strategy:
- fail-fast: false
- matrix:
- include:
- - os: ubuntu-latest
- platform: linux-x86_64
- exe: ""
- archive: tar.gz
- - os: ubuntu-24.04-arm
- platform: linux-aarch64
- exe: ""
- archive: tar.gz
- - os: macos-15-intel
- platform: macos-x86_64
- exe: ""
- archive: tar.gz
- - os: macos-14
- platform: macos-aarch64
- exe: ""
- archive: tar.gz
- - os: windows-latest
- platform: windows-x86_64
- exe: ".exe"
- archive: zip
-
- steps:
- - name: Checkout
- uses: actions/checkout@v7
-
- - name: Install Rust
- shell: bash
- run: |
- rustup toolchain install stable --profile minimal
- rustup default stable
-
- - name: Build
- run: cargo build --release --locked
-
- - name: Package Unix
- if: matrix.archive == 'tar.gz'
- shell: bash
- run: |
- tag="${GITHUB_REF_NAME:-manual}"
- package="iuna-${tag}-${{ matrix.platform }}"
- mkdir -p "dist/${package}"
- cp "target/release/iuna" "dist/${package}/"
- cp README.md LICENSE "dist/${package}/"
- tar -C dist -czf "dist/${package}.tar.gz" "${package}"
-
- - name: Package Windows
- if: matrix.archive == 'zip'
- shell: pwsh
- run: |
- $tag = if ($env:GITHUB_REF_NAME) { $env:GITHUB_REF_NAME } else { "manual" }
- $package = "iuna-$tag-${{ matrix.platform }}"
- New-Item -ItemType Directory -Force -Path "dist/$package" | Out-Null
- Copy-Item "target/release/iuna${{ matrix.exe }}" "dist/$package/"
- Copy-Item "README.md", "LICENSE" "dist/$package/"
- Compress-Archive -Path "dist/$package" -DestinationPath "dist/$package.zip" -Force
-
- - name: Upload build artifact
- uses: actions/upload-artifact@v7
- with:
- name: iuna-${{ github.ref_name }}-${{ matrix.platform }}
- path: dist/iuna-${{ github.ref_name }}-${{ matrix.platform }}.*
- if-no-files-found: error
-
- desktop:
- name: Build ${{ matrix.platform }}
- runs-on: ${{ matrix.os }}
- strategy:
- fail-fast: false
- matrix:
- include:
- - os: macos-14
- platform: macos-aarch64-desktop
- sidecar: iuna-sidecar-aarch64-apple-darwin
- bundle: app
- - os: windows-latest
- platform: windows-x86_64-desktop
- sidecar: iuna-sidecar-x86_64-pc-windows-msvc.exe
- bundle: nsis
-
- steps:
- - name: Checkout
- uses: actions/checkout@v7
-
- - name: Install Rust
- shell: bash
- run: |
- rustup toolchain install stable --profile minimal
- rustup default stable
-
- - name: Build iuna sidecar
- run: cargo build --release --locked
-
- - name: Prepare macOS sidecar
- if: runner.os == 'macOS'
- shell: bash
- run: |
- mkdir -p src-tauri/binaries
- cp target/release/iuna "src-tauri/binaries/${{ matrix.sidecar }}"
- chmod +x "src-tauri/binaries/${{ matrix.sidecar }}"
-
- - name: Prepare Windows sidecar
- if: runner.os == 'Windows'
- shell: pwsh
- run: |
- New-Item -ItemType Directory -Force -Path "src-tauri/binaries" | Out-Null
- Copy-Item "target/release/iuna.exe" "src-tauri/binaries/${{ matrix.sidecar }}"
-
- - name: Install Tauri CLI
- run: cargo +stable install tauri-cli --locked --version "^2"
-
- - name: Build desktop app
- working-directory: src-tauri
- run: cargo +stable tauri build --bundles ${{ matrix.bundle }}
-
- - name: Sign macOS app
- if: runner.os == 'macOS'
- shell: bash
- run: |
- app="src-tauri/target/release/bundle/macos/iuna.app"
- codesign --force --deep --sign - --options runtime "${app}"
- codesign --verify --deep --strict --verbose=4 "${app}"
-
- - name: Package macOS app
- if: runner.os == 'macOS'
- shell: bash
- run: |
- tag="${GITHUB_REF_NAME:-manual}"
- mkdir -p dist
- ditto -c -k --keepParent "src-tauri/target/release/bundle/macos/iuna.app" "dist/iuna-${tag}-${{ matrix.platform }}.app.zip"
-
- - name: Package Windows installer
- if: runner.os == 'Windows'
- shell: pwsh
- run: |
- $tag = if ($env:GITHUB_REF_NAME) { $env:GITHUB_REF_NAME } else { "manual" }
- New-Item -ItemType Directory -Force -Path "dist" | Out-Null
- $installer = Get-ChildItem "src-tauri/target/release/bundle/nsis" -Filter "*.exe" | Select-Object -First 1
- Copy-Item $installer.FullName "dist/iuna-$tag-${{ matrix.platform }}-setup.exe"
-
- - name: Upload desktop artifact
- uses: actions/upload-artifact@v7
- with:
- name: iuna-${{ github.ref_name }}-${{ matrix.platform }}
- path: dist/iuna-${{ github.ref_name }}-${{ matrix.platform }}*
- if-no-files-found: error
-
- release:
- name: Publish GitHub Release
- needs:
- - build
- - desktop
- runs-on: ubuntu-latest
- if: startsWith(github.ref, 'refs/tags/')
- steps:
- - name: Download artifacts
- uses: actions/download-artifact@v8
- with:
- path: dist
- merge-multiple: true
-
- - name: Write checksums
- run: |
- cd dist
- sha256sum * > SHA256SUMS
-
- - name: Create or update release
- env:
- GH_TOKEN: ${{ github.token }}
- GH_REPO: ${{ github.repository }}
- run: |
- if gh release view "${GITHUB_REF_NAME}" >/dev/null 2>&1; then
- gh release upload "${GITHUB_REF_NAME}" dist/* --clobber
- else
- gh release create "${GITHUB_REF_NAME}" dist/* --title "${GITHUB_REF_NAME}" --generate-notes
- fi
diff --git a/.gitignore b/.gitignore
@@ -3,3 +3,5 @@
/src-tauri/target
/src-tauri/gen
/src-tauri/binaries/iuna-sidecar-*
+/downloads/*
+!/downloads/.gitkeep
diff --git a/Dockerfile b/Dockerfile
@@ -0,0 +1,71 @@
+# syntax=docker/dockerfile:1
+
+########################################################################
+# 1. Build stagit - static git page generator (log/commits/files/refs).
+# Low-resource on purpose: once generated, serving is plain static
+# files, no git process or CGI running at request time.
+########################################################################
+FROM debian:bookworm-slim AS stagit-builder
+
+RUN apt-get update && apt-get install -y --no-install-recommends \
+ build-essential \
+ pkg-config \
+ libgit2-dev \
+ git \
+ ca-certificates \
+ && rm -rf /var/lib/apt/lists/*
+
+RUN git clone --depth 1 https://github.com/oxalorg/stagit.git /usr/src/stagit
+WORKDIR /usr/src/stagit
+RUN make PREFIX=/usr/local && make PREFIX=/usr/local install
+
+########################################################################
+# 2. Generate the site: HTML browser pages + a clonable bare repo.
+#
+# The build context must include .git. The .dockerignore in this repo
+# keeps it available so stagit can publish history and refs.
+########################################################################
+FROM stagit-builder AS site-builder
+
+COPY . /src/iuna-work
+
+RUN set -eux; \
+ test -d /src/iuna-work/.git; \
+ git clone --bare /src/iuna-work /src/iuna.git; \
+ mkdir -p /site/git/iuna /var/cache/stagit-iuna; \
+ echo "iuna - experimental devnet protocol" > /src/iuna.git/description; \
+ echo "iuna-labs" > /src/iuna.git/owner; \
+ echo "https://iuna.jhx.app/git/iuna.git" > /src/iuna.git/url; \
+ cd /src/iuna.git; \
+ git update-server-info; \
+ cd /site/git/iuna; \
+ stagit -c /var/cache/stagit-iuna/cache /src/iuna.git; \
+ test -f /site/git/iuna/log.html; \
+ cp /site/git/iuna/log.html /site/git/iuna/index.html; \
+ cd /site/git && stagit-index /src/iuna.git > index.html; \
+ cp /src/iuna-work/www/assets/static-listing.css /site/git/style.css; \
+ cp /src/iuna-work/www/assets/static-listing.css /site/git/iuna/style.css; \
+ cp /src/iuna-work/src-tauri/icons/32x32.png /site/git/logo.png; \
+ cp /src/iuna-work/src-tauri/icons/32x32.png /site/git/iuna/logo.png; \
+ cp -a /src/iuna.git /site/git/iuna.git; \
+ find /site/git -type f -name '*.html' -exec sed -i -E 's|<a href="(\.\./)+"><img |<a href="/"><img |g' {} +
+
+COPY www /site
+COPY downloads /site/downloads
+RUN set -eux; \
+ version="$(sed -n 's/^version = "\(.*\)"/\1/p' /src/iuna-work/Cargo.toml | head -n 1)"; \
+ mkdir -p /site/downloads; \
+ cp /site/downloads.html /site/downloads/index.html; \
+ sed -i "s|\${IUNA_VERSION}|${version}|g" /site/downloads/index.html; \
+ printf '{"tag":"v%s","version":"%s","url":"https://iuna.jhx.app/downloads/"}\n' "$version" "$version" > /site/downloads/latest.json; \
+ rm -f /site/downloads.html
+
+########################################################################
+# 3. Ship it - plain nginx, static files only.
+########################################################################
+FROM nginx:1.27-alpine
+
+COPY --from=site-builder /site /usr/share/nginx/html
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+
+EXPOSE 80
diff --git a/Dockerfile.node b/Dockerfile.node
@@ -0,0 +1,25 @@
+# syntax=docker/dockerfile:1
+
+FROM rust:1.86-bookworm AS builder
+
+WORKDIR /src/iuna
+COPY . .
+RUN cargo build --release --locked
+
+FROM debian:bookworm-slim
+
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends ca-certificates \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+COPY --from=builder /src/iuna/target/release/iuna /usr/local/bin/iuna
+COPY README.md /usr/share/doc/iuna/README.md
+COPY LICENSE /usr/share/doc/iuna/LICENSE
+
+WORKDIR /data
+
+EXPOSE 18661 9444
+
+ENTRYPOINT ["iuna"]
+CMD ["--data-dir", "/data", "--http", "0.0.0.0:18661", "--p2p", "0.0.0.0:9444", "--p2p-announce", "iuna.jhx.app:9444"]
diff --git a/README.md b/README.md
@@ -1,10 +1,5 @@
# iuna
-[](https://discord.gg/JcXRSSDhS)
-[](https://github.com/iuna-labs/iuna/blob/main/docs/protocol.md)
-[](https://iuna-labs.github.io/iuna/)
-[](https://github.com/iuna-labs/iuna/actions/workflows/release.yml)
-
iuna is an experimental cryptocurrency devnet.
It combines three ideas:
@@ -34,8 +29,8 @@ This is still an experiment. The design needs real-world testing before those go
The simplest way to run iuna is:
-1. Go to [GitHub Releases](https://github.com/iuna-labs/iuna/releases).
-2. Download the latest build for your platform.
+1. Go to [iuna.jhx.app/downloads/](https://iuna.jhx.app/downloads/).
+2. Download the latest available build.
3. Start the app or binary.
4. Follow the setup screen.
@@ -43,6 +38,48 @@ The setup flow helps you create or import a wallet, back up your recovery phrase
You do not need Rust or Cargo unless you want to work on the code.
+## Source
+
+The public source browser is published at [iuna.jhx.app/git/iuna/](https://iuna.jhx.app/git/iuna/).
+
+Clone the static HTTP repo with:
+
+```sh
+git clone https://iuna.jhx.app/git/iuna.git
+```
+
+## Static Site Image
+
+The Docker image publishes the website, a static git browser, a clonable HTTP repo, and release downloads.
+
+```sh
+docker build -t iuna-static-site:test .
+docker run --rm -p 8080:80 iuna-static-site:test
+```
+
+The Linux CLI archives are built inside the image for x86_64 and aarch64. Prebuilt desktop artifacts must be added before the image build:
+
+- `downloads/iuna-v0.2.47-macos-aarch64-desktop.app.zip`
+- `downloads/iuna-v0.2.47-windows-x86_64-desktop-setup.exe`
+
+Release and deploy with:
+
+```sh
+./deployment.sh 0.2.48
+```
+
+Deployment publishes two images to the `jhx-app` k3s cluster:
+
+- `https://iuna.jhx.app/` routes to the static website image.
+- `iuna.jhx.app:18661` routes to the node management UI.
+- `iuna.jhx.app:9444` routes to the node P2P listener.
+
+Useful overrides:
+
+```sh
+IUNA_DEPLOY_HOST=root@jhx.app IUNA_KUBECTL_CONTEXT=jhx-app ./deployment.sh 0.2.48
+```
+
## What You Can Run
You can use iuna as a wallet, a node, or a public peer.
diff --git a/config/deployment.yml b/config/deployment.yml
@@ -0,0 +1,129 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+ name: iuna
+---
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: local-path-db-pvc
+ namespace: iuna
+spec:
+ accessModes:
+ - ReadWriteOnce
+ storageClassName: local-path
+ resources:
+ requests:
+ storage: 1Gi
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: www
+ namespace: iuna
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: iuna-www
+ template:
+ metadata:
+ labels:
+ app: iuna-www
+ spec:
+ containers:
+ - name: iuna-www
+ image: ${IUNA_WWW_IMAGE}
+ imagePullPolicy: IfNotPresent
+ ports:
+ - name: http
+ containerPort: 80
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: iuna-www
+ namespace: iuna
+spec:
+ selector:
+ app: iuna-www
+ ports:
+ - name: http
+ port: 80
+ targetPort: 80
+---
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: iuna-tls-ingress
+ namespace: iuna
+ annotations:
+ cert-manager.io/cluster-issuer: letsencrypt-prod
+ traefik.ingress.kubernetes.io/router.middlewares: default-redirect-https@kubernetescrd
+spec:
+ ingressClassName: traefik
+ rules:
+ - host: iuna.jhx.app
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: iuna-www
+ port:
+ number: 80
+ tls:
+ - secretName: iuna-tls
+ hosts:
+ - iuna.jhx.app
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: node
+ namespace: iuna
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app: iuna-node
+ template:
+ metadata:
+ labels:
+ app: iuna-node
+ spec:
+ containers:
+ - name: iuna-node
+ image: ${IUNA_NODE_IMAGE}
+ imagePullPolicy: IfNotPresent
+ ports:
+ - name: management
+ containerPort: 18661
+ - name: p2p
+ containerPort: 9444
+ volumeMounts:
+ - name: data
+ mountPath: /data
+ volumes:
+ - name: data
+ persistentVolumeClaim:
+ claimName: local-path-db-pvc
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: iuna
+ namespace: iuna
+spec:
+ type: LoadBalancer
+ externalTrafficPolicy: Local
+ selector:
+ app: iuna-node
+ ports:
+ - name: management
+ port: 18661
+ targetPort: 18661
+ - name: p2p
+ port: 9444
+ targetPort: 9444
diff --git a/deployment.sh b/deployment.sh
@@ -0,0 +1,275 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+usage() {
+ echo "Usage: $0 <version>" >&2
+ echo "Example: $0 0.2.48" >&2
+}
+
+die() {
+ echo "error: $*" >&2
+ exit 1
+}
+
+replace_in_file() {
+ local file="$1"
+ local pattern="$2"
+ local replacement="$3"
+ perl -0pi -e "s/${pattern}/${replacement}/g" "$file"
+}
+
+ensure_clean_worktree() {
+ if ! git diff --quiet || ! git diff --cached --quiet || [ -n "$(git ls-files --others --exclude-standard)" ]; then
+ die "worktree is not clean; commit or stash changes before releasing"
+ fi
+}
+
+ensure_tauri_cli() {
+ if ! cargo tauri --version >/dev/null 2>&1; then
+ cargo install tauri-cli --locked --version "^2"
+ fi
+}
+
+update_versions() {
+ local version="$1"
+
+ replace_in_file Cargo.toml '(\[package\]\nname = "iuna"\nversion = ")[^"]+' "\${1}${version}"
+ replace_in_file src-tauri/Cargo.toml '(\[package\]\nname = "iuna-desktop"\nversion = ")[^"]+' "\${1}${version}"
+ replace_in_file src-tauri/tauri.conf.json '("version": ")[^"]+' "\${1}${version}"
+ replace_in_file README.md 'downloads/iuna-v[0-9]+\.[0-9]+\.[0-9]+-macos-aarch64-desktop\.app\.zip' "downloads/iuna-v${version}-macos-aarch64-desktop.app.zip"
+ replace_in_file README.md 'downloads/iuna-v[0-9]+\.[0-9]+\.[0-9]+-windows-x86_64-desktop-setup\.exe' "downloads/iuna-v${version}-windows-x86_64-desktop-setup.exe"
+
+ cargo update -p iuna --precise "$version"
+ cargo update --manifest-path src-tauri/Cargo.toml -p iuna-desktop --precise "$version"
+ cargo check --locked >/dev/null
+ cargo check --locked --manifest-path src-tauri/Cargo.toml >/dev/null
+}
+
+commit_and_tag() {
+ local version="$1"
+ local tag="v${version}"
+
+ git add Cargo.toml Cargo.lock src-tauri/Cargo.toml src-tauri/Cargo.lock src-tauri/tauri.conf.json README.md
+ git commit -m "Release ${tag}"
+ git tag -a "$tag" -m "Release ${tag}"
+}
+
+build_macos_desktop_if_possible() {
+ local version="$1"
+ local artifact="downloads/iuna-v${version}-macos-aarch64-desktop.app.zip"
+
+ [ -f "$artifact" ] && return 0
+ [ "$(uname -s)" = "Darwin" ] || return 0
+ [ "$(uname -m)" = "arm64" ] || die "macOS desktop artifact requires Apple silicon; expected ${artifact}"
+
+ ensure_tauri_cli
+ cargo build --release --locked
+ mkdir -p src-tauri/binaries downloads
+ cp target/release/iuna src-tauri/binaries/iuna-sidecar-aarch64-apple-darwin
+ chmod +x src-tauri/binaries/iuna-sidecar-aarch64-apple-darwin
+ (cd src-tauri && cargo tauri build --bundles app)
+
+ local app="src-tauri/target/release/bundle/macos/iuna.app"
+ codesign --force --deep --sign - --options runtime "$app"
+ codesign --verify --deep --strict --verbose=4 "$app"
+ ditto -c -k --keepParent "$app" "$artifact"
+}
+
+build_windows_desktop_if_possible() {
+ local version="$1"
+ local artifact="downloads/iuna-v${version}-windows-x86_64-desktop-setup.exe"
+
+ [ -f "$artifact" ] && return 0
+ case "$(uname -s)" in
+ MINGW*|MSYS*|CYGWIN*) ;;
+ *) return 0 ;;
+ esac
+
+ ensure_tauri_cli
+ cargo build --release --locked
+ mkdir -p src-tauri/binaries downloads
+ cp target/release/iuna.exe src-tauri/binaries/iuna-sidecar-x86_64-pc-windows-msvc.exe
+ (cd src-tauri && cargo tauri build --bundles nsis)
+
+ local installer
+ installer="$(find src-tauri/target/release/bundle/nsis -maxdepth 1 -type f -name '*.exe' | head -n 1)"
+ [ -n "$installer" ] || die "Windows installer was not produced"
+ cp "$installer" "$artifact"
+}
+
+require_desktop_artifacts() {
+ local version="$1"
+ local macos_artifact="downloads/iuna-v${version}-macos-aarch64-desktop.app.zip"
+ local windows_artifact="downloads/iuna-v${version}-windows-x86_64-desktop-setup.exe"
+
+ [ -f "$macos_artifact" ] || die "missing ${macos_artifact}"
+ [ -f "$windows_artifact" ] || die "missing ${windows_artifact}"
+}
+
+build_linux_cli_archives() {
+ local version="$1"
+ local tag="v${version}"
+ local linux_x86_64_package="iuna-${tag}-linux-x86_64"
+ local linux_aarch64_package="iuna-${tag}-linux-aarch64"
+
+ mkdir -p downloads
+ [ -f "downloads/${linux_x86_64_package}.tar.gz" ] && [ -f "downloads/${linux_aarch64_package}.tar.gz" ] && return 0
+
+ docker run --rm --platform=linux/amd64 \
+ -e "IUNA_VERSION=${version}" \
+ -e "HOST_UID=$(id -u)" \
+ -e "HOST_GID=$(id -g)" \
+ -v "$(pwd):/src/iuna:ro" \
+ -v "$(pwd)/downloads:/out" \
+ rust:1.86-bookworm \
+ bash -lc '
+ set -euo pipefail
+
+ apt-get update
+ apt-get install -y --no-install-recommends gcc-aarch64-linux-gnu libc6-dev-arm64-cross
+ rm -rf /var/lib/apt/lists/*
+ rustup target add aarch64-unknown-linux-gnu
+
+ mkdir -p /work/iuna
+ tar -C /src/iuna \
+ --exclude=./target \
+ --exclude=./src-tauri/target \
+ --exclude=./src-tauri/binaries \
+ --exclude=./.agents \
+ --exclude=./.codex \
+ -cf - . | tar -C /work/iuna -xf -
+
+ cd /work/iuna
+ CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc \
+ AR_aarch64_unknown_linux_gnu=aarch64-linux-gnu-ar \
+ CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc \
+ cargo build --release --locked --target aarch64-unknown-linux-gnu
+ cargo build --release --locked
+
+ tag="v${IUNA_VERSION}"
+ linux_x86_64_package="iuna-${tag}-linux-x86_64"
+ linux_aarch64_package="iuna-${tag}-linux-aarch64"
+ mkdir -p "/tmp/site/${linux_x86_64_package}" "/tmp/site/${linux_aarch64_package}"
+ cp target/release/iuna "/tmp/site/${linux_x86_64_package}/"
+ cp target/aarch64-unknown-linux-gnu/release/iuna "/tmp/site/${linux_aarch64_package}/"
+ cp README.md LICENSE "/tmp/site/${linux_x86_64_package}/"
+ cp README.md LICENSE "/tmp/site/${linux_aarch64_package}/"
+ tar -C /tmp/site -czf "/out/${linux_x86_64_package}.tar.gz" "${linux_x86_64_package}"
+ tar -C /tmp/site -czf "/out/${linux_aarch64_package}.tar.gz" "${linux_aarch64_package}"
+ chown "${HOST_UID}:${HOST_GID}" "/out/${linux_x86_64_package}.tar.gz" "/out/${linux_aarch64_package}.tar.gz"
+ '
+}
+
+write_download_checksums() {
+ (
+ cd downloads
+ rm -f SHA256SUMS
+
+ local files=()
+ local file
+ for file in *; do
+ [ -f "$file" ] || continue
+ case "$file" in
+ .gitkeep|index.html|SHA256SUMS) continue ;;
+ esac
+ files+=("$file")
+ done
+
+ [ "${#files[@]}" -gt 0 ] || return 0
+ if command -v sha256sum >/dev/null 2>&1; then
+ sha256sum "${files[@]}" > SHA256SUMS
+ else
+ for file in "${files[@]}"; do
+ shasum -a 256 "$file" | awk "{print \$1 \" \" \$2}"
+ done > SHA256SUMS
+ fi
+ )
+}
+
+build_versions() {
+ local version="$1"
+
+ mkdir -p downloads
+ build_linux_cli_archives "$version"
+ build_macos_desktop_if_possible "$version"
+ build_windows_desktop_if_possible "$version"
+ require_desktop_artifacts "$version"
+ write_download_checksums
+}
+
+build_docker_image() {
+ local version="$1"
+ local www_image="${IUNA_WWW_IMAGE:-iuna-www:v${version}}"
+ local node_image="${IUNA_NODE_IMAGE:-iuna-node:v${version}}"
+
+ docker build --platform=linux/amd64 --progress=plain -t "$www_image" .
+ docker build --platform=linux/amd64 --progress=plain -t "$node_image" -f Dockerfile.node .
+ echo "Built Docker images: ${www_image}, ${node_image}"
+}
+
+import_image_to_k3s() {
+ local image="$1"
+ local tmp_folder="$2"
+ local remote_host="${IUNA_DEPLOY_HOST:-root@jhx.app}"
+ local remote_file="${image//[:\/]/_}.tar"
+ local image_file="${tmp_folder}/${remote_file}"
+
+ docker save "$image" -o "$image_file"
+ scp "$image_file" "${remote_host}:~/"
+ ssh "$remote_host" "sudo k3s ctr -n k8s.io images import ~/${remote_file} && rm ~/${remote_file}"
+}
+
+render_manifest() {
+ local www_image="$1"
+ local node_image="$2"
+ local output="$3"
+
+ sed \
+ -e "s|\${IUNA_WWW_IMAGE}|${www_image}|g" \
+ -e "s|\${IUNA_NODE_IMAGE}|${node_image}|g" \
+ config/deployment.yml > "$output"
+}
+
+deploy_docker_image() {
+ local version="$1"
+ local www_image="${IUNA_WWW_IMAGE:-iuna-www:v${version}}"
+ local node_image="${IUNA_NODE_IMAGE:-iuna-node:v${version}}"
+ local kubectl_context="${IUNA_KUBECTL_CONTEXT:-jhx-app}"
+ local tmp_folder
+
+ tmp_folder="$(mktemp -d)"
+ trap 'rm -rf "$tmp_folder"' RETURN
+
+ import_image_to_k3s "$www_image" "$tmp_folder"
+ import_image_to_k3s "$node_image" "$tmp_folder"
+ render_manifest "$www_image" "$node_image" "${tmp_folder}/deployment.yml"
+
+ local current_www_selector
+ current_www_selector="$(kubectl --context "$kubectl_context" -n iuna get deployment www -o jsonpath='{.spec.selector.matchLabels.app}' 2>/dev/null || true)"
+ if [ -n "$current_www_selector" ] && [ "$current_www_selector" != "iuna-www" ]; then
+ kubectl --context "$kubectl_context" -n iuna delete deployment www --wait=true
+ fi
+
+ kubectl --context "$kubectl_context" apply -f "${tmp_folder}/deployment.yml"
+ kubectl --context "$kubectl_context" -n iuna rollout restart deployment/www deployment/node
+ kubectl --context "$kubectl_context" -n iuna rollout status deployment/www
+ kubectl --context "$kubectl_context" -n iuna rollout status deployment/node
+}
+
+main() {
+ [ "$#" -eq 1 ] || { usage; exit 2; }
+
+ local version="${1#v}"
+ [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || die "version must look like 0.2.48"
+
+ ensure_clean_worktree
+ git rev-parse --verify "v${version}" >/dev/null 2>&1 && die "tag v${version} already exists"
+
+ update_versions "$version"
+ commit_and_tag "$version"
+ build_versions "$version"
+ build_docker_image "$version"
+ deploy_docker_image "$version"
+}
+
+main "$@"
diff --git a/devlogs/001-node-first.md b/devlogs/001-node-first.md
@@ -1,17 +0,0 @@
-# Devlog 001: Node First
-
-iuna starts from a running node first, then lets the explanation grow around the code.
-
-The first version is a single binary: wallet, node, miner, HTTP management UI, and P2P listener all in one place. It is not trying to survive hostile internet conditions yet. It is trying to make the iuna feel alive as quickly as possible.
-
-The important design choice is the hexagonal split. The iuna rules live in the domain layer. The TCP server and HTTP UI sit outside that. Because of that, tests can run a little iuna network entirely in memory, without ports, sleeps, containers, or a pretend deployment.
-
-The consensus sketch is intentionally small:
-
-- burn IUNA into a block,
-- turn those burns into mature one-shot tickets for a short future block window,
-- use parent-bound VDF work as the pacing signal,
-- give the selected ticket owner the signed right to mine the next block,
-- forget the stake because the IUNA were already burned.
-
-That gives us something real to poke at now, while leaving plenty of room to make the cryptography and networking less toy-like later.
diff --git a/devlogs/002-vdf-clock.md b/devlogs/002-vdf-clock.md
@@ -1,13 +0,0 @@
-# Devlog 002: The VDF Is The Clock
-
-The first UI had a "mine next block" button. That was useful for proving the ledger worked, but it was the wrong feeling for iuna.
-
-Now the node runs by itself. Each wallet has a fixed burn amount. If that amount is above zero, once per chain height the node creates a burn transaction for that amount. Those burns become lottery tickets after a short maturity delay, then stay eligible for a small future block window.
-
-The important correction is that there is no exact timer like "sleep 10 minutes, then make a block." The selected leader makes the block content and then does the VDF work. When the VDF is finished, the block is gossiped. That means the VDF is the clock.
-
-The code also had to move the VDF outside the main node lock. If the VDF is supposed to be the thing that takes real time, the UI should not freeze just because the local node is hashing. So the node prepares the block content, runs the VDF separately, and then comes back to apply and gossip the block if it still fits the local chain.
-
-The management page is also starting to feel less like a toy console and more like a tiny node dashboard. It shows the current leader, the fixed block reward, the burn setting, recent blocks, and what peers the node knows about.
-
-Still friendly-node land. Still deliberately simple. But the rhythm is closer to the actual iuna idea now.
diff --git a/devlogs/003-friend-join.md b/devlogs/003-friend-join.md
@@ -1,13 +0,0 @@
-# Devlog 003: Friends Join The Chain
-
-The first thought was a shared genesis file. That is fine for a lab, but it is not the friend-net experience I want.
-
-The better flow is: I start a chain, you point your node at mine, and your node joins what I already started.
-
-So the P2P port now does one extra friendly thing. When a node connects, the peer sends a chain snapshot: genesis allocations, VDF rounds, and the blocks it has. A joining node imports that snapshot before it starts mining. If it cannot get the snapshot, it refuses to start a separate chain.
-
-The default burn is now zero. That matters because a friend who just joined probably has no IUNA yet. They can still follow the chain, receive IUNA, and only then decide how much to burn per block.
-
-Genesis changed too. The starter does not begin rich anymore. The starter gets 1 synthetic iuna in genesis and burns it immediately, so their visible balance is 0, but the chain has bootstrap lottery tickets. Those tickets let the starter produce the first real reward blocks while normal burn tickets mature.
-
-This is still not real adversarial sync. It trusts the friend you join. But for the current iuna phase, that is exactly the point: make a small network feel real first, then harden it later.
diff --git a/devlogs/004-wallet-file.md b/devlogs/004-wallet-file.md
@@ -1,9 +0,0 @@
-# Devlog 004: Wallet File
-
-The node no longer has a baked-in dev wallet seed.
-
-On first real startup, `--start` or `--join`, iuna creates a wallet file and reuses it next time. The default is `~/.iuna/wallet.json`, or `<data-dir>/wallet.json` when a node uses its own data directory.
-
-That matters for friend testing. You can restart your node and keep the same address, but friends do not need to pass a seed just to be someone else. They join your chain, get their own fresh local wallet, and start with 0 IUNA until you send them some.
-
-This is still prototype-wallet simple: the file contains the seed, so it should be treated like a private key.
diff --git a/devlogs/005-burned-blocks-and-vdf.md b/devlogs/005-burned-blocks-and-vdf.md
@@ -1,9 +0,0 @@
-# Devlog 005: Burned Blocks And VDF
-
-I tightened the rule that felt wrong during local testing: a normal block cannot be empty of burns anymore.
-
-That means a block has to carry at least one positive burn transaction. Otherwise it would create a future stretch with no rolling-window lottery tickets, which is basically a protocol pothole.
-
-The VDF also now runs over the candidate block content hash instead of just the previous hash. So if the leader changes the timestamp, miner, reward, rounds, previous hash, or transactions after doing the VDF, peers reject it.
-
-One practical consequence: the default genesis still leaves the starter wallet at 0, so it creates the chain but waits. For a moving local demo, start with one extra genesis iuna and burn it into block 1.
diff --git a/devlogs/006-dynamic-vdf-target.md b/devlogs/006-dynamic-vdf-target.md
@@ -1,9 +0,0 @@
-# Devlog 006: Let The Chain Aim For Ten Minutes
-
-The sync problem was tempting to solve in the wrong place. We could make peers trust each other more, but that weakens the protocol exactly where it should be strongest.
-
-So this change keeps VDF validation as consensus, but makes the VDF round count dynamic. Blocks still carry the exact round count they used. Nodes validate that it is the round count the chain expected for that height.
-
-After each block, the chain looks at a rolling average of recent block times and nudges the next round count toward a 10 minute target. The nudge is small, about 10% per block, so one weird timestamp cannot throw the chain completely off.
-
-This gives gossip and catch-up more breathing room while keeping the rule deterministic: every node can derive the same next VDF rounds from the chain it has validated.
diff --git a/devlogs/007-gossip-grows-up.md b/devlogs/007-gossip-grows-up.md
@@ -1,9 +0,0 @@
-# Devlog 007: Gossip Grows Up A Bit
-
-The first gossip protocol was basically "push whatever just happened, and if someone is behind, throw a full snapshot at them." That worked for tiny chains, but it was too blunt.
-
-Now peers announce both height and tip hash when a connection opens. If a node sees that a peer is behind, it sends a batch of missing blocks instead of a whole chain snapshot. The receiver still validates the blocks, including the VDF output, before importing them.
-
-Snapshots are still useful for initial join and fallback, but normal catch-up now has a more blockchain-shaped path: ask for the missing range, validate it, apply it.
-
-The UI also shows the last height and tip hash reported by each peer, which makes it much easier to see whether gossip is actually moving or just quietly stuck.
diff --git a/devlogs/008-persistent-peer-sessions.md b/devlogs/008-persistent-peer-sessions.md
@@ -1,9 +0,0 @@
-# Devlog 008: Persistent Peer Sessions
-
-The old P2P layer opened a fresh TCP connection for almost every little thing: send a burn, send a block, ask for status, ask for missing blocks. It was easy to write, but it made the logs noisy and the network feel twitchy. Lots of "connection reset by peer" messages were basically the sound of short-lived sockets closing at awkward moments.
-
-The new layer keeps one outbound session per known peer. Each peer gets a bounded queue, a reconnect loop with backoff, and a simple line-based message stream. Status messages keep flowing over the same connection, and if a peer reports that it is ahead, the node asks for the missing block range on that same session.
-
-This is still intentionally small. It is not trying to be libp2p. But it is much closer to how the iuna should behave: peers stay connected, gossip is queued instead of redialed, quiet disconnects are treated as normal, and catch-up is driven by the protocol instead of a separate polling fetch path.
-
-The important part for testing is that the node core did not become network-shaped. The session layer is still an adapter around the same `GossipEnvelope` messages, so the fast deterministic tests can keep exercising the protocol without real sockets.
diff --git a/devlogs/009-longer-fork-reorgs.md b/devlogs/009-longer-fork-reorgs.md
@@ -1,9 +0,0 @@
-# Devlog 009: Longer Fork Reorgs
-
-Until now, iuna mostly behaved like there was only one possible chain. If a snapshot disagreed with a block we already had, the node rejected it. That is nice and simple, but it is not how a real network behaves. Two friendly nodes can still mine competing blocks if messages arrive in a weird order.
-
-The new rule is intentionally small: a remote chain can replace the local chain only if it has the same genesis, fully validates, shares a common ancestor, and is strictly longer. Same-height forks do not cause flip-flopping. The node waits until one side grows longer.
-
-When a reorg happens, local pending transactions are not thrown away. Transactions from abandoned local blocks are also put back through the mempool rules, so useful burns/transfers get another chance on the new tip if they are still valid.
-
-This is not final chain-selection science yet. There is no cumulative-work score beyond height. But it is a real fork recovery path, and it gives the gossip layer something sane to do when peers briefly disagree.
diff --git a/devlogs/010-hello-and-inventory.md b/devlogs/010-hello-and-inventory.md
@@ -1,16 +0,0 @@
-# Devlog 010: Hello, Inventory
-
-The P2P protocol now starts with a real `Hello`. A node tells the peer its protocol version, network id, genesis hash, listen address, height, and tip hash. If the protocol, network, or genesis does not match, the session is rejected early.
-
-That matters because "it connected" is not enough for a iuna. A node on a different genesis should not be able to quietly trade blocks with us and create weird local errors later.
-
-Gossip also changed. Instead of pushing full transactions and blocks every time, nodes announce inventory: transaction signatures and block hashes. Peers then request only the objects they do not have yet.
-
-So the flow is now more like:
-
-1. I have tx/block ids.
-2. You tell me which ones you need.
-3. I send the full objects.
-4. You validate before importing.
-
-It is still simple, but it is now much closer to a real P2P shape. Less duplicate payload spam, better validation boundary, and a cleaner place to add peer scoring/rate limits later.
diff --git a/devlogs/011-fast-vdf-verification.md b/devlogs/011-fast-vdf-verification.md
@@ -1,9 +0,0 @@
-# Fast VDF verification
-
-The nodes were still drifting because followers had to re-run the whole VDF for every block they imported.
-
-That was the wrong shape. The miner should spend the delay time, but peers should be able to verify the result quickly. Otherwise a node that is one block behind has to do the same work as the miner just to catch up, and if it misses a few blocks it is basically doomed to trail behind.
-
-This pass changes the block VDF output into a small `output:proof` receipt. Mining still does sequential work, but import checks the proof quickly. Combined with inventory/request and active catchup, peers should now catch up in seconds instead of one VDF at a time.
-
-This is still devnet-level crypto, not a final mainnet VDF construction, but the architecture is now pointed in the right direction: slow produce, fast verify.
diff --git a/devlogs/012-consensus-vocabulary.md b/devlogs/012-consensus-vocabulary.md
@@ -1,13 +0,0 @@
-# Consensus vocabulary
-
-Fork choice was working, but the code still read like loose booleans and hash comparisons.
-
-This pass gives the domain language names: `ForkPoint`, `LeaderScore`, `ForkQuality`, and `ForkChoice`. The behavior stays the same, but the code now says what it means:
-
-- find the common ancestor
-- reject finalized history rewrites
-- compare leader quality inside the reorg window
-- decide whether to keep local or switch
-- carry abandoned local transactions back into the mempool
-
-For now `LeaderScore` is still derived from the block hash. That is a devnet stand-in for an explicit VRF proof/score, but at least the concept now has a home in the domain model.
diff --git a/devlogs/013-chain-persistence.md b/devlogs/013-chain-persistence.md
@@ -1,9 +0,0 @@
-# 013 - Chain Persistence
-
-Until now the chain lived in memory. That made tests nice, but restarts were too fragile: a node could keep its wallet and still forget what chain it was on.
-
-The new piece is a SQLite adapter that stores the latest validated chain snapshot in the node's data directory. On startup, if that database exists, the node loads it before looking at `--start` or `--join`. So a restart keeps following the same chain instead of creating a fresh genesis or needing the bootstrap peer to be online at exactly that moment.
-
-This is still intentionally small. It saves one current snapshot, not a fully indexed block database. But it sits in the adapter layer, away from the ledger rules, and it is tested separately. That gives us the boring restart behavior now while leaving room to grow it into a richer block store later.
-
-Persistence runs in the background and only saves when the tip changes. The web UI, gossip loop, and miner should not care that SQLite exists.
diff --git a/downloads/.gitkeep b/downloads/.gitkeep
@@ -0,0 +1 @@
+
diff --git a/nginx.conf b/nginx.conf
@@ -0,0 +1,26 @@
+server {
+ listen 80;
+ server_name _;
+
+ root /usr/share/nginx/html;
+ index index.html;
+
+ location / {
+ try_files $uri $uri/ =404;
+ }
+
+ location = /downloads/latest.json {
+ add_header Access-Control-Allow-Origin "*" always;
+ add_header Cache-Control "no-cache" always;
+ try_files $uri =404;
+ }
+
+ location /downloads/ {
+ autoindex on;
+ try_files $uri $uri/ =404;
+ }
+
+ location /git/ {
+ try_files $uri $uri/ =404;
+ }
+}
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
@@ -20,7 +20,7 @@
}
],
"security": {
- "csp": "default-src 'self'; connect-src http://127.0.0.1:18661 https://api.github.com; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
+ "csp": "default-src 'self'; connect-src http://127.0.0.1:18661; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'"
}
},
"bundle": {
diff --git a/src/adapters/http/index_html.rs b/src/adapters/http/index_html.rs
@@ -439,7 +439,7 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html>
.block-card { flex-basis: 108px; }
}
</style>
- <script defer src="/assets/iuna-ui.js?v=101"></script>
+ <script defer src="/assets/iuna-ui.js?v=102"></script>
<script defer src="/assets/alpine.min.js"></script>
</head>
<body x-data="iunaApp()" x-init="init()" @keydown.window.escape="closeModals()" x-cloak>
@@ -1448,7 +1448,7 @@ pub(super) const INDEX_HTML: &str = r#"<!doctype html>
<div class="setup-section setup-network">
<div class="panel-head">
<h3>Network</h3>
- <a class="setup-network-link" href="https://github.com/iuna-labs/iuna/blob/main/KNOWN_NODES.txt" target="_blank" rel="noreferrer">Known nodes</a>
+ <a class="setup-network-link" href="https://iuna.jhx.app/git/iuna/file/KNOWN_NODES.txt.html" target="_blank" rel="noreferrer">Known nodes</a>
</div>
<div class="setup-network-row">
<label><span x-text="setupRequiresPeer() ? 'Bootstrap peer (required)' : 'Bootstrap peer'"></span><input x-model="setupPeerAddress" placeholder="iuna.jhx.app:9444"></label>
diff --git a/src/adapters/http/tests.rs b/src/adapters/http/tests.rs
@@ -1466,7 +1466,7 @@ fn metrics_response_skips_bootstrap_points_for_block_time_and_vdf_rounds() {
#[test]
fn metrics_screen_includes_block_range_filter() {
- assert!(super::INDEX_HTML.contains("iuna-ui.js?v=101"));
+ assert!(super::INDEX_HTML.contains("iuna-ui.js?v=102"));
assert!(super::INDEX_HTML.contains("aria-label=\"Metrics block range\""));
assert!(super::INDEX_HTML.contains("setMetricsRange(100)"));
assert!(super::INDEX_HTML.contains("setMetricsRange(1000)"));
diff --git a/www/assets/iuna-downloads.js b/www/assets/iuna-downloads.js
@@ -0,0 +1,154 @@
+(function () {
+ var rawVersion = window.IUNA_DOWNLOADS_VERSION || "";
+ var version = rawVersion.replace(/^v/, "");
+ var tag = "v" + version;
+ var base = "/downloads/";
+
+ var artifacts = [
+ {
+ label: "Linux CLI",
+ title: "Linux x86_64",
+ description: "Command-line node and wallet UI server.",
+ file: "iuna-" + tag + "-linux-x86_64.tar.gz",
+ button: "Download tar.gz"
+ },
+ {
+ label: "Linux CLI",
+ title: "Linux aarch64",
+ description: "Command-line node and wallet UI server for ARM64 Linux.",
+ file: "iuna-" + tag + "-linux-aarch64.tar.gz",
+ button: "Download tar.gz"
+ },
+ {
+ label: "macOS desktop",
+ title: "Apple silicon",
+ description: "Desktop app bundle for macOS.",
+ file: "iuna-" + tag + "-macos-aarch64-desktop.app.zip",
+ button: "Download app.zip"
+ },
+ {
+ label: "Windows desktop",
+ title: "Windows x86_64",
+ description: "Desktop installer for Windows.",
+ file: "iuna-" + tag + "-windows-x86_64-desktop-setup.exe",
+ button: "Download setup.exe"
+ },
+ {
+ label: "Checksums",
+ title: "SHA256SUMS",
+ description: "SHA-256 checksums for available files.",
+ file: "SHA256SUMS",
+ button: "Download checksums",
+ hideCard: true
+ }
+ ];
+
+ function escapeHtml(value) {
+ return String(value).replace(/[&<>"']/g, function (character) {
+ return {
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ '"': """,
+ "'": "'"
+ }[character];
+ });
+ }
+
+ function formatBytes(value) {
+ if (!Number.isFinite(value) || value <= 0) {
+ return "unknown";
+ }
+
+ var units = ["B", "KB", "MB", "GB"];
+ var size = value;
+ var unit = 0;
+ while (size >= 1024 && unit < units.length - 1) {
+ size /= 1024;
+ unit += 1;
+ }
+
+ return new Intl.NumberFormat(undefined, {
+ maximumFractionDigits: size >= 10 || unit === 0 ? 0 : 1
+ }).format(size) + " " + units[unit];
+ }
+
+ function checkArtifact(artifact) {
+ return fetch(base + artifact.file, { method: "HEAD", cache: "no-store" })
+ .then(function (response) {
+ if (!response.ok) {
+ return null;
+ }
+
+ return Object.assign({}, artifact, {
+ size: Number(response.headers.get("content-length"))
+ });
+ })
+ .catch(function () {
+ return null;
+ });
+ }
+
+ function renderCards(files) {
+ var container = document.querySelector("[data-download-cards]");
+ if (!container) {
+ return;
+ }
+
+ var cards = files.filter(function (file) {
+ return !file.hideCard;
+ });
+
+ if (cards.length === 0) {
+ container.innerHTML = '<article class="card"><span class="tag pending">Pending</span><h2>No artifacts yet</h2><p class="muted">Release files have not been uploaded for this version.</p></article>';
+ return;
+ }
+
+ container.innerHTML = cards.map(function (file) {
+ return [
+ '<article class="card">',
+ '<span class="tag">' + escapeHtml(file.label) + '</span>',
+ '<h2>' + escapeHtml(file.title) + '</h2>',
+ '<p>' + escapeHtml(file.description) + '</p>',
+ '<p><a class="button" href="' + escapeHtml(base + file.file) + '">' + escapeHtml(file.button) + '</a></p>',
+ '</article>'
+ ].join("");
+ }).join("");
+ }
+
+ function renderFiles(files) {
+ var body = document.querySelector("[data-download-files]");
+ if (!body) {
+ return;
+ }
+
+ if (files.length === 0) {
+ body.innerHTML = '<tr><td colspan="2" class="muted">No files found.</td></tr>';
+ return;
+ }
+
+ body.innerHTML = files.map(function (file) {
+ return '<tr><td><a href="' + escapeHtml(base + file.file) + '">' + escapeHtml(file.file) + '</a></td><td>' + escapeHtml(formatBytes(file.size)) + '</td></tr>';
+ }).join("");
+ }
+
+ function hydrateStaticText() {
+ var versionNodes = document.querySelectorAll("[data-download-version]");
+ var linuxFile = "iuna-" + tag + "-linux-x86_64.tar.gz";
+ var linuxNodes = document.querySelectorAll("[data-linux-x86-file]");
+
+ versionNodes.forEach(function (node) {
+ node.textContent = tag;
+ });
+ linuxNodes.forEach(function (node) {
+ node.textContent = linuxFile;
+ });
+ }
+
+ hydrateStaticText();
+ Promise.all(artifacts.map(checkArtifact)).then(function (results) {
+ var files = results.filter(Boolean);
+ renderCards(files);
+ renderFiles(files);
+ });
+}());
diff --git a/www/assets/iuna-ui.js b/www/assets/iuna-ui.js
@@ -1,3 +1,6 @@
+const IUNA_DOWNLOADS_URL = "https://iuna.jhx.app/downloads/";
+const IUNA_RELEASE_METADATA_URL = "https://iuna.jhx.app/downloads/latest.json";
+
window.iunaApp = function iunaApp() {
return {
tab: "wallet",
@@ -248,7 +251,7 @@ window.iunaApp = function iunaApp() {
},
async openLatestRelease() {
- const url = this.latestRelease?.url || "https://github.com/iuna-labs/iuna/releases";
+ const url = this.latestRelease?.url || IUNA_DOWNLOADS_URL;
try {
const tauriOpen = window.__TAURI__?.shell?.open;
if (typeof tauriOpen === "function") {
@@ -1059,14 +1062,21 @@ window.iunaApp = function iunaApp() {
this.releaseCheckState = "checking";
this.releaseCheckError = null;
try {
- const response = await fetch("https://api.github.com/repos/iuna-labs/iuna/releases/latest", {
- headers: { Accept: "application/vnd.github+json" },
+ const response = await fetch(IUNA_RELEASE_METADATA_URL, {
+ cache: "no-store",
+ headers: { Accept: "application/json" },
});
- if (!response.ok) throw new Error(`release check returned ${response.status}`);
+ if (!response.ok) {
+ throw new Error(`Release check failed (${response.status})`);
+ }
const release = await response.json();
+ const version = this.normalizeVersion(release.tag || release.version);
+ if (!version) {
+ throw new Error("Release metadata is missing a version");
+ }
this.latestRelease = {
- tag: release.tag_name || "",
- url: release.html_url || "https://github.com/iuna-labs/iuna/releases",
+ tag: `v${version}`,
+ url: release.url || IUNA_DOWNLOADS_URL,
};
this.releaseCheckState = "done";
} catch (error) {
diff --git a/www/assets/static-listing.css b/www/assets/static-listing.css
@@ -0,0 +1,290 @@
+:root {
+ color-scheme: dark;
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ background: #0d0f10;
+ color: #edf2f5;
+ --bg: #0d0f10;
+ --panel: #15191b;
+ --panel-2: #101315;
+ --line: #273036;
+ --muted: #98a6ad;
+ --text: #edf2f5;
+ --green: #d5f55f;
+ --cyan: #8de9cd;
+}
+
+* { box-sizing: border-box; }
+
+body {
+ margin: 0 auto;
+ padding: 0 0 56px;
+ background: var(--bg);
+ color: var(--text);
+}
+
+a {
+ color: var(--green);
+ text-decoration-thickness: 1px;
+ text-underline-offset: 3px;
+}
+
+a.line {
+ color: #6f7b82;
+ text-decoration: none;
+}
+
+a.line:hover,
+a.line:target {
+ color: #a8b2b8;
+ background: rgba(255, 255, 255, .04);
+}
+
+img.logo, #logo img {
+ width: 32px;
+ height: 32px;
+ border-radius: 8px;
+ vertical-align: middle;
+ margin-right: 10px;
+}
+
+h1, h2, h3 { letter-spacing: 0; }
+h1 { margin: 0 0 10px; font-size: 42px; line-height: 1; }
+h2 { margin-top: 30px; font-size: 22px; }
+p, td, th, li { color: #b7c3c9; line-height: 1.55; }
+
+.wrap {
+ width: min(1060px, calc(100% - 44px));
+ margin: 0 auto;
+}
+
+body > :not(.topbar):not(script) {
+ width: min(1060px, calc(100% - 44px));
+ margin-left: auto;
+ margin-right: auto;
+}
+
+.topbar {
+ position: sticky;
+ top: 0;
+ z-index: 2;
+ border-bottom: 1px solid rgba(39, 48, 54, .9);
+ background: rgba(13, 15, 16, .82);
+ backdrop-filter: blur(12px);
+}
+
+.topbar .wrap {
+ display: flex;
+ justify-content: space-between;
+ gap: 18px;
+ align-items: center;
+ min-height: 58px;
+}
+
+.brand {
+ display: flex;
+ gap: 10px;
+ align-items: center;
+ color: var(--text);
+ font-weight: 900;
+ text-decoration: none;
+}
+
+.brand:hover { color: var(--green); }
+
+.mark {
+ position: relative;
+ width: 32px;
+ height: 32px;
+ display: grid;
+ place-items: center;
+ overflow: hidden;
+ border: 1px solid #e8ff8d;
+ border-radius: 8px;
+ background: linear-gradient(145deg, #ecff8a 0%, var(--green) 54%, var(--cyan) 100%);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, .38), 0 9px 20px rgba(213, 245, 95, .14);
+}
+
+.mark svg {
+ position: relative;
+ z-index: 1;
+ width: 21px;
+ height: 21px;
+ display: block;
+}
+
+.mark .mark-loop {
+ fill: none;
+ stroke: #101315;
+ stroke-width: 4.2;
+ stroke-linecap: round;
+ stroke-linejoin: round;
+}
+
+.mark .mark-dot { fill: #101315; }
+
+nav {
+ display: flex;
+ gap: 16px;
+ flex-wrap: wrap;
+ color: var(--muted);
+ font-size: 13px;
+ font-weight: 750;
+}
+
+nav a {
+ color: var(--muted);
+ text-decoration: none;
+}
+
+nav a:hover { color: var(--green); }
+
+pre, code {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
+}
+
+pre {
+ overflow-x: auto;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ padding: 14px;
+ background: #0b0d0e;
+}
+
+table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 18px 0 28px;
+ overflow: hidden;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: var(--panel);
+}
+
+th, td {
+ padding: 12px 14px;
+ border-bottom: 1px solid var(--line);
+ text-align: left;
+ vertical-align: top;
+}
+
+th {
+ color: var(--text);
+ font-size: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0;
+ background: var(--panel-2);
+}
+
+tr:last-child td { border-bottom: 0; }
+
+.topnav {
+ display: flex;
+ gap: 14px;
+ flex-wrap: wrap;
+ margin-bottom: 28px;
+ font-size: 14px;
+ font-weight: 800;
+}
+
+#header, #navbar {
+ display: flex;
+ gap: 14px;
+ flex-wrap: wrap;
+ align-items: center;
+ margin-bottom: 18px;
+}
+
+#desc {
+ color: #b7c3c9;
+ font-size: 15px;
+}
+
+#navbar a {
+ font-weight: 800;
+}
+
+.hero {
+ padding: 18px 0 22px;
+ border-bottom: 1px solid var(--line);
+ margin-bottom: 24px;
+}
+
+.grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 12px;
+ margin: 18px 0 28px;
+}
+
+.card {
+ min-width: 0;
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ padding: 16px;
+ background: var(--panel);
+}
+
+.card h2 {
+ margin: 0 0 8px;
+ font-size: 18px;
+}
+
+.tag {
+ display: inline-flex;
+ margin-bottom: 10px;
+ border: 1px solid rgba(213, 245, 95, .42);
+ border-radius: 999px;
+ padding: 3px 8px;
+ color: var(--green);
+ font-size: 11px;
+ font-weight: 850;
+ text-transform: uppercase;
+}
+
+.tag.pending {
+ border-color: #3b464d;
+ color: var(--muted);
+}
+
+.button {
+ display: inline-flex;
+ align-items: center;
+ min-height: 38px;
+ border: 1px solid var(--green);
+ border-radius: 8px;
+ padding: 0 12px;
+ background: var(--green);
+ color: #11140c;
+ font-size: 14px;
+ font-weight: 850;
+ text-decoration: none;
+}
+
+.button.secondary {
+ border-color: var(--line);
+ background: var(--panel-2);
+ color: var(--text);
+}
+
+.muted { color: var(--muted); }
+
+#content, #files, #branches, #tags {
+ border: 1px solid var(--line);
+ border-radius: 8px;
+ background: var(--panel);
+}
+
+hr {
+ height: 1px;
+ border: 0;
+ background: var(--line);
+}
+
+@media (max-width: 780px) {
+ body { padding-bottom: 44px; }
+ .wrap { width: min(100% - 32px, 1060px); }
+ nav { display: none; }
+ h1 { font-size: 34px; }
+ .grid { grid-template-columns: 1fr; }
+ table { display: block; overflow-x: auto; }
+}
diff --git a/www/downloads.html b/www/downloads.html
@@ -0,0 +1,66 @@
+<!doctype html>
+<html lang="en">
+<head>
+ <meta charset="utf-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+ <meta name="color-scheme" content="dark">
+ <title>iuna Downloads</title>
+ <link rel="stylesheet" href="/assets/static-listing.css">
+</head>
+<body>
+ <div class="topbar">
+ <div class="wrap">
+ <a class="brand repo-link" href="/">
+ <span class="mark" aria-label="iuna">
+ <svg viewBox="0 0 32 32" aria-hidden="true" focusable="false">
+ <circle class="mark-dot" cx="9.4" cy="7.6" r="2.8"></circle>
+ <path class="mark-loop" d="M9.4 13v7.1c0 3.7 2.9 6.4 6.6 6.4s6.6-2.7 6.6-6.4V13"></path>
+ </svg>
+ </span>
+ <span>iuna</span>
+ </a>
+ <nav aria-label="Page sections">
+ <a href="/">Home</a>
+ <a href="/git/iuna/file/docs/protocol.md.html">Protocol</a>
+ <a href="/git/iuna/">Source</a>
+ <a href="/downloads/">Downloads</a>
+ </nav>
+ </div>
+ </div>
+
+ <main class="wrap">
+ <section class="hero">
+ <h1>iuna Downloads</h1>
+ <p>Release artifacts for iuna <span data-download-version>${IUNA_VERSION}</span>.</p>
+ </section>
+
+ <div class="grid" data-download-cards>
+ <article class="card">
+ <span class="tag pending">Checking</span>
+ <h2>Downloads</h2>
+ <p class="muted">Looking for release artifacts.</p>
+ </article>
+ </div>
+
+ <h2>Files</h2>
+ <table>
+ <thead>
+ <tr>
+ <th>File</th>
+ <th>Size</th>
+ </tr>
+ </thead>
+ <tbody data-download-files>
+ <tr><td colspan="2" class="muted">Checking files.</td></tr>
+ </tbody>
+ </table>
+
+ <pre><code>curl -fsSLO https://iuna.jhx.app/downloads/<span data-linux-x86-file>iuna-v${IUNA_VERSION}-linux-x86_64.tar.gz</span>
+sha256sum -c SHA256SUMS</code></pre>
+ </main>
+ <script>
+ window.IUNA_DOWNLOADS_VERSION = "${IUNA_VERSION}";
+ </script>
+ <script src="/assets/iuna-downloads.js"></script>
+</body>
+</html>
diff --git a/www/index.html b/www/index.html
@@ -305,12 +305,13 @@
<body>
<div class="topbar">
<div class="wrap">
- <a class="brand repo-link" href="https://github.com/iuna-labs/iuna"><span class="mark" aria-label="iuna"><svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><circle class="mark-dot" cx="9.4" cy="7.6" r="2.8"></circle><path class="mark-loop" d="M9.4 13v7.1c0 3.7 2.9 6.4 6.6 6.4s6.6-2.7 6.6-6.4V13"></path></svg></span><span>iuna</span></a>
+ <a class="brand repo-link" href="/"><span class="mark" aria-label="iuna"><svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><circle class="mark-dot" cx="9.4" cy="7.6" r="2.8"></circle><path class="mark-loop" d="M9.4 13v7.1c0 3.7 2.9 6.4 6.6 6.4s6.6-2.7 6.6-6.4V13"></path></svg></span><span>iuna</span></a>
<nav aria-label="Page sections">
<a href="#whatisiuna">What is iuna</a>
<a href="#howtojoin">Join</a>
- <a href="https://github.com/iuna-labs/iuna/blob/main/docs/protocol.md" target="_blank" rel="noreferrer">Protocol</a>
- <a href="https://github.com/iuna-labs/iuna">GitHub</a>
+ <a href="/git/iuna/file/docs/protocol.md.html">Protocol</a>
+ <a href="/git/iuna/">Source</a>
+ <a href="/downloads/">Downloads</a>
</nav>
</div>
</div>
@@ -323,10 +324,11 @@
<p class="pronunciation" aria-label="Pronounced yoo-nuh">/ˈjuː.nə/</p>
<p class="lead">An experimental cryptocurrency devnet that combines VDF finalization, a burn lottery, and open proof-of-work issuance.</p>
<p>iuna is not a mainnet and not money yet. It is a small L1 lab for learning how a wallet, node, finalizer, miner, mempool, and peer network behave when real people run it on real machines.</p>
+ <pre><code>git clone https://iuna.jhx.app/git/iuna.git</code></pre>
<div class="actions">
- <a class="button primary" href="#whatisiuna">What is iuna</a>
- <a class="button" href="#howtojoin">How to join</a>
- <a class="button" href="https://github.com/iuna-labs/iuna/blob/main/docs/protocol.md" target="_blank" rel="noreferrer">Read protocol</a>
+ <a class="button primary" href="/downloads/">Download builds</a>
+ <a class="button" href="/git/iuna/file/docs/protocol.md.html">Read protocol</a>
+ <a class="button" href="https://discord.gg/JcXRSSDhS" target="_blank" rel="noopener noreferrer">Join Discord</a>
</div>
</div>
<aside class="status-panel" aria-label="Protocol snapshot">
@@ -372,14 +374,14 @@
<div class="wrap">
<div class="section-head">
<h2>How To Join</h2>
- <p>Download the latest build from <a href="https://github.com/iuna-labs/iuna/releases">GitHub Releases</a>, start it, and follow setup. Choose how much of the network you want to run.</p>
+ <p>Download the latest available build from <a href="/downloads/">Downloads</a>, start it, and follow setup. Choose how much of the network you want to run.</p>
</div>
<div class="join-panel">
<div>
<div class="join-steps">
<div class="join-step">
<h3>Download</h3>
- <p>Get the latest build for your platform from GitHub Releases.</p>
+ <p>Get the latest available build from the local downloads page.</p>
</div>
<div class="join-step">
<h3>Set Up</h3>
@@ -391,7 +393,9 @@
</div>
</div>
<div class="actions join-actions">
- <a class="button primary" href="https://github.com/iuna-labs/iuna/releases">Download from Releases</a>
+ <a class="button primary" href="/downloads/">Download builds</a>
+ <a class="button" href="/git/iuna/">Browse source</a>
+ <a class="button" href="https://discord.gg/JcXRSSDhS" target="_blank" rel="noopener noreferrer">Join Discord</a>
</div>
</div>
<div class="role-list" aria-label="iuna roles">
@@ -420,7 +424,7 @@
<section class="footer">
<div class="wrap">
<span>iuna devnet prototype.</span>
- <a href="https://github.com/iuna-labs/iuna">github.com/iuna-labs/iuna</a>
+ <a href="/git/iuna/">Source</a>
</div>
</section>
</body>