diff --git a/.gitignore b/.gitignore index 02574536..73bc6eba 100644 --- a/.gitignore +++ b/.gitignore @@ -162,3 +162,6 @@ uploads/ # Generated PWA dev output (vite-plugin-pwa) — never a source artifact neode-ui/dev-dist/ + +# Isolated feature worktree compilation and validation artifacts +.build/ diff --git a/aiui/packages/app/src/__tests__/useAI.test.ts b/aiui/packages/app/src/__tests__/useAI.test.ts index dcad6757..d0f03df5 100644 --- a/aiui/packages/app/src/__tests__/useAI.test.ts +++ b/aiui/packages/app/src/__tests__/useAI.test.ts @@ -61,6 +61,12 @@ function mockClaudeResponse(events: string[]) { } } +it('starts on Routstr before any saved provider selection', () => { + setActivePinia(createPinia()) + const { activeProvider } = useAI() + expect(activeProvider.value).toBe('routstr') +}) + describe('useAI', () => { beforeEach(() => { setActivePinia(createPinia()) @@ -74,11 +80,6 @@ describe('useAI', () => { }) describe('provider selection', () => { - it('defaults to claude provider', () => { - const { activeProvider } = useAI() - expect(activeProvider.value).toBe('claude') - }) - it('switches provider via setProvider', () => { const { setProvider, activeProvider, activeModel } = useAI() setProvider('openrouter') diff --git a/aiui/packages/app/src/components/renderers/CodeRunner.vue b/aiui/packages/app/src/components/renderers/CodeRunner.vue index 3808a740..66cd3f8f 100644 --- a/aiui/packages/app/src/components/renderers/CodeRunner.vue +++ b/aiui/packages/app/src/components/renderers/CodeRunner.vue @@ -10,6 +10,9 @@ > Run +

Choose a profile identity to manage its files.

After removing a profile from Archipelago, restart Blossom to revoke that profile’s uploads.

Store a file

Choose a file up to 16 MiB. Review it before storing.

External access and public replication are separate choices in Publish a website. Public copies may be impossible to erase.

Your files

diff --git a/docker/public-web-router/Dockerfile b/docker/public-web-router/Dockerfile new file mode 100644 index 00000000..7b3ad9cf --- /dev/null +++ b/docker/public-web-router/Dockerfile @@ -0,0 +1,6 @@ +FROM docker.io/library/python:3.13-slim-bookworm@sha256:a1165e272e578941b84abc79e4ab38a0305cd12803a5c4247979ac7655f4d641 +COPY download.py /build/download.py +RUN python3 /build/download.py && rm -rf /build +COPY router.py /app/router.py +ENV XDG_DATA_HOME=/data XDG_CONFIG_HOME=/data/config PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 +ENTRYPOINT ["python3", "/app/router.py"] diff --git a/docker/public-web-router/download.py b/docker/public-web-router/download.py new file mode 100644 index 00000000..ac8c6900 --- /dev/null +++ b/docker/public-web-router/download.py @@ -0,0 +1,28 @@ +import hashlib +import io +import platform +import tarfile +import urllib.request +from pathlib import Path + +arch = {'x86_64': 'amd64', 'aarch64': 'arm64'}[platform.machine()] +pins = { + 'frp': ('0.71.0', {'amd64': '84f27e39f11169f7adcef8e8b70c9329de17747b1f14dad9fb95eef5682ea716', 'arm64': 'f33c293c275d8fc68c654b6fba8f10b2551d6463d09a9fc9cffb7227eae82266'}), + 'caddy': ('2.11.7', {'amd64': '727b91701a392de6ebc5027509f548bf39979e5216340d0faed8fa5e69c84f8b', 'arm64': 'd8fc6d179a5d283028a472a5618564f6ad8a86fed513e64f032b3b0b7cc45e42'}), +} +for name, (version, digests) in pins.items(): + repo = 'fatedier/frp' if name == 'frp' else 'caddyserver/caddy' + url = f'https://github.com/{repo}/releases/download/v{version}/{name}_{version}_linux_{arch}.tar.gz' + with urllib.request.urlopen(url, timeout=120) as response: + data = response.read(64 * 1024 * 1024 + 1) + if hashlib.sha256(data).hexdigest() != digests[arch]: + raise ValueError(f'{name} archive checksum mismatch') + binary = 'frpc' if name == 'frp' else 'caddy' + member = f'frp_{version}_linux_{arch}/frpc' if name == 'frp' else 'caddy' + with tarfile.open(fileobj=io.BytesIO(data)) as archive: + info = archive.getmember(member) + if not info.isfile() or info.size > 128 * 1024 * 1024: + raise ValueError('Invalid binary archive member') + output = Path('/usr/local/bin') / binary + output.write_bytes(archive.extractfile(info).read()) + output.chmod(0o755) diff --git a/docker/public-web-router/router.py b/docker/public-web-router/router.py new file mode 100644 index 00000000..90cdc35f --- /dev/null +++ b/docker/public-web-router/router.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Supervise node-owned frpc and Caddy. Configuration is supplied by Setup. + +No local management listener or arbitrary TCP forwarding. Invalid or removed +configuration stops the owned children. Certificates persist in /data. +""" +import ipaddress +import json +import os +from pathlib import Path +import re +import signal +import subprocess +import time + +DOMAIN = re.compile(r'(?=.{1,253}\Z)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}\Z') +NAME = re.compile(r'[a-z0-9][a-z0-9-]{0,47}\Z') + + +def render(config): + if config.get('schema') != 1: + raise ValueError('Unsupported configuration') + gateway = config['gateway'] + host = gateway['host'] + try: + ipaddress.ip_address(host) + except ValueError: + if not DOMAIN.fullmatch(host): + raise ValueError('Invalid gateway hostname') + port = gateway['port'] + if type(port) is not int or not 1024 <= port <= 65535: + raise ValueError('Invalid gateway control port') + node = gateway['node_id'] + if not NAME.fullmatch(node): + raise ValueError('Invalid enrollment name') + for key in ('transport_token', 'enrollment_token'): + if not isinstance(gateway[key], str) or not 32 <= len(gateway[key]) <= 256: + raise ValueError('Invalid enrollment credential') + pem = gateway['ca_pem'] + if len(pem) > 16384 or not pem.startswith('-----BEGIN CERTIFICATE-----') or 'PRIVATE KEY' in pem: + raise ValueError('A gateway CA certificate is required') + server_name = gateway['tls_server_name'] + try: + ipaddress.ip_address(server_name) + except ValueError: + if not DOMAIN.fullmatch(server_name): + raise ValueError('Invalid gateway TLS name') + mode = config.get('certificate_mode', 'public') + if mode not in ('public', 'test'): + raise ValueError('Invalid certificate mode') + routes = config['routes'] + if not isinstance(routes, list) or len(routes) > 32: + raise ValueError('Too many routes') + caddy = '{\n admin off\n auto_https disable_redirects\n skip_install_trust\n}\n' + proxies = [] + domains, names = set(), set() + for route in routes: + name, domain = route['id'], route['domain'] + if not NAME.fullmatch(name) or not DOMAIN.fullmatch(domain) or name in names or domain in domains: + raise ValueError('Invalid or duplicate route') + if domain not in gateway.get('domains', []): + raise ValueError('Domain is not assigned by enrollment') + names.add(name); domains.add(domain) + address = ipaddress.IPv6Address(route['fips_address']) + if address not in ipaddress.IPv6Network('fd00::/8'): + raise ValueError('A FIPS ULA address is required') + upstream = route['port'] + app_id = route.get('app_id') + if app_id is not None: + if not isinstance(app_id, str) or not NAME.fullmatch(app_id) or name != 'app-' + app_id or type(upstream) is not int or not 1024 <= upstream <= 65535: + raise ValueError('Invalid catalogue app route') + identity_header = f'X-Archipelago-App {app_id}' + else: + if type(upstream) is not int or not 32000 <= upstream < 32032: + raise ValueError('Only published website listeners are supported') + identity_header = f'X-Archipelago-Website {name}' + tls = 'tls internal' if mode == 'test' else 'tls {\n issuer acme {\n disable_http_challenge\n }\n }' + caddy += f'https://{domain}:8443 {{\n bind 127.0.0.1\n {tls}\n reverse_proxy http://[{address}]:{upstream} {{\n header_up {identity_header}\n }}\n}}\n' + proxies.append({'name': name, 'type': 'https', 'localIP': '127.0.0.1', 'localPort': 8443, 'customDomains': [domain]}) + frpc = {'serverAddr': host, 'serverPort': port, 'user': node, + 'metadatas': {'enrollment_token': gateway['enrollment_token']}, + 'auth': {'method': 'token', 'token': gateway['transport_token'], 'additionalScopes': ['HeartBeats', 'NewWorkConns']}, + 'transport': {'tls': {'enable': True, 'trustedCaFile': '/tmp/router/gateway.crt', 'serverName': server_name}}, + 'loginFailExit': False, 'proxies': proxies, 'log': {'to': 'console', 'level': 'error'}} + return caddy, frpc, pem + + +def main(): + os.umask(0o077) + root = Path('/tmp/router'); root.mkdir(exist_ok=True) + source = Path('/config/router.json') + children = [] + stopping = False + previous = None + + def stop_children(): + for child in children: + if child.poll() is None: + child.terminate() + for child in children: + try: child.wait(timeout=5) + except subprocess.TimeoutExpired: + child.kill(); child.wait() + children.clear() + + def shutdown(*_): + nonlocal stopping + stopping = True + + signal.signal(signal.SIGTERM, shutdown) + signal.signal(signal.SIGINT, shutdown) + try: + while not stopping: + try: + if source.stat().st_size > 131072: + raise ValueError('Oversized config') + raw = source.read_bytes() + caddy, frpc, pem = render(json.loads(raw)) + if previous != raw or any(child.poll() is not None for child in children): + stop_children() + (root/'gateway.crt').write_text(pem) + (root/'frpc.json').write_text(json.dumps(frpc)) + (root/'Caddyfile').write_text(caddy) + if frpc['proxies']: + for command in [ ['/usr/local/bin/caddy', 'validate', '--config', str(root/'Caddyfile'), '--adapter', 'caddyfile'], ['/usr/local/bin/frpc', 'verify', '-c', str(root/'frpc.json')] ]: + subprocess.run(command, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, timeout=15) + for command in [['/usr/local/bin/caddy', 'run', '--config', str(root/'Caddyfile'), '--adapter', 'caddyfile'], ['/usr/local/bin/frpc', '-c', str(root/'frpc.json')]]: + children.append(subprocess.Popen(command)) + previous = raw + (root/'status.json').write_text(json.dumps({'configured': True, 'routes': len(frpc['proxies']), 'certificate_mode': json.loads(raw).get('certificate_mode', 'public'), 'externally_verified': False})) + except (OSError, ValueError, KeyError, TypeError, AttributeError, subprocess.SubprocessError): + stop_children(); previous = None + for name in ('frpc.json', 'Caddyfile', 'gateway.crt'): + (root/name).unlink(missing_ok=True) + (root/'status.json').write_text(json.dumps({'configured': False, 'externally_verified': False})) + (root/'heartbeat').touch() + time.sleep(2) + finally: + stop_children() + + +if __name__ == '__main__': + main() diff --git a/docs/app-manifest-spec.md b/docs/app-manifest-spec.md index ce3b0d2d..39e57970 100644 --- a/docs/app-manifest-spec.md +++ b/docs/app-manifest-spec.md @@ -173,6 +173,22 @@ override wins over the manifest in both directions and applies on the next request — your app cannot assume the gate is or isn't in front of it, so it must always enforce its own authorization for sensitive operations. +## Optional guest access + +`metadata.guest_access: true` opts an application into Setup's expiring, +revocable app-only access credentials. It requires an explicitly declared gated +port, an enabled AppGate, and no `session_passthrough`. The signed catalog remains +authoritative for catalog apps; a disk manifest cannot override its policy. +Wallets, signing surfaces and node administration apps must not opt in. + +A guest credential opens only the selected application, never dashboard login or +RPC. The application must still enforce its own accounts and permissions. Guest +credentials expire after the operator-selected interval (one hour to 30 days) +and can be revoked. Every subsequent HTTP request checks current scope, expiry +and revocation; an already established stream or WebSocket is not disconnected +by this first implementation. AppGate strips guest credentials before proxying. +Do not treat the guest gate as authorization for an application's internal API. + ## Launch metadata `metadata.launch` is consumed by catalog generation and the dashboard diff --git a/docs/external-access-and-websites.md b/docs/external-access-and-websites.md new file mode 100644 index 00000000..4e51fed0 --- /dev/null +++ b/docs/external-access-and-websites.md @@ -0,0 +1,644 @@ +# External access and website publishing + +Approved on 2026-10-08. Worktree: `archy-external-access`; branch: +`work/external-access-websites`; base: `c57119e9`. The release checkout, +services, build directories, artifacts and publication refs are not work areas. + +## Product contract + +AI creation uses Routstr by default, with Claude and OpenAI API-key choices in +the trusted dashboard. The operator explicitly approved these optional API +providers. Reuse the node's ecash receive flow for top-ups; funding must not change +the spending allowance or trigger inference. Preserve saved provider choices. +Ollama is deprioritized and its live-model test is not a release prerequisite for +this work; older progress entries below describe the superseded local-model path. + +Setup offers Allow external connections and Publish a website. Both use node-owned +connection state. Each app or site can select FIPS, public HTTPS, Tor, and (static +sites only) Nostr publication together. Each route has independent status and +revocation. Configured, locally available, and externally verified are different +states. Never infer public reachability from a saved record or running daemon. + +Reuse existing FIPS IPv6 ingress, AppGate and IPv4 loopback backends. Never widen +container bindings as a blanket IPv6 migration. Preserve local-only APIs and +wallet/admin exclusion policies. Existing routes are not adopted or revoked +without an explicit ownership handoff. Private route success does not satisfy a +public website's prerequisite. Removing one publication must retain shared routes. + +All required components are open source and self-hostable. No Tailscale or +proprietary control-plane dependency. Public routing uses frp with TLS terminating +on the node; gateways are selectable and replaceable. DNS and gateways remain +operator dependencies, even when their software is open source. + +Consider Nostr first wherever an existing standard fits: FIPS identity/discovery, +NIP-5A/nsyte/Blossom for optional static-site replication, existing signing flows, +and ngit contribution/review. Public announcements and replication require an +explicit choice. Keys and infrastructure credentials never enter model context. + +Mynymbox is an optional external Bitcoin/Lightning domain checkout. Explain its +registrant-of-record model. Generate exact DNS record instructions for the chosen +route; support existing domains and free addresses. Preserve mail records and +verify authoritative DNS, hostname routing, TLS and external HTTP independently. +FIPS/onion addresses do not require a purchased domain. No automatic purchases. + +AIUI creates node-owned static website projects with isolated previews, revisions, +download, publish, rollback and unpublish. Local/open model operation is supported; +no silent fallback to a proprietary model. A published website has a separate +origin from management and cannot read dashboard cookies or access signing +authority or RPC. Direct FIPS ports share a hostname, so a browser may send +host cookies to the trusted static handler; it neither reflects nor forwards +them, and published HTML runs under a script-blocking sandbox policy. Public +copies may survive unpublishing from Nostr/Blossom. + +The user also requested removal of the File Browser Setup card because the app +is already bundled in the ISO. Keep the installed app and launcher unchanged. + +## Implementation and acceptance ledger + +- [x] Isolated worktree and branch created. +- [ ] Persistent, versioned project and multi-route configuration; conflict-safe writes. +- [ ] Both Setup entry points and shared connection readiness. +- [ ] DNS guidance with Mynymbox handoff and correct per-route records. +- [ ] Static site workspace, local model generation, isolated preview and version history. +- [ ] FIPS publication and revocation through owned listeners/policies. +- [ ] Public HTTPS/frp configuration, scoped enrollment and node TLS lifecycle. +- [ ] Tor publication preserving service identity through restart. +- [ ] NIP-5A signing and Blossom replication with explicit consent and pinned versions. +- [ ] Per-app restricted access without sharing administrator credentials. +- [ ] External verification, certificate renewal, restarts, rollback and route isolation. +- [ ] Framework acceptance with confirmed identity, access and release coordination. +- [ ] ngit review and exact accepted-commit mirror parity before any release. + +The user authorized Framework as a free test node and a separate test proxy route +on Yaya. Access has been verified on both actual nodes. Preserve all +wallet/channel/app data. Source tests are not node acceptance. Backend unit tests +run only through `scripts/test-backend-isolated.sh`; use a worktree-local target. + +### Current integration checkpoint + +The operator reaffirmed the existing Setup walkthrough design during UAT. +The follow-up UI uses the existing numbered goal cards, progress styling and +Back/Continue navigation, with one expanded step. Previously saved connections +are reused; installed Blossom omits the installation step. Blossom installation +uses the normal app-store installer. Navigation itself never saves, signs or +publishes. This UI revision is now active on Framework. The actual dashboard walkthrough +saved the synthetic draft, archived it in local Blossom with a profile identity, +fetched it back to verify exact bytes, and published it through FIPS port 32000. +The 390-pixel mobile layout passed the overflow check. Browser request monitoring +recorded no external requests during this journey. + +Live archive acceptance exposed an older `node-*` identity whose `is_node` flag +was false. The container correctly rejected its upload because the canonical +signer allowlist excludes legacy node records. The website identity filter now +matches that rule, including node-name fallbacks and public-key validation, and +checks it again before signing. No public Nostr event or external replica was +created during this failure. The selected 20-test suite covers the regression and +walkthrough navigation; the production typecheck and Vite build passed. A further +new-project connection-inheritance check passed, giving eight Setup and thirteen +Nostr tests for the revised UI. + +Blossom is installed and healthy on Framework through the normal app installer. +Protocol, real HTTP/HTTPS tab signing and lifecycle/data-preservation evidence is +recorded in `apps/blossom/README.md`. The combined dashboard/backend candidate +from local commit `28a92fcc` is now deployed privately on Framework. The +authenticated publishing status probe passes; native Bitcoin/LND process IDs and +start times are unchanged. Complete browser acceptance is still in progress. No public Nostr test events or external file replicas have +been created. The earlier standalone proxy route/certificate were removed. A new owned UAT +route now connects the dashboard-published synthetic site to +`https://free.archipelago.builders` through Yaya. Trusted TLS, exact page bytes, +`/rpc` returning 404, and traversal rejection (400) pass. The route remains for UAT; +FIPS/HTTPS revocation retained the Tor publication and archive, and Tor +revocation retained HTTPS. Republishing retained the onion hostname. The existing +Yaya Tor client timed out after republish, while a separate fresh Tor client +fetched the exact restored page; do not treat the first timeout as a confirmed +publisher defect or a universal reachability pass. Temporary notice logging was +removed and the isolated test client was stopped after qualification. + +A management-service restart retained exact project/archive state, all app +container IDs/states, and native Bitcoin/LND PIDs/start times. Public HTTPS and +Tor both returned exact content after that restart. A full machine reboot is +separate and awaits the operator's recovery arrangement. The actual dashboard +Blossom iframe passed profile selection, explicit denial with zero uploads, and +an approved local upload through the canonical signer. Physical companion +acceptance remains separate; this was a browser iframe test. + +The operator requested a further UX pass informed by all existing guides. The +seven existing goal guides, help tree, shared walkthrough and onboarding patterns +were reviewed. The revised flow uses concise visitor-oriented multiselect cards, +an AIUI-first creation path, optional HTML/model controls, a saved-version preview, +explicit Save and continue, and contextual Help entries. The Home shortcuts now +respect the same dedicated guide routes as the Setup cards. The final polish is active on Framework. The production typecheck and build pass, +as do 23 focused tests. The deployed flow again passed real draft save, local +Blossom archive/readback, FIPS publishing and the mobile overflow check, with zero +external browser requests. It preserves original Setup visuals, a single main +Save and continue action, keyboard focus/scroll handling, and visible revoke +controls even when an already-published route is deselected. +No local Ollama service responded on Framework; live model generation remains +unqualified. No proprietary fallback was used or added. + +New source work includes local Blossom website archives, explicit app-only guest +credentials, and an on-demand HTTPS check against exact published page bytes. +Guest tokens cannot authenticate to node login; scope/expiry are checked on each +request, and revocation affects subsequent requests, not established streams. +Only opted-in gated app manifests expose guest access. Persistent credentials use +serialized, atomic 0600 writes and refuse corruption/capacity without evicting an +existing device. HTTPS checks pin validated public DNS addresses, validate TLS, +refuse redirects/proxies and bound response reads. They are point-in-time checks +from the node, not proof of outside-device access or future certificate renewal. + +Public-web projects can explicitly publish a FIPS upstream for an existing proxy +without selecting FIPS again. The confirmation still explains its FIPS visibility. +Automated frp enrollment/end-to-node TLS and selective local public Blossom assets +remain unfinished. Source validation and standalone routes must not be described +as acceptance of those features or of the complete dashboard journey. + +The current dashboard production build and supported AIUI build both pass and +are activated on Framework. The original backend and full web tree remain +backed up for rollback. The selected +dashboard suite passed 36 tests; subsequent HTTPS UI coverage passed six tests, +and tightened Nostr signing/receipt coverage passed 12 tests. The latest combined +18-test run, TypeScript check and dashboard rebuild passed. Catalog drift is zero +(37 catalog entries, 64 manifests). Full isolated backend validation now passes +1,699 tests, zero failures and four explicit ignores. The focused app-gate run +passes 53 tests, and all three credential tests pass. The deployable backend build +passed. Its stripped deployment artifact SHA-256 is +`11e571a7636779d7a956f9e98dab951f19de12262cf89e5ea478cdc8ba864eae`. +Passing tests and the initial authenticated activation probe do not establish +complete live-node acceptance. The private catalogue signing ceremony remains +pending; six app-sharing policies have not yet been activated. + +## Development evidence (2026-10-08, not release acceptance) + +Latest addition: [Blossom candidate package and acceptance ledger](../apps/blossom/README.md). +Setup offers catalogue installation and skips that prompt for installed Blossom. +The candidate is built and protocol-tested on Framework, and normal installation +and the real HTTPS tab signer work. Further lifecycle acceptance is in progress. +The operator temporarily disabled dashboard 2FA for tests; restore it afterwards. +Nostr publication now includes a local +preparation/review step showing exact HTML, hash, identity, manifest and destinations; +upload and announcement require explicit consent. No public Nostr events or external +Blossom uploads have been performed. Local Blossom website-asset integration remains +outstanding. Earlier evidence below records its own point in development. + +The isolated branch now contains versioned node-owned projects, multi-route +preferences, both Setup screens, local Ollama draft generation, sandboxed static +previews, revision restore and FIPS-only static publication/revocation. AIUI can +hand HTML to Setup for explicit import. Public HTTPS, Tor and Nostr adapters and +per-app grants remain outstanding; selecting a route does not enable it. + +The File Browser Setup card has been removed as requested. Its catalog entry and +launcher remain intact. No installed applications were changed. + +The backend compilation passed, including the supervisor snapshot repair. Eleven focused backend tests passed +through the isolated runner, including the actual publishing and FIPS interface +modules. The initial frontend typecheck and six publishing tests passed. Later +AIUI handoff checks subsequently passed: AIUI typechecking, dashboard typechecking, +and 30 bridge/import tests, including rejection of messages from another frame or +origin. The earlier combined dashboard run passed 52 tests. These are source +checks, not full application deployment acceptance. + +Framework access and availability were confirmed by the operator. Read-only SSH +inspection identified framework-pt and its installed FIPS 0.4.1. Yaya access was +also confirmed; its reverse proxy has the existing archipelago.builders route. +The operator subsequently confirmed Yaya is free and explicitly authorized a +separate test route. The standalone production publisher driver ran on Framework +under `archy-publishing-smoke.service`, using only +`/home/archipelago/publishing-smoke`. The main backend was not replaced or +restarted. No wallet, channel, application data or DNS settings were changed. + +Live checks completed: + +- Two temporary static sites used FIPS ports 32000 and 32001. Yaya fetched the + first over FIPS with HTTP 200 and the restrictive CSP intact. +- Restarting only the test publisher retained the sites. Unpublishing the first + closed its listener and removed its rule while the second still returned 200. +- Temporarily removing the test state file closed the second listener and removed + its allowance. Restoring the file restored the publication. This validates the + repaired stale-snapshot failure case. +- NPM proxy host 9 routed only `free.archipelago.builders` to Framework's second + FIPS site. Certificate 16 was issued successfully. Public HTTPS returned the + expected page with normal certificate verification; `/rpc` returned 404 and + `/../../etc/passwd` was rejected with 400. +- Unpublishing the remaining site left no website allowances or listeners. The + proxy request timed out without returning the old page (not a claimed 404 or + verified friendly error page). +- The temporary publisher was stopped; its empty owned firewall drop-in was + removed and the FIPS baseline reapplied. Framework's main backend remained + active. Test proxy host 9 was deleted; certificate cleanup is checked separately. + +This proves the static serving module and the existing-proxy/FIPS path. It does +not prove dashboard RPC integration on Framework, full-node reboot recovery, +certificate renewal, automated gateway enrollment, Tor or Nostr publishing. The +test HTTPS setup terminated TLS at the operator's proxy; end-to-node TLS for a +new frp gateway is still separate outstanding work. + +## Research links + +- FIPS master `57bc5108f708258c67dfc713e98e6bbb5a828e95` (2026-10-07), latest release + v0.5.2: https://github.com/jmcorgan/fips . Gateway forwards accept IPv6 targets; + Archipelago already has a separate FIPS-to-IPv4 relay and an IPv6-capable AppGate. +- frp: https://github.com/fatedier/frp (Apache-2.0). +- nsyte: https://github.com/sandwichfarm/nsyte (MIT). +- NIP-5A: https://github.com/nostr-protocol/nips/blob/master/5A.md (draft). +- Mynymbox: https://mynymbox.io/domainregistration and + https://mynymbox.io/docs?doc=domains/dns-records . + +## Tor website adapter + +Website publication uses a dedicated child Tor process with `SocksPort 0` and +`ControlPort 0`, explicit owned configuration, and 0700 identity/runtime directories +under `publishing/onions`. It does not regenerate app Tor configuration or restart +the system Tor daemon. Each onion forwards only to its own static listener on +127.0.0.1:32100–32131. Removing a website closes that listener before reloading +this owned process; the other onions and all private keys are retained. The +process exits when there are no published onion websites. No key wipe is part of +unpublishing. The UI distinguishes having an onion address from verified external +reachability. + +A standalone candidate on Framework served the second temporary onion to Yaya's +Tor client with HTTP 200 and the expected CSP. After unpublishing the first onion, +the second still returned 200. Republishing the first retained its hostname; a +publisher restart also retained that hostname. The existing system Tor process +remained PID 1449 throughout these checks. A fresh external fetch after restart +and final cleanup are recorded below when complete. + +Source validation now includes per-transport revoke isolation, Tor configuration +path/port constraints and shared connection preferences. All 12 isolated backend +tests passed; the latest selected frontend run passed 36 tests and typechecking. +The AIUI package typecheck passed separately. The final integrated backend check +passed. NPM test certificate 16 was successfully deleted after proxy +host 9; no test proxy remains on Yaya. + +The fresh external fetch of the first onion after republish and publisher restart +returned HTTP 200 with the original hostname and expected page. Both test onions +were then unpublished and the smoke unit stopped. Only system Tor PID 1449 +remained; no website listeners or owned FIPS drop-in remained. Both onion identity +directories were preserved. The final state-directory durability change passed +the 12-test isolated backend suite as well. + +### Walkthrough layout correction — 2026-10-08 + +The publishing guides now use the same available width and step alignment as +GoalDetail, with the existing small glass action buttons throughout. Step +navigation and grouped actions align left with consistent wrapping and gaps. +The external-access guide no longer renders the entire app inventory. A native +searchable app dropdown offers only guest-enabled apps and reveals grant controls +after a valid selection; installations without eligible apps show a short empty +state and a Browse apps link. + +The UI-only update was deployed to Framework with the previous UI retained at +`/opt/archipelago/web-ui.before-guide-layout-uat`. Production build and ten +walkthrough tests passed. Live browser comparisons at 1440px and 390px confirmed +matching original-guide widths/alignment and no horizontal overflow. Management +and wallet services were not restarted for this update. + +### Signed private catalogue and guest access — 2026-10-08 + +After the operator signed the private candidate, verification against the pinned +release root passed locally and on Framework. The node accepted the exact signed +catalogue through `ARCHY_APP_CATALOG_CANDIDATE`; wallet process identities and all +app container IDs/states were unchanged. This remains a private UAT catalogue, +not a published release. The owned override is +`/etc/systemd/system/archipelago.service.d/50-external-access-uat-catalog.conf`; +remove it after the reviewed catalogue release or rollback to restore normal +catalogue refresh. The preceding cache is retained in +`~/external-access-uat/catalog-before-private-candidate.json` on Framework. + +Framework now reports guest eligibility for Home Assistant, Immich, Jellyfin, +Nextcloud, PhotoPrism and Strfry. Actual-node checks with a temporary Home Assistant +grant passed anonymous challenge, bearer and browser-cookie access, denial at +Immich, rejection for dashboard login, and revocation of both bearer and cookie +access. One-hour expiry metadata was checked; elapsed expiry remains covered by +unit tests, not a one-hour live wait. The temporary grant was removed. No Nostr +events were posted and no other app data was changed. + +### Controlled Framework reboot — 2026-10-08 + +The operator confirmed physical recovery access and authorized remaining +qualification. A fresh native LND snapshot and static channel backup were retained +privately on the node before reboot; no pending HTLCs were present. A changed boot +ID confirms the full reboot. Native wallet identity, channel set, on-chain and +channel balances matched exactly afterward, and LND reported chain sync without +manual unlock/restart. Backend and signed-catalogue hashes matched. All app +running/stopped states, exact publishing/project/archive state, FIPS address, onion +address and guest eligibility survived. Public HTTPS and Tor returned the exact +synthetic page. Guest scope, dashboard denial and revocation passed again. + +Physical companion acceptance remains OPEN: the operator found the native +`datalist` app picker invisible in the companion, and Blossom blank after choosing +an identity. These are tracked as current regressions, not successful companion +acceptance. The picker replacement uses an in-page glass menu; the tab signer +must copy public identity fields instead of passing a Vue reactive Proxy through +postMessage. Blossom also requests the canonical chooser once on opening and +disables the unrelated generic NIP-98 web-app login. Deployment and actual-device +retest are required before closing these reports. Operator will restore 2FA after +the remaining installer/signer tests. + +### Selective public archive implementation — 2026-10-08 + +Each FIPS/public-web or Tor publication can separately expose its exact archived +HTML snapshot at `/`, only after an acknowledged action verifies the +local archive receipt matches the published bytes. This is a read-only +hash-addressed snapshot route, not a publicly opened Blossom app or upload API. +GET/HEAD and CORS reads serve only the selected immutable bytes with sandbox and +attachment headers. Unknown hashes, listings and uploads remain unavailable. +Later drafts cannot change the served bytes; publishing an update resets archive +sharing, and removing sharing does not unpublish the page or remove private files. + +The focused harness and isolated platform suite each passed 12 publishing tests. +The candidate backend is deployed on Framework with its preceding executable and +publishing state retained under `~/external-access-uat/`. Live trusted HTTPS +readback matched the exact snapshot; unknown hashes/list/upload returned 404. +Revocation returned the selected hash to 404 while the website still served. +The synthetic archive was unshared after the test. No external replica or Nostr +announcement was made. UI deployment and live UI acceptance are still pending. + +Stored Publication now has an optional `public_archive` field. Before rolling back +to the preceding binary, account for its deny-unknown-fields parser: retain the +latest state and migrate only this field away, or restore the pre-test state only +if no user changes would be lost. Do not blindly restore an older project file. + +### Companion corrections deployed — 2026-10-08 + +The final dashboard build includes the in-page searchable glass app picker and +the tab signer's explicit cloneable identity fields. Sixteen UI tests passed, +including a structuredClone regression test using a reactive picker identity. +Live touch-browser checks at 390px and 1440px opened all six choices, filtered to +Immich, selected it and exposed the grant controls without horizontal overflow. +The normal Blossom lifecycle rebuilt/restarted the private candidate with +`data-app-id="blossom"`, `data-no-nip98` and one automatic chooser request. Its +previous image and build context are retained for rollback. The live direct app +window reproduced the blank frame before the signer correction; after deployment, +automatic selection returned to the visible file page, the signer iframe was +hidden, no generic login request occurred, refusal prevented upload and explicit +approval stored the synthetic file. Actual phone confirmation is still pending. +The archive UI is deployed with backend capability gating; UI tests cover fresh +consent on snapshot changes and independent revocation. No public release made. + +### AI provider direction — 2026-10-08 + +The operator selected Routstr as the default, with Claude and OpenAI API keys as +alternatives, and deprioritized Ollama. The isolated publishing branch merged the +already accepted provider setup through commit `83ba98ab`, preserving its history. +Website design now opens that trusted dashboard setup and its existing ecash +Receive flow directly. New/missing provider settings default to Routstr; saved +provider selections remain unchanged. AIUI lists Routstr first. The Ollama draft +form was removed from the walkthrough; its compatibility RPC remains available. +The failed Framework Ollama test installation was removed through normal package +uninstall with `preserve_data: true`; no model was downloaded. + +Funding and spending permission remain separate. Opening setup/top-up does not +change the allowance, send a payment, start inference, or publish content. API +keys use the existing private node credential store and are not passed to AIUI. +Focused provider/publishing tests and production qualification are in progress; +these changes are not yet deployed or publicly released. + +Provider-focused validation: 20 dashboard/setup/signer tests, 22 AIUI provider +and generation tests, and 22 trusted bridge/integration tests pass. Initial +publishing tests required an AI-connection component stub for their isolated +mounts; the provider default test now checks initial state before the suite's +explicit Claude selection. The full backend suite and production builds remain +pending. Framework still runs the preceding candidate; its management service is +active and no Ollama container exists after cleanup. +The funding modal's Scan action now opens the existing wallet scanner and returns +to funding on close; six focused connection-modal tests pass after that wiring. +The first dashboard production build passed; it will be rebuilt for this final +scanner wiring before deployment. AIUI and isolated backend builds are ongoing. + +The first full isolated backend run passed 1,717 tests with one outdated default +selection assertion failing (four explicit ignores). The assertion expected an +unconfigured node to choose Claude. Updated coverage distinguishes the new Routstr +default from a saved legacy Auto choice, and the Routstr adapter now rejects zero +allowance before even discovering providers. A rerun is required; no passing full +suite or deployment is claimed yet. Final dashboard and AIUI production builds +have both passed. + +Final isolated backend rerun: **1,719 passed, zero failed, four explicit ignores**. +This includes the default Routstr zero-allowance stop and saved Auto behavior. +The deployable backend build is in progress; Framework deployment remains pending. + +### Routstr-first Framework deployment — 2026-10-08 + +The private provider backend built successfully and is active on Framework: +SHA-256 `6002af4c131545e9c93c21a65e31ef8719e6beb2682d47825d0ac95ee724e12a`. +Authenticated publishing health passed. Native Bitcoin/LND process IDs and start +times were identical before and after the management restart. The prior backend +is retained as `~/external-access-uat/backend-before-provider-setup`. + +Live qualification found a browser-history race while replacing the connection +modal with Receive: closing the first panel consumed a history entry after the +new panel opened, immediately dismissing it. AI setup now owns one history entry +across connection, funding and scanning. Other BaseModal/Receive callers retain +their default history behavior. Ten modal/connection tests and nine receive tests +pass. The final dashboard production build passed and is deployed; index SHA-256 +`d53ef48ec61f0c947df75ca57af9dd44ccf1c8a6db13ff61931b73e0dd0df1d3`. +UI backups are `/opt/archipelago/web-ui.before-provider-setup-uat` and +`/opt/archipelago/web-ui.before-provider-handoff-uat`. + +Actual Framework Chromium checks at 390px and 1440px pass: Routstr-first setup, +Claude/OpenAI inputs, empty password fields, direct ecash receive/Lightning +address display, repeated top-up opens, close/return and browser Back. No horizontal +overflow and no provider-setting, allowance or publishing mutations occurred. +The existing Claude credential is recognized by status; its value was never read. +No paid inference, new API-key save, or public content publication was performed. +Real paid-provider responses and physical companion confirmation remain separate +acceptance items. The earlier routing, Nostr, 2FA-restoration and release gates +remain open; this is a private Framework UAT update, not a general release. + +### Existing Claude credential clarity — 2026-10-08 + +Framework already reports `claude_configured: true`. The chooser now explicitly +recognizes the Settings credential, hides the empty key form, and offers Use +Claude; Change API key deliberately reveals an empty replacement input. The form +also waits for credential status before asking for a missing key. Existing OpenAI +credentials use the same presentation. Selecting an existing key writes only the +provider choice, never reads back or rewrites the credential. Seven focused tests +and the production build pass. The UI-only update is deployed on Framework, with +rollback at `/opt/archipelago/web-ui.before-existing-claude-uat`. Actual browser +checks at 390px and 1440px pass recognition, enabled Use Claude, hidden secret +input and explicit empty replacement field. Live checks did not change provider, +allowance or keys and did not run inference. Refresh the dashboard to load it. + +For the remaining end-to-end AIUI journey, also check that a provider chosen in +Setup is synchronized into an already-cached Chat iframe on return. Source +inspection shows configuration synchronization on iframe readiness; reactivation +currently arms listeners without explicitly refreshing the provider. This is a +follow-up acceptance concern, not a confirmed live inference result. + +### Remaining qualification decisions — 2026-10-08 + +Operator confirmed all three physical companion checks pass: the app dropdown, +Blossom identity selection, and AI top-up screen. This closes those manual checks. +The operator authorized isolated Yaya test ports for the new tunnel. Preserve +existing ingress on ports 80/443 and the working free.archipelago.builders route. +Isolated-port TLS qualification must not be described as public ACME issuance. +Local nsite asset integration and cached Chat provider synchronization are in +source qualification; they are not yet deployed on Framework. + +### Isolated Yaya tunnel qualification — 2026-10-08 + +Pinned frp0.71.0 and Caddy2.11.7 archives were SHA-256 verified against the +upstream release digests before use. Separate user services under +`~/external-access-uat/tunnel` run frps and the enrollment admission plugin on +Yaya (192.168.63.169:17400/control, :14443/HTTPS, loopback:17700/policy), and +frpc/Caddy on Framework (Caddy loopback:33443). Existing ports80/443 and NPM +configuration were not changed. These transient qualification units are not yet +the finished app installer or reboot-persistent product implementation. + +The gateway forwards SNI TLS to Framework. Caddy's test CA and leaf private keys +were generated on Framework and stayed there; only its public root certificate +was retrieved for verification. The frpc control connection pins Yaya's test +certificate and requires TLS plus token authentication. A separate enrollment +policy restricts Framework to free.archipelago.builders and HTTPS proxies; +policy checks also apply to new connections and heartbeats. Enrollment values +remain in private0600 files, outside publishing state and the catalogue. + +Actual-node tests passed exact synthetic website bytes (SHA-256 +`6618540be22ec1a7fbdb89ef329ac851d7ddd0391cec8aa847ac8976f9b8598d`), +404 for management/upload/list paths, rejection of unassigned SNI, revocation of +new connections to an existing route, restored-enrollment recovery, gateway +restart/reconnect, and fail-closed admission-plugin outage/recovery. Both the +isolated route and the existing public HTTPS route returned the exact same +synthetic bytes. Evidence: `.build/isolated-tunnel-live.log`. Four focused Python +admission-policy tests pass. The first outage-test cleanup attempted to restart +a removed transient unit; recreated that owned unit and reran the full live +sequence successfully. Public ACME issuance on443 remains unqualified by these +private-certificate tests. No public Nostr events were sent. + +The manifest-based router image now builds on Framework and has passed the same +live isolated-port sequence inside a rootless slirp4netns container with read-only +root, no capabilities, no published host ports and a256MiB memory limit. Evidence: +`.build/isolated-container-tunnel-live.log`. The old transient Framework frpc/Caddy +units were stopped; the owned test container is `archy-uat-public-web-router`. +Yaya's frps/admission units remain separate from existing public ingress. Actual +frpc clients were denied for an unassigned domain and a wrong enrollment token; +a wrong TLS server name also failed login (frpc reported session shutdown). + +The new source includes a private enrollment adapter, normal-catalogue installer +button, shared Setup connection and per-website connection controls. Three gateway +UI tests,27publishing UI tests, eight gateway/router Python tests and16manifest +checks pass. Final full backend tests/build, matched UI deployment, trusted +catalogue signing/install, automatic app-gate routes, public ACME443 acceptance, +final reboot and release gates remain pending. The current installed management +backend is unchanged. No paid AI call or public Nostr event was made here. + +Container lifecycle qualification also passed removal of configuration, restored +configuration, and container restart, with the same certificate and exact bytes +after recovery (`.build/router-lifecycle-live.log`). The first full isolated +backend run passed1,721tests, zero failed, four ignored; that run predates the new +gateway integration, so it is not final candidate acceptance. The subsequent +full build/test pipeline remains in progress. Automatic catalogue-app routes +and live app-identity/policy enforcement have now been added in source; their +backend and live qualification remain pending. + +### Saved Claude credential: actual inference — 2026-10-08 + +The authenticated Framework AIUI Claude proxy returned its model list, then +successfully handled one synthetic HTML request using the existing saved key. +The selected available model was `claude-haiku-4-5-20251001`, maximum64output +tokens; actual usage was44input and33output tokens. Returned HTML SHA-256: +`0c61e55d9f4c80f36d0db9dce2834677ae02a3d1cefa587d60426e6b14b8d6b1`. +The private result is `~/external-access-uat/claude-live-generated.html` on +Framework. No tools, private files, prior conversation history, provider-setting +writes, allowance changes or publications were involved. The key was injected by +the node proxy and never read back. This verifies real saved-key inference, not +completion of the separate browser AIUI-to-publishing handoff. This request may +incur the provider's normal API charge; no top-up or payment transaction was made. +The first curl-cookie attempt was unauthorized; using the existing qualification +helper's authenticated cookie handling succeeded without disabling authentication. + +### Final candidate deployment and live installer checks — 2026-10-08 + +Backend SHA-256 `683a02e1cf00d291ee82bcc2e95d159c8cf7f922b9da7e1c72187de5d8595b66` +is deployed on Framework with the matched dashboard and signed private gateway +catalogue. Final isolated backend suite: 1,726 passed, zero failed, four ignored; +focused publishing suite: 21 passed. The dashboard build initially caught a null +store access; optional chaining fixed it and the production build passes. + +Live normal installation exposed the Setup helper's missing `dockerImage`. +Both Setup install buttons now resolve the image/build tag and version from the +backend-verified catalogue and use the normal package installer. Sixteen Setup +component tests and two installation-contract tests pass. The signed catalogue +listing also resolves build tags. No catalogue signature changed. + +Framework restores runtime assets from `web-ui/archipelago-runtime` at startup. +Staging only `/opt/archipelago/apps` was therefore insufficient: startup restored +the old manifests. Updated the owned runtime payload for Blossom and Public Web +Router, then repeated normal installation successfully through the orchestrator. +The initially bare test installs were stopped/removed; their data was empty. +The owned manual qualification container was removed after the normal app was +ready; its existing certificate storage was preserved in the manifest data bind. + +Normal Router enrollment through owner RPC, private 0600 configuration, credential +redaction and exact selected website HTTPS bytes pass. Blossom updated normally +to 6.4.1-archy.2 and is healthy. Its automatic identity chooser, signing denial and +approved local upload passed again. Guest app routing through isolated Yaya TLS +passed anonymous challenge, app-only token login, Secure/HttpOnly/SameSite cookie +and revocation. Removed the temporary grant/app route and restored the website. +Existing Yaya public80/443 remains unchanged. Native wallet processes retained +PID/start time throughout management restarts. + +Local nsite live acceptance passed signer-authorized BUD-02 upload into Blossom, +exact selected hash over public HTTPS, CORS and sandboxed attachment headers, +denial of upload/list/unknown-hash endpoints, and asset revocation. Restored the +original synthetic project's routes and left its website available. No manifest +was signed or sent to relays. Evidence: `.build/local-nsite-live.log`, +`.build/gateway-app-live.log`, `.build/blossom-archy2-live.log`. + +The normal rootless router has read-only root/config, dropped capabilities and +slirp networking. Framework reports memory cgroup limit zero despite the manifest +request: resource-limit enforcement is a retained host-runtime limitation, not a +passed 256MiB boundary. Public ACME443 and general publication remain outside this +isolated-port acceptance. Final AIUI handoff and reboot checks follow below. + +The full browser AIUI path subsequently passed with the saved Claude key: actual +synthetic HTML generation, Continue to website setup, and explicit import into a +new private project. The first attempt hit the test's short navigation timeout; +the rerun with the normal page-load allowance passed. Original AI provider settings +were restored. No generated site was published. Evidence: +`.build/aiui-handoff-live.log`. Claude's normal inference charges may apply; no +Routstr top-up, wallet payment, or allowance change was performed. + +### Final controlled Framework reboot — PASS, 2026-10-08 + +The operator-authorized reboot changed boot ID from +`1eb5205a-ba5e-46de-a519-89a066bd8aac` to +`b30e5001-5ca0-4738-9e82-0a29cef0e7a6`. Preflight saved the native LND snapshot +and static channel backup privately and verified no pending HTLCs. LND initially +reported locked/not-ready during normal startup; the dashboard RPC correctly +returned unavailable rather than a false zero. It unlocked automatically without +manual restart or unlock. Native identity, channel set, on-chain/channel balances, +and chain sync then passed the saved-snapshot comparison. + +The complete installed app set returned. Blossom is healthy; the normally +installed router started without intervention and retained its certificate. +Publishing state, gateway settings, onion identity and the absence of temporary +guest grants matched the pre-reboot snapshot exactly. Both the existing public443 +route and the isolated14443 tunnel returned the original synthetic website hash +`6618540be22ec1a7fbdb89ef329ac851d7ddd0391cec8aa847ac8976f9b8598d`. +Backend and dashboard bytes and the shipped router manifest survived restart. +Dashboard index SHA-256: +`dbcff02ed9bf8cc6bab4765e1b6f81155a938145f75b3f588bc2154dbb5476a9`. + +Repeated the normal router's negative/lifecycle sequence after reboot: management, +upload/list paths denied; unassigned SNI denied; enrollment revocation denied new +connections; restore recovered; isolated gateway restart reconnected; policy +outage failed closed and recovered. Initial attempt could not authenticate to +Yaya because the old SSH control session had expired; no policy mutation occurred. +Reauthenticated with the supplied account and the complete sequence passed. +Evidence: `.build/normal-router-after-reboot-live.log`. Existing public ingress +was unchanged. Final related UI regression group passed27tests and gateway Python +group passed10tests. No public Nostr events, source push, catalogue publication, +OTA or ISO publication occurred. + +Framework UAT candidate is ready. Retained boundaries: public ACME443 passthrough +needs a dedicated public ingress, external Nostr propagation is deliberately not +claimed, Framework's rootless memory cgroup limit is not enforced, and general +release remains gated by the separate release checklist and ngit/mirror review. +The operator was asked to restore the 2FA they temporarily disabled for testing. +Private catalogue pin and isolated Yaya services remain for UAT; remove/replace +them only during the reviewed release or explicit rollback. + +Final Tor readback from Yaya's SOCKS client also returned the original synthetic +website SHA-256 after reboot. Thus FIPS-backed public HTTPS, the isolated TLS +passthrough, and the existing Tor onion all retained the same content. diff --git a/neode-ui/public/assets/img/app-icons/blossom.svg b/neode-ui/public/assets/img/app-icons/blossom.svg new file mode 100644 index 00000000..d1792614 --- /dev/null +++ b/neode-ui/public/assets/img/app-icons/blossom.svg @@ -0,0 +1 @@ + diff --git a/neode-ui/public/catalog.json b/neode-ui/public/catalog.json index a93bccb5..26ed0c66 100644 --- a/neode-ui/public/catalog.json +++ b/neode-ui/public/catalog.json @@ -715,6 +715,31 @@ "tier": "optional", "icon": "/assets/img/app-icons/gashboard.svg", "repoUrl": "https://gitworkshop.dev/npub1w3sqdkrhn0gyuvsex32effzgnfpyde6qrrc4u467flg5e9txh4wsfn5vjg/relay.ngit.dev/archy" + }, + { + "id": "blossom", + "author": "hzrd149 / Archipelago", + "requires": [], + "tier": "optional", + "title": "Blossom", + "version": "6.4.1-archy.2", + "description": "Local file storage for Nostr and websites, using your Archipelago signer. External publishing is a separate explicit choice.", + "dockerImage": "localhost/archipelago-blossom:6.4.1-archy.2", + "category": "data", + "repoUrl": "https://github.com/hzrd149/blossom-server", + "icon": "/assets/img/app-icons/blossom.svg" + }, + { + "id": "public-web-router", + "author": "Archipelago", + "requires": [], + "tier": "optional", + "title": "Public Web Router", + "version": "0.1.0", + "description": "Connect explicitly published websites to your own public gateway. HTTPS keys stay on this node. Configure routes through Setup.", + "dockerImage": "localhost/archipelago-public-web-router:0.1.0", + "category": "networking", + "icon": "/assets/img/app-icons/nginx.svg" } ] } diff --git a/neode-ui/src/components/AIConnectionModal.vue b/neode-ui/src/components/AIConnectionModal.vue index cdf574f4..0121e56a 100644 --- a/neode-ui/src/components/AIConnectionModal.vue +++ b/neode-ui/src/components/AIConnectionModal.vue @@ -1,5 +1,5 @@ diff --git a/neode-ui/src/components/BaseModal.vue b/neode-ui/src/components/BaseModal.vue index 46561a19..748b125c 100644 --- a/neode-ui/src/components/BaseModal.vue +++ b/neode-ui/src/components/BaseModal.vue @@ -62,10 +62,13 @@ const props = withDefaults(defineProps<{ maxWidth?: string zIndex?: string contentClass?: string + /** A parent flow may own one history entry across several modal panels. */ + manageHistory?: boolean }>(), { maxWidth: 'max-w-md', zIndex: 'z-[3000]', contentClass: '', + manageHistory: true, }) const emit = defineEmits<{ @@ -109,7 +112,7 @@ useBodyScrollLock(computed(() => props.show)) // Browser/mouse/gesture Back closes the modal instead of navigating the // router out from under it — the native-app behaviour kiosk and mobile // browsers expect (the companion webview already provides it natively). -useModalHistory(computed(() => props.show), close) +useModalHistory(computed(() => props.show && props.manageHistory), close) diff --git a/neode-ui/src/components/WebsiteArchiveSharing.vue b/neode-ui/src/components/WebsiteArchiveSharing.vue new file mode 100644 index 00000000..555e0605 --- /dev/null +++ b/neode-ui/src/components/WebsiteArchiveSharing.vue @@ -0,0 +1,38 @@ + + +