Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/contracts.md
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,8 @@ recipes explicit project data. Existing page instances are never rewritten.
against shared bounds before storage and consumption. Browser storage is consulted only
when the project file is absent, for migration. Changes use a single pending value and
serialized writes; conflicts stop saving until the user reloads the project settings.
Opening a project without saved guide preferences uses defaults in memory. It does not create
`.stacki/guide.json`; the first guide edit or migration of a readable legacy record creates it.

`.stacki/pages.json` contains `{ version: 1, entries: [...] }`. Entries are relative to
`src/pages`; folder keys end in `/`. Desktop and Studio apply the same order to sibling pages
Expand All @@ -682,6 +684,9 @@ Desktop confines them to the open project and writes through the document actors
the supplied base. Studio exposes the same records through `/studio/api/metadata`, its
serialized document rooms and durable draft storage. Neither endpoint permits arbitrary
metadata paths. Settings writes reject stale bases rather than overwriting newer edits.
Each metadata save creates `.stacki/` when needed, independently of the guide file. Saving slot
component defaults, publish settings, page order or component sources never requires opening or
changing the guide first. Reads and invalid writes do not create project settings files.
These files participate in Studio publishing and GitHub sync. The legacy local `.stacki/`
Git exclusion is narrowed to `.stacki/preview/` and `.stacki/canvas/`; user-authored ignore rules are untouched.

Expand Down
21 changes: 21 additions & 0 deletions docs/releases.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,27 @@ Use an isolated checkout when other tasks are editing the working directory. Mat
Studio, Studio release manifest, and npm starter versions. Never move an existing release tag to
include later changes.

## 0.1.91 project settings

Opening a project without saved guide preferences now uses in-memory defaults without creating
`.stacki/guide.json`. The first guide edit still saves the file, and readable legacy preferences
still migrate. Missing, corrupt and inaccessible browser storage do not create a defaults file.

Settings saves remain independent: slot component defaults, publish destinations and scripts,
page order and component sources create their own `.stacki` files when first saved. Desktop and
Studio read and write `.stacki/publish.json`; credentials remain in host authentication storage.
Regression tests exercise first saves without a metadata directory, named-slot insertion,
stale-write protection and publish settings exchanged between the actual Desktop and Studio hosts.

PR 66's Windows failure was the creation-event watcher assertion already corrected in 0.1.89.
Updating the branch from main retains that regression fix and the removed variables-panel probe.
The refreshed Ubuntu check exposed a separate first-capture `UnknownVizError` from Electron.
Replaying that exact rejection in the real local capture test reproduced the missing thumbnail.
Capture now retries that error once, bounds each attempt to three seconds, and preserves the
previous image on failure. Tests cover recovery, persistent failure, other errors and a stalled
capture without showing the window. The native Ubuntu gate remains required for verification.
Desktop, Studio and the starter are aligned at 0.1.91.

## 0.1.90 release validation

At the user's request, remove the variables-panel layout probe because its visible Electron
Expand Down
41 changes: 34 additions & 7 deletions electron/project/thumbs.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import { BrowserWindow } from 'electron';
import { BrowserWindow, type NativeImage, type WebContents } from 'electron';
import { ok, err, type Result } from '../../shared/core/result';

import { toRecord } from '../../shared/core/record';

Expand All @@ -27,6 +28,8 @@ const THUMB_WIDTH = 720;
// still gets its picture taken.
const SETTLE_MS = 1800;
const LOAD_TIMEOUT = 15000;
const CAPTURE_TIMEOUT_MS = 3000;
const CAPTURE_RETRY_MS = 150;

const thumbsDirectory = (userDataPath: string): string => path.join(userDataPath, 'thumbs');
const keyFor = (projectPath: string): string =>
Expand Down Expand Up @@ -185,6 +188,31 @@ async function within<T>(promise: Promise<T>, ms: number, message: string): Prom
}
}

async function captureImage(contents: WebContents): Promise<Result<NativeImage, string>> {
const first = await captureImageAttempt(contents);
if (first.ok) {
return first;
}
if (first.error !== 'UnknownVizError') {
return first;
}
// A cold compositor can reject its first copy even after load and animation frames.
// Retry only that transient error, once, without reloading or showing the window.
await wait(CAPTURE_RETRY_MS);
return captureImageAttempt(contents);
}

function captureImageAttempt(contents: WebContents): Promise<Result<NativeImage, string>> {
return within(
contents.capturePage(
{ x: 0, y: 0, width: VIEWPORT.width, height: VIEWPORT.height },
{ stayHidden: false, stayAwake: false },
),
CAPTURE_TIMEOUT_MS,
'the page capture did not finish',
).then(ok, (error: unknown) => err(error instanceof Error ? error.message : String(error)));
}

type CaptureResult = { readonly ok: true } | { readonly ok: false; readonly error: string };

/**
Expand Down Expand Up @@ -245,12 +273,11 @@ async function capture(
).catch(() => {});
await wait(150);

const image = await win.webContents.capturePage({
x: 0,
y: 0,
width: VIEWPORT.width,
height: VIEWPORT.height,
});
const captured = await captureImage(win.webContents);
if (!captured.ok) {
return captured;
}
const image = captured.value;
if (image.isEmpty()) {
return { ok: false, error: 'the capture came back empty' };
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "stacki",
"productName": "Stacki",
"version": "0.1.90",
"version": "0.1.91",
"description": "Stacki \u2014 a visual Astro builder with live preview and publishing.",
"homepage": "https://github.com/flowtricks/stacki",
"main": "dist/electron/main.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/create-stacki/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "create-stacki",
"version": "0.1.90",
"version": "0.1.91",
"description": "Add Stacki Studio to an existing Astro project",
"type": "module",
"bin": {
Expand Down
11 changes: 6 additions & 5 deletions src/features/preview/guidePersistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,15 @@ class ProjectGuideController {
return;
}
this.source = stored.source;
// Without a file, only settings from app storage are written; defaults wait for a change.
const legacy =
this.source === undefined ? loadLegacyGuideSettings(this.project) : undefined;
this.settings =
this.source === undefined
? loadLegacyGuideSettings(this.project)
: guideFromSource(this.source);
this.source === undefined ? (legacy ?? GUIDE_DEFAULTS) : guideFromSource(this.source);
this.status = 'ready';
this.report('');
if (this.source === undefined) {
this.pending = this.settings;
if (legacy !== undefined) {
this.pending = legacy;
await this.save();
}
})
Expand Down
26 changes: 18 additions & 8 deletions src/features/preview/guideSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,24 +165,34 @@ function storageKey(projectKey: string): string {
return GUIDE_STORAGE_PREFIX + projectKey;
}

export function loadLegacyGuideSettings(projectKey: string | undefined): GuideSettings {
// Missing or unreadable records cannot justify creating a project settings file on open.
export function loadLegacyGuideSettings(projectKey: string | undefined): GuideSettings | undefined {
if (projectKey === undefined) {
return GUIDE_DEFAULTS;
return undefined;
}
const key = storageKey(projectKey);
try {
const raw = localStorage.getItem(key);
if (raw === null) {
return GUIDE_DEFAULTS;
const raw = localStorage.getItem(key) ?? undefined;
if (raw === undefined) {
return undefined;
}
if (raw.length > GUIDE_STORED_CHARS_MAX) {
return GUIDE_DEFAULTS;
return undefined;
}
const stored: unknown = JSON.parse(raw);
if (typeof stored !== 'object') {
return undefined;
}
if (stored === null) {
return undefined;
}
if (Array.isArray(stored)) {
return undefined;
}
return parseGuideSettings(stored);
} catch {
// Unreadable storage or a corrupt record: the defaults, never a failed load.
return GUIDE_DEFAULTS;
// The controller uses in-memory defaults without migrating inaccessible storage.
return undefined;
}
}

Expand Down
4 changes: 2 additions & 2 deletions studio/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion studio/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@stacki/studio",
"version": "0.1.90",
"version": "0.1.91",
"type": "module",
"license": "MIT",
"engines": {
Expand Down
2 changes: 1 addition & 1 deletion studio/src/shared/release.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"version": "0.1.90"
"version": "0.1.91"
}
Loading
Loading