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
|
||||
|
||||
# Arbeitsverzeichnis im Container
|
||||
FROM node:18-alpine AS base
|
||||
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 ./
|
||||
RUN npm ci
|
||||
|
||||
# Installation von Abhängigkeiten
|
||||
RUN npm install
|
||||
|
||||
# Kopieren des Quellcodes
|
||||
COPY . .
|
||||
|
||||
# Build der React-App
|
||||
# 2) Build the React client (only re-run when client sources change)
|
||||
COPY public ./public
|
||||
COPY src ./src
|
||||
RUN npm run build
|
||||
|
||||
# Freigegebener Port
|
||||
EXPOSE 3000
|
||||
# 3) Copy server-side files (changes here skip the expensive client build layer)
|
||||
COPY server.js ./
|
||||
COPY services ./services
|
||||
RUN mkdir -p config
|
||||
|
||||
# Starten des Node.js-Servers
|
||||
EXPOSE 3000
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
196
server.js
196
server.js
@@ -2,6 +2,7 @@ const express = require('express');
|
||||
const path = require('path');
|
||||
const cors = require('cors');
|
||||
|
||||
const { v4: uuid } = require('uuid');
|
||||
const sessionStore = require('./services/sessionStore');
|
||||
const credentialStore = require('./services/credentialStore');
|
||||
const { readConfig, writeConfig } = require('./services/configStore');
|
||||
@@ -15,6 +16,7 @@ const SIXTY_DAYS_MS = 60 * 24 * 60 * 60 * 1000;
|
||||
|
||||
const app = express();
|
||||
const port = process.env.PORT || 3000;
|
||||
const storeRefreshJobs = new Map();
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
@@ -83,30 +85,124 @@ function mergeStoresIntoConfig(config = [], stores = []) {
|
||||
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) {
|
||||
return { stores: [], refreshed: false };
|
||||
}
|
||||
|
||||
const cache = session.storesCache;
|
||||
const now = Date.now();
|
||||
const isCacheValid =
|
||||
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 cacheFresh = isStoreCacheFresh(session);
|
||||
if ((forceRefresh || !cacheFresh) && session.cookieHeader) {
|
||||
triggerStoreRefresh(session, { force: true, reason: reason || 'session-check' });
|
||||
}
|
||||
|
||||
const stores = await foodsharingClient.fetchStores(session.cookieHeader, session.profile.id, {
|
||||
delayBetweenRequestsMs: settings.storePickupCheckDelayMs
|
||||
});
|
||||
sessionStore.update(session.id, {
|
||||
storesCache: { data: stores, fetchedAt: now }
|
||||
});
|
||||
return { stores, refreshed: true };
|
||||
return {
|
||||
stores: session.storesCache?.data || [],
|
||||
refreshed: cacheFresh
|
||||
};
|
||||
}
|
||||
|
||||
async function restoreSessionsFromDisk() {
|
||||
@@ -202,16 +298,7 @@ app.post('/api/auth/login', async (req, res) => {
|
||||
};
|
||||
const isAdminUser = isAdmin(profile);
|
||||
const settings = adminConfig.readSettings();
|
||||
|
||||
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 existingToken = existingCredentials?.token;
|
||||
@@ -228,18 +315,18 @@ app.post('/api/auth/login', async (req, res) => {
|
||||
}, existingToken, ONE_YEAR_MS);
|
||||
|
||||
credentialStore.save(profile.id, { email, password, token: session.id });
|
||||
sessionStore.update(session.id, {
|
||||
storesCache: { data: stores, fetchedAt: Date.now() }
|
||||
});
|
||||
scheduleConfig(session.id, config, settings);
|
||||
const refreshResult = triggerStoreRefresh(session, { force: true, reason: 'login' });
|
||||
|
||||
return res.json({
|
||||
token: session.id,
|
||||
profile,
|
||||
stores,
|
||||
stores: session.storesCache?.data || [],
|
||||
config,
|
||||
isAdmin: isAdminUser,
|
||||
adminSettings: isAdminUser ? settings : undefined
|
||||
adminSettings: isAdminUser ? settings : undefined,
|
||||
storeRefreshJob: summarizeJob(refreshResult.job),
|
||||
storesFresh: isStoreCacheFresh(session)
|
||||
});
|
||||
} catch (error) {
|
||||
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) => {
|
||||
sessionStore.delete(req.session.id);
|
||||
credentialStore.remove(req.session.profile.id);
|
||||
storeRefreshJobs.delete(req.session.id);
|
||||
res.json({ success: true });
|
||||
});
|
||||
|
||||
app.get('/api/auth/session', requireAuth, async (req, res) => {
|
||||
const settings = adminConfig.readSettings();
|
||||
const { stores, refreshed } = await loadStoresForSession(req.session, settings);
|
||||
if (refreshed) {
|
||||
let config = readConfig(req.session.profile.id);
|
||||
const { merged, changed } = mergeStoresIntoConfig(config, stores);
|
||||
if (changed) {
|
||||
writeConfig(req.session.profile.id, merged);
|
||||
}
|
||||
}
|
||||
const { stores } = await loadStoresForSession(req.session, settings, { reason: 'session-check' });
|
||||
res.json({
|
||||
profile: req.session.profile,
|
||||
stores,
|
||||
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) => {
|
||||
const settings = adminConfig.readSettings();
|
||||
const { stores, refreshed } = await loadStoresForSession(req.session, settings, { forceRefresh: true });
|
||||
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(stores);
|
||||
res.json(req.session.storesCache?.data || []);
|
||||
});
|
||||
|
||||
app.post('/api/stores/refresh', requireAuth, (req, res) => {
|
||||
const force = req.body?.force !== undefined ? !!req.body.force : true;
|
||||
const reason = req.body?.reason || 'manual';
|
||||
const result = triggerStoreRefresh(req.session, { force, reason });
|
||||
res.json({
|
||||
started: !!result.started,
|
||||
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) => {
|
||||
|
||||
@@ -126,6 +126,10 @@ async function fetchStores(cookieHeader, profileId, options = {}) {
|
||||
const delayBetweenRequestsMs = Number.isFinite(options.delayBetweenRequestsMs)
|
||||
? Math.max(0, options.delayBetweenRequestsMs)
|
||||
: 0;
|
||||
const onStoreCheck =
|
||||
typeof options.onStoreCheck === 'function'
|
||||
? options.onStoreCheck
|
||||
: null;
|
||||
try {
|
||||
const response = await client.get(`/api/user/${profileId}/stores`, {
|
||||
headers: buildHeaders(cookieHeader),
|
||||
@@ -143,14 +147,14 @@ async function fetchStores(cookieHeader, profileId, options = {}) {
|
||||
zip: store.zip || ''
|
||||
}));
|
||||
|
||||
return annotateStoresWithPickupSlots(normalized, cookieHeader, delayBetweenRequestsMs);
|
||||
return annotateStoresWithPickupSlots(normalized, cookieHeader, delayBetweenRequestsMs, onStoreCheck);
|
||||
} catch (error) {
|
||||
console.warn('Stores konnten nicht geladen werden:', error.message);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function annotateStoresWithPickupSlots(stores, cookieHeader, delayBetweenRequestsMs = 0) {
|
||||
async function annotateStoresWithPickupSlots(stores, cookieHeader, delayBetweenRequestsMs = 0, onStoreCheck) {
|
||||
if (!Array.isArray(stores) || stores.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -160,6 +164,13 @@ async function annotateStoresWithPickupSlots(stores, cookieHeader, delayBetweenR
|
||||
|
||||
for (let index = 0; index < stores.length; index += 1) {
|
||||
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) {
|
||||
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 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) => {
|
||||
setSyncProgress({ active: true, percent, message, block });
|
||||
@@ -117,7 +118,7 @@ function App() {
|
||||
const bootstrapSession = useCallback(
|
||||
async (token, { progress } = {}) => {
|
||||
if (!token) {
|
||||
return;
|
||||
return {};
|
||||
}
|
||||
setLoading(true);
|
||||
setError('');
|
||||
@@ -153,45 +154,70 @@ function App() {
|
||||
progress?.update?.('Konfiguration wird geladen...', 75);
|
||||
setConfig(Array.isArray(configData) ? configData : []);
|
||||
progress?.update?.('Synchronisierung abgeschlossen', 95);
|
||||
return {
|
||||
storeRefreshJob: data.storeRefreshJob,
|
||||
storesFresh: data.storesFresh
|
||||
};
|
||||
} catch (err) {
|
||||
setError(`Session konnte nicht wiederhergestellt werden: ${err.message}`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
return {};
|
||||
},
|
||||
[handleUnauthorized, normalizeAdminSettings]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
let ticker;
|
||||
try {
|
||||
const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY);
|
||||
if (storedToken) {
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const storedToken = localStorage.getItem(TOKEN_STORAGE_KEY);
|
||||
if (!storedToken) {
|
||||
return;
|
||||
}
|
||||
startSyncProgress('Session wird wiederhergestellt...', 5, true);
|
||||
ticker = setInterval(() => nudgeSyncProgress('Session wird wiederhergestellt...', 1, 40), 1000);
|
||||
(async () => {
|
||||
try {
|
||||
await bootstrapSession(storedToken, { progress: { update: updateSyncProgress } });
|
||||
} finally {
|
||||
if (ticker) {
|
||||
clearInterval(ticker);
|
||||
}
|
||||
finishSyncProgress();
|
||||
const result = await bootstrapSession(storedToken, { progress: { update: updateSyncProgress } });
|
||||
if (ticker) {
|
||||
clearInterval(ticker);
|
||||
ticker = null;
|
||||
}
|
||||
if (!cancelled) {
|
||||
const needsStoreSync = !result?.storesFresh || !!result?.storeRefreshJob;
|
||||
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 () => {
|
||||
cancelled = true;
|
||||
if (ticker) {
|
||||
clearInterval(ticker);
|
||||
}
|
||||
};
|
||||
}, [bootstrapSession, startSyncProgress, updateSyncProgress, finishSyncProgress, nudgeSyncProgress]);
|
||||
}, [
|
||||
bootstrapSession,
|
||||
startSyncProgress,
|
||||
updateSyncProgress,
|
||||
finishSyncProgress,
|
||||
nudgeSyncProgress,
|
||||
syncStoresWithProgress
|
||||
]);
|
||||
|
||||
const authorizedFetch = useCallback(
|
||||
async (url, options = {}, tokenOverride) => {
|
||||
@@ -276,8 +302,17 @@ function App() {
|
||||
}
|
||||
clearInterval(ticker);
|
||||
updateSyncProgress('Zugang bestätigt. Session wird aufgebaut...', 45);
|
||||
await bootstrapSession(data.token, { progress: { update: updateSyncProgress } });
|
||||
updateSyncProgress('Login abgeschlossen', 95);
|
||||
const bootstrapResult = await bootstrapSession(data.token, { progress: { update: updateSyncProgress } });
|
||||
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.');
|
||||
setTimeout(() => setStatus(''), 3000);
|
||||
} catch (err) {
|
||||
@@ -351,22 +386,101 @@ function App() {
|
||||
}
|
||||
}, [session?.token, authorizedFetch]);
|
||||
|
||||
const refreshStoresAndConfig = useCallback(
|
||||
async ({ block = false } = {}) => {
|
||||
const syncStoresWithProgress = useCallback(
|
||||
async ({ block = false, reason = 'manual', startJob = true, reuseOverlay = false } = {}) => {
|
||||
if (!session?.token) {
|
||||
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 {
|
||||
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();
|
||||
updateSyncProgress('Konfiguration wird aktualisiert...', 70);
|
||||
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 {
|
||||
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 () => {
|
||||
|
||||
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