Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/img/opensauce.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 41 additions & 0 deletions components/LogoHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,41 @@
</p>
</div>

<!-- Open Sauce event branding variant -->
<div
v-else-if="eventMode.enabled && eventMode.eventTag === 'Open Sauce'"
class="logo-header-content"
>
<div class="logo-container">
<div class="logo-glow">
<img
v-if="themeStore.isDark"
src="@/assets/img/logo.svg"
class="logo-icon"
alt="Meshtastic Logo"
>
<img
v-else
src="@/assets/img/logo-dark.svg"
class="logo-icon"
alt="Meshtastic Logo"
>
</div>
<span class="logo-separator">×</span>
<img
src="@/assets/img/opensauce.webp"
class="logo-icon-event logo-icon-opensauce"
alt="Open Sauce 2026 Logo"
>
</div>
<h1 class="logo-title">
<span class="logo-title-gradient">{{ $t('header_title') }}</span>
</h1>
<p class="logo-tagline">
{{ eventMode.tagline || eventMode.eventName }}
</p>
</div>

<!-- Hamvention event branding variant -->
<div
v-else-if="eventMode.enabled && eventMode.eventTag === 'Hamvention'"
Expand Down Expand Up @@ -246,6 +281,12 @@ const { eventMode } = useEventMode()
border-radius: 0.5rem;
}

/* The Open Sauce logo ships white for dark headers; darken to a solid
silhouette in light mode so it stays visible on a light background. */
:root[data-theme="light"] .logo-icon-opensauce {
filter: brightness(0);
}

@media (min-width: 640px) {
.logo-icon-event {
height: 6rem;
Expand Down
19 changes: 14 additions & 5 deletions plugins/eventMode.client.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fetchApiManifest, fetchBundledManifest, hostMatches, manifestEditionToEventMode, resolveActiveEdition, applyEventTheme } from '~/utils/eventManifest'
import { fetchApiManifest, fetchBundledManifest, hostMatches, isFirmwareDowngrade, manifestEditionToEventMode, resolveActiveEdition, applyEventTheme } from '~/utils/eventManifest'
import type { EventFirmwareResponse } from '~/types/eventFirmware'
import { setActiveEventMode, staticEventModes } from '~/types/resources'
import { eventMode, setActiveEventMode, staticEventModes } from '~/types/resources'

// Resolve the active event edition for the current host. ssr:false guarantees
// window is available, and Nuxt awaits async plugins — so we gate first paint
Expand All @@ -12,10 +12,19 @@ export default defineNuxtPlugin(async () => {
// ?event=DEFCON lets you preview an edition on localhost.
const override = new URLSearchParams(window.location.search).get('event')

const applyManifest = (manifest: EventFirmwareResponse): boolean => {
const applyManifest = (manifest: EventFirmwareResponse, opts: { preserveShippedFirmware?: boolean } = {}): boolean => {
const edition = resolveActiveEdition(manifest, hostname, override)
if (!edition) return false
setActiveEventMode(manifestEditionToEventMode(edition))
const next = manifestEditionToEventMode(edition)
// A background API refresh must not DOWNGRADE an already-resolved edition:
// if the live manifest's build for the same event hasn't shipped yet but we
// already have a flashable build for it (e.g. from the bundled snapshot),
// keep what we have instead of reverting the UI to "coming soon". The live
// API still wins the moment it carries a real build.
if (opts.preserveShippedFirmware && isFirmwareDowngrade(eventMode, next)) {
return true
}
setActiveEventMode(next)
applyEventTheme(edition.theme)
return true
}
Expand All @@ -40,7 +49,7 @@ export default defineNuxtPlugin(async () => {
// this app was deployed). Never blocks mount.
fetchApiManifest()
.then((api) => {
if (api) applyManifest(api)
if (api) applyManifest(api, { preserveShippedFirmware: true })
})
.catch(() => {})
})
14 changes: 7 additions & 7 deletions public/data/event_firmware.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,19 +50,19 @@
{ "label": "Event Website", "url": "https://opensauce.com" }
],
"theme": {
"name": null,
"tagline": null,
"name": "Makers & Mesh",
"tagline": "Where makers, science, and mesh collide.",
"colors": { "primary": "#E94F1D", "secondary": null, "accent": null },
"palette": ["#E94F1D"],
"fonts": null
},
"firmware": {
"slug": "opensauce2026",
"version": null,
"id": null,
"title": null,
"zipUrl": null,
"releaseNotes": null
"version": "2.7.26.004b486",
"id": "v2.7.26.004b486",
"title": "Meshtastic Firmware 2.7.26.004b486",
"zipUrl": "https://github.com/meshtastic/meshtastic.github.io/raw/master/event/opensauce2026/firmware-2.7.26.004b486.zip",
"releaseNotes": "## Welcome to Open Sauce 2026! 🔧\n\nThis firmware has been customized for Open Sauce with factory default configurations.\n\n### ⚠️ Important: Backup Before Flashing\n\nIf your device has existing settings or encryption keys, **backup your keys / configurations** before proceeding. Flashing will reset your device to factory settings for the event.\n\n### Quick Start\n\n1. Ensure a **data-capable USB cable** is connected\n2. Select your device type\n3. Choose \"Full Erase and Install\"\n4. After flashing, download the Meshtastic app and pair via Bluetooth\n5. If you updated from a previous version or installed a UF2 on an NRF52 device, you will need to perform a factory reset on the device to activate the Open Sauce mode.\n\n**Happy meshing from San Mateo!**"
}
},
{
Expand Down
73 changes: 73 additions & 0 deletions utils/eventManifest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { describe, it, expect } from 'vitest'
import { manifestEditionToEventMode, isFirmwareDowngrade } from './eventManifest'
import type { EventFirmwareEdition } from '~/types/eventFirmware'

// A manifest edition whose custom build has shipped (Open Sauce-shaped).
const shippedEdition: EventFirmwareEdition = {
edition: 'OPEN_SAUCE',
displayName: 'Open Sauce 2026',
welcomeMessage: 'Welcome to Open Sauce! 🔧',
tag: 'Open Sauce',
domain: 'opensauce.meshtastic.org',
theme: { colors: { primary: '#E94F1D' } },
firmware: {
slug: 'opensauce2026',
version: '2.7.26.004b486',
id: 'v2.7.26.004b486',
title: 'Meshtastic Firmware 2.7.26.004b486',
zipUrl: 'https://example.com/firmware-2.7.26.004b486.zip',
releaseNotes: '## Welcome',
},
}

// The same edition before its build ships (version/id/zip still null) — the
// shape the live API currently returns for Open Sauce.
const comingSoonEdition: EventFirmwareEdition = {
...shippedEdition,
firmware: { slug: 'opensauce2026', version: null, id: null, title: null, zipUrl: null, releaseNotes: null },
}

describe('manifestEditionToEventMode', () => {
it('maps a shipped build into a flashable event config', () => {
const cfg = manifestEditionToEventMode(shippedEdition)
expect(cfg.enabled).toBe(true)
expect(cfg.eventName).toBe('Open Sauce 2026')
expect(cfg.pathPrefix).toBe('opensauce2026')
expect(cfg.firmware.id).toBe('v2.7.26.004b486')
expect(cfg.firmware.zip_url).toBe('https://example.com/firmware-2.7.26.004b486.zip')
})

it('leaves firmware id empty ("coming soon") when the build has not shipped', () => {
const cfg = manifestEditionToEventMode(comingSoonEdition)
expect(cfg.enabled).toBe(true)
expect(cfg.firmware.id).toBe('')
// Falls back to the display name so the header still reads sensibly.
expect(cfg.firmware.title).toBe('Open Sauce 2026')
})
})

describe('isFirmwareDowngrade', () => {
const shipped = manifestEditionToEventMode(shippedEdition)
const comingSoon = manifestEditionToEventMode(comingSoonEdition)

it('flags a lagging API edition (no build) replacing a shipped build for the same event', () => {
expect(isFirmwareDowngrade(shipped, comingSoon)).toBe(true)
})

it('allows a real build to replace a shipped build (upgrade path)', () => {
expect(isFirmwareDowngrade(shipped, shipped)).toBe(false)
})

it('does not flag when there is no shipped build to protect', () => {
expect(isFirmwareDowngrade(comingSoon, comingSoon)).toBe(false)
})

it('does not flag across different events', () => {
const other = manifestEditionToEventMode({ ...comingSoonEdition, domain: 'defcon.meshtastic.org' })
expect(isFirmwareDowngrade(shipped, other)).toBe(false)
})

it('does not flag a disabled current config', () => {
expect(isFirmwareDowngrade({ ...shipped, enabled: false }, comingSoon)).toBe(false)
})
})
15 changes: 15 additions & 0 deletions utils/eventManifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,21 @@ export function manifestEditionToEventMode(edition: EventFirmwareEdition): Event
}
}

/**
* Whether applying `next` would DOWNGRADE the currently-active event: it's the
* same event (matched by non-empty domain) but its build hasn't shipped yet
* (empty firmware id) while we already resolved a flashable build for it. The
* background API refresh uses this to avoid reverting a shipped bundled build
* back to "coming soon" when the live manifest still lags behind.
*/
export function isFirmwareDowngrade(current: EventModeConfig, next: EventModeConfig): boolean {
return current.enabled
&& !!current.firmware.id
&& !next.firmware.id
&& !!current.domain
&& current.domain === next.domain
}

function clampByte(n: number): number {
return Math.max(0, Math.min(255, Math.round(n)))
}
Expand Down
Loading