diff --git a/deno.json b/deno.json index e94b6f9..daa6188 100644 --- a/deno.json +++ b/deno.json @@ -24,8 +24,8 @@ "deploy:app": "deno task build:webapp && cd webapp && sm deploy", "stage:load": "deno run -A tools/stage-loadserver.ts", "deploy:load": "deno task stage:load && cd deploy/load && sm deploy", - "test:tiles": "deno run -A src/sync/tiles-logic.test.ts && deno run -A src/sync/richtext-crdt.test.ts && deno run -A src/sync/richtext-editing.test.ts", - "test:sync": "deno run -A src/sync/spaces/scopes.test.ts && deno run -A src/sync/spaces/client.test.ts && deno run -A src/sync/spaces/session-fetch.test.ts && deno run -A src/sync/spaces/provider.test.ts && deno run -A src/sync/spaces/webstore.test.ts && deno run -A src/web/home.test.ts && deno run -A src/web/webhost.test.ts && deno run -A src/web/tiles.test.ts && deno run -A src/sync/tiles-logic.test.ts && deno run -A src/sync/richtext-crdt.test.ts && deno run -A src/sync/richtext-editing.test.ts && deno run -A relay/test.ts && deno run -A src/sync/mls.test.ts && deno run -A src/sync/mls-transport.test.ts && deno run -A src/sync/atproto.test.ts && deno run -A src/sync/e2e.test.ts && deno run -A src/sync/newcomer.test.ts && deno run -A src/sync/reconnect.test.ts && deno run -A src/sync/restart-invite.test.ts && deno run -A src/sync/signedout.test.ts && deno run -A src/server/keeper.test.ts && deno run -A src/server/deeplink.test.ts", + "test:tiles": "deno run -A src/sync/tiles-logic.test.ts && deno run -A src/sync/richtext-crdt.test.ts && deno run -A src/sync/richtext-editing.test.ts && deno run -A src/sync/richtext-suggest.test.ts", + "test:sync": "deno run -A src/sync/spaces/scopes.test.ts && deno run -A src/sync/spaces/client.test.ts && deno run -A src/sync/spaces/session-fetch.test.ts && deno run -A src/sync/spaces/provider.test.ts && deno run -A src/sync/spaces/webstore.test.ts && deno run -A src/web/home.test.ts && deno run -A src/web/webhost.test.ts && deno run -A src/web/tiles.test.ts && deno run -A src/sync/tiles-logic.test.ts && deno run -A src/sync/richtext-crdt.test.ts && deno run -A src/sync/richtext-editing.test.ts && deno run -A src/sync/richtext-suggest.test.ts && deno run -A relay/test.ts && deno run -A src/sync/mls.test.ts && deno run -A src/sync/mls-transport.test.ts && deno run -A src/sync/atproto.test.ts && deno run -A src/sync/e2e.test.ts && deno run -A src/sync/newcomer.test.ts && deno run -A src/sync/reconnect.test.ts && deno run -A src/sync/restart-invite.test.ts && deno run -A src/sync/signedout.test.ts && deno run -A src/server/keeper.test.ts && deno run -A src/server/deeplink.test.ts", "desktop": "deno task build:ui && deno desktop --hmr -A --include static --include vendor --include tiles --include build main.ts", "build": "deno task build:ui && deno desktop -A --include static --include vendor --include tiles --include build --icon build/icon-mac-1024.png --output dist/Ziran.app main.ts && deno run -A tools/mac-bundle.ts dist/Ziran.app", "build:mac": "deno task build:ui && deno desktop -A --include static --include vendor --include tiles --include build --icon build/icon-mac-1024.png --target aarch64-apple-darwin --output dist/Ziran-arm64.app main.ts && deno run -A tools/mac-bundle.ts dist/Ziran-arm64.app", diff --git a/deploy/load/index.js b/deploy/load/index.js index de19f66..b9f845d 100644 --- a/deploy/load/index.js +++ b/deploy/load/index.js @@ -56,6 +56,17 @@ const vendorRoot = new URL('./vendor/', import.meta.url); const app = express(); app.set('trust proxy', true); // behind the supramundane front +app.disable('x-powered-by'); + +// The front's generic nice-headers snippet is OFF for this service +// ("headers": false): its X-Frame-Options sameorigin would forbid the very +// framing this server exists for, and its other headers duplicated (and +// contradicted) the hardened set below. HSTS was the one generic header +// worth keeping — set it here, on everything. +app.use((_req, res, next) => { + res.set('strict-transport-security', 'max-age=63072000; preload'); + next(); +}); app.get('/health', (_req, res) => res.json({ ok: true })); diff --git a/deploy/load/service.json b/deploy/load/service.json index f630d0c..f67d5a2 100644 --- a/deploy/load/service.json +++ b/deploy/load/service.json @@ -17,7 +17,7 @@ "*.load.ziran.space" ] }, - "headers": true, + "headers": false, "upstreamPort": 3080, "data": false, "wildcardTls": "acmedns" diff --git a/docs/WEB-APP.md b/docs/WEB-APP.md index 0238144..3f6ef55 100644 --- a/docs/WEB-APP.md +++ b/docs/WEB-APP.md @@ -239,9 +239,25 @@ same cadences, from the worker. Two web-specific notes: materializes the SAVED state → ops both ways). **Remaining (human):** `deno task deploy:load` + `deno task deploy:app`, then the exit pass in real browsers — open an imported doc, edit, reload, second browser. -- **D. Shell.** Ribbon + subtabs; connections panel, invitations strip, - presence — the components as they are, over the RPC backend. Import/ - export. `/d/` page gains the web path. +- **D. Shell — built (2026-09-01), Robin's redesign notes applied.** The + interim custom page is gone: past the sign-in gate the web app renders + the SAME components as the desktop — `zn-app`/`zn-base-window` (two + columns, Most recent | By location with a "Home" group, Models column, + ink ribbon, identity chip with avatar/name/sign-out, invitations strip, + ⌘K, drag-drop import) over `src/web/webbackend.ts`, installed with + `setBackend()` so no component knows the difference. Documents open in + ordinary BROWSER TABS (`#doc=[&ref=…]`) rendered by + `src/web/zn-web-doc.ts`: slim ink bar with the doc's zn-connections chip, + invitations when they arrive, identity, then the isolated tile frame. + "Open Tile" is the same button (browser file dialog); starters ship as + static assets (`build:webapp` stages tiles/built + tokens/base css); + ".tile" scrubbed from user-facing copy. Polling became polite: + hot 5s only for the visible doc tab, warm 60s, listSpaces 120s, inbox + 90s, hidden tabs drop to warm, visibility-return polls immediately (no + push exists in the protocol yet — the notify bridge stays the recorded + follow-up). Filesystem-only affordances (move, reveal, rename) answer + honestly. Remaining for the phase: export-to-file, and the desktop + `/d/` page web path. - **E. Ship.** sm services; smoke suite (browser harness driving two identities end to end); website "use it in the browser" entry; alpha caveats page (Safari matrix, no offline background sync). diff --git a/src/components/zn-base-window.ts b/src/components/zn-base-window.ts index fdf14e2..a32da36 100644 --- a/src/components/zn-base-window.ts +++ b/src/components/zn-base-window.ts @@ -2,7 +2,7 @@ import { css, html, LitElement, nothing } from 'lit'; import { baseStyles } from '../styles/shared.ts'; import { desk } from '../state/desk.ts'; import { keyed, modelIconUrl, recentIconUrl, relativeTime } from '../state/backend.ts'; -import type { ModelEntry, RecentEntry } from '../types.ts'; +import type { ModelEntry, RecentEntry, SyncSpace } from '../types.ts'; import { icons, zmark } from './icons.ts'; @@ -35,7 +35,7 @@ export class ZnBaseWindow extends LitElement { this.query = ''; this.dropping = false; this.menuRecent = undefined; - this.docSort = 'recent'; + this.docSort = 'location'; this.confirmModel = undefined; this.menuConfirmDelete = false; } @@ -320,12 +320,26 @@ export class ZnBaseWindow extends LitElement { min-height: 32px; min-width: 0; } - .invite-dot { - width: 7px; - height: 7px; + .invite-avatar { + width: 22px; + height: 22px; border-radius: 50%; - background: var(--signal); + object-fit: cover; flex: none; + display: block; + background: var(--panel); + } + .invite-avatar.fallback { + display: grid; + place-items: center; + background: var(--signal); + color: #fff; + font-size: 0.7rem; + font-weight: var(--w-semibold); + line-height: 1; + } + .invite-handle { + color: var(--ink-muted); } .invite-text { flex: 1; @@ -456,6 +470,42 @@ export class ZnBaseWindow extends LitElement { flex-direction: column; min-height: 0; } + .space-people { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--sp-2) var(--sp-3); + padding: 2px var(--sp-2) 8px; + font-size: var(--text-xs); + color: var(--ink-muted); + } + .space-people .person { + display: inline-flex; + align-items: center; + gap: 6px; + min-width: 0; + } + .space-people .person.pending { + opacity: 0.6; + } + .space-people .p-avatar { + width: 18px; + height: 18px; + border-radius: 50%; + object-fit: cover; + flex: none; + display: block; + background: var(--panel); + } + .space-people .p-avatar.fallback { + display: grid; + place-items: center; + background: var(--signal); + color: #fff; + font-size: 0.6rem; + font-weight: var(--w-semibold); + line-height: 1; + } .scroll { overflow-y: auto; min-height: 0; @@ -740,6 +790,12 @@ export class ZnBaseWindow extends LitElement { display: inline-grid; transform: scale(0.9); } + footer .foot-err { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } @media (max-width: 839px) { .regions { grid-template-columns: 1fr; @@ -807,23 +863,13 @@ export class ZnBaseWindow extends LitElement {
- ${!desk.recents.length && !q - ? html` -
-

Your documents live here.

-

- Ziran documents are tiles: they carry their own editor and live on this - machine, with no cloud and no account. Invite someone and a document - syncs live, everyone keeping their own copy. Start from a model to make - your first one, or drop a .tile file anywhere on this window. -

-
- ` - : this.docSort === 'location' - ? this.renderByLocation(recents, open) + ${this.docSort === 'location' + ? this.renderByLocation(recents, open) + : !desk.recents.length && !q + ? html`

No documents

` : recents.length ? recents.map((d) => this.renderRow(d, open.has(d.id), d === enterDoc)) - : html`

Nothing called “${this.query}” — try a model instead?

`} + : html`

No documents match “${this.query}”

`}
@@ -832,8 +878,8 @@ export class ZnBaseWindow extends LitElement { ${models.length ? models.map((m) => this.renderModel(m, m === enterModel)) : q - ? html`

No model matches “${this.query}”.

` - : html`

No models in your library yet.

`} + ? html`

No models match “${this.query}”

` + : html`

No models

`}
@@ -859,6 +905,17 @@ export class ZnBaseWindow extends LitElement { `; } + // Sync being down is footer-truth even before anything is shared — + // an empty spaces list with no explanation reads as "you have none". + if (sync?.provider === 'spaces' && canLive && !sync.relayOk) { + const why = sync.relayError ?? `${sync.relay ?? 'your PDS'} is unreachable; Ziran keeps trying`; + return html` + + `; + } if (sync && shared.length && !canLive) { return html` `; } + if (sync?.provider === "spaces" && canLive && !sync.relayOk) { + const why = sync.relayError ?? `${sync.relay ?? "your PDS"} is unreachable; Ziran keeps trying`; + return b2` + + `; + } if (sync && shared.length && !canLive) { return b2`