Skip to content
Merged

Dev #29

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
8029b2c
U: Updated gititnore
pavlicekdominik Sep 28, 2026
997a403
D: deleted outdated tests
pavlicekdominik Sep 28, 2026
6caa459
U: Consolidate Steam upload scripts
pavlicekdominik Oct 1, 2026
e7c3ec8
A: Add Electron security and credential modules
pavlicekdominik Oct 1, 2026
1aa9ac1
A: Add release and native validation scripts
pavlicekdominik Oct 1, 2026
313210d
U: Update package scripts and tool configs
pavlicekdominik Oct 1, 2026
f29d02e
U: Rework CI and deploy workflows
pavlicekdominik Oct 1, 2026
99b1c85
A: Add monitoring config and error presentation
pavlicekdominik Oct 1, 2026
7fa3ecc
U: Rework localization string table
pavlicekdominik Oct 1, 2026
5371e57
A: Add persistence layer and domain integrity
pavlicekdominik Oct 1, 2026
24d65fb
U: Refactor entity stores
pavlicekdominik Oct 1, 2026
7787e1a
U: Rework sync engine around immutable revisions
pavlicekdominik Oct 1, 2026
a1537c7
U: Track achievements and activity per profile
pavlicekdominik Oct 1, 2026
1a6df6a
A: Add editor draft recovery
pavlicekdominik Oct 1, 2026
97dd246
U: Polish dialogue editor components
pavlicekdominik Oct 1, 2026
3ead180
A: Add project recovery and archive import UI
pavlicekdominik Oct 1, 2026
a0d3bc7
A: Add sync conflict and queue UI
pavlicekdominik Oct 1, 2026
a55972c
U: Update dialogs and UI primitives
pavlicekdominik Oct 1, 2026
c74223f
U: Update locale files
pavlicekdominik Oct 1, 2026
ab47ac9
A: Add regression test suites
pavlicekdominik Oct 1, 2026
6460986
U: Fixing locked file access error
pavlicekdominik Oct 6, 2026
0595792
U: Added const and codacy fixes
pavlicekdominik Oct 6, 2026
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: 8 additions & 1 deletion .eslintrc.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@ module.exports = {
'plugin:react/jsx-runtime',
'plugin:react-hooks/recommended',
],
ignorePatterns: ['dist', '.eslintrc.cjs'],
// Disconnected pre-Zustand implementations are retained as historical fixtures.
ignorePatterns: ['dist', 'release', 'tmp', 'node_modules', 'src/helpers/**', 'src/indexedDB.js', 'src/hooks/useAutoSave.js', 'src/hooks/useAutoSaveNodesAndEdges.js'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.3' } },
plugins: ['react-refresh'],
overrides: [
{
files: ['electron/**/*.{js,cjs,mjs}', 'scripts/**/*.{js,cjs,mjs}', 'tests/**/*.{js,cjs,mjs}', '*.config.{js,cjs,mjs}', '.eslintrc.cjs'],
env: { node: true },
},
],
rules: {
'react/prop-types': 'off',
'react-refresh/only-export-components': [
Expand Down
180 changes: 153 additions & 27 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,37 +1,163 @@
name: CI

on:
push:
branches:
- master
branches: [master, 'validation/**']
pull_request:
workflow_dispatch:

permissions:
contents: read
jobs:
quality:
runs-on: ubuntu-latest
timeout-minutes: 30
runs-on: ubuntu-22.04
timeout-minutes: 45
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Install Playwright browser
run: npx playwright install --with-deps chromium

- name: Lint
run: npm run lint:ci

- name: Build
node-version: '24'
cache: npm
- run: npm ci
- name: Audit shipped runtime and build dependencies
run: |
mkdir -p tmp
npm audit --audit-level=high --json > tmp/dependency-audit.json
- run: npx playwright install --with-deps chromium
- run: npm run lint:ci
- run: npm run test:regressions
- name: Build the renderer once
run: npm run build

- name: Smoke E2E
run: npm run test:e2e:smoke
env:
VITE_SENTRY_DSN: ${{ vars.VITE_SENTRY_DSN }}
- name: Finalize private maps and validate exact production files
run: npm run validate:artifact
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
- uses: actions/upload-artifact@v4
with:
name: validated-renderer-${{ github.sha }}
path: dist
if-no-files-found: error
retention-days: 14
- name: Preserve renderer validation evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: quality-evidence-${{ github.sha }}
path: |
tmp/dependency-audit.json
tmp/playwright-report
tmp/playwright-results
tmp/production-results
dist/release.json
dist/artifact-integrity.json
dist/artifact-validation.json
if-no-files-found: warn
retention-days: 14
native:
name: Unsigned package (${{ matrix.target }})
needs: quality
strategy:
fail-fast: false
matrix:
include:
- target: windows-x64
os: windows-2022
platform: win32
arch: x64
- target: macos-intel
os: macos-15-intel
platform: darwin
arch: x64
- target: macos-arm64
os: macos-15
platform: darwin
arch: arm64
- target: linux-x64
os: ubuntu-22.04
platform: linux
arch: x64
runs-on: ${{ matrix.os }}
timeout-minutes: 60
env:
MOUNTEA_EXPECTED_PLATFORM: ${{ matrix.platform }}
MOUNTEA_EXPECTED_ARCH: ${{ matrix.arch }}
MOUNTEA_NATIVE_EVIDENCE_PATH: tmp/native-evidence/startup.json
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '24'
architecture: ${{ matrix.arch }}
cache: npm
- name: Record runner identity
shell: bash
run: |
mkdir -p tmp/native-evidence
node -e 'const fs=require("node:fs"),os=require("node:os"); fs.writeFileSync("tmp/native-evidence/host.json",JSON.stringify({platform:process.platform,arch:process.arch,node:process.version,os:os.release(),commit:process.env.GITHUB_SHA,runnerImage:process.env.ImageOS,runnerVersion:process.env.ImageVersion},null,2)); if(process.platform!==process.env.MOUNTEA_EXPECTED_PLATFORM||process.arch!==process.env.MOUNTEA_EXPECTED_ARCH) process.exit(1)'
- run: npm ci
- name: Install Linux display and runtime libraries
if: runner.os == 'Linux'
run: |
npx playwright install-deps chromium
sudo apt-get install -y xvfb xauth libgtk-3-0 libnss3 libasound2 libgbm1
- name: Install platform browser
run: npx playwright install chromium
- name: Run platform module regressions
shell: bash
run: npm run test:regressions 2>&1 | tee tmp/native-evidence/regressions.log
- uses: actions/download-artifact@v4
with:
name: validated-renderer-${{ github.sha }}
path: dist
- run: node scripts/release-artifact.mjs verify
- name: Package the tested renderer without rebuilding
shell: bash
run: npm run electron:pack -- --${{ matrix.arch }} 2>&1 | tee tmp/native-evidence/package.log
env:
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
- name: Configure packaged Linux sandbox
if: runner.os == 'Linux'
shell: bash
run: |
test -f release/linux-unpacked/chrome-sandbox
sudo chown root:root release/linux-unpacked/chrome-sandbox
sudo chmod 4755 release/linux-unpacked/chrome-sandbox
stat -c '%U:%G %a %n' release/linux-unpacked/chrome-sandbox > tmp/native-evidence/sandbox.txt
- name: Validate actual packaged startup
shell: bash
run: |
if [ "$RUNNER_OS" = Linux ]; then
xvfb-run --auto-servernum npm run validate:native-package 2>&1 | tee tmp/native-evidence/startup.log
else
npm run validate:native-package 2>&1 | tee tmp/native-evidence/startup.log
fi
- run: node scripts/release-artifact.mjs verify
- name: Validate the shared production renderer on this platform
shell: bash
run: npm run test:e2e:production 2>&1 | tee tmp/native-evidence/production.log
- run: node scripts/release-artifact.mjs verify
- name: Record gate outcome
if: always()
shell: bash
env:
NATIVE_JOB_STATUS: ${{ job.status }}
run: |
mkdir -p tmp/native-evidence
node -e 'require("node:fs").writeFileSync("tmp/native-evidence/outcome.json",JSON.stringify({status:process.env.NATIVE_JOB_STATUS,gate:"unsigned-unpacked",platform:process.env.MOUNTEA_EXPECTED_PLATFORM,arch:process.env.MOUNTEA_EXPECTED_ARCH,commit:process.env.GITHUB_SHA},null,2))'
- name: Preserve native evidence even on failure
if: always()
uses: actions/upload-artifact@v4
with:
name: native-evidence-${{ matrix.target }}-${{ github.sha }}
path: |
tmp/native-evidence
tmp/playwright-report
tmp/playwright-results
tmp/production-results
dist/release.json
dist/artifact-integrity.json
dist/artifact-validation.json
if-no-files-found: error
retention-days: 14
68 changes: 41 additions & 27 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,31 +1,45 @@
name: Deploy to GitHub Pages

name: Deploy validated GitHub Pages artifact
on:
push:
branches:
- master
workflow_dispatch:

workflow_run:
workflows: [CI]
types: [completed]
branches: [master]
permissions:
contents: read
concurrency:
group: github-pages
cancel-in-progress: false
jobs:
build-and-deploy:
deploy:
if: github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'master' && github.event.workflow_run.head_repository.full_name == github.repository
runs-on: ubuntu-latest

permissions:
contents: read
actions: read
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install dependencies
run: npm install

- name: Build
run: npm run build

- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GH_PAGES_DEPLOY_TOKEN }}
publish_dir: ./dist
- uses: actions/checkout@v4
with:
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: '24'
- uses: actions/download-artifact@v4
with:
name: validated-renderer-${{ github.event.workflow_run.head_sha }}
path: dist
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}
- name: Verify exact artifact validated by CI
run: node scripts/release-artifact.mjs verify
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: dist
- id: deployment
uses: actions/deploy-pages@v4
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,6 @@ dist-ssr
/tmp
/.mountea-user-data
/.steamcmd_probe

# Local browser automation artifacts
/.playwright-cli
21 changes: 21 additions & 0 deletions electron/artifact-integrity.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const fs = require('node:fs');
const path = require('node:path');
const { createHash } = require('node:crypto');

// Electron's patched fs also reads files inside app.asar. Include the validation
// records themselves so the installed renderer must match every validated byte.
function fingerprintDirectory(directory) {
const files = {};
function visit(root, prefix = '') {
for (const entry of fs.readdirSync(root, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
const name = prefix + entry.name, location = path.join(root, entry.name);
if (entry.isSymbolicLink()) throw new Error(`Packaged renderer contains a symbolic link: ${name}`);
if (entry.isDirectory()) visit(location, `${name}/`);
else if (entry.isFile()) files[name] = createHash('sha256').update(fs.readFileSync(location)).digest('hex');
else throw new Error(`Unsupported renderer entry: ${name}`);
}
}
visit(directory);
return files;
}
module.exports = { fingerprintDirectory };
40 changes: 40 additions & 0 deletions electron/credentials.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const fs = require('node:fs/promises');
const path = require('node:path');
const crypto = require('node:crypto');

function createCredentialVault({ safeStorage, directory, platform = process.platform }) {
let pending = Promise.resolve();
const file = path.join(directory, 'credentials.v1.json');
const available = () => Boolean(safeStorage.isEncryptionAvailable()) && (platform !== 'linux' || (typeof safeStorage.getSelectedStorageBackend === 'function' && safeStorage.getSelectedStorageBackend() !== 'basic_text'));
const read = async () => {
try { return JSON.parse(await fs.readFile(file, 'utf8')); } catch (error) { if (error.code === 'ENOENT') return {}; throw error; }
};
const serialize = (action) => {
const result = pending.then(action, action);
pending = result.catch(() => {});
return result;
};
const write = async (records) => {
await fs.mkdir(directory, { recursive: true });
const temporary = `${file}.${crypto.randomUUID()}.tmp`;
try { await fs.writeFile(temporary, JSON.stringify(records), { mode: 0o600 }); await fs.rename(temporary, file); }
finally { await fs.rm(temporary, { force: true }); }
};
return {
status: () => ({ canRemember: available() }),
get: (profileId, key) => serialize(async () => {
if (!available()) return null;
const value = (await read())[`${profileId}:${key}`];
return value ? safeStorage.decryptString(Buffer.from(value, 'base64')) : null;
}),
set: (profileId, key, value) => serialize(async () => {
if (!available()) throw new Error('Secure credential storage is unavailable; remembering is disabled');
const records = await read();
records[`${profileId}:${key}`] = safeStorage.encryptString(value).toString('base64');
await write(records);
return true;
}),
remove: (profileId, key) => serialize(async () => { const records = await read(); delete records[`${profileId}:${key}`]; await write(records); return true; }),
};
}
module.exports = { createCredentialVault };
20 changes: 20 additions & 0 deletions electron/diagnostics.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const fs = require('node:fs');

function sanitizeDiagnostics(value, depth = 0) {
if (depth > 4) return '[depth limit]';
if (Array.isArray(value)) return value.slice(0, 25).map((entry) => sanitizeDiagnostics(entry, depth + 1));
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).slice(0, 40).map(([key, entry]) => [key, /token|secret|passphrase|password|authorization|content|snapshot|email|name|title|description|text|url|path/i.test(key) ? '[redacted]' : sanitizeDiagnostics(entry, depth + 1)]));
// Provider errors are arbitrary prose and may echo credentials or authored
// content. Retain only numbers/booleans and the separately validated event ID.
return typeof value === 'string' ? '[redacted]' : value;
}
function appendDiagnostic(file, event, details) {
const line = `${new Date().toISOString()} ${String(event).replace(/[^A-Za-z0-9_.:-]/g, '_').slice(0, 80)} ${JSON.stringify(sanitizeDiagnostics(details))}\n`;
if (fs.existsSync(file) && fs.statSync(file).size + Buffer.byteLength(line) > 5 * 1024 * 1024) {
fs.rmSync(`${file}.3`, { force: true });
for (let index = 2; index >= 1; index--) if (fs.existsSync(`${file}.${index}`)) fs.renameSync(`${file}.${index}`, `${file}.${index + 1}`);
fs.renameSync(file, `${file}.1`);
}
fs.appendFileSync(file, line, { encoding: 'utf8', mode: 0o600 });
}
module.exports = { sanitizeDiagnostics, appendDiagnostic };
Loading
Loading