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}`) }}
+
+
{{ $t('falukant.nobility.errors.unmet') }}
+
+ -
+ {{ formatRequirement(u.type, u.required) }}
+
+
+
- -
+
-
{{ formatRequirement(req.requirementType, req.requirementValue) }}
@@ -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
)";