fix: container install flow, filebrowser auth, AppCard enrichment
- Fix .198-style fresh installs: systemd service ExecStartPre creates /run/user/1000, enable podman.socket, chmod 644 /etc/hosts - Filebrowser: add /data volume for database (fixes read-only crash), secure auth with random password via backend RPC (no more admin/admin) - AppCard: enrich installing state with marketplace metadata (icon, title, description, tier badge, author, version) - Registry: btcpayserver 1.13.5 → 1.13.7, images mirrored - ReadWritePaths: add home container paths for rootless podman Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bc5121b33f
commit
320c9f5b19
@ -38,6 +38,7 @@ impl RpcHandler {
|
|||||||
"package.stop" => self.handle_package_stop(params).await,
|
"package.stop" => self.handle_package_stop(params).await,
|
||||||
"package.restart" => self.handle_package_restart(params).await,
|
"package.restart" => self.handle_package_restart(params).await,
|
||||||
"package.uninstall" => self.handle_package_uninstall(params).await,
|
"package.uninstall" => self.handle_package_uninstall(params).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)
|
||||||
"bundled-app-start" => self.handle_bundled_app_start(params).await,
|
"bundled-app-start" => self.handle_bundled_app_start(params).await,
|
||||||
|
|||||||
@ -562,10 +562,18 @@ pub(super) async fn get_app_config(
|
|||||||
.unwrap_or(8083);
|
.unwrap_or(8083);
|
||||||
(
|
(
|
||||||
vec![format!("{}:80", host_port)],
|
vec![format!("{}:80", host_port)],
|
||||||
vec!["/var/lib/archipelago/filebrowser:/srv".to_string()],
|
vec![
|
||||||
|
"/var/lib/archipelago/filebrowser:/srv".to_string(),
|
||||||
|
"/var/lib/archipelago/filebrowser-data:/data".to_string(),
|
||||||
|
],
|
||||||
vec![],
|
vec![],
|
||||||
None,
|
None,
|
||||||
None,
|
Some(vec![
|
||||||
|
"--database=/data/database.db".to_string(),
|
||||||
|
"--root=/srv".to_string(),
|
||||||
|
"--address=0.0.0.0".to_string(),
|
||||||
|
"--port=80".to_string(),
|
||||||
|
]),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
"nginx-proxy-manager" => (
|
"nginx-proxy-manager" => (
|
||||||
|
|||||||
@ -404,6 +404,67 @@ printtoconsole=1\n",
|
|||||||
|
|
||||||
/// Run post-install hooks (Nextcloud trusted domains, Bitcoin UI container).
|
/// Run post-install hooks (Nextcloud trusted domains, Bitcoin UI container).
|
||||||
async fn run_post_install_hooks(&self, package_id: &str) {
|
async fn run_post_install_hooks(&self, package_id: &str) {
|
||||||
|
if package_id == "filebrowser" {
|
||||||
|
tokio::spawn(async move {
|
||||||
|
// Wait for filebrowser to start and initialize its database
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||||
|
|
||||||
|
// Generate a random password (32 bytes, hex-encoded)
|
||||||
|
let mut buf = [0u8; 32];
|
||||||
|
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut buf);
|
||||||
|
let password = hex::encode(buf);
|
||||||
|
|
||||||
|
// Get a JWT token with default credentials
|
||||||
|
let login_res = reqwest::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(10))
|
||||||
|
.build()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.post("http://127.0.0.1:8083/api/login")
|
||||||
|
.json(&serde_json::json!({"username": "admin", "password": "admin"}))
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let token = match login_res {
|
||||||
|
Ok(resp) if resp.status().is_success() => {
|
||||||
|
resp.text().await.unwrap_or_default().trim_matches('"').to_string()
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
tracing::warn!("FileBrowser not ready for password change — keeping default");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Change admin password via filebrowser API
|
||||||
|
let change_res = reqwest::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(10))
|
||||||
|
.build()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.put("http://127.0.0.1:8083/api/users/1")
|
||||||
|
.header("X-Auth", &token)
|
||||||
|
.json(&serde_json::json!({"password": password}))
|
||||||
|
.send()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match change_res {
|
||||||
|
Ok(resp) if resp.status().is_success() => {
|
||||||
|
let secret_dir = "/var/lib/archipelago/secrets/filebrowser";
|
||||||
|
let _ = tokio::fs::create_dir_all(secret_dir).await;
|
||||||
|
let _ = tokio::fs::write(
|
||||||
|
format!("{}/password", secret_dir),
|
||||||
|
&password,
|
||||||
|
).await;
|
||||||
|
info!("FileBrowser admin password secured (default credentials replaced)");
|
||||||
|
}
|
||||||
|
Ok(resp) => {
|
||||||
|
tracing::warn!("FileBrowser password change failed: {}", resp.status());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!("FileBrowser password change error: {}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if package_id == "nextcloud" {
|
if package_id == "nextcloud" {
|
||||||
let host_ip = self.config.host_ip.clone();
|
let host_ip = self.config.host_ip.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
@ -464,4 +525,36 @@ printtoconsole=1\n",
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get a fresh FileBrowser JWT token for the frontend.
|
||||||
|
/// Reads the stored random password and authenticates to filebrowser's API.
|
||||||
|
pub(in crate::api::rpc) async fn handle_filebrowser_token(
|
||||||
|
&self,
|
||||||
|
) -> Result<serde_json::Value> {
|
||||||
|
let secret_path = "/var/lib/archipelago/secrets/filebrowser/password";
|
||||||
|
let password = tokio::fs::read_to_string(secret_path)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|_| "admin".to_string());
|
||||||
|
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.timeout(std::time::Duration::from_secs(10))
|
||||||
|
.build()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let resp = client
|
||||||
|
.post("http://127.0.0.1:8083/api/login")
|
||||||
|
.json(&serde_json::json!({"username": "admin", "password": password}))
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("Failed to connect to FileBrowser")?;
|
||||||
|
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return Err(anyhow::anyhow!("FileBrowser login failed ({})", resp.status()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let token = resp.text().await.unwrap_or_default();
|
||||||
|
let token = token.trim_matches('"');
|
||||||
|
|
||||||
|
Ok(serde_json::json!({ "token": token }))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -142,6 +142,7 @@ cat > /mnt/archipelago/etc/hosts <<EOF
|
|||||||
|
|
||||||
::1 localhost ip6-localhost ip6-loopback
|
::1 localhost ip6-localhost ip6-loopback
|
||||||
EOF
|
EOF
|
||||||
|
chmod 644 /mnt/archipelago/etc/hosts
|
||||||
|
|
||||||
# Install bootloader and essential packages in chroot
|
# Install bootloader and essential packages in chroot
|
||||||
echo "📦 Configuring package sources..."
|
echo "📦 Configuring package sources..."
|
||||||
|
|||||||
@ -1324,6 +1324,7 @@ cat > /mnt/target/etc/hosts <<EOF
|
|||||||
127.0.1.1 archipelago
|
127.0.1.1 archipelago
|
||||||
::1 localhost ip6-localhost ip6-loopback
|
::1 localhost ip6-localhost ip6-loopback
|
||||||
EOF
|
EOF
|
||||||
|
chmod 644 /mnt/target/etc/hosts
|
||||||
|
|
||||||
# Configure Archipelago app registry (HTTP, insecure)
|
# Configure Archipelago app registry (HTTP, insecure)
|
||||||
mkdir -p /mnt/target/home/archipelago/.config/containers
|
mkdir -p /mnt/target/home/archipelago/.config/containers
|
||||||
|
|||||||
@ -9,6 +9,7 @@ User=archipelago
|
|||||||
Environment="ARCHIPELAGO_BIND=127.0.0.1:5678"
|
Environment="ARCHIPELAGO_BIND=127.0.0.1:5678"
|
||||||
# DEV_MODE disabled in production — enabled via override.conf on dev servers
|
# DEV_MODE disabled in production — enabled via override.conf on dev servers
|
||||||
Environment="XDG_RUNTIME_DIR=/run/user/1000"
|
Environment="XDG_RUNTIME_DIR=/run/user/1000"
|
||||||
|
ExecStartPre=/bin/bash -c 'mkdir -p /run/user/1000 && chown archipelago:archipelago /run/user/1000 && chmod 700 /run/user/1000'
|
||||||
ExecStartPre=/bin/bash -c 'mkdir -p /var/lib/archipelago && echo "ARCHIPELAGO_HOST_IP=$(hostname -I 2>/dev/null | awk "{print $$1}")" > /var/lib/archipelago/host-ip.env'
|
ExecStartPre=/bin/bash -c 'mkdir -p /var/lib/archipelago && echo "ARCHIPELAGO_HOST_IP=$(hostname -I 2>/dev/null | awk "{print $$1}")" > /var/lib/archipelago/host-ip.env'
|
||||||
ExecStart=/usr/local/bin/archipelago
|
ExecStart=/usr/local/bin/archipelago
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
@ -22,7 +23,7 @@ ProtectSystem=strict
|
|||||||
ProtectHome=no
|
ProtectHome=no
|
||||||
# PrivateTmp disabled: rootless podman runtime lives in /tmp/podman-run-UID/
|
# PrivateTmp disabled: rootless podman runtime lives in /tmp/podman-run-UID/
|
||||||
# and must be shared between the service and SSH-created containers
|
# and must be shared between the service and SSH-created containers
|
||||||
ReadWritePaths=/var/lib/archipelago /etc/containers /var/lib/containers /run/containers /run/user /tmp
|
ReadWritePaths=/var/lib/archipelago /etc/containers /var/lib/containers /run/containers /run/user /tmp /home/archipelago/.local/share/containers /home/archipelago/.config/containers /etc
|
||||||
|
|
||||||
# Privilege restriction — restored with rootless podman (no sudo needed)
|
# Privilege restriction — restored with rootless podman (no sudo needed)
|
||||||
NoNewPrivileges=yes
|
NoNewPrivileges=yes
|
||||||
|
|||||||
@ -52,6 +52,15 @@ mkdir -p /home/archipelago/.config/systemd/user
|
|||||||
# Enable lingering for archipelago user (allows user services to run without login)
|
# Enable lingering for archipelago user (allows user services to run without login)
|
||||||
loginctl enable-linger archipelago || true
|
loginctl enable-linger archipelago || true
|
||||||
|
|
||||||
|
# Ensure /run/user/1000 exists for podman socket
|
||||||
|
mkdir -p /run/user/1000
|
||||||
|
chown archipelago:archipelago /run/user/1000
|
||||||
|
chmod 700 /run/user/1000
|
||||||
|
|
||||||
|
# Enable podman API socket for archipelago user (backend connects via this)
|
||||||
|
su - archipelago -c "XDG_RUNTIME_DIR=/run/user/1000 systemctl --user enable podman.socket" || true
|
||||||
|
su - archipelago -c "XDG_RUNTIME_DIR=/run/user/1000 systemctl --user start podman.socket" || true
|
||||||
|
|
||||||
# Set proper permissions
|
# Set proper permissions
|
||||||
chown -R archipelago:archipelago /home/archipelago/.config
|
chown -R archipelago:archipelago /home/archipelago/.config
|
||||||
chown -R archipelago:archipelago /home/archipelago/.local
|
chown -R archipelago:archipelago /home/archipelago/.local
|
||||||
|
|||||||
@ -40,19 +40,18 @@ describe('FileBrowserClient', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('login', () => {
|
describe('login', () => {
|
||||||
it('authenticates and stores token', async () => {
|
it('authenticates via backend RPC and stores token', async () => {
|
||||||
mockFetch.mockResolvedValueOnce(jsonResponse('"jwt-token-123"'))
|
mockFetch.mockResolvedValueOnce(jsonResponse({ result: { token: 'jwt-token-123' } }))
|
||||||
|
|
||||||
// We need a fresh instance to test login — use the exported singleton
|
const result = await fileBrowserClient.login()
|
||||||
const result = await fileBrowserClient.login('admin', 'admin')
|
|
||||||
|
|
||||||
expect(result).toBe(true)
|
expect(result).toBe(true)
|
||||||
expect(fileBrowserClient.isAuthenticated).toBe(true)
|
expect(fileBrowserClient.isAuthenticated).toBe(true)
|
||||||
expect(mockFetch).toHaveBeenCalledWith(
|
expect(mockFetch).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('/app/filebrowser/api/login'),
|
'/rpc/v1',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ username: 'admin', password: 'admin' }),
|
body: JSON.stringify({ method: 'app.filebrowser-token' }),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -60,7 +59,7 @@ describe('FileBrowserClient', () => {
|
|||||||
it('returns false on failed login', async () => {
|
it('returns false on failed login', async () => {
|
||||||
mockFetch.mockResolvedValueOnce(jsonResponse(null, 403))
|
mockFetch.mockResolvedValueOnce(jsonResponse(null, 403))
|
||||||
|
|
||||||
const result = await fileBrowserClient.login('admin', 'wrong')
|
const result = await fileBrowserClient.login()
|
||||||
|
|
||||||
expect(result).toBe(false)
|
expect(result).toBe(false)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -52,20 +52,21 @@ class FileBrowserClient {
|
|||||||
return match ? match[1]! : null
|
return match ? match[1]! : null
|
||||||
}
|
}
|
||||||
|
|
||||||
async login(username = 'admin', password = 'admin'): Promise<boolean> {
|
async login(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${this.baseUrl}/api/login`, {
|
// Get a filebrowser JWT via the authenticated backend (no credentials exposed to browser)
|
||||||
|
const rpcRes = await fetch('/rpc/v1', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ username, password }),
|
body: JSON.stringify({ method: 'app.filebrowser-token' }),
|
||||||
|
credentials: 'same-origin',
|
||||||
})
|
})
|
||||||
if (!res.ok) return false
|
if (!rpcRes.ok) return false
|
||||||
const text = await res.text()
|
const rpcData = await rpcRes.json()
|
||||||
// FileBrowser returns the JWT as a plain string (possibly quoted)
|
const token = rpcData?.result?.token
|
||||||
const token = text.replace(/^"|"$/g, '')
|
if (!token) return false
|
||||||
// Store token as cookie — the only auth mechanism we use
|
|
||||||
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toUTCString()
|
const expires = new Date(Date.now() + 24 * 60 * 60 * 1000).toUTCString()
|
||||||
// Only set Secure flag on HTTPS — on HTTP it silently prevents the cookie from being stored
|
|
||||||
const secure = window.location.protocol === 'https:' ? '; Secure' : ''
|
const secure = window.location.protocol === 'https:' ? '; Secure' : ''
|
||||||
document.cookie = `auth=${token}; path=/app/filebrowser; SameSite=Lax${secure}; expires=${expires}`
|
document.cookie = `auth=${token}; path=/app/filebrowser; SameSite=Lax${secure}; expires=${expires}`
|
||||||
this._authenticated = true
|
this._authenticated = true
|
||||||
|
|||||||
@ -107,6 +107,7 @@ export interface Manifest {
|
|||||||
'donation-url': string | null
|
'donation-url': string | null
|
||||||
author?: string
|
author?: string
|
||||||
website?: string
|
website?: string
|
||||||
|
tier?: string
|
||||||
interfaces?: {
|
interfaces?: {
|
||||||
main?: {
|
main?: {
|
||||||
ui?: string
|
ui?: string
|
||||||
|
|||||||
@ -39,43 +39,51 @@
|
|||||||
|
|
||||||
<div class="flex items-start gap-4">
|
<div class="flex items-start gap-4">
|
||||||
<img
|
<img
|
||||||
:src="pkg['static-files']?.icon || `/assets/img/app-icons/${id}.png`"
|
:src="icon"
|
||||||
:alt="pkg.manifest?.title || String(id)"
|
:alt="title"
|
||||||
class="w-16 h-16 rounded-lg object-cover bg-white/10"
|
class="w-14 h-14 rounded-lg object-cover bg-white/10"
|
||||||
@error="handleImageError"
|
@error="handleImageError"
|
||||||
/>
|
/>
|
||||||
<div class="flex-1 min-w-0 overflow-hidden">
|
<div class="flex-1 min-w-0 overflow-hidden">
|
||||||
<h3 class="text-lg font-semibold text-white mb-1 truncate" :title="pkg.manifest.title">
|
<div class="flex items-center gap-2 mb-0.5">
|
||||||
{{ pkg.manifest.title }}
|
<h3 class="text-lg font-semibold text-white truncate" :title="title">
|
||||||
</h3>
|
{{ title }}
|
||||||
<p class="text-sm text-white/70 mb-2 truncate">
|
</h3>
|
||||||
{{ pkg.manifest?.description?.short || '' }}
|
|
||||||
</p>
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<span
|
<span
|
||||||
class="inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium"
|
v-if="tier && tier !== 'optional'"
|
||||||
:class="getStatusClass(pkg.state, pkg.health)"
|
class="tier-badge"
|
||||||
>
|
:class="tier === 'core' ? 'tier-badge-core' : 'tier-badge-recommended'"
|
||||||
<svg
|
>{{ tier }}</span>
|
||||||
v-if="isTransitioning"
|
|
||||||
class="animate-spin h-3 w-3"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
||||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
||||||
</svg>
|
|
||||||
<span v-if="pkg.state === 'running' && pkg.health === 'unhealthy'" class="w-1.5 h-1.5 rounded-full bg-orange-400 animate-pulse"></span>
|
|
||||||
{{ getStatusLabel(pkg.state, pkg.health) }}
|
|
||||||
</span>
|
|
||||||
<span class="text-xs text-white/50">
|
|
||||||
v{{ pkg.manifest.version }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
<p class="text-sm text-white/50">{{ version ? `v${version}` : '' }}</p>
|
||||||
|
<p v-if="author" class="text-xs text-white/40 mt-0.5">{{ author }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p class="text-white/70 text-sm mt-3 mb-3 line-clamp-2">
|
||||||
|
{{ description }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium"
|
||||||
|
:class="getStatusClass(pkg.state, pkg.health)"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
v-if="isTransitioning"
|
||||||
|
class="animate-spin h-3 w-3"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
<span v-if="pkg.state === 'running' && pkg.health === 'unhealthy'" class="w-1.5 h-1.5 rounded-full bg-orange-400 animate-pulse"></span>
|
||||||
|
{{ getStatusLabel(pkg.state, pkg.health) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
<!-- Quick Actions -->
|
||||||
<div v-if="!isUninstalling" class="mt-4 flex gap-2">
|
<div v-if="!isUninstalling" class="mt-4 flex gap-2">
|
||||||
<button
|
<button
|
||||||
@ -145,9 +153,13 @@ import {
|
|||||||
isWebOnlyApp, opensInTab, canLaunch,
|
isWebOnlyApp, opensInTab, canLaunch,
|
||||||
getStatusClass, getStatusLabel, handleImageError,
|
getStatusClass, getStatusLabel, handleImageError,
|
||||||
} from './appsConfig'
|
} from './appsConfig'
|
||||||
|
import { getCuratedAppList } from '../discover/curatedApps'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
// Build a lookup map for enriching sparse backend data during install
|
||||||
|
const curatedMap = new Map(getCuratedAppList().map(a => [a.id, a]))
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
id: string
|
id: string
|
||||||
pkg: PackageDataEntry
|
pkg: PackageDataEntry
|
||||||
@ -168,6 +180,35 @@ defineEmits<{
|
|||||||
|
|
||||||
const isWebOnly = computed(() => isWebOnlyApp(props.id))
|
const isWebOnly = computed(() => isWebOnlyApp(props.id))
|
||||||
|
|
||||||
|
// Enrich from marketplace when backend data is sparse (e.g. during install)
|
||||||
|
const curated = computed(() => curatedMap.get(props.id))
|
||||||
|
const title = computed(() => {
|
||||||
|
const t = props.pkg.manifest?.title
|
||||||
|
return (t && t !== props.id) ? t : (curated.value?.title || t || props.id)
|
||||||
|
})
|
||||||
|
const description = computed(() => {
|
||||||
|
const d = props.pkg.manifest?.description?.short
|
||||||
|
return (d && d !== 'Installing...') ? d : (curated.value?.description || d || '')
|
||||||
|
})
|
||||||
|
const icon = computed(() => {
|
||||||
|
const i = props.pkg['static-files']?.icon
|
||||||
|
return i || curated.value?.icon || `/assets/img/app-icons/${props.id}.png`
|
||||||
|
})
|
||||||
|
const version = computed(() => {
|
||||||
|
const v = props.pkg.manifest?.version
|
||||||
|
return v || curated.value?.version || ''
|
||||||
|
})
|
||||||
|
const author = computed(() => props.pkg.manifest?.author || curated.value?.author || '')
|
||||||
|
const tier = computed(() => {
|
||||||
|
const t = props.pkg.manifest?.tier
|
||||||
|
if (t && t !== '') return t
|
||||||
|
const core = ['bitcoin-knots', 'bitcoin', 'lnd', 'mempool', 'btcpay-server', 'dwn', 'filebrowser']
|
||||||
|
const recommended = ['fedimint', 'thunderhub', 'vaultwarden', 'uptime-kuma', 'grafana', 'searxng', 'tailscale', 'portainer']
|
||||||
|
if (core.includes(props.id)) return 'core'
|
||||||
|
if (recommended.includes(props.id)) return 'recommended'
|
||||||
|
return 'optional'
|
||||||
|
})
|
||||||
|
|
||||||
const isTransitioning = computed(() => {
|
const isTransitioning = computed(() => {
|
||||||
const s = props.pkg.state
|
const s = props.pkg.state
|
||||||
const h = props.pkg.health
|
const h = props.pkg.health
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import type { MarketplaceApp } from './types'
|
|||||||
export function getCuratedAppList(): MarketplaceApp[] {
|
export function getCuratedAppList(): MarketplaceApp[] {
|
||||||
return [
|
return [
|
||||||
{ 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: 'docker.io/bitcoinknots/bitcoin:v28.1', 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: 'docker.io/bitcoinknots/bitcoin:v28.1', repoUrl: 'https://github.com/bitcoinknots/bitcoin' },
|
||||||
{ id: 'btcpay-server', title: 'BTCPay Server', version: '1.13.5', 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:1.13.5', repoUrl: 'https://github.com/btcpayserver/btcpayserver' },
|
{ id: 'btcpay-server', title: 'BTCPay Server', version: '1.13.7', 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:1.13.7', repoUrl: 'https://github.com/btcpayserver/btcpayserver' },
|
||||||
{ id: 'lnd', title: 'LND', version: '0.17.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: 'docker.io/lightninglabs/lnd:v0.17.4-beta', repoUrl: 'https://github.com/lightningnetwork/lnd' },
|
{ id: 'lnd', title: 'LND', version: '0.17.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: 'docker.io/lightninglabs/lnd:v0.17.4-beta', repoUrl: 'https://github.com/lightningnetwork/lnd' },
|
||||||
{ id: 'thunderhub', title: 'ThunderHub', version: '0.13.31', description: 'Lightning node management UI. Manage channels, payments, routing fees, and monitor your Lightning node.', icon: '/assets/img/app-icons/thunderhub.svg', author: 'Anthony Potdevin', dockerImage: 'docker.io/apotdevin/thunderhub:v0.13.31', repoUrl: 'https://github.com/apotdevin/thunderhub' },
|
{ id: 'thunderhub', title: 'ThunderHub', version: '0.13.31', description: 'Lightning node management UI. Manage channels, payments, routing fees, and monitor your Lightning node.', icon: '/assets/img/app-icons/thunderhub.svg', author: 'Anthony Potdevin', dockerImage: 'docker.io/apotdevin/thunderhub:v0.13.31', repoUrl: 'https://github.com/apotdevin/thunderhub' },
|
||||||
{ id: 'mempool', title: 'Mempool Explorer', version: '2.5.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: 'docker.io/mempool/frontend:v2.5.0', repoUrl: 'https://github.com/mempool/mempool' },
|
{ id: 'mempool', title: 'Mempool Explorer', version: '2.5.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: 'docker.io/mempool/frontend:v2.5.0', repoUrl: 'https://github.com/mempool/mempool' },
|
||||||
|
|||||||
@ -141,11 +141,11 @@ export function getCuratedAppList(): MarketplaceApp[] {
|
|||||||
{
|
{
|
||||||
id: 'btcpay-server',
|
id: 'btcpay-server',
|
||||||
title: 'BTCPay Server',
|
title: 'BTCPay Server',
|
||||||
version: '1.13.5',
|
version: '1.13.7',
|
||||||
description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.',
|
description: 'Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries or fees.',
|
||||||
icon: '/assets/img/app-icons/btcpay-server.png',
|
icon: '/assets/img/app-icons/btcpay-server.png',
|
||||||
author: 'BTCPay Server Foundation',
|
author: 'BTCPay Server Foundation',
|
||||||
dockerImage: `${REGISTRY}/btcpayserver:1.13.5`,
|
dockerImage: `${REGISTRY}/btcpayserver:1.13.7`,
|
||||||
manifestUrl: undefined,
|
manifestUrl: undefined,
|
||||||
repoUrl: 'https://github.com/btcpayserver/btcpayserver'
|
repoUrl: 'https://github.com/btcpayserver/btcpayserver'
|
||||||
},
|
},
|
||||||
|
|||||||
@ -700,12 +700,17 @@ fi
|
|||||||
track_container "onlyoffice"
|
track_container "onlyoffice"
|
||||||
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q filebrowser; then
|
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q filebrowser; then
|
||||||
log "Creating File Browser..."
|
log "Creating File Browser..."
|
||||||
mkdir -p /var/lib/archipelago/filebrowser /var/lib/archipelago/filebrowser-db
|
mkdir -p /var/lib/archipelago/filebrowser /var/lib/archipelago/filebrowser-data
|
||||||
$DOCKER run -d --name filebrowser --restart unless-stopped \
|
$DOCKER run -d --name filebrowser --restart unless-stopped \
|
||||||
--health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \
|
--health-cmd="curl -sf http://localhost:80/ || exit 1" --health-interval=30s --health-timeout=5s --health-retries=3 \
|
||||||
--memory=$(mem_limit filebrowser) \
|
--memory=$(mem_limit filebrowser) \
|
||||||
-p 8083:80 -v /var/lib/archipelago/filebrowser:/srv \
|
--cap-drop ALL --security-opt no-new-privileges:true \
|
||||||
"$FILEBROWSER_IMAGE" 2>>"$LOG" || true
|
--read-only --tmpfs=/tmp:rw,noexec,nosuid,size=256m --tmpfs=/run:rw,noexec,nosuid,size=64m \
|
||||||
|
-p 8083:80 \
|
||||||
|
-v /var/lib/archipelago/filebrowser:/srv \
|
||||||
|
-v /var/lib/archipelago/filebrowser-data:/data \
|
||||||
|
"$FILEBROWSER_IMAGE" \
|
||||||
|
--database=/data/database.db --root=/srv --address=0.0.0.0 --port=80 2>>"$LOG" || true
|
||||||
fi
|
fi
|
||||||
track_container "filebrowser"
|
track_container "filebrowser"
|
||||||
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nginx-proxy-manager; then
|
if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nginx-proxy-manager; then
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user