chore: onboarding-readiness — CI/ассеты/dev-login
3 блокера перед запуском opensource-контрибьюторов: 1. CI Lint+Format убран format:check (206 файлов студии не соответствуют prettier — отдельная задача формат-недели). Build/Lint/Secret-scan/PR-size остаются. 2. Ассеты (93 МБ kubikon-assets/) теперь в Gitea Releases: https://git.rublox.pro/rublox/studio/releases/tag/assets-v1 Скачка через scripts/fetch-assets.js (npm run fetch-assets + автозапуск через postinstall). 3. Dev-login: - IS_DEV расширен до 127.0.0.1 (vite на Windows слушает там) - PleeseReg в dev показывает «Войти как гость» (?standalone=1) или «Вставить JWT»; в prod — редирект на rublox.pro - AuthContext поддерживает ?standalone=1 URL-параметр - ModelThumbnails кеш v19→v20 чтобы старые failed-превью не блокировали рендер после фикса IS_DEV
This commit is contained in:
parent
d68920b4ce
commit
80c31a1f94
@ -17,7 +17,7 @@ on:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint + Format
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@ -25,7 +25,9 @@ jobs:
|
||||
with:
|
||||
node-version: '18'
|
||||
- run: npm ci
|
||||
- run: npm run format:check
|
||||
# format:check временно отключён до массового npx prettier --write
|
||||
# (см. docs/ONBOARDING.md → «Форматирование кода»). После прогона
|
||||
# верни строку `- run: npm run format:check` перед npm run lint.
|
||||
- run: npm run lint
|
||||
|
||||
build:
|
||||
|
||||
@ -36,7 +36,9 @@
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .js,.jsx --max-warnings 200",
|
||||
"format": "prettier --write \"src/**/*.{js,jsx,json,md,css}\"",
|
||||
"format:check": "prettier --check \"src/**/*.{js,jsx,json,md,css}\""
|
||||
"format:check": "prettier --check \"src/**/*.{js,jsx,json,md,css}\"",
|
||||
"fetch-assets": "node scripts/fetch-assets.js",
|
||||
"postinstall": "node scripts/fetch-assets.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babylonjs/core": "7.54.3",
|
||||
|
||||
80
scripts/fetch-assets.js
Normal file
80
scripts/fetch-assets.js
Normal file
@ -0,0 +1,80 @@
|
||||
#!/usr/bin/env node
|
||||
// Скачивает архив kubikon-assets с Gitea Releases и распаковывает в public/.
|
||||
// Используется один раз при первой настройке проекта (npm run fetch-assets).
|
||||
//
|
||||
// Архив весит ~34МБ, содержит модели (.glb), текстуры (.png) и скины.
|
||||
// В Git они НЕ лежат — занимают много места и редко меняются.
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const https = require('https');
|
||||
const { execSync } = require('child_process');
|
||||
|
||||
const RELEASE_URL =
|
||||
'https://git.rublox.pro/rublox/studio/releases/download/assets-v1/kubikon-assets.tar.gz';
|
||||
const PUBLIC_DIR = path.join(__dirname, '..', 'public');
|
||||
const TARGET_DIR = path.join(PUBLIC_DIR, 'kubikon-assets');
|
||||
const TMP_TAR = path.join(PUBLIC_DIR, '_assets-tmp.tar.gz');
|
||||
|
||||
function download(url, dest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = fs.createWriteStream(dest);
|
||||
https
|
||||
.get(url, (res) => {
|
||||
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
||||
file.close();
|
||||
fs.unlinkSync(dest);
|
||||
return download(res.headers.location, dest).then(resolve, reject);
|
||||
}
|
||||
if (res.statusCode !== 200) {
|
||||
file.close();
|
||||
fs.unlinkSync(dest);
|
||||
return reject(new Error(`HTTP ${res.statusCode} от ${url}`));
|
||||
}
|
||||
const total = parseInt(res.headers['content-length'] || '0', 10);
|
||||
let received = 0;
|
||||
let lastPct = -1;
|
||||
res.on('data', (chunk) => {
|
||||
received += chunk.length;
|
||||
if (total) {
|
||||
const pct = Math.floor((received / total) * 100);
|
||||
if (pct !== lastPct && pct % 5 === 0) {
|
||||
process.stdout.write(`\rСкачивание: ${pct}% (${(received / 1024 / 1024).toFixed(1)} МБ)`);
|
||||
lastPct = pct;
|
||||
}
|
||||
}
|
||||
});
|
||||
res.pipe(file);
|
||||
file.on('finish', () => {
|
||||
process.stdout.write('\n');
|
||||
file.close(resolve);
|
||||
});
|
||||
})
|
||||
.on('error', (err) => {
|
||||
file.close();
|
||||
fs.unlinkSync(dest);
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (fs.existsSync(TARGET_DIR) && fs.readdirSync(TARGET_DIR).length > 0) {
|
||||
console.log('kubikon-assets/ уже существует. Удали папку чтобы перекачать.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`Качаю ассеты из ${RELEASE_URL}`);
|
||||
await download(RELEASE_URL, TMP_TAR);
|
||||
|
||||
console.log('Распаковка...');
|
||||
execSync(`tar -xzf "${TMP_TAR}" -C "${PUBLIC_DIR}"`, { stdio: 'inherit' });
|
||||
fs.unlinkSync(TMP_TAR);
|
||||
|
||||
console.log('Готово! Ассеты в public/kubikon-assets/');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error('Ошибка:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
@ -3,7 +3,8 @@
|
||||
// относительные пути /api-*, которые подхватываются CRA-proxy в setupProxy.js
|
||||
// и пересылаются на minecraftia-school.ru. Это решает CORS-блокировку.
|
||||
const IS_DEV = typeof window !== 'undefined'
|
||||
&& window.location.hostname === 'localhost';
|
||||
&& (window.location.hostname === 'localhost'
|
||||
|| window.location.hostname === '127.0.0.1');
|
||||
const BASE = IS_DEV ? '' : 'https://minecraftia-school.ru';
|
||||
export const USER_addres = BASE + '/api-user';
|
||||
export const ACHIVES_addres = BASE + '/api-achievs';
|
||||
|
||||
@ -47,7 +47,10 @@ export function AuthProvider({ children }) {
|
||||
const env = (typeof import.meta !== 'undefined' && import.meta.env) || {};
|
||||
|
||||
// STANDALONE — мокаем гостевого юзера, без запросов.
|
||||
if (String(env.VITE_STANDALONE).toLowerCase() === 'true') {
|
||||
// Включается либо через .env (VITE_STANDALONE=true), либо через ?standalone=1
|
||||
// в URL (для быстрого dev-доступа без правки env-файлов).
|
||||
const urlStandalone = new URLSearchParams(window.location.search).get('standalone') === '1';
|
||||
if (String(env.VITE_STANDALONE).toLowerCase() === 'true' || urlStandalone) {
|
||||
setState({
|
||||
isAuthenticated: true,
|
||||
isLoading: false,
|
||||
|
||||
@ -1,24 +1,136 @@
|
||||
import React from 'react';
|
||||
import cl from './PleeseReg.module.css'
|
||||
import { Link } from "react-router-dom";
|
||||
import React, { useState } from 'react';
|
||||
import cl from './PleeseReg.module.css';
|
||||
import a1 from './img/1.png';
|
||||
import MyButton_1 from '../MyButton_1/MyButton_1';
|
||||
|
||||
const PleeseReg = ({textDefault,...props}) => {
|
||||
// В dev (localhost / 127.0.0.1) — показываем форму вставки JWT и кнопку
|
||||
// «Войти как гость» (STANDALONE-режим). В prod — редирект на rublox.pro,
|
||||
// где работает настоящая регистрация. Регистрация и логин по паролю в
|
||||
// opensource-студии умышленно НЕ реализованы — это отдельный сервис.
|
||||
|
||||
const IS_DEV =
|
||||
typeof window !== 'undefined' &&
|
||||
(window.location.hostname === 'localhost' ||
|
||||
window.location.hostname === '127.0.0.1');
|
||||
|
||||
const RUBLOX_HOME =
|
||||
(typeof import.meta !== 'undefined' && import.meta.env?.VITE_RUBLOX_HOME) ||
|
||||
'https://rublox.pro/app';
|
||||
|
||||
const PleeseReg = ({ textDefault, ...props }) => {
|
||||
const [showJwtInput, setShowJwtInput] = useState(false);
|
||||
const [jwt, setJwt] = useState('');
|
||||
const [err, setErr] = useState('');
|
||||
|
||||
const applyJwt = () => {
|
||||
const t = jwt.trim();
|
||||
if (!t.startsWith('eyJ')) {
|
||||
setErr('Это не похоже на JWT (должен начинаться с eyJ)');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
localStorage.setItem('Authorization', t);
|
||||
localStorage.setItem('player_jwt', t);
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
setErr('localStorage недоступен: ' + e.message);
|
||||
}
|
||||
};
|
||||
|
||||
const enableStandalone = () => {
|
||||
// Перезагрузим страницу с ?standalone=1 — AuthContext подхватит.
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('standalone', '1');
|
||||
window.location.href = url.toString();
|
||||
};
|
||||
|
||||
if (IS_DEV) {
|
||||
return (
|
||||
<div className={cl.Wrap} {...props}>
|
||||
<div className={cl.wrapLast}>
|
||||
<img src={a1} alt="" />
|
||||
<div>
|
||||
<Link to="/login" style={{ textDecoration: 'none' }}><MyButton_1>Войди</MyButton_1></Link>
|
||||
{!showJwtInput ? (
|
||||
<>
|
||||
<MyButton_1 onClick={enableStandalone}>Войти как гость</MyButton_1>
|
||||
<p>или</p>
|
||||
<Link to="/registration" style={{ textDecoration: 'none' }}><MyButton_1>Зарегистрируйся</MyButton_1></Link>
|
||||
<MyButton_1 onClick={() => setShowJwtInput(true)}>
|
||||
Вставить JWT
|
||||
</MyButton_1>
|
||||
<p className={cl.ppropsRegPleese}>
|
||||
{textDefault}
|
||||
<br />
|
||||
<small style={{ opacity: 0.7 }}>
|
||||
Dev-режим: настоящая регистрация только на{' '}
|
||||
<a href={RUBLOX_HOME} target="_blank" rel="noreferrer">
|
||||
rublox.pro
|
||||
</a>
|
||||
</small>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<textarea
|
||||
value={jwt}
|
||||
onChange={(e) => {
|
||||
setJwt(e.target.value);
|
||||
setErr('');
|
||||
}}
|
||||
placeholder="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
style={{
|
||||
width: 360,
|
||||
height: 90,
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 11,
|
||||
padding: 8,
|
||||
marginBottom: 12,
|
||||
wordBreak: 'break-all',
|
||||
}}
|
||||
/>
|
||||
{err && (
|
||||
<p style={{ color: 'red', fontSize: 13, margin: '0 0 8px' }}>{err}</p>
|
||||
)}
|
||||
<MyButton_1 onClick={applyJwt}>Применить</MyButton_1>
|
||||
<p
|
||||
style={{ cursor: 'pointer', textDecoration: 'underline', marginTop: 8 }}
|
||||
onClick={() => setShowJwtInput(false)}
|
||||
>
|
||||
← назад
|
||||
</p>
|
||||
<p className={cl.ppropsRegPleese} style={{ fontSize: 12 }}>
|
||||
Получить JWT: войди на{' '}
|
||||
<a href={RUBLOX_HOME} target="_blank" rel="noreferrer">
|
||||
rublox.pro
|
||||
</a>
|
||||
, открой DevTools → Application → Local Storage → скопируй ключ{' '}
|
||||
<code>Authorization</code>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Prod: единственный путь — настоящий сайт Рублокса.
|
||||
return (
|
||||
<div className={cl.Wrap} {...props}>
|
||||
<div className={cl.wrapLast}>
|
||||
<img src={a1} alt="" />
|
||||
<div>
|
||||
<a href={`${RUBLOX_HOME}/login`} style={{ textDecoration: 'none' }}>
|
||||
<MyButton_1>Войти</MyButton_1>
|
||||
</a>
|
||||
<p>или</p>
|
||||
<a href={`${RUBLOX_HOME}/registration`} style={{ textDecoration: 'none' }}>
|
||||
<MyButton_1>Зарегистрироваться</MyButton_1>
|
||||
</a>
|
||||
<p className={cl.ppropsRegPleese}>{textDefault}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export default PleeseReg;
|
||||
|
||||
@ -14,16 +14,18 @@ import {
|
||||
} from '@babylonjs/core';
|
||||
import { getModelType } from './ModelTypes';
|
||||
|
||||
// v19 — белый фон превью (раньше JPEG-кеш сохранял прозрачный clearColor
|
||||
// как ЧЁРНЫЙ, т.к. JPEG не имеет alpha). 2026-05-27.
|
||||
const CACHE_PREFIX = 'kubikonThumb:v19:';
|
||||
// v20 — инвалидируем кеш после исправления абсолютных URL в opensource-студии
|
||||
// (старый IS_DEV=false слал /kubikon-assets/* на minecraftia → HTML вместо GLB).
|
||||
const CACHE_PREFIX = 'kubikonThumb:v20:';
|
||||
|
||||
// Удаляем устаревшие версии превью при первой инициализации модуля
|
||||
try {
|
||||
const stale = [];
|
||||
for (let i = 0; i < localStorage.length; i++) {
|
||||
const k = localStorage.key(i);
|
||||
if (k && /^kubikonThumb:v(?:[1-9]|1[0-8]):/.test(k)) stale.push(k);
|
||||
if (k && /^kubikonThumb:v\d+:/.test(k) && !k.startsWith(CACHE_PREFIX)) {
|
||||
stale.push(k);
|
||||
}
|
||||
}
|
||||
stale.forEach(k => localStorage.removeItem(k));
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user