Verbesserte
This commit is contained in:
10
.dockerignore
Normal file
10
.dockerignore
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.git
|
||||||
|
node_modules
|
||||||
|
build
|
||||||
|
config
|
||||||
|
data
|
||||||
|
Dockerfile.dev
|
||||||
|
.env
|
||||||
|
npm-debug.log
|
||||||
|
.DS_Store
|
||||||
|
rebuildContainer.sh
|
||||||
26
Dockerfile
26
Dockerfile
@@ -1,22 +1,20 @@
|
|||||||
FROM node:18-alpine
|
FROM node:18-alpine AS base
|
||||||
|
|
||||||
# Arbeitsverzeichnis im Container
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
|
||||||
# Kopieren der package.json-Dateien für effizienteres Caching
|
# 1) Install dependencies (cached until package files change)
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
# Installation von Abhängigkeiten
|
# 2) Build the React client (only re-run when client sources change)
|
||||||
RUN npm install
|
COPY public ./public
|
||||||
|
COPY src ./src
|
||||||
# Kopieren des Quellcodes
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Build der React-App
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
# Freigegebener Port
|
# 3) Copy server-side files (changes here skip the expensive client build layer)
|
||||||
EXPOSE 3000
|
COPY server.js ./
|
||||||
|
COPY services ./services
|
||||||
|
RUN mkdir -p config
|
||||||
|
|
||||||
# Starten des Node.js-Servers
|
EXPOSE 3000
|
||||||
CMD ["node", "server.js"]
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
196
server.js
196
server.js
@@ -2,6 +2,7 @@ const express = require('express');
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
const cors = require('cors');
|
const cors = require('cors');
|
||||||
|
|
||||||
|
const { v4: uuid } = require('uuid');
|
||||||
const sessionStore = require('./services/sessionStore');
|
const sessionStore = require('./services/sessionStore');
|
||||||
const credentialStore = require('./services/credentialStore');
|
const credentialStore = require('./services/credentialStore');
|
||||||
const { readConfig, writeConfig } = require('./services/configStore');
|
const { readConfig, writeConfig } = require('./services/configStore');
|
||||||
@@ -15,6 +16,7 @@ const SIXTY_DAYS_MS = 60 * 24 * 60 * 60 * 1000;
|
|||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = process.env.PORT || 3000;
|
const port = process.env.PORT || 3000;
|
||||||
|
const storeRefreshJobs = new Map();
|
||||||
|
|
||||||
app.use(cors());
|
app.use(cors());
|
||||||
app.use(express.json({ limit: '1mb' }));
|
app.use(express.json({ limit: '1mb' }));
|
||||||
@@ -83,30 +85,124 @@ function mergeStoresIntoConfig(config = [], stores = []) {
|
|||||||
return { merged: Array.from(map.values()), changed };
|
return { merged: Array.from(map.values()), changed };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadStoresForSession(session, settings, { forceRefresh = false } = {}) {
|
function isStoreCacheFresh(session) {
|
||||||
|
if (!session?.storesCache?.fetchedAt) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Date.now() - session.storesCache.fetchedAt <= SIXTY_DAYS_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
function summarizeJob(job) {
|
||||||
|
if (!job) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: job.id,
|
||||||
|
status: job.status,
|
||||||
|
processed: job.processed,
|
||||||
|
total: job.total,
|
||||||
|
currentStore: job.currentStore,
|
||||||
|
startedAt: job.startedAt,
|
||||||
|
finishedAt: job.finishedAt,
|
||||||
|
reason: job.reason || null,
|
||||||
|
error: job.error || null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStoreRefreshJob(sessionId) {
|
||||||
|
const job = storeRefreshJobs.get(sessionId);
|
||||||
|
if (!job) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (job.status === 'done' && Date.now() - (job.finishedAt || 0) > 5 * 60 * 1000) {
|
||||||
|
storeRefreshJobs.delete(sessionId);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerStoreRefresh(session, { force = false, reason } = {}) {
|
||||||
|
if (!session?.id) {
|
||||||
|
return { started: false };
|
||||||
|
}
|
||||||
|
const existing = getStoreRefreshJob(session.id);
|
||||||
|
if (existing && existing.status === 'running') {
|
||||||
|
return { started: false, job: existing };
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheFresh = isStoreCacheFresh(session);
|
||||||
|
if (!force && cacheFresh) {
|
||||||
|
return { started: false, cacheFresh: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const job = {
|
||||||
|
id: uuid(),
|
||||||
|
status: 'queued',
|
||||||
|
processed: 0,
|
||||||
|
total: 0,
|
||||||
|
currentStore: null,
|
||||||
|
startedAt: null,
|
||||||
|
finishedAt: null,
|
||||||
|
reason: reason || null,
|
||||||
|
error: null
|
||||||
|
};
|
||||||
|
|
||||||
|
storeRefreshJobs.set(session.id, job);
|
||||||
|
runStoreRefreshJob(session, job).catch((error) => {
|
||||||
|
console.error('[STORE-REFRESH] Job fehlgeschlagen:', error.message);
|
||||||
|
job.status = 'error';
|
||||||
|
job.error = error.message;
|
||||||
|
job.finishedAt = Date.now();
|
||||||
|
});
|
||||||
|
return { started: true, job };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runStoreRefreshJob(session, job) {
|
||||||
|
job.status = 'running';
|
||||||
|
job.startedAt = Date.now();
|
||||||
|
const settings = adminConfig.readSettings();
|
||||||
|
const stores = await foodsharingClient.fetchStores(session.cookieHeader, session.profile.id, {
|
||||||
|
delayBetweenRequestsMs: settings.storePickupCheckDelayMs,
|
||||||
|
onStoreCheck: (store, processed, total) => {
|
||||||
|
job.processed = processed;
|
||||||
|
job.total = total;
|
||||||
|
job.currentStore = store.name || `Store ${store.id}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
job.processed = stores.length;
|
||||||
|
job.total = stores.length;
|
||||||
|
job.currentStore = null;
|
||||||
|
|
||||||
|
sessionStore.update(session.id, {
|
||||||
|
storesCache: { data: stores, fetchedAt: Date.now() }
|
||||||
|
});
|
||||||
|
|
||||||
|
let config = readConfig(session.profile.id);
|
||||||
|
const { merged, changed } = mergeStoresIntoConfig(config, stores);
|
||||||
|
if (changed) {
|
||||||
|
config = merged;
|
||||||
|
writeConfig(session.profile.id, config);
|
||||||
|
scheduleWithCurrentSettings(session.id, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
job.status = 'done';
|
||||||
|
job.finishedAt = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStoresForSession(session, _settings, { forceRefresh = false, reason } = {}) {
|
||||||
if (!session?.profile?.id) {
|
if (!session?.profile?.id) {
|
||||||
return { stores: [], refreshed: false };
|
return { stores: [], refreshed: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
const cache = session.storesCache;
|
const cacheFresh = isStoreCacheFresh(session);
|
||||||
const now = Date.now();
|
if ((forceRefresh || !cacheFresh) && session.cookieHeader) {
|
||||||
const isCacheValid =
|
triggerStoreRefresh(session, { force: true, reason: reason || 'session-check' });
|
||||||
cache &&
|
|
||||||
Array.isArray(cache.data) &&
|
|
||||||
Number.isFinite(cache.fetchedAt) &&
|
|
||||||
now - cache.fetchedAt <= SIXTY_DAYS_MS;
|
|
||||||
|
|
||||||
if (isCacheValid && !forceRefresh) {
|
|
||||||
return { stores: cache.data, refreshed: false };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const stores = await foodsharingClient.fetchStores(session.cookieHeader, session.profile.id, {
|
return {
|
||||||
delayBetweenRequestsMs: settings.storePickupCheckDelayMs
|
stores: session.storesCache?.data || [],
|
||||||
});
|
refreshed: cacheFresh
|
||||||
sessionStore.update(session.id, {
|
};
|
||||||
storesCache: { data: stores, fetchedAt: now }
|
|
||||||
});
|
|
||||||
return { stores, refreshed: true };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function restoreSessionsFromDisk() {
|
async function restoreSessionsFromDisk() {
|
||||||
@@ -202,16 +298,7 @@ app.post('/api/auth/login', async (req, res) => {
|
|||||||
};
|
};
|
||||||
const isAdminUser = isAdmin(profile);
|
const isAdminUser = isAdmin(profile);
|
||||||
const settings = adminConfig.readSettings();
|
const settings = adminConfig.readSettings();
|
||||||
|
|
||||||
let config = readConfig(profile.id);
|
let config = readConfig(profile.id);
|
||||||
const stores = await foodsharingClient.fetchStores(auth.cookieHeader, profile.id, {
|
|
||||||
delayBetweenRequestsMs: settings.storePickupCheckDelayMs
|
|
||||||
});
|
|
||||||
const { merged, changed } = mergeStoresIntoConfig(config, stores);
|
|
||||||
if (changed) {
|
|
||||||
config = merged;
|
|
||||||
writeConfig(profile.id, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingCredentials = credentialStore.get(profile.id);
|
const existingCredentials = credentialStore.get(profile.id);
|
||||||
const existingToken = existingCredentials?.token;
|
const existingToken = existingCredentials?.token;
|
||||||
@@ -228,18 +315,18 @@ app.post('/api/auth/login', async (req, res) => {
|
|||||||
}, existingToken, ONE_YEAR_MS);
|
}, existingToken, ONE_YEAR_MS);
|
||||||
|
|
||||||
credentialStore.save(profile.id, { email, password, token: session.id });
|
credentialStore.save(profile.id, { email, password, token: session.id });
|
||||||
sessionStore.update(session.id, {
|
|
||||||
storesCache: { data: stores, fetchedAt: Date.now() }
|
|
||||||
});
|
|
||||||
scheduleConfig(session.id, config, settings);
|
scheduleConfig(session.id, config, settings);
|
||||||
|
const refreshResult = triggerStoreRefresh(session, { force: true, reason: 'login' });
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
token: session.id,
|
token: session.id,
|
||||||
profile,
|
profile,
|
||||||
stores,
|
stores: session.storesCache?.data || [],
|
||||||
config,
|
config,
|
||||||
isAdmin: isAdminUser,
|
isAdmin: isAdminUser,
|
||||||
adminSettings: isAdminUser ? settings : undefined
|
adminSettings: isAdminUser ? settings : undefined,
|
||||||
|
storeRefreshJob: summarizeJob(refreshResult.job),
|
||||||
|
storesFresh: isStoreCacheFresh(session)
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Login fehlgeschlagen:', error.message);
|
console.error('Login fehlgeschlagen:', error.message);
|
||||||
@@ -250,24 +337,20 @@ app.post('/api/auth/login', async (req, res) => {
|
|||||||
app.post('/api/auth/logout', requireAuth, (req, res) => {
|
app.post('/api/auth/logout', requireAuth, (req, res) => {
|
||||||
sessionStore.delete(req.session.id);
|
sessionStore.delete(req.session.id);
|
||||||
credentialStore.remove(req.session.profile.id);
|
credentialStore.remove(req.session.profile.id);
|
||||||
|
storeRefreshJobs.delete(req.session.id);
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/auth/session', requireAuth, async (req, res) => {
|
app.get('/api/auth/session', requireAuth, async (req, res) => {
|
||||||
const settings = adminConfig.readSettings();
|
const settings = adminConfig.readSettings();
|
||||||
const { stores, refreshed } = await loadStoresForSession(req.session, settings);
|
const { stores } = await loadStoresForSession(req.session, settings, { reason: 'session-check' });
|
||||||
if (refreshed) {
|
|
||||||
let config = readConfig(req.session.profile.id);
|
|
||||||
const { merged, changed } = mergeStoresIntoConfig(config, stores);
|
|
||||||
if (changed) {
|
|
||||||
writeConfig(req.session.profile.id, merged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
res.json({
|
res.json({
|
||||||
profile: req.session.profile,
|
profile: req.session.profile,
|
||||||
stores,
|
stores,
|
||||||
isAdmin: !!req.session.isAdmin,
|
isAdmin: !!req.session.isAdmin,
|
||||||
adminSettings: req.session.isAdmin ? settings : undefined
|
adminSettings: req.session.isAdmin ? settings : undefined,
|
||||||
|
storeRefreshJob: summarizeJob(getStoreRefreshJob(req.session.id)),
|
||||||
|
storesFresh: isStoreCacheFresh(req.session)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -293,16 +376,27 @@ app.post('/api/config', requireAuth, (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/stores', requireAuth, async (req, res) => {
|
app.get('/api/stores', requireAuth, async (req, res) => {
|
||||||
const settings = adminConfig.readSettings();
|
res.json(req.session.storesCache?.data || []);
|
||||||
const { stores, refreshed } = await loadStoresForSession(req.session, settings, { forceRefresh: true });
|
});
|
||||||
if (refreshed) {
|
|
||||||
let config = readConfig(req.session.profile.id);
|
app.post('/api/stores/refresh', requireAuth, (req, res) => {
|
||||||
const { merged, changed } = mergeStoresIntoConfig(config, stores);
|
const force = req.body?.force !== undefined ? !!req.body.force : true;
|
||||||
if (changed) {
|
const reason = req.body?.reason || 'manual';
|
||||||
writeConfig(req.session.profile.id, merged);
|
const result = triggerStoreRefresh(req.session, { force, reason });
|
||||||
}
|
res.json({
|
||||||
}
|
started: !!result.started,
|
||||||
res.json(stores);
|
storesFresh: isStoreCacheFresh(req.session),
|
||||||
|
job: summarizeJob(result.job)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
app.get('/api/stores/refresh/status', requireAuth, (req, res) => {
|
||||||
|
const job = getStoreRefreshJob(req.session.id);
|
||||||
|
res.json({
|
||||||
|
job: summarizeJob(job),
|
||||||
|
storesFresh: isStoreCacheFresh(req.session),
|
||||||
|
cacheUpdatedAt: req.session.storesCache?.fetchedAt || null
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/admin/settings', requireAuth, requireAdmin, (_req, res) => {
|
app.get('/api/admin/settings', requireAuth, requireAdmin, (_req, res) => {
|
||||||
|
|||||||
@@ -126,6 +126,10 @@ async function fetchStores(cookieHeader, profileId, options = {}) {
|
|||||||
const delayBetweenRequestsMs = Number.isFinite(options.delayBetweenRequestsMs)
|
const delayBetweenRequestsMs = Number.isFinite(options.delayBetweenRequestsMs)
|
||||||
? Math.max(0, options.delayBetweenRequestsMs)
|
? Math.max(0, options.delayBetweenRequestsMs)
|
||||||
: 0;
|
: 0;
|
||||||
|
const onStoreCheck =
|
||||||
|
typeof options.onStoreCheck === 'function'
|
||||||
|
? options.onStoreCheck
|
||||||
|
: null;
|
||||||
try {
|
try {
|
||||||
const response = await client.get(`/api/user/${profileId}/stores`, {
|
const response = await client.get(`/api/user/${profileId}/stores`, {
|
||||||
headers: buildHeaders(cookieHeader),
|
headers: buildHeaders(cookieHeader),
|
||||||
@@ -143,14 +147,14 @@ async function fetchStores(cookieHeader, profileId, options = {}) {
|
|||||||
zip: store.zip || ''
|
zip: store.zip || ''
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return annotateStoresWithPickupSlots(normalized, cookieHeader, delayBetweenRequestsMs);
|
return annotateStoresWithPickupSlots(normalized, cookieHeader, delayBetweenRequestsMs, onStoreCheck);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Stores konnten nicht geladen werden:', error.message);
|
console.warn('Stores konnten nicht geladen werden:', error.message);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function annotateStoresWithPickupSlots(stores, cookieHeader, delayBetweenRequestsMs = 0) {
|
async function annotateStoresWithPickupSlots(stores, cookieHeader, delayBetweenRequestsMs = 0, onStoreCheck) {
|
||||||
if (!Array.isArray(stores) || stores.length === 0) {
|
if (!Array.isArray(stores) || stores.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -160,6 +164,13 @@ async function annotateStoresWithPickupSlots(stores, cookieHeader, delayBetweenR
|
|||||||
|
|
||||||
for (let index = 0; index < stores.length; index += 1) {
|
for (let index = 0; index < stores.length; index += 1) {
|
||||||
const store = stores[index];
|
const store = stores[index];
|
||||||
|
if (onStoreCheck) {
|
||||||
|
try {
|
||||||
|
onStoreCheck(store, index + 1, stores.length);
|
||||||
|
} catch (callbackError) {
|
||||||
|
console.warn('Store-Progress-Callback fehlgeschlagen:', callbackError);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (delayMs > 0 && index > 0) {
|
if (delayMs > 0 && index > 0) {
|
||||||
await wait(delayMs);
|
await wait(delayMs);
|
||||||
}
|
}
|
||||||
|
|||||||
172
src/App.js
172
src/App.js
@@ -28,6 +28,7 @@ function App() {
|
|||||||
const [syncProgress, setSyncProgress] = useState({ active: false, percent: 0, message: '', block: false });
|
const [syncProgress, setSyncProgress] = useState({ active: false, percent: 0, message: '', block: false });
|
||||||
|
|
||||||
const weekdays = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'];
|
const weekdays = ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag', 'Samstag', 'Sonntag'];
|
||||||
|
const delay = useCallback((ms) => new Promise((resolve) => setTimeout(resolve, ms)), []);
|
||||||
|
|
||||||
const startSyncProgress = useCallback((message, percent, block = false) => {
|
const startSyncProgress = useCallback((message, percent, block = false) => {
|
||||||
setSyncProgress({ active: true, percent, message, block });
|
setSyncProgress({ active: true, percent, message, block });
|
||||||
@@ -117,7 +118,7 @@ function App() {
|
|||||||
const bootstrapSession = useCallback(
|
const bootstrapSession = useCallback(
|
||||||
async (token, { progress } = {}) => {
|
async (token, { progress } = {}) => {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return;
|
return {};
|
||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
@@ -153,45 +154,70 @@ function App() {
|
|||||||
progress?.update?.('Konfiguration wird geladen...', 75);
|
progress?.update?.('Konfiguration wird geladen...', 75);
|
||||||
setConfig(Array.isArray(configData) ? configData : []);
|
setConfig(Array.isArray(configData) ? configData : []);
|
||||||
progress?.update?.('Synchronisierung abgeschlossen', 95);
|
progress?.update?.('Synchronisierung abgeschlossen', 95);
|
||||||
|
return {
|
||||||
|
storeRefreshJob: data.storeRefreshJob,
|
||||||
|
storesFresh: data.storesFresh
|
||||||
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(`Session konnte nicht wiederhergestellt werden: ${err.message}`);
|
setError(`Session konnte nicht wiederhergestellt werden: ${err.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
return {};
|
||||||
},
|
},
|
||||||
[handleUnauthorized, normalizeAdminSettings]
|
[handleUnauthorized, normalizeAdminSettings]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let ticker;
|
let ticker;
|
||||||
try {
|
let cancelled = false;
|
||||||
const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY);
|
(async () => {
|
||||||
if (storedToken) {
|
try {
|
||||||
|
const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY);
|
||||||
|
if (!storedToken) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
startSyncProgress('Session wird wiederhergestellt...', 5, true);
|
startSyncProgress('Session wird wiederhergestellt...', 5, true);
|
||||||
ticker = setInterval(() => nudgeSyncProgress('Session wird wiederhergestellt...', 1, 40), 1000);
|
ticker = setInterval(() => nudgeSyncProgress('Session wird wiederhergestellt...', 1, 40), 1000);
|
||||||
(async () => {
|
const result = await bootstrapSession(storedToken, { progress: { update: updateSyncProgress } });
|
||||||
try {
|
if (ticker) {
|
||||||
await bootstrapSession(storedToken, { progress: { update: updateSyncProgress } });
|
clearInterval(ticker);
|
||||||
} finally {
|
ticker = null;
|
||||||
if (ticker) {
|
}
|
||||||
clearInterval(ticker);
|
if (!cancelled) {
|
||||||
}
|
const needsStoreSync = !result?.storesFresh || !!result?.storeRefreshJob;
|
||||||
finishSyncProgress();
|
if (needsStoreSync) {
|
||||||
|
await syncStoresWithProgress({
|
||||||
|
reason: 'session-auto',
|
||||||
|
startJob: !result?.storeRefreshJob,
|
||||||
|
reuseOverlay: true,
|
||||||
|
block: true
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Konnte gespeicherten Token nicht lesen oder wiederherstellen:', err);
|
||||||
|
} finally {
|
||||||
|
if (ticker) {
|
||||||
|
clearInterval(ticker);
|
||||||
|
}
|
||||||
|
finishSyncProgress();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
})();
|
||||||
console.warn('Konnte gespeicherten Token nicht lesen:', err);
|
|
||||||
if (ticker) {
|
|
||||||
clearInterval(ticker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return () => {
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
if (ticker) {
|
if (ticker) {
|
||||||
clearInterval(ticker);
|
clearInterval(ticker);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [bootstrapSession, startSyncProgress, updateSyncProgress, finishSyncProgress, nudgeSyncProgress]);
|
}, [
|
||||||
|
bootstrapSession,
|
||||||
|
startSyncProgress,
|
||||||
|
updateSyncProgress,
|
||||||
|
finishSyncProgress,
|
||||||
|
nudgeSyncProgress,
|
||||||
|
syncStoresWithProgress
|
||||||
|
]);
|
||||||
|
|
||||||
const authorizedFetch = useCallback(
|
const authorizedFetch = useCallback(
|
||||||
async (url, options = {}, tokenOverride) => {
|
async (url, options = {}, tokenOverride) => {
|
||||||
@@ -276,8 +302,17 @@ function App() {
|
|||||||
}
|
}
|
||||||
clearInterval(ticker);
|
clearInterval(ticker);
|
||||||
updateSyncProgress('Zugang bestätigt. Session wird aufgebaut...', 45);
|
updateSyncProgress('Zugang bestätigt. Session wird aufgebaut...', 45);
|
||||||
await bootstrapSession(data.token, { progress: { update: updateSyncProgress } });
|
const bootstrapResult = await bootstrapSession(data.token, { progress: { update: updateSyncProgress } });
|
||||||
updateSyncProgress('Login abgeschlossen', 95);
|
const needsStoreSync = !bootstrapResult?.storesFresh || !!bootstrapResult?.storeRefreshJob;
|
||||||
|
if (needsStoreSync) {
|
||||||
|
await syncStoresWithProgress({
|
||||||
|
reason: 'login-auto',
|
||||||
|
startJob: !bootstrapResult?.storeRefreshJob,
|
||||||
|
reuseOverlay: true,
|
||||||
|
block: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateSyncProgress('Login abgeschlossen', 98);
|
||||||
setStatus('Anmeldung erfolgreich. Konfiguration geladen.');
|
setStatus('Anmeldung erfolgreich. Konfiguration geladen.');
|
||||||
setTimeout(() => setStatus(''), 3000);
|
setTimeout(() => setStatus(''), 3000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -351,22 +386,101 @@ function App() {
|
|||||||
}
|
}
|
||||||
}, [session?.token, authorizedFetch]);
|
}, [session?.token, authorizedFetch]);
|
||||||
|
|
||||||
const refreshStoresAndConfig = useCallback(
|
const syncStoresWithProgress = useCallback(
|
||||||
async ({ block = false } = {}) => {
|
async ({ block = false, reason = 'manual', startJob = true, reuseOverlay = false } = {}) => {
|
||||||
if (!session?.token) {
|
if (!session?.token) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
startSyncProgress('Betriebe werden geprüft...', 15, block);
|
if (!reuseOverlay) {
|
||||||
|
startSyncProgress('Betriebe werden geprüft...', 5, block);
|
||||||
|
} else {
|
||||||
|
updateSyncProgress('Betriebe werden geprüft...', 35);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
|
let jobStarted = false;
|
||||||
|
const triggerRefresh = async () => {
|
||||||
|
const response = await authorizedFetch('/api/stores/refresh', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ force: true, reason })
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP ${response.status}`);
|
||||||
|
}
|
||||||
|
await response.json();
|
||||||
|
jobStarted = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (startJob) {
|
||||||
|
await triggerRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
let completed = false;
|
||||||
|
while (!completed) {
|
||||||
|
const statusResp = await authorizedFetch('/api/stores/refresh/status');
|
||||||
|
if (!statusResp.ok) {
|
||||||
|
throw new Error(`HTTP ${statusResp.status}`);
|
||||||
|
}
|
||||||
|
const statusData = await statusResp.json();
|
||||||
|
const job = statusData.job;
|
||||||
|
if (job?.status === 'running') {
|
||||||
|
const total = job.total || 0;
|
||||||
|
const processed = job.processed || 0;
|
||||||
|
const percent = total > 0 ? Math.min(95, 10 + Math.round((processed / total) * 80)) : undefined;
|
||||||
|
const message = job.currentStore
|
||||||
|
? `Prüfe ${job.currentStore} (${processed}/${total || '?'})`
|
||||||
|
: 'Betriebe werden geprüft...';
|
||||||
|
updateSyncProgress(message, percent);
|
||||||
|
} else if (!job) {
|
||||||
|
if (statusData.storesFresh) {
|
||||||
|
updateSyncProgress('Betriebe aktuell.', 90);
|
||||||
|
completed = true;
|
||||||
|
} else if (!jobStarted) {
|
||||||
|
await triggerRefresh();
|
||||||
|
await delay(500);
|
||||||
|
} else {
|
||||||
|
updateSyncProgress('Warte auf Rückmeldung...', undefined);
|
||||||
|
}
|
||||||
|
} else if (job.status === 'done') {
|
||||||
|
updateSyncProgress('Synchronisierung abgeschlossen', 95);
|
||||||
|
completed = true;
|
||||||
|
} else if (job.status === 'error') {
|
||||||
|
throw new Error(job.error || 'Unbekannter Fehler beim Prüfen der Betriebe.');
|
||||||
|
}
|
||||||
|
if (!completed) {
|
||||||
|
await delay(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
await fetchStoresList();
|
await fetchStoresList();
|
||||||
updateSyncProgress('Konfiguration wird aktualisiert...', 70);
|
|
||||||
await fetchConfig(undefined, { silent: true });
|
await fetchConfig(undefined, { silent: true });
|
||||||
updateSyncProgress('Synchronisierung abgeschlossen', 95);
|
setStatus('Betriebe aktualisiert.');
|
||||||
|
setTimeout(() => setStatus(''), 3000);
|
||||||
|
} catch (err) {
|
||||||
|
setError(`Aktualisieren der Betriebe fehlgeschlagen: ${err.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
finishSyncProgress();
|
if (!reuseOverlay) {
|
||||||
|
finishSyncProgress();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[session?.token, fetchStoresList, fetchConfig, startSyncProgress, updateSyncProgress, finishSyncProgress]
|
[
|
||||||
|
session?.token,
|
||||||
|
authorizedFetch,
|
||||||
|
startSyncProgress,
|
||||||
|
updateSyncProgress,
|
||||||
|
finishSyncProgress,
|
||||||
|
delay,
|
||||||
|
fetchStoresList,
|
||||||
|
fetchConfig,
|
||||||
|
setError,
|
||||||
|
setStatus
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const refreshStoresAndConfig = useCallback(
|
||||||
|
({ block = false } = {}) => syncStoresWithProgress({ block, reason: 'manual', startJob: true }),
|
||||||
|
[syncStoresWithProgress]
|
||||||
);
|
);
|
||||||
|
|
||||||
const saveConfig = async () => {
|
const saveConfig = async () => {
|
||||||
|
|||||||
22
startContainer.sh
Executable file
22
startContainer.sh
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# Optionaler erster Parameter als Git-Message
|
||||||
|
MSG="${1:-Aktueller Stand}"
|
||||||
|
|
||||||
|
# Prüfen, ob Änderungen vorhanden sind
|
||||||
|
if [[ -n $(git status --porcelain) ]]; then
|
||||||
|
echo "📦 Änderungen erkannt – committe mit Nachricht: '$MSG'"
|
||||||
|
git add .
|
||||||
|
git commit -m "$MSG"
|
||||||
|
git push
|
||||||
|
else
|
||||||
|
echo "✅ Keine Änderungen – überspringe Git-Commit."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Container neu bauen und starten
|
||||||
|
echo "🐳 Starte Docker Compose Build & Up..."
|
||||||
|
DOCKER_BUILDKIT=1 COMPOSE_DOCKER_CLI_BUILD=1 docker compose up --build -d pickup-config-app
|
||||||
|
|
||||||
|
echo "🚀 Fertig!"
|
||||||
|
|
||||||
Reference in New Issue
Block a user