Merge branch 'dev'
This commit is contained in:
commit
e693b2425e
|
|
@ -300,6 +300,7 @@ services:
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- scribus-designs:/data/designs
|
- scribus-designs:/data/designs
|
||||||
|
- rspace-files:/data/files
|
||||||
environment:
|
environment:
|
||||||
- BRIDGE_SECRET=${SCRIBUS_BRIDGE_SECRET}
|
- BRIDGE_SECRET=${SCRIBUS_BRIDGE_SECRET}
|
||||||
- BRIDGE_PORT=8765
|
- BRIDGE_PORT=8765
|
||||||
|
|
|
||||||
|
|
@ -49,10 +49,18 @@ const styles = css`
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: calc(100% - 36px);
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,6 +144,7 @@ const styles = css`
|
||||||
|
|
||||||
.preview-area {
|
.preview-area {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -148,11 +157,13 @@ const styles = css`
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.render-preview img {
|
.render-preview img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
@ -284,6 +295,7 @@ export class FolkBlender extends FolkShape {
|
||||||
const root = super.createRenderRoot();
|
const root = super.createRenderRoot();
|
||||||
|
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "wrapper";
|
||||||
wrapper.innerHTML = html`
|
wrapper.innerHTML = html`
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<span class="header-title">
|
<span class="header-title">
|
||||||
|
|
|
||||||
|
|
@ -59,10 +59,18 @@ const styles = css`
|
||||||
background: rgba(255,255,255,0.2);
|
background: rgba(255,255,255,0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: calc(100% - 36px);
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -123,6 +131,7 @@ const styles = css`
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.step {
|
.step {
|
||||||
|
|
@ -234,6 +243,7 @@ export class FolkDesignAgent extends FolkShape {
|
||||||
const root = super.createRenderRoot();
|
const root = super.createRenderRoot();
|
||||||
|
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "wrapper";
|
||||||
wrapper.innerHTML = html`
|
wrapper.innerHTML = html`
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<span class="header-title">
|
<span class="header-title">
|
||||||
|
|
|
||||||
|
|
@ -49,10 +49,18 @@ const styles = css`
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: calc(100% - 36px);
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -111,11 +119,13 @@ const styles = css`
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-area img {
|
.preview-area img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
@ -234,6 +244,7 @@ export class FolkFreeCAD extends FolkShape {
|
||||||
const root = super.createRenderRoot();
|
const root = super.createRenderRoot();
|
||||||
|
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "wrapper";
|
||||||
wrapper.innerHTML = html`
|
wrapper.innerHTML = html`
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<span class="header-title">
|
<span class="header-title">
|
||||||
|
|
|
||||||
|
|
@ -49,10 +49,18 @@ const styles = css`
|
||||||
background: rgba(255, 255, 255, 0.2);
|
background: rgba(255, 255, 255, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: calc(100% - 36px);
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -151,10 +159,15 @@ const styles = css`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-area img {
|
.preview-area img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
@ -294,6 +307,7 @@ export class FolkKiCAD extends FolkShape {
|
||||||
const root = super.createRenderRoot();
|
const root = super.createRenderRoot();
|
||||||
|
|
||||||
const wrapper = document.createElement("div");
|
const wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "wrapper";
|
||||||
wrapper.innerHTML = html`
|
wrapper.innerHTML = html`
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<span class="header-title">
|
<span class="header-title">
|
||||||
|
|
|
||||||
|
|
@ -118,16 +118,21 @@ async function generateAndPlaceImage(args: Record<string, any>): Promise<any> {
|
||||||
const data = await res.json() as any;
|
const data = await res.json() as any;
|
||||||
if (!data.url) return { error: "Image generation failed", details: data };
|
if (!data.url) return { error: "Image generation failed", details: data };
|
||||||
|
|
||||||
// Download the image to a local path inside the Scribus container
|
// Download the image and save to shared volume (rspace-files, mounted in both containers)
|
||||||
const imageUrl = data.url;
|
const imageUrl = data.url;
|
||||||
const downloadRes = await fetch(imageUrl, { signal: AbortSignal.timeout(30_000) });
|
const downloadRes = await fetch(imageUrl, { signal: AbortSignal.timeout(30_000) });
|
||||||
if (!downloadRes.ok) return { error: "Failed to download generated image" };
|
if (!downloadRes.ok) return { error: "Failed to download generated image" };
|
||||||
|
|
||||||
|
const { writeFile, mkdir } = await import("node:fs/promises");
|
||||||
const imageName = `gen_${Date.now()}.png`;
|
const imageName = `gen_${Date.now()}.png`;
|
||||||
const imagePath = `/data/designs/_generated/${imageName}`;
|
const imageDir = "/data/files/generated";
|
||||||
|
const imagePath = `${imageDir}/${imageName}`;
|
||||||
|
|
||||||
// Write image to bridge container via a bridge command
|
await mkdir(imageDir, { recursive: true });
|
||||||
// For now, place the frame with the URL reference
|
const imageBytes = Buffer.from(await downloadRes.arrayBuffer());
|
||||||
|
await writeFile(imagePath, imageBytes);
|
||||||
|
|
||||||
|
// Place the image frame in Scribus — path is accessible via shared rspace-files volume
|
||||||
const placeResult = await bridgeCommand("add_image_frame", {
|
const placeResult = await bridgeCommand("add_image_frame", {
|
||||||
x: args.x,
|
x: args.x,
|
||||||
y: args.y,
|
y: args.y,
|
||||||
|
|
|
||||||
|
|
@ -106,14 +106,24 @@ export function txExplorerLink(txHash: string, chainId: string): string {
|
||||||
return `${base}/tx/${txHash}`;
|
return `${base}/tx/${txHash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Convert raw wei string to human-readable number, preserving precision for large values */
|
||||||
|
function weiToHuman(raw: string, decimals: number): number {
|
||||||
|
try {
|
||||||
|
const wei = BigInt(raw || "0");
|
||||||
|
const divisor = 10n ** BigInt(decimals);
|
||||||
|
return Number(wei / divisor) + Number(wei % divisor) / Number(divisor);
|
||||||
|
} catch {
|
||||||
|
return parseFloat(raw || "0") / Math.pow(10, decimals);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getTransferValue(transfer: any): number {
|
export function getTransferValue(transfer: any): number {
|
||||||
if (transfer.type === "ERC20_TRANSFER" || transfer.transferType === "ERC20_TRANSFER") {
|
if (transfer.type === "ERC20_TRANSFER" || transfer.transferType === "ERC20_TRANSFER") {
|
||||||
const decimals = transfer.tokenInfo?.decimals || transfer.token?.decimals || 18;
|
const decimals = transfer.tokenInfo?.decimals || transfer.token?.decimals || 18;
|
||||||
const raw = transfer.value || "0";
|
return weiToHuman(transfer.value || "0", decimals);
|
||||||
return parseFloat(raw) / Math.pow(10, decimals);
|
|
||||||
}
|
}
|
||||||
if (transfer.type === "ETHER_TRANSFER" || transfer.transferType === "ETHER_TRANSFER") {
|
if (transfer.type === "ETHER_TRANSFER" || transfer.transferType === "ETHER_TRANSFER") {
|
||||||
return parseFloat(transfer.value || "0") / 1e18;
|
return weiToHuman(transfer.value || "0", 18);
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
@ -134,14 +144,15 @@ const STABLECOINS = new Set([
|
||||||
"GHO", "PYUSD", "DOLA", "Yield-USD", "yUSD",
|
"GHO", "PYUSD", "DOLA", "Yield-USD", "yUSD",
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Approximate USD prices for major non-stablecoin tokens (updated periodically)
|
// Approximate USD prices for major non-stablecoin tokens (updated 2026-03-25 from CoinGecko)
|
||||||
const NATIVE_APPROX_USD: Record<string, number> = {
|
const NATIVE_APPROX_USD: Record<string, number> = {
|
||||||
ETH: 2500, WETH: 2500, stETH: 2500, cbETH: 2500, rETH: 2800, wstETH: 2900,
|
ETH: 2165, WETH: 2165, stETH: 2165, cbETH: 2165, rETH: 2500, wstETH: 2665,
|
||||||
MATIC: 0.40, POL: 0.40, WMATIC: 0.40,
|
MATIC: 0.20, POL: 0.20, WMATIC: 0.20,
|
||||||
BNB: 600, WBNB: 600,
|
BNB: 600, WBNB: 600,
|
||||||
AVAX: 35, WAVAX: 35,
|
AVAX: 20, WAVAX: 20,
|
||||||
xDAI: 1, WXDAI: 1,
|
xDAI: 1, WXDAI: 1,
|
||||||
CELO: 0.50, GNO: 250,
|
CELO: 0.30, GNO: 129,
|
||||||
|
SAFE: 0.10, COW: 0.21, ENS: 6.11, LDO: 0.30, BAL: 0.15,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function estimateUSD(value: number, symbol: string): number | null {
|
export function estimateUSD(value: number, symbol: string): number | null {
|
||||||
|
|
@ -217,7 +228,7 @@ export function transformToTimelineData(
|
||||||
// Fallback: parse from dataDecoded or direct value
|
// Fallback: parse from dataDecoded or direct value
|
||||||
if (txTransfers.length === 0) {
|
if (txTransfers.length === 0) {
|
||||||
if (tx.value && tx.value !== "0") {
|
if (tx.value && tx.value !== "0") {
|
||||||
const val = parseFloat(tx.value) / 1e18;
|
const val = weiToHuman(tx.value, 18);
|
||||||
const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
|
const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
|
||||||
txTransfers.push({ to: tx.to, value: val, symbol: sym, usd: estimateUSD(val, sym) });
|
txTransfers.push({ to: tx.to, value: val, symbol: sym, usd: estimateUSD(val, sym) });
|
||||||
}
|
}
|
||||||
|
|
@ -226,7 +237,7 @@ export function transformToTimelineData(
|
||||||
const params = tx.dataDecoded.parameters || [];
|
const params = tx.dataDecoded.parameters || [];
|
||||||
const to = params.find((p: any) => p.name === "to")?.value;
|
const to = params.find((p: any) => p.name === "to")?.value;
|
||||||
const rawVal = params.find((p: any) => p.name === "value")?.value || "0";
|
const rawVal = params.find((p: any) => p.name === "value")?.value || "0";
|
||||||
const val = parseFloat(rawVal) / 1e18;
|
const val = weiToHuman(rawVal, 18);
|
||||||
txTransfers.push({ to, value: val, symbol: "Token", usd: null });
|
txTransfers.push({ to, value: val, symbol: "Token", usd: null });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -235,14 +246,14 @@ export function transformToTimelineData(
|
||||||
if (txsParam?.valueDecoded) {
|
if (txsParam?.valueDecoded) {
|
||||||
for (const inner of txsParam.valueDecoded) {
|
for (const inner of txsParam.valueDecoded) {
|
||||||
if (inner.value && inner.value !== "0") {
|
if (inner.value && inner.value !== "0") {
|
||||||
const val = parseFloat(inner.value) / 1e18;
|
const val = weiToHuman(inner.value, 18);
|
||||||
const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
|
const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
|
||||||
txTransfers.push({ to: inner.to, value: val, symbol: sym, usd: estimateUSD(val, sym) });
|
txTransfers.push({ to: inner.to, value: val, symbol: sym, usd: estimateUSD(val, sym) });
|
||||||
}
|
}
|
||||||
if (inner.dataDecoded?.method === "transfer") {
|
if (inner.dataDecoded?.method === "transfer") {
|
||||||
const to2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "to")?.value;
|
const to2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "to")?.value;
|
||||||
const raw2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "value")?.value || "0";
|
const raw2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "value")?.value || "0";
|
||||||
const val2 = parseFloat(raw2) / 1e18;
|
const val2 = weiToHuman(raw2, 18);
|
||||||
txTransfers.push({ to: to2, value: val2, symbol: "Token", usd: null });
|
txTransfers.push({ to: to2, value: val2, symbol: "Token", usd: null });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -334,7 +345,7 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
|
||||||
const txLabel = tx.transfers?.[0]?._toLabel;
|
const txLabel = tx.transfers?.[0]?._toLabel;
|
||||||
|
|
||||||
if (tx.value && tx.value !== "0" && tx.to) {
|
if (tx.value && tx.value !== "0" && tx.to) {
|
||||||
const val = parseFloat(tx.value) / 1e18;
|
const val = weiToHuman(tx.value, 18);
|
||||||
const sym = (chainId && CHAIN_NATIVE_SYMBOL[chainId]) || "ETH";
|
const sym = (chainId && CHAIN_NATIVE_SYMBOL[chainId]) || "ETH";
|
||||||
const key = `${tx.to.toLowerCase()}:${sym}`;
|
const key = `${tx.to.toLowerCase()}:${sym}`;
|
||||||
const existing = outflowAgg.get(key) || { to: tx.to, value: 0, symbol: sym, label: txLabel };
|
const existing = outflowAgg.get(key) || { to: tx.to, value: 0, symbol: sym, label: txLabel };
|
||||||
|
|
@ -347,7 +358,7 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
|
||||||
const to = params.find((p: any) => p.name === "to")?.value;
|
const to = params.find((p: any) => p.name === "to")?.value;
|
||||||
const rawVal = params.find((p: any) => p.name === "value")?.value || "0";
|
const rawVal = params.find((p: any) => p.name === "value")?.value || "0";
|
||||||
if (to) {
|
if (to) {
|
||||||
const val = parseFloat(rawVal) / 1e18;
|
const val = weiToHuman(rawVal, 18);
|
||||||
const key = `${to.toLowerCase()}:Token`;
|
const key = `${to.toLowerCase()}:Token`;
|
||||||
const existing = outflowAgg.get(key) || { to, value: 0, symbol: "Token" };
|
const existing = outflowAgg.get(key) || { to, value: 0, symbol: "Token" };
|
||||||
existing.value += val;
|
existing.value += val;
|
||||||
|
|
@ -360,7 +371,7 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
|
||||||
if (txsParam?.valueDecoded) {
|
if (txsParam?.valueDecoded) {
|
||||||
for (const inner of txsParam.valueDecoded) {
|
for (const inner of txsParam.valueDecoded) {
|
||||||
if (inner.value && inner.value !== "0" && inner.to) {
|
if (inner.value && inner.value !== "0" && inner.to) {
|
||||||
const val = parseFloat(inner.value) / 1e18;
|
const val = weiToHuman(inner.value, 18);
|
||||||
const sym = (chainId && CHAIN_NATIVE_SYMBOL[chainId]) || "ETH";
|
const sym = (chainId && CHAIN_NATIVE_SYMBOL[chainId]) || "ETH";
|
||||||
const key = `${inner.to.toLowerCase()}:${sym}`;
|
const key = `${inner.to.toLowerCase()}:${sym}`;
|
||||||
const existing = outflowAgg.get(key) || { to: inner.to, value: 0, symbol: sym };
|
const existing = outflowAgg.get(key) || { to: inner.to, value: 0, symbol: sym };
|
||||||
|
|
@ -371,7 +382,7 @@ export function transformToSankeyData(chainData: any, safeAddress: string, chain
|
||||||
const to2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "to")?.value;
|
const to2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "to")?.value;
|
||||||
const raw2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "value")?.value || "0";
|
const raw2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "value")?.value || "0";
|
||||||
if (to2) {
|
if (to2) {
|
||||||
const val2 = parseFloat(raw2) / 1e18;
|
const val2 = weiToHuman(raw2, 18);
|
||||||
const key = `${to2.toLowerCase()}:Token`;
|
const key = `${to2.toLowerCase()}:Token`;
|
||||||
const existing = outflowAgg.get(key) || { to: to2, value: 0, symbol: "Token" };
|
const existing = outflowAgg.get(key) || { to: to2, value: 0, symbol: "Token" };
|
||||||
existing.value += val2;
|
existing.value += val2;
|
||||||
|
|
@ -513,7 +524,7 @@ export function transformToMultichainData(
|
||||||
const txLabel = tx.transfers?.[0]?._toLabel;
|
const txLabel = tx.transfers?.[0]?._toLabel;
|
||||||
|
|
||||||
if (tx.value && tx.value !== "0" && tx.to) {
|
if (tx.value && tx.value !== "0" && tx.to) {
|
||||||
const val = parseFloat(tx.value) / 1e18;
|
const val = weiToHuman(tx.value, 18);
|
||||||
const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
|
const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
|
||||||
outTransfers.push({ to: tx.to, value: val, symbol: sym, label: txLabel });
|
outTransfers.push({ to: tx.to, value: val, symbol: sym, label: txLabel });
|
||||||
}
|
}
|
||||||
|
|
@ -522,7 +533,7 @@ export function transformToMultichainData(
|
||||||
const params = tx.dataDecoded.parameters || [];
|
const params = tx.dataDecoded.parameters || [];
|
||||||
const to = params.find((p: any) => p.name === "to")?.value;
|
const to = params.find((p: any) => p.name === "to")?.value;
|
||||||
const rawVal = params.find((p: any) => p.name === "value")?.value || "0";
|
const rawVal = params.find((p: any) => p.name === "value")?.value || "0";
|
||||||
if (to) outTransfers.push({ to, value: parseFloat(rawVal) / 1e18, symbol: "Token" });
|
if (to) outTransfers.push({ to, value: weiToHuman(rawVal, 18), symbol: "Token" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tx.dataDecoded?.method === "multiSend") {
|
if (tx.dataDecoded?.method === "multiSend") {
|
||||||
|
|
@ -530,14 +541,14 @@ export function transformToMultichainData(
|
||||||
if (txsParam?.valueDecoded) {
|
if (txsParam?.valueDecoded) {
|
||||||
for (const inner of txsParam.valueDecoded) {
|
for (const inner of txsParam.valueDecoded) {
|
||||||
if (inner.value && inner.value !== "0" && inner.to) {
|
if (inner.value && inner.value !== "0" && inner.to) {
|
||||||
const val = parseFloat(inner.value) / 1e18;
|
const val = weiToHuman(inner.value, 18);
|
||||||
const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
|
const sym = CHAIN_NATIVE_SYMBOL[chainId] || "ETH";
|
||||||
outTransfers.push({ to: inner.to, value: val, symbol: sym });
|
outTransfers.push({ to: inner.to, value: val, symbol: sym });
|
||||||
}
|
}
|
||||||
if (inner.dataDecoded?.method === "transfer") {
|
if (inner.dataDecoded?.method === "transfer") {
|
||||||
const to2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "to")?.value;
|
const to2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "to")?.value;
|
||||||
const raw2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "value")?.value || "0";
|
const raw2 = inner.dataDecoded.parameters?.find((p: any) => p.name === "value")?.value || "0";
|
||||||
if (to2) outTransfers.push({ to: to2, value: parseFloat(raw2) / 1e18, symbol: "Token" });
|
if (to2) outTransfers.push({ to: to2, value: weiToHuman(raw2, 18), symbol: "Token" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -213,7 +213,11 @@ export async function enrichWithPrices(
|
||||||
if (price === 0) return b;
|
if (price === 0) return b;
|
||||||
|
|
||||||
const decimals = b.token?.decimals ?? 18;
|
const decimals = b.token?.decimals ?? 18;
|
||||||
const balHuman = Number(balWei) / Math.pow(10, decimals);
|
// Split BigInt to preserve precision for values > 2^53
|
||||||
|
const divisor = 10n ** BigInt(decimals);
|
||||||
|
const intPart = balWei / divisor;
|
||||||
|
const fracPart = balWei % divisor;
|
||||||
|
const balHuman = Number(intPart) + Number(fracPart) / Number(divisor);
|
||||||
const fiatValue = balHuman * price;
|
const fiatValue = balHuman * price;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,9 @@ export function renderTimeline(
|
||||||
|
|
||||||
function drawContent(scale: any) {
|
function drawContent(scale: any) {
|
||||||
contentGroup.selectAll("*").remove();
|
contentGroup.selectAll("*").remove();
|
||||||
const smoothCurve = d3.curveBasis;
|
// curveStepAfter: balance is constant between txs, steps at each tx.
|
||||||
|
// This ensures the river edges align exactly with the waterfall shapes.
|
||||||
|
const smoothCurve = d3.curveStepAfter;
|
||||||
|
|
||||||
// River glow
|
// River glow
|
||||||
contentGroup.append("path").datum(balanceData).attr("fill", "rgba(0,212,255,0.08)")
|
contentGroup.append("path").datum(balanceData).attr("fill", "rgba(0,212,255,0.08)")
|
||||||
|
|
|
||||||
|
|
@ -176,34 +176,44 @@ export function assembleKicadResult(orch: OrchestrationResult): KicadResult {
|
||||||
let drcResults: { violations: string[] } | null = null;
|
let drcResults: { violations: string[] } | null = null;
|
||||||
|
|
||||||
for (const entry of orch.toolCallLog) {
|
for (const entry of orch.toolCallLog) {
|
||||||
try {
|
const text = entry.result;
|
||||||
const parsed = JSON.parse(entry.result);
|
|
||||||
switch (entry.tool) {
|
// Try JSON parse first, then fall back to regex path extraction
|
||||||
case "export_svg":
|
let parsed: any = null;
|
||||||
// Could be schematic or board SVG — check args or content
|
try { parsed = JSON.parse(text); } catch {}
|
||||||
|
|
||||||
|
switch (entry.tool) {
|
||||||
|
case "export_svg": {
|
||||||
|
const path = parsed?.svg_path || parsed?.path || parsed?.url
|
||||||
|
|| extractPathFromText(text, [".svg"]);
|
||||||
|
if (path) {
|
||||||
if (entry.args.type === "board" || entry.args.board) {
|
if (entry.args.type === "board" || entry.args.board) {
|
||||||
boardSvg = parsed.svg_path || parsed.path || parsed.url || null;
|
boardSvg = path;
|
||||||
} else {
|
} else {
|
||||||
schematicSvg = parsed.svg_path || parsed.path || parsed.url || null;
|
schematicSvg = path;
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
case "run_drc":
|
break;
|
||||||
|
}
|
||||||
|
case "run_drc":
|
||||||
|
if (parsed) {
|
||||||
drcResults = {
|
drcResults = {
|
||||||
violations: parsed.violations || parsed.errors || [],
|
violations: parsed.violations || parsed.errors || [],
|
||||||
};
|
};
|
||||||
break;
|
}
|
||||||
case "export_gerber":
|
break;
|
||||||
gerberUrl = parsed.gerber_path || parsed.path || parsed.url || null;
|
case "export_gerber":
|
||||||
break;
|
gerberUrl = parsed?.gerber_path || parsed?.path || parsed?.url
|
||||||
case "export_bom":
|
|| extractPathFromText(text, [".zip", ".gbr"]);
|
||||||
bomUrl = parsed.bom_path || parsed.path || parsed.url || null;
|
break;
|
||||||
break;
|
case "export_bom":
|
||||||
case "export_pdf":
|
bomUrl = parsed?.bom_path || parsed?.path || parsed?.url
|
||||||
pdfUrl = parsed.pdf_path || parsed.path || parsed.url || null;
|
|| extractPathFromText(text, [".csv", ".json", ".xml"]);
|
||||||
break;
|
break;
|
||||||
}
|
case "export_pdf":
|
||||||
} catch {
|
pdfUrl = parsed?.pdf_path || parsed?.path || parsed?.url
|
||||||
// Non-JSON results are fine (intermediate steps)
|
|| extractPathFromText(text, [".pdf"]);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -232,37 +242,35 @@ export function assembleFreecadResult(orch: OrchestrationResult): FreecadResult
|
||||||
let stepUrl: string | null = null;
|
let stepUrl: string | null = null;
|
||||||
let stlUrl: string | null = null;
|
let stlUrl: string | null = null;
|
||||||
|
|
||||||
|
// Scan ALL tool results for file paths (not just execute_python_script)
|
||||||
for (const entry of orch.toolCallLog) {
|
for (const entry of orch.toolCallLog) {
|
||||||
try {
|
const text = entry.result;
|
||||||
const parsed = JSON.parse(entry.result);
|
|
||||||
// FreeCAD exports via execute_python_script — look for file paths in results
|
// Extract preview image path
|
||||||
if (entry.tool === "execute_python_script" || entry.tool === "execute_script") {
|
if (!previewUrl) {
|
||||||
const text = entry.result.toLowerCase();
|
const pngPath = extractPathFromText(text, [".png", ".jpg", ".jpeg"]);
|
||||||
if (text.includes(".step") || text.includes(".stp")) {
|
if (pngPath) previewUrl = pngPath;
|
||||||
stepUrl = parsed.path || parsed.file_path || extractPathFromText(entry.result, [".step", ".stp"]);
|
}
|
||||||
}
|
|
||||||
if (text.includes(".stl")) {
|
// Extract STEP path
|
||||||
stlUrl = parsed.path || parsed.file_path || extractPathFromText(entry.result, [".stl"]);
|
if (!stepUrl) {
|
||||||
}
|
stepUrl = extractPathFromText(text, [".step", ".stp"]);
|
||||||
}
|
}
|
||||||
// save_document may also produce a path
|
|
||||||
if (entry.tool === "save_document") {
|
// Extract STL path
|
||||||
const path = parsed.path || parsed.file_path || null;
|
if (!stlUrl) {
|
||||||
if (path && (path.endsWith(".FCStd") || path.endsWith(".fcstd"))) {
|
stlUrl = extractPathFromText(text, [".stl"]);
|
||||||
// Not directly servable, but note it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Try extracting paths from raw text
|
|
||||||
stepUrl = stepUrl || extractPathFromText(entry.result, [".step", ".stp"]);
|
|
||||||
stlUrl = stlUrl || extractPathFromText(entry.result, [".stl"]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert internal paths to servable URLs
|
||||||
|
// /data/files/generated/... is served at /data/files/generated/...
|
||||||
|
const toUrl = (p: string | null) => p; // paths are already servable
|
||||||
|
|
||||||
return {
|
return {
|
||||||
previewUrl,
|
previewUrl: toUrl(previewUrl),
|
||||||
stepUrl,
|
stepUrl: toUrl(stepUrl),
|
||||||
stlUrl,
|
stlUrl: toUrl(stlUrl),
|
||||||
summary: orch.finalMessage,
|
summary: orch.finalMessage,
|
||||||
toolCallLog: orch.toolCallLog,
|
toolCallLog: orch.toolCallLog,
|
||||||
};
|
};
|
||||||
|
|
@ -314,6 +322,17 @@ Follow this workflow:
|
||||||
5. save_document to save the FreeCAD file
|
5. save_document to save the FreeCAD file
|
||||||
6. execute_python_script to export STEP: Part.export([obj], "/data/files/generated/freecad-<id>/model.step")
|
6. execute_python_script to export STEP: Part.export([obj], "/data/files/generated/freecad-<id>/model.step")
|
||||||
7. execute_python_script to export STL: Mesh.export([obj], "/data/files/generated/freecad-<id>/model.stl")
|
7. execute_python_script to export STL: Mesh.export([obj], "/data/files/generated/freecad-<id>/model.stl")
|
||||||
|
8. execute_python_script to render a preview PNG (do this last, ignore errors if GUI unavailable):
|
||||||
|
try:
|
||||||
|
import FreeCADGui
|
||||||
|
FreeCADGui.showMainWindow()
|
||||||
|
view = FreeCADGui.ActiveDocument.ActiveView
|
||||||
|
view.viewIsometric()
|
||||||
|
view.fitAll()
|
||||||
|
view.saveImage("/data/files/generated/freecad-<id>/preview.png", 1024, 1024, "White")
|
||||||
|
print("/data/files/generated/freecad-<id>/preview.png")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Preview rendering not available: {e}")
|
||||||
|
|
||||||
Important:
|
Important:
|
||||||
- Use /data/files/generated/freecad-${Date.now()}/ as the working directory
|
- Use /data/files/generated/freecad-${Date.now()}/ as the working directory
|
||||||
|
|
@ -321,4 +340,7 @@ Important:
|
||||||
- For complex shapes, build up from primitives with boolean operations
|
- For complex shapes, build up from primitives with boolean operations
|
||||||
- Wall thickness should be at least 1mm for 3D printing
|
- Wall thickness should be at least 1mm for 3D printing
|
||||||
- Always export both STEP (for CAD) and STL (for 3D printing)
|
- Always export both STEP (for CAD) and STL (for 3D printing)
|
||||||
|
- Try to render a preview PNG as the final step — this is displayed to the user
|
||||||
|
- When exporting files, always print() the FULL absolute path so it can be extracted from output
|
||||||
|
- Example: after Part.export(...), add print("/data/files/generated/freecad-xxx/model.step")
|
||||||
- If a tool call fails, try an alternative approach`;
|
- If a tool call fails, try an alternative approach`;
|
||||||
|
|
|
||||||
|
|
@ -618,6 +618,10 @@ export function receiveSyncMessage(
|
||||||
|
|
||||||
const peerState = getPeerSyncState(slug, peerId);
|
const peerState = getPeerSyncState(slug, peerId);
|
||||||
|
|
||||||
|
// Snapshot server-authoritative fields before sync merge
|
||||||
|
const prevVisibility = doc.meta?.visibility;
|
||||||
|
const prevOwnerDID = doc.meta?.ownerDID;
|
||||||
|
|
||||||
// Apply incoming sync message
|
// Apply incoming sync message
|
||||||
const result = Automerge.receiveSyncMessage(
|
const result = Automerge.receiveSyncMessage(
|
||||||
doc,
|
doc,
|
||||||
|
|
@ -625,9 +629,23 @@ export function receiveSyncMessage(
|
||||||
message
|
message
|
||||||
);
|
);
|
||||||
|
|
||||||
const newDoc = result[0];
|
let newDoc = result[0];
|
||||||
const newSyncState = result[1];
|
const newSyncState = result[1];
|
||||||
|
|
||||||
|
// Pin server-authoritative fields — clients must not overwrite these via sync.
|
||||||
|
// Visibility and ownership can only be changed through the authenticated API.
|
||||||
|
if (newDoc !== doc) {
|
||||||
|
const visChanged = newDoc.meta?.visibility !== prevVisibility;
|
||||||
|
const ownerChanged = newDoc.meta?.ownerDID !== prevOwnerDID;
|
||||||
|
if (visChanged || ownerChanged) {
|
||||||
|
console.warn(`[Store] Sync tried to change authoritative fields in ${slug} — reverting (vis: ${prevVisibility}→${newDoc.meta?.visibility}, owner: ${prevOwnerDID}→${newDoc.meta?.ownerDID})`);
|
||||||
|
newDoc = Automerge.change(newDoc, 'Pin server-authoritative fields', (d) => {
|
||||||
|
if (visChanged && prevVisibility) d.meta.visibility = prevVisibility;
|
||||||
|
if (ownerChanged && prevOwnerDID) d.meta.ownerDID = prevOwnerDID;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
communities.set(slug, newDoc);
|
communities.set(slug, newDoc);
|
||||||
peerState.syncState = newSyncState;
|
peerState.syncState = newSyncState;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -198,23 +198,37 @@ app.get("/collect.js", async (c) => {
|
||||||
|
|
||||||
// ── Serve generated files from /data/files/generated/ and /api/files/generated/ ──
|
// ── Serve generated files from /data/files/generated/ and /api/files/generated/ ──
|
||||||
// The /api/ route avoids Cloudflare/Traefik redirecting /data/ paths
|
// The /api/ route avoids Cloudflare/Traefik redirecting /data/ paths
|
||||||
|
const GENERATED_MIME: Record<string, string> = {
|
||||||
|
png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp",
|
||||||
|
glb: "model/gltf-binary", gltf: "model/gltf+json",
|
||||||
|
step: "application/step", stp: "application/step",
|
||||||
|
stl: "application/sla", fcstd: "application/octet-stream",
|
||||||
|
svg: "image/svg+xml", pdf: "application/pdf",
|
||||||
|
};
|
||||||
|
|
||||||
function serveGeneratedFile(c: any) {
|
function serveGeneratedFile(c: any) {
|
||||||
|
// Support both flat files and subdirectory paths (e.g. freecad-xxx/model.step)
|
||||||
const filename = c.req.param("filename");
|
const filename = c.req.param("filename");
|
||||||
if (!filename || filename.includes("..") || filename.includes("/")) {
|
const subdir = c.req.param("subdir");
|
||||||
|
const relPath = subdir ? `${subdir}/${filename}` : filename;
|
||||||
|
if (!relPath || relPath.includes("..")) {
|
||||||
return c.json({ error: "Invalid filename" }, 400);
|
return c.json({ error: "Invalid filename" }, 400);
|
||||||
}
|
}
|
||||||
const dir = resolve(process.env.FILES_DIR || "./data/files", "generated");
|
const dir = resolve(process.env.FILES_DIR || "./data/files", "generated");
|
||||||
const filePath = resolve(dir, filename);
|
const filePath = resolve(dir, relPath);
|
||||||
|
// Ensure resolved path stays within generated dir
|
||||||
|
if (!filePath.startsWith(dir)) return c.json({ error: "Invalid path" }, 400);
|
||||||
const file = Bun.file(filePath);
|
const file = Bun.file(filePath);
|
||||||
return file.exists().then((exists: boolean) => {
|
return file.exists().then((exists: boolean) => {
|
||||||
if (!exists) return c.notFound();
|
if (!exists) return c.notFound();
|
||||||
const ext = filename.split(".").pop() || "";
|
const ext = filePath.split(".").pop()?.toLowerCase() || "";
|
||||||
const mimeMap: Record<string, string> = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp", glb: "model/gltf-binary", gltf: "model/gltf+json" };
|
return new Response(file, { headers: { "Content-Type": GENERATED_MIME[ext] || "application/octet-stream", "Cache-Control": "public, max-age=86400" } });
|
||||||
return new Response(file, { headers: { "Content-Type": mimeMap[ext] || "application/octet-stream", "Cache-Control": "public, max-age=86400" } });
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
app.get("/data/files/generated/:filename", serveGeneratedFile);
|
app.get("/data/files/generated/:filename", serveGeneratedFile);
|
||||||
|
app.get("/data/files/generated/:subdir/:filename", serveGeneratedFile);
|
||||||
app.get("/api/files/generated/:filename", serveGeneratedFile);
|
app.get("/api/files/generated/:filename", serveGeneratedFile);
|
||||||
|
app.get("/api/files/generated/:subdir/:filename", serveGeneratedFile);
|
||||||
|
|
||||||
// ── Link preview / unfurl API ──
|
// ── Link preview / unfurl API ──
|
||||||
const linkPreviewCache = new Map<string, { title: string; description: string; image: string | null; domain: string; fetchedAt: number }>();
|
const linkPreviewCache = new Map<string, { title: string; description: string; image: string | null; domain: string; fetchedAt: number }>();
|
||||||
|
|
@ -2922,8 +2936,8 @@ async function serveStatic(path: string, url?: URL): Promise<Response | null> {
|
||||||
const headers: Record<string, string> = { "Content-Type": getContentType(path) };
|
const headers: Record<string, string> = { "Content-Type": getContentType(path) };
|
||||||
if (url?.searchParams.has("v")) {
|
if (url?.searchParams.has("v")) {
|
||||||
headers["Cache-Control"] = "public, max-age=31536000, immutable";
|
headers["Cache-Control"] = "public, max-age=31536000, immutable";
|
||||||
} else if (path.endsWith(".html")) {
|
} else if (path.endsWith(".html") || path === "sw.js" || path === "manifest.json") {
|
||||||
// HTML must revalidate so browsers pick up new hashed JS/CSS references
|
// HTML, service worker, and manifest must revalidate every time
|
||||||
headers["Cache-Control"] = "no-cache";
|
headers["Cache-Control"] = "no-cache";
|
||||||
} else if (path.startsWith("assets/")) {
|
} else if (path.startsWith("assets/")) {
|
||||||
// Vite content-hashed assets are safe to cache long-term
|
// Vite content-hashed assets are safe to cache long-term
|
||||||
|
|
|
||||||
|
|
@ -278,7 +278,7 @@ export function renderShell(opts: ShellOptions): string {
|
||||||
import '/shell.js';
|
import '/shell.js';
|
||||||
// ── Service worker registration ──
|
// ── Service worker registration ──
|
||||||
if ("serviceWorker" in navigator && location.hostname !== "localhost") {
|
if ("serviceWorker" in navigator && location.hostname !== "localhost") {
|
||||||
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
navigator.serviceWorker.register("/sw.js?v=4").catch(() => {});
|
||||||
}
|
}
|
||||||
// ── Install prompt capture ──
|
// ── Install prompt capture ──
|
||||||
window.addEventListener("beforeinstallprompt", (e) => {
|
window.addEventListener("beforeinstallprompt", (e) => {
|
||||||
|
|
@ -2113,7 +2113,7 @@ export function renderModuleLanding(opts: ModuleLandingOptions): string {
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import '/shell.js';
|
import '/shell.js';
|
||||||
if ("serviceWorker" in navigator && location.hostname !== "localhost") {
|
if ("serviceWorker" in navigator && location.hostname !== "localhost") {
|
||||||
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
navigator.serviceWorker.register("/sw.js?v=4").catch(() => {});
|
||||||
}
|
}
|
||||||
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
||||||
try {
|
try {
|
||||||
|
|
@ -2448,7 +2448,7 @@ export function renderSubPageInfo(opts: SubPageInfoOptions): string {
|
||||||
<script type="module">
|
<script type="module">
|
||||||
import '/shell.js';
|
import '/shell.js';
|
||||||
if ("serviceWorker" in navigator && location.hostname !== "localhost") {
|
if ("serviceWorker" in navigator && location.hostname !== "localhost") {
|
||||||
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
navigator.serviceWorker.register("/sw.js?v=4").catch(() => {});
|
||||||
}
|
}
|
||||||
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
document.querySelector('rstack-app-switcher')?.setModules(${moduleListJSON});
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -2613,7 +2613,7 @@
|
||||||
|
|
||||||
// Register service worker for offline support
|
// Register service worker for offline support
|
||||||
if ("serviceWorker" in navigator && window.location.hostname !== "localhost") {
|
if ("serviceWorker" in navigator && window.location.hostname !== "localhost") {
|
||||||
navigator.serviceWorker.register("/sw.js").then((reg) => {
|
navigator.serviceWorker.register("/sw.js?v=4").then((reg) => {
|
||||||
console.log("[Canvas] Service worker registered, scope:", reg.scope);
|
console.log("[Canvas] Service worker registered, scope:", reg.scope);
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
console.warn("[Canvas] Service worker registration failed:", err);
|
console.warn("[Canvas] Service worker registration failed:", err);
|
||||||
|
|
|
||||||
|
|
@ -300,7 +300,7 @@
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
if ("serviceWorker" in navigator && location.hostname !== "localhost") {
|
if ("serviceWorker" in navigator && location.hostname !== "localhost") {
|
||||||
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
navigator.serviceWorker.register("/sw.js?v=4").catch(() => {});
|
||||||
}
|
}
|
||||||
import { RStackIdentity } from "@shared/components/rstack-identity";
|
import { RStackIdentity } from "@shared/components/rstack-identity";
|
||||||
import { RStackAppSwitcher } from "@shared/components/rstack-app-switcher";
|
import { RStackAppSwitcher } from "@shared/components/rstack-app-switcher";
|
||||||
|
|
|
||||||
|
|
@ -523,7 +523,7 @@
|
||||||
|
|
||||||
<script type="module">
|
<script type="module">
|
||||||
if ("serviceWorker" in navigator && location.hostname !== "localhost") {
|
if ("serviceWorker" in navigator && location.hostname !== "localhost") {
|
||||||
navigator.serviceWorker.register("/sw.js").catch(() => {});
|
navigator.serviceWorker.register("/sw.js?v=4").catch(() => {});
|
||||||
}
|
}
|
||||||
import { RStackIdentity, isAuthenticated, getAccessToken } from "@shared/components/rstack-identity";
|
import { RStackIdentity, isAuthenticated, getAccessToken } from "@shared/components/rstack-identity";
|
||||||
import { RStackAppSwitcher } from "@shared/components/rstack-app-switcher";
|
import { RStackAppSwitcher } from "@shared/components/rstack-app-switcher";
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
/// <reference lib="webworker" />
|
/// <reference lib="webworker" />
|
||||||
declare const self: ServiceWorkerGlobalScope;
|
declare const self: ServiceWorkerGlobalScope;
|
||||||
|
|
||||||
const CACHE_VERSION = "rspace-v3";
|
const CACHE_VERSION = "rspace-v4";
|
||||||
const STATIC_CACHE = `${CACHE_VERSION}-static`;
|
const STATIC_CACHE = `${CACHE_VERSION}-static`;
|
||||||
const HTML_CACHE = `${CACHE_VERSION}-html`;
|
const HTML_CACHE = `${CACHE_VERSION}-html`;
|
||||||
const API_CACHE = `${CACHE_VERSION}-api`;
|
const API_CACHE = `${CACHE_VERSION}-api`;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue