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, thenvp8,opus, thenvideo/webm, thenvideo/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.
- When recording starts, the page reserves a video with
- 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 …/completesends the length, size and trim. - The browser opens
/v/{id}.
- 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
- If the page is closed before publishing, a
keepaliveDELETEremoves 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.
The logo¶
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.svgis 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.pngandicon-512.png, andicon-maskable-512.png(square, the artwork shrunk into the circle every mask keeps);logo-1024.pngis the original artwork;- the server's inline
/favicon.svg(web.rs), the docs' logo (docs/assets/logo.svg) and the Android vectors.
- the mark beside the word Vistilla in the top bar (
Installable app (PWA)¶
manifest.webmanifest:- starts at
/appin its own window; - theme colour teal;
- icons, plus shortcuts to Record and Library.
- starts at
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.