Compare commits
3
Commits
v1.8.1-alpha
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d05fa6988d | ||
|
|
2399eeac66 | ||
|
|
63cb9dd22c |
@@ -1,5 +1,10 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.8.2-alpha (2026-08-13)
|
||||||
|
|
||||||
|
- **An app that can't be shown inside the dashboard now becomes a tab app by itself.** A few apps refuse to render inside another page no matter what — they break out with their own code or insist on owning the whole browser window. Opening one used to mean staring at a grey pane. Now the dashboard notices, offers the app in its own tab, and remembers: from then on that app's button opens a tab directly (with the little launch icon that tab apps carry), first click, every time. If a later update makes the app embeddable after all, the dashboard notices that too and goes back to embedding it.
|
||||||
|
- **The screensaver and login emblem are back to their clean black finish.** A glossy paint experiment on the circle behind the logo shipped by mistake in the previous update; it's removed everywhere.
|
||||||
|
|
||||||
## v1.8.1-alpha (2026-08-13)
|
## v1.8.1-alpha (2026-08-13)
|
||||||
|
|
||||||
- **Apps that refused to open inside the dashboard now embed like everything else.** Some apps ship browser headers that forbid being shown inside another page — correct hardening on the open web, but inside Archipelago it produced a dead grey pane when you opened them from My Apps (Alby Hub was the first to hit it). The app gate, which already checks your login on every request to an app, now removes just those framing headers on the way through; each app's own content-security rules pass through untouched. No more per-app proxy workarounds.
|
- **Apps that refused to open inside the dashboard now embed like everything else.** Some apps ship browser headers that forbid being shown inside another page — correct hardening on the open web, but inside Archipelago it produced a dead grey pane when you opened them from My Apps (Alby Hub was the first to hit it). The app gate, which already checks your login on every request to an app, now removes just those framing headers on the way through; each app's own content-security rules pass through untouched. No more per-app proxy workarounds.
|
||||||
|
|||||||
@@ -159,6 +159,14 @@ the top-level origin (OAuth redirect flows, WebAuthn), or sets
|
|||||||
in the real embedded app session, **not** a plain browser tab: tabs don't
|
in the real embedded app session, **not** a plain browser tab: tabs don't
|
||||||
enforce framing headers, so a tab proves nothing about the iframe.
|
enforce framing headers, so a tab proves nothing about the iframe.
|
||||||
|
|
||||||
|
**The dashboard also self-heals**: if a running app's frame fails to load,
|
||||||
|
the session offers "open in tab" and *remembers the app as a tab app* — every
|
||||||
|
later launch opens a tab directly, with the tab-launch icon on its button.
|
||||||
|
The memory clears itself when the app later embeds successfully, and expires
|
||||||
|
weekly so fixes get re-probed. This safety net is not a licence to skip the
|
||||||
|
manifest flag: declaring `open_in_new_tab: true` up front spares your users
|
||||||
|
the one dead-pane encounter the detector needs.
|
||||||
|
|
||||||
(Historical note: before the gate handled this, embeddable-but-blocking apps
|
(Historical note: before the gate handled this, embeddable-but-blocking apps
|
||||||
each carried a hand-built nginx strip proxy — gitea's port-3000 proxy is the
|
each carried a hand-built nginx strip proxy — gitea's port-3000 proxy is the
|
||||||
surviving example. Do not copy that pattern for new apps.)
|
surviving example. Do not copy that pattern for new apps.)
|
||||||
|
|||||||
@@ -1,39 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<!-- KAMMERGUT GLOSS TEST (2026-08-12): SVG paint filter lifted verbatim
|
|
||||||
from plan-b's Kammergut wordmark (#paintGloss). Consumed by the
|
|
||||||
.logo-gradient-border::after override in style.css. Revert by
|
|
||||||
deleting this svg block + that css block (one commit). -->
|
|
||||||
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
|
|
||||||
<defs>
|
|
||||||
<filter id="paintGloss" x="-12%" y="-30%" width="124%" height="160%" color-interpolation-filters="sRGB">
|
|
||||||
<feMorphology in="SourceAlpha" operator="dilate" radius="2.5" result="dilated"/>
|
|
||||||
<feGaussianBlur in="dilated" stdDeviation="6" result="puddle"/>
|
|
||||||
<feFlood flood-color="#fff4dc" flood-opacity="0.85" result="puddleColor"/>
|
|
||||||
<feComposite in="puddleColor" in2="puddle" operator="in" result="puddleHi"/>
|
|
||||||
<feFlood flood-color="#1A1A18" flood-opacity="0.18" result="puddleShadowColor"/>
|
|
||||||
<feComposite in="puddleShadowColor" in2="puddle" operator="in" result="puddleShadow"/>
|
|
||||||
<feOffset in="puddleShadow" dx="0" dy="3" result="puddleShadowOff"/>
|
|
||||||
<feGaussianBlur in="SourceAlpha" stdDeviation="1.4" result="bump"/>
|
|
||||||
<feSpecularLighting in="bump" surfaceScale="4" specularConstant="0.9"
|
|
||||||
specularExponent="40" lighting-color="#fff2d4" result="spec2">
|
|
||||||
<fePointLight x="600" y="-200" z="380"/>
|
|
||||||
</feSpecularLighting>
|
|
||||||
<feComposite in="spec2" in2="SourceAlpha" operator="in" result="specClip2"/>
|
|
||||||
<feGaussianBlur in="SourceAlpha" stdDeviation="4" result="dsBlur"/>
|
|
||||||
<feOffset in="dsBlur" dx="0" dy="6" result="dsOff"/>
|
|
||||||
<feComponentTransfer in="dsOff" result="dsFinal">
|
|
||||||
<feFuncA type="linear" slope="0.45"/>
|
|
||||||
</feComponentTransfer>
|
|
||||||
<feMerge>
|
|
||||||
<feMergeNode in="dsFinal"/>
|
|
||||||
<feMergeNode in="SourceGraphic"/>
|
|
||||||
<feMergeNode in="specClip2"/>
|
|
||||||
</feMerge>
|
|
||||||
</filter>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<!-- Splash Screen (only on first visit) -->
|
<!-- Splash Screen (only on first visit) -->
|
||||||
<SplashScreen v-if="showSplash" @complete="handleSplashComplete" />
|
<SplashScreen v-if="showSplash" @complete="handleSplashComplete" />
|
||||||
|
|
||||||
|
|||||||
@@ -1182,7 +1182,15 @@ function measure() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initialBuild() {
|
function initialBuild() {
|
||||||
staticMode = prefersReducedMotion()
|
// Kiosk TVs get the fully static map: no entrance animation, no ticker.
|
||||||
|
// The GSAP intro needs healthy rAF delivery to ever reach opacity 1 — on
|
||||||
|
// a paint-starved kiosk it stalls and the screen reads as BLANK until a
|
||||||
|
// lucky refresh (framework-pt, 2026-08-14). Half-rate ticking (below)
|
||||||
|
// helped but any continuous SVG animation is still the single biggest
|
||||||
|
// load on the kiosk's compositor, and at TV distance the still map loses
|
||||||
|
// nothing. Everything lands in place immediately, graph updates apply
|
||||||
|
// instantly, and the 2D/3D toggle still works.
|
||||||
|
staticMode = prefersReducedMotion() || kioskLowPower
|
||||||
built = true
|
built = true
|
||||||
applyGraph()
|
applyGraph()
|
||||||
|
|
||||||
|
|||||||
@@ -1128,16 +1128,6 @@ html.controller-nav [data-controller-container]:focus {
|
|||||||
z-index: 0;
|
z-index: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* KAMMERGUT GLOSS TEST (2026-08-12) — black gloss paint on the badge's
|
|
||||||
inner circle, lifted from plan-b's Kammergut wordmark (.paint-3d
|
|
||||||
gradient + #paintGloss filter, defs injected in App.vue). Revert:
|
|
||||||
delete this block + the svg defs block in App.vue (one commit). */
|
|
||||||
.logo-gradient-border::after {
|
|
||||||
background: linear-gradient(180deg, #2a2a26 0%, #1a1a18 45%, #060604 100%);
|
|
||||||
filter: url(#paintGloss);
|
|
||||||
-webkit-filter: url(#paintGloss);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-gradient-border img,
|
.logo-gradient-border img,
|
||||||
.logo-gradient-border svg {
|
.logo-gradient-border svg {
|
||||||
border-radius: 9999px;
|
border-radius: 9999px;
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Auto-tab memory — apps observed to refuse iframe embedding.
|
||||||
|
*
|
||||||
|
* The gate strips frame-blocking headers for gated apps (1.8.1+), but three
|
||||||
|
* failure modes survive any header fix: JS frame-busting, apps that must be
|
||||||
|
* the top-level origin (OAuth/WebAuthn), and SameSite=Strict session cookies.
|
||||||
|
* Cross-origin embed failure is not reliably detectable up front from the
|
||||||
|
* browser, so the app session's load-timeout is the detector — and this
|
||||||
|
* module is the memory: once an app is seen blocked while RUNNING, it is
|
||||||
|
* remembered here and every later launch opens it as a tab app directly from
|
||||||
|
* the click (a user gesture, so never popup-blocked). The dead grey pane
|
||||||
|
* happens at most once per app.
|
||||||
|
*
|
||||||
|
* Entries expire after 7 days so a node update that fixes embedding (for
|
||||||
|
* example a gate improvement) gets re-probed instead of being remembered as
|
||||||
|
* broken forever; a successful iframe load also clears the entry immediately.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const KEY = 'archipelago_auto_new_tab_apps'
|
||||||
|
const EXPIRY_MS = 7 * 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
function read(): Record<string, number> {
|
||||||
|
try {
|
||||||
|
const raw = JSON.parse(localStorage.getItem(KEY) || '{}')
|
||||||
|
if (raw && typeof raw === 'object' && !Array.isArray(raw)) return raw
|
||||||
|
} catch { /* corrupt/unavailable storage reads as empty */ }
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
function write(entries: Record<string, number>) {
|
||||||
|
try { localStorage.setItem(KEY, JSON.stringify(entries)) } catch { /* full/denied */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAutoTabApp(id: string): boolean {
|
||||||
|
const at = read()[id]
|
||||||
|
return typeof at === 'number' && Date.now() - at < EXPIRY_MS
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rememberAutoTabApp(id: string): void {
|
||||||
|
const entries = read()
|
||||||
|
entries[id] = Date.now()
|
||||||
|
write(entries)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function forgetAutoTabApp(id: string): void {
|
||||||
|
const entries = read()
|
||||||
|
if (id in entries) {
|
||||||
|
delete entries[id]
|
||||||
|
write(entries)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -105,6 +105,7 @@ import { useAppLauncherStore } from '@/stores/appLauncher'
|
|||||||
import { useAppStore } from '@/stores/app'
|
import { useAppStore } from '@/stores/app'
|
||||||
import { useScreensaverStore } from '@/stores/screensaver'
|
import { useScreensaverStore } from '@/stores/screensaver'
|
||||||
import NostrIdentityPicker from '@/components/NostrIdentityPicker.vue'
|
import NostrIdentityPicker from '@/components/NostrIdentityPicker.vue'
|
||||||
|
import { isAutoTabApp, rememberAutoTabApp, forgetAutoTabApp } from '@/utils/autoTabApps'
|
||||||
import AppSessionHeader from './appSession/AppSessionHeader.vue'
|
import AppSessionHeader from './appSession/AppSessionHeader.vue'
|
||||||
import AppSessionFrame from './appSession/AppSessionFrame.vue'
|
import AppSessionFrame from './appSession/AppSessionFrame.vue'
|
||||||
import MobileGamepad from './appSession/MobileGamepad.vue'
|
import MobileGamepad from './appSession/MobileGamepad.vue'
|
||||||
@@ -199,9 +200,27 @@ function updateIsMobile() { isMobile.value = window.innerWidth < 768 }
|
|||||||
// same-origin by the mock backend, so the prod new-tab list doesn't apply.
|
// same-origin by the mock backend, so the prod new-tab list doesn't apply.
|
||||||
const mustOpenNewTab = computed(() =>
|
const mustOpenNewTab = computed(() =>
|
||||||
(NEW_TAB_APPS.has(appId.value) && !(IS_DEMO && isDemoApp(appId.value))) ||
|
(NEW_TAB_APPS.has(appId.value) && !(IS_DEMO && isDemoApp(appId.value))) ||
|
||||||
|
// Remembered embed-refusers (frame-busting, top-level-origin apps): the
|
||||||
|
// first blocked encounter records them, every later launch is a tab app.
|
||||||
|
isAutoTabApp(appId.value) ||
|
||||||
(IS_DEMO && isDemoExternal(appId.value))
|
(IS_DEMO && isDemoExternal(appId.value))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// The auto-tab detector: the load-timeout marked the frame blocked, the
|
||||||
|
// warming-up retry loop has given up, and the backend says the app is
|
||||||
|
// actually RUNNING — that combination is the embed-refusal signature (a
|
||||||
|
// down app is "warming up" or shows a blocked reason instead). Remember it
|
||||||
|
// so this app never shows the dead pane again; a later successful iframe
|
||||||
|
// load (onLoad) clears the memory, and entries expire on their own.
|
||||||
|
watch([iframeBlocked, warmingUp], ([blocked, warming]) => {
|
||||||
|
if (
|
||||||
|
blocked && !warming && !blockedReason.value &&
|
||||||
|
(packageEntry.value?.state === PackageState.Running)
|
||||||
|
) {
|
||||||
|
rememberAutoTabApp(appId.value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// ElectrumX shows a sync screen before its real UI (the Electrum server only
|
// ElectrumX shows a sync screen before its real UI (the Electrum server only
|
||||||
// serves clients once its index is built). Poll /electrs-status while this is
|
// serves clients once its index is built). Poll /electrs-status while this is
|
||||||
// the Electrum app; pass the status to the frame only while still syncing.
|
// the Electrum app; pass the status to the frame only while still syncing.
|
||||||
@@ -344,6 +363,10 @@ function onLoad() {
|
|||||||
loading.value = false
|
loading.value = false
|
||||||
isRefreshing.value = false
|
isRefreshing.value = false
|
||||||
autoRetryCount.value = 0
|
autoRetryCount.value = 0
|
||||||
|
// A frame that loads embeds fine — heal any stale auto-tab memory (for
|
||||||
|
// example an app remembered as blocked before the gate learned to strip
|
||||||
|
// frame headers).
|
||||||
|
forgetAutoTabApp(appId.value)
|
||||||
// TV/keyboard: hand focus to the app so keys (incl. the gamepad bridge's
|
// TV/keyboard: hand focus to the app so keys (incl. the gamepad bridge's
|
||||||
// virtual keyboard) flow into the iframe without needing a pointer click.
|
// virtual keyboard) flow into the iframe without needing a pointer click.
|
||||||
try { frameRef.value?.iframeRef?.focus() } catch { /* cross-origin is fine */ }
|
try { frameRef.value?.iframeRef?.focus() } catch { /* cross-origin is fine */ }
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { Ref } from 'vue'
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { PackageState, type PackageDataEntry } from '@/types/api'
|
import { PackageState, type PackageDataEntry } from '@/types/api'
|
||||||
import { resolveAppUrl } from '../appSession/appSessionConfig'
|
import { resolveAppUrl } from '../appSession/appSessionConfig'
|
||||||
|
import { isAutoTabApp } from '@/utils/autoTabApps'
|
||||||
|
|
||||||
export type AppsTab = 'apps' | 'websites' | 'services'
|
export type AppsTab = 'apps' | 'websites' | 'services'
|
||||||
|
|
||||||
@@ -173,7 +174,10 @@ export const TAB_LAUNCH_APPS = new Set([
|
|||||||
])
|
])
|
||||||
|
|
||||||
export function opensInTab(id: string): boolean {
|
export function opensInTab(id: string): boolean {
|
||||||
return TAB_LAUNCH_APPS.has(id)
|
// The launch icon and launch behavior follow the dynamic verdict too:
|
||||||
|
// apps observed refusing the iframe (remembered by the app session) show
|
||||||
|
// the same tab-launch affordance as the statically-known tab apps.
|
||||||
|
return TAB_LAUNCH_APPS.has(id) || isAutoTabApp(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backend services that ship no icon of their own reuse their PARENT app's icon
|
// Backend services that ship no icon of their own reuse their PARENT app's icon
|
||||||
|
|||||||
@@ -362,6 +362,17 @@ init()
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
<div class="overflow-y-auto flex-1 min-h-0 space-y-6 pr-1">
|
||||||
|
<!-- v1.8.2-alpha -->
|
||||||
|
<div>
|
||||||
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
<span class="text-xs font-mono px-2 py-0.5 rounded bg-orange-500/20 text-orange-300">v1.8.2-alpha</span>
|
||||||
|
<span class="text-xs text-white/40">August 13, 2026</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-3 text-sm text-white/80 pl-3 border-l border-white/10">
|
||||||
|
<p>**An app that can't be shown inside the dashboard now becomes a tab app by itself.** A few apps refuse to render inside another page no matter what — they break out with their own code or insist on owning the whole browser window. Opening one used to mean staring at a grey pane. Now the dashboard notices, offers the app in its own tab, and remembers: from then on that app's button opens a tab directly (with the little launch icon that tab apps carry), first click, every time. If a later update makes the app embeddable after all, the dashboard notices that too and goes back to embedding it.</p>
|
||||||
|
<p>**The screensaver and login emblem are back to their clean black finish.** A glossy paint experiment on the circle behind the logo shipped by mistake in the previous update; it's removed everywhere.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<!-- v1.8.1-alpha -->
|
<!-- v1.8.1-alpha -->
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2 mb-3">
|
<div class="flex items-center gap-2 mb-3">
|
||||||
|
|||||||
Reference in New Issue
Block a user