aktueller Stand
This commit is contained in:
22
server.js
22
server.js
@@ -105,7 +105,9 @@ async function restoreSessionsFromDisk() {
|
|||||||
};
|
};
|
||||||
const isAdminUser = isAdmin(profile);
|
const isAdminUser = isAdmin(profile);
|
||||||
let config = readConfig(profile.id);
|
let config = readConfig(profile.id);
|
||||||
const stores = await foodsharingClient.fetchStores(auth.cookieHeader, profile.id);
|
const stores = await foodsharingClient.fetchStores(auth.cookieHeader, profile.id, {
|
||||||
|
delayBetweenRequestsMs: schedulerSettings.storePickupCheckDelayMs
|
||||||
|
});
|
||||||
const { merged, changed } = mergeStoresIntoConfig(config, stores);
|
const { merged, changed } = mergeStoresIntoConfig(config, stores);
|
||||||
if (changed) {
|
if (changed) {
|
||||||
config = merged;
|
config = merged;
|
||||||
@@ -169,9 +171,12 @@ app.post('/api/auth/login', async (req, res) => {
|
|||||||
email: auth.profile.email || email
|
email: auth.profile.email || email
|
||||||
};
|
};
|
||||||
const isAdminUser = isAdmin(profile);
|
const isAdminUser = isAdmin(profile);
|
||||||
|
const settings = adminConfig.readSettings();
|
||||||
|
|
||||||
let config = readConfig(profile.id);
|
let config = readConfig(profile.id);
|
||||||
const stores = await foodsharingClient.fetchStores(auth.cookieHeader, profile.id);
|
const stores = await foodsharingClient.fetchStores(auth.cookieHeader, profile.id, {
|
||||||
|
delayBetweenRequestsMs: settings.storePickupCheckDelayMs
|
||||||
|
});
|
||||||
const { merged, changed } = mergeStoresIntoConfig(config, stores);
|
const { merged, changed } = mergeStoresIntoConfig(config, stores);
|
||||||
if (changed) {
|
if (changed) {
|
||||||
config = merged;
|
config = merged;
|
||||||
@@ -193,7 +198,6 @@ 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 });
|
||||||
const settings = adminConfig.readSettings();
|
|
||||||
scheduleConfig(session.id, config, settings);
|
scheduleConfig(session.id, config, settings);
|
||||||
|
|
||||||
return res.json({
|
return res.json({
|
||||||
@@ -217,7 +221,10 @@ app.post('/api/auth/logout', requireAuth, (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/auth/session', requireAuth, async (req, res) => {
|
app.get('/api/auth/session', requireAuth, async (req, res) => {
|
||||||
const stores = await foodsharingClient.fetchStores(req.session.cookieHeader, req.session.profile.id);
|
const settings = adminConfig.readSettings();
|
||||||
|
const stores = await foodsharingClient.fetchStores(req.session.cookieHeader, req.session.profile.id, {
|
||||||
|
delayBetweenRequestsMs: settings.storePickupCheckDelayMs
|
||||||
|
});
|
||||||
let config = readConfig(req.session.profile.id);
|
let config = readConfig(req.session.profile.id);
|
||||||
const { merged, changed } = mergeStoresIntoConfig(config, stores);
|
const { merged, changed } = mergeStoresIntoConfig(config, stores);
|
||||||
if (changed) {
|
if (changed) {
|
||||||
@@ -228,7 +235,7 @@ app.get('/api/auth/session', requireAuth, async (req, res) => {
|
|||||||
profile: req.session.profile,
|
profile: req.session.profile,
|
||||||
stores,
|
stores,
|
||||||
isAdmin: !!req.session.isAdmin,
|
isAdmin: !!req.session.isAdmin,
|
||||||
adminSettings: req.session.isAdmin ? adminConfig.readSettings() : undefined
|
adminSettings: req.session.isAdmin ? settings : undefined
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -254,7 +261,10 @@ app.post('/api/config', requireAuth, (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/stores', requireAuth, async (req, res) => {
|
app.get('/api/stores', requireAuth, async (req, res) => {
|
||||||
const stores = await foodsharingClient.fetchStores(req.session.cookieHeader, req.session.profile.id);
|
const settings = adminConfig.readSettings();
|
||||||
|
const stores = await foodsharingClient.fetchStores(req.session.cookieHeader, req.session.profile.id, {
|
||||||
|
delayBetweenRequestsMs: settings.storePickupCheckDelayMs
|
||||||
|
});
|
||||||
let config = readConfig(req.session.profile.id);
|
let config = readConfig(req.session.profile.id);
|
||||||
const { merged, changed } = mergeStoresIntoConfig(config, stores);
|
const { merged, changed } = mergeStoresIntoConfig(config, stores);
|
||||||
if (changed) {
|
if (changed) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ const DEFAULT_SETTINGS = {
|
|||||||
randomDelayMaxSeconds: 120,
|
randomDelayMaxSeconds: 120,
|
||||||
initialDelayMinSeconds: 5,
|
initialDelayMinSeconds: 5,
|
||||||
initialDelayMaxSeconds: 30,
|
initialDelayMaxSeconds: 30,
|
||||||
|
storePickupCheckDelayMs: 400,
|
||||||
ignoredSlots: [
|
ignoredSlots: [
|
||||||
{
|
{
|
||||||
storeId: '51450',
|
storeId: '51450',
|
||||||
@@ -60,6 +61,10 @@ function readSettings() {
|
|||||||
randomDelayMaxSeconds: sanitizeNumber(parsed.randomDelayMaxSeconds, DEFAULT_SETTINGS.randomDelayMaxSeconds),
|
randomDelayMaxSeconds: sanitizeNumber(parsed.randomDelayMaxSeconds, DEFAULT_SETTINGS.randomDelayMaxSeconds),
|
||||||
initialDelayMinSeconds: sanitizeNumber(parsed.initialDelayMinSeconds, DEFAULT_SETTINGS.initialDelayMinSeconds),
|
initialDelayMinSeconds: sanitizeNumber(parsed.initialDelayMinSeconds, DEFAULT_SETTINGS.initialDelayMinSeconds),
|
||||||
initialDelayMaxSeconds: sanitizeNumber(parsed.initialDelayMaxSeconds, DEFAULT_SETTINGS.initialDelayMaxSeconds),
|
initialDelayMaxSeconds: sanitizeNumber(parsed.initialDelayMaxSeconds, DEFAULT_SETTINGS.initialDelayMaxSeconds),
|
||||||
|
storePickupCheckDelayMs: sanitizeNumber(
|
||||||
|
parsed.storePickupCheckDelayMs,
|
||||||
|
DEFAULT_SETTINGS.storePickupCheckDelayMs
|
||||||
|
),
|
||||||
ignoredSlots: sanitizeIgnoredSlots(parsed.ignoredSlots)
|
ignoredSlots: sanitizeIgnoredSlots(parsed.ignoredSlots)
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -76,6 +81,10 @@ function writeSettings(patch = {}) {
|
|||||||
randomDelayMaxSeconds: sanitizeNumber(patch.randomDelayMaxSeconds, current.randomDelayMaxSeconds),
|
randomDelayMaxSeconds: sanitizeNumber(patch.randomDelayMaxSeconds, current.randomDelayMaxSeconds),
|
||||||
initialDelayMinSeconds: sanitizeNumber(patch.initialDelayMinSeconds, current.initialDelayMinSeconds),
|
initialDelayMinSeconds: sanitizeNumber(patch.initialDelayMinSeconds, current.initialDelayMinSeconds),
|
||||||
initialDelayMaxSeconds: sanitizeNumber(patch.initialDelayMaxSeconds, current.initialDelayMaxSeconds),
|
initialDelayMaxSeconds: sanitizeNumber(patch.initialDelayMaxSeconds, current.initialDelayMaxSeconds),
|
||||||
|
storePickupCheckDelayMs: sanitizeNumber(
|
||||||
|
patch.storePickupCheckDelayMs,
|
||||||
|
current.storePickupCheckDelayMs
|
||||||
|
),
|
||||||
ignoredSlots:
|
ignoredSlots:
|
||||||
patch.ignoredSlots !== undefined
|
patch.ignoredSlots !== undefined
|
||||||
? sanitizeIgnoredSlots(patch.ignoredSlots)
|
? sanitizeIgnoredSlots(patch.ignoredSlots)
|
||||||
|
|||||||
@@ -115,10 +115,17 @@ async function fetchProfile(cookieHeader) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchStores(cookieHeader, profileId) {
|
function wait(ms = 0) {
|
||||||
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchStores(cookieHeader, profileId, options = {}) {
|
||||||
if (!profileId) {
|
if (!profileId) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
const delayBetweenRequestsMs = Number.isFinite(options.delayBetweenRequestsMs)
|
||||||
|
? Math.max(0, options.delayBetweenRequestsMs)
|
||||||
|
: 0;
|
||||||
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),
|
||||||
@@ -136,42 +143,37 @@ async function fetchStores(cookieHeader, profileId) {
|
|||||||
zip: store.zip || ''
|
zip: store.zip || ''
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return annotateStoresWithPickupSlots(normalized, cookieHeader);
|
return annotateStoresWithPickupSlots(normalized, cookieHeader, delayBetweenRequestsMs);
|
||||||
} 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, concurrency = 5) {
|
async function annotateStoresWithPickupSlots(stores, cookieHeader, delayBetweenRequestsMs = 0) {
|
||||||
if (!Array.isArray(stores) || stores.length === 0) {
|
if (!Array.isArray(stores) || stores.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const cappedConcurrency = Math.max(1, Math.min(concurrency, stores.length));
|
const delayMs = Number.isFinite(delayBetweenRequestsMs) ? Math.max(0, delayBetweenRequestsMs) : 0;
|
||||||
const results = new Array(stores.length);
|
const annotated = [];
|
||||||
let pointer = 0;
|
|
||||||
|
|
||||||
async function worker() {
|
for (let index = 0; index < stores.length; index += 1) {
|
||||||
while (true) {
|
const store = stores[index];
|
||||||
const currentIndex = pointer++;
|
if (delayMs > 0 && index > 0) {
|
||||||
if (currentIndex >= stores.length) {
|
await wait(delayMs);
|
||||||
return;
|
|
||||||
}
|
|
||||||
const store = stores[currentIndex];
|
|
||||||
let hasPickupSlots = null;
|
|
||||||
try {
|
|
||||||
const pickups = await fetchPickups(store.id, cookieHeader);
|
|
||||||
hasPickupSlots = Array.isArray(pickups) && pickups.length > 0;
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Pickups für Store ${store.id} konnten nicht geprüft werden:`, error.message);
|
|
||||||
}
|
|
||||||
results[currentIndex] = { ...store, hasPickupSlots };
|
|
||||||
}
|
}
|
||||||
|
let hasPickupSlots = null;
|
||||||
|
try {
|
||||||
|
const pickups = await fetchPickups(store.id, cookieHeader);
|
||||||
|
hasPickupSlots = Array.isArray(pickups) && pickups.length > 0;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`Pickups für Store ${store.id} konnten nicht geprüft werden:`, error.message);
|
||||||
|
}
|
||||||
|
annotated.push({ ...store, hasPickupSlots });
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(Array.from({ length: cappedConcurrency }, () => worker()));
|
return annotated;
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchPickups(storeId, cookieHeader) {
|
async function fetchPickups(storeId, cookieHeader) {
|
||||||
|
|||||||
143
src/App.js
143
src/App.js
@@ -25,9 +25,39 @@ function App() {
|
|||||||
const [availableCollapsed, setAvailableCollapsed] = useState(true);
|
const [availableCollapsed, setAvailableCollapsed] = useState(true);
|
||||||
const [adminSettings, setAdminSettings] = useState(null);
|
const [adminSettings, setAdminSettings] = useState(null);
|
||||||
const [adminSettingsLoading, setAdminSettingsLoading] = useState(false);
|
const [adminSettingsLoading, setAdminSettingsLoading] = useState(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 startSyncProgress = useCallback((message, percent, block = false) => {
|
||||||
|
setSyncProgress({ active: true, percent, message, block });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateSyncProgress = useCallback((message, percent) => {
|
||||||
|
setSyncProgress((prev) => {
|
||||||
|
if (!prev.active) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
message: message || prev.message,
|
||||||
|
percent: Math.min(100, Math.max(percent, prev.percent))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const finishSyncProgress = useCallback(() => {
|
||||||
|
setSyncProgress((prev) => {
|
||||||
|
if (!prev.active) {
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
return { ...prev, percent: 100 };
|
||||||
|
});
|
||||||
|
setTimeout(() => {
|
||||||
|
setSyncProgress({ active: false, percent: 0, message: '', block: false });
|
||||||
|
}, 400);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const normalizeAdminSettings = useCallback((raw) => {
|
const normalizeAdminSettings = useCallback((raw) => {
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return null;
|
return null;
|
||||||
@@ -38,6 +68,7 @@ function App() {
|
|||||||
randomDelayMaxSeconds: raw.randomDelayMaxSeconds ?? '',
|
randomDelayMaxSeconds: raw.randomDelayMaxSeconds ?? '',
|
||||||
initialDelayMinSeconds: raw.initialDelayMinSeconds ?? '',
|
initialDelayMinSeconds: raw.initialDelayMinSeconds ?? '',
|
||||||
initialDelayMaxSeconds: raw.initialDelayMaxSeconds ?? '',
|
initialDelayMaxSeconds: raw.initialDelayMaxSeconds ?? '',
|
||||||
|
storePickupCheckDelayMs: raw.storePickupCheckDelayMs ?? '',
|
||||||
ignoredSlots: Array.isArray(raw.ignoredSlots)
|
ignoredSlots: Array.isArray(raw.ignoredSlots)
|
||||||
? raw.ignoredSlots.map((slot) => ({
|
? raw.ignoredSlots.map((slot) => ({
|
||||||
storeId: slot?.storeId ? String(slot.storeId) : '',
|
storeId: slot?.storeId ? String(slot.storeId) : '',
|
||||||
@@ -70,9 +101,15 @@ function App() {
|
|||||||
}, [resetSessionState]);
|
}, [resetSessionState]);
|
||||||
|
|
||||||
const bootstrapSession = useCallback(
|
const bootstrapSession = useCallback(
|
||||||
async (token) => {
|
async (token, { withProgress = false } = {}) => {
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError('');
|
setError('');
|
||||||
|
if (withProgress) {
|
||||||
|
startSyncProgress('Session wird aufgebaut...', 10, true);
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const response = await fetch('/api/auth/session', {
|
const response = await fetch('/api/auth/session', {
|
||||||
headers: { Authorization: `Bearer ${token}` }
|
headers: { Authorization: `Bearer ${token}` }
|
||||||
@@ -86,6 +123,9 @@ function App() {
|
|||||||
}
|
}
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setSession({ token, profile: data.profile, isAdmin: data.isAdmin });
|
setSession({ token, profile: data.profile, isAdmin: data.isAdmin });
|
||||||
|
if (withProgress) {
|
||||||
|
updateSyncProgress('Betriebe werden geprüft...', 45);
|
||||||
|
}
|
||||||
setStores(Array.isArray(data.stores) ? data.stores : []);
|
setStores(Array.isArray(data.stores) ? data.stores : []);
|
||||||
setAdminSettings(data.isAdmin ? normalizeAdminSettings(data.adminSettings) : null);
|
setAdminSettings(data.isAdmin ? normalizeAdminSettings(data.adminSettings) : null);
|
||||||
|
|
||||||
@@ -100,14 +140,23 @@ function App() {
|
|||||||
throw new Error(`HTTP ${configResponse.status}`);
|
throw new Error(`HTTP ${configResponse.status}`);
|
||||||
}
|
}
|
||||||
const configData = await configResponse.json();
|
const configData = await configResponse.json();
|
||||||
|
if (withProgress) {
|
||||||
|
updateSyncProgress('Konfiguration wird geladen...', 75);
|
||||||
|
}
|
||||||
setConfig(Array.isArray(configData) ? configData : []);
|
setConfig(Array.isArray(configData) ? configData : []);
|
||||||
|
if (withProgress) {
|
||||||
|
updateSyncProgress('Synchronisierung abgeschlossen', 95);
|
||||||
|
}
|
||||||
} 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);
|
||||||
|
if (withProgress) {
|
||||||
|
finishSyncProgress();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[handleUnauthorized, normalizeAdminSettings]
|
[handleUnauthorized, normalizeAdminSettings, startSyncProgress, updateSyncProgress, finishSyncProgress]
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -200,10 +249,7 @@ function App() {
|
|||||||
} catch (storageError) {
|
} catch (storageError) {
|
||||||
console.warn('Konnte Token nicht speichern:', storageError);
|
console.warn('Konnte Token nicht speichern:', storageError);
|
||||||
}
|
}
|
||||||
setSession({ token: data.token, profile: data.profile, isAdmin: data.isAdmin });
|
await bootstrapSession(data.token, { withProgress: true });
|
||||||
setConfig(Array.isArray(data.config) ? data.config : []);
|
|
||||||
setStores(Array.isArray(data.stores) ? data.stores : []);
|
|
||||||
setAdminSettings(data.isAdmin ? normalizeAdminSettings(data.adminSettings) : null);
|
|
||||||
setStatus('Anmeldung erfolgreich. Konfiguration geladen.');
|
setStatus('Anmeldung erfolgreich. Konfiguration geladen.');
|
||||||
setTimeout(() => setStatus(''), 3000);
|
setTimeout(() => setStatus(''), 3000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -276,6 +322,24 @@ function App() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshStoresAndConfig = useCallback(
|
||||||
|
async ({ block = false } = {}) => {
|
||||||
|
if (!session?.token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startSyncProgress('Betriebe werden geprüft...', 15, block);
|
||||||
|
try {
|
||||||
|
await fetchStoresList();
|
||||||
|
updateSyncProgress('Konfiguration wird aktualisiert...', 70);
|
||||||
|
await fetchConfig(undefined, { silent: true });
|
||||||
|
updateSyncProgress('Synchronisierung abgeschlossen', 95);
|
||||||
|
} finally {
|
||||||
|
finishSyncProgress();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[session?.token, fetchStoresList, fetchConfig, startSyncProgress, updateSyncProgress, finishSyncProgress]
|
||||||
|
);
|
||||||
|
|
||||||
const saveConfig = async () => {
|
const saveConfig = async () => {
|
||||||
if (!session?.token) {
|
if (!session?.token) {
|
||||||
return;
|
return;
|
||||||
@@ -571,6 +635,7 @@ function App() {
|
|||||||
randomDelayMaxSeconds: toNumber(adminSettings.randomDelayMaxSeconds),
|
randomDelayMaxSeconds: toNumber(adminSettings.randomDelayMaxSeconds),
|
||||||
initialDelayMinSeconds: toNumber(adminSettings.initialDelayMinSeconds),
|
initialDelayMinSeconds: toNumber(adminSettings.initialDelayMinSeconds),
|
||||||
initialDelayMaxSeconds: toNumber(adminSettings.initialDelayMaxSeconds),
|
initialDelayMaxSeconds: toNumber(adminSettings.initialDelayMaxSeconds),
|
||||||
|
storePickupCheckDelayMs: toNumber(adminSettings.storePickupCheckDelayMs),
|
||||||
ignoredSlots: (adminSettings.ignoredSlots || []).map((slot) => ({
|
ignoredSlots: (adminSettings.ignoredSlots || []).map((slot) => ({
|
||||||
storeId: slot.storeId || '',
|
storeId: slot.storeId || '',
|
||||||
description: slot.description || ''
|
description: slot.description || ''
|
||||||
@@ -672,7 +737,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<button
|
<button
|
||||||
onClick={fetchStoresList}
|
onClick={() => refreshStoresAndConfig({ block: false })}
|
||||||
className="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors"
|
className="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded focus:outline-none focus:ring-2 focus:ring-blue-500 transition-colors"
|
||||||
>
|
>
|
||||||
Betriebe aktualisieren
|
Betriebe aktualisieren
|
||||||
@@ -721,10 +786,11 @@ function App() {
|
|||||||
statusClass = 'text-gray-500';
|
statusClass = 'text-gray-500';
|
||||||
} else if (needsRestore) {
|
} else if (needsRestore) {
|
||||||
statusLabel = 'Ausgeblendet – erneut hinzufügen';
|
statusLabel = 'Ausgeblendet – erneut hinzufügen';
|
||||||
|
statusClass = 'text-amber-600';
|
||||||
}
|
}
|
||||||
if (blockedByNoPickups) {
|
if (blockedByNoPickups) {
|
||||||
statusLabel = 'Keine Pickups – automatisch verborgen';
|
statusLabel = 'Keine Pickups – automatisch verborgen';
|
||||||
statusClass = 'text-amber-600';
|
statusClass = 'text-red-600';
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
@@ -1104,6 +1170,18 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Verzögerung Store-Prüfung (ms)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
value={adminSettings.storePickupCheckDelayMs}
|
||||||
|
onChange={(e) => handleAdminSettingChange('storePickupCheckDelayMs', e.target.value, true)}
|
||||||
|
className="border rounded p-2 w-full focus:outline-none focus:ring-2 focus:ring-purple-500 focus:border-purple-500"
|
||||||
|
placeholder="z. B. 400"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-gray-500 mt-1">Hilft Rate-Limits beim Abfragen der Pickups zu vermeiden.</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="border border-purple-200 rounded-lg bg-white p-4">
|
<div className="border border-purple-200 rounded-lg bg-white p-4">
|
||||||
@@ -1166,16 +1244,19 @@ function App() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<div className="min-h-screen bg-gray-100 py-6">
|
<>
|
||||||
<div className="max-w-7xl mx-auto px-4">
|
<div className="min-h-screen bg-gray-100 py-6">
|
||||||
<NavigationTabs isAdmin={session?.isAdmin} />
|
<div className="max-w-7xl mx-auto px-4">
|
||||||
<Routes>
|
<NavigationTabs isAdmin={session?.isAdmin} />
|
||||||
<Route path="/" element={dashboardContent} />
|
<Routes>
|
||||||
<Route path="/admin" element={adminPageContent} />
|
<Route path="/" element={dashboardContent} />
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="/admin" element={adminPageContent} />
|
||||||
</Routes>
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
|
</Routes>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<StoreSyncOverlay state={syncProgress} />
|
||||||
|
</>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1222,4 +1303,32 @@ function AdminAccessMessage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function StoreSyncOverlay({ state }) {
|
||||||
|
if (!state?.active) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const percent = Math.round(state.percent || 0);
|
||||||
|
const backgroundColor = state.block ? 'rgba(255,255,255,0.95)' : 'rgba(15,23,42,0.4)';
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-0 z-50 flex items-center justify-center px-4" style={{ backgroundColor }}>
|
||||||
|
<div className="bg-white shadow-2xl rounded-lg p-6 w-full max-w-md">
|
||||||
|
<p className="text-gray-800 font-semibold mb-3">{state.message || 'Synchronisiere...'}</p>
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-3 overflow-hidden">
|
||||||
|
<div
|
||||||
|
className="h-3 bg-blue-600 transition-all duration-300 ease-out"
|
||||||
|
style={{ width: `${percent}%` }}
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between mt-2 text-sm text-gray-500">
|
||||||
|
<span>{percent}%</span>
|
||||||
|
<span>Bitte warten...</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-gray-400 mt-2">
|
||||||
|
Die Verzögerung schützt vor Rate-Limits während die Betriebe geprüft werden.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
|
|||||||
Reference in New Issue
Block a user