service worked updates. Needs testing

This commit is contained in:
Max committed 2026-10-28 11:21:19 -07:00
1 parent 5bebe072b2
commit 2d5d23b104
10 files changed
+3196 -81

No files matched your search

+20 -1
View File
@@ -2,7 +2,26 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="description" content="Encrypted notes and scratch pad" />
<!-- PWA. The manifest is a real static file: it used to be injected at
runtime as a base64 data: URI from App.vue, with the production origin
hardcoded, which broke icons and scope outside production. -->
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#000000" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" type="image/svg+xml" href="/img/icons/logo.svg" />
<!-- iOS has no share-target support, but still installs to the home screen.
The apple-touch icon must be the opaque variant -- iOS composites
transparent PNGs onto black and the dark half of the mark vanishes. -->
<link rel="apple-touch-icon" href="/img/icons/apple-touch-icon-180.png" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="Scribe" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<title>Solid Scribe</title>
</head>
<body>
+3 -1
View File
@@ -25,6 +25,8 @@
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.1.2"
"vite": "^7.1.2",
"vite-plugin-pwa": "^2.0.0",
"workbox-window": "^7.4.1"
}
}
+3035 -8
View File
File diff suppressed because it is too large. Load diff
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

+4 -68
View File
@@ -51,6 +51,8 @@
<global-notification />
<update-prompt />
</div>
</template>
@@ -59,12 +61,14 @@
import axios from 'axios'
import GlobalSiteMenu from '@/components/GlobalSiteMenu.vue'
import GlobalNotification from '@/components/GlobalNotificationComponent.vue'
import UpdatePrompt from '@/components/UpdatePrompt.vue'
export default {
name: 'App',
components: {
'global-site-menu': GlobalSiteMenu,
'global-notification': GlobalNotification,
'update-prompt': UpdatePrompt,
},
data: function(){
return {
@@ -83,74 +87,6 @@ export default {
// - Gets new session tokens from server and uses them in app
beforeCreate: function(){
const root = 'https://www.solidscribe.com'
// const root = 'https://192.168.1.164'
const manifest = {
"theme_color":"#000",
"background_color": "#000",
"description": "Take Notes",
"display_override": ["fullscreen", "minimal-ui"],
"display": "standalone",
"icons": [
{
"src": root+"/api/static/assets/logo.png",
"sizes": "495x495",
"type": "image/png",
"purpose": "any"
},
{
"src": root+"/api/static/assets/maskable_icon.png",
"sizes": "826x826",
"type": "image/png",
"purpose": "maskable"
}
],
"screenshots" : [
{
"src": root+"/api/static/assets/lightmode.png",
"sizes": "1000x1080",
"type": "image/png"
},
{
"src": root+"/api/static/assets/darkmode.png",
"sizes": "1000x1081",
"type": "image/png"
}
],
"shortcuts" : [
{
"name": "Files & Links",
"url": " /attachments",
"description": "All Note files and links",
"icons": [{
"src": root+"/api/static/assets/icon1.png",
"sizes": "96x96",
"type": "image/png"
}]
},
{
"name": "Metric Track",
"url": "/metrictrack",
"description": "Metric Tracking",
"icons": [{
"src": root+"/api/static/assets/icon2.png",
"sizes": "96x96",
"type": "image/png"
}]
}
],
"name": "Solid Scribe",
"short_name": "Solid Scribe",
"author": "Max",
"scope": root,
"start_url": window.location.href
}
const link = document.createElement('link')
link.rel = 'manifest'
link.href = `data:application/json;base64,${btoa(JSON.stringify(manifest))}`
document.head.appendChild(link)
//Before all requests going out
axios.interceptors.request.use(
(config) => {
@@ -153,8 +153,10 @@
this.$io.emit('user_connect', token)
localStorage.setItem('loginToken', token)
//Redirect user to notes section after login
this.$router.push('/notes')
//Redirect user to notes section after login, or back to
//wherever they were headed (e.g. an Android share waiting to
//be saved -- see ShareTargetPage.vue)
this.$router.push(this.$route.query.redirect || '/notes')
}
},
register(){
+8
View File
@@ -23,6 +23,7 @@ import './tailwind.css'
import io from 'socket.io-client'
import EventBus from './EventBus'
import Helpers from './Helpers'
import { installLaunchRedirect } from './LaunchPage'
const socket = io({ path: '/socket' })
@@ -31,6 +32,10 @@ router.beforeEach((to, from, next) => {
next()
})
// Installed launches land on "/" and are redirected to the user's chosen
// launch page -- see LaunchPage.js
installLaunchRedirect(router)
const app = createApp(App)
app.config.globalProperties.$io = socket
@@ -41,3 +46,6 @@ app.use(store)
app.use(Helpers)
app.mount('#app')
// After mount, so a failed registration can never block the app rendering
import('./registerServiceWorker')
@@ -44,6 +44,42 @@
</div>
</div>
<!-- App / PWA -->
<h4 class="ui header">
App
</h4>
<div class="ui segment">
<div class="ui doubling grid">
<div class="sixteen wide column">
<p>
Which page the installed app opens to. Set automatically to
whichever page you installed from, and changeable here.
</p>
<div
v-for="option in launchOptions"
:key="option.path"
class="ui compact basic button"
:class="{'green':(launchPage == option.path)}"
v-on:click="chooseLaunchPage(option.path)">
{{ option.label }}
</div>
<div class="ui divider"></div>
<p>
If the app gets stuck on an old version, this clears the stored
copies of its files and reloads. Your notes are not affected.
</p>
<div class="ui compact basic button" v-on:click="clearAppCache()">
<i class="redo icon"></i>
Reset App Cache
</div>
</div>
</div>
</div>
<!-- Enable Two Factor -->
<h4>Two Factor Authentication</h4>
<div class="ui segment">
@@ -165,6 +201,9 @@ import { defineAsyncComponent } from 'vue'
import axios from 'axios'
import { LAUNCH_OPTIONS, getLaunchPage, setLaunchPage } from '@/LaunchPage'
import { resetAppCache } from '@/registerServiceWorker'
export default {
name: 'SettingsPage',
components: {
@@ -177,6 +216,8 @@ import { defineAsyncComponent } from 'vue'
verificationToken: '',
showNewNoteConfirm: false,
launchOptions: LAUNCH_OPTIONS,
launchPage: getLaunchPage(),
themeColors: [
'#21BA45', //Green
'#b5cc18', //Lime
@@ -198,6 +239,14 @@ import { defineAsyncComponent } from 'vue'
}
},
methods: {
chooseLaunchPage(path){
setLaunchPage(path)
this.launchPage = getLaunchPage()
this.$bus.$emit('notification', 'App will open to ' + path)
},
clearAppCache(){
resetAppCache()
},
newQuickNote(){
this.showNewNoteConfirm = true
+2
View File
@@ -13,6 +13,7 @@ const OverviewPage = () => import('@/pages/OverviewPage.vue')
const BookmarkletPage = () => import('@/pages/BookmarkletPage.vue')
const NotFoundPage = () => import('@/pages/NotFoundPage.vue')
const MetrictrackingPage = () => import('@/pages/MetrictrackingPage.vue')
const ShareTargetPage = () => import('@/pages/ShareTargetPage.vue')
export default createRouter({
history: createWebHistory(),
@@ -28,6 +29,7 @@ export default createRouter({
{ path: '/bookmarklet', name: 'Bookmarklet', meta: { title: 'Bookmarklet' }, component: BookmarkletPage },
{ path: '/settings', name: 'Settings', meta: { title: 'Settings' }, component: SettingsPage },
{ path: '/public/note/:id/:token', name: 'Share', meta: { title: 'Shared' }, component: SharePage },
{ path: '/share-target', name: 'Share Target', meta: { title: 'Saving Share' }, component: ShareTargetPage },
{ path: '/quick', name: 'Quick', meta: { title: 'Scratch Pad' }, component: QuickPage },
{ path: '/attachments', name: 'Attachments', meta: { title: 'Attachments' }, component: AttachmentsPage },
{ path: '/attachments/note/:id', name: 'Attachments for Note', meta: { title: 'Attachments for Note' }, component: AttachmentsPage },
+71
View File
@@ -1,8 +1,23 @@
import fs from 'node:fs'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import tailwindcss from '@tailwindcss/vite'
import { VitePWA } from 'vite-plugin-pwa'
import { fileURLToPath, URL } from 'node:url'
// Opt-in HTTPS for the dev server, off by default so the usual
// http://localhost:5173 workflow is unchanged. Only needed if you want to test
// the installed PWA from a phone over the LAN *without* using
// chrome://flags/#unsafely-treat-insecure-origin-as-secure -- and note Chrome
// needs the mkcert root CA installed on the device for the origin to be
// service-worker eligible; clicking through a cert warning is not enough.
// DEV_HTTPS=1 pnpm dev
const certDir = fileURLToPath(new URL('../client/certs', import.meta.url))
const keyPath = `${certDir}/192.168.1.164+4-key.pem`
const certPath = `${certDir}/192.168.1.164+4.pem`
const useHttps =
process.env.DEV_HTTPS === '1' && fs.existsSync(keyPath) && fs.existsSync(certPath)
export default defineConfig({
plugins: [
vue({
@@ -11,6 +26,59 @@ export default defineConfig({
},
}),
tailwindcss(),
VitePWA({
// Updates are user-driven via the prompt in src/registerServiceWorker.js
registerType: 'prompt',
injectRegister: false,
// Lands at dist/sw.js -- root of the served directory, so the worker gets
// root scope without needing a Service-Worker-Allowed header.
filename: 'sw.js',
// We ship public/manifest.json by hand rather than generating one.
manifest: false,
workbox: {
//
// SECURITY INVARIANT -- do not relax without thinking it through.
//
// Solid Scribe is end-to-end encrypted. The service worker precaches
// ONLY static build output and is network-only for /api/*. Caching an
// API response would write decrypted note content into Cache Storage on
// disk, where it survives logout and sits outside the encryption model.
//
// Structurally this holds because globPatterns only ever reads dist/,
// and user content is served by Express from staticFiles/ at
// /api/static -- which never enters dist. The NetworkOnly rule below is
// belt-and-braces: it documents the invariant and stops a future
// runtimeCaching entry from silently widening the net.
//
// woff2 is NOT in Workbox's default globPatterns. Without it the Roboto
// files are skipped and the offline shell renders in a fallback face.
globPatterns: ['**/*.{js,css,html,woff2,png,svg,ico,json}'],
// Vendor chunks (fomantic, chart.js) exceed the 2MiB default. An
// oversized file is silently OMITTED from the precache, which half
// breaks the offline shell -- watch the build output for a warning.
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
navigateFallback: '/index.html',
// Matters for top-level navigations to /api/static/<file> (clicking an
// attachment link) -- without this the worker answers with index.html.
navigateFallbackDenylist: [/^\/api\//, /^\/socket/],
cleanupOutdatedCaches: true,
// Let the user decide when to swap assets, rather than doing it
// mid-session underneath them.
clientsClaim: false,
skipWaiting: false,
runtimeCaching: [
{
urlPattern: ({ url }) =>
url.pathname.startsWith('/api/') || url.pathname.startsWith('/socket'),
handler: 'NetworkOnly',
},
],
},
// Flip to true temporarily to exercise the worker on the dev server.
// Remember to unregister it afterwards -- a lingering dev service worker
// is a classic "why is my phone showing an ancient build".
devOptions: { enabled: false, type: 'module' },
}),
],
resolve: {
alias: {
@@ -19,6 +87,9 @@ export default defineConfig({
},
server: {
host: '0.0.0.0',
...(useHttps
? { https: { key: fs.readFileSync(keyPath), cert: fs.readFileSync(certPath) } }
: {}),
proxy: {
'/api': 'http://localhost:3000',
'/socket': { target: 'http://localhost:3001', ws: true },