Add standalone /carmode route and nginx documentation
- Add /carmode URL that auto-enters car mode without exit option - /carmode uses its own API route (/carmode/api/) for isolation - Hide exit button and disable Escape key on /carmode - Skip lock screen on dedicated car mode URL - Replace macOS/Ubuntu docs with nginx configuration guide Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,80 +0,0 @@
|
|||||||
News App - macOS Service Management
|
|
||||||
====================================
|
|
||||||
|
|
||||||
Service: com.je.carnews
|
|
||||||
Port: 5555
|
|
||||||
Plist: /Library/LaunchDaemons/com.je.carnews.plist
|
|
||||||
|
|
||||||
|
|
||||||
BASIC COMMANDS
|
|
||||||
--------------
|
|
||||||
|
|
||||||
Start the service:
|
|
||||||
sudo launchctl kickstart system/com.je.carnews
|
|
||||||
|
|
||||||
Stop the service:
|
|
||||||
sudo launchctl kill SIGTERM system/com.je.carnews
|
|
||||||
|
|
||||||
Restart the service:
|
|
||||||
sudo launchctl kickstart -k system/com.je.carnews
|
|
||||||
|
|
||||||
Check status:
|
|
||||||
sudo launchctl print system/com.je.carnews
|
|
||||||
|
|
||||||
Check if running:
|
|
||||||
lsof -i :5555
|
|
||||||
|
|
||||||
|
|
||||||
ADD TO STARTUP (BOOT)
|
|
||||||
---------------------
|
|
||||||
|
|
||||||
1. Copy the plist to LaunchDaemons:
|
|
||||||
sudo cp /opt/homebrew/var/www/news-app/com.je.carnews.plist /Library/LaunchDaemons/
|
|
||||||
|
|
||||||
2. Load the service:
|
|
||||||
sudo launchctl bootstrap system /Library/LaunchDaemons/com.je.carnews.plist
|
|
||||||
|
|
||||||
3. Start it:
|
|
||||||
sudo launchctl kickstart system/com.je.carnews
|
|
||||||
|
|
||||||
The service will now start automatically on boot.
|
|
||||||
|
|
||||||
|
|
||||||
REMOVE FROM STARTUP (BOOT)
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
1. Stop and unload the service:
|
|
||||||
sudo launchctl bootout system/com.je.carnews
|
|
||||||
|
|
||||||
2. Remove the plist file:
|
|
||||||
sudo rm /Library/LaunchDaemons/com.je.carnews.plist
|
|
||||||
|
|
||||||
The service will no longer start on boot.
|
|
||||||
|
|
||||||
|
|
||||||
VIEW LOGS
|
|
||||||
---------
|
|
||||||
|
|
||||||
Output log:
|
|
||||||
tail -f /opt/homebrew/var/www/news-app/logs/out.log
|
|
||||||
|
|
||||||
Error log:
|
|
||||||
tail -f /opt/homebrew/var/www/news-app/logs/error.log
|
|
||||||
|
|
||||||
|
|
||||||
TROUBLESHOOTING
|
|
||||||
---------------
|
|
||||||
|
|
||||||
If the service fails to start, check:
|
|
||||||
|
|
||||||
1. Node version - the plist uses:
|
|
||||||
/Users/jared/.nvm/versions/node/v22.19.0/bin/node
|
|
||||||
|
|
||||||
2. If you update Node via nvm, rebuild native modules:
|
|
||||||
cd /opt/homebrew/var/www/news-app
|
|
||||||
npm rebuild better-sqlite3
|
|
||||||
|
|
||||||
3. Then update the node path in the plist if needed and reload:
|
|
||||||
sudo launchctl bootout system/com.je.carnews
|
|
||||||
sudo launchctl bootstrap system /Library/LaunchDaemons/com.je.carnews.plist
|
|
||||||
sudo launchctl kickstart system/com.je.carnews
|
|
||||||
88
news-app/How_To_Nginx.txt
Normal file
88
news-app/How_To_Nginx.txt
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
Nginx Configuration for Car News App
|
||||||
|
=====================================
|
||||||
|
|
||||||
|
This app requires two route groups in your nginx server block:
|
||||||
|
|
||||||
|
1. /carnews/ - Full news app with regular and car mode views
|
||||||
|
2. /carmode/ - Standalone car mode display (no exit, no links to main app)
|
||||||
|
|
||||||
|
Both routes share the same backend API server (port 5555) and static files.
|
||||||
|
|
||||||
|
|
||||||
|
NGINX CONFIGURATION
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
Add the following to your server block:
|
||||||
|
|
||||||
|
# --- Car News App ---
|
||||||
|
location = /carnews {
|
||||||
|
return 301 /carnews/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /carnews/api/ {
|
||||||
|
proxy_pass http://127.0.0.1:5555/api/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 180s;
|
||||||
|
proxy_connect_timeout 180s;
|
||||||
|
proxy_send_timeout 180s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /carnews/ {
|
||||||
|
alias /var/www/news-feed-car-mode/news-app/dist/;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /carnews/index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Car Mode Direct Access ---
|
||||||
|
location = /carmode {
|
||||||
|
return 301 /carmode/;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /carmode/api/ {
|
||||||
|
proxy_pass http://127.0.0.1:5555/api/;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_read_timeout 180s;
|
||||||
|
proxy_connect_timeout 180s;
|
||||||
|
proxy_send_timeout 180s;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /carmode/ {
|
||||||
|
alias /var/www/news-feed-car-mode/news-app/dist/;
|
||||||
|
index index.html;
|
||||||
|
try_files $uri $uri/ /carmode/index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
CONFIGURATION NOTES
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
- The API timeout is set to 180s because AI grouping requests can take time
|
||||||
|
- Both /carnews/ and /carmode/ serve the same dist/ folder
|
||||||
|
- The frontend JS detects which URL it's on and adjusts behavior accordingly
|
||||||
|
- proxy_http_version 1.1 is required for proper keep-alive connections
|
||||||
|
|
||||||
|
|
||||||
|
AFTER CHANGES
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Test and reload nginx:
|
||||||
|
|
||||||
|
sudo nginx -t
|
||||||
|
sudo systemctl reload nginx
|
||||||
|
|
||||||
|
|
||||||
|
BUILDING THE APP
|
||||||
|
----------------
|
||||||
|
|
||||||
|
After code changes, rebuild the frontend:
|
||||||
|
|
||||||
|
cd /var/www/news-feed-car-mode/news-app
|
||||||
|
npm run build
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
Based on the codebase analysis, here's what needs to change for your Ubuntu deployment:
|
|
||||||
|
|
||||||
Required Changes
|
|
||||||
|
|
||||||
Create a systemd service file instead:
|
|
||||||
|
|
||||||
# /etc/systemd/system/carnews.service
|
|
||||||
[Unit]
|
|
||||||
Description=Car News API Server
|
|
||||||
After=network.target
|
|
||||||
|
|
||||||
[Service]
|
|
||||||
Type=simple
|
|
||||||
User=<your-ubuntu-user>
|
|
||||||
WorkingDirectory=/path/to/news-app
|
|
||||||
ExecStart=/usr/bin/node server.js
|
|
||||||
Restart=on-failure
|
|
||||||
EnvironmentFile=/path/to/news-app/.env
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=multi-user.target
|
|
||||||
|
|
||||||
2. Rebuild native modules
|
|
||||||
|
|
||||||
The better-sqlite3 package requires recompilation on Ubuntu:
|
|
||||||
npm rebuild better-sqlite3
|
|
||||||
|
|
||||||
3. Nginx configuration for Ubuntu
|
|
||||||
|
|
||||||
You'll need to create an Nginx config (not included in this repo). Example:
|
|
||||||
|
|
||||||
location /carnews/ {
|
|
||||||
alias /path/to/news-app/dist/;
|
|
||||||
try_files $uri $uri/ /carnews/index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
location /carnews/api/ {
|
|
||||||
proxy_pass http://localhost:5555/api/;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
}
|
|
||||||
|
|
||||||
4. Update paths if directory structure differs
|
|
||||||
|
|
||||||
On Ubuntu: /var/www/news-feed-car-mode/news-app
|
|
||||||
|
|
||||||
No Changes Needed
|
|
||||||
|
|
||||||
- vite.config.js - base: '/carnews/' stays the same
|
|
||||||
- server.js - Port 5555 works as-is
|
|
||||||
- .env file - copied over
|
|
||||||
- All source code - Uses relative paths
|
|
||||||
|
|
||||||
Deployment Steps
|
|
||||||
|
|
||||||
1. Copy the codebase to Ubuntu
|
|
||||||
2. Run npm install and npm rebuild better-sqlite3
|
|
||||||
3. Run npm run build to generate fresh /dist files
|
|
||||||
4. Create the systemd service file
|
|
||||||
5. Configure Nginx with the proxy rules
|
|
||||||
6. Enable and start the service: sudo systemctl enable --now carnews
|
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import './style.css'
|
import './style.css'
|
||||||
|
|
||||||
const API_BASE = '/carnews/api'
|
// Check if we're on the dedicated car mode URL
|
||||||
|
const isCarModeUrl = window.location.pathname === '/carmode' || window.location.pathname === '/carmode/'
|
||||||
|
|
||||||
|
// Use appropriate API base depending on URL
|
||||||
|
const API_BASE = isCarModeUrl ? '/carmode/api' : '/carnews/api'
|
||||||
const app = document.querySelector('#app')
|
const app = document.querySelector('#app')
|
||||||
|
|
||||||
// Fetch with timeout helper (default 2 minutes for AI calls)
|
// Fetch with timeout helper (default 2 minutes for AI calls)
|
||||||
@@ -49,15 +53,16 @@ const state = {
|
|||||||
error: null,
|
error: null,
|
||||||
filter: 'all',
|
filter: 'all',
|
||||||
viewMode: 'regular', // 'regular' or 'ai'
|
viewMode: 'regular', // 'regular' or 'ai'
|
||||||
carMode: false,
|
carMode: isCarModeUrl, // Auto-enter car mode if on /carmode URL
|
||||||
carModeIndex: 0,
|
carModeIndex: 0,
|
||||||
carModeInterval: null,
|
carModeInterval: null,
|
||||||
carModeScrollTimeout: null,
|
carModeScrollTimeout: null,
|
||||||
carModeScrollInterval: null,
|
carModeScrollInterval: null,
|
||||||
aiArticleCount: 0,
|
aiArticleCount: 0,
|
||||||
locked: !getCookie('news_feed_unlocked'),
|
locked: isCarModeUrl ? false : !getCookie('news_feed_unlocked'), // Skip lock for car mode URL
|
||||||
unlockClicks: 0,
|
unlockClicks: 0,
|
||||||
isTransitioning: false,
|
isTransitioning: false,
|
||||||
|
isCarModeUrl: isCarModeUrl, // Track if we're on dedicated car mode URL
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatDate(dateString) {
|
function formatDate(dateString) {
|
||||||
@@ -274,11 +279,13 @@ function renderCarMode() {
|
|||||||
<div class="w-16 h-16 border-4 border-white/30 border-t-white rounded-full animate-spin mx-auto mb-6"></div>
|
<div class="w-16 h-16 border-4 border-white/30 border-t-white rounded-full animate-spin mx-auto mb-6"></div>
|
||||||
<p class="text-xl">${loadingMessage}</p>
|
<p class="text-xl">${loadingMessage}</p>
|
||||||
</div>
|
</div>
|
||||||
|
${!state.isCarModeUrl ? `
|
||||||
<button onclick="exitCarMode()" class="absolute top-14 right-6 z-20 p-3 text-white/70 hover:text-white hover:bg-white/10 rounded-full transition-colors" title="Exit Car Mode (Esc)">
|
<button onclick="exitCarMode()" class="absolute top-14 right-6 z-20 p-3 text-white/70 hover:text-white hover:bg-white/10 rounded-full transition-colors" title="Exit Car Mode (Esc)">
|
||||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
@@ -291,12 +298,14 @@ function renderCarMode() {
|
|||||||
<div class="car-mode-blur-2"></div>
|
<div class="car-mode-blur-2"></div>
|
||||||
<div class="car-mode-blur-3"></div>
|
<div class="car-mode-blur-3"></div>
|
||||||
|
|
||||||
|
${!state.isCarModeUrl ? `
|
||||||
<!-- Exit button -->
|
<!-- Exit button -->
|
||||||
<button onclick="exitCarMode()" class="absolute top-2 right-6 z-20 p-3 text-white/70 hover:text-white hover:bg-white/10 rounded-full transition-colors backdrop-blur-sm bg-black/10" title="Exit Car Mode (Esc)">
|
<button onclick="exitCarMode()" class="absolute top-2 right-6 z-20 p-3 text-white/70 hover:text-white hover:bg-white/10 rounded-full transition-colors backdrop-blur-sm bg-black/10" title="Exit Car Mode (Esc)">
|
||||||
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
` : ''}
|
||||||
|
|
||||||
<!-- Car mode indicator -->
|
<!-- Car mode indicator -->
|
||||||
<div class="absolute top-2 left-6 z-20 flex items-center gap-2 text-white/70 backdrop-blur-sm px-3 py-1.5 rounded-full bg-black/10">
|
<div class="absolute top-2 left-6 z-20 flex items-center gap-2 text-white/70 backdrop-blur-sm px-3 py-1.5 rounded-full bg-black/10">
|
||||||
@@ -599,6 +608,11 @@ window.enterCarMode = async function() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window.exitCarMode = function() {
|
window.exitCarMode = function() {
|
||||||
|
// On dedicated car mode URL, just refresh the page to restart
|
||||||
|
if (state.isCarModeUrl) {
|
||||||
|
window.location.reload()
|
||||||
|
return
|
||||||
|
}
|
||||||
state.carMode = false
|
state.carMode = false
|
||||||
stopCarModeCycle()
|
stopCarModeCycle()
|
||||||
render()
|
render()
|
||||||
@@ -692,7 +706,10 @@ document.addEventListener('keydown', (e) => {
|
|||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'Escape':
|
case 'Escape':
|
||||||
|
// Don't allow escape on dedicated car mode URL
|
||||||
|
if (!state.isCarModeUrl) {
|
||||||
window.exitCarMode()
|
window.exitCarMode()
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
case ' ':
|
case ' ':
|
||||||
@@ -704,9 +721,17 @@ document.addEventListener('keydown', (e) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
fetchNews()
|
// Initialize: either start car mode or fetch regular news
|
||||||
|
if (isCarModeUrl) {
|
||||||
|
// On dedicated car mode URL, directly enter car mode
|
||||||
|
window.enterCarMode()
|
||||||
|
} else {
|
||||||
|
fetchNews()
|
||||||
|
}
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
|
// Don't auto-refresh if in car mode (it handles its own data)
|
||||||
|
if (state.carMode) return
|
||||||
if (state.viewMode === 'ai') {
|
if (state.viewMode === 'ai') {
|
||||||
fetchGroupedNews()
|
fetchGroupedNews()
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user