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
9 changes: 9 additions & 0 deletions apps/jumentix-website/documentation/COMMERCIAL-EXPERIENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,15 @@ Use a seeded account (`apps/backend-template/seed/users.ts`). The script loads t
Management sample model, splits it into two services for the architecture view, and signs in to
the frontend for the admin and dashboard views.

`public/product/screenshots.json` lists each image, the commit it was captured at (`capturedAt`,
written by the capture script) and the source paths of the screen it shows (`watch`).
`bun run website:check-screenshot-freshness` runs in `ci:gate` and warns — without failing — when
a watched path changed after the capture, naming the commits, so a stale image is visible in the
gate log instead of being noticed by a visitor. It fails only when the manifest itself is wrong: an
image missing from the manifest or from disk, an entry without `watch`, or a `capturedAt` that is
not a commit (a shallow clone only warns). After a recapture, commit the images and the manifest
together.

Every image on the site shows Jumentix itself. The starter-template leftovers the site was
scaffolded with — the `mantine+nextjs+nextra-template.png` placeholder, the unused `Welcome`,
`ProductHunt`, `Content`, `Sponsors` and `ColorSchemeToggle` components, the template author's
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@ Use uma conta do seed (`apps/backend-template/seed/users.ts`). O script carrega
do Service Management, divide em dois serviços para a visão de arquitetura e entra no frontend
para as visões administrativa e de dashboard.

`public/product/screenshots.json` lista cada imagem, o commit em que foi capturada (`capturedAt`,
escrito pelo script de captura) e os caminhos de código da tela que ela mostra (`watch`).
`bun run website:check-screenshot-freshness` roda no `ci:gate` e avisa — sem falhar — quando um
caminho observado mudou depois da captura, nomeando os commits, para que uma imagem desatualizada
apareça no log do gate em vez de ser notada por um visitante. Ele só falha quando o próprio
manifesto está errado: imagem ausente do manifesto ou do disco, entrada sem `watch`, ou um
`capturedAt` que não é um commit (um clone raso só avisa). Depois de recapturar, faça commit das
imagens e do manifesto juntos.

Toda imagem do site mostra o próprio Jumentix. As sobras do template inicial usado no scaffold —
o placeholder `mantine+nextjs+nextra-template.png`, os componentes sem uso `Welcome`,
`ProductHunt`, `Content`, `Sponsors` e `ColorSchemeToggle`, as listas de links do autor do
Expand Down
65 changes: 65 additions & 0 deletions apps/jumentix-website/public/product/screenshots.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
{
"screenshots": [
{
"file": "domain-designer.png",
"capturedAt": "a46de5d2",
"watch": [
"apps/service-management/src",
"apps/service-management/index.html",
"apps/service-management/script.js",
"apps/service-management/styles.css",
"apps/service-management/tokens.css",
"packages/designer-core/src"
]
},
{
"file": "architecture-designer.png",
"capturedAt": "a46de5d2",
"watch": [
"apps/service-management/src",
"apps/service-management/index.html",
"apps/service-management/script.js",
"apps/service-management/styles.css",
"apps/service-management/tokens.css"
]
},
{
"file": "openapi-swagger.png",
"capturedAt": "a46de5d2",
"watch": [
"apps/service-management/src",
"apps/service-management/index.html",
"apps/service-management/script.js",
"apps/service-management/styles.css",
"apps/service-management/tokens.css",
"apps/backend-template/OASdoc"
]
},
{
"file": "code-workspace.png",
"capturedAt": "a46de5d2",
"watch": [
"apps/service-management/src",
"apps/service-management/index.html",
"apps/service-management/script.js",
"apps/service-management/styles.css",
"apps/service-management/tokens.css",
"packages/designer-core/src"
]
},
{
"file": "frontend-dashboard.png",
"capturedAt": "a46de5d2",
"watch": [
"apps/frontend/src"
]
},
{
"file": "frontend-xcrud-users.png",
"capturedAt": "a46de5d2",
"watch": [
"apps/frontend/src"
]
}
]
}
21 changes: 19 additions & 2 deletions apps/jumentix-website/scripts/capture-product-screenshots.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/**
* Capture the website's product screenshots from the running applications
* (JUM-896). Every image under public/product/ is produced here, so a refresh
* Capture the website's product screenshots from the running applications.
* Every image under public/product/ is produced here, so a refresh
* is one command against current builds rather than a manual session.
*
* Prerequisites (see documentation/COMMERCIAL-EXPERIENCE.md):
Expand All @@ -18,13 +18,15 @@
*/
import fs from 'node:fs';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';

const require = createRequire(import.meta.url);
const { webkit } = require('playwright-webkit');

const websiteRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const manifestPath = path.join(websiteRoot, 'public', 'product', 'screenshots.json');
const smUrl = process.env.SERVICE_MANAGEMENT_URL ?? 'http://127.0.0.1:3200';
const feUrl = process.env.FRONTEND_URL ?? 'http://127.0.0.1:3001';
const outDir = process.env.SCREENSHOT_OUT ?? path.join(websiteRoot, 'public', 'product');
Expand Down Expand Up @@ -115,10 +117,25 @@ async function captureFrontend(browser) {
await page.close();
}

/**
* Stamp every manifest entry with the commit just captured, so
* check-screenshot-freshness.mjs measures staleness from this capture.
* Captures written elsewhere (SCREENSHOT_OUT) leave the manifest alone.
*/
function stampManifest() {
if (path.resolve(outDir) !== path.dirname(manifestPath)) return;
const commit = execFileSync('git', ['rev-parse', '--short=12', 'HEAD'], { cwd: websiteRoot, encoding: 'utf8' }).trim();
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
for (const entry of manifest.screenshots) entry.capturedAt = commit;
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
console.log(`[capture] screenshots.json capturedAt=${commit}`);
}

const browser = await webkit.launch();
try {
await captureServiceManagement(browser);
await captureFrontend(browser);
} finally {
await browser.close();
}
stampManifest();
109 changes: 109 additions & 0 deletions apps/jumentix-website/scripts/check-screenshot-freshness.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
#!/usr/bin/env node
/**
* Screenshot freshness.
*
* `public/product/screenshots.json` records, per product screenshot, the
* commit it was captured against and the source paths of the screen it shows.
* When any of those paths changed after that commit, the screenshot may no
* longer match the product, and this check says which one and why.
*
* It warns and exits 0 on staleness: whether a change is visible enough to
* recapture is a human judgement, and an unrelated PR must not be blocked by
* it. It fails (exit 1) only when the manifest itself is wrong — unreadable,
* naming a missing image, an image the manifest does not list, or a commit
* git cannot resolve — because a broken manifest silently stops the warning.
*
* Recapture: `bun run --filter @jumentix/website screenshots:capture`, which
* rewrites every entry's `capturedAt` to the commit it captured.
*/
import fs from 'node:fs';
import path from 'node:path';
import { execFileSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';

const websiteRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const repoRoot = path.resolve(websiteRoot, '..', '..');
const productDir = path.join(websiteRoot, 'public', 'product');
export const MANIFEST_PATH = path.join(productDir, 'screenshots.json');

function git(args) {
return execFileSync('git', args, { cwd: repoRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
}

export const defaultIo = {
readManifest: () => JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8')),
listImages: () => fs.readdirSync(productDir).filter((file) => file.endsWith('.png')),
isShallow: () => git(['rev-parse', '--is-shallow-repository']) === 'true',
commitExists: (sha) => {
try {
git(['cat-file', '-e', `${sha}^{commit}`]);
return true;
} catch {
return false;
}
},
commitsSince: (sha, paths) => {
const out = git(['log', '--format=%h %s', `${sha}..HEAD`, '--', ...paths]);
return out ? out.split('\n') : [];
}
};

export function checkScreenshotFreshness(io = defaultIo) {
const failures = [];
const warnings = [];
let manifest;
try {
manifest = io.readManifest();
} catch (error) {
return { failures: [`screenshots.json is unreadable: ${error.message}`], warnings };
}
const entries = Array.isArray(manifest?.screenshots) ? manifest.screenshots : null;
if (!entries) return { failures: ['screenshots.json must hold a "screenshots" array'], warnings };

const listed = new Set(entries.map((entry) => entry.file));
for (const image of io.listImages()) {
if (!listed.has(image)) failures.push(`${image}: not listed in screenshots.json`);
}
const images = new Set(io.listImages());
for (const entry of entries) {
if (!entry.file || !entry.capturedAt || !Array.isArray(entry.watch) || entry.watch.length === 0) {
failures.push(`${entry.file ?? '<unnamed>'}: needs "file", "capturedAt" and a non-empty "watch" list`);
continue;
}
if (!images.has(entry.file)) {
failures.push(`${entry.file}: listed in screenshots.json but missing from public/product`);
continue;
}
if (!io.commitExists(entry.capturedAt)) {
// A shallow clone may simply not hold the commit; only a full history can
// prove the manifest wrong.
if (io.isShallow()) {
warnings.push(`${entry.file}: capturedAt ${entry.capturedAt} is outside this shallow clone; freshness not checked`);
} else {
failures.push(`${entry.file}: capturedAt ${entry.capturedAt} is not a commit in this repository`);
}
continue;
}
const commits = io.commitsSince(entry.capturedAt, entry.watch);
if (commits.length > 0) {
warnings.push(
`${entry.file} may be stale — ${commits.length} commit(s) touched ${entry.watch.join(', ')} since ${entry.capturedAt}: `
+ commits.slice(0, 3).join('; ')
);
}
}
return { failures, warnings };
}

function main() {
const { failures, warnings } = checkScreenshotFreshness();
warnings.forEach((warning) => console.warn(`[screenshots] WARN ${warning}`));
if (failures.length > 0) {
failures.forEach((failure) => console.error(`[screenshots] ${failure}`));
process.exitCode = 1;
return;
}
console.log(`[screenshots] manifest valid; ${warnings.length} screenshot(s) possibly stale.`);
}

if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) main();
107 changes: 107 additions & 0 deletions apps/jumentix-website/scripts/check-screenshot-freshness.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/**
* The screenshot freshness contract: stale captures warn, a broken manifest
* fails. A manifest that silently stops matching the images would turn the
* warning off without anyone noticing, so every way it can drift is a failure.
*/
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { checkScreenshotFreshness, defaultIo } from './check-screenshot-freshness.mjs';

const productDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'public', 'product');

function fakeIo({
manifest = { screenshots: [{ file: 'a.png', capturedAt: 'abc123', watch: ['apps/x/src'] }] },
images = ['a.png'],
commits = {},
known = ['abc123'],
shallow = false,
} = {}) {
return {
readManifest: () => {
if (manifest instanceof Error) throw manifest;
return manifest;
},
listImages: () => images,
isShallow: () => shallow,
commitExists: (sha) => known.includes(sha),
commitsSince: (sha) => commits[sha] ?? [],
};
}

describe('screenshot freshness', () => {
it('passes quietly when nothing watched changed since the capture', () => {
expect.hasAssertions();

expect(checkScreenshotFreshness(fakeIo())).toStrictEqual({ failures: [], warnings: [] });
});

it('warns, without failing, when a watched path changed after the capture', () => {
expect.hasAssertions();

const result = checkScreenshotFreshness(fakeIo({ commits: { abc123: ['d4e5f6 fix(x): new toolbar'] } }));

expect(result.failures).toStrictEqual([]);
expect(result.warnings).toHaveLength(1);
expect(result.warnings[0]).toContain('a.png may be stale');
expect(result.warnings[0]).toContain('d4e5f6 fix(x): new toolbar');
});

it('fails when an image on disk is missing from the manifest', () => {
expect.hasAssertions();

const result = checkScreenshotFreshness(fakeIo({ images: ['a.png', 'b.png'] }));

expect(result.failures).toStrictEqual(['b.png: not listed in screenshots.json']);
});

it('fails when the manifest lists an image that does not exist', () => {
expect.hasAssertions();

const result = checkScreenshotFreshness(fakeIo({ images: [] }));

expect(result.failures).toStrictEqual(['a.png: listed in screenshots.json but missing from public/product']);
});

it('fails on an entry without a watch list', () => {
expect.hasAssertions();

const manifest = { screenshots: [{ file: 'a.png', capturedAt: 'abc123', watch: [] }] };
const result = checkScreenshotFreshness(fakeIo({ manifest }));

expect(result.failures[0]).toContain('needs "file", "capturedAt" and a non-empty "watch" list');
});

it('fails on an unknown commit in a full clone and only warns in a shallow one', () => {
expect.hasAssertions();

expect(checkScreenshotFreshness(fakeIo({ known: [] })).failures).toStrictEqual([
'a.png: capturedAt abc123 is not a commit in this repository',
]);

const shallow = checkScreenshotFreshness(fakeIo({ known: [], shallow: true }));

expect(shallow.failures).toStrictEqual([]);
expect(shallow.warnings[0]).toContain('outside this shallow clone');
});

it('fails on an unreadable manifest or one without a screenshots array', () => {
expect.hasAssertions();

expect(checkScreenshotFreshness(fakeIo({ manifest: new Error('boom') })).failures).toStrictEqual([
'screenshots.json is unreadable: boom',
]);
expect(checkScreenshotFreshness(fakeIo({ manifest: {} })).failures).toStrictEqual([
'screenshots.json must hold a "screenshots" array',
]);
});

it('lists every product image in the committed manifest', () => {
expect.hasAssertions();

const images = fs.readdirSync(productDir).filter((file) => file.endsWith('.png')).sort();
const listed = defaultIo.readManifest().screenshots.map((entry) => entry.file).sort();

expect(listed).toStrictEqual(images);
});
});
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,10 @@
"ci:smoke": "JUMENTIX_JWT_TOKEN_SECRET_KEY=${JUMENTIX_JWT_TOKEN_SECRET_KEY:-ci_jwt_secret_key} NODE_ENV=ci bun apps/backend-template/scripts/run-api-smoke.js",
"ci:integration": "JUMENTIX_JWT_TOKEN_SECRET_KEY=${JUMENTIX_JWT_TOKEN_SECRET_KEY:-ci_jwt_secret_key} bun ci-cd/run-integration-tests.js",
"ci:security-smoke": "NODE_ENV=ci bun apps/backend-template/scripts/run-security-smoke.js",
"ci:gate": "bun run check-bun-version && bun run deps:check-overrides && bun run deps:audit && bun run lint && bun run deps:check-cycles && bun run arch:check-boundaries && bun run arch:check-users-legacy-imports && bun run arch:check-workspace-boundaries && bun run arch:check-ownership-placement && bun run arch:check-http-adapters && bun run workspace:check-quality && bun run workspace:check-coverage-policy && bun run release:governance:check && bun run governance:check-authorship && bun run requirements:check && bun run docs:check-current-governance && bun run docs:check-audience && bun run packages:check-suites && bun run packages:check-build-freshness && bun run cli:check-template-freshness && bun run website:check-content-routes && bun run rtdb:check-indexes && bun run test:integrity && bun run test-map:check && bun run ci:check-provider && bun run ci:check-third-party-review && bun run integrations:check && bun run integration-migration:check && bun run agent-registry:check && bun run test:unit && bun run frontend:test:coverage && bun run frontend:coverage:check && bun run ci:security-smoke && bun run oas:check-routes && bun run oas:check-relations && bun run serverless:check-handlers && bun run build:dev && bun run ci:smoke",
"ci:gate": "bun run check-bun-version && bun run deps:check-overrides && bun run deps:audit && bun run lint && bun run deps:check-cycles && bun run arch:check-boundaries && bun run arch:check-users-legacy-imports && bun run arch:check-workspace-boundaries && bun run arch:check-ownership-placement && bun run arch:check-http-adapters && bun run workspace:check-quality && bun run workspace:check-coverage-policy && bun run release:governance:check && bun run governance:check-authorship && bun run requirements:check && bun run docs:check-current-governance && bun run docs:check-audience && bun run packages:check-suites && bun run packages:check-build-freshness && bun run cli:check-template-freshness && bun run website:check-content-routes && bun run website:check-screenshot-freshness && bun run rtdb:check-indexes && bun run test:integrity && bun run test-map:check && bun run ci:check-provider && bun run ci:check-third-party-review && bun run integrations:check && bun run integration-migration:check && bun run agent-registry:check && bun run test:unit && bun run frontend:test:coverage && bun run frontend:coverage:check && bun run ci:security-smoke && bun run oas:check-routes && bun run oas:check-relations && bun run serverless:check-handlers && bun run build:dev && bun run ci:smoke",
"ci:gate:branch": "bun ci-cd/run-branch-quality-gate.js",
"ci:gate:task": "bun run workspace:build:packages && bun ci-cd/run-task-change-tests.js",
"ci:gate:static": "bun run deps:check-overrides && bun run deps:audit && bun run arch:check-http-adapters && bun run governance:check-authorship && bun run packages:check-suites && bun run packages:check-build-freshness && bun run cli:check-template-freshness && bun run website:check-content-routes && bun run rtdb:check-indexes && bun run test-map:check && bun run ci:check-provider && bun run oas:check-relations",
"ci:gate:static": "bun run deps:check-overrides && bun run deps:audit && bun run arch:check-http-adapters && bun run governance:check-authorship && bun run packages:check-suites && bun run packages:check-build-freshness && bun run cli:check-template-freshness && bun run website:check-content-routes && bun run website:check-screenshot-freshness && bun run rtdb:check-indexes && bun run test-map:check && bun run ci:check-provider && bun run oas:check-relations",
"ci:gate:strict": "bun ci-cd/run-full-test-matrix.js",
"ci:gate:generated-automation": "bun ci-cd/check-generated-automation-pr.js",
"dev": "bun run pm2:start:dev:restapi",
Expand Down Expand Up @@ -314,6 +314,7 @@
"smoke:db-repositories": "bun run docker:up:cassandra && bun run docker:up:mongodb && bun run test:integration:db-repositories; status=$?; bun run docker:down:cassandra; bun run docker:down:mongodb; exit $status",
"packages:check-build-freshness": "bun ci-cd/check-package-build-freshness.js",
"website:check-content-routes": "bun apps/jumentix-website/scripts/check-content-routes.js",
"website:check-screenshot-freshness": "bun apps/jumentix-website/scripts/check-screenshot-freshness.mjs",
"rtdb:check-indexes": "bun packages/agent-registry/bin/check-rtdb-indexes.js",
"test:integrity": "bun ci-cd/check-test-integrity.js",
"rtdb:export-rules": "bun ci-cd/export-database-rules.js",
Expand Down
Loading
Loading