This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
|
||||
export const disableExplorer = sdk.Action.withoutInput(
|
||||
// id
|
||||
'disable-explorer',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => {
|
||||
const current = await storeJson.read((s) => s.enableExplorer).once()
|
||||
return {
|
||||
name: i18n('Disable Embedded Explorer'),
|
||||
description: i18n(
|
||||
'Stop the embedded PostgreSQL + Go indexer. Indexed data on disk (/data/postgres, /data/explorer-indexer) is preserved and will be reused if the explorer is re-enabled later. Service restarts automatically.',
|
||||
),
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: current === true ? 'enabled' : 'hidden',
|
||||
}
|
||||
},
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
await storeJson.merge(effects, { enableExplorer: false })
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: i18n(
|
||||
'Embedded explorer disabled. The service is restarting in spaces-only mode (spaced + web terminal). Run "Enable Embedded Explorer" to turn it back on.',
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,37 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
|
||||
export const enableExplorer = sdk.Action.withoutInput(
|
||||
// id
|
||||
'enable-explorer',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => {
|
||||
const current = await storeJson.read((s) => s.enableExplorer).once()
|
||||
return {
|
||||
name: i18n('Enable Embedded Explorer'),
|
||||
description: i18n(
|
||||
'Turn on the embedded PostgreSQL + Go indexer. Spaces protocol data (blocks, transactions, spaces, rollouts) will be indexed locally for use by the future explorer web UI. Service restarts automatically.',
|
||||
),
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: current === true ? 'hidden' : 'enabled',
|
||||
}
|
||||
},
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
await storeJson.merge(effects, { enableExplorer: true })
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: i18n(
|
||||
'Embedded explorer enabled. The service is restarting; the indexer will begin fetching blocks shortly. First-time start can take several minutes for the Go build.',
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -1,3 +1,4 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { dataDir, SPACED_CHAIN } from '../utils'
|
||||
@@ -23,6 +24,18 @@ export const exportWallet = sdk.Action.withoutInput(
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
const spacedAuth = await storeJson.read((s) => s.spacedAuth).once()
|
||||
if (!spacedAuth) {
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Failure'),
|
||||
message: i18n('Could not export wallet: ${error}', {
|
||||
error: 'spacedAuth missing from store.json',
|
||||
}),
|
||||
result: null,
|
||||
}
|
||||
}
|
||||
|
||||
const mounts = sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'main',
|
||||
subpath: null,
|
||||
@@ -32,7 +45,7 @@ export const exportWallet = sdk.Action.withoutInput(
|
||||
|
||||
const cmd = [
|
||||
`mkdir -p ${BACKUP_DIR}`,
|
||||
`/root/.cargo/bin/space-cli --chain ${SPACED_CHAIN} --rpc-cookie ${dataDir}/${SPACED_CHAIN}/.cookie exportwallet ${BACKUP_PATH}`,
|
||||
`/root/.cargo/bin/space-cli --chain ${SPACED_CHAIN} --rpc-user ${spacedAuth.username} --rpc-password ${spacedAuth.password} exportwallet ${BACKUP_PATH}`,
|
||||
].join(' && ')
|
||||
|
||||
const result = await sdk.SubContainer.withTemp(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { dataDir, SPACED_CHAIN } from '../utils'
|
||||
@@ -7,7 +8,6 @@ const { InputSpec, Value } = sdk
|
||||
const BACKUP_DIR = `${dataDir}/${SPACED_CHAIN}/wallets_backup`
|
||||
const BACKUP_PATH = `${BACKUP_DIR}/default.json`
|
||||
const WALLET_DIR = `${dataDir}/${SPACED_CHAIN}/wallets/default`
|
||||
const COOKIE_PATH = `${dataDir}/${SPACED_CHAIN}/.cookie`
|
||||
|
||||
const inputSpec = InputSpec.of({
|
||||
walletJson: Value.textarea({
|
||||
@@ -63,9 +63,23 @@ export const importWallet = sdk.Action.withInput(
|
||||
}
|
||||
}
|
||||
|
||||
const spacedAuth = await storeJson.read((s) => s.spacedAuth).once()
|
||||
if (!spacedAuth) {
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Failure'),
|
||||
message: i18n('Could not import wallet: ${error}', {
|
||||
error: 'spacedAuth missing from store.json',
|
||||
}),
|
||||
result: null,
|
||||
}
|
||||
}
|
||||
|
||||
// base64-encode so shell quoting can never break on user content.
|
||||
const b64 = Buffer.from(input.walletJson, 'utf8').toString('base64')
|
||||
|
||||
const cliAuth = `--rpc-user '${spacedAuth.username}' --rpc-password '${spacedAuth.password}'`
|
||||
|
||||
const script = `set -e
|
||||
mkdir -p '${BACKUP_DIR}'
|
||||
|
||||
@@ -93,8 +107,8 @@ if [ -d "$WDIR" ]; then
|
||||
fi
|
||||
|
||||
# Import + load via spaced RPC.
|
||||
/root/.cargo/bin/space-cli --chain ${SPACED_CHAIN} --rpc-cookie '${COOKIE_PATH}' importwallet "$BACKUP"
|
||||
/root/.cargo/bin/space-cli --chain ${SPACED_CHAIN} --rpc-cookie '${COOKIE_PATH}' loadwallet
|
||||
/root/.cargo/bin/space-cli --chain ${SPACED_CHAIN} ${cliAuth} importwallet "$BACKUP"
|
||||
/root/.cargo/bin/space-cli --chain ${SPACED_CHAIN} ${cliAuth} loadwallet
|
||||
`
|
||||
|
||||
const res = await sdk.SubContainer.withTemp(
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { sdk } from '../sdk'
|
||||
import { disableExplorer } from './disableExplorer'
|
||||
import { enableExplorer } from './enableExplorer'
|
||||
import { exportWallet } from './exportWallet'
|
||||
import { importWallet } from './importWallet'
|
||||
import { resetDbState } from './resetDbState'
|
||||
import { resetExplorerState } from './resetExplorerState'
|
||||
import { resetIndexerState } from './resetIndexerState'
|
||||
import { resetPassword } from './resetPassword'
|
||||
import { resetSpacedState } from './resetSpacedState'
|
||||
import { setBitcoinRpc } from './setBitcoinRpc'
|
||||
import { showCredentials } from './showCredentials'
|
||||
import { showDbCredentials } from './showDbCredentials'
|
||||
import { showPassword } from './showPassword'
|
||||
import { syncStatus } from './syncStatus'
|
||||
|
||||
@@ -17,3 +23,9 @@ export const actions = sdk.Actions.of()
|
||||
.addAction(resetSpacedState)
|
||||
.addAction(exportWallet)
|
||||
.addAction(importWallet)
|
||||
.addAction(enableExplorer)
|
||||
.addAction(disableExplorer)
|
||||
.addAction(showDbCredentials)
|
||||
.addAction(resetDbState)
|
||||
.addAction(resetIndexerState)
|
||||
.addAction(resetExplorerState)
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { dataDir, pgDataDir } from '../utils'
|
||||
|
||||
export const resetDbState = sdk.Action.withoutInput(
|
||||
// id
|
||||
'reset-db-state',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Reset Database State'),
|
||||
description: i18n(
|
||||
'Wipe /data/postgres so PostgreSQL re-initializes from scratch.',
|
||||
),
|
||||
warning: i18n(
|
||||
'This deletes all PostgreSQL data on disk. The next start will re-create an empty database. store.json (passwords + RPC credentials) is preserved.',
|
||||
),
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
const res = await sdk.SubContainer.withTemp(
|
||||
effects,
|
||||
{ imageId: 'spaces' },
|
||||
sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'main',
|
||||
subpath: null,
|
||||
mountpoint: dataDir,
|
||||
readonly: false,
|
||||
}),
|
||||
'spaces-reset-db',
|
||||
(subc) => subc.exec(['rm', '-rf', pgDataDir], { user: 'root' }),
|
||||
)
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Failure'),
|
||||
message: i18n('Could not wipe PostgreSQL state: ${error}', {
|
||||
error: (res.stderr ?? '').toString() || `exit ${res.exitCode}`,
|
||||
}),
|
||||
result: null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: i18n(
|
||||
'PostgreSQL data has been wiped. Start (or restart) the service to re-initialize the database.',
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,58 @@
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { dataDir, EXPLORER_DIR } from '../utils'
|
||||
|
||||
export const resetExplorerState = sdk.Action.withoutInput(
|
||||
// id
|
||||
'reset-explorer-state',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Reset Explorer UI State'),
|
||||
description: i18n(
|
||||
'Wipe /data/explorer-ui so the next start re-fetches the explorer source and rebuilds it from scratch.',
|
||||
),
|
||||
warning: i18n(
|
||||
'This deletes the cached explorer source and the built SvelteKit bundle. The next start will need internet access to re-download from GitHub and to fetch npm dependencies. PostgreSQL data is preserved.',
|
||||
),
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
const res = await sdk.SubContainer.withTemp(
|
||||
effects,
|
||||
{ imageId: 'explorer-ui' },
|
||||
sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'main',
|
||||
subpath: null,
|
||||
mountpoint: dataDir,
|
||||
readonly: false,
|
||||
}),
|
||||
'spaces-reset-explorer',
|
||||
(subc) => subc.exec(['rm', '-rf', EXPLORER_DIR], { user: 'root' }),
|
||||
)
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Failure'),
|
||||
message: i18n('Could not wipe explorer state: ${error}', {
|
||||
error: (res.stderr ?? '').toString() || `exit ${res.exitCode}`,
|
||||
}),
|
||||
result: null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: i18n(
|
||||
'Explorer cache has been wiped. Start (or restart) the service to re-fetch and rebuild from GitHub.',
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,58 @@
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { dataDir, INDEXER_DIR } from '../utils'
|
||||
|
||||
export const resetIndexerState = sdk.Action.withoutInput(
|
||||
// id
|
||||
'reset-indexer-state',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Reset Indexer State'),
|
||||
description: i18n(
|
||||
'Wipe /data/explorer-indexer so the next start re-fetches the indexer source and rebuilds the sync + goose binaries.',
|
||||
),
|
||||
warning: i18n(
|
||||
'This deletes the cached indexer source and compiled binaries. The next start will need internet access to re-download from GitHub and to fetch Go modules. PostgreSQL data (the indexed blocks themselves) is preserved — use Reset Database State if you also want to clear that.',
|
||||
),
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
const res = await sdk.SubContainer.withTemp(
|
||||
effects,
|
||||
{ imageId: 'spaces' },
|
||||
sdk.Mounts.of().mountVolume({
|
||||
volumeId: 'main',
|
||||
subpath: null,
|
||||
mountpoint: dataDir,
|
||||
readonly: false,
|
||||
}),
|
||||
'spaces-reset-indexer',
|
||||
(subc) => subc.exec(['rm', '-rf', INDEXER_DIR], { user: 'root' }),
|
||||
)
|
||||
|
||||
if (res.exitCode !== 0) {
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Failure'),
|
||||
message: i18n('Could not wipe indexer state: ${error}', {
|
||||
error: (res.stderr ?? '').toString() || `exit ${res.exitCode}`,
|
||||
}),
|
||||
result: null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Success'),
|
||||
message: i18n(
|
||||
'Indexer cache has been wiped. Start (or restart) the service to re-fetch and rebuild from GitHub.',
|
||||
),
|
||||
result: null,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -9,7 +9,7 @@ export const resetPassword = sdk.Action.withoutInput(
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Reset Web UI Password'),
|
||||
name: i18n('Reset Space-CLI Web UI Password'),
|
||||
description: i18n(
|
||||
'Generate a new admin password for the Spaces web terminal',
|
||||
),
|
||||
|
||||
@@ -9,7 +9,7 @@ export const showCredentials = sdk.Action.withoutInput(
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Show Web UI Credentials'),
|
||||
name: i18n('Show Space-CLI Web UI Credentials'),
|
||||
description: i18n(
|
||||
'Display the username and password for the Spaces web terminal',
|
||||
),
|
||||
@@ -25,7 +25,7 @@ export const showCredentials = sdk.Action.withoutInput(
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Show Web UI Credentials'),
|
||||
title: i18n('Show Space-CLI Web UI Credentials'),
|
||||
message: i18n(
|
||||
'Use these credentials to log in to the Spaces web terminal.',
|
||||
),
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { POSTGRES_PORT } from '../utils'
|
||||
|
||||
export const showDbCredentials = sdk.Action.withoutInput(
|
||||
// id
|
||||
'show-db-credentials',
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Show Database Credentials'),
|
||||
description: i18n(
|
||||
'Display the PostgreSQL username, password, database, and connection URL.',
|
||||
),
|
||||
warning: null,
|
||||
allowedStatuses: 'any',
|
||||
group: null,
|
||||
visibility: 'enabled',
|
||||
}),
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
const dbAuth = await storeJson.read((s) => s.dbAuth).once()
|
||||
const username = dbAuth?.username ?? ''
|
||||
const password = dbAuth?.password ?? ''
|
||||
const database = dbAuth?.database ?? ''
|
||||
const url = dbAuth
|
||||
? `postgres://${dbAuth.username}:${dbAuth.password}@127.0.0.1:${POSTGRES_PORT}/${dbAuth.database}`
|
||||
: ''
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Show Database Credentials'),
|
||||
message: i18n(
|
||||
'Use these credentials to connect to the Spaces PostgreSQL database (loopback only inside the container).',
|
||||
),
|
||||
result: {
|
||||
type: 'group',
|
||||
value: [
|
||||
{
|
||||
type: 'single',
|
||||
name: i18n('Username'),
|
||||
description: null,
|
||||
value: username,
|
||||
masked: false,
|
||||
copyable: true,
|
||||
qr: false,
|
||||
},
|
||||
{
|
||||
type: 'single',
|
||||
name: i18n('Password'),
|
||||
description: null,
|
||||
value: password,
|
||||
masked: true,
|
||||
copyable: true,
|
||||
qr: false,
|
||||
},
|
||||
{
|
||||
type: 'single',
|
||||
name: i18n('Database'),
|
||||
description: null,
|
||||
value: database,
|
||||
masked: false,
|
||||
copyable: true,
|
||||
qr: false,
|
||||
},
|
||||
{
|
||||
type: 'single',
|
||||
name: i18n('Connection URL'),
|
||||
description: null,
|
||||
value: url,
|
||||
masked: true,
|
||||
copyable: true,
|
||||
qr: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -9,7 +9,7 @@ export const showPassword = sdk.Action.withoutInput(
|
||||
|
||||
// metadata
|
||||
async ({ effects }) => ({
|
||||
name: i18n('Show Web UI Password'),
|
||||
name: i18n('Show Space-CLI Web UI Password'),
|
||||
description: i18n(
|
||||
'Display the existing username and password for the Spaces web terminal.',
|
||||
),
|
||||
@@ -25,7 +25,7 @@ export const showPassword = sdk.Action.withoutInput(
|
||||
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Show Web UI Password'),
|
||||
title: i18n('Show Space-CLI Web UI Password'),
|
||||
message: i18n(
|
||||
'Use these credentials to log in to the Spaces web terminal.',
|
||||
),
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { storeJson } from '../fileModels/storeJson'
|
||||
import { i18n } from '../i18n'
|
||||
import { sdk } from '../sdk'
|
||||
import { dataDir, SPACED_CHAIN } from '../utils'
|
||||
@@ -20,6 +21,16 @@ export const syncStatus = sdk.Action.withoutInput(
|
||||
|
||||
// run
|
||||
async ({ effects }) => {
|
||||
const spacedAuth = await storeJson.read((s) => s.spacedAuth).once()
|
||||
if (!spacedAuth) {
|
||||
return {
|
||||
version: '1',
|
||||
title: i18n('Failure'),
|
||||
message: i18n('Could not query spaced. Is the service running?'),
|
||||
result: null,
|
||||
}
|
||||
}
|
||||
|
||||
const res = await sdk.SubContainer.withTemp(
|
||||
effects,
|
||||
{ imageId: 'spaces' },
|
||||
@@ -35,8 +46,10 @@ export const syncStatus = sdk.Action.withoutInput(
|
||||
'/root/.cargo/bin/space-cli',
|
||||
'--chain',
|
||||
SPACED_CHAIN,
|
||||
'--rpc-cookie',
|
||||
`${dataDir}/${SPACED_CHAIN}/.cookie`,
|
||||
'--rpc-user',
|
||||
spacedAuth.username,
|
||||
'--rpc-password',
|
||||
spacedAuth.password,
|
||||
'getserverinfo',
|
||||
]),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user