feat(demo): curated cloud files drop-in + fix backend asset copies
- demo/files/<Folder>/<file> becomes the cloud's content for every visitor (read-only; "private login" = git/repo access). Text inlined, binaries streamed from disk; empty folder falls back to the built-in seeded set. - Dockerfile.backend now copies docker/bitcoin-ui and demo/files into the image (they live outside neode-ui/) — this also fixes the Bitcoin UI mock, which the backend reads from /docker/bitcoin-ui and was previously absent in the container. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
df2ae3d7d8
commit
79c3769542
22
demo/README-curated-files.md
Normal file
22
demo/README-curated-files.md
Normal file
@ -0,0 +1,22 @@
|
||||
# Curated demo files
|
||||
|
||||
Drop real files into `demo/files/` to make them the cloud's content for **every**
|
||||
demo visitor (read-only — visitors can browse, download, and "buy" them, but only
|
||||
maintainers add them). This is the "private login": the only way to add files is
|
||||
to commit them here, which requires repo access.
|
||||
|
||||
```
|
||||
demo/files/
|
||||
Documents/whitepaper.pdf
|
||||
Photos/rig.jpg
|
||||
Music/track.mp3
|
||||
```
|
||||
|
||||
- Folder structure becomes the cloud's folders.
|
||||
- Text files (`.md .txt .json .csv …`, < 1 MB) are inlined; everything else is
|
||||
streamed from disk on download.
|
||||
- If `demo/files/` is empty, the demo falls back to the built-in seeded set
|
||||
(Documents/Photos/Music/Videos with sample content).
|
||||
|
||||
After adding files, commit and push — CI rebuilds the `:demo` image and Portainer
|
||||
redeploys. Keep the total modest (these load into the demo image).
|
||||
0
demo/files/.gitkeep
Normal file
0
demo/files/.gitkeep
Normal file
@ -14,6 +14,11 @@ RUN npm install
|
||||
# Copy application code
|
||||
COPY neode-ui/ ./
|
||||
|
||||
# Sibling assets the mock backend reads relative to /app (../docker, ../demo):
|
||||
# the Bitcoin UI mock shell and any curated cloud files dropped into demo/files.
|
||||
COPY docker/bitcoin-ui /docker/bitcoin-ui
|
||||
COPY demo/files /demo/files
|
||||
|
||||
# Expose port
|
||||
EXPOSE 5959
|
||||
|
||||
|
||||
@ -36,6 +36,7 @@ const DEMO =
|
||||
|
||||
// Find container socket: Podman (macOS/Linux) or Docker
|
||||
import { existsSync, readFileSync } from 'fs'
|
||||
import * as fsSync from 'fs'
|
||||
|
||||
// Report the real app version, suffixed with -demo in the public sandbox so it's
|
||||
// obviously the demo while still tracking whatever version the UI ships.
|
||||
@ -3448,6 +3449,53 @@ const SEED_FILE_CONTENTS = {
|
||||
'/Documents/backup-log.json': JSON.stringify({ backups: [{ id: 'bkp-2025-03-01', timestamp: '2025-03-01T02:00:00Z', type: 'full', apps: ['bitcoin-knots', 'lnd', 'mempool'], size_mb: 2340, status: 'success' }] }, null, 2),
|
||||
}
|
||||
|
||||
// Curated real files: drop files into <repo>/demo/files/<Folder>/<file> and they
|
||||
// replace the seeded cloud content for every visitor (read-only — visitors can
|
||||
// view/download/buy them but only maintainers add them, via git = the "private
|
||||
// login"). If the folder is absent/empty the hardcoded seeds above are kept.
|
||||
// Binary files are streamed from disk on demand (diskFilePaths); text is inlined.
|
||||
const diskFilePaths = {}
|
||||
function loadDemoDiskFiles() {
|
||||
const root = path.join(__dirname, '..', 'demo', 'files')
|
||||
let top
|
||||
try { top = fsSync.readdirSync(root, { withFileTypes: true }) } catch { return }
|
||||
const tree = { '/': [] }
|
||||
const contents = {}
|
||||
const TEXT_EXT = new Set(['txt', 'md', 'json', 'csv', 'log', 'yaml', 'yml', 'xml', 'conf', 'ini'])
|
||||
const walk = (absDir, relDir) => {
|
||||
let entries
|
||||
try { entries = fsSync.readdirSync(absDir, { withFileTypes: true }) } catch { return }
|
||||
tree[relDir] = tree[relDir] || []
|
||||
for (const e of entries) {
|
||||
if (e.name.startsWith('.')) continue
|
||||
const abs = path.join(absDir, e.name)
|
||||
const rel = relDir === '/' ? `/${e.name}` : `${relDir}/${e.name}`
|
||||
if (e.isDirectory()) {
|
||||
tree[relDir].push({ name: e.name, path: rel, size: 0, modified: new Date().toISOString(), isDir: true, type: '' })
|
||||
walk(abs, rel)
|
||||
} else {
|
||||
let st; try { st = fsSync.statSync(abs) } catch { continue }
|
||||
const ext = (e.name.includes('.') ? e.name.split('.').pop() : '').toLowerCase()
|
||||
const type = fbType(e.name)
|
||||
tree[relDir].push({ name: e.name, path: rel, size: st.size, modified: st.mtime.toISOString(), isDir: false, type })
|
||||
if (TEXT_EXT.has(ext) && st.size < 1_000_000) {
|
||||
try { contents[rel] = fsSync.readFileSync(abs, 'utf-8') } catch { /* skip */ }
|
||||
} else {
|
||||
diskFilePaths[rel] = abs // streamed from disk by the raw handler
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
walk(root, '/')
|
||||
if (!tree['/'].length) return // empty folder → keep the hardcoded seeds
|
||||
for (const k of Object.keys(SEED_FILES)) delete SEED_FILES[k]
|
||||
Object.assign(SEED_FILES, tree)
|
||||
for (const k of Object.keys(SEED_FILE_CONTENTS)) delete SEED_FILE_CONTENTS[k]
|
||||
Object.assign(SEED_FILE_CONTENTS, contents)
|
||||
console.log(`[Demo] Loaded curated files from demo/files (${Object.keys(diskFilePaths).length} binary, ${Object.keys(contents).length} text)`)
|
||||
}
|
||||
loadDemoDiskFiles()
|
||||
|
||||
// FileBrowser UI (demo placeholder when launched directly)
|
||||
app.get('/app/filebrowser/', (req, res) => {
|
||||
res.type('html').send(`<!DOCTYPE html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
@ -3605,6 +3653,13 @@ app.patch('/app/filebrowser/api/resources/*', (req, res) => {
|
||||
// FileBrowser raw file content (text reads, blob/stream fetches)
|
||||
app.get('/app/filebrowser/api/raw/*', (req, res) => {
|
||||
const full = fbNormalize(req.params[0] || '')
|
||||
// Curated binary files live on disk and stream directly.
|
||||
if (diskFilePaths[full]) {
|
||||
res.type(fbContentType(fbBase(full)))
|
||||
return fsSync.createReadStream(diskFilePaths[full]).on('error', () => {
|
||||
if (!res.headersSent) res.status(404).send('File not found')
|
||||
}).pipe(res)
|
||||
}
|
||||
const content = currentStore().files.contents[full]
|
||||
if (content === undefined) return res.status(404).send('File not found')
|
||||
res.type(fbContentType(fbBase(full)))
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user