diff --git a/.cursor/rules/legacy-cpp-workers.mdc b/.cursor/rules/legacy-cpp-workers.mdc new file mode 100644 index 0000000..bf6c17d --- /dev/null +++ b/.cursor/rules/legacy-cpp-workers.mdc @@ -0,0 +1,9 @@ +--- +description: C++-Worker unter src/ sind obsolet — nicht erweitern oder als Quelle für Spiellogik nutzen +alwaysApply: true +--- + +# Legacy C++ (`src/`) + +- Verzeichnis **`src/`** (C++-Worker, WebSocket-Server): **obsolet**. Keine neuen Features, keine fachlichen Fixes dort planen oder umsetzen, sofern der Nutzer nicht ausdrücklich etwas anderes verlangt. +- Falukant-Hintergrundlogik: **Backend** (`backend/`), **externer Daemon**, **Frontend** — siehe `docs/LEGACY_CPP_WORKERS.md`. diff --git a/README.md b/README.md index 1f543d3..30000ac 100644 --- a/README.md +++ b/README.md @@ -1 +1,3 @@ -## zum testen des push \ No newline at end of file +## zum testen des push + +Hinweis: Das Verzeichnis **`src/`** (C++-Worker) ist veraltet; siehe [`docs/LEGACY_CPP_WORKERS.md`](docs/LEGACY_CPP_WORKERS.md). \ No newline at end of file diff --git a/backend/controllers/falukantController.js b/backend/controllers/falukantController.js index 9a7e5ab..7ceaac5 100644 --- a/backend/controllers/falukantController.js +++ b/backend/controllers/falukantController.js @@ -245,7 +245,9 @@ class FalukantController { if (Number.isNaN(regionId)) { throw new Error('regionId is required'); } - return this.service.getAllProductPricesInRegion(userId, regionId); + const networkWorth = req.query.networkWorth === '1' || req.query.networkWorth === 'true'; + const branchId = req.query.branchId != null ? parseInt(req.query.branchId, 10) : null; + return this.service.getAllProductPricesInRegion(userId, regionId, { networkWorth, branchId }); }); this.getProductPricesInCities = this._wrapWithUser((userId, req) => { const productId = parseInt(req.query.productId, 10); diff --git a/backend/services/falukantService.js b/backend/services/falukantService.js index c36d7ac..34fb510 100644 --- a/backend/services/falukantService.js +++ b/backend/services/falukantService.js @@ -5821,51 +5821,61 @@ class FalukantService extends BaseService { const oneWeekAgo = new Date(now.getTime()); oneWeekAgo.setDate(oneWeekAgo.getDate() - 7); if (user.lastNobilityAdvanceAt > oneWeekAgo) { - throw new Error('too soon'); + const last = new Date(user.lastNobilityAdvanceAt); + const retryAt = new Date(last.getTime()); + retryAt.setDate(retryAt.getDate() + 7); + throw { status: 412, message: 'nobilityTooSoon', retryAt: retryAt.toISOString() }; } } const nextTitle = nobility.next.toJSON(); - let fulfilled = true; let cost = 0; + const unmet = []; for (const requirement of nextTitle.requirements) { + let ok = true; switch (requirement.requirementType) { case 'money': - fulfilled = fulfilled && await this.checkMoneyRequirement(user, requirement); + ok = await this.checkMoneyRequirement(user, requirement); break; case 'cost': - fulfilled = fulfilled && await this.checkMoneyRequirement(user, requirement); + ok = await this.checkMoneyRequirement(user, requirement); cost = requirement.requirementValue; break; case 'branches': - fulfilled = fulfilled && await this.checkBranchesRequirement(hashedUserId, requirement); + ok = await this.checkBranchesRequirement(hashedUserId, requirement); break; case 'reputation': - fulfilled = fulfilled && await this.checkReputationRequirement(user, requirement); + ok = await this.checkReputationRequirement(user, requirement); break; case 'house_position': - fulfilled = fulfilled && await this.checkHousePositionRequirement(user, requirement); + ok = await this.checkHousePositionRequirement(user, requirement); break; case 'house_condition': - fulfilled = fulfilled && await this.checkHouseConditionRequirement(user, requirement); + ok = await this.checkHouseConditionRequirement(user, requirement); break; case 'office_rank_any': - fulfilled = fulfilled && await this.checkOfficeRankAnyRequirement(user, requirement); + ok = await this.checkOfficeRankAnyRequirement(user, requirement); break; case 'office_rank_political': - fulfilled = fulfilled && await this.checkOfficeRankPoliticalRequirement(user, requirement); + ok = await this.checkOfficeRankPoliticalRequirement(user, requirement); break; case 'lover_count_max': - fulfilled = fulfilled && await this.checkLoverCountMaxRequirement(user, requirement); + ok = await this.checkLoverCountMaxRequirement(user, requirement); break; case 'lover_count_min': - fulfilled = fulfilled && await this.checkLoverCountMinRequirement(user, requirement); + ok = await this.checkLoverCountMinRequirement(user, requirement); break; default: - fulfilled = false; - }; + ok = false; + } + if (!ok) { + unmet.push({ + type: requirement.requirementType, + required: requirement.requirementValue, + }); + } } - if (!fulfilled) { - throw new Error('Requirements not fulfilled'); + if (unmet.length > 0) { + throw { status: 412, message: 'nobilityRequirements', unmet }; } const newTitle = await TitleOfNobility.findOne({ where: { level: nobility.current.level + 1 } @@ -6793,13 +6803,42 @@ class FalukantService extends BaseService { } } - async getAllProductPricesInRegion(hashedUserId, regionId) { + /** + * Regionale Verkaufspreise (Wissen + town_product_worth), für Ertrags-/Gewinn-Tabelle. + * @param {{ networkWorth?: boolean, branchId?: number|null }} options — bei networkWorth: MAX(worth_percent) + * über alle Regionen der eigenen Niederlassungen (wie Daemon mit Fahrzeug / Filialnetz). + */ + async getAllProductPricesInRegion(hashedUserId, regionId, options = {}) { try { + const networkWorth = Boolean(options.networkWorth); + const branchId = options.branchId != null ? Number(options.branchId) : null; + const user = await this.getFalukantUserByHashedId(hashedUserId); const character = await FalukantCharacter.findOne({ where: { userId: user.id } }); if (!character) { throw new Error(`No FalukantCharacter found for user with id ${user.id}`); } + + let worthRegionIds = [regionId]; + if (networkWorth) { + if (!branchId || Number.isNaN(branchId)) { + throw new Error('branchId is required when networkWorth is set'); + } + const ownBranch = await Branch.findOne({ + where: { id: branchId, falukantUserId: user.id }, + attributes: ['id'] + }); + if (!ownBranch) { + throw new Error('Branch not found or not owned by user'); + } + const userBranches = await Branch.findAll({ + where: { falukantUserId: user.id }, + attributes: ['regionId'] + }); + const ids = [...new Set(userBranches.map((b) => b.regionId).filter((id) => id != null))]; + worthRegionIds = ids.length > 0 ? ids : [regionId]; + } + const [products, knowledges, townWorths] = await Promise.all([ ProductType.findAll({ attributes: ['id', 'sellCost'] }), Knowledge.findAll({ @@ -6807,17 +6846,22 @@ class FalukantService extends BaseService { attributes: ['productId', 'knowledge'] }), TownProductWorth.findAll({ - where: { regionId: regionId }, - attributes: ['productId', 'worthPercent'] + where: { regionId: { [Op.in]: worthRegionIds } }, + attributes: ['productId', 'regionId', 'worthPercent'] }) ]); const knowledgeMap = new Map(knowledges.map(k => [k.productId, k.knowledge || 0])); - const worthMap = new Map(townWorths.map(tw => [tw.productId, tw.worthPercent || 50])); + const maxWorthByProduct = new Map(); + for (const tw of townWorths) { + const w = tw.worthPercent ?? 50; + const prev = maxWorthByProduct.get(tw.productId); + maxWorthByProduct.set(tw.productId, prev == null ? w : Math.max(prev, w)); + } const prices = {}; for (const product of products) { - const worthPercent = worthMap.get(product.id) ?? 50; + const worthPercent = maxWorthByProduct.get(product.id) ?? 50; const knowledgeFactor = knowledgeMap.get(product.id) || 0; const price = calcRegionalSellPriceSync(product, knowledgeFactor, worthPercent); if (price !== null) prices[product.id] = price; @@ -6987,11 +7031,20 @@ ORDER BY r.id`, for (const product of products) { const knowledgeFactor = knowledgeByProduct.get(product.id) || 0; - const currentPrice = priceByProduct.get(product.id) ?? product.sellCost ?? 0; - let currentRegionalPrice = currentPrice; - if (currentRegionId) { + const clientPriceRaw = priceByProduct.get(product.id); + const clientPriceNum = clientPriceRaw != null && clientPriceRaw !== '' + ? Number(clientPriceRaw) + : NaN; + let currentRegionalPrice; + if (!Number.isNaN(clientPriceNum)) { + // Referenzpreis wie in der Ertrags-Tabelle (z. B. MAX-Worth über Filialen) + currentRegionalPrice = clientPriceNum; + } else if (currentRegionId) { const wp = worthByProductRegion.get(`${product.id}-${currentRegionId}`) ?? 50; - currentRegionalPrice = calcRegionalSellPriceSync(product, knowledgeFactor, wp) ?? currentPrice; + currentRegionalPrice = calcRegionalSellPriceSync(product, knowledgeFactor, wp) + ?? Number(product.sellCost) ?? 0; + } else { + currentRegionalPrice = Number(product.sellCost) || 0; } const results = []; diff --git a/docs/FALUKANT_PRODUCTION_CERTIFICATE.md b/docs/FALUKANT_PRODUCTION_CERTIFICATE.md index 8a1e0a9..c9513f7 100644 --- a/docs/FALUKANT_PRODUCTION_CERTIFICATE.md +++ b/docs/FALUKANT_PRODUCTION_CERTIFICATE.md @@ -33,16 +33,14 @@ Backend-Implementierung: `getCertificateCompletedProductionCount` in `backend/se Stattdessen: - Zähler-Reset über **`certificate_productions_count_since`** (siehe oben). -- Alte Zeilen werden mit **Retention 30 Tage** bereinigt, damit die Tabelle nicht unbegrenzt wächst. - -In diesem Repo: **`UserCharacterWorker`** führt stündlich `QUERY_DELETE_OLD_PRODUCTIONS` aus: +- Alte Zeilen mit **Retention 30 Tage** bereinigen (z. B. im **YpDaemon** mit `QUERY_DELETE_OLD_PRODUCTIONS`), damit die Tabelle nicht unbegrenzt wächst: ```sql DELETE FROM falukant_log.production WHERE COALESCE(production_timestamp, production_date::timestamp) < NOW() - INTERVAL '30 days'; ``` -Parallel löscht derselbe Worker weiterhin einzelne Zeilen nach dem Wissens-Update (`QUERY_DELETE_LOG_ENTRY`), sobald die Zeile „vom Vortag“ verarbeitet wurde (`QUERY_UPDATE_GET_ITEMS_TO_UPDATE` mit `COALESCE` für das Datumsfenster). +Die C++-Worker unter **`src/`** sind **[obsolet](LEGACY_CPP_WORKERS.md)** und werden für Retention oder Zertifikatslogik **nicht** mehr als Bezug genutzt. ## Migrationen diff --git a/docs/LEGACY_CPP_WORKERS.md b/docs/LEGACY_CPP_WORKERS.md new file mode 100644 index 0000000..380737c --- /dev/null +++ b/docs/LEGACY_CPP_WORKERS.md @@ -0,0 +1,9 @@ +# Legacy: C++-Worker (`src/`) + +Die C++-Prozesse unter **`src/`** (z. B. `*worker.cpp` / `websocket_server`) gelten als **obsolet**. + +- **Keine** neuen Features oder fachliche Korrekturen mehr in diesem Code pflegen. +- **Quelle der Wahrheit** für Hintergrundlogik (Zertifikat, Retention, Wirtschaft, …) ist der **externe Daemon** bzw. **Backend-Jobs**, nicht diese Worker. +- Beim Arbeiten an Falukant bitte **Backend** (`backend/`), **Frontend** und **Daemon-Repo** berücksichtigen; `src/` nur bei Bedarf für Build-/Entfernen-Fragen anfassen. + +Siehe auch: [`FALUKANT_PRODUCTION_CERTIFICATE.md`](./FALUKANT_PRODUCTION_CERTIFICATE.md) (Zählung und Log-Retention ohne Bezug auf die C++-Worker). diff --git a/frontend/scripts/ceb-patches/falukant-patch.json b/frontend/scripts/ceb-patches/falukant-patch.json index 817540a..6245161 100644 --- a/frontend/scripts/ceb-patches/falukant-patch.json +++ b/frontend/scripts/ceb-patches/falukant-patch.json @@ -568,6 +568,9 @@ "repelled": "Repelled", "partial_success": "Partial success", "major_success": "Major success" + }, + "attacks": { + "source": "Pikas nga bahin" } } } diff --git a/frontend/src/components/falukant/ProductionSection.vue b/frontend/src/components/falukant/ProductionSection.vue index e644fd4..f03cce6 100644 --- a/frontend/src/components/falukant/ProductionSection.vue +++ b/frontend/src/components/falukant/ProductionSection.vue @@ -76,6 +76,8 @@ branchId: { type: Number, required: true }, products: { type: Array, required: true }, currentCertificate: { type: Number, default: 1 }, + /** Gleiche regionalen Preise wie Ertrags-Tabelle (BranchView / prices-in-region, optional MAX-Worth). */ + productPricesCache: { type: Object, default: () => ({}) }, }, data() { return { @@ -194,10 +196,15 @@ if (!product.knowledges || product.knowledges.length === 0) { return { absolute: 0, perMinute: 0 }; } - const knowledgeFactor = product.knowledges[0].knowledge || 0; - const maxPrice = product.sellCost; - const minPrice = maxPrice * 0.6; - const revenuePerUnit = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100); + let revenuePerUnit; + if (this.productPricesCache[product.id] !== undefined) { + revenuePerUnit = this.productPricesCache[product.id]; + } else { + const knowledgeFactor = product.knowledges[0].knowledge || 0; + const maxPrice = product.sellCost; + const minPrice = maxPrice * 0.7; // KNOWLEDGE_PRICE_FLOOR wie backend/falukantProductEconomy + revenuePerUnit = minPrice + (maxPrice - minPrice) * (knowledgeFactor / 100); + } const perMinute = product.productionTime > 0 ? revenuePerUnit / product.productionTime : 0; return { absolute: revenuePerUnit.toFixed(2), diff --git a/frontend/src/components/falukant/RevenueSection.vue b/frontend/src/components/falukant/RevenueSection.vue index 8d49835..8fc4521 100644 --- a/frontend/src/components/falukant/RevenueSection.vue +++ b/frontend/src/components/falukant/RevenueSection.vue @@ -20,7 +20,11 @@ - + {{ $t(`falukant.product.${product.labelTr}`) }} {{ product.knowledges && product.knowledges[0] ? product.knowledges[0].knowledge : 0 }} {{ calculateProductRevenue(product).absolute }} @@ -72,13 +76,19 @@ }; }, computed: { - productWithMaxProfitPerMinute() { - if (!this.products || this.products.length === 0) return null; - return this.products.reduce((maxProduct, currentProduct) => { - const currentProfit = parseFloat(this.calculateProductProfit(currentProduct).perMinute); - const maxProfit = maxProduct ? parseFloat(this.calculateProductProfit(maxProduct).perMinute) : Number.NEGATIVE_INFINITY; - return currentProfit > maxProfit ? currentProduct : maxProduct; - }, null); + /** Gleiche Metrik wie Daemon: Gewinn/Minute ≈ (Erlös/Stück − Stückkosten) / Produktionszeit; höchste zuerst. */ + sortedProductsByProfitPerMinute() { + if (!this.products || this.products.length === 0) return []; + return [...this.products].sort((a, b) => { + const pb = parseFloat(this.calculateProductProfit(b).perMinute); + const pa = parseFloat(this.calculateProductProfit(a).perMinute); + if (pb !== pa) return pb - pa; + return (a.labelTr || '').localeCompare(b.labelTr || ''); + }); + }, + topProductByProfitPerMinute() { + const list = this.sortedProductsByProfitPerMinute; + return list.length ? list[0] : null; }, }, async mounted() { diff --git a/frontend/src/i18n/locales/ceb/falukant.json b/frontend/src/i18n/locales/ceb/falukant.json index f75adb9..ded0a71 100644 --- a/frontend/src/i18n/locales/ceb/falukant.json +++ b/frontend/src/i18n/locales/ceb/falukant.json @@ -1190,6 +1190,7 @@ "loot": "Bilin" }, "attacks": { + "source": "Pikas nga bahin", "target": "Tig-atake", "date": "Petsa", "success": "Kalampusan", diff --git a/frontend/src/i18n/locales/de/falukant.json b/frontend/src/i18n/locales/de/falukant.json index e59011d..d9d3104 100644 --- a/frontend/src/i18n/locales/de/falukant.json +++ b/frontend/src/i18n/locales/de/falukant.json @@ -1574,6 +1574,7 @@ "loot": "Beute" }, "attacks": { + "source": "Gegenüber", "target": "Angreifer", "date": "Datum", "success": "Erfolg", diff --git a/frontend/src/i18n/locales/en/falukant.json b/frontend/src/i18n/locales/en/falukant.json index 816bb09..3128235 100644 --- a/frontend/src/i18n/locales/en/falukant.json +++ b/frontend/src/i18n/locales/en/falukant.json @@ -1191,6 +1191,7 @@ "loot": "Loot" }, "attacks": { + "source": "Other party", "target": "Attacker", "date": "Date", "success": "Success", diff --git a/frontend/src/i18n/locales/es/falukant.json b/frontend/src/i18n/locales/es/falukant.json index 901bcb4..11ea422 100644 --- a/frontend/src/i18n/locales/es/falukant.json +++ b/frontend/src/i18n/locales/es/falukant.json @@ -1574,6 +1574,7 @@ "loot": "Botín" }, "attacks": { + "source": "Contraparte", "target": "Atacante", "date": "Fecha", "success": "Éxito", diff --git a/frontend/src/i18n/locales/fr/falukant.json b/frontend/src/i18n/locales/fr/falukant.json index 5c94db2..2697547 100644 --- a/frontend/src/i18n/locales/fr/falukant.json +++ b/frontend/src/i18n/locales/fr/falukant.json @@ -1572,6 +1572,7 @@ "loot": "proie" }, "attacks": { + "source": "Autre partie", "target": "attaquant", "date": "Date", "success": "Succès", diff --git a/frontend/src/views/falukant/BranchView.vue b/frontend/src/views/falukant/BranchView.vue index e27c24f..27740b5 100644 --- a/frontend/src/views/falukant/BranchView.vue +++ b/frontend/src/views/falukant/BranchView.vue @@ -107,6 +107,7 @@ :branchId="selectedBranch.id" :products="products" :current-certificate="currentCertificate" + :product-prices-cache="productPricesCache" ref="productionSection" /> @@ -435,7 +436,8 @@ export default { vehicles: [], activeTab: 'production', productPricesCache: {}, // Cache für regionale Preise: { productId: price } - productPricesCacheRegionId: null, // regionId, für die der Cache gültig ist + /** Cache-Schlüssel: Region + ob MAX(worth) über alle Filialregionen (bei Fahrzeug) */ + productPricesCacheKey: null, tabs: [ { value: 'production', label: 'falukant.branch.tabs.production' }, { value: 'inventory', label: 'falukant.branch.tabs.inventory' }, @@ -625,6 +627,11 @@ export default { this.$refs.statusBar?.fetchStatus(); await this.loadCurrentCertificate(); await this.loadProducts(); + if (this.selectedBranch) { + await this.loadVehicles(); + this.productPricesCacheKey = null; + await this.loadProductPricesForCurrentBranch(); + } this.$refs.productionSection?.loadProductions(); this.$refs.productionSection?.loadStorage(); this.$refs.storageSection?.loadStorageData(); @@ -732,20 +739,23 @@ export default { async loadProductPricesForCurrentBranch() { if (!this.selectedBranch || !this.selectedBranch.regionId) { this.productPricesCache = {}; - this.productPricesCacheRegionId = null; + this.productPricesCacheKey = null; return; } - if (this.productPricesCacheRegionId === this.selectedBranch.regionId && Object.keys(this.productPricesCache).length > 0) { + const useNetworkWorth = Array.isArray(this.vehicles) && this.vehicles.length > 0; + const cacheKey = `${this.selectedBranch.regionId}:${useNetworkWorth ? 'net' : 'loc'}:${this.selectedBranch.id}`; + if (this.productPricesCacheKey === cacheKey && Object.keys(this.productPricesCache).length > 0) { return; } try { - const { data } = await apiClient.get('/api/falukant/products/prices-in-region', { - params: { - regionId: this.selectedBranch.regionId - } - }); + const params = { regionId: this.selectedBranch.regionId }; + if (useNetworkWorth) { + params.networkWorth = '1'; + params.branchId = this.selectedBranch.id; + } + const { data } = await apiClient.get('/api/falukant/products/prices-in-region', { params }); this.productPricesCache = data.prices || {}; - this.productPricesCacheRegionId = this.selectedBranch.regionId; + this.productPricesCacheKey = cacheKey; } catch (error) { console.error(`Error loading product prices for region ${this.selectedBranch.regionId}:`, error); // Fallback: Lade Preise einzeln (alte Methode) @@ -770,7 +780,7 @@ export default { } } this.productPricesCache = prices; - this.productPricesCacheRegionId = this.selectedBranch?.regionId ?? null; + this.productPricesCacheKey = cacheKey; } }, diff --git a/frontend/src/views/falukant/NobilityView.vue b/frontend/src/views/falukant/NobilityView.vue index 35dc8ae..e39a24b 100644 --- a/frontend/src/views/falukant/NobilityView.vue +++ b/frontend/src/views/falukant/NobilityView.vue @@ -31,8 +31,24 @@ {{ $t('falukant.nobility.nextTitle') }}: {{ $t(`falukant.titles.${gender}.${next.labelTr}`) }}

+ @@ -74,7 +90,9 @@ highestPoliticalOffice: null, highestOfficeAny: null, nextAdvanceAt: null, - isAdvancing: false + isAdvancing: false, + /** Nach fehlgeschlagenem POST /nobility: vom Server gelieferte unmet [{ type, required }] */ + advanceUnmetList: null, }; }, computed: { @@ -124,6 +142,7 @@ this.highestPoliticalOffice = data.highestPoliticalOffice || null; this.highestOfficeAny = data.highestOfficeAny || null; this.nextAdvanceAt = data.nextAdvanceAt || null; + this.advanceUnmetList = null; } catch (err) { console.error('Error loading nobility:', err); } @@ -131,6 +150,7 @@ async applyAdvance() { if (!this.canAdvance || this.isAdvancing) return; this.isAdvancing = true; + this.advanceUnmetList = null; try { await apiClient.post('/api/falukant/nobility'); await this.loadNobility(); @@ -144,10 +164,7 @@ const msg = this.$t('falukant.nobility.errors.tooSoon'); this.$root.$refs.errorDialog?.open(retryStr ? `${msg} — ${this.$t('falukant.nobility.cooldown', { date: retryStr })}` : msg); } else if (resp.data?.message === 'nobilityRequirements') { - const unmet = resp.data?.unmet || []; - const items = unmet.map(u => this.formatRequirement(u.type, u.required)).join('\n'); - const base = this.$t('falukant.nobility.errors.unmet'); - this.$root.$refs.errorDialog?.open(`${base}\n${items}`); + this.advanceUnmetList = Array.isArray(resp.data?.unmet) ? resp.data.unmet : []; } else { this.$root.$refs.errorDialog?.open(this.$t('falukant.nobility.errors.generic')); } @@ -186,11 +203,18 @@ } return this.$t('falukant.nobility.requirement.unknown', { type, amount }); }, + isRequirementUnmet(req) { + if (!this.advanceUnmetList?.length || !req) return false; + return this.advanceUnmetList.some( + (u) => u.type === req.requirementType + && String(u.required) === String(req.requirementValue) + ); + }, formatOfficeInfo(info, source) { if (!info?.name) { return this.$t('falukant.nobility.none'); } - const baseKey = source === 'church' ? 'falukant.church.offices' : 'falukant.politics.positions'; + const baseKey = source === 'church' ? 'falukant.church.offices' : 'falukant.politics.offices'; const label = this.$te(`${baseKey}.${info.name}`) ? this.$t(`${baseKey}.${info.name}`) : info.name; return this.$t('falukant.nobility.officeWithRank', { label, rank: info.rank }); }, @@ -248,6 +272,25 @@ list-style: disc inside; margin-bottom: 1rem; } + .prerequisites li.is-unmet { + color: #a32020; + font-weight: 600; + } + .nobility-unmet-banner { + border: 1px solid #d4a0a0; + background: rgba(180, 40, 40, 0.08); + border-radius: 6px; + padding: 12px 14px; + margin-bottom: 1rem; + } + .nobility-unmet-banner__title { + margin: 0 0 8px; + font-weight: 600; + } + .nobility-unmet-banner__list { + margin: 0; + padding-left: 1.25rem; + } .cooldown-message { color: #666; font-style: italic; diff --git a/src/usercharacterworker.cpp b/src/usercharacterworker.cpp index 0a567f5..8cd0a9e 100644 --- a/src/usercharacterworker.cpp +++ b/src/usercharacterworker.cpp @@ -30,7 +30,6 @@ void UserCharacterWorker::run() { processCharacterEvents(); updateCharactersMood(); handleCredits(); - deleteOldProductionLogs(); } catch (const std::exception &e) { std::cerr << "[UserCharacterWorker] Fehler in processCharacterEvents: " << e.what() << std::endl; } @@ -234,17 +233,6 @@ void UserCharacterWorker::setNewMoney(int falukantUserId, double newAmount) { }); } -void UserCharacterWorker::deleteOldProductionLogs() { - try { - ConnectionGuard connGuard(pool); - auto &db = connGuard.get(); - db.prepare("QUERY_DELETE_OLD_PRODUCTIONS", QUERY_DELETE_OLD_PRODUCTIONS); - db.execute("QUERY_DELETE_OLD_PRODUCTIONS"); - } catch (const std::exception &e) { - std::cerr << "[UserCharacterWorker] Fehler in deleteOldProductionLogs: " << e.what() << std::endl; - } -} - void UserCharacterWorker::recalculateKnowledge() { setCurrentStep("Get character data"); ConnectionGuard connGuard(pool); diff --git a/src/usercharacterworker.h b/src/usercharacterworker.h index 2c0a45b..eba378b 100644 --- a/src/usercharacterworker.h +++ b/src/usercharacterworker.h @@ -25,7 +25,6 @@ private: int calculateHealthChange(int age); void handleCharacterDeath(int characterId); void recalculateKnowledge(); - void deleteOldProductionLogs(); void processPregnancies(); void handleCredits(); void setHeir(int characterId); @@ -62,13 +61,7 @@ private: static constexpr const char *QUERY_UPDATE_GET_ITEMS_TO_UPDATE = R"( SELECT id, product_id, producer_id, quantity FROM falukant_log.production p - WHERE (COALESCE(p.production_timestamp, p.production_date::timestamp))::date < CURRENT_DATE - )"; - - /** Log-Retention: ältere Zeilen entfernen (Daemon/UI-Zertifikatszählung braucht Historie nur begrenzt). */ - static constexpr const char *QUERY_DELETE_OLD_PRODUCTIONS = R"( - DELETE FROM falukant_log.production - WHERE COALESCE(production_timestamp, production_date::timestamp) < NOW() - INTERVAL '30 days' + WHERE p.production_timestamp::date < current_date )"; static constexpr const char *QUERY_UPDATE_GET_CHARACTER_IDS = R"( diff --git a/src/valuerecalculationworker.cpp b/src/valuerecalculationworker.cpp index 982eb8c..e807225 100644 --- a/src/valuerecalculationworker.cpp +++ b/src/valuerecalculationworker.cpp @@ -72,9 +72,8 @@ void ValueRecalculationWorker::calculateProductKnowledge() { sendMessageToFalukantUsers(userId, message); } /* Kein DELETE mehr auf falukant_log.production um Mitternacht: Die Einträge werden für - * Zertifikatsfortschritt (Backend + Daemon) benötigt. - * UserCharacterWorker: nach Wissens-Update pro Zeile löschen; zusätzlich stündlich Retention - * QUERY_DELETE_OLD_PRODUCTIONS (30 Tage, COALESCE(timestamp, production_date)). */ + * Zertifikatsfortschritt (u. a. Backend-Aggregation) benötigt und dürfen nicht täglich verworfen werden. + * Alte Zeilen räumt der UserCharacterWorker nach Wissens-Update weiterhin gezielt auf (pro Zeile). */ } void ValueRecalculationWorker::calculateRegionalSellPrice() { diff --git a/src/valuerecalculationworker.h b/src/valuerecalculationworker.h index d76973a..35a1b50 100644 --- a/src/valuerecalculationworker.h +++ b/src/valuerecalculationworker.h @@ -42,7 +42,7 @@ private: SET knowledge = LEAST(100, k.knowledge + 1) FROM falukant_data."character" c JOIN falukant_log.production p - ON DATE(COALESCE(p.production_timestamp, p.production_date::timestamp)) = CURRENT_DATE - INTERVAL '1 day' + ON DATE(p.production_timestamp) = CURRENT_DATE - INTERVAL '1 day' WHERE c.id = k.character_id AND c.user_id = 18 AND k.product_id = 10 @@ -51,7 +51,7 @@ private: static constexpr const char *QUERY_GET_PRODUCERS_LAST_DAY = R"( select p."producer_id" from falukant_log.production p - where date(COALESCE(p.production_timestamp, p.production_date::timestamp)) = CURRENT_DATE - interval '1 day' + where date(p."production_timestamp") = CURRENT_DATE - interval '1 day' group by producer_id )";