fix(apps): unblock saleor and netbird first-use flows
This commit is contained in:
parent
608f4c17f0
commit
f4368785f0
@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.7.77-alpha (2026-05-20)
|
||||||
|
|
||||||
|
- Saleor first-use now exposes generated credentials through Archipelago instead of leaving users at an unexplained dashboard login: App Details shows copyable `admin@example.com` credentials, and My Apps/mobile icon launches show a pre-launch credentials modal.
|
||||||
|
- Saleor installs now create or repair the `admin@example.com` staff account idempotently after sample data loads, use the correct dashboard mount path, and re-check stack containers after startup so stopped containers are caught.
|
||||||
|
- NetBird embedded login now uses the upstream-compatible IdP signing-key behavior and sends ID tokens from the dashboard to the management API, fixing the post-signup `Unauthenticated` state while preserving the unified local proxy/logout routes.
|
||||||
|
- Transient unnamed Podman helper containers created during app install tasks are hidden from My Apps, so generated names like `eager_keldysh` no longer appear as user applications.
|
||||||
|
- Validation passed with catalog/release JSON checks, `npm run type-check`, and `cargo fmt --all --check --manifest-path core/Cargo.toml`; live checks on `100.114.134.21` confirmed Saleor dashboard/API availability, generated Saleor admin login, NetBird OAuth availability, and NetBird logout redirects.
|
||||||
|
|
||||||
## v1.7.76-alpha (2026-05-20)
|
## v1.7.76-alpha (2026-05-20)
|
||||||
|
|
||||||
- Saleor installs now use dashboard port `9010`, avoiding the existing Portainer `9000` binding on the test node while keeping API `8000`, Mailpit `8025`, and Jaeger `16686` unchanged.
|
- Saleor installs now use dashboard port `9010`, avoiding the existing Portainer `9000` binding on the test node while keeping API `8000`, Mailpit `8025`, and Jaeger `16686` unchanged.
|
||||||
|
|||||||
@ -55,6 +55,7 @@ impl RpcHandler {
|
|||||||
"package.restart" => self.handle_package_restart(params).await,
|
"package.restart" => self.handle_package_restart(params).await,
|
||||||
"package.uninstall" => self.clone().spawn_package_uninstall(params).await,
|
"package.uninstall" => self.clone().spawn_package_uninstall(params).await,
|
||||||
"package.update" => self.clone().spawn_package_update(params).await,
|
"package.update" => self.clone().spawn_package_update(params).await,
|
||||||
|
"package.credentials" => self.handle_package_credentials(params).await,
|
||||||
"app.filebrowser-token" => self.handle_filebrowser_token().await,
|
"app.filebrowser-token" => self.handle_filebrowser_token().await,
|
||||||
|
|
||||||
// Bundled app management (for pre-loaded container images)
|
// Bundled app management (for pre-loaded container images)
|
||||||
|
|||||||
@ -1859,6 +1859,42 @@ autopilot.active=false\n",
|
|||||||
|
|
||||||
Ok(serde_json::json!({ "token": token }))
|
Ok(serde_json::json!({ "token": token }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(in crate::api::rpc) async fn handle_package_credentials(
|
||||||
|
&self,
|
||||||
|
params: Option<serde_json::Value>,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
let app_id = params
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|p| p.get("app_id"))
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.unwrap_or_default();
|
||||||
|
super::validation::validate_app_id(app_id)?;
|
||||||
|
|
||||||
|
match app_id {
|
||||||
|
"saleor" => {
|
||||||
|
let password =
|
||||||
|
tokio::fs::read_to_string("/var/lib/archipelago/secrets/saleor-admin-password")
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
.trim()
|
||||||
|
.to_string();
|
||||||
|
if password.is_empty() {
|
||||||
|
return Ok(serde_json::json!({ "credentials": [] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"title": "Saleor admin login",
|
||||||
|
"description": "Saleor opens to its own dashboard login. Use this generated admin account to sign in.",
|
||||||
|
"credentials": [
|
||||||
|
{ "label": "Email", "value": "admin@example.com", "sensitive": false },
|
||||||
|
{ "label": "Password", "value": password, "sensitive": true }
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
_ => Ok(serde_json::json!({ "credentials": [] })),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn cleanup_stale_package_ports(package_id: &str) {
|
async fn cleanup_stale_package_ports(package_id: &str) {
|
||||||
|
|||||||
@ -1929,6 +1929,17 @@ impl RpcHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let admin_script = format!(
|
||||||
|
r#"from django.contrib.auth import get_user_model
|
||||||
|
User = get_user_model()
|
||||||
|
user, _ = User.objects.get_or_create(email="admin@example.com", defaults={{"is_staff": True, "is_superuser": True}})
|
||||||
|
user.is_staff = True
|
||||||
|
user.is_superuser = True
|
||||||
|
user.set_password({:?})
|
||||||
|
user.save()
|
||||||
|
"#,
|
||||||
|
admin_pass
|
||||||
|
);
|
||||||
let mut admin_cmd = tokio::process::Command::new("podman");
|
let mut admin_cmd = tokio::process::Command::new("podman");
|
||||||
admin_cmd.args([
|
admin_cmd.args([
|
||||||
"run",
|
"run",
|
||||||
@ -1940,17 +1951,14 @@ impl RpcHandler {
|
|||||||
]);
|
]);
|
||||||
admin_cmd.args(&saleor_env);
|
admin_cmd.args(&saleor_env);
|
||||||
admin_cmd.args([
|
admin_cmd.args([
|
||||||
"-e",
|
|
||||||
"DJANGO_SUPERUSER_EMAIL=admin@example.com",
|
|
||||||
"-e",
|
|
||||||
&format!("DJANGO_SUPERUSER_PASSWORD={}", admin_pass),
|
|
||||||
SALEOR_API_IMAGE,
|
SALEOR_API_IMAGE,
|
||||||
"python3",
|
"python3",
|
||||||
"manage.py",
|
"manage.py",
|
||||||
"createsuperuser",
|
"shell",
|
||||||
"--noinput",
|
"-c",
|
||||||
|
&admin_script,
|
||||||
]);
|
]);
|
||||||
run_required_stack_command("saleor", "create admin user", &mut admin_cmd).await?;
|
run_required_stack_command("saleor", "create or update admin user", &mut admin_cmd).await?;
|
||||||
install_log("INSTALL INFO: saleor admin email admin@example.com; password stored in /var/lib/archipelago/secrets/saleor-admin-password").await;
|
install_log("INSTALL INFO: saleor admin email admin@example.com; password stored in /var/lib/archipelago/secrets/saleor-admin-password").await;
|
||||||
|
|
||||||
let mut api_cmd = tokio::process::Command::new("podman");
|
let mut api_cmd = tokio::process::Command::new("podman");
|
||||||
@ -2044,7 +2052,7 @@ impl RpcHandler {
|
|||||||
"-e",
|
"-e",
|
||||||
&format!("API_URL={}", api_url),
|
&format!("API_URL={}", api_url),
|
||||||
"-e",
|
"-e",
|
||||||
&format!("APP_MOUNT_URI={}", dashboard_url),
|
"APP_MOUNT_URI=/",
|
||||||
SALEOR_DASHBOARD_IMAGE,
|
SALEOR_DASHBOARD_IMAGE,
|
||||||
]);
|
]);
|
||||||
run_required_stack_command("saleor", "create dashboard", &mut dashboard_cmd).await?;
|
run_required_stack_command("saleor", "create dashboard", &mut dashboard_cmd).await?;
|
||||||
@ -2063,6 +2071,21 @@ impl RpcHandler {
|
|||||||
120,
|
120,
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||||
|
wait_for_stack_containers(
|
||||||
|
"saleor",
|
||||||
|
&[
|
||||||
|
"saleor-db",
|
||||||
|
"saleor-cache",
|
||||||
|
"saleor-jaeger",
|
||||||
|
"saleor-mailpit",
|
||||||
|
"saleor-api",
|
||||||
|
"saleor-worker",
|
||||||
|
"saleor",
|
||||||
|
],
|
||||||
|
30,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
self.set_install_phase("saleor", InstallPhase::WaitingHealthy)
|
self.set_install_phase("saleor", InstallPhase::WaitingHealthy)
|
||||||
.await;
|
.await;
|
||||||
@ -2117,7 +2140,7 @@ async fn write_netbird_config_files(host_ip: &str) -> Result<()> {
|
|||||||
auth:
|
auth:
|
||||||
issuer: "{public_origin}/oauth2"
|
issuer: "{public_origin}/oauth2"
|
||||||
localAuthDisabled: false
|
localAuthDisabled: false
|
||||||
signKeyRefreshEnabled: true
|
signKeyRefreshEnabled: false
|
||||||
dashboardRedirectURIs:
|
dashboardRedirectURIs:
|
||||||
- "{public_origin}/nb-auth"
|
- "{public_origin}/nb-auth"
|
||||||
- "{public_origin}/nb-silent-auth"
|
- "{public_origin}/nb-silent-auth"
|
||||||
@ -2145,7 +2168,7 @@ USE_AUTH0=false
|
|||||||
AUTH_SUPPORTED_SCOPES=openid profile email groups
|
AUTH_SUPPORTED_SCOPES=openid profile email groups
|
||||||
AUTH_REDIRECT_URI=/nb-auth
|
AUTH_REDIRECT_URI=/nb-auth
|
||||||
AUTH_SILENT_REDIRECT_URI=/nb-silent-auth
|
AUTH_SILENT_REDIRECT_URI=/nb-silent-auth
|
||||||
NETBIRD_TOKEN_SOURCE=accessToken
|
NETBIRD_TOKEN_SOURCE=idToken
|
||||||
NGINX_SSL_PORT=443
|
NGINX_SSL_PORT=443
|
||||||
LETSENCRYPT_DOMAIN=none
|
LETSENCRYPT_DOMAIN=none
|
||||||
"#
|
"#
|
||||||
|
|||||||
@ -125,6 +125,11 @@ impl DockerPackageScanner {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if is_transient_podman_helper(&app_id, &container.ports) {
|
||||||
|
debug!("Skipping transient Podman helper container: {}", app_id);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip podman-compose infrastructure containers (e.g. indeedhub-build_api_1)
|
// Skip podman-compose infrastructure containers (e.g. indeedhub-build_api_1)
|
||||||
// These have the project prefix pattern: {project}_{service}_{instance}
|
// These have the project prefix pattern: {project}_{service}_{instance}
|
||||||
if app_id.starts_with("indeedhub-build_") {
|
if app_id.starts_with("indeedhub-build_") {
|
||||||
@ -299,6 +304,21 @@ fn get_app_tier(app_id: &str) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_transient_podman_helper(app_id: &str, ports: &[String]) -> bool {
|
||||||
|
if !ports.is_empty() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some((left, right)) = app_id.split_once('_') else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
!left.is_empty()
|
||||||
|
&& !right.is_empty()
|
||||||
|
&& left.chars().all(|c| c.is_ascii_lowercase())
|
||||||
|
&& right.chars().all(|c| c.is_ascii_lowercase())
|
||||||
|
}
|
||||||
|
|
||||||
fn get_app_metadata(app_id: &str) -> AppMetadata {
|
fn get_app_metadata(app_id: &str) -> AppMetadata {
|
||||||
let mut meta = match app_id {
|
let mut meta = match app_id {
|
||||||
"bitcoin-core" => AppMetadata {
|
"bitcoin-core" => AppMetadata {
|
||||||
@ -499,7 +519,7 @@ fn get_app_metadata(app_id: &str) -> AppMetadata {
|
|||||||
},
|
},
|
||||||
"saleor" => AppMetadata {
|
"saleor" => AppMetadata {
|
||||||
title: "Saleor".to_string(),
|
title: "Saleor".to_string(),
|
||||||
description: "Composable commerce platform with GraphQL API and dashboard. Admin email: admin@example.com; password is stored on the node at /var/lib/archipelago/secrets/saleor-admin-password".to_string(),
|
description: "Composable commerce platform with GraphQL API and dashboard. Log in with admin@example.com; the password is stored on the node at /var/lib/archipelago/secrets/saleor-admin-password".to_string(),
|
||||||
icon: "/assets/img/app-icons/saleor.svg".to_string(),
|
icon: "/assets/img/app-icons/saleor.svg".to_string(),
|
||||||
repo: "https://github.com/saleor/saleor".to_string(),
|
repo: "https://github.com/saleor/saleor".to_string(),
|
||||||
tier: "",
|
tier: "",
|
||||||
|
|||||||
@ -140,6 +140,18 @@ export interface InterfaceAddress {
|
|||||||
'lan-address': string | null
|
'lan-address': string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppCredential {
|
||||||
|
label: string
|
||||||
|
value: string
|
||||||
|
sensitive?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppCredentialsResponse {
|
||||||
|
title?: string
|
||||||
|
description?: string
|
||||||
|
credentials: AppCredential[]
|
||||||
|
}
|
||||||
|
|
||||||
export const ServiceStatus = {
|
export const ServiceStatus = {
|
||||||
Stopped: 'stopped',
|
Stopped: 'stopped',
|
||||||
Starting: 'starting',
|
Starting: 'starting',
|
||||||
@ -275,4 +287,3 @@ export interface Update {
|
|||||||
sequence: number
|
sequence: number
|
||||||
patch: PatchOperation[]
|
patch: PatchOperation[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -56,6 +56,7 @@
|
|||||||
:lan-url="lanUrl"
|
:lan-url="lanUrl"
|
||||||
:tor-url="torUrl"
|
:tor-url="torUrl"
|
||||||
:show-tor-address="showTorAddress"
|
:show-tor-address="showTorAddress"
|
||||||
|
:credentials="credentials"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -137,6 +138,7 @@ import { useAppLauncherStore } from '../stores/appLauncher'
|
|||||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||||
import { dummyApps } from '../utils/dummyApps'
|
import { dummyApps } from '../utils/dummyApps'
|
||||||
import { rpcClient } from '@/api/rpc-client'
|
import { rpcClient } from '@/api/rpc-client'
|
||||||
|
import type { AppCredentialsResponse } from '@/types/api'
|
||||||
import AppHeroSection from './appDetails/AppHeroSection.vue'
|
import AppHeroSection from './appDetails/AppHeroSection.vue'
|
||||||
import AppContentSection from './appDetails/AppContentSection.vue'
|
import AppContentSection from './appDetails/AppContentSection.vue'
|
||||||
import AppSidebar from './appDetails/AppSidebar.vue'
|
import AppSidebar from './appDetails/AppSidebar.vue'
|
||||||
@ -214,6 +216,7 @@ const needsBitcoinSync = computed(() => BITCOIN_DEPENDENT_APPS.includes(packageK
|
|||||||
const bitcoinSyncPercent = ref(0)
|
const bitcoinSyncPercent = ref(0)
|
||||||
const bitcoinBlockHeight = ref(0)
|
const bitcoinBlockHeight = ref(0)
|
||||||
const bitcoinSynced = computed(() => bitcoinSyncPercent.value >= 99.9)
|
const bitcoinSynced = computed(() => bitcoinSyncPercent.value >= 99.9)
|
||||||
|
const credentials = ref<AppCredentialsResponse | null>(null)
|
||||||
|
|
||||||
async function loadBitcoinSync() {
|
async function loadBitcoinSync() {
|
||||||
if (!needsBitcoinSync.value) return
|
if (!needsBitcoinSync.value) return
|
||||||
@ -230,8 +233,23 @@ async function loadBitcoinSync() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadCredentials() {
|
||||||
|
if (!appId.value) return
|
||||||
|
try {
|
||||||
|
const result = await rpcClient.call<AppCredentialsResponse>({
|
||||||
|
method: 'package.credentials',
|
||||||
|
params: { app_id: packageKey.value },
|
||||||
|
timeout: 5000,
|
||||||
|
})
|
||||||
|
credentials.value = result.credentials?.length ? result : null
|
||||||
|
} catch {
|
||||||
|
credentials.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadBitcoinSync()
|
loadBitcoinSync()
|
||||||
|
loadCredentials()
|
||||||
})
|
})
|
||||||
|
|
||||||
const actionError = ref('')
|
const actionError = ref('')
|
||||||
|
|||||||
@ -173,6 +173,37 @@
|
|||||||
@confirm="onConfirmUninstall"
|
@confirm="onConfirmUninstall"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Transition name="fade">
|
||||||
|
<div
|
||||||
|
v-if="credentialModal.show"
|
||||||
|
class="fixed inset-0 z-[2700] flex items-end justify-center bg-black/60 backdrop-blur-md p-0 md:items-center md:p-6"
|
||||||
|
@click.self="closeCredentialModal"
|
||||||
|
>
|
||||||
|
<div class="sideload-modal">
|
||||||
|
<div class="flex items-start justify-between gap-4 mb-5">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-white">{{ credentialModal.title }}</h2>
|
||||||
|
<p class="text-sm text-white/55 mt-1">{{ credentialModal.description }}</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="sideload-close-btn" aria-label="Close" @click="closeCredentialModal">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div v-for="cred in credentialModal.credentials" :key="cred.label" class="rounded-lg border border-white/10 bg-white/[0.04] p-3">
|
||||||
|
<div class="flex items-center justify-between gap-3 mb-1">
|
||||||
|
<span class="text-white/60 text-xs uppercase tracking-wide">{{ cred.label }}</span>
|
||||||
|
<button type="button" class="text-xs text-blue-300 hover:text-blue-200" @click="copyModalCredential(cred.label, cred.value)">{{ credentialModal.copied === cred.label ? 'Copied' : 'Copy' }}</button>
|
||||||
|
</div>
|
||||||
|
<p class="font-mono text-sm text-white break-all">{{ cred.value }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 flex gap-3">
|
||||||
|
<button type="button" class="flex-1 glass-button px-4 py-3 rounded-lg" @click="closeCredentialModal">Cancel</button>
|
||||||
|
<button type="button" class="flex-1 glass-button px-4 py-3 rounded-lg font-semibold" @click="continueCredentialLaunch">Continue to app</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
|
||||||
<Transition name="fade">
|
<Transition name="fade">
|
||||||
<div
|
<div
|
||||||
v-if="showSideload"
|
v-if="showSideload"
|
||||||
@ -255,7 +286,7 @@ import { useAppStore } from '@/stores/app'
|
|||||||
import { useServerStore } from '@/stores/server'
|
import { useServerStore } from '@/stores/server'
|
||||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||||
import { rpcClient } from '@/api/rpc-client'
|
import { rpcClient } from '@/api/rpc-client'
|
||||||
import { type PackageDataEntry, type PackageState } from '@/types/api'
|
import { type AppCredential, type AppCredentialsResponse, type PackageDataEntry, type PackageState } from '@/types/api'
|
||||||
import AppCard from './apps/AppCard.vue'
|
import AppCard from './apps/AppCard.vue'
|
||||||
import AppIconGrid from './apps/AppIconGrid.vue'
|
import AppIconGrid from './apps/AppIconGrid.vue'
|
||||||
import AppsUninstallModal from './apps/AppsUninstallModal.vue'
|
import AppsUninstallModal from './apps/AppsUninstallModal.vue'
|
||||||
@ -284,6 +315,14 @@ const sideloadForm = ref({
|
|||||||
port: '',
|
port: '',
|
||||||
description: '',
|
description: '',
|
||||||
})
|
})
|
||||||
|
const credentialModal = ref({
|
||||||
|
show: false,
|
||||||
|
appId: '',
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
credentials: [] as AppCredential[],
|
||||||
|
copied: '',
|
||||||
|
})
|
||||||
|
|
||||||
// Only stagger-animate on first mount
|
// Only stagger-animate on first mount
|
||||||
const showStagger = !appsAnimationDone
|
const showStagger = !appsAnimationDone
|
||||||
@ -431,7 +470,13 @@ function goToApp(id: string) {
|
|||||||
router.push(`/dashboard/apps/${id}`).catch(() => {})
|
router.push(`/dashboard/apps/${id}`).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
function launchApp(id: string) {
|
async function launchApp(id: string) {
|
||||||
|
const shown = await maybeShowCredentialsBeforeLaunch(id)
|
||||||
|
if (shown) return
|
||||||
|
launchAppNow(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchAppNow(id: string) {
|
||||||
const pkg = packages.value[id]
|
const pkg = packages.value[id]
|
||||||
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
|
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
|
||||||
const webOnlyUrl = WEB_ONLY_APP_URLS[id]
|
const webOnlyUrl = WEB_ONLY_APP_URLS[id]
|
||||||
@ -456,6 +501,52 @@ function launchApp(id: string) {
|
|||||||
useAppLauncherStore().openSession(id)
|
useAppLauncherStore().openSession(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function maybeShowCredentialsBeforeLaunch(id: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await rpcClient.call<AppCredentialsResponse>({
|
||||||
|
method: 'package.credentials',
|
||||||
|
params: { app_id: id },
|
||||||
|
timeout: 5000,
|
||||||
|
})
|
||||||
|
if (!result.credentials?.length) return false
|
||||||
|
credentialModal.value = {
|
||||||
|
show: true,
|
||||||
|
appId: id,
|
||||||
|
title: result.title || `${packages.value[id]?.manifest.title || id} credentials`,
|
||||||
|
description: result.description || 'Use these credentials when the app asks you to sign in.',
|
||||||
|
credentials: result.credentials,
|
||||||
|
copied: '',
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCredentialModal() {
|
||||||
|
credentialModal.value.show = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function continueCredentialLaunch() {
|
||||||
|
const id = credentialModal.value.appId
|
||||||
|
closeCredentialModal()
|
||||||
|
if (id) launchAppNow(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyModalCredential(label: string, value: string) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(value)
|
||||||
|
} catch {
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.value = value
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
}
|
||||||
|
credentialModal.value.copied = label
|
||||||
|
}
|
||||||
|
|
||||||
async function updateApp(id: string) {
|
async function updateApp(id: string) {
|
||||||
try {
|
try {
|
||||||
await serverStore.updatePackage(id)
|
await serverStore.updatePackage(id)
|
||||||
|
|||||||
@ -86,6 +86,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="credentials?.credentials?.length" class="glass-card p-6">
|
||||||
|
<h3 class="text-lg font-bold text-white mb-2">Credentials</h3>
|
||||||
|
<p v-if="credentials.description" class="text-sm text-white/60 mb-4">{{ credentials.description }}</p>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div v-for="cred in credentials.credentials" :key="cred.label" class="rounded-lg border border-white/10 bg-white/[0.04] p-3">
|
||||||
|
<div class="flex items-center justify-between gap-3 mb-1">
|
||||||
|
<span class="text-white/60 text-xs uppercase tracking-wide">{{ cred.label }}</span>
|
||||||
|
<button type="button" class="text-xs text-blue-300 hover:text-blue-200" @click="copyCredential(cred.label, cred.value)">
|
||||||
|
{{ copiedCredential === cred.label ? 'Copied' : 'Copy' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="font-mono text-sm text-white break-all">{{ cred.value }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Requirements Card (hidden for web-only apps) -->
|
<!-- Requirements Card (hidden for web-only apps) -->
|
||||||
<div v-if="!isWebOnly" class="glass-card p-6">
|
<div v-if="!isWebOnly" class="glass-card p-6">
|
||||||
<h3 class="text-lg font-bold text-white mb-4">{{ t('appDetails.requirements') }}</h3>
|
<h3 class="text-lg font-bold text-white mb-4">{{ t('appDetails.requirements') }}</h3>
|
||||||
@ -151,9 +167,29 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
import type { AppCredentialsResponse } from '@/types/api'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
const copiedCredential = ref('')
|
||||||
|
|
||||||
|
async function copyCredential(label: string, value: string) {
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(value)
|
||||||
|
} catch {
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.value = value
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
}
|
||||||
|
copiedCredential.value = label
|
||||||
|
setTimeout(() => {
|
||||||
|
if (copiedCredential.value === label) copiedCredential.value = ''
|
||||||
|
}, 1800)
|
||||||
|
}
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
pkg: Record<string, any>
|
pkg: Record<string, any>
|
||||||
@ -164,5 +200,6 @@ defineProps<{
|
|||||||
lanUrl: string
|
lanUrl: string
|
||||||
torUrl: string
|
torUrl: string
|
||||||
showTorAddress: boolean
|
showTorAddress: boolean
|
||||||
|
credentials: AppCredentialsResponse | null
|
||||||
}>()
|
}>()
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -70,6 +70,33 @@
|
|||||||
@click="scrollToPage(i)"
|
@click="scrollToPage(i)"
|
||||||
></button>
|
></button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Transition name="fade">
|
||||||
|
<div v-if="credentialModal.show" class="fixed inset-0 z-[2700] flex items-end justify-center bg-black/60 backdrop-blur-md p-0 md:items-center md:p-6" @click.self="closeCredentialModal">
|
||||||
|
<div class="sideload-modal">
|
||||||
|
<div class="flex items-start justify-between gap-4 mb-5">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-white">{{ credentialModal.title }}</h2>
|
||||||
|
<p class="text-sm text-white/55 mt-1">{{ credentialModal.description }}</p>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="sideload-close-btn" aria-label="Close" @click="closeCredentialModal">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div v-for="cred in credentialModal.credentials" :key="cred.label" class="rounded-lg border border-white/10 bg-white/[0.04] p-3">
|
||||||
|
<div class="flex items-center justify-between gap-3 mb-1">
|
||||||
|
<span class="text-white/60 text-xs uppercase tracking-wide">{{ cred.label }}</span>
|
||||||
|
<button type="button" class="text-xs text-blue-300 hover:text-blue-200" @click="copyModalCredential(cred.label, cred.value)">{{ credentialModal.copied === cred.label ? 'Copied' : 'Copy' }}</button>
|
||||||
|
</div>
|
||||||
|
<p class="font-mono text-sm text-white break-all">{{ cred.value }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 flex gap-3">
|
||||||
|
<button type="button" class="flex-1 glass-button px-4 py-3 rounded-lg" @click="closeCredentialModal">Cancel</button>
|
||||||
|
<button type="button" class="flex-1 glass-button px-4 py-3 rounded-lg font-semibold" @click="continueCredentialLaunch">Continue to app</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -77,7 +104,8 @@
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useServerStore } from '@/stores/server'
|
import { useServerStore } from '@/stores/server'
|
||||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||||
import type { PackageDataEntry } from '@/types/api'
|
import type { AppCredential, AppCredentialsResponse, PackageDataEntry } from '@/types/api'
|
||||||
|
import { rpcClient } from '@/api/rpc-client'
|
||||||
import { resolveAppUrl } from '@/views/appSession/appSessionConfig'
|
import { resolveAppUrl } from '@/views/appSession/appSessionConfig'
|
||||||
import { canLaunch, handleImageError, isWebsitePackage, opensInTab, resolveAppIcon, resolveRuntimeLaunchUrl, WEB_ONLY_APP_URLS } from './appsConfig'
|
import { canLaunch, handleImageError, isWebsitePackage, opensInTab, resolveAppIcon, resolveRuntimeLaunchUrl, WEB_ONLY_APP_URLS } from './appsConfig'
|
||||||
import { getCuratedAppList } from '../discover/curatedApps'
|
import { getCuratedAppList } from '../discover/curatedApps'
|
||||||
@ -88,6 +116,14 @@ const serverStore = useServerStore()
|
|||||||
const appLauncher = useAppLauncherStore()
|
const appLauncher = useAppLauncherStore()
|
||||||
|
|
||||||
const curatedMap = new Map(getCuratedAppList().map(a => [a.id, a]))
|
const curatedMap = new Map(getCuratedAppList().map(a => [a.id, a]))
|
||||||
|
const credentialModal = ref({
|
||||||
|
show: false,
|
||||||
|
appId: '',
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
credentials: [] as AppCredential[],
|
||||||
|
copied: '',
|
||||||
|
})
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
apps: [string, PackageDataEntry][]
|
apps: [string, PackageDataEntry][]
|
||||||
@ -118,34 +154,79 @@ function getIcon(id: string, pkg: PackageDataEntry): string {
|
|||||||
return resolveAppIcon(id, pkg, curatedMap.get(id)?.icon)
|
return resolveAppIcon(id, pkg, curatedMap.get(id)?.icon)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTap(id: string, pkg: PackageDataEntry) {
|
async function handleTap(id: string, pkg: PackageDataEntry) {
|
||||||
if (canLaunch(pkg)) {
|
if (canLaunch(pkg)) {
|
||||||
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
|
const shown = await maybeShowCredentialsBeforeLaunch(id, pkg)
|
||||||
const webOnlyUrl = WEB_ONLY_APP_URLS[id]
|
if (shown) return
|
||||||
if (webOnlyUrl) {
|
launchNow(id, pkg)
|
||||||
appLauncher.open({ url: webOnlyUrl, title: getTitle(id, pkg), openInNewTab: !isMobile })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (isWebsitePackage(id, pkg)) {
|
|
||||||
const url = resolveRuntimeLaunchUrl(pkg)
|
|
||||||
if (url) {
|
|
||||||
appLauncher.open({ url, title: getTitle(id, pkg), openInNewTab: !isMobile })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isMobile && opensInTab(id)) {
|
|
||||||
const appUrl = resolveRuntimeLaunchUrl(pkg) || resolveAppUrl(id)
|
|
||||||
if (appUrl) {
|
|
||||||
window.open(appUrl, '_blank', 'noopener,noreferrer')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
appLauncher.openSession(id)
|
|
||||||
} else {
|
} else {
|
||||||
emit('goToApp', id)
|
emit('goToApp', id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function launchNow(id: string, pkg: PackageDataEntry) {
|
||||||
|
const isMobile = typeof window !== 'undefined' && window.innerWidth < 768
|
||||||
|
const webOnlyUrl = WEB_ONLY_APP_URLS[id]
|
||||||
|
if (webOnlyUrl) {
|
||||||
|
appLauncher.open({ url: webOnlyUrl, title: getTitle(id, pkg), openInNewTab: !isMobile })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isWebsitePackage(id, pkg)) {
|
||||||
|
const url = resolveRuntimeLaunchUrl(pkg)
|
||||||
|
if (url) {
|
||||||
|
appLauncher.open({ url, title: getTitle(id, pkg), openInNewTab: !isMobile })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isMobile && opensInTab(id)) {
|
||||||
|
const appUrl = resolveRuntimeLaunchUrl(pkg) || resolveAppUrl(id)
|
||||||
|
if (appUrl) {
|
||||||
|
window.open(appUrl, '_blank', 'noopener,noreferrer')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appLauncher.openSession(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function maybeShowCredentialsBeforeLaunch(id: string, pkg: PackageDataEntry): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const result = await rpcClient.call<AppCredentialsResponse>({ method: 'package.credentials', params: { app_id: id }, timeout: 5000 })
|
||||||
|
if (!result.credentials?.length) return false
|
||||||
|
credentialModal.value = {
|
||||||
|
show: true,
|
||||||
|
appId: id,
|
||||||
|
title: result.title || `${getTitle(id, pkg)} credentials`,
|
||||||
|
description: result.description || 'Use these credentials when the app asks you to sign in.',
|
||||||
|
credentials: result.credentials,
|
||||||
|
copied: '',
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeCredentialModal() { credentialModal.value.show = false }
|
||||||
|
|
||||||
|
function continueCredentialLaunch() {
|
||||||
|
const id = credentialModal.value.appId
|
||||||
|
const entry = props.apps.find(([appId]) => appId === id)
|
||||||
|
closeCredentialModal()
|
||||||
|
if (entry) launchNow(entry[0], entry[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyModalCredential(label: string, value: string) {
|
||||||
|
try { await navigator.clipboard.writeText(value) } catch {
|
||||||
|
const textarea = document.createElement('textarea')
|
||||||
|
textarea.value = value
|
||||||
|
document.body.appendChild(textarea)
|
||||||
|
textarea.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(textarea)
|
||||||
|
}
|
||||||
|
credentialModal.value.copied = label
|
||||||
|
}
|
||||||
|
|
||||||
function onScroll() {
|
function onScroll() {
|
||||||
const el = scrollContainer.value
|
const el = scrollContainer.value
|
||||||
if (!el) return
|
if (!el) return
|
||||||
|
|||||||
@ -79,7 +79,7 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
|||||||
{ id: 'bitcoin-knots', title: 'Bitcoin Knots', version: '28.1.0', description: 'Run a full Bitcoin node. Validate and relay blocks and transactions on the Bitcoin network.', icon: '/assets/img/app-icons/bitcoin-knots.webp', author: 'Bitcoin Knots', dockerImage: `${R}/bitcoin-knots:latest`, repoUrl: 'https://github.com/bitcoinknots/bitcoin' },
|
{ id: 'bitcoin-knots', title: 'Bitcoin Knots', version: '28.1.0', description: 'Run a full Bitcoin node. Validate and relay blocks and transactions on the Bitcoin network.', icon: '/assets/img/app-icons/bitcoin-knots.webp', author: 'Bitcoin Knots', dockerImage: `${R}/bitcoin-knots:latest`, repoUrl: 'https://github.com/bitcoinknots/bitcoin' },
|
||||||
{ id: 'bitcoin-core', title: 'Bitcoin Core', version: '28.4', description: 'Reference implementation of the Bitcoin protocol. Run a full node validating and relaying blocks on the Bitcoin network.', icon: '/assets/img/app-icons/bitcoin-core.svg', author: 'Bitcoin Core contributors', dockerImage: 'docker.io/bitcoin/bitcoin:28.4', repoUrl: 'https://github.com/bitcoin/bitcoin' },
|
{ id: 'bitcoin-core', title: 'Bitcoin Core', version: '28.4', description: 'Reference implementation of the Bitcoin protocol. Run a full node validating and relaying blocks on the Bitcoin network.', icon: '/assets/img/app-icons/bitcoin-core.svg', author: 'Bitcoin Core contributors', dockerImage: 'docker.io/bitcoin/bitcoin:28.4', repoUrl: 'https://github.com/bitcoin/bitcoin' },
|
||||||
{ id: 'btcpay-server', title: 'BTCPay Server', version: '2.3.9', description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.', icon: '/assets/img/app-icons/btcpay-server.png', author: 'BTCPay Server Foundation', dockerImage: 'docker.io/btcpayserver/btcpayserver:2.3.9', repoUrl: 'https://github.com/btcpayserver/btcpayserver' },
|
{ id: 'btcpay-server', title: 'BTCPay Server', version: '2.3.9', description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.', icon: '/assets/img/app-icons/btcpay-server.png', author: 'BTCPay Server Foundation', dockerImage: 'docker.io/btcpayserver/btcpayserver:2.3.9', repoUrl: 'https://github.com/btcpayserver/btcpayserver' },
|
||||||
{ id: 'saleor', title: 'Saleor', version: '3.23', category: 'commerce', description: 'Composable commerce platform with GraphQL API, dashboard, worker, mail testing, and tracing.', icon: '/assets/img/app-icons/saleor.svg', author: 'Saleor', dockerImage: 'ghcr.io/saleor/saleor:3.23', repoUrl: 'https://github.com/saleor/saleor' },
|
{ id: 'saleor', title: 'Saleor', version: '3.23', category: 'commerce', description: 'Composable commerce platform with GraphQL API, dashboard, worker, mail testing, and tracing. Log in with admin@example.com; the password is stored on the node.', icon: '/assets/img/app-icons/saleor.svg', author: 'Saleor', dockerImage: 'ghcr.io/saleor/saleor:3.23', repoUrl: 'https://github.com/saleor/saleor' },
|
||||||
{ id: 'lnd', title: 'LND', version: '0.18.4', description: 'Lightning Network Daemon. Fast and cheap Bitcoin payments through the Lightning Network.', icon: '/assets/img/app-icons/lnd.svg', author: 'Lightning Labs', dockerImage: `${R}/lnd:v0.18.4-beta`, repoUrl: 'https://github.com/lightningnetwork/lnd' },
|
{ id: 'lnd', title: 'LND', version: '0.18.4', description: 'Lightning Network Daemon. Fast and cheap Bitcoin payments through the Lightning Network.', icon: '/assets/img/app-icons/lnd.svg', author: 'Lightning Labs', dockerImage: `${R}/lnd:v0.18.4-beta`, repoUrl: 'https://github.com/lightningnetwork/lnd' },
|
||||||
{ id: 'mempool', title: 'Mempool Explorer', version: '3.0.0', description: 'Self-hosted Bitcoin blockchain and mempool visualizer. Monitor transactions without revealing your addresses to third parties.', icon: '/assets/img/app-icons/mempool.webp', author: 'Mempool', dockerImage: `${R}/mempool-frontend:v3.0.0`, repoUrl: 'https://github.com/mempool/mempool' },
|
{ id: 'mempool', title: 'Mempool Explorer', version: '3.0.0', description: 'Self-hosted Bitcoin blockchain and mempool visualizer. Monitor transactions without revealing your addresses to third parties.', icon: '/assets/img/app-icons/mempool.webp', author: 'Mempool', dockerImage: `${R}/mempool-frontend:v3.0.0`, repoUrl: 'https://github.com/mempool/mempool' },
|
||||||
{ id: 'homeassistant', title: 'Home Assistant', version: '2024.1', description: 'Open-source home automation. Control smart home devices privately, on your own hardware.', icon: '/assets/img/app-icons/homeassistant.png', author: 'Home Assistant', dockerImage: `${R}/home-assistant:2024.1`, repoUrl: 'https://github.com/home-assistant/core' },
|
{ id: 'homeassistant', title: 'Home Assistant', version: '2024.1', description: 'Open-source home automation. Control smart home devices privately, on your own hardware.', icon: '/assets/img/app-icons/homeassistant.png', author: 'Home Assistant', dockerImage: `${R}/home-assistant:2024.1`, repoUrl: 'https://github.com/home-assistant/core' },
|
||||||
|
|||||||
@ -187,8 +187,10 @@ init()
|
|||||||
<span class="text-xs text-white/40">May 20, 2026</span>
|
<span class="text-xs text-white/40">May 20, 2026</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
<p>Saleor now installs on dashboard port 9010 instead of conflicting with Portainer on 9000, keeps its API on 8000, and starts cleanly in rootless Podman with the nginx capabilities it needs.</p>
|
<p>Saleor now installs on dashboard port 9010 instead of conflicting with Portainer on 9000, keeps its API on 8000, creates or repairs the admin@example.com staff account after sample data loads, and starts cleanly with the correct dashboard mount path.</p>
|
||||||
<p>NetBird API and OAuth routes now proxy through the stable host-published server port, so login/logout routes survive a netbird-server restart without 502s from stale Podman DNS.</p>
|
<p>Apps with generated first-use credentials now show them in Archipelago before launch and in App Details. Saleor exposes copyable admin email/password fields so the dashboard login screen is no longer a dead end.</p>
|
||||||
|
<p>NetBird API and OAuth routes now proxy through the stable host-published server port, and the embedded IdP keeps upstream-compatible signing-key refresh settings while the dashboard sends ID tokens to the API so signup no longer lands in an Unauthenticated dashboard state.</p>
|
||||||
|
<p>Transient unnamed Podman helper containers created during app installs are hidden from My Apps, so random generated names like eager_keldysh no longer appear as applications.</p>
|
||||||
<p>Mobile App Store categories are now visible as horizontal chips above the tab bar, Discover is reachable on mobile, category choices update the actual view, and apps that require a real tab open directly from the icon tap.</p>
|
<p>Mobile App Store categories are now visible as horizontal chips above the tab bar, Discover is reachable on mobile, category choices update the actual view, and apps that require a real tab open directly from the icon tap.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -4,7 +4,11 @@
|
|||||||
"changelog": [
|
"changelog": [
|
||||||
"Saleor installs now use dashboard port `9010`, avoiding the existing Portainer `9000` binding on the test node while keeping API `8000`, Mailpit `8025`, and Jaeger `16686` unchanged.",
|
"Saleor installs now use dashboard port `9010`, avoiding the existing Portainer `9000` binding on the test node while keeping API `8000`, Mailpit `8025`, and Jaeger `16686` unchanged.",
|
||||||
"Saleor's Valkey cache no longer bind-mounts `/var/lib/archipelago/saleor-cache`, and the dashboard container has the minimal rootless nginx capabilities it needs to chown cache files, bind port 80 inside the container, and drop workers to the nginx user.",
|
"Saleor's Valkey cache no longer bind-mounts `/var/lib/archipelago/saleor-cache`, and the dashboard container has the minimal rootless nginx capabilities it needs to chown cache files, bind port 80 inside the container, and drop workers to the nginx user.",
|
||||||
|
"Saleor installs now create or repair the `admin@example.com` staff/superuser account idempotently after sample data loads, publish the correct dashboard mount path, and document where the generated admin password is stored so first-use login no longer blocks users.",
|
||||||
|
"Apps with generated first-use credentials now expose those credentials through Archipelago: Saleor shows copyable admin email/password fields in the app details Access area and in a pre-launch modal before opening the Saleor dashboard.",
|
||||||
"NetBird's browser proxy now sends API, OAuth, relay, WebSocket, and management traffic through the stable host-published server port at `169.254.1.2:8086`, avoiding stale rootless Podman DNS/IPs after `netbird-server` restarts.",
|
"NetBird's browser proxy now sends API, OAuth, relay, WebSocket, and management traffic through the stable host-published server port at `169.254.1.2:8086`, avoiding stale rootless Podman DNS/IPs after `netbird-server` restarts.",
|
||||||
|
"NetBird's embedded IdP keeps signing-key refresh disabled like the upstream quickstart config and the dashboard now uses the ID token for API calls, preventing newly-created local users from landing in an Unauthenticated dashboard state after signup.",
|
||||||
|
"Transient unnamed Podman helper containers created during app install tasks are hidden from My Apps, so random generated names like `eager_keldysh` no longer appear as user applications.",
|
||||||
"Mobile App Store category chips now stay visible above the tab bar, Discover is available on mobile, and category selection updates the page route/query so the selected category is actually shown.",
|
"Mobile App Store category chips now stay visible above the tab bar, Discover is available on mobile, and category selection updates the page route/query so the selected category is actually shown.",
|
||||||
"Apps that require a real browser tab now open directly from the app icon tap instead of first entering an in-shell app-session route, including BTCPay, Grafana, Home Assistant, Vaultwarden, Nextcloud, Portainer, OnlyOffice, Tailscale, Uptime Kuma, Gitea, and Nginx Proxy Manager.",
|
"Apps that require a real browser tab now open directly from the app icon tap instead of first entering an in-shell app-session route, including BTCPay, Grafana, Home Assistant, Vaultwarden, Nextcloud, Portainer, OnlyOffice, Tailscale, Uptime Kuma, Gitea, and Nginx Proxy Manager.",
|
||||||
"Validation passed with catalog JSON checks, `npm run type-check`, `cargo fmt --all --check --manifest-path core/Cargo.toml`, and `cargo check -p archipelago --manifest-path core/Cargo.toml`; live checks on `100.70.96.88` confirmed Saleor dashboard `9010`/API `8000` and NetBird API/OAuth routes survive `netbird-server` restart."
|
"Validation passed with catalog JSON checks, `npm run type-check`, `cargo fmt --all --check --manifest-path core/Cargo.toml`, and `cargo check -p archipelago --manifest-path core/Cargo.toml`; live checks on `100.70.96.88` confirmed Saleor dashboard `9010`/API `8000` and NetBird API/OAuth routes survive `netbird-server` restart."
|
||||||
|
|||||||
@ -4,7 +4,11 @@
|
|||||||
"changelog": [
|
"changelog": [
|
||||||
"Saleor installs now use dashboard port `9010`, avoiding the existing Portainer `9000` binding on the test node while keeping API `8000`, Mailpit `8025`, and Jaeger `16686` unchanged.",
|
"Saleor installs now use dashboard port `9010`, avoiding the existing Portainer `9000` binding on the test node while keeping API `8000`, Mailpit `8025`, and Jaeger `16686` unchanged.",
|
||||||
"Saleor's Valkey cache no longer bind-mounts `/var/lib/archipelago/saleor-cache`, and the dashboard container has the minimal rootless nginx capabilities it needs to chown cache files, bind port 80 inside the container, and drop workers to the nginx user.",
|
"Saleor's Valkey cache no longer bind-mounts `/var/lib/archipelago/saleor-cache`, and the dashboard container has the minimal rootless nginx capabilities it needs to chown cache files, bind port 80 inside the container, and drop workers to the nginx user.",
|
||||||
|
"Saleor installs now create or repair the `admin@example.com` staff/superuser account idempotently after sample data loads, publish the correct dashboard mount path, and document where the generated admin password is stored so first-use login no longer blocks users.",
|
||||||
|
"Apps with generated first-use credentials now expose those credentials through Archipelago: Saleor shows copyable admin email/password fields in the app details Access area and in a pre-launch modal before opening the Saleor dashboard.",
|
||||||
"NetBird's browser proxy now sends API, OAuth, relay, WebSocket, and management traffic through the stable host-published server port at `169.254.1.2:8086`, avoiding stale rootless Podman DNS/IPs after `netbird-server` restarts.",
|
"NetBird's browser proxy now sends API, OAuth, relay, WebSocket, and management traffic through the stable host-published server port at `169.254.1.2:8086`, avoiding stale rootless Podman DNS/IPs after `netbird-server` restarts.",
|
||||||
|
"NetBird's embedded IdP keeps signing-key refresh disabled like the upstream quickstart config and the dashboard now uses the ID token for API calls, preventing newly-created local users from landing in an Unauthenticated dashboard state after signup.",
|
||||||
|
"Transient unnamed Podman helper containers created during app install tasks are hidden from My Apps, so random generated names like `eager_keldysh` no longer appear as user applications.",
|
||||||
"Mobile App Store category chips now stay visible above the tab bar, Discover is available on mobile, and category selection updates the page route/query so the selected category is actually shown.",
|
"Mobile App Store category chips now stay visible above the tab bar, Discover is available on mobile, and category selection updates the page route/query so the selected category is actually shown.",
|
||||||
"Apps that require a real browser tab now open directly from the app icon tap instead of first entering an in-shell app-session route, including BTCPay, Grafana, Home Assistant, Vaultwarden, Nextcloud, Portainer, OnlyOffice, Tailscale, Uptime Kuma, Gitea, and Nginx Proxy Manager.",
|
"Apps that require a real browser tab now open directly from the app icon tap instead of first entering an in-shell app-session route, including BTCPay, Grafana, Home Assistant, Vaultwarden, Nextcloud, Portainer, OnlyOffice, Tailscale, Uptime Kuma, Gitea, and Nginx Proxy Manager.",
|
||||||
"Validation passed with catalog JSON checks, `npm run type-check`, `cargo fmt --all --check --manifest-path core/Cargo.toml`, and `cargo check -p archipelago --manifest-path core/Cargo.toml`; live checks on `100.70.96.88` confirmed Saleor dashboard `9010`/API `8000` and NetBird API/OAuth routes survive `netbird-server` restart."
|
"Validation passed with catalog JSON checks, `npm run type-check`, `cargo fmt --all --check --manifest-path core/Cargo.toml`, and `cargo check -p archipelago --manifest-path core/Cargo.toml`; live checks on `100.70.96.88` confirmed Saleor dashboard `9010`/API `8000` and NetBird API/OAuth routes survive `netbird-server` restart."
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user