انتقل إلى المحتوى

Frontend

The interface is plain HTML, CSS and JavaScript ES modules:

  • no framework;
  • no bundler;
  • no npm packages;
  • no build step.

Each page is a static file in web/, and its script is loaded with <script type="module" src="/assets/…">. Nothing is loaded from another website: no CDNs and no web fonts. The Content-Security-Policy enforces this.

Pages

URL File Purpose
/ index.html Landing page, sign in, create a workspace (with the emailed code step), forgot password.
/app app.html The library: teams, collections, search, selection, upload, activity bell.
/record record.html The recorder.
/v/{id} watch.html Video page: player, comments, transcript, summary, sharing, insights, video replies.
/s/{token} share.html Public share page for guests.
/settings settings.html People, invites, account, AI provider, new team.
/join/{token} join.html Accept an invitation.
/reset/{token} reset.html Choose a new password.
/tour tour.html "How Vistilla works": a 10-chapter hands-on tour.

Modules (web/assets)

Module Purpose
api.js Same-origin fetch wrapper (api.get/post/patch/put/del). Throws ApiError with the server's message. Every request has a time limit (45 s, or 180 s when sending a file part), so nothing waits for ever on a lost request. putBlob uses XHR for upload progress, and gives up when an upload stops moving for 90 s.
ui.js Shared helpers: the el() element builder, avatar() (a photo when the person has one, else initials on their colour), toast, confirmDialog, formDialog, avatars, time formatting, reply boxes, and the Track timeline with comment markers.
select.js Draws a styled dropdown over a native <select>, which stays the source of truth. Supports icons (used for provider logos).
player.js Player pieces: control bar, speed, resume position, skip silences, stall recovery, keyboard shortcuts, the viewer link buttons (each with its site's logo from /media/favicon/{host}).
vreply.js The video reply card in the comment thread: its own frosted controls and progress line.
moments.js The emoji reaction row under the player, with a count per emoji.
record.js The recorder: capture, compositing, blur, mixing, live upload, trim, publish.
ticker-worker.js A Web Worker "clock" so the canvas keeps drawing while the tab is in the background.
i18n.js English or Arabic. In Arabic it swaps every piece of text shown, as it appears (a MutationObserver), for its Arabic from assets/i18n/ar.json: whole strings by exact match, text built from parts by the list's patterns ({0}, {1} are the parts, looked up in turn), and the attributes people read (placeholder, title, aria-label, alt). People's own words (titles, comments, transcripts, summaries) are skipped. theme-boot.js sets <html lang="ar" dir="rtl"> before the first paint and hides the page until the words are in; ?lang=ar in a link chooses. The EN | ع switch sits beside the theme button. Arabic text is set in Cairo (fonts/cairo-arabic.woff2, SIL OFL). ar.json is shared with the Android app.
tvrange.js Turns the compositor's canvas into a limited-range (BT.709 "TV") I420 video track on the GPU (WebGL2, then VideoFrames into a MediaStreamTrackGenerator). A canvas's own stream is full range and looked washed out, light tints going white, in players that take every video for limited range; screen-only recordings are limited range already. Falls back to canvas.captureStream where the browser lacks a part (Firefox).
app.js Library page, including the Storage card (GET /api/orgs/{id}/storage) and uploads from a file (in 16 MiB parts above 64 MiB).
upload-meter.js The upload meter (percentage, bar, speed, time left) shared by the recorder's Publish and the library's file upload.
watch.js Video page, including the video-reply studio.
share.js Share page.
settings.js Settings page. Your account is read-only until Edit; Save is enabled only once something differs. A new photo is cropped in the browser to the middle square, drawn at 256 × 256 and sent as a JPEG (PUT /api/me/avatar) on Save.
index.js, join.js, reset.js, tour.js The other pages.
palette.js Ctrl+K quick search and navigation.
guide.js Hands-on guided tours. Four dark panels frame one real control, which stays clickable: a click step moves on when the control is used, a go step hands over to the next page's tour (through sessionStorage), and a look step has a Next button. A tour never starts by itself: the page's ? button plays it (the library's also from ?guide=1), and only a tour handed over from the page before carries on unasked. Used by the library, recorder, video, settings and share pages.
theme-boot.js Sets light or dark before the first paint, to avoid a flash.
theme.js The light/dark toggle, synced across tabs.
no-autofill.js Turns browser autofill off on every field, including fields added later.
app-shell.js Registers the service worker and the install button on the welcome tour page.
vendor-marks.js Simple SVG marks for the AI providers.

Stylesheets:

  • base.css: the design system and most components;
  • player.css, watch.css, tour.css.

Theming

  • Colours are CSS custom properties on :root. The dark theme redefines them under :root[data-theme="dark"].
  • The brand colour is teal, --brand: #2a7d94.
  • Neutrals are slightly teal-tinted greys, and the video stage is always black.
  • The chosen theme is stored in localStorage["vistilla.theme"]. Without a choice, the system preference is used.
  • Fonts are system stacks. Inter and JetBrains Mono are used only if installed locally.

Recording in the browser (record.js)

getDisplayMedia -+                       +- mic (getUserMedia)
getUserMedia ----+                       +- tab / loopback audio
 (camera)        v                       v
         canvas compositor          Web Audio mixer
   (screen + blur boxes +           (AudioContext >
    round camera bubble)             MediaStreamDestination)
         | captureStream(30)              |
         +--------------+-----------------+
                        v
                  MediaRecorder  (VP9/Opus WebM preferred)
                        | 1-second chunks
                        v
           5 MiB parts > PUT /api/videos/{id}/parts/{n}
  • Codec: the first one the browser supports from video/webm;codecs=vp9,opus, then vp8,opus, then video/webm, then video/mp4.
  • Bitrate: width × height × fps × 0.04, kept between 1 and 4 Mbit/s (2.5 Mbit/s asked of a 1080p screen, and VP9 spends less on a mostly still one), with Opus audio at 64 kbit/s. That is about a gigabyte for a two-hour meeting.
  • Keyframes every second, so the server can trim without re-encoding.
  • Camera bubble: a circle 26% of the frame height in the bottom-right corner, with a white ring.
  • Blur: every frame, each box is shrunk to about 45 cells per frame height and stretched back smoothly, which smears it beyond reading. The blur is part of the recorded pixels.
  • Live upload:
    • When recording starts, the page reserves a video with POST /api/videos (live: true).
    • Chunks are collected into 5 MiB parts and uploaded as they fill. A failed part is retried up to 3 times, after 1, 2 and 4 seconds (not for errors like 4xx that would fail again).
    • If that fails, Publish sends the rest of the file, from the first piece that did not arrive, in 5 MiB pieces again (a proxy in front refuses one big request). Only a store that cannot take pieces gets the whole file with PUT /api/videos/{id}/blob.
    • Each piece goes through putBlob, so the upload meter counts bytes as they leave: percentage, size sent, speed over the last 8 seconds, and time left.
  • Publish:
    • The poster is a frame from the preview, taken inside the kept range, drawn at most 1280 px wide as a JPEG (quality 0.78), and sent with PUT …/thumbnail.
    • Then POST …/complete sends the length, size and trim.
    • The browser opens /v/{id}.
  • If the page is closed before publishing, a keepalive DELETE removes the reservation.

Video replies

The video-reply studio in watch.js records the camera, or the screen if chosen, with a plain MediaRecorder at 2.5 Mbit/s. It uploads with the same POST /api/videos (with reply_to_comment_id) → PUT …/blob → POST …/complete flow.

A rounded light-teal tile with a dark teal screen and a pale play triangle on it, and under it a progress bar played up to an orange knob.

  • assets/logo.svg is the drawing. Every other copy is made from it:
    • the mark beside the word Vistilla in the top bar (.brand-mark) and the tab icon;
    • assets/icons/: favicon-32.png, apple-touch-icon.png (180 px, square, the phone rounds it), icon-192.png and icon-512.png, and icon-maskable-512.png (square, the artwork shrunk into the circle every mask keeps); logo-1024.png is the original artwork;
    • the server's inline /favicon.svg (web.rs), the docs' logo (docs/assets/logo.svg) and the Android vectors.

Installable app (PWA)

  • manifest.webmanifest:
    • starts at /app in its own window;
    • theme colour teal;
    • icons, plus shortcuts to Record and Library.
  • sw.js:
    • caches nothing;
    • its only job is to show a friendly "You are offline" page when a navigation fails, which reloads itself when the connection returns.