Add localhost screenshot endpoint and email-upload guide

- Electron main now exposes http://127.0.0.1:9990/screenshot that returns
  a PNG via CDP Page.captureScreenshot (GNOME Wayland blocks external
  capture, and webContents.capturePage() hangs on this compositor). The
  endpoint is localhost-only.
- EMAIL_UPLOAD.md documents the n8n IMAP -> Samba share workflow for
  Skylight-style email-to-photo-frame uploads.
This commit is contained in:
root
2026-04-14 18:59:24 -05:00
parent cede430dc9
commit a5803f70e3
2 changed files with 112 additions and 0 deletions

83
EMAIL_UPLOAD.md Normal file
View File

@@ -0,0 +1,83 @@
# Email-to-Photo-Frame Upload
Photos emailed to a dedicated mailbox are automatically saved to the
photo-frame Samba share, where the kiosk picks them up on its next scan
(PhotoManager rescans every 10 minutes).
## Three moving parts
### 1. Mailbox to receive photos
Easiest path: a dedicated Gmail account (e.g. `deathstarhomephotos@gmail.com`).
- Enable 2FA on the Google account
- Generate an **app password** under Security → App passwords (needed for IMAP,
since n8n's IMAP node uses username/password auth)
- Share the address with family members
Lock it down in the n8n workflow by whitelisting sender addresses; reject
anything else so strangers can't spam the frame.
### 2. Samba share visible to n8n
n8n runs on 192.168.1.254 and needs to write to the same Samba share the
kiosk reads from. Two options:
**Option A — Mount the share inside the n8n container**
```bash
# On the n8n LXC host (192.168.1.254), install cifs-utils and mount:
apt install -y cifs-utils
mkdir -p /mnt/family-photos
echo '//SERVER/SHARE /mnt/family-photos cifs credentials=/etc/samba/credentials-n8n,uid=1000,gid=1000,rw,_netdev,x-systemd.automount 0 0' >> /etc/fstab
# credentials file with chmod 600
mount -a
```
Then bind-mount `/mnt/family-photos` into the n8n Docker container so the
Write Binary File node can target it at a path like `/data/family-photos/`.
**Option B — SSH to the fileshare LXC (192.168.1.193)**
Give n8n an SSH key to the fileshare host. Use the Execute Command node to
`scp` / `cat > file` onto the share. Simpler to configure than cifs, but
adds SSH surface area on the fileshare box.
### 3. n8n workflow
Pseudocode:
```
IMAP Email Trigger (mark read, download attachments, poll every 2 min)
→ IF From ∈ [your-email, wife-email]
→ Split attachments
→ IF mimeType startsWith "image/"
→ Rename: ${yyyyMMdd_HHmmss}_${randomHex}.${ext}
→ Write Binary File → /data/family-photos/${filename}
```
Nodes used:
- **Email Trigger (IMAP)** — reads Gmail (server `imap.gmail.com`, port 993, SSL)
- **IF** — sender whitelist guard
- **Item Lists / Split In Batches** — one item per attachment
- **IF** — filter on `$binary.attachment_0.mimeType`
- **Set** — build filename
- **Write Binary File** — write to the mount path
Kiosk picks them up on the next rescan (up to 10 min), or restart the app
to force an immediate scan.
## Hardening
- Limit attachment size (e.g. reject > 30 MB per file) to avoid filling
the share
- Strip EXIF if you don't want location data persisted on the share
- Keep an audit trail: add an extra branch that appends sender + filename
to a log file
## Credentials I'll need to build the workflow
- Gmail address + app password (or OAuth refresh token)
- Which option above for the Samba mount (A or B)
- If option B, SSH key + fileshare path to write to
- List of allowed sender emails

View File

@@ -1,6 +1,7 @@
import { app, BrowserWindow, ipcMain, screen, powerSaveBlocker } from 'electron';
import * as path from 'path';
import * as fs from 'fs';
import * as http from 'http';
import { ScreenManager } from './services/ScreenManager';
import { PresenceDetector } from './services/PresenceDetector';
import { FrigateStreamer } from './services/FrigateStreamer';
@@ -230,6 +231,34 @@ app.on('window-all-closed', () => {
}
});
// Localhost-only HTTP endpoint for self-screenshots. GNOME Wayland blocks
// external screen capture, but webContents.capturePage() works from inside.
// Usage: curl -s http://127.0.0.1:9990/screenshot > out.png
http
.createServer(async (req, res) => {
if (req.url === '/screenshot' && mainWindow) {
try {
const wc = mainWindow.webContents;
if (!wc.debugger.isAttached()) wc.debugger.attach('1.3');
const result = (await wc.debugger.sendCommand('Page.captureScreenshot', {
format: 'png',
captureBeyondViewport: false,
})) as { data: string };
res.setHeader('Content-Type', 'image/png');
res.end(Buffer.from(result.data, 'base64'));
} catch (err) {
res.statusCode = 500;
res.end(String(err));
}
} else {
res.statusCode = 404;
res.end();
}
})
.listen(9990, '127.0.0.1', () => {
console.log('Screenshot endpoint on http://127.0.0.1:9990/screenshot');
});
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
console.error('Uncaught exception:', error);