diff --git a/arma/client/addons/actor/functions/fnc_initRepository.sqf b/arma/client/addons/actor/functions/fnc_initRepository.sqf
index 71cdd8a..12a2b4d 100644
--- a/arma/client/addons/actor/functions/fnc_initRepository.sqf
+++ b/arma/client/addons/actor/functions/fnc_initRepository.sqf
@@ -33,7 +33,7 @@ GVAR(ActorRepositoryBaseClass) = compileFinal createHashMapFromArray [
[SRPC(actor,requestInitActor), [_uid]] call CFUNC(serverEvent);
_self set ["lastSave", time];
- systemChat format ["Actor loaded for %1", name player];
+ systemChat format ["Loading actor for %1", name player];
diag_log "[FORGE:Client:Actor] Actor Repository Initialized!";
}],
["save", compileFinal {
diff --git a/arma/client/addons/garage/XEH_postInitClient.sqf b/arma/client/addons/garage/XEH_postInitClient.sqf
index 76cd623..e187c2d 100644
--- a/arma/client/addons/garage/XEH_postInitClient.sqf
+++ b/arma/client/addons/garage/XEH_postInitClient.sqf
@@ -55,7 +55,7 @@ if (isNil QGVAR(VGRepository)) then { call FUNC(initVGRepository); };
}] call CFUNC(addEventHandler);
[{
- EGVAR(bank,BankRepository) get "isLoaded";
+ EGVAR(actor,ActorRepository) get "isLoaded";
}, {
[QGVAR(initGarage), []] call CFUNC(localEvent);
}] call CFUNC(waitUntilAndExecute);
diff --git a/arma/client/addons/locker/XEH_postInitClient.sqf b/arma/client/addons/locker/XEH_postInitClient.sqf
index 20123fa..a0a878d 100644
--- a/arma/client/addons/locker/XEH_postInitClient.sqf
+++ b/arma/client/addons/locker/XEH_postInitClient.sqf
@@ -36,7 +36,7 @@ if (isNil QGVAR(VARepository)) then { call FUNC(initVARepository); };
}] call CFUNC(addEventHandler);
[{
- EGVAR(garage,GarageRepository) get "isLoaded";
+ EGVAR(actor,ActorRepository) get "isLoaded";
}, {
[QGVAR(initLocker), []] call CFUNC(localEvent);
}] call CFUNC(waitUntilAndExecute);
diff --git a/arma/client/addons/org/XEH_postInitClient.sqf b/arma/client/addons/org/XEH_postInitClient.sqf
index 5ddcfc0..081d03a 100644
--- a/arma/client/addons/org/XEH_postInitClient.sqf
+++ b/arma/client/addons/org/XEH_postInitClient.sqf
@@ -50,8 +50,20 @@ if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initUIBridge); };
GVAR(OrgUIBridge) call ["handleCreditLineResponse", [_payload]];
}] call CFUNC(addEventHandler);
+[QGVAR(responseInviteOrg), {
+ params [["_payload", createHashMap, [createHashMap]]];
+
+ GVAR(OrgUIBridge) call ["handleInviteResponse", [_payload]];
+}] call CFUNC(addEventHandler);
+
+[QGVAR(responseInviteDecision), {
+ params [["_payload", createHashMap, [createHashMap]]];
+
+ GVAR(OrgUIBridge) call ["handleInviteDecisionResponse", [_payload]];
+}] call CFUNC(addEventHandler);
+
[{
- EGVAR(locker,VARepository) get "isLoaded";
+ EGVAR(actor,ActorRepository) get "isLoaded";
}, {
[QGVAR(initOrg), []] call CFUNC(localEvent);
}] call CFUNC(waitUntilAndExecute);
diff --git a/arma/client/addons/org/functions/fnc_handleUIEvents.sqf b/arma/client/addons/org/functions/fnc_handleUIEvents.sqf
index 6563ac6..16746de 100644
--- a/arma/client/addons/org/functions/fnc_handleUIEvents.sqf
+++ b/arma/client/addons/org/functions/fnc_handleUIEvents.sqf
@@ -46,6 +46,15 @@ switch (_event) do {
case "org::credit::request": {
GVAR(OrgUIBridge) call ["requestCreditLine", [_data]];
};
+ case "org::invite::request": {
+ GVAR(OrgUIBridge) call ["requestInvite", [_data]];
+ };
+ case "org::invite::accept": {
+ GVAR(OrgUIBridge) call ["requestAcceptInvite", [_data]];
+ };
+ case "org::invite::decline": {
+ GVAR(OrgUIBridge) call ["requestDeclineInvite", [_data]];
+ };
case "org::ready": {
GVAR(OrgUIBridge) call ["handleReady", [_control]];
};
diff --git a/arma/client/addons/org/functions/fnc_initUIBridge.sqf b/arma/client/addons/org/functions/fnc_initUIBridge.sqf
index a5593df..dc4f455 100644
--- a/arma/client/addons/org/functions/fnc_initUIBridge.sqf
+++ b/arma/client/addons/org/functions/fnc_initUIBridge.sqf
@@ -161,6 +161,26 @@ GVAR(OrgUIBridgeBaseClass) = compileFinal createHashMapFromArray [
};
};
}],
+ ["handleInviteResponse", compileFinal {
+ params [["_payload", createHashMap, [createHashMap]]];
+
+ private _eventName = [
+ "org::invite::failure",
+ "org::invite::success"
+ ] select (_payload getOrDefault ["success", false]);
+
+ _self call ["sendEvent", [_eventName, _payload]];
+ }],
+ ["handleInviteDecisionResponse", compileFinal {
+ params [["_payload", createHashMap, [createHashMap]]];
+
+ private _eventName = [
+ "org::invite::decision::failure",
+ "org::invite::decision::success"
+ ] select (_payload getOrDefault ["success", false]);
+
+ _self call ["sendEvent", [_eventName, _payload]];
+ }],
["requestDisband", compileFinal {
[SRPC(org,requestDisbandOrg), [getPlayerUID player]] call CFUNC(serverEvent);
}],
@@ -176,6 +196,25 @@ GVAR(OrgUIBridgeBaseClass) = compileFinal createHashMapFromArray [
[SRPC(org,requestAssignCreditLine), [getPlayerUID player, _memberUid, _memberName, _amount]] call CFUNC(serverEvent);
}],
+ ["requestInvite", compileFinal {
+ params [["_data", createHashMap, [createHashMap]]];
+
+ private _targetUid = _data getOrDefault ["targetUid", ""];
+ private _targetName = _data getOrDefault ["targetName", ""];
+ [SRPC(org,requestInviteOrgMember), [getPlayerUID player, _targetUid, _targetName]] call CFUNC(serverEvent);
+ }],
+ ["requestAcceptInvite", compileFinal {
+ params [["_data", createHashMap, [createHashMap]]];
+
+ private _orgID = _data getOrDefault ["orgId", ""];
+ [SRPC(org,requestAcceptOrgInvite), [getPlayerUID player, _orgID]] call CFUNC(serverEvent);
+ }],
+ ["requestDeclineInvite", compileFinal {
+ params [["_data", createHashMap, [createHashMap]]];
+
+ private _orgID = _data getOrDefault ["orgId", ""];
+ [SRPC(org,requestDeclineOrgInvite), [getPlayerUID player, _orgID]] call CFUNC(serverEvent);
+ }],
["refreshPortal", compileFinal {
_self call ["requestHydrate", ["org::sync"]]
}]
diff --git a/arma/client/addons/org/ui/_site/org-ui.js b/arma/client/addons/org/ui/_site/org-ui.js
index 38b3707..36e4ca3 100644
--- a/arma/client/addons/org/ui/_site/org-ui.js
+++ b/arma/client/addons/org/ui/_site/org-ui.js
@@ -1 +1 @@
-!function(){const e=window.ForgeWebUI,n=window.RegistryApp=window.RegistryApp||{},r=window.OrgPortal=window.OrgPortal||{};n.runtime=e,r.runtime=e,window.AppRuntime=e}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{createSignal:n}=e.runtime;e.store=new class{constructor(){[this.getView,this.setView]=n("home"),[this.getIsAuthenticating,this.setIsAuthenticating]=n(!1),[this.getLoginError,this.setLoginError]=n(""),[this.getIsCreating,this.setIsCreating]=n(!1),[this.getCreateError,this.setCreateError]=n("")}startLogin(){this.setLoginError(""),this.setIsAuthenticating(!0)}startCreate(){this.setCreateError(""),this.setIsCreating(!0)}failLogin(e){this.setIsAuthenticating(!1),this.setLoginError(e||"Authentication failed.")}failCreate(e){this.setIsCreating(!1),this.setCreateError(e||"Organization registration failed.")}hydratePortal(e){const n=window.OrgPortal&&window.OrgPortal.data?window.OrgPortal.data:null,r=window.OrgPortal&&window.OrgPortal.store?window.OrgPortal.store:null,t=e&&e.portalData?e.portalData:null,a=e&&e.session?e.session:null;return!!(n&&"function"==typeof n.applyLoginPayload&&r&&"function"==typeof r.hydrateFromPayload&&t&&a)&&(n.applyLoginPayload(e),r.hydrateFromPayload(e),!0)}completeLogin(e){this.hydratePortal(e)?(this.setLoginError(""),this.setIsAuthenticating(!1),this.setView("portal")):this.failLogin("Login response was missing portal data.")}completeCreate(e){this.hydratePortal(e)?(this.setCreateError(""),this.setIsCreating(!1),this.setView("portal")):this.failCreate("Organization registration response was missing portal data.")}}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},n=e.store,r=window.ForgeWebUI.createBridge({closeEvent:"org::close",globalName:"ForgeBridge",readyEvent:"org::ready"});function t(e,n){return r.send(e,n)}r.on("org::login::success",e=>{n.completeLogin(e)}),r.on("org::login::failure",e=>{n.failLogin(e.message||"Authentication failed.")}),r.on("org::create::success",e=>{n.completeCreate(e)}),r.on("org::create::failure",e=>{n.failCreate(e.message||"Organization registration failed.")}),r.on("org::sync",e=>{n&&"function"==typeof n.hydratePortal&&n.hydratePortal(e)}),r.on("org::credit::success",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("success",e.message||"Credit line assigned.")}),r.on("org::credit::failure",e=>{const n=window.OrgPortal;n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Unable to assign credit line.")}),r.on("org::member::creditUpdated",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setCreditLines(n=>{const r={amount:e.availableAmount||e.amount||0,amountDue:e.amountDue||0,approvedAmount:e.approvedAmount||e.availableAmount||e.amount||0,availableAmount:e.availableAmount||e.amount||0,interestRate:e.interestRate||.1,member:e.memberName||"",outstandingPrincipal:e.outstandingPrincipal||0,uid:e.memberUid||""},t=n.findIndex(e=>e.uid===r.uid);return-1===t?[...n,r]:n.map((e,n)=>n===t?r:e)})}),r.on("org::disband::success",()=>{const e=window.OrgPortal;e&&e.store&&(e.store.setModal(null),e.store.setOrgDisbanded(!0))}),r.on("org::disband::failure",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Organization disbanding failed.")}),r.on("org::leave::success",e=>{const r=window.OrgPortal;r&&r.store&&r.store.setModal(null),n.failLogin(e.message||"You have left the organization."),n.setView("home")}),r.on("org::leave::failure",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Unable to leave the organization.")}),r.on("org::portal::revoked",e=>{const r=window.OrgPortal;r&&r.store&&r.store.setModal(null),n.failLogin(e.message||"Organization access is no longer available."),n.setView("home")}),e.bridge={close:r.close,ready:r.ready,receive:r.receive,requestLogin:function(e){n.startLogin(),t("org::login::request",e)||n.failLogin("Arma login bridge is unavailable.")},requestCreateOrg:function(e){n.startCreate(),t("org::create::request",e)||n.failCreate("Arma registration bridge is unavailable.")},requestDisbandOrg:function(){if(t("org::disband::request",{}))return;const e=window.OrgPortal;e&&e.actions&&e.actions.showTreasuryNotice("error","Arma disband bridge is unavailable.")},requestLeaveOrg:function(){if(t("org::leave::request",{}))return;const e=window.OrgPortal;e&&e.actions&&e.actions.showTreasuryNotice("error","Arma leave bridge is unavailable.")},requestCreditLine:function(e){if(t("org::credit::request",e))return!0;const n=window.OrgPortal;return n&&n.actions&&n.actions.showTreasuryNotice("error","Arma credit line bridge is unavailable."),!1},sendEvent:t}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},n={type:"Organization",status:"Operational",headquarters:"ArmA Verse"};function r(e){return JSON.parse(JSON.stringify(e))}function t(e,n){Object.keys(e).forEach(n=>delete e[n]),Object.assign(e,r(n))}function a(e,n){e.splice(0,e.length,...r(n))}function o(e){if(e&&"object"==typeof e&&!Array.isArray(e))return e;if(Array.isArray(e)){if(e.every(e=>Array.isArray(e)&&e.length>=2&&"string"==typeof e[0]))return Object.fromEntries(e)}if("string"==typeof e&&""!==e.trim())try{return o(JSON.parse(e))}catch(n){return e}return e}function i(e){return(Array.isArray(e)?e:e&&"object"==typeof e?Object.values(e):[]).map(o).filter(Boolean)}e.data={portalData:{org:Object.assign({name:"",tag:"",owner:"",ownerUid:"",isDefault:!1},n),funds:0,reputation:0,creditLines:[],members:[],fleet:[],assets:[],activity:[],roadmap:[{name:"Contracts Board",status:"Planned",detail:"Track payouts, assignments, and claim approvals."},{name:"Diplomacy",status:"Future Review",detail:"Possible future module pending a full design and scope review."},{name:"Logistics Queue",status:"Future Review",detail:"Possible future module pending a full design and scope review."},{name:"Permissions",status:"Future Review",detail:"Possible future module pending a full design and scope review."}]},session:{actorName:"",actorUid:"",role:"",ceo:!1},applyLoginPayload(e){t(this.portalData.org,Object.assign({},e.portalData.org||{},n)),this.portalData.funds=e.portalData.funds||0,this.portalData.reputation=e.portalData.reputation||0,a(this.portalData.creditLines,i(e.portalData.creditLines)),a(this.portalData.members,i(e.portalData.members)),a(this.portalData.fleet,i(e.portalData.fleet)),a(this.portalData.assets,i(e.portalData.assets)),a(this.portalData.activity,i(e.portalData.activity)),a(this.portalData.roadmap,i(e.portalData.roadmap)),t(this.session,e.session||{})}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{createSignal:n}=window.RegistryApp.runtime,{portalData:r}=e.data;function t(e){if(e&&"object"==typeof e&&!Array.isArray(e))return e;if(Array.isArray(e)){if(e.every(e=>Array.isArray(e)&&e.length>=2&&"string"==typeof e[0]))return Object.fromEntries(e)}if("string"==typeof e&&""!==e.trim())try{return t(JSON.parse(e))}catch(n){return e}return e}function a(e){return(Array.isArray(e)?e:e&&"object"==typeof e?Object.values(e):[]).map(t).filter(Boolean)}e.store=new class{constructor(){[this.getFunds,this.setFunds]=n(r.funds),[this.getReputation,this.setReputation]=n(r.reputation),[this.getMembers,this.setMembers]=n([...r.members]),[this.getCreditLines,this.setCreditLines]=n([...r.creditLines]),[this.getFleet,this.setFleet]=n([...r.fleet]),[this.getAssets,this.setAssets]=n([...r.assets]),[this.getActivity,this.setActivity]=n([...r.activity]),[this.getTreasuryNotice,this.setTreasuryNotice]=n({type:"",text:""}),[this.getModal,this.setModal]=n(null),[this.getOrgDisbanded,this.setOrgDisbanded]=n(!1)}hydrateFromPayload(e){const n=e.portalData||{};this.setFunds(n.funds||0),this.setReputation(n.reputation||0),this.setMembers([...a(n.members)]),this.setCreditLines([...a(n.creditLines)]),this.setFleet([...a(n.fleet)]),this.setAssets([...a(n.assets)]),this.setActivity([...a(n.activity)])}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{portalData:n,session:r}=e.data;e.getters=new class{formatCurrency(e){return"$"+Number(e||0).toLocaleString()}formatVehicleType(e){return e?e.charAt(0).toUpperCase()+e.slice(1):""}formatAssetType(e){return e?e.charAt(0).toUpperCase()+e.slice(1):""}formatDisplayName(e){return e?String(e).trim().split(/\s+/).map(e=>e?e.charAt(0).toUpperCase()+e.slice(1).toLowerCase():"").join(" "):""}getAssetReadiness(){const r=e.store?e.store.getFleet():n.fleet;if(0===r.length)return null;const t=r.reduce((e,n)=>e+(100-parseInt(n.damage,10)),0);return Math.round(t/r.length)}getNormalizedRole(){return String(r.role||"").trim().toUpperCase()}isDefaultOrg(){return!0===n.org.isDefault||"DEFAULT"===String(n.org.tag||"").trim().toUpperCase()}isOrgOwner(){const e=String(n.org.ownerUid||n.org.owner||"").trim().toLowerCase(),t=String(r.actorUid||"").trim().toLowerCase();return e&&t?t===e:String(r.actorName||"").trim().toLowerCase()===String(n.org.owner||"").trim().toLowerCase()}isSessionCeo(){return!0===r.ceo}isOrgLeaderOrCeo(){return this.isOrgOwner()||"LEADER"===this.getNormalizedRole()||this.isDefaultOrg()&&this.isSessionCeo()}canManageMembers(){return this.isOrgLeaderOrCeo()}canManageTreasury(){return this.isOrgLeaderOrCeo()}canDisbandOrg(){return this.isOrgOwner()&&!this.isDefaultOrg()}canLeaveOrg(){return!this.isDefaultOrg()&&!this.isOrgOwner()}getMemberName(e){return String(e&&"object"==typeof e?e.name||"":e||"")}getMemberUid(e){return e&&"object"==typeof e?String(e.uid||""):""}isOwnerMember(e){return this.getMemberName(e).trim().toLowerCase()===String(n.org.owner||"").trim().toLowerCase()}isCurrentMember(e){const n=this.getMemberUid(e).trim().toLowerCase(),t=String(r.actorUid||"").trim().toLowerCase();return n&&t?n===t:this.getMemberName(e).trim().toLowerCase()===String(r.actorName||"").trim().toLowerCase()}isProtectedMember(e){return this.isOwnerMember(e)||this.isCurrentMember(e)}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{portalData:n}=e.data,r=e.store,t=e.getters,a=window.RegistryApp.store;e.actions=new class{constructor(){this.treasuryNoticeTimer=null}showTreasuryNotice(e,n){r.setTreasuryNotice({type:e,text:n}),this.treasuryNoticeTimer&&clearTimeout(this.treasuryNoticeTimer),this.treasuryNoticeTimer=setTimeout(()=>{r.setTreasuryNotice({type:"",text:""}),this.treasuryNoticeTimer=null},3500)}parseAmount(e){const n=Number(e);return Number.isFinite(n)?Math.round(n):0}getInputValue(e){const n=document.getElementById(e);return n?n.value:""}closePortal(){const e=window.RegistryApp?window.RegistryApp.bridge:null;e&&"function"==typeof e.close?e.close({}):a&&a.setView("home")}openModal(e){"payroll"!==e&&"transfer"!==e&&"credit"!==e||t.canManageTreasury()?("disband"!==e||t.canDisbandOrg())&&("leave"!==e||t.canLeaveOrg())&&r.setModal({type:e}):this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions.")}closeModal(){r.setModal(null)}removeMember(e){if(!t.canManageMembers())return!1;if(t.isProtectedMember(e))return!1;const n=t.getMemberUid(e),a=t.getMemberName(e);return r.setMembers(e=>e.filter(e=>n?e.uid!==n:e.name!==a)),r.setCreditLines(e=>e.filter(e=>n?e.uid!==n:e.member!==a)),!0}disbandOrganization(){if(!t.canDisbandOrg())return!1;const e=window.RegistryApp?window.RegistryApp.bridge:null;return e&&"function"==typeof e.requestDisbandOrg?(this.closeModal(),e.requestDisbandOrg(),!0):(this.showTreasuryNotice("error","Disband bridge is unavailable."),!1)}leaveOrganization(){if(!t.canLeaveOrg())return!1;const e=window.RegistryApp?window.RegistryApp.bridge:null;return e&&"function"==typeof e.requestLeaveOrg?(this.closeModal(),e.requestLeaveOrg(),!0):(this.showTreasuryNotice("error","Leave bridge is unavailable."),!1)}runPayroll(e){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;const n=r.getMembers(),a=r.getFunds();if(0===n.length)return this.showTreasuryNotice("error","No members available for payroll."),!1;if(e<=0)return this.showTreasuryNotice("error","Enter a valid payroll amount."),!1;const o=e*n.length;return o>a?(this.showTreasuryNotice("error","Insufficient org funds for payroll."),!1):(r.setFunds(a-o),this.showTreasuryNotice("success",`Payroll sent to ${n.length} members for ${t.formatCurrency(o)}.`),!0)}sendFundsToMember(e,n){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;const a=r.getFunds();return e?n<=0?(this.showTreasuryNotice("error","Enter a valid transfer amount."),!1):n>a?(this.showTreasuryNotice("error","Insufficient org funds for this transfer."),!1):(r.setFunds(a-n),this.showTreasuryNotice("success",`${t.formatCurrency(n)} sent to ${e}.`),!0):(this.showTreasuryNotice("error","Select a member to receive funds."),!1)}grantCreditLine(e,n){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;if(!e)return this.showTreasuryNotice("error","Select a member for the credit line."),!1;if(n<=0)return this.showTreasuryNotice("error","Enter a valid credit line amount."),!1;const a=r.getMembers().find(n=>t.getMemberUid(n)===e),o=a?t.getMemberName(a):"";if(!o)return this.showTreasuryNotice("error","Selected member was not found in the organization roster."),!1;const i=window.RegistryApp?window.RegistryApp.bridge:null;return i&&"function"==typeof i.requestCreditLine?i.requestCreditLine({memberUid:e,memberName:o,amount:n}):(this.showTreasuryNotice("error","Credit line bridge is unavailable."),!1)}}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-navbar",o=`[${a}]`,i=`\n${o} {\n background: var(--bg-surface);\n border-bottom: 1px solid var(--border);\n box-shadow: var(--shadow);\n}\n\n${o} .app-navbar-inner {\n display: flex;\n justify-content: space-between;\n align-items: center;\n max-width: 1200px;\n width: 100%;\n margin: 0 auto;\n padding: 1rem 2rem;\n box-sizing: border-box;\n}\n\n${o} .app-navbar-brand {\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n}\n\n${o} .app-navbar-kicker {\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n font-weight: 600;\n}\n\n${o} .app-navbar-title {\n font-size: 1.25rem;\n font-weight: 700;\n color: var(--primary-hover);\n letter-spacing: -0.025em;\n}\n\n${o} .app-navbar-actions {\n display: flex;\n align-items: center;\n gap: 1.5rem;\n}\n\n${o} .app-navbar-view {\n font-size: 0.8rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--text-muted);\n font-weight: 600;\n}\n\n${o} .app-close-btn {\n background: transparent;\n color: var(--text-muted);\n border: 1px solid var(--border);\n padding: 0.5rem 1rem;\n font-size: 0.85rem;\n}\n\n${o} .app-close-btn:hover {\n background: var(--bg-surface-hover);\n color: var(--primary-hover);\n border-color: var(--primary);\n transform: none;\n box-shadow: none;\n}\n\n@media (max-width: 960px) {\n ${o} .app-navbar-inner {\n flex-direction: column;\n align-items: flex-start;\n padding: 1rem 1.5rem;\n }\n\n ${o} .app-navbar-actions {\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.Navbar=function({kicker:e="ORBIS",title:n="",viewLabel:o="",actionLabel:s="",onAction:l=null}){return t("shared-navbar",i),r("nav",{className:"app-navbar",[a]:""},r("div",{className:"app-navbar-inner"},r("div",{className:"app-navbar-brand"},r("span",{className:"app-navbar-kicker"},e),r("span",{className:"app-navbar-title"},n)),r("div",{className:"app-navbar-actions"},r("span",{className:"app-navbar-view"},o),s&&"function"==typeof l?r("button",{type:"button",className:"app-close-btn",onClick:l},s):null)))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Header=function({title:e,subtitle:n="Organization Registration & Management Portal",onTitleClick:t=null}){return r("div",{className:"header"},r("h1",{style:{cursor:t?"pointer":"default"},onClick:t},e),r("p",null,n))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.OrgPortal=window.OrgPortal||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Hero=function({className:e="",kicker:n="",title:t="",subtitle:a="",meta:o=""}){const i=["card org-panel org-span-12 org-page-header",e].filter(Boolean).join(" ");return r("section",{className:i},r("div",{className:"org-page-heading"},r("span",{className:"org-page-kicker"},n),r("h1",{className:"org-page-title"},t),r("p",{className:"org-page-subtitle"},a),r("span",{className:"org-page-meta"},o)))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Footer=function({sections:e=[]}){return r("div",{className:"footer"},r("div",{className:"wrapper"},...e.map(e=>r("div",null,r("h3",null,e.title),r("ul",{style:{listStyleType:"none",padding:0}},...(e.items||[]).map(e=>r("li",null,e)))))))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-modal",o=`[${a}]`,i=`\n${o} {\n position: fixed;\n inset: 0;\n background: rgb(15 23 42 / 0.38);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 1.5rem;\n z-index: 20;\n}\n\n${o} .app-modal-card {\n width: min(100%, 30rem);\n margin-bottom: 0;\n text-align: left;\n}\n\n${o} .app-modal-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n margin-bottom: 1rem;\n}\n\n${o} .app-modal-title {\n margin: 0;\n color: var(--primary-hover);\n font-size: 1.45rem;\n}\n\n${o} .app-modal-close {\n width: 2.25rem;\n height: 2.25rem;\n padding: 0;\n background: var(--bg-surface);\n color: var(--text-main);\n border: 1px solid var(--border);\n box-shadow: none;\n transform: none;\n}\n\n${o} .app-modal-close:hover {\n background: var(--bg-surface-hover);\n color: var(--text-main);\n box-shadow: none;\n transform: none;\n}\n\n${o} .app-modal-form {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n${o} .app-modal-form label {\n display: block;\n margin-bottom: 0.5rem;\n color: var(--text-muted);\n font-weight: 500;\n font-size: 0.9rem;\n}\n\n${o} .app-modal-form input,\n${o} .app-modal-form select {\n width: 100%;\n padding: 0.75rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: var(--bg-app);\n color: var(--text-main);\n font-family: inherit;\n font-size: 1rem;\n box-sizing: border-box;\n transition: border-color 0.2s, box-shadow 0.2s;\n}\n\n${o} .app-modal-form input:focus,\n${o} .app-modal-form select:focus {\n outline: none;\n border-color: var(--primary);\n box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12);\n}\n\n${o} .app-modal-form input:disabled,\n${o} .app-modal-form select:disabled {\n background: #f1f5f9;\n color: var(--text-muted);\n cursor: not-allowed;\n}\n\n${o} .app-modal-actions {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 0.75rem;\n margin-top: 0.5rem;\n}\n\n${o} .app-modal-actions button + button,\n${o} .app-modal-danger-actions button + button {\n margin-left: 0;\n}\n\n${o} .app-modal-danger {\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid #fecaca;\n border-radius: var(--radius);\n background: #fff1f2;\n align-items: flex-start;\n}\n\n${o} .app-modal-danger p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .app-modal-danger-actions {\n display: flex;\n flex-wrap: wrap;\n gap: 0.75rem;\n}\n\n@media (max-width: 960px) {\n ${o} .app-modal-head,\n ${o} .app-modal-danger {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.Modal=function({title:e="",body:n=null,onClose:o=null}){return t("shared-modal",i),r("div",{className:"app-modal-backdrop",[a]:"",onClick:e=>{e.target===e.currentTarget&&o&&o()}},r("div",{className:"card app-modal-card"},r("div",{className:"app-modal-head"},r("div",null,r("h2",{className:"app-modal-title"},e)),r("button",{type:"button",className:"app-modal-close",onClick:o,"aria-label":"Close dialog"},"x")),n))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-panel-card",o=`[${a}]`,i=`\n${o} {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n}\n\n${o} .org-panel-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n margin-bottom: 1.5rem;\n}\n\n${o} .org-panel-body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-height: 0;\n}\n\n${o} .org-eyebrow {\n font-size: 0.8rem;\n font-weight: 700;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-muted);\n margin-bottom: 0.4rem;\n}\n\n${o} .org-panel-title {\n margin: 0;\n color: var(--primary-hover);\n font-size: 1.45rem;\n}\n\n${o} .org-panel-subtitle {\n margin: 0.35rem 0 0;\n color: var(--text-muted);\n font-size: 0.95rem;\n}\n\n@media (max-width: 960px) {\n ${o} .org-panel-head {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.PanelCard=function({className:e="",eyebrow:n="",title:o="",subtitle:s="",headerExtras:l=null,body:d=null,rootProps:c={}}){const m=["card org-panel",e].filter(Boolean).join(" ");return t("shared-panel-card",i),r("section",{className:m,[a]:"",...c},r("div",{className:"org-panel-head"},r("div",null,n?r("div",{className:"org-eyebrow"},n):null,r("h2",{className:"org-panel-title"},o),s?r("p",{className:"org-panel-subtitle"},s):null),l),r("div",{className:"org-panel-body"},d))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-metric-card",a=`[${t}]`,o=`\n${a} {\n display: flex;\n flex-direction: column;\n gap: 0.45rem;\n padding: 1rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);\n}\n\n${a}:nth-child(4n + 2),\n${a}:nth-child(4n + 3) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(226 232 240) 100%);\n border-color: rgb(100 116 139 / 0.35);\n box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6);\n}\n\n${a} .org-metric-label {\n font-size: 0.76rem;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--text-muted);\n}\n\n${a} .org-metric-value {\n font-size: 1.8rem;\n color: var(--primary-hover);\n line-height: 1.1;\n}\n\n${a}:nth-child(4n + 2) .org-metric-value,\n${a}:nth-child(4n + 3) .org-metric-value {\n color: #334155;\n}\n\n${a} .org-metric-note {\n color: var(--text-muted);\n font-size: 0.9rem;\n}\n\n@media (max-width: 960px) {\n ${a}:nth-child(4n + 3) {\n background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);\n border-color: var(--border);\n box-shadow: none;\n }\n\n ${a}:nth-child(4n + 3) .org-metric-value {\n color: var(--primary-hover);\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.MetricCard=function(e,a,i){return r("portal-metric-card",o),n("div",{className:"org-metric-card",[t]:""},n("span",{className:"org-metric-label"},e),n("strong",{className:"org-metric-value"},a),n("span",{className:"org-metric-note"},i))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-simple-stat",a=`[${t}]`,o=`\n${a} {\n display: flex;\n flex-direction: column;\n gap: 0.2rem;\n min-width: 90px;\n}\n\n${a} .org-simple-label {\n font-size: 0.72rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${a} .org-simple-value {\n font-size: 0.95rem;\n color: var(--text-main);\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.SimpleStat=function(e,a){return r("portal-simple-stat",o),n("div",{className:"org-simple-stat",[t]:""},n("span",{className:"org-simple-label"},e),n("strong",{className:"org-simple-value"},a))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.store,o=e.getters,i="data-ui-overview-card",s=`[${i}]`,l=`\n${s} .org-hero-grid {\n display: grid;\n grid-template-columns: 1.3fr 1fr;\n gap: 1.5rem;\n align-items: start;\n}\n\n${s} .org-summary {\n margin: 0;\n font-size: 1.05rem;\n color: var(--text-main);\n}\n\n${s} .org-meta-row {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 1rem;\n margin-top: 1.5rem;\n}\n\n${s} .org-meta-item {\n display: flex;\n flex-direction: column;\n gap: 0.4rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${s} .org-meta-item:nth-child(even) {\n background: linear-gradient(180deg, rgb(241 245 249) 0%, rgb(226 232 240) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${s} .org-meta-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${s} .org-meta-value {\n font-size: 1rem;\n font-weight: 600;\n color: var(--primary-hover);\n}\n\n${s} .org-metric-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${s} .org-hero-grid,\n ${s} .org-meta-row,\n ${s} .org-metric-grid {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.OverviewCard=function(){const s=e.componentFns.MetricCard,d=window.SharedUI.componentFns.PanelCard,c=o.getAssetReadiness(),m=t.org.headquarters||"ArmA Verse",g=a.getAssets().length,p=a.getFleet().length,u=a.getFunds(),f=a.getMembers().length,b=a.getReputation();return r("portal-overview-card",l),d({className:"org-span-12",eyebrow:t.org.tag,title:"Organization Overview",rootProps:{[i]:""},body:n("div",{className:"org-hero-grid"},n("div",{className:"org-hero-copy"},n("p",{className:"org-summary"},t.org.type," operating from ",m,". Treasury, fleet status, inventory, and roster management are surfaced here first."),n("div",{className:"org-meta-row"},n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Director"),n("span",{className:"org-meta-value"},o.formatDisplayName(t.org.owner))),n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Active Members"),n("span",{className:"org-meta-value"},`${f} total`)),n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Fleet Readiness"),n("span",{className:"org-meta-value"},null===c?"N/A":`${c}%`)))),n("div",{className:"org-metric-grid"},s("Org Funds",o.formatCurrency(u),"Organization treasury balance"),s("Reputation",b,"Organization standing"),s("Asset Lines",g,"Tracked supply and equipment entries"),s("Fleet Vehicles",p,"Tracked air, ground, and naval vehicles")))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.getters,o="data-ui-fleet-card",i=`[${o}]`,s=`\n${i} .org-simple-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${i} .org-simple-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${i} .org-simple-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${i} .org-simple-name {\n color: var(--primary-hover);\n}\n\n${i} .org-simple-meta {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${i} .org-simple-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.FleetCard=function(){const t=window.SharedUI.componentFns.PanelCard,i=e.componentFns.SimpleStat,l=e.store.getFleet();return r("portal-fleet-card",s),t({className:"org-scroll-panel org-span-7",title:"Fleet",subtitle:"Individual vehicles with type, status, and overall damage.",rootProps:{[o]:""},body:n("div",{className:"org-simple-list"},...l.map(e=>n("article",{className:"org-simple-row"},n("strong",{className:"org-simple-name"},e.name),n("div",{className:"org-simple-meta"},i("Type",a.formatVehicleType(e.type)),i("Status",e.status),i("Damage",e.damage)))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r,createSignal:t}=e.runtime,{portalData:a}=e.data,o=e.store,i=e.getters,s=e.actions,l="data-ui-treasury-card",d=`[${l}]`,[c,m]=t("overview"),[g,p]=t(!1),u=`\n${d} .org-treasury-menu {\n position: relative;\n}\n\n${d} .org-menu-btn {\n width: 2.75rem;\n height: 2.75rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 1px solid var(--border);\n background: #f8fafc;\n color: var(--text-muted);\n}\n\n${d} .org-menu-btn:hover {\n color: var(--primary-hover);\n border-color: rgb(148 163 184 / 0.65);\n}\n\n${d} .org-menu-btn svg {\n width: 1.1rem;\n height: 1.1rem;\n}\n\n${d} .org-menu-dropdown {\n position: absolute;\n top: calc(100% + 0.6rem);\n right: 0;\n min-width: 10.5rem;\n padding: 0.45rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #fff;\n box-shadow: 0 12px 28px rgb(15 23 42 / 0.12);\n display: flex;\n flex-direction: column;\n gap: 0.35rem;\n z-index: 5;\n}\n\n${d} .org-menu-option + .org-menu-option {\n margin-left: 0;\n}\n\n${d} .org-menu-option {\n width: 100%;\n justify-content: flex-start;\n background: transparent;\n color: var(--text-main);\n border: 1px solid transparent;\n}\n\n${d} .org-menu-option:hover {\n background: #f8fafc;\n border-color: rgb(148 163 184 / 0.35);\n}\n\n${d} .org-menu-option.is-active {\n background: rgb(226 232 240 / 0.7);\n color: var(--primary-hover);\n border-color: rgb(148 163 184 / 0.35);\n}\n\n${d} .org-finance-meta {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n margin-bottom: 1.5rem;\n}\n\n${d} .org-finance-meta > div {\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n display: flex;\n flex-direction: column;\n gap: 0.4rem;\n}\n\n${d} .org-meta-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${d} .org-action-grid {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n margin-bottom: 1rem;\n}\n\n${d} .org-action-grid button + button {\n margin-left: 0;\n}\n\n${d} .org-action-grid button {\n width: 100%;\n}\n\n${d} .org-access-note {\n margin: 0 0 1rem;\n color: var(--text-muted);\n font-size: 0.95rem;\n}\n\n${d} .org-credit-summary {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n padding: 0.85rem 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${d} .org-credit-summary strong {\n font-size: 1rem;\n}\n\n${d} .org-credit-summary span:last-child {\n font-size: 0.92rem;\n line-height: 1.45;\n}\n\n${d} .org-credit-lines-list {\n display: flex;\n flex-direction: column;\n gap: 0.85rem;\n}\n\n${d} .org-treasury-body {\n display: flex;\n flex: 1;\n flex-direction: column;\n gap: 1rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${d} .org-credit-line-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${d} .org-credit-line-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${d} .org-credit-line-member {\n display: flex;\n flex-direction: column;\n gap: 0.3rem;\n}\n\n${d} .org-credit-line-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${d} .org-credit-line-empty {\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n color: var(--text-muted);\n}\n\n@media (max-width: 960px) {\n ${d} .org-finance-meta {\n grid-template-columns: 1fr;\n }\n\n ${d} .org-credit-line-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.TreasuryCard=function(){const e=window.SharedUI.componentFns.PanelCard,t=o.getCreditLines(),a=o.getReputation(),d=i.canManageTreasury(),f=c(),b=g(),w=t.reduce((e,n)=>e+Number(n.availableAmount||n.amount||0),0),h=t.reduce((e,n)=>e+Number(n.amountDue||0),0),y=1===t.length?"1 active credit line":`${t.length} active credit lines`;return r("portal-treasury-card",u),e({className:"org-scroll-panel org-span-5",title:"Treasury",subtitle:"Organization funds, reputation and payouts.",headerExtras:n("div",{className:"org-treasury-menu"},n("button",{type:"button",className:"org-menu-btn",title:"Treasury views","aria-label":"Treasury views",onClick:()=>p(e=>!e)},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},n("line",{x1:"4",y1:"7",x2:"20",y2:"7"}),n("line",{x1:"4",y1:"12",x2:"20",y2:"12"}),n("line",{x1:"4",y1:"17",x2:"20",y2:"17"}))),b?n("div",{className:"org-menu-dropdown"},n("button",{type:"button",className:"overview"===f?"org-menu-option is-active":"org-menu-option",onClick:()=>{m("overview"),p(!1)}},"Overview"),n("button",{type:"button",className:"credit"===f?"org-menu-option is-active":"org-menu-option",onClick:()=>{m("credit"),p(!1)}},"Credit Lines")):null),rootProps:{[l]:""},body:n("div",{className:"org-treasury-body"},"credit"===f?t.length>0?n("div",{className:"org-credit-lines-list"},...t.map(e=>n("article",{className:"org-credit-line-row"},n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Member"),n("strong",null,e.member)),n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Available"),n("strong",null,i.formatCurrency(e.availableAmount||e.amount))),n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Amount Due"),n("strong",null,i.formatCurrency(e.amountDue))),n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Interest"),n("strong",null,`${Math.round(100*Number(e.interestRate||0))}%`))))):n("div",{className:"org-credit-line-empty"},"No active credit lines."):n("div",null,n("div",{className:"org-finance-meta"},n("div",null,n("span",{className:"org-meta-label"},"Funds"),n("strong",null,i.formatCurrency(o.getFunds()))),n("div",null,n("span",{className:"org-meta-label"},"Reputation"),n("strong",null,`${a}`)),n("div",null,n("span",{className:"org-meta-label"},"Reserved Credit"),n("strong",null,i.formatCurrency(w))),n("div",null,n("span",{className:"org-meta-label"},"Outstanding Due"),n("strong",null,i.formatCurrency(h)))),d?n("div",{className:"org-action-grid"},n("button",{type:"button",onClick:()=>s.openModal("payroll")},"Run Payroll"),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>s.openModal("transfer")},"Send Funds"),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>s.openModal("credit")},"Credit Line")):n("p",{className:"org-access-note"},"Only the organization leader or CEO can manage treasury actions."),n("div",{className:"org-credit-summary"},n("span",{className:"org-meta-label"},"Credit Line Status"),n("strong",null,y),n("span",null,t.length>0?"Open the Credit Lines tab to review reserved balances, due amounts, and member exposure.":"Assign a credit line to create the first approved member limit."))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.getters,o="data-ui-assets-card",i=`[${o}]`,s=`\n${i} .org-simple-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${i} .org-simple-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${i} .org-simple-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${i} .org-simple-name {\n color: var(--primary-hover);\n}\n\n${i} .org-simple-meta {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${i} .org-simple-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.AssetsCard=function(){const t=window.SharedUI.componentFns.PanelCard,i=e.componentFns.SimpleStat,l=e.store.getAssets();return r("portal-assets-card",s),t({className:"org-scroll-panel org-span-7",title:"Assets",subtitle:"Inventory supplies and equipment with quantity totals.",rootProps:{[o]:""},body:n("div",{className:"org-simple-list"},...l.map(e=>n("article",{className:"org-simple-row"},n("strong",{className:"org-simple-name"},e.name),n("div",{className:"org-simple-meta"},i("Type",a.formatAssetType(e.type)),i("Quantity",e.quantity)))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.getters,o=e.actions,i="data-ui-members-card",s=`[${i}]`,l=`\n${s} .org-name-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${s} .org-name-row {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${s} .org-name-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${s} .org-name-row button {\n margin-left: auto;\n}\n\n@media (max-width: 960px) {\n ${s} .org-name-row {\n flex-direction: column;\n align-items: flex-start;\n }\n\n ${s} .org-name-row button {\n margin-left: 0;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.MembersCard=function(){const e=window.SharedUI.componentFns.PanelCard,s=t.getMembers(),d=a.canManageMembers();return r("portal-members-card",l),e({className:"org-scroll-panel org-span-5",title:"Members",subtitle:"Current roster listing. The organization owner and your own member entry cannot be removed.",rootProps:{[i]:""},body:n("div",{className:"org-name-list"},...s.map(e=>{const r=d&&!a.isProtectedMember(e);return n("article",{className:"org-name-row"},n("strong",null,e.name),r?n("button",{type:"button",className:"org-danger-btn org-icon-btn",title:`Remove ${e.name}`,"aria-label":`Remove ${e.name}`,onClick:()=>o.removeMember(e)},n("svg",{className:"org-icon",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},n("path",{d:"M9 3h6"}),n("path",{d:"M4 7h16"}),n("path",{d:"M6 7l1 13h10l1-13"}),n("path",{d:"M10 11v6"}),n("path",{d:"M14 11v6"}))):null)}))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a="data-ui-activity-card",o=`[${a}]`,i=`\n${o} .org-activity-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${o} .org-activity-row {\n padding: 1rem;\n border: 1px solid var(--border);\n border-left: 3px solid #94a3b8;\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${o} .org-activity-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n border-left-color: #64748b;\n}\n\n${o} .org-activity-row p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .org-activity-time {\n display: inline-block;\n margin-bottom: 0.35rem;\n color: var(--text-muted);\n font-size: 0.8rem;\n font-weight: 700;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.ActivityCard=function(){const t=window.SharedUI.componentFns.PanelCard,o=e.store.getActivity();return r("portal-activity-card",i),t({className:"org-scroll-panel org-span-6",title:"Command Feed",subtitle:"Recent organization-level actions and updates.",rootProps:{[a]:""},body:n("div",{className:"org-activity-list"},...o.map(e=>n("article",{className:"org-activity-row"},n("span",{className:"org-activity-time"},e.time),n("p",null,e.text))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-future-card",a=[{name:"Contracts Board",status:"Planned",detail:"Track payouts, assignments, and claim approvals."},{name:"Diplomacy",status:"Future Review",detail:"Possible future module pending a full design and scope review."},{name:"Logistics Queue",status:"Future Review",detail:"Possible future module pending a full design and scope review."},{name:"Permissions",status:"Future Review",detail:"Possible future module pending a full design and scope review."}],o=`[${t}]`,i=`\n${o} .org-roadmap-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${o} .org-roadmap-card {\n padding: 1rem;\n display: flex;\n flex-direction: column;\n gap: 0.7rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${o} .org-roadmap-card:nth-child(4n + 2),\n${o} .org-roadmap-card:nth-child(4n + 3) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(100 116 139 / 0.4);\n}\n\n${o} .org-roadmap-card p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .org-list-tag {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0.2rem 0.55rem;\n border-radius: 999px;\n font-size: 0.72rem;\n font-weight: 700;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n background: #e2e8f0;\n color: var(--primary-hover);\n}\n\n${o} .org-roadmap-card:nth-child(4n + 2) .org-list-tag,\n${o} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {\n background: #cbd5e1;\n color: #1e293b;\n}\n\n@media (max-width: 960px) {\n ${o} .org-roadmap-grid {\n grid-template-columns: 1fr;\n }\n\n ${o} .org-roadmap-card:nth-child(4n + 3) {\n background: #f8fafc;\n border-color: var(--border);\n }\n\n ${o} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {\n background: #e2e8f0;\n color: var(--primary-hover);\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.FutureCard=function(){const e=window.SharedUI.componentFns.PanelCard;return r("portal-future-card",i),e({className:"org-scroll-panel org-span-6",title:"Expansion Slots",subtitle:"Potential modules are tagged by status such as Planned, In Design, In Review, and Future Review.",rootProps:{[t]:""},body:n("div",{className:"org-roadmap-grid"},...a.map(e=>n("article",{className:"org-roadmap-card"},n("span",{className:"org-list-tag"},e.status),n("strong",null,e.name),n("p",null,e.detail))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.getters,a=e.actions,o="data-ui-danger-card",i=`[${o}]`,s=`\n${i} {\n border-color: #fecaca;\n background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%);\n}\n\n${i} .org-danger-copy {\n margin-bottom: 1rem;\n}\n\n${i} .org-danger-copy strong,\n${i} .org-danger-copy p {\n display: block;\n}\n\n${i} .org-danger-copy p {\n margin: 0.4rem 0 0;\n color: var(--text-muted);\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.DangerCard=function(){const e=window.SharedUI.componentFns.PanelCard;return r("portal-danger-card",s),t.canDisbandOrg()?e({className:"org-span-12 org-danger-panel",title:"Organization Controls",subtitle:"Leader-only actions for membership and permanent organization removal.",rootProps:{[o]:""},body:n("div",null,n("div",{className:"org-danger-copy"},n("strong",null,"Disband organization"),n("p",null,"This removes the organization and revokes access to the portal for all members.")),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.openModal("disband")},"Disband Organization"))}):null}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n}=e.runtime,{portalData:r}=e.data,t=e.store,a=e.actions;e.componentFns=e.componentFns||{},e.componentFns.ModalLayer=function(){const e=window.SharedUI.componentFns.Modal,o=t.getModal();if(!o)return null;const i=t.getMembers(),s=0===i.length?{disabled:!0}:{};let l="",d=null;return"payroll"===o.type?(l="Run Payroll",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Amount Per Member"),n("input",{id:"treasury-payroll-amount",type:"number",min:"1",placeholder:"500",autofocus:"true"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",onClick:()=>{a.runPayroll(a.parseAmount(a.getInputValue("treasury-payroll-amount")))&&a.closeModal()}},"Run Payroll")))):"transfer"===o.type?(l="Send Funds",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Member"),n("select",{id:"treasury-transfer-member",...s},...i.map(e=>n("option",{value:e.name},e.name)))),n("div",null,n("label",null,"Amount"),n("input",{id:"treasury-transfer-amount",type:"number",min:"1",placeholder:"1500"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",...s,onClick:()=>{a.sendFundsToMember(String(a.getInputValue("treasury-transfer-member")||""),a.parseAmount(a.getInputValue("treasury-transfer-amount")))&&a.closeModal()}},"Send Funds")))):"credit"===o.type?(l="Assign Credit Line",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Member"),n("select",{id:"treasury-credit-member",...s},...i.map(e=>n("option",{value:e.uid},e.name)))),n("div",null,n("label",null,"Credit Amount"),n("input",{id:"treasury-credit-amount",type:"number",min:"1",placeholder:"5000"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",...s,onClick:()=>{a.grantCreditLine(String(a.getInputValue("treasury-credit-member")||""),a.parseAmount(a.getInputValue("treasury-credit-amount")))&&a.closeModal()}},"Assign Credit Line")))):"disband"===o.type?(l="Disband Organization",d=n("div",{className:"app-modal-danger"},n("p",null,"This action is permanent. Disband ",r.org.name,"?"),n("div",{className:"app-modal-danger-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.disbandOrganization()},"Confirm Disband")))):"leave"===o.type&&(l="Leave Organization",d=n("div",{className:"app-modal-danger"},n("p",null,"Leave ",r.org.name," and return to the default organization?"),n("div",{className:"app-modal-danger-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.leaveOrganization()},"Confirm Leave")))),e({title:l,body:d,onClose:()=>a.closeModal()})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n}=e.runtime,{portalData:r}=e.data,t=window.RegistryApp.store;e.componentFns=e.componentFns||{},e.componentFns.DisbandedView=function(){return(0,window.SharedUI.componentFns.PanelCard)({className:"org-span-12 org-empty-state",eyebrow:"Organization Removed",title:r.org.name,body:n("div",null,n("p",{className:"org-summary"},"This organization has been disbanded. Member access, assets, and fleet management are no longer available from this portal preview."),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>t.setView("home")},"Return to Registry"))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t,session:a}=e.data,o=e.store,i="[data-ui-portal-view]";r("portal-view",`\n ${i} {\n --org-row-card-max-height: 36rem;\n }\n\n ${i} .org-toast-stack {\n position: fixed;\n top: 1.5rem;\n right: 2rem;\n z-index: 20;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n pointer-events: none;\n }\n\n ${i} .org-toast {\n max-width: 24rem;\n padding: 0.9rem 1rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: #fff;\n box-shadow: 0 12px 28px rgb(15 23 42 / 0.14);\n font-size: 0.92rem;\n pointer-events: auto;\n }\n\n ${i} .org-toast.is-success {\n background: #ecfdf5;\n border-color: #bbf7d0;\n color: #166534;\n }\n\n ${i} .org-toast.is-error {\n background: #fef2f2;\n border-color: #fecaca;\n color: #991b1b;\n }\n\n ${i} .org-dashboard-grid {\n display: grid;\n grid-template-columns: repeat(12, minmax(0, 1fr));\n gap: 1.5rem;\n align-items: stretch;\n }\n\n ${i} .org-panel {\n margin-bottom: 0;\n text-align: left;\n }\n\n ${i} .org-scroll-panel {\n display: flex;\n flex-direction: column;\n min-height: 0;\n max-height: var(--org-row-card-max-height);\n overflow: hidden;\n }\n\n ${i} .org-island-root {\n display: flex;\n align-self: stretch;\n min-height: 0;\n min-width: 0;\n }\n\n ${i} .org-island-root > .org-panel {\n height: 100%;\n width: 100%;\n }\n\n ${i} .org-span-12 {\n grid-column: span 12;\n }\n\n ${i} .org-span-7 {\n grid-column: span 7;\n }\n\n ${i} .org-span-6 {\n grid-column: span 6;\n }\n\n ${i} .org-span-5 {\n grid-column: span 5;\n }\n\n @media (max-width: 960px) {\n ${i} .org-toast-stack {\n top: 1rem;\n right: 1rem;\n left: 1rem;\n }\n\n ${i} .org-toast {\n max-width: none;\n }\n\n ${i} .org-span-12,\n ${i} .org-span-7,\n ${i} .org-span-6,\n ${i} .org-span-5 {\n grid-column: span 12;\n }\n\n ${i} .org-scroll-panel {\n max-height: none;\n }\n\n }\n `),e.components=e.components||{},e.componentFns=e.componentFns||{},e.componentFns.TreasuryNoticeLayer=function(){const e=o.getTreasuryNotice();return e.text?n("div",{className:"org-toast-stack"},n("div",{className:"error"===e.type?"org-toast is-error":"org-toast is-success"},e.text)):null},e.components.App=function(){const r=window.SharedUI.componentFns.Hero,i=window.SharedUI.componentFns.Footer,s=e.componentFns.FutureCard,l=e.componentFns.DangerCard,d=e.componentFns.DisbandedView,c=[{title:"Organization Controls",items:["Roster Management","Fleet Assignment","Treasury Permissions","Asset Registry"]},{title:"Planned Extensions",items:["Contracts Board","Diplomacy Layer","Procurement Queue","Reputation History"]}];return o.getOrgDisbanded()?n("main",{"data-ui-portal-view":""},n("div",{className:"container"},n("div",{className:"org-dashboard-grid"},r({kicker:t.org.tag,title:t.org.name,subtitle:"Player organization command portal",meta:`${a.actorName} - ${a.role}`}),d())),n("div",{id:"org-portal-modal-root"}),i({sections:c})):n("main",{"data-ui-portal-view":""},n("div",{id:"org-portal-toast-root"}),n("div",{className:"container"},n("div",{className:"org-dashboard-grid"},r({kicker:t.org.tag,title:t.org.name,subtitle:"Player organization command portal",meta:`${a.actorName} - ${a.role}`}),n("div",{className:"org-island-root org-span-12",id:"org-overview-card-root"}),n("div",{className:"org-island-root org-span-7",id:"org-fleet-card-root"}),n("div",{className:"org-island-root org-span-5",id:"org-treasury-card-root"}),n("div",{className:"org-island-root org-span-5",id:"org-members-card-root"}),n("div",{className:"org-island-root org-span-7",id:"org-assets-card-root"}),n("div",{className:"org-island-root org-span-6",id:"org-activity-card-root"}),s(),l())),n("div",{id:"org-portal-modal-root"}),i({sections:c}))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.bridge,o="data-ui-registration-view",i=`[${o}]`,s=`\n${i} {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2rem;\n align-items: center;\n width: 100%;\n}\n\n${i} .info-panel {\n text-align: left;\n padding: 1rem;\n}\n\n${i} .create-feature-list {\n text-align: left;\n margin-top: 1.5rem;\n list-style-type: none;\n padding: 0;\n}\n\n${i} .create-feature-item {\n margin-bottom: 0.5rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n${i} .create-feature-icon {\n width: 1.2rem;\n height: 1.2rem;\n flex-shrink: 0;\n}\n\n${i} .price-tag {\n margin-top: 2rem;\n padding: 1rem;\n background: var(--bg-app);\n border-radius: var(--radius);\n border: 1px solid var(--border);\n}\n\n${i} .price-label {\n display: block;\n font-size: 0.9rem;\n color: var(--text-muted);\n}\n\n${i} .price-value {\n display: block;\n font-size: 2rem;\n font-weight: 700;\n color: var(--primary);\n}\n\n${i} .form-panel {\n margin: 0;\n}\n\n${i} .app-form {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n text-align: left;\n}\n\n${i} .app-form label {\n display: block;\n margin-bottom: 0.5rem;\n color: var(--text-muted);\n font-weight: 500;\n font-size: 0.9rem;\n}\n\n${i} .app-form input,\n${i} .app-form select {\n width: 100%;\n padding: 0.75rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: var(--bg-app);\n color: var(--text-main);\n font-family: inherit;\n font-size: 1rem;\n box-sizing: border-box;\n transition: border-color 0.2s;\n}\n\n${i} .app-form input:focus,\n${i} .app-form select:focus {\n outline: none;\n border-color: var(--primary);\n box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1);\n}\n\n${i} .form-actions {\n margin-top: 1rem;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n align-items: center;\n}\n\n${i} .submit-btn {\n width: 100%;\n}\n\n${i} .cancel-link {\n font-size: 0.9rem;\n color: var(--text-muted);\n cursor: pointer;\n text-decoration: underline;\n}\n\n${i} .cancel-link:hover {\n color: var(--primary);\n}\n\n${i} .form-feedback {\n padding: 0.85rem 1rem;\n border-radius: var(--radius);\n font-size: 0.92rem;\n}\n\n${i} .form-feedback.is-error {\n background: #fef2f2;\n border: 1px solid #fecaca;\n color: #991b1b;\n}\n\n@media (max-width: 960px) {\n ${i} {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.RegistrationView=function(){const e=t.getIsCreating(),i=t.getCreateError();r("main-registration-view",s);return n("div",{className:"split-container",[o]:""},n("div",{className:"info-panel"},n("h2",null,"Registration Details"),n("p",null,"Complete the form to add your organization to the Global Organization Registry."),n("ul",{className:"create-feature-list"},n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Official Organization Designator"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Secure Comms Channel"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Deployment Roster Access"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"After-Action Report Tools")),n("div",{className:"price-tag"},n("span",{className:"price-label"},"Registration Fee"),n("span",{className:"price-value"},"$50,000"))),n("div",{className:"form-panel card"},n("h2",null,"Organization Registration"),n("div",{className:"app-form"},n("div",null,n("label",null,"Organization Name"),n("input",{id:"org-create-name",type:"text",placeholder:"e.g. Task Force 141"})),n("div",null,n("label",null,"Organization Type"),n("select",{id:"org-create-type"},n("option",{value:"infantry"},"Infantry / Milsim"),n("option",{value:"aviation"},"Aviation Wing"),n("option",{value:"pmc"},"Private Military Company"),n("option",{value:"support"},"Logistics & Support"))),n("div",{className:"form-actions"},i?n("div",{className:"form-feedback is-error"},i):null,n("button",{type:"button",className:"submit-btn",disabled:e,onClick:()=>{const e={orgName:String(document.getElementById("org-create-name")?.value||"").trim(),type:String(document.getElementById("org-create-type")?.value||"")};a&&"function"==typeof a.requestCreateOrg?a.requestCreateOrg(e):t.failCreate("Registration bridge is not available.")}},e?"Submitting Registration...":"Submit Registration"),n("span",{className:"cancel-link",onClick:()=>t.setView("home")},"Cancel / Return to Main")))))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.bridge,o="data-ui-home-view",i=`[${o}]`,s=`\n${i} {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2rem;\n margin-bottom: 2rem;\n}\n\n${i} .home-feedback {\n padding: 0.85rem 1rem;\n border-radius: var(--radius);\n font-size: 0.92rem;\n background: #fef2f2;\n border: 1px solid #fecaca;\n color: #991b1b;\n}\n\n@media (max-width: 960px) {\n ${i} {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.HomeView=function(){const e=t.getIsAuthenticating(),i=t.getLoginError();return r("main-home-view",s),n("div",{className:"content",[o]:""},n("div",{className:"card"},n("h2",null,"Create Organization"),n("p",null,"Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly."),n("button",{onClick:()=>t.setView("create")},"Register")),n("div",{className:"card"},n("h2",null,"Organization Portal"),n("p",null,"Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink."),i?n("div",{className:"home-feedback"},i):null,n("button",{disabled:e,onClick:()=>{a?a.requestLogin({}):t.failLogin("Login bridge is not available.")}},e?"Opening Portal...":"Login")))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n}=e.runtime,r=e.store;e.components=e.components||{},e.components.App=function(){const t=window.SharedUI.componentFns.Navbar,a=window.SharedUI.componentFns.Header,o=window.SharedUI.componentFns.Footer,i=window.SharedUI.componentFns.WindowTitleBar,s=e.componentFns.HomeView,l=e.componentFns.RegistrationView,d=window.OrgPortal&&window.OrgPortal.components?window.OrgPortal.components.App:null,c=r.getView(),m=window.OrgPortal&&window.OrgPortal.getters?window.OrgPortal.getters:null,g=window.OrgPortal&&window.OrgPortal.actions?window.OrgPortal.actions:null,p="create"===c?"Organization Registration":"portal"===c?"Organization Portal":"Entry Hub";function u(){e.bridge&&"function"==typeof e.bridge.close?e.bridge.close({}):r.setView("home")}if("portal"===c&&d){const e=m&&"function"==typeof m.canLeaveOrg&&m.canLeaveOrg();return n("div",{className:"app-shell"},i({kicker:"FORGE ORBIS",title:"Global Organization Network",onClose:u,closeLabel:"Close organization interface"}),t({title:"Global Organization Network",viewLabel:p,actionLabel:e?"Leave Organization":"",onAction:e&&g&&"function"==typeof g.openModal?()=>g.openModal("leave"):null}),n("div",{id:"org-portal-frame-root"}))}let f;return"home"===c?f=s():"create"===c&&(f=l()),n("div",{className:"app-shell"},i({kicker:"FORGE ORBIS",title:"Global Organization Network",onClose:u,closeLabel:"Close organization interface"}),n("main",null,t({title:"Global Organization Network",viewLabel:p}),n("div",{className:"container"},a({title:"Global Organization Network",onTitleClick:()=>r.setView("home")}),f),o({sections:[{title:"Registry Resources",items:["Registration Guidelines","Tax & Fee Schedule","Legal Compliance","Trademark Database"]},{title:"Bureau Support",items:["Office: Sector 7 Admin Block","Hours: 0800 - 1600 (GST)","Helpdesk: 555-01-REGISTRY","support@org-bureau.gov"]}]})))}}(),function(){const e=window.ForgeWebUI,n=window.RegistryApp,r=window.OrgPortal,t=[{id:"org-portal-frame-root",preserveScroll:!0,render:()=>r.components.App()},{id:"org-portal-toast-root",preserveScroll:!1,render:()=>r.componentFns.TreasuryNoticeLayer()},{id:"org-overview-card-root",preserveScroll:!1,render:()=>r.componentFns.OverviewCard()},{id:"org-fleet-card-root",preserveScroll:!0,render:()=>r.componentFns.FleetCard()},{id:"org-treasury-card-root",preserveScroll:!1,render:()=>r.componentFns.TreasuryCard()},{id:"org-members-card-root",preserveScroll:!0,render:()=>r.componentFns.MembersCard()},{id:"org-assets-card-root",preserveScroll:!0,render:()=>r.componentFns.AssetsCard()},{id:"org-activity-card-root",preserveScroll:!0,render:()=>r.componentFns.ActivityCard()},{id:"org-portal-modal-root",preserveScroll:!1,render:()=>r.componentFns.ModalLayer()}];e.createApp({name:"org",root:"#app",setup({root:r}){const a=function(){const n=new Map;return{sync:function(){t.forEach(r=>{const t=document.getElementById(r.id),a=n.get(r.id);if(!t)return void(a&&(a.handle.dispose(),n.delete(r.id)));if(a&&a.container===t)return;a&&a.handle.dispose();const o=e.mount(t,r.render,{preserveScroll:r.preserveScroll});n.set(r.id,{container:t,handle:o})})}}}();e.mount(r,()=>n.components.App(),{preserveScroll:!1}),n.bridge.ready({loaded:!0}),e.effect(()=>{n.store.getView(),requestAnimationFrame(()=>{a.sync()})})}}).start()}();
\ No newline at end of file
+!function(){const e=window.ForgeWebUI,n=window.RegistryApp=window.RegistryApp||{},r=window.OrgPortal=window.OrgPortal||{};n.runtime=e,r.runtime=e,window.AppRuntime=e}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{createSignal:n}=e.runtime;e.store=new class{constructor(){[this.getView,this.setView]=n("home"),[this.getIsAuthenticating,this.setIsAuthenticating]=n(!1),[this.getLoginError,this.setLoginError]=n(""),[this.getIsCreating,this.setIsCreating]=n(!1),[this.getCreateError,this.setCreateError]=n("")}startLogin(){this.setLoginError(""),this.setIsAuthenticating(!0)}startCreate(){this.setCreateError(""),this.setIsCreating(!0)}failLogin(e){this.setIsAuthenticating(!1),this.setLoginError(e||"Authentication failed.")}failCreate(e){this.setIsCreating(!1),this.setCreateError(e||"Organization registration failed.")}hydratePortal(e){const n=window.OrgPortal&&window.OrgPortal.data?window.OrgPortal.data:null,r=window.OrgPortal&&window.OrgPortal.store?window.OrgPortal.store:null,t=e&&e.portalData?e.portalData:null,a=e&&e.session?e.session:null;return!!(n&&"function"==typeof n.applyLoginPayload&&r&&"function"==typeof r.hydrateFromPayload&&t&&a)&&(n.applyLoginPayload(e),r.hydrateFromPayload(e),!0)}completeLogin(e){this.hydratePortal(e)?(this.setLoginError(""),this.setIsAuthenticating(!1),this.setView("portal")):this.failLogin("Login response was missing portal data.")}completeCreate(e){this.hydratePortal(e)?(this.setCreateError(""),this.setIsCreating(!1),this.setView("portal")):this.failCreate("Organization registration response was missing portal data.")}}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},n=e.store,r=window.ForgeWebUI.createBridge({closeEvent:"org::close",globalName:"ForgeBridge",readyEvent:"org::ready"});function t(e,n){return r.send(e,n)}r.on("org::login::success",e=>{n.completeLogin(e)}),r.on("org::login::failure",e=>{n.failLogin(e.message||"Authentication failed.")}),r.on("org::create::success",e=>{n.completeCreate(e)}),r.on("org::create::failure",e=>{n.failCreate(e.message||"Organization registration failed.")}),r.on("org::sync",e=>{n&&"function"==typeof n.hydratePortal&&n.hydratePortal(e)}),r.on("org::credit::success",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("success",e.message||"Credit line assigned.")}),r.on("org::credit::failure",e=>{const n=window.OrgPortal;n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Unable to assign credit line.")}),r.on("org::invite::success",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("success",e.message||"Organization invite sent.")}),r.on("org::invite::failure",e=>{const n=window.OrgPortal;n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Unable to send organization invite.")}),r.on("org::invite::decision::success",e=>{const n=window.OrgPortal;n&&n.actions&&n.actions.showTreasuryNotice("success",e.message||"Organization invite updated.")}),r.on("org::invite::decision::failure",e=>{const n=window.OrgPortal;n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Unable to update organization invite.")}),r.on("org::member::creditUpdated",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setCreditLines(n=>{const r={amount:e.availableAmount||e.amount||0,amountDue:e.amountDue||0,approvedAmount:e.approvedAmount||e.availableAmount||e.amount||0,availableAmount:e.availableAmount||e.amount||0,interestRate:e.interestRate||.1,member:e.memberName||"",outstandingPrincipal:e.outstandingPrincipal||0,uid:e.memberUid||""},t=n.findIndex(e=>e.uid===r.uid);return-1===t?[...n,r]:n.map((e,n)=>n===t?r:e)})}),r.on("org::disband::success",()=>{const e=window.OrgPortal;e&&e.store&&(e.store.setModal(null),e.store.setOrgDisbanded(!0))}),r.on("org::disband::failure",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Organization disbanding failed.")}),r.on("org::leave::success",e=>{const r=window.OrgPortal;r&&r.store&&r.store.setModal(null),n.failLogin(e.message||"You have left the organization."),n.setView("home")}),r.on("org::leave::failure",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Unable to leave the organization.")}),r.on("org::portal::revoked",e=>{const r=window.OrgPortal;r&&r.store&&r.store.setModal(null),n.failLogin(e.message||"Organization access is no longer available."),n.setView("home")}),e.bridge={close:r.close,ready:r.ready,receive:r.receive,requestLogin:function(e){n.startLogin(),t("org::login::request",e)||n.failLogin("Arma login bridge is unavailable.")},requestCreateOrg:function(e){n.startCreate(),t("org::create::request",e)||n.failCreate("Arma registration bridge is unavailable.")},requestDisbandOrg:function(){if(t("org::disband::request",{}))return;const e=window.OrgPortal;e&&e.actions&&e.actions.showTreasuryNotice("error","Arma disband bridge is unavailable.")},requestLeaveOrg:function(){if(t("org::leave::request",{}))return;const e=window.OrgPortal;e&&e.actions&&e.actions.showTreasuryNotice("error","Arma leave bridge is unavailable.")},requestCreditLine:function(e){if(t("org::credit::request",e))return!0;const n=window.OrgPortal;return n&&n.actions&&n.actions.showTreasuryNotice("error","Arma credit line bridge is unavailable."),!1},requestInvitePlayer:function(e){if(t("org::invite::request",e))return!0;const n=window.OrgPortal;return n&&n.actions&&n.actions.showTreasuryNotice("error","Arma organization invite bridge is unavailable."),!1},requestAcceptInvite:function(e){if(t("org::invite::accept",e))return!0;const n=window.OrgPortal;return n&&n.actions&&n.actions.showTreasuryNotice("error","Arma organization invite bridge is unavailable."),!1},requestDeclineInvite:function(e){if(t("org::invite::decline",e))return!0;const n=window.OrgPortal;return n&&n.actions&&n.actions.showTreasuryNotice("error","Arma organization invite bridge is unavailable."),!1},sendEvent:t}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},n={type:"Organization",status:"Operational",headquarters:"ArmA Verse"};function r(e){return JSON.parse(JSON.stringify(e))}function t(e,n){Object.keys(e).forEach(n=>delete e[n]),Object.assign(e,r(n))}function a(e,n){e.splice(0,e.length,...r(n))}function o(e){if(e&&"object"==typeof e&&!Array.isArray(e))return e;if(Array.isArray(e)){if(e.every(e=>Array.isArray(e)&&e.length>=2&&"string"==typeof e[0]))return Object.fromEntries(e)}if("string"==typeof e&&""!==e.trim())try{return o(JSON.parse(e))}catch(n){return e}return e}function i(e){return(Array.isArray(e)?e:e&&"object"==typeof e?Object.values(e):[]).map(o).filter(Boolean)}e.data={portalData:{org:Object.assign({name:"",tag:"",owner:"",ownerUid:"",isDefault:!1},n),funds:0,reputation:0,creditLines:[],members:[],pendingInvites:[],inviteablePlayers:[],fleet:[],assets:[],activity:[],roadmap:[{name:"Contracts Board",status:"Planned",detail:"Track payouts, assignments, and claim approvals."},{name:"Diplomacy",status:"Future Review",detail:"Possible future module pending a full design and scope review."},{name:"Logistics Queue",status:"Future Review",detail:"Possible future module pending a full design and scope review."},{name:"Permissions",status:"Future Review",detail:"Possible future module pending a full design and scope review."}]},session:{actorName:"",actorUid:"",role:"",ceo:!1},applyLoginPayload(e){t(this.portalData.org,Object.assign({},e.portalData.org||{},n)),this.portalData.funds=e.portalData.funds||0,this.portalData.reputation=e.portalData.reputation||0,a(this.portalData.creditLines,i(e.portalData.creditLines)),a(this.portalData.members,i(e.portalData.members)),a(this.portalData.pendingInvites,i(e.portalData.pendingInvites)),a(this.portalData.inviteablePlayers,i(e.portalData.inviteablePlayers)),a(this.portalData.fleet,i(e.portalData.fleet)),a(this.portalData.assets,i(e.portalData.assets)),a(this.portalData.activity,i(e.portalData.activity)),a(this.portalData.roadmap,i(e.portalData.roadmap)),t(this.session,e.session||{})}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{createSignal:n}=window.RegistryApp.runtime,{portalData:r}=e.data;function t(e){if(e&&"object"==typeof e&&!Array.isArray(e))return e;if(Array.isArray(e)){if(e.every(e=>Array.isArray(e)&&e.length>=2&&"string"==typeof e[0]))return Object.fromEntries(e)}if("string"==typeof e&&""!==e.trim())try{return t(JSON.parse(e))}catch(n){return e}return e}function a(e){return(Array.isArray(e)?e:e&&"object"==typeof e?Object.values(e):[]).map(t).filter(Boolean)}e.store=new class{constructor(){[this.getFunds,this.setFunds]=n(r.funds),[this.getReputation,this.setReputation]=n(r.reputation),[this.getMembers,this.setMembers]=n([...r.members]),[this.getPendingInvites,this.setPendingInvites]=n([...r.pendingInvites]),[this.getInviteablePlayers,this.setInviteablePlayers]=n([...r.inviteablePlayers]),[this.getCreditLines,this.setCreditLines]=n([...r.creditLines]),[this.getFleet,this.setFleet]=n([...r.fleet]),[this.getAssets,this.setAssets]=n([...r.assets]),[this.getActivity,this.setActivity]=n([...r.activity]),[this.getTreasuryNotice,this.setTreasuryNotice]=n({type:"",text:""}),[this.getModal,this.setModal]=n(null),[this.getInviteMenuOpen,this.setInviteMenuOpen]=n(!1),[this.getOrgDisbanded,this.setOrgDisbanded]=n(!1)}hydrateFromPayload(e){const n=e.portalData||{};this.setFunds(n.funds||0),this.setReputation(n.reputation||0),this.setMembers([...a(n.members)]),this.setPendingInvites([...a(n.pendingInvites)]),this.setInviteablePlayers([...a(n.inviteablePlayers)]),this.setCreditLines([...a(n.creditLines)]),this.setFleet([...a(n.fleet)]),this.setAssets([...a(n.assets)]),this.setActivity([...a(n.activity)])}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{portalData:n,session:r}=e.data;e.getters=new class{formatCurrency(e){return"$"+Number(e||0).toLocaleString()}formatVehicleType(e){return e?e.charAt(0).toUpperCase()+e.slice(1):""}formatAssetType(e){return e?e.charAt(0).toUpperCase()+e.slice(1):""}formatDisplayName(e){return e?String(e).trim().split(/\s+/).map(e=>e?e.charAt(0).toUpperCase()+e.slice(1).toLowerCase():"").join(" "):""}getAssetReadiness(){const r=e.store?e.store.getFleet():n.fleet;if(0===r.length)return null;const t=r.reduce((e,n)=>e+(100-parseInt(n.damage,10)),0);return Math.round(t/r.length)}getNormalizedRole(){return String(r.role||"").trim().toUpperCase()}isDefaultOrg(){return!0===n.org.isDefault||"DEFAULT"===String(n.org.tag||"").trim().toUpperCase()}isOrgOwner(){const e=String(n.org.ownerUid||n.org.owner||"").trim().toLowerCase(),t=String(r.actorUid||"").trim().toLowerCase();return e&&t?t===e:String(r.actorName||"").trim().toLowerCase()===String(n.org.owner||"").trim().toLowerCase()}isSessionCeo(){return!0===r.ceo}isOrgLeaderOrCeo(){return this.isOrgOwner()||"LEADER"===this.getNormalizedRole()||this.isDefaultOrg()&&this.isSessionCeo()}canManageMembers(){return this.isOrgLeaderOrCeo()}canManageTreasury(){return this.isOrgLeaderOrCeo()}canDisbandOrg(){return this.isOrgOwner()&&!this.isDefaultOrg()}canLeaveOrg(){return!this.isDefaultOrg()&&!this.isOrgOwner()}getMemberName(e){return String(e&&"object"==typeof e?e.name||"":e||"")}getMemberUid(e){return e&&"object"==typeof e?String(e.uid||""):""}isOwnerMember(e){return this.getMemberName(e).trim().toLowerCase()===String(n.org.owner||"").trim().toLowerCase()}isCurrentMember(e){const n=this.getMemberUid(e).trim().toLowerCase(),t=String(r.actorUid||"").trim().toLowerCase();return n&&t?n===t:this.getMemberName(e).trim().toLowerCase()===String(r.actorName||"").trim().toLowerCase()}isProtectedMember(e){return this.isOwnerMember(e)||this.isCurrentMember(e)}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{portalData:n}=e.data,r=e.store,t=e.getters,a=window.RegistryApp.store;e.actions=new class{constructor(){this.treasuryNoticeTimer=null}showTreasuryNotice(e,n){r.setTreasuryNotice({type:e,text:n}),this.treasuryNoticeTimer&&clearTimeout(this.treasuryNoticeTimer),this.treasuryNoticeTimer=setTimeout(()=>{r.setTreasuryNotice({type:"",text:""}),this.treasuryNoticeTimer=null},3500)}parseAmount(e){const n=Number(e);return Number.isFinite(n)?Math.round(n):0}getInputValue(e){const n=document.getElementById(e);return n?n.value:""}closePortal(){const e=window.RegistryApp?window.RegistryApp.bridge:null;e&&"function"==typeof e.close?e.close({}):a&&a.setView("home")}openModal(e){"payroll"!==e&&"transfer"!==e&&"credit"!==e||t.canManageTreasury()?"invite"!==e||t.canManageMembers()?("disband"!==e||t.canDisbandOrg())&&("leave"!==e||t.canLeaveOrg())&&r.setModal({type:e}):this.showTreasuryNotice("error","Only the organization leader or CEO can invite players."):this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions.")}closeModal(){r.setModal(null)}toggleInviteMenu(){r.setInviteMenuOpen(!r.getInviteMenuOpen())}closeInviteMenu(){r.setInviteMenuOpen(!1)}removeMember(e){if(!t.canManageMembers())return!1;if(t.isProtectedMember(e))return!1;const n=t.getMemberUid(e),a=t.getMemberName(e);return r.setMembers(e=>e.filter(e=>n?e.uid!==n:e.name!==a)),r.setCreditLines(e=>e.filter(e=>n?e.uid!==n:e.member!==a)),!0}disbandOrganization(){if(!t.canDisbandOrg())return!1;const e=window.RegistryApp?window.RegistryApp.bridge:null;return e&&"function"==typeof e.requestDisbandOrg?(this.closeModal(),e.requestDisbandOrg(),!0):(this.showTreasuryNotice("error","Disband bridge is unavailable."),!1)}leaveOrganization(){if(!t.canLeaveOrg())return!1;const e=window.RegistryApp?window.RegistryApp.bridge:null;return e&&"function"==typeof e.requestLeaveOrg?(this.closeModal(),e.requestLeaveOrg(),!0):(this.showTreasuryNotice("error","Leave bridge is unavailable."),!1)}runPayroll(e){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;const n=r.getMembers(),a=r.getFunds();if(0===n.length)return this.showTreasuryNotice("error","No members available for payroll."),!1;if(e<=0)return this.showTreasuryNotice("error","Enter a valid payroll amount."),!1;const o=e*n.length;return o>a?(this.showTreasuryNotice("error","Insufficient org funds for payroll."),!1):(r.setFunds(a-o),this.showTreasuryNotice("success",`Payroll sent to ${n.length} members for ${t.formatCurrency(o)}.`),!0)}sendFundsToMember(e,n){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;const a=r.getFunds();return e?n<=0?(this.showTreasuryNotice("error","Enter a valid transfer amount."),!1):n>a?(this.showTreasuryNotice("error","Insufficient org funds for this transfer."),!1):(r.setFunds(a-n),this.showTreasuryNotice("success",`${t.formatCurrency(n)} sent to ${e}.`),!0):(this.showTreasuryNotice("error","Select a member to receive funds."),!1)}grantCreditLine(e,n){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;if(!e)return this.showTreasuryNotice("error","Select a member for the credit line."),!1;if(n<=0)return this.showTreasuryNotice("error","Enter a valid credit line amount."),!1;const a=r.getMembers().find(n=>t.getMemberUid(n)===e),o=a?t.getMemberName(a):"";if(!o)return this.showTreasuryNotice("error","Selected member was not found in the organization roster."),!1;const i=window.RegistryApp?window.RegistryApp.bridge:null;return i&&"function"==typeof i.requestCreditLine?i.requestCreditLine({memberUid:e,memberName:o,amount:n}):(this.showTreasuryNotice("error","Credit line bridge is unavailable."),!1)}sendInvite(e){if(!t.canManageMembers())return this.showTreasuryNotice("error","Only the organization leader or CEO can invite players."),!1;const n=r.getInviteablePlayers().find(n=>String(n.uid||"")===String(e));if(!n)return this.showTreasuryNotice("error","Select an online player to invite."),!1;const a=window.RegistryApp?window.RegistryApp.bridge:null;return a&&"function"==typeof a.requestInvitePlayer?a.requestInvitePlayer({targetUid:String(n.uid||""),targetName:String(n.name||"")}):(this.showTreasuryNotice("error","Organization invite bridge is unavailable."),!1)}acceptInvite(e){const n=window.RegistryApp?window.RegistryApp.bridge:null;return n&&"function"==typeof n.requestAcceptInvite?(this.closeInviteMenu(),n.requestAcceptInvite({orgId:e})):(this.showTreasuryNotice("error","Organization invite bridge is unavailable."),!1)}declineInvite(e){const n=window.RegistryApp?window.RegistryApp.bridge:null;return n&&"function"==typeof n.requestDeclineInvite?(this.closeInviteMenu(),n.requestDeclineInvite({orgId:e})):(this.showTreasuryNotice("error","Organization invite bridge is unavailable."),!1)}}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-navbar",o=`[${a}]`,i=`\n${o} {\n background: var(--bg-surface);\n border-bottom: 1px solid var(--border);\n box-shadow: var(--shadow);\n}\n\n${o} .app-navbar-inner {\n display: flex;\n justify-content: space-between;\n align-items: center;\n max-width: 1200px;\n width: 100%;\n margin: 0 auto;\n padding: 1rem 2rem;\n box-sizing: border-box;\n}\n\n${o} .app-navbar-brand {\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n}\n\n${o} .app-navbar-kicker {\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n font-weight: 600;\n}\n\n${o} .app-navbar-title {\n font-size: 1.25rem;\n font-weight: 700;\n color: var(--primary-hover);\n letter-spacing: -0.025em;\n}\n\n${o} .app-navbar-actions {\n display: flex;\n align-items: center;\n gap: 1.5rem;\n}\n\n${o} .app-navbar-view {\n font-size: 0.8rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--text-muted);\n font-weight: 600;\n}\n\n${o} .app-close-btn {\n background: transparent;\n color: var(--text-muted);\n border: 1px solid var(--border);\n padding: 0.5rem 1rem;\n font-size: 0.85rem;\n}\n\n${o} .app-close-btn:hover {\n background: var(--bg-surface-hover);\n color: var(--primary-hover);\n border-color: var(--primary);\n transform: none;\n box-shadow: none;\n}\n\n@media (max-width: 960px) {\n ${o} .app-navbar-inner {\n flex-direction: column;\n align-items: flex-start;\n padding: 1rem 1.5rem;\n }\n\n ${o} .app-navbar-actions {\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.Navbar=function({kicker:e="ORBIS",title:n="",viewLabel:o="",actionLabel:s="",onAction:l=null}){return t("shared-navbar",i),r("nav",{className:"app-navbar",[a]:""},r("div",{className:"app-navbar-inner"},r("div",{className:"app-navbar-brand"},r("span",{className:"app-navbar-kicker"},e),r("span",{className:"app-navbar-title"},n)),r("div",{className:"app-navbar-actions"},r("span",{className:"app-navbar-view"},o),s&&"function"==typeof l?r("button",{type:"button",className:"app-close-btn",onClick:l},s):null)))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Header=function({title:e,subtitle:n="Organization Registration & Management Portal",onTitleClick:t=null}){return r("div",{className:"header"},r("h1",{style:{cursor:t?"pointer":"default"},onClick:t},e),r("p",null,n))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.OrgPortal=window.OrgPortal||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Hero=function({className:e="",kicker:n="",title:t="",subtitle:a="",meta:o=""}){const i=["card org-panel org-span-12 org-page-header",e].filter(Boolean).join(" ");return r("section",{className:i},r("div",{className:"org-page-heading"},r("span",{className:"org-page-kicker"},n),r("h1",{className:"org-page-title"},t),r("p",{className:"org-page-subtitle"},a),r("span",{className:"org-page-meta"},o)))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Footer=function({sections:e=[]}){return r("div",{className:"footer"},r("div",{className:"wrapper"},...e.map(e=>r("div",null,r("h3",null,e.title),r("ul",{style:{listStyleType:"none",padding:0}},...(e.items||[]).map(e=>r("li",null,e)))))))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-modal",o=`[${a}]`,i=`\n${o} {\n position: fixed;\n inset: 0;\n background: rgb(15 23 42 / 0.38);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 1.5rem;\n z-index: 20;\n}\n\n${o} .app-modal-card {\n width: min(100%, 30rem);\n margin-bottom: 0;\n text-align: left;\n}\n\n${o} .app-modal-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n margin-bottom: 1rem;\n}\n\n${o} .app-modal-title {\n margin: 0;\n color: var(--primary-hover);\n font-size: 1.45rem;\n}\n\n${o} .app-modal-close {\n width: 2.25rem;\n height: 2.25rem;\n padding: 0;\n background: var(--bg-surface);\n color: var(--text-main);\n border: 1px solid var(--border);\n box-shadow: none;\n transform: none;\n}\n\n${o} .app-modal-close:hover {\n background: var(--bg-surface-hover);\n color: var(--text-main);\n box-shadow: none;\n transform: none;\n}\n\n${o} .app-modal-form {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n${o} .app-modal-form label {\n display: block;\n margin-bottom: 0.5rem;\n color: var(--text-muted);\n font-weight: 500;\n font-size: 0.9rem;\n}\n\n${o} .app-modal-form input,\n${o} .app-modal-form select {\n width: 100%;\n padding: 0.75rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: var(--bg-app);\n color: var(--text-main);\n font-family: inherit;\n font-size: 1rem;\n box-sizing: border-box;\n transition: border-color 0.2s, box-shadow 0.2s;\n}\n\n${o} .app-modal-form input:focus,\n${o} .app-modal-form select:focus {\n outline: none;\n border-color: var(--primary);\n box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12);\n}\n\n${o} .app-modal-form input:disabled,\n${o} .app-modal-form select:disabled {\n background: #f1f5f9;\n color: var(--text-muted);\n cursor: not-allowed;\n}\n\n${o} .app-modal-actions {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 0.75rem;\n margin-top: 0.5rem;\n}\n\n${o} .app-modal-actions button + button,\n${o} .app-modal-danger-actions button + button {\n margin-left: 0;\n}\n\n${o} .app-modal-danger {\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid #fecaca;\n border-radius: var(--radius);\n background: #fff1f2;\n align-items: flex-start;\n}\n\n${o} .app-modal-danger p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .app-modal-danger-actions {\n display: flex;\n flex-wrap: wrap;\n gap: 0.75rem;\n}\n\n@media (max-width: 960px) {\n ${o} .app-modal-head,\n ${o} .app-modal-danger {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.Modal=function({title:e="",body:n=null,onClose:o=null}){return t("shared-modal",i),r("div",{className:"app-modal-backdrop",[a]:"",onClick:e=>{e.target===e.currentTarget&&o&&o()}},r("div",{className:"card app-modal-card"},r("div",{className:"app-modal-head"},r("div",null,r("h2",{className:"app-modal-title"},e)),r("button",{type:"button",className:"app-modal-close",onClick:o,"aria-label":"Close dialog"},"x")),n))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-panel-card",o=`[${a}]`,i=`\n${o} {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n}\n\n${o} .org-panel-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n margin-bottom: 1.5rem;\n}\n\n${o} .org-panel-body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-height: 0;\n}\n\n${o} .org-eyebrow {\n font-size: 0.8rem;\n font-weight: 700;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-muted);\n margin-bottom: 0.4rem;\n}\n\n${o} .org-panel-title {\n margin: 0;\n color: var(--primary-hover);\n font-size: 1.45rem;\n}\n\n${o} .org-panel-subtitle {\n margin: 0.35rem 0 0;\n color: var(--text-muted);\n font-size: 0.95rem;\n}\n\n@media (max-width: 960px) {\n ${o} .org-panel-head {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.PanelCard=function({className:e="",eyebrow:n="",title:o="",subtitle:s="",headerExtras:l=null,body:d=null,rootProps:c={}}){const m=["card org-panel",e].filter(Boolean).join(" ");return t("shared-panel-card",i),r("section",{className:m,[a]:"",...c},r("div",{className:"org-panel-head"},r("div",null,n?r("div",{className:"org-eyebrow"},n):null,r("h2",{className:"org-panel-title"},o),s?r("p",{className:"org-panel-subtitle"},s):null),l),r("div",{className:"org-panel-body"},d))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-metric-card",a=`[${t}]`,o=`\n${a} {\n display: flex;\n flex-direction: column;\n gap: 0.45rem;\n padding: 1rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);\n}\n\n${a}:nth-child(4n + 2),\n${a}:nth-child(4n + 3) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(226 232 240) 100%);\n border-color: rgb(100 116 139 / 0.35);\n box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6);\n}\n\n${a} .org-metric-label {\n font-size: 0.76rem;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--text-muted);\n}\n\n${a} .org-metric-value {\n font-size: 1.8rem;\n color: var(--primary-hover);\n line-height: 1.1;\n}\n\n${a}:nth-child(4n + 2) .org-metric-value,\n${a}:nth-child(4n + 3) .org-metric-value {\n color: #334155;\n}\n\n${a} .org-metric-note {\n color: var(--text-muted);\n font-size: 0.9rem;\n}\n\n@media (max-width: 960px) {\n ${a}:nth-child(4n + 3) {\n background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);\n border-color: var(--border);\n box-shadow: none;\n }\n\n ${a}:nth-child(4n + 3) .org-metric-value {\n color: var(--primary-hover);\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.MetricCard=function(e,a,i){return r("portal-metric-card",o),n("div",{className:"org-metric-card",[t]:""},n("span",{className:"org-metric-label"},e),n("strong",{className:"org-metric-value"},a),n("span",{className:"org-metric-note"},i))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-simple-stat",a=`[${t}]`,o=`\n${a} {\n display: flex;\n flex-direction: column;\n gap: 0.2rem;\n min-width: 90px;\n}\n\n${a} .org-simple-label {\n font-size: 0.72rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${a} .org-simple-value {\n font-size: 0.95rem;\n color: var(--text-main);\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.SimpleStat=function(e,a){return r("portal-simple-stat",o),n("div",{className:"org-simple-stat",[t]:""},n("span",{className:"org-simple-label"},e),n("strong",{className:"org-simple-value"},a))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.store,o=e.getters,i="data-ui-overview-card",s=`[${i}]`,l=`\n${s} .org-hero-grid {\n display: grid;\n grid-template-columns: 1.3fr 1fr;\n gap: 1.5rem;\n align-items: start;\n}\n\n${s} .org-summary {\n margin: 0;\n font-size: 1.05rem;\n color: var(--text-main);\n}\n\n${s} .org-meta-row {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 1rem;\n margin-top: 1.5rem;\n}\n\n${s} .org-meta-item {\n display: flex;\n flex-direction: column;\n gap: 0.4rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${s} .org-meta-item:nth-child(even) {\n background: linear-gradient(180deg, rgb(241 245 249) 0%, rgb(226 232 240) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${s} .org-meta-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${s} .org-meta-value {\n font-size: 1rem;\n font-weight: 600;\n color: var(--primary-hover);\n}\n\n${s} .org-metric-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${s} .org-hero-grid,\n ${s} .org-meta-row,\n ${s} .org-metric-grid {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.OverviewCard=function(){const s=e.componentFns.MetricCard,d=window.SharedUI.componentFns.PanelCard,c=o.getAssetReadiness(),m=t.org.headquarters||"ArmA Verse",g=a.getAssets().length,p=a.getFleet().length,u=a.getFunds(),b=a.getMembers().length,f=a.getReputation();return r("portal-overview-card",l),d({className:"org-span-12",eyebrow:t.org.tag,title:"Organization Overview",rootProps:{[i]:""},body:n("div",{className:"org-hero-grid"},n("div",{className:"org-hero-copy"},n("p",{className:"org-summary"},t.org.type," operating from ",m,". Treasury, fleet status, inventory, and roster management are surfaced here first."),n("div",{className:"org-meta-row"},n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Director"),n("span",{className:"org-meta-value"},o.formatDisplayName(t.org.owner))),n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Active Members"),n("span",{className:"org-meta-value"},`${b} total`)),n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Fleet Readiness"),n("span",{className:"org-meta-value"},null===c?"N/A":`${c}%`)))),n("div",{className:"org-metric-grid"},s("Org Funds",o.formatCurrency(u),"Organization treasury balance"),s("Reputation",f,"Organization standing"),s("Asset Lines",g,"Tracked supply and equipment entries"),s("Fleet Vehicles",p,"Tracked air, ground, and naval vehicles")))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.getters,o="data-ui-fleet-card",i=`[${o}]`,s=`\n${i} .org-simple-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${i} .org-simple-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${i} .org-simple-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${i} .org-simple-name {\n color: var(--primary-hover);\n}\n\n${i} .org-simple-meta {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${i} .org-simple-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.FleetCard=function(){const t=window.SharedUI.componentFns.PanelCard,i=e.componentFns.SimpleStat,l=e.store.getFleet();return r("portal-fleet-card",s),t({className:"org-scroll-panel org-span-7",title:"Fleet",subtitle:"Individual vehicles with type, status, and overall damage.",rootProps:{[o]:""},body:n("div",{className:"org-simple-list"},...l.map(e=>n("article",{className:"org-simple-row"},n("strong",{className:"org-simple-name"},e.name),n("div",{className:"org-simple-meta"},i("Type",a.formatVehicleType(e.type)),i("Status",e.status),i("Damage",e.damage)))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r,createSignal:t}=e.runtime,{portalData:a}=e.data,o=e.store,i=e.getters,s=e.actions,l="data-ui-treasury-card",d=`[${l}]`,[c,m]=t("overview"),[g,p]=t(!1),u=`\n${d} .org-treasury-menu {\n position: relative;\n}\n\n${d} .org-menu-btn {\n width: 2.75rem;\n height: 2.75rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 1px solid var(--border);\n background: #f8fafc;\n color: var(--text-muted);\n}\n\n${d} .org-menu-btn:hover {\n color: var(--primary-hover);\n border-color: rgb(148 163 184 / 0.65);\n}\n\n${d} .org-menu-btn svg {\n width: 1.1rem;\n height: 1.1rem;\n}\n\n${d} .org-menu-dropdown {\n position: absolute;\n top: calc(100% + 0.6rem);\n right: 0;\n min-width: 10.5rem;\n padding: 0.45rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #fff;\n box-shadow: 0 12px 28px rgb(15 23 42 / 0.12);\n display: flex;\n flex-direction: column;\n gap: 0.35rem;\n z-index: 5;\n}\n\n${d} .org-menu-option + .org-menu-option {\n margin-left: 0;\n}\n\n${d} .org-menu-option {\n width: 100%;\n justify-content: flex-start;\n background: transparent;\n color: var(--text-main);\n border: 1px solid transparent;\n}\n\n${d} .org-menu-option:hover {\n background: #f8fafc;\n border-color: rgb(148 163 184 / 0.35);\n}\n\n${d} .org-menu-option.is-active {\n background: rgb(226 232 240 / 0.7);\n color: var(--primary-hover);\n border-color: rgb(148 163 184 / 0.35);\n}\n\n${d} .org-finance-meta {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n margin-bottom: 1.5rem;\n}\n\n${d} .org-finance-meta > div {\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n display: flex;\n flex-direction: column;\n gap: 0.4rem;\n}\n\n${d} .org-meta-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${d} .org-action-grid {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n margin-bottom: 1rem;\n}\n\n${d} .org-action-grid button + button {\n margin-left: 0;\n}\n\n${d} .org-action-grid button {\n width: 100%;\n}\n\n${d} .org-access-note {\n margin: 0 0 1rem;\n color: var(--text-muted);\n font-size: 0.95rem;\n}\n\n${d} .org-credit-summary {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n padding: 0.85rem 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${d} .org-credit-summary strong {\n font-size: 1rem;\n}\n\n${d} .org-credit-summary span:last-child {\n font-size: 0.92rem;\n line-height: 1.45;\n}\n\n${d} .org-credit-lines-list {\n display: flex;\n flex-direction: column;\n gap: 0.85rem;\n}\n\n${d} .org-treasury-body {\n display: flex;\n flex: 1;\n flex-direction: column;\n gap: 1rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${d} .org-credit-line-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${d} .org-credit-line-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${d} .org-credit-line-member {\n display: flex;\n flex-direction: column;\n gap: 0.3rem;\n}\n\n${d} .org-credit-line-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${d} .org-credit-line-empty {\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n color: var(--text-muted);\n}\n\n@media (max-width: 960px) {\n ${d} .org-finance-meta {\n grid-template-columns: 1fr;\n }\n\n ${d} .org-credit-line-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.TreasuryCard=function(){const e=window.SharedUI.componentFns.PanelCard,t=o.getCreditLines(),a=o.getReputation(),d=i.canManageTreasury(),b=c(),f=g(),v=t.reduce((e,n)=>e+Number(n.availableAmount||n.amount||0),0),h=t.reduce((e,n)=>e+Number(n.amountDue||0),0),w=1===t.length?"1 active credit line":`${t.length} active credit lines`;return r("portal-treasury-card",u),e({className:"org-scroll-panel org-span-5",title:"Treasury",subtitle:"Organization funds, reputation and payouts.",headerExtras:n("div",{className:"org-treasury-menu"},n("button",{type:"button",className:"org-menu-btn",title:"Treasury views","aria-label":"Treasury views",onClick:()=>p(e=>!e)},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},n("line",{x1:"4",y1:"7",x2:"20",y2:"7"}),n("line",{x1:"4",y1:"12",x2:"20",y2:"12"}),n("line",{x1:"4",y1:"17",x2:"20",y2:"17"}))),f?n("div",{className:"org-menu-dropdown"},n("button",{type:"button",className:"overview"===b?"org-menu-option is-active":"org-menu-option",onClick:()=>{m("overview"),p(!1)}},"Overview"),n("button",{type:"button",className:"credit"===b?"org-menu-option is-active":"org-menu-option",onClick:()=>{m("credit"),p(!1)}},"Credit Lines")):null),rootProps:{[l]:""},body:n("div",{className:"org-treasury-body"},"credit"===b?t.length>0?n("div",{className:"org-credit-lines-list"},...t.map(e=>n("article",{className:"org-credit-line-row"},n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Member"),n("strong",null,e.member)),n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Available"),n("strong",null,i.formatCurrency(e.availableAmount||e.amount))),n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Amount Due"),n("strong",null,i.formatCurrency(e.amountDue))),n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Interest"),n("strong",null,`${Math.round(100*Number(e.interestRate||0))}%`))))):n("div",{className:"org-credit-line-empty"},"No active credit lines."):n("div",null,n("div",{className:"org-finance-meta"},n("div",null,n("span",{className:"org-meta-label"},"Funds"),n("strong",null,i.formatCurrency(o.getFunds()))),n("div",null,n("span",{className:"org-meta-label"},"Reputation"),n("strong",null,`${a}`)),n("div",null,n("span",{className:"org-meta-label"},"Reserved Credit"),n("strong",null,i.formatCurrency(v))),n("div",null,n("span",{className:"org-meta-label"},"Outstanding Due"),n("strong",null,i.formatCurrency(h)))),d?n("div",{className:"org-action-grid"},n("button",{type:"button",onClick:()=>s.openModal("payroll")},"Run Payroll"),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>s.openModal("transfer")},"Send Funds"),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>s.openModal("credit")},"Credit Line")):n("p",{className:"org-access-note"},"Only the organization leader or CEO can manage treasury actions."),n("div",{className:"org-credit-summary"},n("span",{className:"org-meta-label"},"Credit Line Status"),n("strong",null,w),n("span",null,t.length>0?"Open the Credit Lines tab to review reserved balances, due amounts, and member exposure.":"Assign a credit line to create the first approved member limit."))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.getters,o="data-ui-assets-card",i=`[${o}]`,s=`\n${i} .org-simple-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${i} .org-simple-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${i} .org-simple-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${i} .org-simple-name {\n color: var(--primary-hover);\n}\n\n${i} .org-simple-meta {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${i} .org-simple-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.AssetsCard=function(){const t=window.SharedUI.componentFns.PanelCard,i=e.componentFns.SimpleStat,l=e.store.getAssets();return r("portal-assets-card",s),t({className:"org-scroll-panel org-span-7",title:"Assets",subtitle:"Inventory supplies and equipment with quantity totals.",rootProps:{[o]:""},body:n("div",{className:"org-simple-list"},...l.map(e=>n("article",{className:"org-simple-row"},n("strong",{className:"org-simple-name"},e.name),n("div",{className:"org-simple-meta"},i("Type",a.formatAssetType(e.type)),i("Quantity",e.quantity)))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.getters,o=e.actions,i="data-ui-members-card",s=`[${i}]`,l=`\n${s} .org-name-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${s} .org-members-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n margin-bottom: 1rem;\n position: relative;\n}\n\n${s} .org-members-copy {\n display: flex;\n flex-direction: column;\n gap: 0.35rem;\n}\n\n${s} .org-members-kicker {\n margin: 0;\n font-size: 0.85rem;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--text-muted);\n}\n\n${s} .org-members-subtitle {\n margin: 0;\n font-size: 0.9rem;\n color: var(--text-muted);\n}\n\n${s} .org-members-tools {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n margin-left: auto;\n}\n\n${s} .org-tool-btn {\n position: relative;\n width: 2.4rem;\n height: 2.4rem;\n padding: 0;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n${s} .org-tool-badge {\n position: absolute;\n top: -0.25rem;\n right: -0.25rem;\n min-width: 1.1rem;\n height: 1.1rem;\n padding: 0 0.2rem;\n border-radius: 999px;\n background: #b91c1c;\n color: white;\n font-size: 0.68rem;\n font-weight: 700;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n${s} .org-invite-menu {\n position: absolute;\n top: calc(100% + 0.5rem);\n right: 0;\n width: min(24rem, 100%);\n max-height: 22rem;\n overflow: auto;\n padding: 0.75rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: white;\n box-shadow: 0 18px 45px rgb(15 23 42 / 0.18);\n z-index: 4;\n}\n\n${s} .org-invite-menu-head {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n margin-bottom: 0.75rem;\n}\n\n${s} .org-invite-menu-title {\n margin: 0;\n font-size: 0.85rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${s} .org-invite-menu-list {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n${s} .org-invite-row,\n${s} .org-name-row {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${s} .org-name-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${s} .org-name-row button {\n margin-left: auto;\n}\n\n${s} .org-name-copy {\n display: flex;\n flex-direction: column;\n gap: 0.2rem;\n}\n\n${s} .org-name-meta {\n font-size: 0.8rem;\n color: var(--text-muted);\n}\n\n${s} .org-inline-actions,\n${s} .org-invite-actions {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n margin-left: auto;\n}\n\n${s} .org-members-empty {\n margin: 0;\n font-size: 0.9rem;\n color: var(--text-muted);\n}\n\n@media (max-width: 960px) {\n ${s} .org-members-head {\n flex-direction: column;\n align-items: flex-start;\n }\n\n ${s} .org-members-tools {\n margin-left: 0;\n }\n\n ${s} .org-invite-menu {\n left: 0;\n right: auto;\n width: 100%;\n }\n\n ${s} .org-name-row,\n ${s} .org-invite-row {\n flex-direction: column;\n align-items: flex-start;\n }\n\n ${s} .org-name-row button,\n ${s} .org-inline-actions,\n ${s} .org-invite-actions {\n margin-left: 0;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.MembersCard=function(){const e=window.SharedUI.componentFns.PanelCard,s=t.getMembers(),d=t.getPendingInvites(),c=t.getInviteMenuOpen(),m=a.canManageMembers();return r("portal-members-card",l),e({className:"org-scroll-panel org-span-5",title:"Members",subtitle:"Current roster listing. The organization owner and your own member entry cannot be removed.",rootProps:{[i]:""},body:n("div",{className:"org-name-list"},n("div",{className:"org-members-head"},n("div",{className:"org-members-copy"},n("h4",{className:"org-members-kicker"},"Roster"),n("p",{className:"org-members-subtitle"},"Manage membership and review incoming organization invites.")),n("div",{className:"org-members-tools"},n("button",{type:"button",className:"org-secondary-btn org-icon-btn org-tool-btn",title:"Pending invitations","aria-label":"Pending invitations",onClick:()=>o.toggleInviteMenu()},n("svg",{className:"org-icon",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},n("path",{d:"M15 17h5l-1.4-1.4A2 2 0 0 1 18 14.2V11a6 6 0 1 0-12 0v3.2a2 2 0 0 1-.6 1.4L4 17h5"}),n("path",{d:"M9.73 21a2 2 0 0 0 4.54 0"})),d.length>0?n("span",{className:"org-tool-badge"},String(d.length)):null),m?n("button",{type:"button",className:"org-secondary-btn org-icon-btn org-tool-btn",title:"Invite player","aria-label":"Invite player",onClick:()=>o.openModal("invite")},n("svg",{className:"org-icon",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},n("path",{d:"M12 5v14"}),n("path",{d:"M5 12h14"}))):null,c?n("div",{className:"org-invite-menu"},n("div",{className:"org-invite-menu-head"},n("h4",{className:"org-invite-menu-title"},"Pending Invites"),n("button",{type:"button",className:"org-secondary-btn org-icon-btn org-tool-btn",title:"Close invites","aria-label":"Close invites",onClick:()=>o.closeInviteMenu()},n("svg",{className:"org-icon",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},n("path",{d:"M18 6 6 18"}),n("path",{d:"m6 6 12 12"})))),0===d.length?n("p",{className:"org-members-empty"},"No incoming organization invites."):n("div",{className:"org-invite-menu-list"},...d.map(e=>n("article",{className:"org-invite-row"},n("div",{className:"org-name-copy"},n("strong",null,e.orgName||"Unknown Organization"),n("span",{className:"org-name-meta"},"Invited by ",e.inviterName||"Unknown")),n("div",{className:"org-invite-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>o.declineInvite(String(e.orgId||""))},"Decline"),n("button",{type:"button",onClick:()=>o.acceptInvite(String(e.orgId||""))},"Accept")))))):null)),...s.map(e=>{const r=m&&!a.isProtectedMember(e);return n("article",{className:"org-name-row"},n("div",{className:"org-name-copy"},n("strong",null,e.name),e.uid?n("span",{className:"org-name-meta"},e.uid):null),r?n("button",{type:"button",className:"org-danger-btn org-icon-btn",title:`Remove ${e.name}`,"aria-label":`Remove ${e.name}`,onClick:()=>o.removeMember(e)},n("svg",{className:"org-icon",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},n("path",{d:"M9 3h6"}),n("path",{d:"M4 7h16"}),n("path",{d:"M6 7l1 13h10l1-13"}),n("path",{d:"M10 11v6"}),n("path",{d:"M14 11v6"}))):null)}))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a="data-ui-activity-card",o=`[${a}]`,i=`\n${o} .org-activity-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${o} .org-activity-row {\n padding: 1rem;\n border: 1px solid var(--border);\n border-left: 3px solid #94a3b8;\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${o} .org-activity-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n border-left-color: #64748b;\n}\n\n${o} .org-activity-row p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .org-activity-time {\n display: inline-block;\n margin-bottom: 0.35rem;\n color: var(--text-muted);\n font-size: 0.8rem;\n font-weight: 700;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.ActivityCard=function(){const t=window.SharedUI.componentFns.PanelCard,o=e.store.getActivity();return r("portal-activity-card",i),t({className:"org-scroll-panel org-span-6",title:"Command Feed",subtitle:"Recent organization-level actions and updates.",rootProps:{[a]:""},body:n("div",{className:"org-activity-list"},...o.map(e=>n("article",{className:"org-activity-row"},n("span",{className:"org-activity-time"},e.time),n("p",null,e.text))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-future-card",a=[{name:"Contracts Board",status:"Planned",detail:"Track payouts, assignments, and claim approvals."},{name:"Diplomacy",status:"Future Review",detail:"Possible future module pending a full design and scope review."},{name:"Logistics Queue",status:"Future Review",detail:"Possible future module pending a full design and scope review."},{name:"Permissions",status:"Future Review",detail:"Possible future module pending a full design and scope review."}],o=`[${t}]`,i=`\n${o} .org-roadmap-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${o} .org-roadmap-card {\n padding: 1rem;\n display: flex;\n flex-direction: column;\n gap: 0.7rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${o} .org-roadmap-card:nth-child(4n + 2),\n${o} .org-roadmap-card:nth-child(4n + 3) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(100 116 139 / 0.4);\n}\n\n${o} .org-roadmap-card p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .org-list-tag {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0.2rem 0.55rem;\n border-radius: 999px;\n font-size: 0.72rem;\n font-weight: 700;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n background: #e2e8f0;\n color: var(--primary-hover);\n}\n\n${o} .org-roadmap-card:nth-child(4n + 2) .org-list-tag,\n${o} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {\n background: #cbd5e1;\n color: #1e293b;\n}\n\n@media (max-width: 960px) {\n ${o} .org-roadmap-grid {\n grid-template-columns: 1fr;\n }\n\n ${o} .org-roadmap-card:nth-child(4n + 3) {\n background: #f8fafc;\n border-color: var(--border);\n }\n\n ${o} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {\n background: #e2e8f0;\n color: var(--primary-hover);\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.FutureCard=function(){const e=window.SharedUI.componentFns.PanelCard;return r("portal-future-card",i),e({className:"org-scroll-panel org-span-6",title:"Expansion Slots",subtitle:"Potential modules are tagged by status such as Planned, In Design, In Review, and Future Review.",rootProps:{[t]:""},body:n("div",{className:"org-roadmap-grid"},...a.map(e=>n("article",{className:"org-roadmap-card"},n("span",{className:"org-list-tag"},e.status),n("strong",null,e.name),n("p",null,e.detail))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.getters,a=e.actions,o="data-ui-danger-card",i=`[${o}]`,s=`\n${i} {\n border-color: #fecaca;\n background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%);\n}\n\n${i} .org-danger-copy {\n margin-bottom: 1rem;\n}\n\n${i} .org-danger-copy strong,\n${i} .org-danger-copy p {\n display: block;\n}\n\n${i} .org-danger-copy p {\n margin: 0.4rem 0 0;\n color: var(--text-muted);\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.DangerCard=function(){const e=window.SharedUI.componentFns.PanelCard;return r("portal-danger-card",s),t.canDisbandOrg()?e({className:"org-span-12 org-danger-panel",title:"Organization Controls",subtitle:"Leader-only actions for membership and permanent organization removal.",rootProps:{[o]:""},body:n("div",null,n("div",{className:"org-danger-copy"},n("strong",null,"Disband organization"),n("p",null,"This removes the organization and revokes access to the portal for all members.")),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.openModal("disband")},"Disband Organization"))}):null}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n}=e.runtime,{portalData:r}=e.data,t=e.store,a=e.actions;e.componentFns=e.componentFns||{},e.componentFns.ModalLayer=function(){const e=window.SharedUI.componentFns.Modal,o=t.getModal();if(!o)return null;const i=t.getMembers(),s=t.getInviteablePlayers(),l=0===i.length?{disabled:!0}:{},d=0===s.length?{disabled:!0}:{};let c="",m=null;return"payroll"===o.type?(c="Run Payroll",m=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Amount Per Member"),n("input",{id:"treasury-payroll-amount",type:"number",min:"1",placeholder:"500",autofocus:"true"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",onClick:()=>{a.runPayroll(a.parseAmount(a.getInputValue("treasury-payroll-amount")))&&a.closeModal()}},"Run Payroll")))):"transfer"===o.type?(c="Send Funds",m=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Member"),n("select",{id:"treasury-transfer-member",...l},...i.map(e=>n("option",{value:e.name},e.name)))),n("div",null,n("label",null,"Amount"),n("input",{id:"treasury-transfer-amount",type:"number",min:"1",placeholder:"1500"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",...l,onClick:()=>{a.sendFundsToMember(String(a.getInputValue("treasury-transfer-member")||""),a.parseAmount(a.getInputValue("treasury-transfer-amount")))&&a.closeModal()}},"Send Funds")))):"credit"===o.type?(c="Assign Credit Line",m=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Member"),n("select",{id:"treasury-credit-member",...l},...i.map(e=>n("option",{value:e.uid},e.name)))),n("div",null,n("label",null,"Credit Amount"),n("input",{id:"treasury-credit-amount",type:"number",min:"1",placeholder:"5000"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",...l,onClick:()=>{a.grantCreditLine(String(a.getInputValue("treasury-credit-member")||""),a.parseAmount(a.getInputValue("treasury-credit-amount")))&&a.closeModal()}},"Assign Credit Line")))):"invite"===o.type?(c="Invite Player",m=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Online Player"),n("select",{id:"org-invite-player",...d},...s.map(e=>n("option",{value:e.uid},e.name||e.uid)))),0===s.length?n("p",null,"No eligible online players are currently available for invites."):null,n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",...d,onClick:()=>{a.sendInvite(String(a.getInputValue("org-invite-player")||""))&&a.closeModal()}},"Send Invite")))):"disband"===o.type?(c="Disband Organization",m=n("div",{className:"app-modal-danger"},n("p",null,"This action is permanent. Disband ",r.org.name,"?"),n("div",{className:"app-modal-danger-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.disbandOrganization()},"Confirm Disband")))):"leave"===o.type&&(c="Leave Organization",m=n("div",{className:"app-modal-danger"},n("p",null,"Leave ",r.org.name," and return to the default organization?"),n("div",{className:"app-modal-danger-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.leaveOrganization()},"Confirm Leave")))),e({title:c,body:m,onClose:()=>a.closeModal()})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n}=e.runtime,{portalData:r}=e.data,t=window.RegistryApp.store;e.componentFns=e.componentFns||{},e.componentFns.DisbandedView=function(){return(0,window.SharedUI.componentFns.PanelCard)({className:"org-span-12 org-empty-state",eyebrow:"Organization Removed",title:r.org.name,body:n("div",null,n("p",{className:"org-summary"},"This organization has been disbanded. Member access, assets, and fleet management are no longer available from this portal preview."),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>t.setView("home")},"Return to Registry"))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t,session:a}=e.data,o=e.store,i="[data-ui-portal-view]";r("portal-view",`\n ${i} {\n --org-row-card-max-height: 36rem;\n }\n\n ${i} .org-toast-stack {\n position: fixed;\n top: 1.5rem;\n right: 2rem;\n z-index: 20;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n pointer-events: none;\n }\n\n ${i} .org-toast {\n max-width: 24rem;\n padding: 0.9rem 1rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: #fff;\n box-shadow: 0 12px 28px rgb(15 23 42 / 0.14);\n font-size: 0.92rem;\n pointer-events: auto;\n }\n\n ${i} .org-toast.is-success {\n background: #ecfdf5;\n border-color: #bbf7d0;\n color: #166534;\n }\n\n ${i} .org-toast.is-error {\n background: #fef2f2;\n border-color: #fecaca;\n color: #991b1b;\n }\n\n ${i} .org-dashboard-grid {\n display: grid;\n grid-template-columns: repeat(12, minmax(0, 1fr));\n gap: 1.5rem;\n align-items: stretch;\n }\n\n ${i} .org-panel {\n margin-bottom: 0;\n text-align: left;\n }\n\n ${i} .org-scroll-panel {\n display: flex;\n flex-direction: column;\n min-height: 0;\n max-height: var(--org-row-card-max-height);\n overflow: hidden;\n }\n\n ${i} .org-island-root {\n display: flex;\n align-self: stretch;\n min-height: 0;\n min-width: 0;\n }\n\n ${i} .org-island-root > .org-panel {\n height: 100%;\n width: 100%;\n }\n\n ${i} .org-span-12 {\n grid-column: span 12;\n }\n\n ${i} .org-span-7 {\n grid-column: span 7;\n }\n\n ${i} .org-span-6 {\n grid-column: span 6;\n }\n\n ${i} .org-span-5 {\n grid-column: span 5;\n }\n\n @media (max-width: 960px) {\n ${i} .org-toast-stack {\n top: 1rem;\n right: 1rem;\n left: 1rem;\n }\n\n ${i} .org-toast {\n max-width: none;\n }\n\n ${i} .org-span-12,\n ${i} .org-span-7,\n ${i} .org-span-6,\n ${i} .org-span-5 {\n grid-column: span 12;\n }\n\n ${i} .org-scroll-panel {\n max-height: none;\n }\n\n }\n `),e.components=e.components||{},e.componentFns=e.componentFns||{},e.componentFns.TreasuryNoticeLayer=function(){const e=o.getTreasuryNotice();return e.text?n("div",{className:"org-toast-stack"},n("div",{className:"error"===e.type?"org-toast is-error":"org-toast is-success"},e.text)):null},e.components.App=function(){const r=window.SharedUI.componentFns.Hero,i=window.SharedUI.componentFns.Footer,s=e.componentFns.FutureCard,l=e.componentFns.DangerCard,d=e.componentFns.DisbandedView,c=[{title:"Organization Controls",items:["Roster Management","Fleet Assignment","Treasury Permissions","Asset Registry"]},{title:"Planned Extensions",items:["Contracts Board","Diplomacy Layer","Procurement Queue","Reputation History"]}];return o.getOrgDisbanded()?n("main",{"data-ui-portal-view":""},n("div",{className:"container"},n("div",{className:"org-dashboard-grid"},r({kicker:t.org.tag,title:t.org.name,subtitle:"Player organization command portal",meta:`${a.actorName} - ${a.role}`}),d())),n("div",{id:"org-portal-modal-root"}),i({sections:c})):n("main",{"data-ui-portal-view":""},n("div",{id:"org-portal-toast-root"}),n("div",{className:"container"},n("div",{className:"org-dashboard-grid"},r({kicker:t.org.tag,title:t.org.name,subtitle:"Player organization command portal",meta:`${a.actorName} - ${a.role}`}),n("div",{className:"org-island-root org-span-12",id:"org-overview-card-root"}),n("div",{className:"org-island-root org-span-7",id:"org-fleet-card-root"}),n("div",{className:"org-island-root org-span-5",id:"org-treasury-card-root"}),n("div",{className:"org-island-root org-span-5",id:"org-members-card-root"}),n("div",{className:"org-island-root org-span-7",id:"org-assets-card-root"}),n("div",{className:"org-island-root org-span-6",id:"org-activity-card-root"}),s(),l())),n("div",{id:"org-portal-modal-root"}),i({sections:c}))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.bridge,o="data-ui-registration-view",i=`[${o}]`,s=`\n${i} {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2rem;\n align-items: center;\n width: 100%;\n}\n\n${i} .info-panel {\n text-align: left;\n padding: 1rem;\n}\n\n${i} .create-feature-list {\n text-align: left;\n margin-top: 1.5rem;\n list-style-type: none;\n padding: 0;\n}\n\n${i} .create-feature-item {\n margin-bottom: 0.5rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n${i} .create-feature-icon {\n width: 1.2rem;\n height: 1.2rem;\n flex-shrink: 0;\n}\n\n${i} .price-tag {\n margin-top: 2rem;\n padding: 1rem;\n background: var(--bg-app);\n border-radius: var(--radius);\n border: 1px solid var(--border);\n}\n\n${i} .price-label {\n display: block;\n font-size: 0.9rem;\n color: var(--text-muted);\n}\n\n${i} .price-value {\n display: block;\n font-size: 2rem;\n font-weight: 700;\n color: var(--primary);\n}\n\n${i} .form-panel {\n margin: 0;\n}\n\n${i} .app-form {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n text-align: left;\n}\n\n${i} .app-form label {\n display: block;\n margin-bottom: 0.5rem;\n color: var(--text-muted);\n font-weight: 500;\n font-size: 0.9rem;\n}\n\n${i} .app-form input,\n${i} .app-form select {\n width: 100%;\n padding: 0.75rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: var(--bg-app);\n color: var(--text-main);\n font-family: inherit;\n font-size: 1rem;\n box-sizing: border-box;\n transition: border-color 0.2s;\n}\n\n${i} .app-form input:focus,\n${i} .app-form select:focus {\n outline: none;\n border-color: var(--primary);\n box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1);\n}\n\n${i} .form-actions {\n margin-top: 1rem;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n align-items: center;\n}\n\n${i} .submit-btn {\n width: 100%;\n}\n\n${i} .cancel-link {\n font-size: 0.9rem;\n color: var(--text-muted);\n cursor: pointer;\n text-decoration: underline;\n}\n\n${i} .cancel-link:hover {\n color: var(--primary);\n}\n\n${i} .form-feedback {\n padding: 0.85rem 1rem;\n border-radius: var(--radius);\n font-size: 0.92rem;\n}\n\n${i} .form-feedback.is-error {\n background: #fef2f2;\n border: 1px solid #fecaca;\n color: #991b1b;\n}\n\n@media (max-width: 960px) {\n ${i} {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.RegistrationView=function(){const e=t.getIsCreating(),i=t.getCreateError();r("main-registration-view",s);return n("div",{className:"split-container",[o]:""},n("div",{className:"info-panel"},n("h2",null,"Registration Details"),n("p",null,"Complete the form to add your organization to the Global Organization Registry."),n("ul",{className:"create-feature-list"},n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Official Organization Designator"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Secure Comms Channel"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Deployment Roster Access"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"After-Action Report Tools")),n("div",{className:"price-tag"},n("span",{className:"price-label"},"Registration Fee"),n("span",{className:"price-value"},"$50,000"))),n("div",{className:"form-panel card"},n("h2",null,"Organization Registration"),n("div",{className:"app-form"},n("div",null,n("label",null,"Organization Name"),n("input",{id:"org-create-name",type:"text",placeholder:"e.g. Task Force 141"})),n("div",null,n("label",null,"Organization Type"),n("select",{id:"org-create-type"},n("option",{value:"infantry"},"Infantry / Milsim"),n("option",{value:"aviation"},"Aviation Wing"),n("option",{value:"pmc"},"Private Military Company"),n("option",{value:"support"},"Logistics & Support"))),n("div",{className:"form-actions"},i?n("div",{className:"form-feedback is-error"},i):null,n("button",{type:"button",className:"submit-btn",disabled:e,onClick:()=>{const e={orgName:String(document.getElementById("org-create-name")?.value||"").trim(),type:String(document.getElementById("org-create-type")?.value||"")};a&&"function"==typeof a.requestCreateOrg?a.requestCreateOrg(e):t.failCreate("Registration bridge is not available.")}},e?"Submitting Registration...":"Submit Registration"),n("span",{className:"cancel-link",onClick:()=>t.setView("home")},"Cancel / Return to Main")))))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.bridge,o="data-ui-home-view",i=`[${o}]`,s=`\n${i} {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2rem;\n margin-bottom: 2rem;\n}\n\n${i} .home-feedback {\n padding: 0.85rem 1rem;\n border-radius: var(--radius);\n font-size: 0.92rem;\n background: #fef2f2;\n border: 1px solid #fecaca;\n color: #991b1b;\n}\n\n@media (max-width: 960px) {\n ${i} {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.HomeView=function(){const e=t.getIsAuthenticating(),i=t.getLoginError();return r("main-home-view",s),n("div",{className:"content",[o]:""},n("div",{className:"card"},n("h2",null,"Create Organization"),n("p",null,"Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly."),n("button",{onClick:()=>t.setView("create")},"Register")),n("div",{className:"card"},n("h2",null,"Organization Portal"),n("p",null,"Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink."),i?n("div",{className:"home-feedback"},i):null,n("button",{disabled:e,onClick:()=>{a?a.requestLogin({}):t.failLogin("Login bridge is not available.")}},e?"Opening Portal...":"Login")))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n}=e.runtime,r=e.store;e.components=e.components||{},e.components.App=function(){const t=window.SharedUI.componentFns.Navbar,a=window.SharedUI.componentFns.Header,o=window.SharedUI.componentFns.Footer,i=window.SharedUI.componentFns.WindowTitleBar,s=e.componentFns.HomeView,l=e.componentFns.RegistrationView,d=window.OrgPortal&&window.OrgPortal.components?window.OrgPortal.components.App:null,c=r.getView(),m=window.OrgPortal&&window.OrgPortal.getters?window.OrgPortal.getters:null,g=window.OrgPortal&&window.OrgPortal.actions?window.OrgPortal.actions:null,p="create"===c?"Organization Registration":"portal"===c?"Organization Portal":"Entry Hub";function u(){e.bridge&&"function"==typeof e.bridge.close?e.bridge.close({}):r.setView("home")}if("portal"===c&&d){const e=m&&"function"==typeof m.canLeaveOrg&&m.canLeaveOrg();return n("div",{className:"app-shell"},i({kicker:"FORGE ORBIS",title:"Global Organization Network",onClose:u,closeLabel:"Close organization interface"}),t({title:"Global Organization Network",viewLabel:p,actionLabel:e?"Leave Organization":"",onAction:e&&g&&"function"==typeof g.openModal?()=>g.openModal("leave"):null}),n("div",{id:"org-portal-frame-root"}))}let b;return"home"===c?b=s():"create"===c&&(b=l()),n("div",{className:"app-shell"},i({kicker:"FORGE ORBIS",title:"Global Organization Network",onClose:u,closeLabel:"Close organization interface"}),n("main",null,t({title:"Global Organization Network",viewLabel:p}),n("div",{className:"container"},a({title:"Global Organization Network",onTitleClick:()=>r.setView("home")}),b),o({sections:[{title:"Registry Resources",items:["Registration Guidelines","Tax & Fee Schedule","Legal Compliance","Trademark Database"]},{title:"Bureau Support",items:["Office: Sector 7 Admin Block","Hours: 0800 - 1600 (GST)","Helpdesk: 555-01-REGISTRY","support@org-bureau.gov"]}]})))}}(),function(){const e=window.ForgeWebUI,n=window.RegistryApp,r=window.OrgPortal,t=[{id:"org-portal-frame-root",preserveScroll:!0,render:()=>r.components.App()},{id:"org-portal-toast-root",preserveScroll:!1,render:()=>r.componentFns.TreasuryNoticeLayer()},{id:"org-overview-card-root",preserveScroll:!1,render:()=>r.componentFns.OverviewCard()},{id:"org-fleet-card-root",preserveScroll:!0,render:()=>r.componentFns.FleetCard()},{id:"org-treasury-card-root",preserveScroll:!1,render:()=>r.componentFns.TreasuryCard()},{id:"org-members-card-root",preserveScroll:!0,render:()=>r.componentFns.MembersCard()},{id:"org-assets-card-root",preserveScroll:!0,render:()=>r.componentFns.AssetsCard()},{id:"org-activity-card-root",preserveScroll:!0,render:()=>r.componentFns.ActivityCard()},{id:"org-portal-modal-root",preserveScroll:!1,render:()=>r.componentFns.ModalLayer()}];e.createApp({name:"org",root:"#app",setup({root:r}){const a=function(){const n=new Map;return{sync:function(){t.forEach(r=>{const t=document.getElementById(r.id),a=n.get(r.id);if(!t)return void(a&&(a.handle.dispose(),n.delete(r.id)));if(a&&a.container===t)return;a&&a.handle.dispose();const o=e.mount(t,r.render,{preserveScroll:r.preserveScroll});n.set(r.id,{container:t,handle:o})})}}}();e.mount(r,()=>n.components.App(),{preserveScroll:!1}),n.bridge.ready({loaded:!0}),e.effect(()=>{n.store.getView(),requestAnimationFrame(()=>{a.sync()})})}}).start()}();
\ No newline at end of file
diff --git a/arma/client/addons/org/ui/src/bridge.js b/arma/client/addons/org/ui/src/bridge.js
index d5fab25..5ae91ab 100644
--- a/arma/client/addons/org/ui/src/bridge.js
+++ b/arma/client/addons/org/ui/src/bridge.js
@@ -80,6 +80,57 @@
return false;
}
+ function requestInvitePlayer(payload) {
+ const sent = sendEvent("org::invite::request", payload);
+ if (sent) {
+ return true;
+ }
+
+ const OrgPortal = window.OrgPortal;
+ if (OrgPortal && OrgPortal.actions) {
+ OrgPortal.actions.showTreasuryNotice(
+ "error",
+ "Arma organization invite bridge is unavailable.",
+ );
+ }
+
+ return false;
+ }
+
+ function requestAcceptInvite(payload) {
+ const sent = sendEvent("org::invite::accept", payload);
+ if (sent) {
+ return true;
+ }
+
+ const OrgPortal = window.OrgPortal;
+ if (OrgPortal && OrgPortal.actions) {
+ OrgPortal.actions.showTreasuryNotice(
+ "error",
+ "Arma organization invite bridge is unavailable.",
+ );
+ }
+
+ return false;
+ }
+
+ function requestDeclineInvite(payload) {
+ const sent = sendEvent("org::invite::decline", payload);
+ if (sent) {
+ return true;
+ }
+
+ const OrgPortal = window.OrgPortal;
+ if (OrgPortal && OrgPortal.actions) {
+ OrgPortal.actions.showTreasuryNotice(
+ "error",
+ "Arma organization invite bridge is unavailable.",
+ );
+ }
+
+ return false;
+ }
+
bridge.on("org::login::success", (payloadData) => {
store.completeLogin(payloadData);
});
@@ -128,6 +179,50 @@
}
});
+ bridge.on("org::invite::success", (payloadData) => {
+ const OrgPortal = window.OrgPortal;
+ if (OrgPortal && OrgPortal.store) {
+ OrgPortal.store.setModal(null);
+ }
+
+ if (OrgPortal && OrgPortal.actions) {
+ OrgPortal.actions.showTreasuryNotice(
+ "success",
+ payloadData.message || "Organization invite sent.",
+ );
+ }
+ });
+
+ bridge.on("org::invite::failure", (payloadData) => {
+ const OrgPortal = window.OrgPortal;
+ if (OrgPortal && OrgPortal.actions) {
+ OrgPortal.actions.showTreasuryNotice(
+ "error",
+ payloadData.message || "Unable to send organization invite.",
+ );
+ }
+ });
+
+ bridge.on("org::invite::decision::success", (payloadData) => {
+ const OrgPortal = window.OrgPortal;
+ if (OrgPortal && OrgPortal.actions) {
+ OrgPortal.actions.showTreasuryNotice(
+ "success",
+ payloadData.message || "Organization invite updated.",
+ );
+ }
+ });
+
+ bridge.on("org::invite::decision::failure", (payloadData) => {
+ const OrgPortal = window.OrgPortal;
+ if (OrgPortal && OrgPortal.actions) {
+ OrgPortal.actions.showTreasuryNotice(
+ "error",
+ payloadData.message || "Unable to update organization invite.",
+ );
+ }
+ });
+
bridge.on("org::member::creditUpdated", (payloadData) => {
const OrgPortal = window.OrgPortal;
if (!OrgPortal || !OrgPortal.store) {
@@ -234,6 +329,9 @@
requestDisbandOrg,
requestLeaveOrg,
requestCreditLine,
+ requestInvitePlayer,
+ requestAcceptInvite,
+ requestDeclineInvite,
sendEvent,
};
})();
diff --git a/arma/client/addons/org/ui/src/components/portal/membersCard.js b/arma/client/addons/org/ui/src/components/portal/membersCard.js
index 6108286..14c3f72 100644
--- a/arma/client/addons/org/ui/src/components/portal/membersCard.js
+++ b/arma/client/addons/org/ui/src/components/portal/membersCard.js
@@ -19,6 +19,107 @@ ${scopeSelector} .org-name-list {
scrollbar-color: #94a3b8 #e2e8f0;
}
+${scopeSelector} .org-members-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ margin-bottom: 1rem;
+ position: relative;
+}
+
+${scopeSelector} .org-members-copy {
+ display: flex;
+ flex-direction: column;
+ gap: 0.35rem;
+}
+
+${scopeSelector} .org-members-kicker {
+ margin: 0;
+ font-size: 0.85rem;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--text-muted);
+}
+
+${scopeSelector} .org-members-subtitle {
+ margin: 0;
+ font-size: 0.9rem;
+ color: var(--text-muted);
+}
+
+${scopeSelector} .org-members-tools {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-left: auto;
+}
+
+${scopeSelector} .org-tool-btn {
+ position: relative;
+ width: 2.4rem;
+ height: 2.4rem;
+ padding: 0;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+${scopeSelector} .org-tool-badge {
+ position: absolute;
+ top: -0.25rem;
+ right: -0.25rem;
+ min-width: 1.1rem;
+ height: 1.1rem;
+ padding: 0 0.2rem;
+ border-radius: 999px;
+ background: #b91c1c;
+ color: white;
+ font-size: 0.68rem;
+ font-weight: 700;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+}
+
+${scopeSelector} .org-invite-menu {
+ position: absolute;
+ top: calc(100% + 0.5rem);
+ right: 0;
+ width: min(24rem, 100%);
+ max-height: 22rem;
+ overflow: auto;
+ padding: 0.75rem;
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ background: white;
+ box-shadow: 0 18px 45px rgb(15 23 42 / 0.18);
+ z-index: 4;
+}
+
+${scopeSelector} .org-invite-menu-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ margin-bottom: 0.75rem;
+}
+
+${scopeSelector} .org-invite-menu-title {
+ margin: 0;
+ font-size: 0.85rem;
+ text-transform: uppercase;
+ letter-spacing: 0.08em;
+ color: var(--text-muted);
+}
+
+${scopeSelector} .org-invite-menu-list {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+${scopeSelector} .org-invite-row,
${scopeSelector} .org-name-row {
display: flex;
align-items: center;
@@ -39,13 +140,56 @@ ${scopeSelector} .org-name-row button {
margin-left: auto;
}
+${scopeSelector} .org-name-copy {
+ display: flex;
+ flex-direction: column;
+ gap: 0.2rem;
+}
+
+${scopeSelector} .org-name-meta {
+ font-size: 0.8rem;
+ color: var(--text-muted);
+}
+
+${scopeSelector} .org-inline-actions,
+${scopeSelector} .org-invite-actions {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-left: auto;
+}
+
+${scopeSelector} .org-members-empty {
+ margin: 0;
+ font-size: 0.9rem;
+ color: var(--text-muted);
+}
+
@media (max-width: 960px) {
- ${scopeSelector} .org-name-row {
+ ${scopeSelector} .org-members-head {
flex-direction: column;
align-items: flex-start;
}
- ${scopeSelector} .org-name-row button {
+ ${scopeSelector} .org-members-tools {
+ margin-left: 0;
+ }
+
+ ${scopeSelector} .org-invite-menu {
+ left: 0;
+ right: auto;
+ width: 100%;
+ }
+
+ ${scopeSelector} .org-name-row,
+ ${scopeSelector} .org-invite-row {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+
+ ${scopeSelector} .org-name-row button,
+ ${scopeSelector} .org-inline-actions,
+ ${scopeSelector} .org-invite-actions {
margin-left: 0;
}
}
@@ -56,6 +200,8 @@ ${scopeSelector} .org-name-row button {
OrgPortal.componentFns.MembersCard = function MembersCard() {
const PanelCard = window.SharedUI.componentFns.PanelCard;
const members = store.getMembers();
+ const pendingInvites = store.getPendingInvites();
+ const inviteMenuOpen = store.getInviteMenuOpen();
const allowMemberManagement = getters.canManageMembers();
ensureScopedStyle("portal-members-card", membersCardCss);
@@ -68,6 +214,217 @@ ${scopeSelector} .org-name-row button {
body: h(
"div",
{ className: "org-name-list" },
+ h(
+ "div",
+ { className: "org-members-head" },
+ h(
+ "div",
+ { className: "org-members-copy" },
+ h("h4", { className: "org-members-kicker" }, "Roster"),
+ h(
+ "p",
+ { className: "org-members-subtitle" },
+ "Manage membership and review incoming organization invites.",
+ ),
+ ),
+ h(
+ "div",
+ { className: "org-members-tools" },
+ h(
+ "button",
+ {
+ type: "button",
+ className:
+ "org-secondary-btn org-icon-btn org-tool-btn",
+ title: "Pending invitations",
+ "aria-label": "Pending invitations",
+ onClick: () => actions.toggleInviteMenu(),
+ },
+ h(
+ "svg",
+ {
+ className: "org-icon",
+ viewBox: "0 0 24 24",
+ fill: "none",
+ stroke: "currentColor",
+ "stroke-width": "2",
+ "stroke-linecap": "round",
+ "stroke-linejoin": "round",
+ "aria-hidden": "true",
+ },
+ h("path", {
+ d: "M15 17h5l-1.4-1.4A2 2 0 0 1 18 14.2V11a6 6 0 1 0-12 0v3.2a2 2 0 0 1-.6 1.4L4 17h5",
+ }),
+ h("path", { d: "M9.73 21a2 2 0 0 0 4.54 0" }),
+ ),
+ pendingInvites.length > 0
+ ? h(
+ "span",
+ { className: "org-tool-badge" },
+ String(pendingInvites.length),
+ )
+ : null,
+ ),
+ allowMemberManagement
+ ? h(
+ "button",
+ {
+ type: "button",
+ className:
+ "org-secondary-btn org-icon-btn org-tool-btn",
+ title: "Invite player",
+ "aria-label": "Invite player",
+ onClick: () =>
+ actions.openModal("invite"),
+ },
+ h(
+ "svg",
+ {
+ className: "org-icon",
+ viewBox: "0 0 24 24",
+ fill: "none",
+ stroke: "currentColor",
+ "stroke-width": "2",
+ "stroke-linecap": "round",
+ "stroke-linejoin": "round",
+ "aria-hidden": "true",
+ },
+ h("path", { d: "M12 5v14" }),
+ h("path", { d: "M5 12h14" }),
+ ),
+ )
+ : null,
+ inviteMenuOpen
+ ? h(
+ "div",
+ { className: "org-invite-menu" },
+ h(
+ "div",
+ { className: "org-invite-menu-head" },
+ h(
+ "h4",
+ {
+ className:
+ "org-invite-menu-title",
+ },
+ "Pending Invites",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className:
+ "org-secondary-btn org-icon-btn org-tool-btn",
+ title: "Close invites",
+ "aria-label": "Close invites",
+ onClick: () =>
+ actions.closeInviteMenu(),
+ },
+ h(
+ "svg",
+ {
+ className: "org-icon",
+ viewBox: "0 0 24 24",
+ fill: "none",
+ stroke: "currentColor",
+ "stroke-width": "2",
+ "stroke-linecap": "round",
+ "stroke-linejoin": "round",
+ "aria-hidden": "true",
+ },
+ h("path", { d: "M18 6 6 18" }),
+ h("path", { d: "m6 6 12 12" }),
+ ),
+ ),
+ ),
+ pendingInvites.length === 0
+ ? h(
+ "p",
+ {
+ className: "org-members-empty",
+ },
+ "No incoming organization invites.",
+ )
+ : h(
+ "div",
+ {
+ className:
+ "org-invite-menu-list",
+ },
+ ...pendingInvites.map((invite) =>
+ h(
+ "article",
+ {
+ className:
+ "org-invite-row",
+ },
+ h(
+ "div",
+ {
+ className:
+ "org-name-copy",
+ },
+ h(
+ "strong",
+ null,
+ invite.orgName ||
+ "Unknown Organization",
+ ),
+ h(
+ "span",
+ {
+ className:
+ "org-name-meta",
+ },
+ "Invited by ",
+ invite.inviterName ||
+ "Unknown",
+ ),
+ ),
+ h(
+ "div",
+ {
+ className:
+ "org-invite-actions",
+ },
+ h(
+ "button",
+ {
+ type: "button",
+ className:
+ "org-secondary-btn",
+ onClick: () =>
+ actions.declineInvite(
+ String(
+ invite.orgId ||
+ "",
+ ),
+ ),
+ },
+ "Decline",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ onClick: () =>
+ actions.acceptInvite(
+ String(
+ invite.orgId ||
+ "",
+ ),
+ ),
+ },
+ "Accept",
+ ),
+ ),
+ ),
+ ),
+ ),
+ )
+ : null,
+ ),
+ ),
...members.map((member) => {
const canRemoveMember =
allowMemberManagement &&
@@ -76,7 +433,18 @@ ${scopeSelector} .org-name-row button {
return h(
"article",
{ className: "org-name-row" },
- h("strong", null, member.name),
+ h(
+ "div",
+ { className: "org-name-copy" },
+ h("strong", null, member.name),
+ member.uid
+ ? h(
+ "span",
+ { className: "org-name-meta" },
+ member.uid,
+ )
+ : null,
+ ),
canRemoveMember
? h(
"button",
diff --git a/arma/client/addons/org/ui/src/components/portal/modalLayer.js b/arma/client/addons/org/ui/src/components/portal/modalLayer.js
index db83264..c1fe372 100644
--- a/arma/client/addons/org/ui/src/components/portal/modalLayer.js
+++ b/arma/client/addons/org/ui/src/components/portal/modalLayer.js
@@ -15,8 +15,11 @@
}
const members = store.getMembers();
+ const inviteablePlayers = store.getInviteablePlayers();
const memberSelectProps =
members.length === 0 ? { disabled: true } : {};
+ const inviteSelectProps =
+ inviteablePlayers.length === 0 ? { disabled: true } : {};
let title = "";
let body = null;
@@ -211,6 +214,72 @@
),
),
);
+ } else if (modal.type === "invite") {
+ title = "Invite Player";
+ body = h(
+ "div",
+ { className: "app-modal-form" },
+ h(
+ "div",
+ null,
+ h("label", null, "Online Player"),
+ h(
+ "select",
+ {
+ id: "org-invite-player",
+ ...inviteSelectProps,
+ },
+ ...inviteablePlayers.map((player) =>
+ h(
+ "option",
+ { value: player.uid },
+ player.name || player.uid,
+ ),
+ ),
+ ),
+ ),
+ inviteablePlayers.length === 0
+ ? h(
+ "p",
+ null,
+ "No eligible online players are currently available for invites.",
+ )
+ : null,
+ h(
+ "div",
+ { className: "app-modal-actions" },
+ h(
+ "button",
+ {
+ type: "button",
+ className: "org-secondary-btn",
+ onClick: () => actions.closeModal(),
+ },
+ "Cancel",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ ...inviteSelectProps,
+ onClick: () => {
+ if (
+ actions.sendInvite(
+ String(
+ actions.getInputValue(
+ "org-invite-player",
+ ) || "",
+ ),
+ )
+ ) {
+ actions.closeModal();
+ }
+ },
+ },
+ "Send Invite",
+ ),
+ ),
+ );
} else if (modal.type === "disband") {
title = "Disband Organization";
body = h(
diff --git a/arma/client/addons/org/ui/src/portal/actions.js b/arma/client/addons/org/ui/src/portal/actions.js
index d27e99c..2ab8a30 100644
--- a/arma/client/addons/org/ui/src/portal/actions.js
+++ b/arma/client/addons/org/ui/src/portal/actions.js
@@ -62,6 +62,14 @@
return;
}
+ if (type === "invite" && !getters.canManageMembers()) {
+ this.showTreasuryNotice(
+ "error",
+ "Only the organization leader or CEO can invite players.",
+ );
+ return;
+ }
+
if (type === "disband" && !getters.canDisbandOrg()) {
return;
}
@@ -77,6 +85,14 @@
store.setModal(null);
}
+ toggleInviteMenu() {
+ store.setInviteMenuOpen(!store.getInviteMenuOpen());
+ }
+
+ closeInviteMenu() {
+ store.setInviteMenuOpen(false);
+ }
+
removeMember(member) {
if (!getters.canManageMembers()) {
return false;
@@ -294,6 +310,79 @@
amount,
});
}
+
+ sendInvite(targetUid) {
+ if (!getters.canManageMembers()) {
+ this.showTreasuryNotice(
+ "error",
+ "Only the organization leader or CEO can invite players.",
+ );
+ return false;
+ }
+
+ const target = store
+ .getInviteablePlayers()
+ .find((entry) => String(entry.uid || "") === String(targetUid));
+
+ if (!target) {
+ this.showTreasuryNotice(
+ "error",
+ "Select an online player to invite.",
+ );
+ return false;
+ }
+
+ const bridge = window.RegistryApp
+ ? window.RegistryApp.bridge
+ : null;
+
+ if (!bridge || typeof bridge.requestInvitePlayer !== "function") {
+ this.showTreasuryNotice(
+ "error",
+ "Organization invite bridge is unavailable.",
+ );
+ return false;
+ }
+
+ return bridge.requestInvitePlayer({
+ targetUid: String(target.uid || ""),
+ targetName: String(target.name || ""),
+ });
+ }
+
+ acceptInvite(orgId) {
+ const bridge = window.RegistryApp
+ ? window.RegistryApp.bridge
+ : null;
+
+ if (!bridge || typeof bridge.requestAcceptInvite !== "function") {
+ this.showTreasuryNotice(
+ "error",
+ "Organization invite bridge is unavailable.",
+ );
+ return false;
+ }
+
+ this.closeInviteMenu();
+ return bridge.requestAcceptInvite({ orgId });
+ }
+
+ declineInvite(orgId) {
+ const bridge = window.RegistryApp
+ ? window.RegistryApp.bridge
+ : null;
+
+ if (!bridge || typeof bridge.requestDeclineInvite !== "function") {
+ this.showTreasuryNotice(
+ "error",
+ "Organization invite bridge is unavailable.",
+ );
+ return false;
+ }
+
+ this.closeInviteMenu();
+ return bridge.requestDeclineInvite({ orgId });
+ }
}
OrgPortal.actions = new OrgPortalActions();
diff --git a/arma/client/addons/org/ui/src/portal/data.js b/arma/client/addons/org/ui/src/portal/data.js
index a544baf..0e7f29d 100644
--- a/arma/client/addons/org/ui/src/portal/data.js
+++ b/arma/client/addons/org/ui/src/portal/data.js
@@ -74,6 +74,8 @@
reputation: 0,
creditLines: [],
members: [],
+ pendingInvites: [],
+ inviteablePlayers: [],
fleet: [],
assets: [],
activity: [],
@@ -126,6 +128,14 @@
this.portalData.members,
normalizeCollection(payload.portalData.members),
);
+ replaceArray(
+ this.portalData.pendingInvites,
+ normalizeCollection(payload.portalData.pendingInvites),
+ );
+ replaceArray(
+ this.portalData.inviteablePlayers,
+ normalizeCollection(payload.portalData.inviteablePlayers),
+ );
replaceArray(
this.portalData.fleet,
normalizeCollection(payload.portalData.fleet),
diff --git a/arma/client/addons/org/ui/src/portal/store.js b/arma/client/addons/org/ui/src/portal/store.js
index abdb361..bc96b18 100644
--- a/arma/client/addons/org/ui/src/portal/store.js
+++ b/arma/client/addons/org/ui/src/portal/store.js
@@ -51,6 +51,11 @@
[this.getMembers, this.setMembers] = createSignal([
...portalData.members,
]);
+ [this.getPendingInvites, this.setPendingInvites] = createSignal([
+ ...portalData.pendingInvites,
+ ]);
+ [this.getInviteablePlayers, this.setInviteablePlayers] =
+ createSignal([...portalData.inviteablePlayers]);
[this.getCreditLines, this.setCreditLines] = createSignal([
...portalData.creditLines,
]);
@@ -68,6 +73,8 @@
text: "",
});
[this.getModal, this.setModal] = createSignal(null);
+ [this.getInviteMenuOpen, this.setInviteMenuOpen] =
+ createSignal(false);
[this.getOrgDisbanded, this.setOrgDisbanded] = createSignal(false);
}
@@ -77,6 +84,12 @@
this.setFunds(nextPortalData.funds || 0);
this.setReputation(nextPortalData.reputation || 0);
this.setMembers([...normalizeCollection(nextPortalData.members)]);
+ this.setPendingInvites([
+ ...normalizeCollection(nextPortalData.pendingInvites),
+ ]);
+ this.setInviteablePlayers([
+ ...normalizeCollection(nextPortalData.inviteablePlayers),
+ ]);
this.setCreditLines([
...normalizeCollection(nextPortalData.creditLines),
]);
diff --git a/arma/server/addons/actor/XEH_preInit.sqf b/arma/server/addons/actor/XEH_preInit.sqf
index 46cf2f8..4fff430 100644
--- a/arma/server/addons/actor/XEH_preInit.sqf
+++ b/arma/server/addons/actor/XEH_preInit.sqf
@@ -4,8 +4,6 @@ PREP_RECOMPILE_START;
#include "XEH_PREP.hpp"
PREP_RECOMPILE_END;
-// private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
-
[QGVAR(requestInitActor), {
params [["_uid", "", [""]]];
@@ -18,7 +16,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid UID!" };
- private _finalData = GVAR(ActorStore) call ["get", [GVAR(Registry), _uid, _field]];
+ private _finalData = GVAR(ActorStore) call ["get", [_uid, _field]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(actor,responseSyncActor), [_finalData], _player] call CFUNC(targetEvent);
@@ -29,7 +27,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "" || _field isEqualTo "") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid UID or Key!" };
- private _hashMap = GVAR(ActorStore) call ["set", [GVAR(Registry), "actor:update", _uid, _field, _value, _sync]];
+ private _hashMap = GVAR(ActorStore) call ["set", [_uid, _field, _value, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(actor,responseSyncActor), [_hashMap], _player] call CFUNC(targetEvent);
@@ -41,7 +39,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid UID!" };
if ((_fieldValuePairs isEqualTo createHashMap) || !(_fieldValuePairs isEqualType createHashMap)) exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid field pairs!" };
- private _hashMap = GVAR(ActorStore) call ["mset", [GVAR(Registry), "actor:update", _uid, _fieldValuePairs, _sync]];
+ private _hashMap = GVAR(ActorStore) call ["mset", [_uid, _fieldValuePairs, _sync]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(actor,responseSyncActor), [_hashMap], _player] call CFUNC(targetEvent);
@@ -53,7 +51,7 @@ PREP_RECOMPILE_END;
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid UID!" };
GVAR(ActorStore) call ["snapshot", [_uid]];
- private _finalData = GVAR(ActorStore) call ["save", [GVAR(Registry), "actor:update", _uid]];
+ private _finalData = GVAR(ActorStore) call ["save", [_uid]];
private _player = [_uid] call EFUNC(common,getPlayer);
[CRPC(actor,responseSyncActor), [_finalData], _player] call CFUNC(targetEvent);
@@ -63,5 +61,5 @@ PREP_RECOMPILE_END;
params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { diag_log "[FORGE:Server:Actor] Empty/Invalid UID!" };
- GVAR(ActorStore) call ["remove", [GVAR(Registry), _uid]];
+ GVAR(ActorStore) call ["remove", [_uid]];
}] call CFUNC(addEventHandler);
diff --git a/arma/server/addons/actor/functions/fnc_initActorStore.sqf b/arma/server/addons/actor/functions/fnc_initActorStore.sqf
index 5dbba52..2f19cc5 100644
--- a/arma/server/addons/actor/functions/fnc_initActorStore.sqf
+++ b/arma/server/addons/actor/functions/fnc_initActorStore.sqf
@@ -4,13 +4,13 @@
* File: fnc_initActorStore.sqf
* Author: IDSolutions
* Date: 2025-12-17
- * Last Update: 2026-04-01
+ * Last Update: 2026-04-05
* Public: Yes
*
* Description:
* Initializes the actor store for managing player actor data.
- * Actor hot state is owned by the extension; SQF maintains a compatibility
- * mirror for engine-adjacent consumers.
+ * Actor hot state is owned by the extension; SQF acts as a thin bridge for
+ * engine-adjacent reads, snapshots, and response fan-out.
*
* Arguments:
* None
@@ -38,7 +38,7 @@ GVAR(ActorModel) = compileFinal createHashMapObject [[
_actor set ["state", "HEALTHY"];
_actor set ["phone_number", ""];
_actor set ["email", ""];
- _actor set ["organization", ""];
+ _actor set ["organization", "default"];
_actor set ["holster", true];
_actor
@@ -109,7 +109,6 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
["#base", EGVAR(common,BaseStore)],
["#type", "ActorBaseStore"],
["#create", compileFinal {
- GVAR(Registry) = createHashMap;
["INFO", "Actor Store Initialized!"] call EFUNC(common,log);
}],
["cacheActor", compileFinal {
@@ -117,9 +116,7 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
if (_uid isEqualTo "" || { !(_actor isEqualType createHashMap) }) exitWith { createHashMap };
- private _finalActor = GVAR(ActorModel) call ["migrate", [+_actor]];
- GVAR(Registry) set [_uid, _finalActor];
- _finalActor
+ GVAR(ActorModel) call ["migrate", [+_actor]]
}],
["callHotActor", compileFinal {
params [["_function", "", [""]], ["_arguments", [], [[]]]];
@@ -138,86 +135,167 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
if !(_data isEqualType createHashMap) exitWith { createHashMap };
_data
}],
+ ["listHotUids", compileFinal {
+ ["actor:hot:keys", []] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
+ if !(_isSuccess) exitWith { [] };
+ if !(_result isEqualType "") exitWith { [] };
+ if ((_result find "Error:") == 0) exitWith {
+ ["ERROR", format ["Actor extension call '%1' failed: %2", "actor:hot:keys", _result]] call EFUNC(common,log);
+ []
+ };
+
+ private _uids = fromJSON _result;
+ if !(_uids isEqualType []) exitWith { [] };
+
+ _uids select { _x isEqualType "" && { _x isNotEqualTo "" } }
+ }],
["loadHotActor", compileFinal {
params [["_uid", "", [""]], ["_initialize", false, [false]]];
if (_uid isEqualTo "") exitWith { createHashMap };
+ if (_initialize) then {
+ // Missing actors should be created explicitly from a server snapshot
+ // before the hot cache is initialized.
+ private _ensureResult = _self call ["ensurePersistentActor", [_uid]];
+ if !(_ensureResult isEqualType true && { _ensureResult }) exitWith { createHashMap };
+ };
private _command = ["actor:hot:get", "actor:hot:init"] select _initialize;
private _actor = _self call ["callHotActor", [_command, [_uid]]];
if (_actor isEqualTo createHashMap) exitWith { _actor };
- _self call ["cacheActor", [_uid, _actor]]
+ _self call ["hydrateActorIfNeeded", [_uid, _actor, true]]
}],
- ["normalizeGetArgs", compileFinal {
- params ["_rawArguments"];
+ ["ensurePersistentActor", compileFinal {
+ params [["_uid", "", [""]]];
- if ((_rawArguments param [0, createHashMap]) isEqualType createHashMap) exitWith {
- [
- _rawArguments param [1, "", [""]],
- _rawArguments param [2, "", [""]]
- ]
+ if (_uid isEqualTo "") exitWith { false };
+
+ ["actor:exists", [_uid]] call EFUNC(extension,extCall) params ["_existsResult", "_existsSuccess"];
+ if (!_existsSuccess || { !(_existsResult isEqualType "") }) exitWith {
+ ["ERROR", format ["Failed to verify persistent actor state for %1.", _uid]] call EFUNC(common,log);
+ false
};
- [
- _rawArguments param [0, "", [""]],
- _rawArguments param [1, "", [""]]
- ]
- }],
- ["normalizeSetArgs", compileFinal {
- params ["_rawArguments"];
+ if (_existsResult isEqualTo "true") exitWith { true };
- if ((_rawArguments param [0, createHashMap]) isEqualType createHashMap) exitWith {
- [
- _rawArguments param [2, "", [""]],
- _rawArguments param [3, "", [""]],
- _rawArguments param [4, nil, [0, "", [], false, createHashMap, objNull, grpNull]],
- _rawArguments param [5, false, [false]]
- ]
+ private _player = [_uid] call EFUNC(common,getPlayer);
+ private _actor = GVAR(ActorModel) call ["fromPlayer", [_player]];
+ _actor set ["uid", _uid];
+
+ if ((_actor getOrDefault ["organization", ""]) isEqualTo "") then {
+ _actor set ["organization", "default"];
};
- [
- _rawArguments param [0, "", [""]],
- _rawArguments param [1, "", [""]],
- _rawArguments param [2, nil, [0, "", [], false, createHashMap, objNull, grpNull]],
- _rawArguments param [3, false, [false]]
- ]
- }],
- ["normalizeMSetArgs", compileFinal {
- params ["_rawArguments"];
+ private _json = _self call ["toJSON", [_actor]];
+ ["actor:create", [_uid, _json]] call EFUNC(extension,extCall) params ["_createResult", "_createSuccess"];
- if ((_rawArguments param [0, createHashMap]) isEqualType createHashMap) exitWith {
- [
- _rawArguments param [2, "", [""]],
- _rawArguments param [3, createHashMap, [createHashMap]],
- _rawArguments param [4, false, [false]]
- ]
+ if (!_createSuccess || { !(_createResult isEqualType "") }) exitWith {
+ ["ERROR", format ["Failed to create actor %1 from server snapshot.", _uid]] call EFUNC(common,log);
+ false
};
- [
- _rawArguments param [0, "", [""]],
- _rawArguments param [1, createHashMap, [createHashMap]],
- _rawArguments param [2, false, [false]]
- ]
- }],
- ["normalizeUidArg", compileFinal {
- params ["_rawArguments"];
-
- if ((_rawArguments param [0, createHashMap]) isEqualType createHashMap) exitWith {
- _rawArguments param [1, "", [""]]
+ if ((_createResult find "Error:") == 0) exitWith {
+ ["ERROR", format ["Actor create for %1 failed: %2", _uid, _createResult]] call EFUNC(common,log);
+ false
};
- _rawArguments param [0, "", [""]]
+ true
+ }],
+ ["hydrateActorIfNeeded", compileFinal {
+ params [["_uid", "", [""]], ["_actor", createHashMap, [createHashMap]], ["_save", true, [false]]];
+
+ if (_uid isEqualTo "" || { !(_actor isEqualType createHashMap) } || { _actor isEqualTo createHashMap }) exitWith {
+ createHashMap
+ };
+
+ // Hot actor reads can still surface older partial records. Repair them
+ // from the live player snapshot when possible and persist the result.
+ private _hydratedActor = GVAR(ActorModel) call ["migrate", [+_actor]];
+ private _defaults = GVAR(ActorModel) call ["defaults", []];
+ private _player = [_uid] call EFUNC(common,getPlayer);
+ private _needsPersist = false;
+
+ if ((_hydratedActor getOrDefault ["uid", ""]) isEqualTo "") then {
+ _hydratedActor set ["uid", _uid];
+ _needsPersist = true;
+ };
+ if ((_hydratedActor getOrDefault ["organization", ""]) isEqualTo "") then {
+ _hydratedActor set ["organization", "default"];
+ _needsPersist = true;
+ };
+
+ {
+ private _value = _hydratedActor getOrDefault [_x, ""];
+ if !(_value isEqualType "") then {
+ _hydratedActor set [_x, _defaults getOrDefault [_x, ""]];
+ _needsPersist = true;
+ };
+ } forEach ["phone_number", "email"];
+
+ if (_player isNotEqualTo objNull) then {
+ private _snapshot = GVAR(ActorModel) call ["fromPlayer", [_player]];
+ private _name = _hydratedActor getOrDefault ["name", ""];
+ if (
+ !(_name isEqualType "")
+ || { _name isEqualTo "" }
+ || { toLowerANSI _name isEqualTo "unknown" }
+ ) then {
+ _hydratedActor set ["name", _snapshot getOrDefault ["name", name _player]];
+ _needsPersist = true;
+ };
+
+ private _position = _hydratedActor getOrDefault ["position", []];
+ if !(_position isEqualType [] && { count _position isEqualTo 3 }) then {
+ _hydratedActor set ["position", _snapshot getOrDefault ["position", getPosASL _player]];
+ _needsPersist = true;
+ };
+
+ private _direction = _hydratedActor getOrDefault ["direction", 0];
+ if !(_direction isEqualType 0) then {
+ _hydratedActor set ["direction", _snapshot getOrDefault ["direction", getDir _player]];
+ _needsPersist = true;
+ };
+
+ {
+ private _fieldValue = _hydratedActor getOrDefault [_x, ""];
+ if (!(_fieldValue isEqualType "") || { _fieldValue isEqualTo "" }) then {
+ _hydratedActor set [_x, _snapshot getOrDefault [_x, _defaults getOrDefault [_x, ""]]];
+ _needsPersist = true;
+ };
+ } forEach ["stance", "rank", "state"];
+
+ private _loadout = _hydratedActor getOrDefault ["loadout", []];
+ if !(_loadout isEqualType [] && { count _loadout > 0 }) then {
+ _hydratedActor set ["loadout", getUnitLoadout _player];
+ _needsPersist = true;
+ };
+ } else {
+ {
+ private _fieldValue = _hydratedActor getOrDefault [_x, ""];
+ if (!(_fieldValue isEqualType "") || { _fieldValue isEqualTo "" }) then {
+ _hydratedActor set [_x, _defaults getOrDefault [_x, ""]];
+ _needsPersist = true;
+ };
+ } forEach ["stance", "rank", "state"];
+ };
+
+ if !_needsPersist exitWith {
+ _self call ["cacheActor", [_uid, _hydratedActor]]
+ };
+
+ private _updatedActor = _self call ["override", [_uid, _hydratedActor, _save]];
+ if (_updatedActor isEqualType createHashMap && { _updatedActor isNotEqualTo createHashMap }) exitWith {
+ _self call ["cacheActor", [_uid, _updatedActor]]
+ };
+
+ ["WARNING", format ["Failed to hydrate actor %1 from player snapshot.", _uid]] call EFUNC(common,log);
+ _self call ["cacheActor", [_uid, _hydratedActor]]
}],
["init", compileFinal {
params [["_uid", "", [""]]];
private _player = [_uid] call EFUNC(common,getPlayer);
- private _cached = GVAR(Registry) getOrDefault [_uid, nil];
- if !(isNil { _cached }) exitWith {
- [CRPC(actor,responseInitActor), [_cached], _player] call CFUNC(targetEvent);
- _cached
- };
["actor:exists", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
@@ -236,14 +314,11 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
_finalActor = _self call ["loadHotActor", [_uid, true]];
["INFO", format ["Found actor for %1", _uid]] call EFUNC(common,log);
} else {
- _finalActor = GVAR(ActorModel) call ["fromPlayer", [_player]];
- _finalActor set ["uid", _uid];
-
- private _json = _self call ["toJSON", [_finalActor]];
- ["actor:create", [_uid, _json]] call EFUNC(extension,extCall) params ["_createResult", "_createSuccess"];
- if (!_createSuccess) exitWith {
+ if !(_self call ["ensurePersistentActor", [_uid]]) exitWith {
["ERROR", format ["Failed to create actor %1! Using fallback actor.", _uid]] call EFUNC(common,log);
+ _finalActor = GVAR(ActorModel) call ["fromPlayer", [_player]];
+ _finalActor set ["uid", _uid];
_finalActor = _self call ["cacheActor", [_uid, _finalActor]];
[CRPC(actor,responseInitActor), [_finalActor], _player] call CFUNC(targetEvent);
_finalActor
@@ -264,16 +339,57 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
_finalActor
}],
["get", compileFinal {
- call (_self get "normalizeGetArgs") params ["_uid", "_field"];
+ params [["_uid", "", [""]], ["_field", "", [""]]];
private _actor = _self call ["loadHotActor", [_uid, false]];
- if (_actor isEqualTo createHashMap) then {
- _actor = _self call ["loadHotActor", [_uid, true]];
- };
if (_field isEqualTo "") exitWith { _actor };
_actor getOrDefault [_field, nil]
}],
+ ["load", compileFinal {
+ params [["_uid", "", [""]]];
+
+ private _actor = _self call ["get", [_uid, ""]];
+ if !(_actor isEqualType createHashMap) exitWith { createHashMap };
+
+ _actor
+ }],
+ ["getFieldOrDefault", compileFinal {
+ params [["_uid", "", [""]], ["_field", "", [""]], ["_default", nil]];
+
+ if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { _default };
+
+ private _actor = _self call ["load", [_uid]];
+ if !(_actor isEqualType createHashMap) exitWith { _default };
+ if (_actor isEqualTo createHashMap) exitWith { _default };
+
+ _actor getOrDefault [_field, _default]
+ }],
+ ["getOrganization", compileFinal {
+ params [["_uid", "", [""]], ["_default", "default", [""]]];
+
+ private _orgID = _self call ["getFieldOrDefault", [_uid, "organization", _default]];
+ if !(_orgID isEqualType "") exitWith { _default };
+ if (_orgID isEqualTo "") exitWith { _default };
+
+ _orgID
+ }],
+ ["getName", compileFinal {
+ params [["_uid", "", [""]], ["_default", "", [""]]];
+
+ private _name = _self call ["getFieldOrDefault", [_uid, "name", _default]];
+ if !(_name isEqualType "") exitWith { _default };
+
+ _name
+ }],
+ ["getPhoneNumber", compileFinal {
+ params [["_uid", "", [""]], ["_default", "", [""]]];
+
+ private _phoneNumber = _self call ["getFieldOrDefault", [_uid, "phone_number", _default]];
+ if !(_phoneNumber isEqualType "") exitWith { _default };
+
+ _phoneNumber
+ }],
["override", compileFinal {
params [
["_uid", "", [""]],
@@ -297,7 +413,12 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
_self call ["cacheActor", [_uid, _actor]]
}],
["set", compileFinal {
- call (_self get "normalizeSetArgs") params ["_uid", "_field", "_value", "_sync"];
+ params [
+ ["_uid", "", [""]],
+ ["_field", "", [""]],
+ ["_value", nil, [0, "", [], false, createHashMap, objNull, grpNull]],
+ ["_sync", false, [false]]
+ ];
if (_uid isEqualTo "" || { _field isEqualTo "" }) exitWith { createHashMap };
@@ -312,7 +433,7 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
createHashMapFromArray [[_field, _updatedActor getOrDefault [_field, _value]]]
}],
["mset", compileFinal {
- call (_self get "normalizeMSetArgs") params ["_uid", "_fieldValuePairs", "_sync"];
+ params [["_uid", "", [""]], ["_fieldValuePairs", createHashMap, [createHashMap]], ["_sync", false, [false]]];
if (_uid isEqualTo "" || { !(_fieldValuePairs isEqualType createHashMap) }) exitWith { createHashMap };
@@ -327,7 +448,7 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
+_fieldValuePairs
}],
["save", compileFinal {
- private _uid = call (_self get "normalizeUidArg");
+ params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { createHashMap };
private _actor = _self call ["callHotActor", ["actor:hot:save", [_uid]]];
@@ -336,11 +457,10 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
_self call ["cacheActor", [_uid, _actor]]
}],
["remove", compileFinal {
- private _uid = call (_self get "normalizeUidArg");
+ params [["_uid", "", [""]]];
if (_uid isEqualTo "") exitWith { false };
- GVAR(Registry) deleteAt _uid;
["actor:hot:remove", [_uid]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
_isSuccess && { _result isEqualTo "OK" }
}],
@@ -364,6 +484,9 @@ GVAR(ActorBaseStore) = compileFinal createHashMapFromArray [
_finalActor set ["rank", rank _player];
_finalActor set ["state", lifeState _player];
_finalActor set ["loadout", getUnitLoadout _player];
+ if ((_finalActor getOrDefault ["organization", ""]) isEqualTo "") then {
+ _finalActor set ["organization", "default"];
+ };
} else {
["WARNING", format ["No player object found for %1 during actor snapshot, using cached values.", _uid]] call EFUNC(common,log);
};
diff --git a/arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf b/arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf
index 6cd9bcd..24f8615 100644
--- a/arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf
+++ b/arma/server/addons/bank/functions/fnc_initPayloadBuilder.sqf
@@ -62,10 +62,7 @@ GVAR(BankPayloadBuilder) = createHashMapObject [[
];
if (_uid isEqualTo "") exitWith { _defaultState };
- private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
- private _orgID = _actor getOrDefault ["organization", "default"];
- if (_orgID isEqualTo "") then { _orgID = "default"; };
-
+ private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid]];
private _org = EGVAR(org,OrgStore) call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) then {
_org = EGVAR(org,OrgStore) call ["loadById", ["default"]];
diff --git a/arma/server/addons/bank/functions/fnc_initSessionManager.sqf b/arma/server/addons/bank/functions/fnc_initSessionManager.sqf
index dc9077e..7d4774b 100644
--- a/arma/server/addons/bank/functions/fnc_initSessionManager.sqf
+++ b/arma/server/addons/bank/functions/fnc_initSessionManager.sqf
@@ -24,16 +24,20 @@
#pragma hemtt ignore_variables ["_self"]
GVAR(BankSessionManager) = createHashMapObject [[
["#type", "BankSessionManager"],
+ ["#create", compileFinal {
+ _self set ["sessions", createHashMap];
+ }],
["getSessionState", compileFinal {
params [["_uid", "", [""]]];
- private _session = GVAR(SessionRegistry) getOrDefault [_uid, createHashMap];
+ private _sessions = _self getOrDefault ["sessions", createHashMap];
+ private _session = _sessions getOrDefault [_uid, createHashMap];
if (_session isEqualTo createHashMap) then {
_session = createHashMapFromArray [
["atmAuthorized", false],
["mode", "bank"]
];
- GVAR(SessionRegistry) set [_uid, _session];
+ _sessions set [_uid, _session];
};
_session
@@ -44,9 +48,10 @@ GVAR(BankSessionManager) = createHashMapObject [[
if (_uid isEqualTo "") exitWith { createHashMap };
private _session = +(_self call ["getSessionState", [_uid]]);
+ private _sessions = _self getOrDefault ["sessions", createHashMap];
{ _session set [_x, _y]; } forEach _fieldValuePairs;
- GVAR(SessionRegistry) set [_uid, _session];
+ _sessions set [_uid, _session];
_session
}],
["resolveMode", compileFinal {
diff --git a/arma/server/addons/bank/functions/fnc_initStore.sqf b/arma/server/addons/bank/functions/fnc_initStore.sqf
index 507f236..d04e37b 100644
--- a/arma/server/addons/bank/functions/fnc_initStore.sqf
+++ b/arma/server/addons/bank/functions/fnc_initStore.sqf
@@ -18,7 +18,6 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
["#base", EGVAR(common,BaseStore)],
["#type", "BankBaseStore"],
["#create", compileFinal {
- GVAR(SessionRegistry) = createHashMap;
["INFO", "Bank Store Initialized!"] call EFUNC(common,log);
}],
["normalizeAccount", compileFinal {
@@ -228,6 +227,17 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
false
};
+ private _persistenceFailures = [];
+ private _savedBank = _self call ["save", [_uid]];
+ if (_savedBank isEqualTo createHashMap) then {
+ _persistenceFailures pushBack "bank";
+ };
+
+ private _orgPersistenceMessage = _orgResult getOrDefault ["persistenceMessage", ""];
+ if !(_orgResult getOrDefault ["persisted", false]) then {
+ _persistenceFailures pushBack "organization";
+ };
+
GVAR(BankMessenger) call ["sendAccountSync", [_uid, _bankPatch]];
GVAR(BankMessenger) call ["sendNotification", [_uid, "info", "Bank", _orgResult getOrDefault ["message", format ["Repaid $%1 toward the organization credit line.", [_amount] call EFUNC(common,formatNumber)]]]];
@@ -241,6 +251,19 @@ GVAR(BankBaseStore) = compileFinal createHashMapFromArray [
} forEach (_orgResult getOrDefault ["memberUids", []]);
};
+ if (_persistenceFailures isNotEqualTo []) then {
+ private _warning = format [
+ "Credit repayment posted, but durable save failed for: %1.",
+ _persistenceFailures joinString ", "
+ ];
+ if (_orgPersistenceMessage isNotEqualTo "") then {
+ _warning = format ["%1 %2", _warning, _orgPersistenceMessage];
+ };
+
+ ["ERROR", format ["Credit repayment for %1 completed with persistence failures: %2", _uid, _persistenceFailures joinString ", "]] call EFUNC(common,log);
+ GVAR(BankMessenger) call ["sendAlert", [_uid, "warning", _warning]];
+ };
+
_self call ["hydrateSession", [_uid, "", false]];
true
}],
diff --git a/arma/server/addons/cad/functions/fnc_initActivityRepository.sqf b/arma/server/addons/cad/functions/fnc_initActivityRepository.sqf
index 3fab742..fa9b05b 100644
--- a/arma/server/addons/cad/functions/fnc_initActivityRepository.sqf
+++ b/arma/server/addons/cad/functions/fnc_initActivityRepository.sqf
@@ -22,48 +22,19 @@
#pragma hemtt ignore_variables ["_self"]
GVAR(ActivityRepositoryBaseClass) = compileFinal createHashMapFromArray [
["#type", "CadActivityRepositoryBaseClass"],
- ["#create", compileFinal {
- _self set ["activityRegistry", []];
- _self set ["persistenceLoaded", false];
- }],
- ["restorePersistedActivity", compileFinal {
- if (_self getOrDefault ["persistenceLoaded", false]) exitWith { true };
-
- private _persistenceService = _self getOrDefault ["persistenceService", createHashMap];
- if (_persistenceService isEqualTo createHashMap) exitWith { false };
-
- private _result = _persistenceService call ["loadActivity", []];
- if !(_result getOrDefault ["success", false]) exitWith { false };
-
- _self set ["activityRegistry", +(_result getOrDefault ["data", []])];
- _self set ["persistenceLoaded", true];
- true
- }],
["appendEntry", compileFinal {
params [["_entry", createHashMap, [createHashMap]]];
if (_entry isEqualTo createHashMap) exitWith { false };
-
- _self call ["restorePersistedActivity", []];
-
- private _activityRegistry = +(_self getOrDefault ["activityRegistry", []]);
private _finalEntry = +_entry;
if ((_finalEntry getOrDefault ["timestamp", -1]) < 0) then {
_finalEntry set ["timestamp", serverTime];
};
- _activityRegistry pushBack _finalEntry;
-
- if ((count _activityRegistry) > 50) then {
- _activityRegistry deleteRange [0, (count _activityRegistry) - 50];
- };
-
- _self set ["activityRegistry", _activityRegistry];
private _persistenceService = _self getOrDefault ["persistenceService", createHashMap];
- if (_persistenceService isNotEqualTo createHashMap) then {
- _persistenceService call ["appendActivity", [_finalEntry]];
- };
- true
+ if (_persistenceService isEqualTo createHashMap) exitWith { false };
+
+ _persistenceService call ["appendActivity", [_finalEntry]]
}],
["appendActivity", compileFinal {
params [
@@ -85,8 +56,13 @@ GVAR(ActivityRepositoryBaseClass) = compileFinal createHashMapFromArray [
_self call ["appendEntry", [_entry]]
}],
["getActivity", compileFinal {
- _self call ["restorePersistedActivity", []];
- +(_self getOrDefault ["activityRegistry", []])
+ private _persistenceService = _self getOrDefault ["persistenceService", createHashMap];
+ if (_persistenceService isEqualTo createHashMap) exitWith { [] };
+
+ private _result = _persistenceService call ["loadActivity", []];
+ if !(_result getOrDefault ["success", false]) exitWith { [] };
+
+ +(_result getOrDefault ["data", []])
}]
];
diff --git a/arma/server/addons/cad/functions/fnc_initAssignmentRepository.sqf b/arma/server/addons/cad/functions/fnc_initAssignmentRepository.sqf
index cb3d5be..b8ab1ae 100644
--- a/arma/server/addons/cad/functions/fnc_initAssignmentRepository.sqf
+++ b/arma/server/addons/cad/functions/fnc_initAssignmentRepository.sqf
@@ -24,15 +24,51 @@
GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
["#type", "CadAssignmentRepositoryBaseClass"],
["#create", compileFinal {
- _self set ["assignmentRegistry", createHashMap];
- _self set ["dispatchOrderRegistry", createHashMap];
- _self set ["persistenceLoaded", false];
+ _self set ["ownershipHydrated", false];
+ }],
+ ["loadState", compileFinal {
+ private _result = createHashMapFromArray [
+ ["success", false],
+ ["assignments", createHashMap],
+ ["dispatchOrders", createHashMap]
+ ];
+
+ private _persistenceService = _self getOrDefault ["persistenceService", createHashMap];
+ if (_persistenceService isEqualTo createHashMap) exitWith { _result };
+
+ private _assignmentsResult = _persistenceService call ["loadAssignments", []];
+ if !(_assignmentsResult getOrDefault ["success", false]) exitWith { _result };
+
+ private _ordersResult = _persistenceService call ["loadDispatchOrders", []];
+ if !(_ordersResult getOrDefault ["success", false]) exitWith { _result };
+
+ private _assignmentRegistry = +(_assignmentsResult getOrDefault ["data", createHashMap]);
+ private _dispatchOrderRegistry = +(_ordersResult getOrDefault ["data", createHashMap]);
+
+ if !(_self getOrDefault ["ownershipHydrated", false]) then {
+ {
+ if ((_y getOrDefault ["state", ""]) isNotEqualTo "acknowledged") then { continue; };
+ if ((_y getOrDefault ["acknowledgedByUid", ""]) isEqualTo "") then { continue; };
+ if ((_dispatchOrderRegistry getOrDefault [_x, createHashMap]) isNotEqualTo createHashMap) then { continue; };
+ if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) isNotEqualTo "active") then { continue; };
+
+ EGVAR(task,TaskStore) call ["bindTaskOwnership", [_x, _y getOrDefault ["acknowledgedByUid", ""]]];
+ } forEach _assignmentRegistry;
+
+ _self set ["ownershipHydrated", true];
+ };
+
+ _result set ["success", true];
+ _result set ["assignments", _assignmentRegistry];
+ _result set ["dispatchOrders", _dispatchOrderRegistry];
+ _result
}],
["pruneAssignments", compileFinal {
- _self call ["restorePersistedState", []];
+ private _state = _self call ["loadState", []];
+ if !(_state getOrDefault ["success", false]) exitWith { 0 };
- private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap];
- private _dispatchOrderRegistry = _self getOrDefault ["dispatchOrderRegistry", createHashMap];
+ private _assignmentRegistry = _state getOrDefault ["assignments", createHashMap];
+ private _dispatchOrderRegistry = _state getOrDefault ["dispatchOrders", createHashMap];
private _keysToRemove = [];
{
@@ -46,12 +82,6 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
};
} forEach _assignmentRegistry;
- {
- _assignmentRegistry deleteAt _x;
- } forEach _keysToRemove;
-
- _self set ["assignmentRegistry", _assignmentRegistry];
-
private _persistenceService = _self getOrDefault ["persistenceService", createHashMap];
if (_persistenceService isNotEqualTo createHashMap) then {
{
@@ -62,50 +92,73 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
count _keysToRemove
}],
["getAssignments", compileFinal {
- _self call ["restorePersistedState", []];
- values (_self getOrDefault ["assignmentRegistry", createHashMap])
+ private _state = _self call ["loadState", []];
+ if !(_state getOrDefault ["success", false]) exitWith { [] };
+
+ values (_state getOrDefault ["assignments", createHashMap])
}],
["isDispatchOrder", compileFinal {
params [["_taskID", "", [""]]];
if (_taskID isEqualTo "") exitWith { false };
- ((_self getOrDefault ["dispatchOrderRegistry", createHashMap]) getOrDefault [_taskID, createHashMap]) isNotEqualTo createHashMap
+ private _state = _self call ["loadState", []];
+ if !(_state getOrDefault ["success", false]) exitWith { false };
+
+ ((_state getOrDefault ["dispatchOrders", createHashMap]) getOrDefault [_taskID, createHashMap]) isNotEqualTo createHashMap
}],
- ["restorePersistedState", compileFinal {
- if (_self getOrDefault ["persistenceLoaded", false]) exitWith { true };
+ ["getAssignmentByTaskId", compileFinal {
+ params [["_taskID", "", [""]]];
- private _persistenceService = _self getOrDefault ["persistenceService", createHashMap];
- if (_persistenceService isEqualTo createHashMap) exitWith { false };
+ if (_taskID isEqualTo "") exitWith { createHashMap };
- private _assignmentsResult = _persistenceService call ["loadAssignments", []];
- if !(_assignmentsResult getOrDefault ["success", false]) exitWith { false };
+ private _state = _self call ["loadState", []];
+ if !(_state getOrDefault ["success", false]) exitWith { createHashMap };
- private _ordersResult = _persistenceService call ["loadDispatchOrders", []];
- if !(_ordersResult getOrDefault ["success", false]) exitWith { false };
+ +((_state getOrDefault ["assignments", createHashMap]) getOrDefault [_taskID, createHashMap])
+ }],
+ ["getDispatchOrderByTaskId", compileFinal {
+ params [["_taskID", "", [""]]];
- private _assignmentRegistry = +(_assignmentsResult getOrDefault ["data", createHashMap]);
- private _dispatchOrderRegistry = +(_ordersResult getOrDefault ["data", createHashMap]);
+ if (_taskID isEqualTo "") exitWith { createHashMap };
- _self set ["assignmentRegistry", _assignmentRegistry];
- _self set ["dispatchOrderRegistry", _dispatchOrderRegistry];
- _self set ["persistenceLoaded", true];
+ private _state = _self call ["loadState", []];
+ if !(_state getOrDefault ["success", false]) exitWith { createHashMap };
+
+ +((_state getOrDefault ["dispatchOrders", createHashMap]) getOrDefault [_taskID, createHashMap])
+ }],
+ ["getCurrentTaskIdForGroup", compileFinal {
+ params [["_groupID", "", [""]]];
+
+ if (_groupID isEqualTo "") exitWith { "" };
+
+ private _state = _self call ["loadState", []];
+ if !(_state getOrDefault ["success", false]) exitWith { "" };
+
+ private _assignmentRegistry = _state getOrDefault ["assignments", createHashMap];
+ private _dispatchOrderRegistry = _state getOrDefault ["dispatchOrders", createHashMap];
+ private _taskID = "";
{
- if ((_y getOrDefault ["state", ""]) isNotEqualTo "acknowledged") then { continue; };
- if (((_y getOrDefault ["acknowledgedByUid", ""]) isEqualTo "")) then { continue; };
- if ((_dispatchOrderRegistry getOrDefault [_x, createHashMap]) isNotEqualTo createHashMap) then { continue; };
- if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) isNotEqualTo "active") then { continue; };
- EGVAR(task,TaskStore) call ["bindTaskOwnership", [_x, _y getOrDefault ["acknowledgedByUid", ""]]];
+ if ((_y getOrDefault ["groupId", ""]) isNotEqualTo _groupID) then { continue; };
+ if !((_y getOrDefault ["state", ""]) in ["assigned", "acknowledged"]) then { continue; };
+
+ private _dispatchOrder = +(_dispatchOrderRegistry getOrDefault [_x, createHashMap]);
+ if (_dispatchOrder isEqualTo createHashMap) then {
+ if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) isNotEqualTo "active") then { continue; };
+ _taskID = _x;
+ } else {
+ _taskID = _dispatchOrder getOrDefault ["title", _x];
+ };
} forEach _assignmentRegistry;
- true
+ _taskID
}],
["buildDispatchOrderEntry", compileFinal {
params [
["_taskID", "", [""]],
["_order", createHashMap, [createHashMap]],
- ["_assignmentRegistry", createHashMap, [createHashMap]],
+ ["_assignment", createHashMap, [createHashMap]],
["_groupRepository", createHashMap, [createHashMap]]
];
@@ -127,7 +180,6 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
};
};
- private _assignment = _assignmentRegistry getOrDefault [_taskID, createHashMap];
_entry set ["taskId", _taskID];
_entry set ["taskID", _taskID];
_entry set ["type", _entry getOrDefault ["type", "dispatch_order"]];
@@ -136,6 +188,23 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
_entry set ["assignmentState", [_assignment getOrDefault ["state", ""], "unassigned"] select (_assignment isEqualTo createHashMap)];
_entry
}],
+ ["buildDispatchOrderEntryForTask", compileFinal {
+ params [
+ ["_taskID", "", [""]],
+ ["_groupRepository", createHashMap, [createHashMap]]
+ ];
+
+ if (_taskID isEqualTo "") exitWith { createHashMap };
+
+ private _state = _self call ["loadState", []];
+ if !(_state getOrDefault ["success", false]) exitWith { createHashMap };
+
+ private _order = +((_state getOrDefault ["dispatchOrders", createHashMap]) getOrDefault [_taskID, createHashMap]);
+ if (_order isEqualTo createHashMap) exitWith { createHashMap };
+
+ private _assignment = +((_state getOrDefault ["assignments", createHashMap]) getOrDefault [_taskID, createHashMap]);
+ _self call ["buildDispatchOrderEntry", [_taskID, _order, _assignment, _groupRepository]]
+ }],
["assignTaskToGroup", compileFinal {
params [
["_requesterUid", "", [""]],
@@ -150,16 +219,20 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
["assignment", createHashMap]
];
- _self call ["restorePersistedState", []];
-
private _permissionService = _self getOrDefault ["permissionService", createHashMap];
if !(_permissionService call ["canDispatch", [_requesterUid]]) exitWith {
_result set ["message", "You are not authorized to assign contracts."];
_result
};
- private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap];
- private _dispatchOrderRegistry = _self getOrDefault ["dispatchOrderRegistry", createHashMap];
+ private _state = _self call ["loadState", []];
+ if !(_state getOrDefault ["success", false]) exitWith {
+ _result set ["message", "CAD extension state is unavailable."];
+ _result
+ };
+
+ private _assignmentRegistry = _state getOrDefault ["assignments", createHashMap];
+ private _dispatchOrderRegistry = _state getOrDefault ["dispatchOrders", createHashMap];
private _isDispatchOrder = (_dispatchOrderRegistry getOrDefault [_taskID, createHashMap]) isNotEqualTo createHashMap;
if (!_isDispatchOrder && { (EGVAR(task,TaskStore) call ["getTaskStatus", [_taskID]]) isNotEqualTo "active" }) exitWith {
@@ -221,9 +294,6 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
_result
};
- _assignmentRegistry set [_taskID, _assignment];
- _self set ["assignmentRegistry", _assignmentRegistry];
-
private _activityEntry = +(_assignData getOrDefault ["activity", createHashMap]);
if (_activityEntry isNotEqualTo createHashMap) then {
private _activityRepository = _self getOrDefault ["activityRepository", createHashMap];
@@ -235,6 +305,9 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
_result set ["assignment", _assignment];
_result set ["leaderUid", _leaderUid];
_result set ["isDispatchOrder", _isDispatchOrder];
+ if (_isDispatchOrder) then {
+ _result set ["order", +(_dispatchOrderRegistry getOrDefault [_taskID, createHashMap])];
+ };
_result
}],
["createDispatchOrder", compileFinal {
@@ -254,8 +327,6 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
["order", createHashMap]
];
- _self call ["restorePersistedState", []];
-
private _permissionService = _self getOrDefault ["permissionService", createHashMap];
if !(_permissionService call ["canDispatch", [_requesterUid]]) exitWith {
_result set ["message", "You are not authorized to create dispatch orders."];
@@ -337,14 +408,6 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
_result
};
- private _dispatchOrderRegistry = _self getOrDefault ["dispatchOrderRegistry", createHashMap];
- _dispatchOrderRegistry set [_taskID, _order];
- _self set ["dispatchOrderRegistry", _dispatchOrderRegistry];
-
- private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap];
- _assignmentRegistry set [_taskID, _assignment];
- _self set ["assignmentRegistry", _assignmentRegistry];
-
private _activityEntry = +(_createData getOrDefault ["activity", createHashMap]);
if (_activityEntry isNotEqualTo createHashMap) then {
private _activityRepository = _self getOrDefault ["activityRepository", createHashMap];
@@ -368,23 +431,25 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
["assignment", createHashMap]
];
- _self call ["restorePersistedState", []];
-
private _permissionService = _self getOrDefault ["permissionService", createHashMap];
if !(_permissionService call ["canDispatch", [_requesterUid]]) exitWith {
_result set ["message", "You are not authorized to close dispatch orders."];
_result
};
- private _dispatchOrderRegistry = _self getOrDefault ["dispatchOrderRegistry", createHashMap];
- private _order = +(_dispatchOrderRegistry getOrDefault [_taskID, createHashMap]);
+ private _state = _self call ["loadState", []];
+ if !(_state getOrDefault ["success", false]) exitWith {
+ _result set ["message", "CAD extension state is unavailable."];
+ _result
+ };
+
+ private _order = +((_state getOrDefault ["dispatchOrders", createHashMap]) getOrDefault [_taskID, createHashMap]);
if (_order isEqualTo createHashMap) exitWith {
_result set ["message", "Dispatch order could not be resolved."];
_result
};
- private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap];
- private _assignment = +(_assignmentRegistry getOrDefault [_taskID, createHashMap]);
+ private _assignment = +((_state getOrDefault ["assignments", createHashMap]) getOrDefault [_taskID, createHashMap]);
private _persistenceService = _self getOrDefault ["persistenceService", createHashMap];
if (_persistenceService isEqualTo createHashMap) exitWith {
@@ -399,14 +464,8 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
};
private _closeData = +(_closeResult getOrDefault ["data", createHashMap]);
- _order = +(_closeData getOrDefault ["order", _order]);
_assignment = +(_closeData getOrDefault ["assignment", _assignment]);
- _dispatchOrderRegistry deleteAt _taskID;
- _self set ["dispatchOrderRegistry", _dispatchOrderRegistry];
- _assignmentRegistry deleteAt _taskID;
- _self set ["assignmentRegistry", _assignmentRegistry];
-
private _activityEntry = +(_closeData getOrDefault ["activity", createHashMap]);
if (_activityEntry isNotEqualTo createHashMap) then {
_activityEntry set ["actorUid", _requesterUid];
@@ -430,12 +489,14 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
];
private _transition = _this param [2, "acknowledge", [""]];
+ private _state = _self call ["loadState", []];
+ if !(_state getOrDefault ["success", false]) exitWith {
+ _result set ["message", "CAD extension state is unavailable."];
+ _result
+ };
- _self call ["restorePersistedState", []];
-
- private _assignmentRegistry = _self getOrDefault ["assignmentRegistry", createHashMap];
- private _assignment = +(_assignmentRegistry getOrDefault [_taskID, createHashMap]);
- private _isDispatchOrder = _self call ["isDispatchOrder", [_taskID]];
+ private _assignment = +((_state getOrDefault ["assignments", createHashMap]) getOrDefault [_taskID, createHashMap]);
+ private _isDispatchOrder = ((_state getOrDefault ["dispatchOrders", createHashMap]) getOrDefault [_taskID, createHashMap]) isNotEqualTo createHashMap;
if (_assignment isEqualTo createHashMap) exitWith {
_result set ["message", "Task is not assigned."];
_result
@@ -508,16 +569,6 @@ GVAR(AssignmentRepositoryBaseClass) = compileFinal createHashMapFromArray [
_result
};
- switch (_transition) do {
- case "decline": {
- _assignmentRegistry deleteAt _taskID;
- };
- default {
- _assignmentRegistry set [_taskID, _assignment];
- };
- };
- _self set ["assignmentRegistry", _assignmentRegistry];
-
private _activityEntry = +(_transitionData getOrDefault ["activity", createHashMap]);
if (_activityEntry isNotEqualTo createHashMap) then {
if (_isDispatchOrder) then {
diff --git a/arma/server/addons/cad/functions/fnc_initCadStore.sqf b/arma/server/addons/cad/functions/fnc_initCadStore.sqf
index 07db1f0..ad70578 100644
--- a/arma/server/addons/cad/functions/fnc_initCadStore.sqf
+++ b/arma/server/addons/cad/functions/fnc_initCadStore.sqf
@@ -10,6 +10,11 @@
* Initializes the CAD store as a coordinator over activity, group,
* assignment, and permission domain objects.
*
+ * CAD operational state is extension-backed but intentionally transient.
+ * Orders, requests, assignments, hydrate state, and recent activity are
+ * scoped to the active server/mission lifecycle and start fresh after a
+ * restart.
+ *
* Arguments:
* None
*
@@ -133,17 +138,13 @@ GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [
private _leaderUid = _result getOrDefault ["leaderUid", ""];
if (_leaderUid isEqualTo "") exitWith { false };
+ private _assignmentRepository = _self get "AssignmentRepository";
private _message = if (_result getOrDefault ["isDispatchOrder", false]) then {
private _order = _result getOrDefault ["order", createHashMap];
if (_order isEqualTo createHashMap) then {
private _assignment = _result getOrDefault ["assignment", createHashMap];
private _taskID = _assignment getOrDefault ["taskId", ""];
- _order = (_self get "AssignmentRepository") call ["buildDispatchOrderEntry", [
- _taskID,
- ((_self get "AssignmentRepository") getOrDefault ["dispatchOrderRegistry", createHashMap]) getOrDefault [_taskID, createHashMap],
- (_self get "AssignmentRepository") getOrDefault ["assignmentRegistry", createHashMap],
- _self get "GroupRepository"
- ]];
+ _order = _assignmentRepository call ["buildDispatchOrderEntryForTask", [_taskID, _self get "GroupRepository"]];
};
format ["Dispatch order assigned: %1. Open CAD to review and acknowledge.", _order getOrDefault ["title", "Dispatch Order"]]
@@ -203,15 +204,10 @@ GVAR(CadStoreBaseClass) = compileFinal createHashMapFromArray [
private _permissionService = _self get "PermissionService";
private _groupRepository = _self get "GroupRepository";
- private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
- if (_actor isEqualTo createHashMap && { _uid isNotEqualTo "" }) then {
- _actor = EGVAR(actor,ActorStore) call ["init", [_uid]];
- };
-
private _groupID = _groupRepository call ["getPlayerGroupId", [_uid]];
private _session = createHashMapFromArray [
["uid", _uid],
- ["orgId", _actor getOrDefault ["organization", "default"]],
+ ["orgId", EGVAR(actor,ActorStore) call ["getOrganization", [_uid]]],
["isDispatcher", _permissionService call ["canDispatch", [_uid]]],
["groupId", _groupID],
["isLeader", _groupRepository call ["isGroupLeader", [_uid, _groupID]]]
diff --git a/arma/server/addons/cad/functions/fnc_initGroupRepository.sqf b/arma/server/addons/cad/functions/fnc_initGroupRepository.sqf
index 3a119a7..9d6f8dc 100644
--- a/arma/server/addons/cad/functions/fnc_initGroupRepository.sqf
+++ b/arma/server/addons/cad/functions/fnc_initGroupRepository.sqf
@@ -24,8 +24,6 @@
GVAR(GroupRepositoryBaseClass) = compileFinal createHashMapFromArray [
["#type", "CadGroupRepositoryBaseClass"],
["#create", compileFinal {
- _self set ["groupRegistry", createHashMap];
- _self set ["groupProfileRegistry", createHashMap];
_self set ["validStatuses", [
"available",
"en_route",
@@ -63,31 +61,11 @@ GVAR(GroupRepositoryBaseClass) = compileFinal createHashMapFromArray [
if (_groupID isEqualTo "") exitWith { "" };
private _assignmentRepository = _self getOrDefault ["assignmentRepository", createHashMap];
- private _assignmentRegistry = _assignmentRepository getOrDefault ["assignmentRegistry", createHashMap];
- private _dispatchOrderRegistry = _assignmentRepository getOrDefault ["dispatchOrderRegistry", createHashMap];
- private _taskID = "";
+ if (_assignmentRepository isEqualTo createHashMap) exitWith { "" };
- {
- if ((_y getOrDefault ["groupId", ""]) isNotEqualTo _groupID) then { continue; };
- if !((_y getOrDefault ["state", ""]) in ["assigned", "acknowledged"]) then { continue; };
- private _dispatchOrder = +(_dispatchOrderRegistry getOrDefault [_x, createHashMap]);
- if (_dispatchOrder isEqualTo createHashMap) then {
- if ((EGVAR(task,TaskStore) call ["getTaskStatus", [_x]]) isNotEqualTo "active") then { continue; };
- _taskID = _x;
- } else {
- _taskID = _dispatchOrder getOrDefault ["title", _x];
- };
-
- } forEach _assignmentRegistry;
-
- _taskID
+ _assignmentRepository call ["getCurrentTaskIdForGroup", [_groupID]]
}],
["syncGroups", compileFinal {
- private _assignmentRepository = _self getOrDefault ["assignmentRepository", createHashMap];
- if (_assignmentRepository isNotEqualTo createHashMap) then {
- _assignmentRepository call ["restorePersistedState", []];
- };
-
private _liveGroups = [];
{
@@ -106,16 +84,9 @@ GVAR(GroupRepositoryBaseClass) = compileFinal createHashMapFromArray [
if (_groupID isEqualTo "") then { continue; };
private _leaderUid = getPlayerUID _leader;
- private _actor = EGVAR(actor,Registry) getOrDefault [_leaderUid, createHashMap];
- if (_actor isEqualTo createHashMap && { _leaderUid isNotEqualTo "" }) then {
- _actor = EGVAR(actor,ActorStore) call ["init", [_leaderUid]];
- };
-
- private _orgID = _actor getOrDefault ["organization", "default"];
- if (_orgID isEqualTo "") then { _orgID = "default"; };
+ private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_leaderUid]];
private _memberUids = [];
private _memberRoster = [];
-
{
private _memberUid = getPlayerUID _x;
private _memberState = toLowerANSI (lifeState _x);
@@ -158,7 +129,6 @@ GVAR(GroupRepositoryBaseClass) = compileFinal createHashMapFromArray [
};
private _nextRegistry = createHashMap;
- private _profileRegistry = createHashMap;
{
if !(_x isEqualType createHashMap) then { continue; };
private _groupID = _x getOrDefault ["groupId", ""];
@@ -166,15 +136,8 @@ GVAR(GroupRepositoryBaseClass) = compileFinal createHashMapFromArray [
private _groupRecord = +_x;
_nextRegistry set [_groupID, _groupRecord];
- _profileRegistry set [_groupID, createHashMapFromArray [
- ["groupId", _groupID],
- ["role", _groupRecord getOrDefault ["role", "infantry"]],
- ["status", _groupRecord getOrDefault ["status", "available"]]
- ]];
} forEach _mergedGroups;
- _self set ["groupProfileRegistry", _profileRegistry];
- _self set ["groupRegistry", _nextRegistry];
_nextRegistry
}],
["getGroupRecord", compileFinal {
@@ -309,12 +272,6 @@ GVAR(GroupRepositoryBaseClass) = compileFinal createHashMapFromArray [
_groupRecord set ["status", _profile getOrDefault ["status", _groupRecord getOrDefault ["status", "available"]]];
_groupRecord set ["lastUpdate", serverTime];
- private _profileRegistry = _self getOrDefault ["groupProfileRegistry", createHashMap];
- _groupRegistry set [_groupID, _groupRecord];
- _self set ["groupRegistry", _groupRegistry];
- _profileRegistry set [_groupID, _profile];
- _self set ["groupProfileRegistry", _profileRegistry];
-
private _activityEntry = +(_profileData getOrDefault ["activity", createHashMap]);
if (_activityEntry isNotEqualTo createHashMap) then {
private _activityRepository = _self getOrDefault ["activityRepository", createHashMap];
diff --git a/arma/server/addons/cad/functions/fnc_initPermissionService.sqf b/arma/server/addons/cad/functions/fnc_initPermissionService.sqf
index 07fea10..f27f215 100644
--- a/arma/server/addons/cad/functions/fnc_initPermissionService.sqf
+++ b/arma/server/addons/cad/functions/fnc_initPermissionService.sqf
@@ -27,10 +27,7 @@ GVAR(PermissionServiceBaseClass) = compileFinal createHashMapFromArray [
if (_uid isEqualTo "") exitWith { false };
- private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
- if (_actor isEqualTo createHashMap) exitWith { false };
-
- private _orgID = _actor getOrDefault ["organization", "default"];
+ private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid]];
private _org = EGVAR(org,OrgStore) call ["loadById", [_orgID]];
if (_org isEqualTo createHashMap) exitWith { false };
diff --git a/arma/server/addons/cad/functions/fnc_initPersistenceService.sqf b/arma/server/addons/cad/functions/fnc_initPersistenceService.sqf
index dd4c053..e1ad1e1 100644
--- a/arma/server/addons/cad/functions/fnc_initPersistenceService.sqf
+++ b/arma/server/addons/cad/functions/fnc_initPersistenceService.sqf
@@ -10,6 +10,10 @@
* Initializes the CAD extension-state service that bridges live SQF
* state to the Rust extension for hot CAD storage and recent history.
*
+ * This is a live operational cache, not a durable persistence layer.
+ * CAD extension state is expected to reset with the current server or
+ * mission lifecycle.
+ *
* Arguments:
* None
*
diff --git a/arma/server/addons/cad/functions/fnc_initRequestRepository.sqf b/arma/server/addons/cad/functions/fnc_initRequestRepository.sqf
index 7a6f89b..eeedad4 100644
--- a/arma/server/addons/cad/functions/fnc_initRequestRepository.sqf
+++ b/arma/server/addons/cad/functions/fnc_initRequestRepository.sqf
@@ -24,8 +24,6 @@
GVAR(RequestRepositoryBaseClass) = compileFinal createHashMapFromArray [
["#type", "CadRequestRepositoryBaseClass"],
["#create", compileFinal {
- _self set ["requestRegistry", createHashMap];
- _self set ["persistenceLoaded", false];
_self set ["validTypes", [
"medevac_9line",
"ace_lace",
@@ -39,20 +37,14 @@ GVAR(RequestRepositoryBaseClass) = compileFinal createHashMapFromArray [
"emergency"
]];
}],
- ["restorePersistedState", compileFinal {
- if (_self getOrDefault ["persistenceLoaded", false]) exitWith { true };
-
+ ["loadRequestRegistry", compileFinal {
private _persistenceService = _self getOrDefault ["persistenceService", createHashMap];
- if (_persistenceService isEqualTo createHashMap) exitWith { false };
+ if (_persistenceService isEqualTo createHashMap) exitWith { createHashMap };
private _result = _persistenceService call ["loadRequests", []];
- if !(_result getOrDefault ["success", false]) exitWith { false };
+ if !(_result getOrDefault ["success", false]) exitWith { createHashMap };
- private _requestRegistry = +(_result getOrDefault ["data", createHashMap]);
-
- _self set ["requestRegistry", _requestRegistry];
- _self set ["persistenceLoaded", true];
- true
+ +(_result getOrDefault ["data", createHashMap])
}],
["submitRequest", compileFinal {
params [
@@ -68,8 +60,6 @@ GVAR(RequestRepositoryBaseClass) = compileFinal createHashMapFromArray [
["request", createHashMap]
];
- _self call ["restorePersistedState", []];
-
private _finalType = toLowerANSI _type;
if !(_finalType in (_self getOrDefault ["validTypes", []])) exitWith {
_result set ["message", "Invalid support request type."];
@@ -132,10 +122,6 @@ GVAR(RequestRepositoryBaseClass) = compileFinal createHashMapFromArray [
_result
};
- private _requestRegistry = _self getOrDefault ["requestRegistry", createHashMap];
- _requestRegistry set [_requestID, _request];
- _self set ["requestRegistry", _requestRegistry];
-
private _activityEntry = +(_submitData getOrDefault ["activity", createHashMap]);
if (_activityEntry isNotEqualTo createHashMap) then {
private _activityRepository = _self getOrDefault ["activityRepository", createHashMap];
@@ -156,9 +142,7 @@ GVAR(RequestRepositoryBaseClass) = compileFinal createHashMapFromArray [
["request", createHashMap]
];
- _self call ["restorePersistedState", []];
-
- private _requestRegistry = _self getOrDefault ["requestRegistry", createHashMap];
+ private _requestRegistry = _self call ["loadRequestRegistry", []];
private _request = +(_requestRegistry getOrDefault [_requestID, createHashMap]);
if (_request isEqualTo createHashMap) exitWith {
_result set ["message", "Support request could not be resolved."];
@@ -188,8 +172,6 @@ GVAR(RequestRepositoryBaseClass) = compileFinal createHashMapFromArray [
private _closeData = +(_closeResult getOrDefault ["data", createHashMap]);
_request = +(_closeData getOrDefault ["request", _request]);
- _requestRegistry deleteAt _requestID;
- _self set ["requestRegistry", _requestRegistry];
private _activityEntry = +(_closeData getOrDefault ["activity", createHashMap]);
if (_activityEntry isNotEqualTo createHashMap) then {
diff --git a/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf b/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf
index 830cee0..613ef18 100644
--- a/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf
+++ b/arma/server/addons/economy/functions/fnc_initFEconomyStore.sqf
@@ -25,7 +25,7 @@ GVAR(FEconomyStore) = createHashMapObject [[
["#type", "IFuelEconomy"],
["#create", {
GVAR(FuelCost) = 5;
- GVAR(FuelRegistry) = createHashMap;
+ _self set ["fuelRegistry", createHashMap];
["INFO", "Fuel Store Initialized!", nil, nil] call EFUNC(common,log);
}],
@@ -34,15 +34,17 @@ GVAR(FEconomyStore) = createHashMapObject [[
private _index = netId _target;
private _uid = getPlayerUID _unit;
+ private _fuelRegistry = _self getOrDefault ["fuelRegistry", createHashMap];
- GVAR(FuelRegistry) set [_index, _uid];
+ _fuelRegistry set [_index, _uid];
SETVAR(_target,liters,0);
}],
["stop", {
params ["_source", "_target"];
private _index = netId _target;
- private _uid = GVAR(FuelRegistry) get _index;
+ private _fuelRegistry = _self getOrDefault ["fuelRegistry", createHashMap];
+ private _uid = _fuelRegistry get _index;
private _player = [_uid] call EFUNC(common,getPlayer);
private _totalLiters = GETVAR(_target,liters,0);
@@ -51,7 +53,7 @@ GVAR(FEconomyStore) = createHashMapObject [[
private _formattedTotalLiters = _totalLiters toFixed 2;
[CRPC(notifications,recieveNotification), ["info", "Refueling", format ["Refueling complete: %1L
Total Cost: $%2", _formattedTotalLiters, _formattedTotalCost]], _player] call CFUNC(targetEvent);
- GVAR(FuelRegistry) deleteAt _index;
+ _fuelRegistry deleteAt _index;
}]
]];
diff --git a/arma/server/addons/extension/functions/fnc_extCall.sqf b/arma/server/addons/extension/functions/fnc_extCall.sqf
index e91ae1a..e7145b6 100644
--- a/arma/server/addons/extension/functions/fnc_extCall.sqf
+++ b/arma/server/addons/extension/functions/fnc_extCall.sqf
@@ -31,16 +31,18 @@ private _chunkPrefix = "FORGE_TRANSPORT_CHUNK:";
private _chunkPrefixLength = count toArray _chunkPrefix;
private _unsupportedRoutePrefix = "Error: Unsupported transport route";
private _requestChunkSize = 12000;
+// Keep bootstrap create/update calls on the direct extension path by default.
+// Actor/bank initialization payloads are small enough for normal callExtension
+// usage, and their correctness depends on preserving the native argument shape
+// of [uid, json]. Transport remains available automatically for genuinely large
+// requests through the chunked-request path below.
private _transportResponseFunctions = [
"actor:get",
- "actor:create",
- "actor:update",
"actor:hot:init",
"actor:hot:get",
+ "actor:hot:keys",
"actor:hot:save",
"bank:get",
- "bank:create",
- "bank:update",
"bank:hot:init",
"bank:hot:get",
"bank:hot:save",
@@ -126,7 +128,10 @@ private _checkRedisAvailability = {
};
private _buildTransportArgumentsJson = {
- params [["_rawArguments", [], [[]]]];
+ private _rawArguments = _this;
+ if !(_rawArguments isEqualType []) then {
+ _rawArguments = [_rawArguments];
+ };
private _stringArguments = _rawArguments apply {
if (_x isEqualType "") exitWith { _x };
@@ -161,10 +166,12 @@ if (_functionLower in ["status", "version"]) exitWith {
[_function, _arguments] call _callExtensionCommand
};
-private _argumentsJson = [_arguments] call _buildTransportArgumentsJson;
+private _argumentsJson = _arguments call _buildTransportArgumentsJson;
private _usesTransportResponse = _functionLower in _transportResponseFunctions;
private _usesChunkedRequest = (count toArray _argumentsJson) > _requestChunkSize;
+// Most calls should stay direct unless they either need chunked response
+// assembly or the request body is large enough to require staging.
if !(_usesTransportResponse || { _usesChunkedRequest }) exitWith {
[_function, _arguments] call _callExtensionCommand
};
diff --git a/arma/server/addons/locker/functions/fnc_initVAStore.sqf b/arma/server/addons/locker/functions/fnc_initVAStore.sqf
index 567010b..ecbc234 100644
--- a/arma/server/addons/locker/functions/fnc_initVAStore.sqf
+++ b/arma/server/addons/locker/functions/fnc_initVAStore.sqf
@@ -4,7 +4,7 @@
* File: fnc_initVAStore.sqf
* Author: IDSolutions
* Date: 2025-12-17
- * Last Update: 2026-04-01
+ * Last Update: 2026-04-05
* Public: No
*
* Description:
@@ -28,7 +28,7 @@ GVAR(VArsenalModel) = compileFinal createHashMapObject [[
private _vArsenal = createHashMap;
_vArsenal set ["backpacks", ["B_AssaultPack_rgr"]];
- _vArsenal set ["items", ["FirstAidKit", "G_Combat", "H_Cap_blk_ION", "H_HelmetB", "ItemCompass", "ItemGPS", "ItemMap", "ItemRadio", "ItemWatch", "U_IG_Guerrilla_6_1", "V_TacVest_oli", "ACE_EarPlugs"]];
+ _vArsenal set ["items", ["FirstAidKit", "G_Combat", "H_Cap_blk_ION", "H_HelmetB", "ItemCompass", "ItemGPS", "ItemMap", "ItemRadio", "ItemWatch", "U_BG_Guerrilla_6_1", "V_TacVest_oli", "ACE_EarPlugs"]];
_vArsenal set ["magazines", ["16Rnd_9x21_Mag", "30Rnd_65x39_caseless_black_mag", "Chemlight_blue", "Chemlight_green", "Chemlight_red", "Chemlight_yellow", "HandGrenade", "SmokeShell", "SmokeShellBlue", "SmokeShellGreen", "SmokeShellOrange", "SmokeShellPurple", "SmokeShellRed", "SmokeShellYellow"]];
_vArsenal set ["weapons", ["arifle_MX_F", "hgun_P07_F"]];
diff --git a/arma/server/addons/main/XEH_PREP.hpp b/arma/server/addons/main/XEH_PREP.hpp
index 3a1cf90..f4df82a 100644
--- a/arma/server/addons/main/XEH_PREP.hpp
+++ b/arma/server/addons/main/XEH_PREP.hpp
@@ -1,2 +1,3 @@
PREP(initStores);
+PREP(initValidationHarness);
PREP(saveHotState);
diff --git a/arma/server/addons/main/functions/fnc_initStores.sqf b/arma/server/addons/main/functions/fnc_initStores.sqf
index 69f5259..d1b9275 100644
--- a/arma/server/addons/main/functions/fnc_initStores.sqf
+++ b/arma/server/addons/main/functions/fnc_initStores.sqf
@@ -47,3 +47,6 @@ if (isNil QEGVAR(org,OrgStore)) then { call EFUNC(org,initOrgStore); };
// Store
if (isNil QEGVAR(store,StoreStore)) then { call EFUNC(store,initStoreStore); };
+
+// Validation Harness
+if (isNil QGVAR(ValidationHarness)) then { call FUNC(initValidationHarness); };
diff --git a/arma/server/addons/main/functions/fnc_initValidationHarness.sqf b/arma/server/addons/main/functions/fnc_initValidationHarness.sqf
new file mode 100644
index 0000000..786531b
--- /dev/null
+++ b/arma/server/addons/main/functions/fnc_initValidationHarness.sqf
@@ -0,0 +1,213 @@
+#include "..\script_component.hpp"
+
+/*
+ * Author: IDSolutions
+ * Initializes the server-side validation harness for targeted runtime smoke
+ * checks around high-risk multi-module flows.
+ *
+ * Arguments:
+ * None
+ *
+ * Return Value:
+ * Validation harness object
+ *
+ * Example:
+ * call forge_server_main_fnc_initValidationHarness;
+ *
+ * Public: No
+ */
+
+#pragma hemtt ignore_variables ["_self"]
+GVAR(ValidationHarness) = createHashMapObject [[
+ ["#type", "ValidationHarness"],
+ ["buildResult", compileFinal {
+ params [
+ ["_action", "", [""]],
+ ["_success", false, [false]],
+ ["_message", "", [""]],
+ ["_data", createHashMap, [createHashMap]]
+ ];
+
+ createHashMapFromArray [
+ ["action", _action],
+ ["success", _success],
+ ["message", _message],
+ ["data", _data]
+ ]
+ }],
+ ["logResult", compileFinal {
+ params [["_result", createHashMap, [createHashMap]]];
+
+ if (_result isEqualTo createHashMap) exitWith { _result };
+
+ private _level = ["WARNING", "INFO"] select (_result getOrDefault ["success", false]);
+ private _action = _result getOrDefault ["action", "validation"];
+ private _message = _result getOrDefault ["message", ""];
+ [_level, format ["Validation harness '%1': %2", _action, _message]] call EFUNC(common,log);
+
+ _result
+ }],
+ ["normalizeMapArg", compileFinal {
+ params [
+ ["_value", createHashMap, [createHashMap, ""]],
+ ["_fallback", createHashMap, [createHashMap]]
+ ];
+
+ if (_value isEqualType createHashMap) exitWith { +_value };
+ if !(_value isEqualType "") exitWith { +_fallback };
+ if (_value isEqualTo "") exitWith { +_fallback };
+
+ private _parsed = fromJSON _value;
+ if !(_parsed isEqualType createHashMap) exitWith { +_fallback };
+
+ _parsed
+ }],
+ ["run", compileFinal {
+ params [["_action", "", [""]], ["_arguments", [], [[]]]];
+
+ private _actionLower = toLowerANSI _action;
+ if (_actionLower isEqualTo "") exitWith {
+ _self call ["logResult", [_self call ["buildResult", ["unknown", false, "A validation action is required.", createHashMap]]]]
+ };
+
+ switch (_actionLower) do {
+ case "save_hot_state": {
+ _arguments params [["_uid", "", [""]]];
+
+ private _success = [_uid] call FUNC(saveHotState);
+ private _message = [
+ format ["Hot-state save failed for '%1'.", _uid],
+ format ["Hot-state save completed for '%1'.", [_uid, "all hot state"] select (_uid isEqualTo "")]
+ ] select _success;
+
+ _self call ["logResult", [_self call ["buildResult", [
+ _actionLower,
+ _success,
+ _message,
+ createHashMapFromArray [["uid", _uid]]
+ ]]]]
+ };
+ case "store_checkout": {
+ _arguments params [["_uid", "", [""]], ["_payload", createHashMap, [createHashMap, ""]]];
+
+ private _player = [_uid] call EFUNC(common,getPlayer);
+ if (_uid isEqualTo "" || { isNull _player }) exitWith {
+ _self call ["logResult", [_self call ["buildResult", [_actionLower, false, "A valid online player UID is required for store checkout validation.", createHashMap]]]]
+ };
+
+ private _payloadMap = _self call ["normalizeMapArg", [_payload, createHashMap]];
+ if (_payloadMap isEqualTo createHashMap) exitWith {
+ _self call ["logResult", [_self call ["buildResult", [_actionLower, false, "Store checkout validation payload was invalid.", createHashMap]]]]
+ };
+
+ private _result = EGVAR(store,StoreStore) call ["checkout", [_uid, _player, toJSON _payloadMap]];
+ private _success = _result getOrDefault ["success", false];
+ private _message = _result getOrDefault ["message", "Store checkout validation completed."];
+
+ _self call ["logResult", [_self call ["buildResult", [_actionLower, _success, _message, _result]]]]
+ };
+ case "org_assign_credit_line": {
+ _arguments params [
+ ["_requesterUid", "", [""]],
+ ["_memberUid", "", [""]],
+ ["_memberName", "", [""]],
+ ["_amount", 0, [0]]
+ ];
+
+ private _result = EGVAR(org,OrgStore) call ["assignCreditLine", [_requesterUid, _memberUid, _memberName, _amount]];
+ private _success = _result getOrDefault ["success", false];
+ private _message = _result getOrDefault ["message", "Credit line validation completed."];
+
+ _self call ["logResult", [_self call ["buildResult", [_actionLower, _success, _message, _result]]]]
+ };
+ case "bank_credit_repayment": {
+ _arguments params [["_uid", "", [""]], ["_amount", 0, [0]]];
+
+ if (_uid isEqualTo "") exitWith {
+ _self call ["logResult", [_self call ["buildResult", [_actionLower, false, "A valid UID is required for bank credit repayment validation.", createHashMap]]]]
+ };
+
+ private _beforeAccount = EGVAR(bank,BankStore) call ["get", [_uid, ""]];
+ private _beforeOrgState = EGVAR(bank,BankPayloadBuilder) call ["resolveOrgState", [_uid]];
+ private _success = EGVAR(bank,BankStore) call ["repayCreditLine", [_uid, _amount]];
+ private _afterAccount = EGVAR(bank,BankStore) call ["get", [_uid, ""]];
+ private _afterOrgState = EGVAR(bank,BankPayloadBuilder) call ["resolveOrgState", [_uid]];
+
+ private _message = [
+ format ["Bank credit repayment validation failed for %1.", _uid],
+ format ["Bank credit repayment validation completed for %1.", _uid]
+ ] select _success;
+
+ _self call ["logResult", [_self call ["buildResult", [
+ _actionLower,
+ _success,
+ _message,
+ createHashMapFromArray [
+ ["beforeAccount", _beforeAccount],
+ ["afterAccount", _afterAccount],
+ ["beforeOrgState", _beforeOrgState],
+ ["afterOrgState", _afterOrgState]
+ ]
+ ]]]]
+ };
+ case "task_reward_context": {
+ _arguments params [["_taskID", "", [""]]];
+
+ private _context = EGVAR(task,TaskStore) call ["resolveRewardContext", [_taskID]];
+ private _success = _taskID isNotEqualTo "" && { (_context getOrDefault ["orgID", ""]) isNotEqualTo "" };
+ private _message = [
+ format ["No reward context was available for task %1.", _taskID],
+ format ["Resolved reward context for task %1.", _taskID]
+ ] select _success;
+
+ _self call ["logResult", [_self call ["buildResult", [_actionLower, _success, _message, _context]]]]
+ };
+ case "task_apply_rating": {
+ _arguments params [["_taskID", "", [""]], ["_delta", 0, [0]]];
+
+ private _result = EGVAR(task,TaskStore) call ["applyRatingOutcome", [_taskID, _delta]];
+ private _success = _result getOrDefault ["success", true];
+ private _message = [
+ _result getOrDefault ["message", format ["Task rating validation failed for %1.", _taskID]],
+ format ["Task rating validation completed for %1.", _taskID]
+ ] select _success;
+
+ _self call ["logResult", [_self call ["buildResult", [_actionLower, _success, _message, _result]]]]
+ };
+ case "task_apply_rewards": {
+ _arguments params [["_taskID", "", [""]], ["_rewards", createHashMap, [createHashMap, ""]]];
+
+ private _rewardsMap = _self call ["normalizeMapArg", [_rewards, createHashMap]];
+ if (_taskID isEqualTo "" || { _rewardsMap isEqualTo createHashMap }) exitWith {
+ _self call ["logResult", [_self call ["buildResult", [_actionLower, false, "Task reward validation requires a task ID and reward payload.", createHashMap]]]]
+ };
+
+ private _rewardContext = EGVAR(task,TaskStore) call ["resolveRewardContext", [_taskID]];
+ private _beforeOrg = EGVAR(org,OrgStore) call ["loadById", [_rewardContext getOrDefault ["orgID", ""]]];
+ private _success = [_taskID, _rewardsMap] call EFUNC(task,handleTaskRewards);
+ private _afterOrg = EGVAR(org,OrgStore) call ["loadById", [_rewardContext getOrDefault ["orgID", ""]]];
+
+ private _message = [
+ format ["Task reward validation failed for %1.", _taskID],
+ format ["Task reward validation completed for %1.", _taskID]
+ ] select _success;
+
+ _self call ["logResult", [_self call ["buildResult", [
+ _actionLower,
+ _success,
+ _message,
+ createHashMapFromArray [
+ ["rewardContext", _rewardContext],
+ ["beforeOrg", _beforeOrg],
+ ["afterOrg", _afterOrg]
+ ]
+ ]]]]
+ };
+ default {
+ _self call ["logResult", [_self call ["buildResult", [_actionLower, false, format ["Unknown validation action '%1'.", _actionLower], createHashMap]]]]
+ };
+ };
+ }]
+]];
+
+GVAR(ValidationHarness)
diff --git a/arma/server/addons/main/functions/fnc_saveHotState.sqf b/arma/server/addons/main/functions/fnc_saveHotState.sqf
index 5fbfb98..dc819da 100644
--- a/arma/server/addons/main/functions/fnc_saveHotState.sqf
+++ b/arma/server/addons/main/functions/fnc_saveHotState.sqf
@@ -28,12 +28,12 @@ if (_uid isEqualTo "") then {
};
} forEach allPlayers;
- if !(isNil QEGVAR(actor,Registry)) then {
+ if !(isNil QEGVAR(actor,ActorStore)) then {
{
if (_x isNotEqualTo "") then {
_uids pushBackUnique _x;
};
- } forEach keys EGVAR(actor,Registry);
+ } forEach (EGVAR(actor,ActorStore) call ["listHotUids", []]);
};
} else {
_uids pushBack _uid;
diff --git a/arma/server/addons/org/XEH_preInit.sqf b/arma/server/addons/org/XEH_preInit.sqf
index fcc4c80..77cefde 100644
--- a/arma/server/addons/org/XEH_preInit.sqf
+++ b/arma/server/addons/org/XEH_preInit.sqf
@@ -85,6 +85,127 @@ PREP_RECOMPILE_END;
]], _requester] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
+[QGVAR(requestInviteOrgMember), {
+ params [["_uid", "", [""]], ["_targetUid", "", [""]], ["_targetName", "", [""]]];
+
+ if (_uid isEqualTo "" || { _targetUid isEqualTo "" }) exitWith {
+ diag_log "[FORGE:Server:Org] Invalid org invite request payload!"
+ };
+
+ private _requester = [_uid] call EFUNC(common,getPlayer);
+ if (_requester isEqualTo objNull) exitWith {};
+
+ private _result = GVAR(OrgStore) call ["inviteMember", [_uid, _targetUid, _targetName]];
+ if (_result getOrDefault ["success", false]) then {
+ {
+ private _memberPlayer = [_x] call EFUNC(common,getPlayer);
+ if (_memberPlayer isNotEqualTo objNull) then {
+ [CRPC(org,responseSyncOrg), [createHashMap], _memberPlayer] call CFUNC(targetEvent);
+ };
+ } forEach [_uid, _result getOrDefault ["targetUid", _targetUid]];
+
+ private _targetPlayer = [_result getOrDefault ["targetUid", _targetUid]] call EFUNC(common,getPlayer);
+ if (_targetPlayer isNotEqualTo objNull) then {
+ [CRPC(notifications,recieveNotification), [
+ "info",
+ "Organization Invite",
+ "You received an organization invite. Open the organization portal to accept or decline it.",
+ 7000
+ ], _targetPlayer] call CFUNC(targetEvent);
+ };
+ };
+
+ [CRPC(org,responseInviteOrg), [createHashMapFromArray [
+ ["success", _result getOrDefault ["success", false]],
+ ["message", _result getOrDefault ["message", "Unable to send organization invite."]]
+ ]], _requester] call CFUNC(targetEvent);
+}] call CFUNC(addEventHandler);
+
+[QGVAR(requestAcceptOrgInvite), {
+ params [["_uid", "", [""]], ["_orgID", "", [""]]];
+
+ if (_uid isEqualTo "" || { _orgID isEqualTo "" }) exitWith {
+ diag_log "[FORGE:Server:Org] Invalid accept invite request payload!"
+ };
+
+ private _player = [_uid] call EFUNC(common,getPlayer);
+ if (_player isEqualTo objNull) exitWith {};
+
+ private _result = GVAR(OrgStore) call ["acceptInvite", [_uid, _orgID]];
+ if (_result getOrDefault ["success", false]) then {
+ private _actorPatch = _result getOrDefault ["actorPatch", createHashMap];
+ if (_actorPatch isNotEqualTo createHashMap) then {
+ [CRPC(actor,responseSyncActor), [_actorPatch], _player] call CFUNC(targetEvent);
+ };
+
+ private _syncTargets = [_uid];
+ {
+ private _orgData = GVAR(OrgStore) call ["loadById", [_x]];
+ if !(_orgData isEqualType createHashMap) then { continue; };
+
+ {
+ private _memberUid = _y getOrDefault ["uid", ""];
+ if (_memberUid isNotEqualTo "") then {
+ _syncTargets pushBackUnique _memberUid;
+ };
+ } forEach (_orgData getOrDefault ["members", createHashMap]);
+ } forEach (_result getOrDefault ["affectedOrgIds", []]);
+
+ {
+ private _memberPlayer = [_x] call EFUNC(common,getPlayer);
+ if (_memberPlayer isNotEqualTo objNull) then {
+ [CRPC(org,responseSyncOrg), [createHashMap], _memberPlayer] call CFUNC(targetEvent);
+ };
+ } forEach _syncTargets;
+ };
+
+ [CRPC(org,responseInviteDecision), [createHashMapFromArray [
+ ["success", _result getOrDefault ["success", false]],
+ ["message", _result getOrDefault ["message", "Unable to accept organization invite."]],
+ ["action", "accept"]
+ ]], _player] call CFUNC(targetEvent);
+}] call CFUNC(addEventHandler);
+
+[QGVAR(requestDeclineOrgInvite), {
+ params [["_uid", "", [""]], ["_orgID", "", [""]]];
+
+ if (_uid isEqualTo "" || { _orgID isEqualTo "" }) exitWith {
+ diag_log "[FORGE:Server:Org] Invalid decline invite request payload!"
+ };
+
+ private _player = [_uid] call EFUNC(common,getPlayer);
+ if (_player isEqualTo objNull) exitWith {};
+
+ private _result = GVAR(OrgStore) call ["declineInvite", [_uid, _orgID]];
+ if (_result getOrDefault ["success", false]) then {
+ private _syncTargets = [_uid];
+ {
+ private _orgData = GVAR(OrgStore) call ["loadById", [_x]];
+ if !(_orgData isEqualType createHashMap) then { continue; };
+
+ {
+ private _memberUid = _y getOrDefault ["uid", ""];
+ if (_memberUid isNotEqualTo "") then {
+ _syncTargets pushBackUnique _memberUid;
+ };
+ } forEach (_orgData getOrDefault ["members", createHashMap]);
+ } forEach (_result getOrDefault ["affectedOrgIds", []]);
+
+ {
+ private _memberPlayer = [_x] call EFUNC(common,getPlayer);
+ if (_memberPlayer isNotEqualTo objNull) then {
+ [CRPC(org,responseSyncOrg), [createHashMap], _memberPlayer] call CFUNC(targetEvent);
+ };
+ } forEach _syncTargets;
+ };
+
+ [CRPC(org,responseInviteDecision), [createHashMapFromArray [
+ ["success", _result getOrDefault ["success", false]],
+ ["message", _result getOrDefault ["message", "Unable to decline organization invite."]],
+ ["action", "decline"]
+ ]], _player] call CFUNC(targetEvent);
+}] call CFUNC(addEventHandler);
+
[QGVAR(requestLeaveOrg), {
params [["_uid", "", [""]]];
diff --git a/arma/server/addons/org/functions/fnc_initOrgStore.sqf b/arma/server/addons/org/functions/fnc_initOrgStore.sqf
index 230564e..e1a8dbd 100644
--- a/arma/server/addons/org/functions/fnc_initOrgStore.sqf
+++ b/arma/server/addons/org/functions/fnc_initOrgStore.sqf
@@ -4,7 +4,7 @@
* File: fnc_initOrgStore.sqf
* Author: IDSolutions
* Date: 2026-02-13
- * Last Update: 2026-04-01
+ * Last Update: 2026-04-04
* Public: Yes
*
* Description:
@@ -36,6 +36,7 @@ GVAR(OrgModel) = compileFinal createHashMapObject [[
_org set ["assets", createHashMap];
_org set ["fleet", createHashMap];
_org set ["members", createHashMap];
+ _org set ["pending_invites", createHashMap];
_org
}],
@@ -111,6 +112,13 @@ GVAR(OrgModel) = compileFinal createHashMapObject [[
_org set ["credit_lines", _creditLines];
+ private _pendingInvites = _org getOrDefault ["pending_invites", createHashMap];
+ if !(_pendingInvites isEqualType createHashMap) then {
+ _pendingInvites = createHashMap;
+ };
+
+ _org set ["pending_invites", _pendingInvites];
+
_org
}],
["validate", compileFinal {
@@ -158,7 +166,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["credit_lines", createHashMap],
["assets", createHashMap],
["fleet", createHashMap],
- ["members", createHashMap]
+ ["members", createHashMap],
+ ["pending_invites", createHashMap]
];
_defaultOrg
};
@@ -173,7 +182,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["credit_lines", createHashMap],
["assets", createHashMap],
["fleet", createHashMap],
- ["members", createHashMap]
+ ["members", createHashMap],
+ ["pending_invites", createHashMap]
];
private _defaultJson = _self call ["toJSON", [_defaultOrg]];
@@ -191,7 +201,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["credit_lines", createHashMap],
["assets", createHashMap],
["fleet", createHashMap],
- ["members", createHashMap]
+ ["members", createHashMap],
+ ["pending_invites", createHashMap]
];
};
@@ -240,6 +251,24 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_data
}],
+ ["callHotOrgArray", compileFinal {
+ params [["_function", "", [""]], ["_arguments", [], [[]]]];
+
+ if (_function isEqualTo "") exitWith { [] };
+
+ [_function, _arguments] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
+ if !(_isSuccess) exitWith { [] };
+ if !(_result isEqualType "") exitWith { [] };
+ if ((_result find "Error:") == 0) exitWith {
+ ["ERROR", format ["Org extension call '%1' failed: %2", _function, _result]] call EFUNC(common,log);
+ []
+ };
+
+ private _data = fromJSON _result;
+ if !(_data isEqualType []) exitWith { [] };
+
+ _data
+ }],
["syncHotOrg", compileFinal {
params [["_org", createHashMap, [createHashMap]]];
@@ -256,16 +285,13 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
if (_uid isEqualTo "") exitWith { "default" };
- private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
- private _orgID = _actor getOrDefault ["organization", "default"];
- if (_orgID isEqualTo "") then { _orgID = "default"; };
- _orgID
+ EGVAR(actor,ActorStore) call ["getOrganization", [_uid]]
}],
["resolveActorName", compileFinal {
params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]];
private _memberName = _actor getOrDefault ["name", ""];
- if (_memberName isEqualTo "" && { _player isNotEqualTo objNull }) then {
+ if ((_memberName isEqualTo "" || { toLowerANSI _memberName isEqualTo "unknown" }) && { _player isNotEqualTo objNull }) then {
_memberName = name _player;
};
if (_memberName isEqualTo "") then { _memberName = "Unknown"; };
@@ -276,8 +302,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
if (_uid isEqualTo "" || { _orgID isEqualTo "" }) exitWith { createHashMap };
- private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", _orgID, false]];
- private _updatedActor = EGVAR(actor,ActorStore) call ["get", [_uid, ""]];
+ private _actorPatch = EGVAR(actor,ActorStore) call ["set", [_uid, "organization", _orgID, true]];
+ private _updatedActor = EGVAR(actor,ActorStore) call ["load", [_uid]];
if (
!(_updatedActor isEqualType createHashMap)
|| { _updatedActor isEqualTo createHashMap }
@@ -290,7 +316,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
};
_forcedActor set ["organization", _orgID];
- _updatedActor = EGVAR(actor,ActorStore) call ["override", [_uid, _forcedActor, false]];
+ _updatedActor = EGVAR(actor,ActorStore) call ["override", [_uid, _forcedActor, true]];
if (_updatedActor isEqualType createHashMap && { _updatedActor isNotEqualTo createHashMap }) then {
_actorPatch = createHashMapFromArray [["organization", _orgID]];
};
@@ -359,6 +385,203 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_self call ["callHotOrg", ["org:hot:ensure_member", [toJSON _context]]]
}],
+ ["listMemberInvites", compileFinal {
+ params [["_uid", "", [""]]];
+
+ if (_uid isEqualTo "") exitWith { [] };
+
+ _self call ["callHotOrgArray", ["org:hot:member_invites", [_uid]]]
+ }],
+ ["inviteMember", compileFinal {
+ params [["_requesterUid", "", [""]], ["_targetUid", "", [""]], ["_targetName", "", [""]]];
+
+ private _result = createHashMapFromArray [
+ ["success", false],
+ ["message", ""],
+ ["targetUid", _targetUid],
+ ["persisted", false],
+ ["persistenceMessage", ""]
+ ];
+
+ if (_requesterUid isEqualTo "" || { _targetUid isEqualTo "" }) exitWith {
+ _result set ["message", "A valid organization invite target is required."];
+ _result
+ };
+
+ private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer);
+ private _requesterActor = EGVAR(actor,ActorStore) call ["load", [_requesterUid]];
+ private _requesterOrgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]];
+ private _requesterName = _self call ["resolveActorName", [_requesterUid, _requesterPlayer, _requesterActor]];
+ private _requesterIsDefaultOrgCeo = (
+ _requesterPlayer isNotEqualTo objNull
+ && { _requesterOrgID isEqualTo "default" }
+ && { toLowerANSI (vehicleVarName _requesterPlayer) isEqualTo "ceo" }
+ );
+ private _targetOrgID = EGVAR(actor,ActorStore) call ["getOrganization", [_targetUid]];
+
+ private _context = createHashMapFromArray [
+ ["requesterUid", _requesterUid],
+ ["requesterName", _requesterName],
+ ["orgId", _requesterOrgID],
+ ["requesterIsDefaultOrgCeo", _requesterIsDefaultOrgCeo],
+ ["targetUid", _targetUid],
+ ["targetName", _targetName],
+ ["targetOrgId", _targetOrgID]
+ ];
+
+ private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:invite_member", [toJSON _context]]];
+ if (_envelope isEqualTo createHashMap) exitWith {
+ _result set ["message", "Unable to send organization invite."];
+ _result
+ };
+
+ _result set ["success", true];
+ _result set ["message", _envelope getOrDefault ["message", "Invitation sent."]];
+ _result set ["targetUid", _envelope getOrDefault ["targetUid", _targetUid]];
+ _self call ["persistMutationResult", [_requesterOrgID, _result, "Organization invite"]]
+ }],
+ ["acceptInvite", compileFinal {
+ params [["_requesterUid", "", [""]], ["_orgID", "", [""]]];
+
+ private _result = createHashMapFromArray [
+ ["success", false],
+ ["message", ""],
+ ["actorPatch", createHashMap],
+ ["affectedOrgIds", []]
+ ];
+
+ if (_requesterUid isEqualTo "" || { _orgID isEqualTo "" }) exitWith {
+ _result set ["message", "A valid invite selection is required."];
+ _result
+ };
+
+ private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer);
+ private _requesterActor = EGVAR(actor,ActorStore) call ["load", [_requesterUid]];
+ private _requesterName = _self call ["resolveActorName", [_requesterUid, _requesterPlayer, _requesterActor]];
+ private _existingOrgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]];
+ private _context = createHashMapFromArray [
+ ["requesterUid", _requesterUid],
+ ["requesterName", _requesterName],
+ ["orgId", _orgID],
+ ["existingOrgId", _existingOrgID]
+ ];
+
+ ["org:hot:accept_invite", [toJSON _context]] call EFUNC(extension,extCall) params ["_rawResult", "_isSuccess"];
+ if !_isSuccess exitWith {
+ _result set ["message", "Organization invite service was unavailable."];
+ _result
+ };
+ if !(_rawResult isEqualType "") exitWith {
+ _result set ["message", "Organization invite service returned an invalid response."];
+ _result
+ };
+ if ((_rawResult find "Error:") == 0) exitWith {
+ _result set ["message", _rawResult select [7]];
+ _result
+ };
+
+ private _envelope = fromJSON _rawResult;
+ if !(_envelope isEqualType createHashMap) exitWith {
+ _result set ["message", "Organization invite service returned malformed data."];
+ _result
+ };
+
+ private _invitedOrg = _self call ["syncHotOrg", [_envelope getOrDefault ["invitedOrg", createHashMap]]];
+ if (_invitedOrg isNotEqualTo createHashMap) then {
+ _envelope set ["invitedOrg", _invitedOrg];
+ };
+
+ private _previousOrgData = _envelope getOrDefault ["previousOrg", createHashMap];
+ if (_previousOrgData isEqualType createHashMap && { _previousOrgData isNotEqualTo createHashMap }) then {
+ private _syncedPreviousOrg = _self call ["syncHotOrg", [_previousOrgData]];
+ if (_syncedPreviousOrg isNotEqualTo createHashMap) then {
+ _envelope set ["previousOrg", _syncedPreviousOrg];
+ };
+ };
+
+ private _actorOrg = _envelope getOrDefault ["actorOrganization", _orgID];
+ private _actorPatch = _self call ["applyActorOrganization", [_requesterUid, _actorOrg, _requesterActor]];
+ if (_actorPatch isEqualTo createHashMap) exitWith {
+ _result set ["message", "Failed to assign the player to the invited organization."];
+ _result
+ };
+
+ private _affectedOrgIds = [_actorOrg];
+ private _previousOrg = _envelope getOrDefault ["previousOrg", createHashMap];
+ if (_previousOrg isEqualType createHashMap && { _previousOrg isNotEqualTo createHashMap }) then {
+ private _previousOrgID = _previousOrg getOrDefault ["id", ""];
+ if (_previousOrgID isNotEqualTo "") then {
+ _affectedOrgIds pushBackUnique _previousOrgID;
+ };
+ };
+
+ {
+ _self call ["saveById", [_x]];
+ } forEach _affectedOrgIds;
+
+ _result set ["success", true];
+ _result set ["message", _envelope getOrDefault ["message", "Organization invite accepted."]];
+ _result set ["actorPatch", _actorPatch];
+ _result set ["affectedOrgIds", _affectedOrgIds];
+ _result
+ }],
+ ["declineInvite", compileFinal {
+ params [["_requesterUid", "", [""]], ["_orgID", "", [""]]];
+
+ private _result = createHashMapFromArray [
+ ["success", false],
+ ["message", ""],
+ ["affectedOrgIds", []]
+ ];
+
+ if (_requesterUid isEqualTo "" || { _orgID isEqualTo "" }) exitWith {
+ _result set ["message", "A valid invite selection is required."];
+ _result
+ };
+
+ private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer);
+ private _requesterActor = EGVAR(actor,ActorStore) call ["load", [_requesterUid]];
+ private _requesterName = _self call ["resolveActorName", [_requesterUid, _requesterPlayer, _requesterActor]];
+ private _existingOrgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]];
+ private _context = createHashMapFromArray [
+ ["requesterUid", _requesterUid],
+ ["requesterName", _requesterName],
+ ["orgId", _orgID],
+ ["existingOrgId", _existingOrgID]
+ ];
+
+ ["org:hot:decline_invite", [toJSON _context]] call EFUNC(extension,extCall) params ["_rawResult", "_isSuccess"];
+ if !_isSuccess exitWith {
+ _result set ["message", "Organization invite service was unavailable."];
+ _result
+ };
+ if !(_rawResult isEqualType "") exitWith {
+ _result set ["message", "Organization invite service returned an invalid response."];
+ _result
+ };
+ if ((_rawResult find "Error:") == 0) exitWith {
+ _result set ["message", _rawResult select [7]];
+ _result
+ };
+
+ private _envelope = fromJSON _rawResult;
+ if !(_envelope isEqualType createHashMap) exitWith {
+ _result set ["message", "Organization invite service returned malformed data."];
+ _result
+ };
+
+ private _invitedOrg = _self call ["syncHotOrg", [_envelope getOrDefault ["invitedOrg", createHashMap]]];
+ if (_invitedOrg isNotEqualTo createHashMap) then {
+ _envelope set ["invitedOrg", _invitedOrg];
+ };
+
+ _self call ["saveById", [_orgID]];
+
+ _result set ["success", true];
+ _result set ["message", _envelope getOrDefault ["message", "Organization invite declined."]];
+ _result set ["affectedOrgIds", [_orgID]];
+ _result
+ }],
["leave", compileFinal {
params [["_uid", "", [""]]];
@@ -375,8 +598,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
};
private _player = [_uid] call EFUNC(common,getPlayer);
- private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
- private _orgID = _actor getOrDefault ["organization", "default"];
+ private _actor = EGVAR(actor,ActorStore) call ["load", [_uid]];
+ private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid]];
private _memberName = _self call ["resolveActorName", [_uid, _player, _actor]];
private _context = createHashMapFromArray [
["requesterUid", _uid],
@@ -418,8 +641,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
};
private _player = [_uid] call EFUNC(common,getPlayer);
- private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
- private _orgID = _actor getOrDefault ["organization", "default"];
+ private _actor = EGVAR(actor,ActorStore) call ["load", [_uid]];
+ private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid]];
private _memberName = _self call ["resolveActorName", [_uid, _player, _actor]];
private _context = createHashMapFromArray [
["requesterUid", _uid],
@@ -438,7 +661,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
private _memberUid = _x getOrDefault ["uid", ""];
if (_memberUid isEqualTo "") then { continue; };
- private _memberActor = EGVAR(actor,Registry) getOrDefault [_memberUid, createHashMap];
+ private _memberActor = EGVAR(actor,ActorStore) call ["load", [_memberUid]];
private _actorPatch = _self call ["applyActorOrganization", [_memberUid, _x getOrDefault ["actorOrganization", "default"], _memberActor]];
if (_actorPatch isEqualTo createHashMap) then {
["WARNING", format ["Failed to restore actor organization for %1 after org disband.", _memberUid]] call EFUNC(common,log);
@@ -471,7 +694,9 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["success", false],
["message", ""],
["patch", createHashMap],
- ["memberUids", []]
+ ["memberUids", []],
+ ["persisted", false],
+ ["persistenceMessage", ""]
];
if (_requesterUid isEqualTo "" || { _memberUid isEqualTo "" } || { _amount <= 0 }) exitWith {
@@ -479,10 +704,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_result
};
- private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
- private _orgID = _requesterActor getOrDefault ["organization", "default"];
- if (_orgID isEqualTo "") then { _orgID = "default"; };
-
+ private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]];
private _requesterPlayer = [_requesterUid] call EFUNC(common,getPlayer);
private _requesterIsDefaultOrgCeo = (
_requesterPlayer isNotEqualTo objNull
@@ -509,7 +731,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_result set ["message", _envelope getOrDefault ["message", "Credit line assigned."]];
_result set ["patch", _envelope getOrDefault ["patch", createHashMap]];
_result set ["memberUids", _envelope getOrDefault ["memberUids", []]];
- _result
+ _self call ["persistMutationResult", [_orgID, _result, "Credit line assignment"]]
}],
["repayCreditLine", compileFinal {
params [["_requesterUid", "", [""]], ["_amount", 0, [0]]];
@@ -518,7 +740,9 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["success", false],
["message", ""],
["patch", createHashMap],
- ["memberUids", []]
+ ["memberUids", []],
+ ["persisted", false],
+ ["persistenceMessage", ""]
];
if (_requesterUid isEqualTo "" || { _amount <= 0 }) exitWith {
@@ -526,10 +750,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_result
};
- private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
- private _orgID = _requesterActor getOrDefault ["organization", "default"];
- if (_orgID isEqualTo "") then { _orgID = "default"; };
-
+ private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]];
private _context = createHashMapFromArray [
["requesterUid", _requesterUid],
["orgId", _orgID],
@@ -546,13 +767,38 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_result set ["message", _envelope getOrDefault ["message", "Credit repayment posted."]];
_result set ["patch", _envelope getOrDefault ["patch", createHashMap]];
_result set ["memberUids", _envelope getOrDefault ["memberUids", []]];
- _result
+ _self call ["persistMutationResult", [_orgID, _result, "Credit repayment"]]
}],
["buildPortalPayload", compileFinal {
params [["_uid", "", [""]]];
GVAR(OrgPayloadBuilder) call ["buildPortalPayload", [_uid]]
}],
+ ["persistMutationResult", compileFinal {
+ params [
+ ["_orgID", "", [""]],
+ ["_result", createHashMap, [createHashMap]],
+ ["_actionLabel", "Organization update", [""]]
+ ];
+
+ if (_orgID isEqualTo "" || { _result isEqualTo createHashMap }) exitWith { _result };
+
+ if !(_result getOrDefault ["success", false]) exitWith { _result };
+
+ _result set ["persisted", false];
+ _result set ["persistenceMessage", ""];
+
+ private _savedOrg = _self call ["saveById", [_orgID]];
+ if (_savedOrg isEqualTo createHashMap) exitWith {
+ private _message = format ["%1 applied, but durable save failed for organization %2.", _actionLabel, _orgID];
+ ["ERROR", _message] call EFUNC(common,log);
+ _result set ["persistenceMessage", _message];
+ _result
+ };
+
+ _result set ["persisted", true];
+ _result
+ }],
["chargeCheckout", compileFinal {
params [["_requesterUid", "", [""]], ["_requesterPlayer", objNull, [objNull]], ["_source", "org_funds", [""]], ["_amount", 0, [0]], ["_commit", false, [false]]];
@@ -560,13 +806,12 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["success", false],
["message", "Unable to process organization payment."],
["patch", createHashMap],
- ["memberUids", []]
+ ["memberUids", []],
+ ["persisted", false],
+ ["persistenceMessage", ""]
];
- private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
- private _orgID = _requesterActor getOrDefault ["organization", "default"];
- if (_orgID isEqualTo "") then { _orgID = "default"; };
-
+ private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]];
private _requesterIsDefaultOrgCeo = (
_requesterPlayer isNotEqualTo objNull
&& { _orgID isEqualTo "default" }
@@ -589,7 +834,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_result set ["message", _envelope getOrDefault ["message", ""]];
_result set ["patch", _envelope getOrDefault ["patch", createHashMap]];
_result set ["memberUids", _envelope getOrDefault ["memberUids", []]];
- _result
+ _self call ["persistMutationResult", [_orgID, _result, "Organization checkout charge"]]
}],
["saveById", compileFinal {
params [["_orgID", "", [""]]];
@@ -605,7 +850,9 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["success", false],
["message", "Unable to update organization assets."],
["patch", createHashMap],
- ["memberUids", []]
+ ["memberUids", []],
+ ["persisted", false],
+ ["persistenceMessage", ""]
];
if (_assets isEqualTo []) exitWith {
@@ -616,8 +863,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
private _resolvedOrgID = _orgID;
if (_resolvedOrgID isEqualTo "") then {
- private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
- _resolvedOrgID = _requesterActor getOrDefault ["organization", "default"];
+ _resolvedOrgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]];
};
if (_resolvedOrgID isEqualTo "") then { _resolvedOrgID = "default"; };
@@ -644,7 +890,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_result set ["message", _envelope getOrDefault ["message", ""]];
_result set ["patch", _envelope getOrDefault ["patch", createHashMap]];
_result set ["memberUids", _envelope getOrDefault ["memberUids", []]];
- _result
+ _self call ["persistMutationResult", [_resolvedOrgID, _result, "Organization asset update"]]
}],
["addFleetVehicles", compileFinal {
params [["_requesterUid", "", [""]], ["_vehicles", [], [[]]], ["_commit", false, [false]], ["_orgID", "", [""]]];
@@ -653,7 +899,9 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["success", false],
["message", "Unable to update organization fleet."],
["patch", createHashMap],
- ["memberUids", []]
+ ["memberUids", []],
+ ["persisted", false],
+ ["persistenceMessage", ""]
];
if (_vehicles isEqualTo []) exitWith {
@@ -664,8 +912,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
private _resolvedOrgID = _orgID;
if (_resolvedOrgID isEqualTo "") then {
- private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
- _resolvedOrgID = _requesterActor getOrDefault ["organization", "default"];
+ _resolvedOrgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]];
};
if (_resolvedOrgID isEqualTo "") then { _resolvedOrgID = "default"; };
@@ -691,7 +938,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_result set ["message", _envelope getOrDefault ["message", ""]];
_result set ["patch", _envelope getOrDefault ["patch", createHashMap]];
_result set ["memberUids", _envelope getOrDefault ["memberUids", []]];
- _result
+ _self call ["persistMutationResult", [_resolvedOrgID, _result, "Organization fleet update"]]
}],
["loadById", compileFinal {
params [["_orgID", "", [""]]];
@@ -715,10 +962,9 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_result
};
- private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
- private _existingOrgID = _actor getOrDefault ["organization", ""];
-
- private _orgID = _actor getOrDefault ["phone_number", ""];
+ private _actor = EGVAR(actor,ActorStore) call ["load", [_uid]];
+ private _existingOrgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid, ""]];
+ private _orgID = EGVAR(actor,ActorStore) call ["getPhoneNumber", [_uid]];
if (_orgID isEqualTo "") exitWith {
_result set ["message", "Player phone number was not available for organization registration."];
_result
@@ -732,12 +978,35 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["existingOrgId", _existingOrgID]
];
- private _envelope = _self call ["callHotOrgEnvelope", ["org:hot:register", [toJSON _context]]];
- if (_envelope isEqualTo createHashMap) exitWith {
- _result set ["message", "Organization registration failed."];
+ ["org:hot:register", [toJSON _context]] call EFUNC(extension,extCall) params ["_rawResult", "_isSuccess"];
+ if !_isSuccess exitWith {
+ _result set ["message", "Organization service was unavailable during registration."];
_result
};
+ if !(_rawResult isEqualType "") exitWith {
+ _result set ["message", "Organization service returned an invalid registration response."];
+ _result
+ };
+
+ if ((_rawResult find "Error:") == 0) exitWith {
+ _result set ["message", _rawResult select [7]];
+ _result
+ };
+
+ private _envelope = fromJSON _rawResult;
+ if !(_envelope isEqualType createHashMap) exitWith {
+ _result set ["message", "Organization service returned malformed registration data."];
+ _result
+ };
+
+ if ("org" in _envelope) then {
+ private _syncedOrg = _self call ["syncHotOrg", [_envelope getOrDefault ["org", createHashMap]]];
+ if (_syncedOrg isNotEqualTo createHashMap) then {
+ _envelope set ["org", _syncedOrg];
+ };
+ };
+
private _actorPatch = _self call ["applyActorOrganization", [_uid, _envelope getOrDefault ["actorOrganization", _orgID], _actor]];
if (_actorPatch isEqualTo createHashMap) exitWith {
_result set ["message", "Failed to assign the player to the new organization."];
@@ -754,12 +1023,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
params [["_uid", "", [""]]];
private _player = [_uid] call EFUNC(common,getPlayer);
- private _actor = EGVAR(actor,Registry) get _uid;
- private _orgID = _actor get "organization";
- if (_orgID isEqualTo "") then {
- _orgID = "default";
- };
-
+ private _actor = EGVAR(actor,ActorStore) call ["load", [_uid]];
+ private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid]];
private _finalOrg = _self call ["loadById", [_orgID]];
if (_finalOrg isEqualTo createHashMap) then {
["WARNING", format ["No existing org found for %1, using default org.", _uid]] call EFUNC(common,log);
diff --git a/arma/server/addons/org/functions/fnc_initPayloadBuilder.sqf b/arma/server/addons/org/functions/fnc_initPayloadBuilder.sqf
index 19fe960..c1a25df 100644
--- a/arma/server/addons/org/functions/fnc_initPayloadBuilder.sqf
+++ b/arma/server/addons/org/functions/fnc_initPayloadBuilder.sqf
@@ -152,6 +152,64 @@ GVAR(OrgPayloadBuilder) = createHashMapObject [[
_creditLinesList
}],
+ ["buildPendingInvitesList", compileFinal {
+ params [["_pendingInvitesRaw", [], [[]]]];
+
+ private _pendingInvites = [];
+ {
+ if !(_x isEqualType createHashMap) then { continue; };
+
+ _pendingInvites pushBack [
+ ["orgId", _x getOrDefault ["orgId", ""]],
+ ["orgName", _x getOrDefault ["orgName", "Unknown Organization"]],
+ ["inviterUid", _x getOrDefault ["inviterUid", ""]],
+ ["inviterName", _x getOrDefault ["inviterName", "Unknown"]],
+ ["targetUid", _x getOrDefault ["targetUid", ""]],
+ ["targetName", _x getOrDefault ["targetName", "Unknown"]]
+ ];
+ } forEach _pendingInvitesRaw;
+
+ _pendingInvites
+ }],
+ ["buildInviteablePlayers", compileFinal {
+ params [
+ ["_uid", "", [""]],
+ ["_orgID", "", [""]],
+ ["_membersRaw", createHashMap, [createHashMap]],
+ ["_pendingInvitesRaw", createHashMap, [createHashMap]]
+ ];
+
+ private _memberUids = [];
+ {
+ _memberUids pushBackUnique (_y getOrDefault ["uid", ""]);
+ } forEach _membersRaw;
+
+ private _pendingInviteUids = [];
+ {
+ _pendingInviteUids pushBackUnique (_x);
+ } forEach _pendingInvitesRaw;
+
+ private _players = [];
+ {
+ private _player = _x;
+ if (isNull _player) then { continue; };
+
+ private _playerUid = getPlayerUID _player;
+ if (_playerUid isEqualTo "" || { _playerUid isEqualTo _uid }) then { continue; };
+ if (_playerUid in _memberUids || { _playerUid in _pendingInviteUids }) then { continue; };
+
+ private _playerOrgID = GVAR(OrgStore) call ["resolveOrgIdForUid", [_playerUid]];
+ if (_playerOrgID isNotEqualTo "default") then { continue; };
+
+ _players pushBack [
+ ["uid", _playerUid],
+ ["name", name _player],
+ ["orgId", _playerOrgID]
+ ];
+ } forEach allPlayers;
+
+ _players
+ }],
["buildPortalPayload", compileFinal {
params [["_uid", "", [""]]];
@@ -160,10 +218,8 @@ GVAR(OrgPayloadBuilder) = createHashMapObject [[
private _player = [_uid] call EFUNC(common,getPlayer);
if (isNull _player) exitWith { createHashMap };
- private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
- private _orgID = _actor getOrDefault ["organization", "default"];
- if (_orgID isEqualTo "") then { _orgID = "default"; };
-
+ private _actor = EGVAR(actor,ActorStore) call ["load", [_uid]];
+ private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid]];
private _org = _self call ["resolveOrgForUid", [_uid]];
if (_org isEqualTo createHashMap) exitWith { createHashMap };
@@ -179,9 +235,14 @@ GVAR(OrgPayloadBuilder) = createHashMapObject [[
private _assetsRaw = _org getOrDefault ["assets", createHashMap];
private _fleetRaw = _org getOrDefault ["fleet", createHashMap];
private _membersRaw = _org getOrDefault ["members", createHashMap];
+ private _pendingInvitesRaw = _org getOrDefault ["pending_invites", createHashMap];
private _isDefaultOrg = (_org getOrDefault ["default", false])
|| { toLowerANSI _id isEqualTo "default" }
|| { toLowerANSI _ownerUid isEqualTo "server" };
+ private _memberInvites = [];
+ if (_isDefaultOrg) then {
+ _memberInvites = GVAR(OrgStore) call ["listMemberInvites", [_uid]];
+ };
private _playerName = name _player;
private _playerVar = vehicleVarName _player;
@@ -211,6 +272,8 @@ GVAR(OrgPayloadBuilder) = createHashMapObject [[
["reputation", _reputation],
["creditLines", _self call ["buildCreditLinesList", [_creditLinesRaw]]],
["members", _memberShape getOrDefault ["members", []]],
+ ["pendingInvites", _self call ["buildPendingInvitesList", [_memberInvites]]],
+ ["inviteablePlayers", _self call ["buildInviteablePlayers", [_uid, _id, _membersRaw, _pendingInvitesRaw]]],
["fleet", _self call ["buildFleetList", [_fleetRaw]]],
["assets", _self call ["buildAssetsList", [_assetsRaw]]],
["activity", []]
diff --git a/arma/server/addons/store/functions/fnc_initStoreStore.sqf b/arma/server/addons/store/functions/fnc_initStoreStore.sqf
index 61fb751..b77f94f 100644
--- a/arma/server/addons/store/functions/fnc_initStoreStore.sqf
+++ b/arma/server/addons/store/functions/fnc_initStoreStore.sqf
@@ -4,7 +4,7 @@
* File: fnc_initStoreStore.sqf
* Author: IDSolutions
* Date: 2026-03-12
- * Last Update: 2026-03-14
+ * Last Update: 2026-04-04
* Public: No
*
* Description:
@@ -50,10 +50,7 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
_bankBalance = _bankAccount getOrDefault ["bank", 0];
};
- private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
- private _orgId = _actor getOrDefault ["organization", "default"];
- if (_orgId isEqualTo "") then { _orgId = "default"; };
-
+ private _orgId = EGVAR(actor,ActorStore) call ["getOrganization", [_uid]];
private _org = EGVAR(org,OrgStore) call ["loadById", [_orgId]];
if (_org isEqualTo createHashMap) then {
_org = EGVAR(org,OrgStore) call ["loadById", ["default"]];
@@ -160,7 +157,10 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
["vehicleGranted", []],
["bankPatch", createHashMap],
["orgPatch", createHashMap],
- ["orgTargetUids", []]
+ ["orgTargetUids", []],
+ ["persistenceSucceeded", false],
+ ["persistenceFailures", []],
+ ["persistenceMessage", ""]
]
}],
["formatCurrency", compileFinal {
@@ -255,6 +255,69 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
true
}],
+ ["persistCheckoutState", compileFinal {
+ params [
+ ["_uid", "", [""]],
+ ["_orgID", "", [""]],
+ ["_backendResult", createHashMap, [createHashMap]]
+ ];
+
+ private _result = createHashMapFromArray [
+ ["success", true],
+ ["failures", []],
+ ["message", ""]
+ ];
+
+ if (_uid isEqualTo "" || { _backendResult isEqualTo createHashMap }) exitWith {
+ _result set ["success", false];
+ _result set ["failures", ["checkout"]];
+ _result set ["message", "Checkout persistence context was invalid."];
+ _result
+ };
+
+ private _persistenceFailures = [];
+
+ if ((keys (_backendResult getOrDefault ["lockerPatch", createHashMap])) isNotEqualTo []) then {
+ if ((EGVAR(locker,LockerStore) call ["save", [_uid]]) isEqualTo createHashMap) then {
+ _persistenceFailures pushBack "locker";
+ };
+ };
+
+ if ((keys (_backendResult getOrDefault ["vaPatch", createHashMap])) isNotEqualTo []) then {
+ if ((EGVAR(locker,VAStore) call ["save", [_uid]]) isEqualTo createHashMap) then {
+ _persistenceFailures pushBack "virtual_arsenal";
+ };
+ };
+
+ if ((keys (_backendResult getOrDefault ["vgaragePatch", createHashMap])) isNotEqualTo []) then {
+ if ((EGVAR(garage,VGarageStore) call ["save", [_uid]]) isEqualTo createHashMap) then {
+ _persistenceFailures pushBack "virtual_garage";
+ };
+ };
+
+ if ((keys (_backendResult getOrDefault ["bankPatch", createHashMap])) isNotEqualTo []) then {
+ if ((EGVAR(bank,BankStore) call ["save", [_uid]]) isEqualTo createHashMap) then {
+ _persistenceFailures pushBack "bank";
+ };
+ };
+
+ if (_orgID isNotEqualTo "" && { (keys (_backendResult getOrDefault ["orgPatch", createHashMap])) isNotEqualTo [] }) then {
+ if ((EGVAR(org,OrgStore) call ["saveById", [_orgID]]) isEqualTo createHashMap) then {
+ _persistenceFailures pushBack "organization";
+ };
+ };
+
+ if (_persistenceFailures isNotEqualTo []) then {
+ _result set ["success", false];
+ _result set ["failures", _persistenceFailures];
+ _result set ["message", format [
+ "Checkout completed, but durable save failed for: %1.",
+ _persistenceFailures joinString ", "
+ ]];
+ };
+
+ _result
+ }],
["checkout", compileFinal {
params [["_uid", "", [""]], ["_player", objNull, [objNull]], ["_payloadJson", "", [""]]];
@@ -310,6 +373,14 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
};
_self call ["syncCheckoutResult", [_player, _backendResult]];
+ private _persistenceResult = _self call [
+ "persistCheckoutState",
+ [
+ _uid,
+ _checkoutContext getOrDefault ["orgId", ""],
+ _backendResult
+ ]
+ ];
_result set ["success", true];
_result set ["message", _backendResult getOrDefault ["message", format [
@@ -320,6 +391,16 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
]]];
_result set ["lockerGranted", _backendResult getOrDefault ["lockerGranted", []]];
_result set ["vehicleGranted", _backendResult getOrDefault ["vehicleGranted", []]];
+ _result set ["persistenceSucceeded", _persistenceResult getOrDefault ["success", false]];
+ _result set ["persistenceFailures", _persistenceResult getOrDefault ["failures", []]];
+ _result set ["persistenceMessage", _persistenceResult getOrDefault ["message", ""]];
+
+ if !(_persistenceResult getOrDefault ["success", false]) then {
+ private _warning = _persistenceResult getOrDefault ["message", "Checkout completed with persistence failures."];
+ ["ERROR", format ["Store checkout for %1 completed with persistence failures: %2", _uid, (_persistenceResult getOrDefault ["failures", []]) joinString ", "]] call EFUNC(common,log);
+ _result set ["message", format ["%1 %2", _result get "message", _warning]];
+ };
+
_result
}]
];
diff --git a/arma/server/addons/task/README.md b/arma/server/addons/task/README.md
index 3ea76e3..ef67658 100644
--- a/arma/server/addons/task/README.md
+++ b/arma/server/addons/task/README.md
@@ -3,6 +3,10 @@
## Overview
The task addon is a server-owned mission/task system for Forge. It manages task execution, task-owned state, participant tracking, contribution-based player earnings, and org-owned rewards.
+Task operational state is mission-scoped. The extension-backed task catalog,
+ownership, status, and defuse state are reset on task store startup, so the
+system intentionally starts clean after each server or mission restart.
+
## Responsibilities
- spawn and monitor task flows on the server
- track per-task entities through `TaskStore`
@@ -95,6 +99,7 @@ If you want the accepting player's org to own the task rewards, use `fnc_handler
- the dynamic mission manager in `fnc_missionManager.sqf` is now limited to attack missions only
- it starts server-owned tasks through `fnc_handler.sqf` and binds them to the `default` org
- task lifecycle for the mission manager is tracked through `TaskStore` status entries
+- task backend state is intentionally transient and resets with the active server/mission lifecycle
- task rewards are org-owned, not player-owned
- participant notifications are sent through the notifications module, not through local server UI
diff --git a/arma/server/addons/task/functions/fnc_handleTaskRewards.sqf b/arma/server/addons/task/functions/fnc_handleTaskRewards.sqf
index e0e83bf..1ba8f83 100644
--- a/arma/server/addons/task/functions/fnc_handleTaskRewards.sqf
+++ b/arma/server/addons/task/functions/fnc_handleTaskRewards.sqf
@@ -51,6 +51,7 @@ if (_orgID isEqualTo "") exitWith {
private _success = true;
private _funds = _rewards getOrDefault ["funds", 0];
private _rewardMessages = [];
+private _failureMessages = [];
private _resolveRewardLabel = {
params [["_className", "", [""]]];
@@ -115,8 +116,15 @@ if (_funds > 0) then {
if (_updatedOrg isEqualTo createHashMap) then {
["ERROR", format ["Failed to update organization %1 funds for task %2.", _orgID, _taskID]] call EFUNC(common,log);
_success = false;
+ _failureMessages pushBack "org funds update";
} else {
private _patch = createHashMapFromArray [["funds", _nextFunds]];
+ private _savedOrg = EGVAR(org,OrgStore) call ["saveById", [_orgID]];
+ if (_savedOrg isEqualTo createHashMap) then {
+ ["ERROR", format ["Task %1 updated organization %2 funds, but durable save failed.", _taskID, _orgID]] call EFUNC(common,log);
+ _success = false;
+ _failureMessages pushBack "org funds persistence";
+ };
[_patch] call _syncOrgPatch;
_rewardMessages pushBack format ["$%1 org funds", [_funds] call EFUNC(common,formatNumber)];
@@ -141,8 +149,15 @@ private _grantOrgAssets = {
if !(_grantResult getOrDefault ["success", false]) then {
["ERROR", format ["Failed to award %1 assets for task %2: %3", _category, _taskID, _grantResult getOrDefault ["message", "Unknown error."]]] call EFUNC(common,log);
_success = false;
+ _failureMessages pushBack format ["%1 asset update", _category];
} else {
[_grantResult getOrDefault ["patch", createHashMap]] call _syncOrgPatch;
+ if !(_grantResult getOrDefault ["persisted", false]) then {
+ private _persistenceMessage = _grantResult getOrDefault ["persistenceMessage", format ["%1 assets updated, but durable save failed.", _category]];
+ ["ERROR", format ["Task %1 %2", _taskID, _persistenceMessage]] call EFUNC(common,log);
+ _success = false;
+ _failureMessages pushBack format ["%1 asset persistence", _category];
+ };
private _labels = _items apply { [_x] call _resolveRewardLabel };
_rewardMessages pushBack format ["%1: %2", _category, _labels joinString ", "];
};
@@ -171,8 +186,15 @@ private _grantOrgFleet = {
if !(_fleetResult getOrDefault ["success", false]) then {
["ERROR", format ["Failed to award vehicle rewards for task %2: %1", _fleetResult getOrDefault ["message", "Unknown error."], _taskID]] call EFUNC(common,log);
_success = false;
+ _failureMessages pushBack "fleet update";
} else {
[_fleetResult getOrDefault ["patch", createHashMap]] call _syncOrgPatch;
+ if !(_fleetResult getOrDefault ["persisted", false]) then {
+ private _persistenceMessage = _fleetResult getOrDefault ["persistenceMessage", "Fleet updated, but durable save failed."];
+ ["ERROR", format ["Task %1 %2", _taskID, _persistenceMessage]] call EFUNC(common,log);
+ _success = false;
+ _failureMessages pushBack "fleet persistence";
+ };
private _labels = _vehicles apply { [_x] call _resolveRewardLabel };
_rewardMessages pushBack format ["vehicles: %1", _labels joinString ", "];
};
@@ -219,7 +241,12 @@ if (_success) then {
["INFO", _message] call EFUNC(common,log);
["success", "Tasks", _message] call _notifyMembers;
} else {
- ["warning", "Tasks", format ["Task %1 completed, but one or more org rewards failed to apply.", _taskID]] call _notifyMembers;
+ private _warningMessage = format ["Task %1 completed, but one or more org rewards failed to apply.", _taskID];
+ if (_failureMessages isNotEqualTo []) then {
+ _warningMessage = format ["%1 Failed areas: %2.", _warningMessage, _failureMessages joinString ", "];
+ };
+
+ ["warning", "Tasks", _warningMessage] call _notifyMembers;
};
_success
diff --git a/arma/server/addons/task/functions/fnc_handler.sqf b/arma/server/addons/task/functions/fnc_handler.sqf
index 0349b27..cffe3a1 100644
--- a/arma/server/addons/task/functions/fnc_handler.sqf
+++ b/arma/server/addons/task/functions/fnc_handler.sqf
@@ -27,14 +27,7 @@ if (_minRating > 0) then {
if (_requesterUid isEqualTo "") then {
["WARNING", format ["Task %1 requires minimum reputation %2 but no requester UID was provided, skipping reputation gate.", _taskType, _minRating]] call EFUNC(common,log);
} else {
- private _requesterActor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
- if (_requesterActor isEqualTo createHashMap) then {
- _requesterActor = EGVAR(actor,ActorStore) call ["init", [_requesterUid]];
- };
-
- private _orgID = _requesterActor getOrDefault ["organization", "default"];
- if (_orgID isEqualTo "") then { _orgID = "default"; };
-
+ private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]];
private _org = EGVAR(org,OrgStore) call ["loadById", [_orgID]];
private _orgReputation = _org getOrDefault ["reputation", 0];
if (_orgReputation < _minRating) exitWith {
diff --git a/arma/server/addons/task/functions/fnc_initTaskStore.sqf b/arma/server/addons/task/functions/fnc_initTaskStore.sqf
index f0410f9..5de0c4e 100644
--- a/arma/server/addons/task/functions/fnc_initTaskStore.sqf
+++ b/arma/server/addons/task/functions/fnc_initTaskStore.sqf
@@ -5,6 +5,10 @@
* Initializes the task store for task entity tracking, participant
* contribution tracking, and task outcome application.
*
+ * Task metadata is extension-backed but intentionally transient. The
+ * task backend is reset when this store is created so task/catalog/status
+ * state starts clean for each server or mission lifecycle.
+ *
* Arguments:
* None
*
@@ -32,6 +36,8 @@ GVAR(TaskStore) = createHashMapObject [[
["targets", createHashMap]
]];
+ // Task extension state is mission-scoped and intentionally reset on
+ // startup rather than being treated as durable account data.
["task:reset", []] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if (
!_isSuccess
@@ -99,18 +105,14 @@ GVAR(TaskStore) = createHashMapObject [[
private _orgID = "default";
if (_requesterUid isNotEqualTo "") then {
- private _actor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
- if (_actor isEqualTo createHashMap) then {
- _actor = EGVAR(actor,ActorStore) call ["init", [_requesterUid]];
- };
+ private _actor = EGVAR(actor,ActorStore) call ["load", [_requesterUid]];
if (_actor isEqualTo createHashMap) exitWith {
_result set ["message", format ["Failed to load actor for %1.", _requesterUid]];
_result
};
- _orgID = _actor getOrDefault ["organization", ""];
- if (_orgID isEqualTo "") then { _orgID = "default"; };
+ _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]];
};
private _context = createHashMapFromArray [
@@ -166,6 +168,14 @@ GVAR(TaskStore) = createHashMapObject [[
_entries
}],
+ ["hasTaskCatalogEntry", compileFinal {
+ params [["_taskID", "", [""]]];
+
+ if (_taskID isEqualTo "") exitWith { false };
+
+ private _entry = _self call ["callTaskState", ["task:catalog:get", [_taskID], objNull]];
+ _entry isEqualType createHashMap
+ }],
["acceptTask", compileFinal {
params [["_taskID", "", [""]], ["_requesterUid", "", [""]]];
@@ -180,17 +190,13 @@ GVAR(TaskStore) = createHashMapObject [[
_result
};
- private _actor = EGVAR(actor,Registry) getOrDefault [_requesterUid, createHashMap];
- if (_actor isEqualTo createHashMap) then {
- _actor = EGVAR(actor,ActorStore) call ["init", [_requesterUid]];
- };
+ private _actor = EGVAR(actor,ActorStore) call ["load", [_requesterUid]];
if (_actor isEqualTo createHashMap) exitWith {
_result set ["message", format ["Failed to load actor for %1.", _requesterUid]];
_result
};
- private _orgID = _actor getOrDefault ["organization", ""];
- if (_orgID isEqualTo "") then { _orgID = "default"; };
+ private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_requesterUid]];
private _context = createHashMapFromArray [
["requesterUid", _requesterUid],
@@ -418,6 +424,10 @@ GVAR(TaskStore) = createHashMapObject [[
if (_taskID isEqualTo "") exitWith { false };
+ if !(isNil QGVAR(MissionManager)) then {
+ GVAR(MissionManager) call ["completeMission", [_taskID]];
+ };
+
private _participantRegistry = _self getOrDefault ["participantRegistry", createHashMap];
_participantRegistry deleteAt _taskID;
_self set ["participantRegistry", _participantRegistry];
@@ -431,7 +441,11 @@ GVAR(TaskStore) = createHashMapObject [[
private _result = createHashMapFromArray [
["participantUids", []],
["orgIds", []],
- ["contributions", createHashMap]
+ ["contributions", createHashMap],
+ ["success", true],
+ ["mutationFailures", []],
+ ["persistenceFailures", []],
+ ["message", ""]
];
if (_taskID isEqualTo "" || { _delta isEqualTo 0 }) exitWith { _result };
@@ -462,6 +476,8 @@ GVAR(TaskStore) = createHashMapObject [[
private _orgIds = [];
private _contributions = createHashMap;
private _totalContribution = 0;
+ private _mutationFailures = [];
+ private _persistenceFailures = [];
if (_delta > 0) then {
{
@@ -481,12 +497,7 @@ GVAR(TaskStore) = createHashMapObject [[
{
private _uid = _x;
- private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
- if (_actor isEqualTo createHashMap) then {
- _actor = EGVAR(actor,ActorStore) call ["init", [_uid]];
- };
-
- private _orgID = _actor getOrDefault ["organization", ""];
+ private _orgID = EGVAR(actor,ActorStore) call ["getOrganization", [_uid, ""]];
if (_orgID isNotEqualTo "") then {
_orgIds pushBackUnique _orgID;
};
@@ -517,6 +528,10 @@ GVAR(TaskStore) = createHashMapObject [[
if (_patch isEqualTo createHashMap) then { continue; };
EGVAR(bank,BankMessenger) call ["sendAccountSync", [_uid, _patch]];
+ if ((EGVAR(bank,BankStore) call ["save", [_uid]]) isEqualTo createHashMap) then {
+ _persistenceFailures pushBackUnique format ["bank:%1", _uid];
+ ["ERROR", format ["Task %1 updated bank earnings for %2, but durable save failed.", _taskID, _uid]] call EFUNC(common,log);
+ };
};
};
} forEach _participantUids;
@@ -547,8 +562,13 @@ GVAR(TaskStore) = createHashMapObject [[
} forEach _memberUids;
_orgIds = [_ownerOrgID];
+ if ((EGVAR(org,OrgStore) call ["saveById", [_ownerOrgID]]) isEqualTo createHashMap) then {
+ _persistenceFailures pushBackUnique format ["organization:%1", _ownerOrgID];
+ ["ERROR", format ["Task %1 updated reputation for organization %2, but durable save failed.", _taskID, _ownerOrgID]] call EFUNC(common,log);
+ };
} else {
["ERROR", format ["Failed to update organization %1 reputation for task %2.", _ownerOrgID, _taskID]] call EFUNC(common,log);
+ _mutationFailures pushBackUnique format ["organization:%1", _ownerOrgID];
};
};
};
@@ -556,6 +576,19 @@ GVAR(TaskStore) = createHashMapObject [[
_result set ["participantUids", _participantUids];
_result set ["orgIds", _orgIds];
_result set ["contributions", _contributions];
+ _result set ["success", (_mutationFailures isEqualTo []) && { _persistenceFailures isEqualTo [] }];
+ _result set ["mutationFailures", _mutationFailures];
+ _result set ["persistenceFailures", _persistenceFailures];
+ if (_mutationFailures isNotEqualTo [] || { _persistenceFailures isNotEqualTo [] }) then {
+ private _messageParts = [];
+ if (_mutationFailures isNotEqualTo []) then {
+ _messageParts pushBack format ["mutation failures: %1", _mutationFailures joinString ", "];
+ };
+ if (_persistenceFailures isNotEqualTo []) then {
+ _messageParts pushBack format ["persistence failures: %1", _persistenceFailures joinString ", "];
+ };
+ _result set ["message", _messageParts joinString "; "];
+ };
_result
}]
]];
diff --git a/arma/server/addons/task/functions/fnc_missionManager.sqf b/arma/server/addons/task/functions/fnc_missionManager.sqf
index a1aec02..e471b27 100644
--- a/arma/server/addons/task/functions/fnc_missionManager.sqf
+++ b/arma/server/addons/task/functions/fnc_missionManager.sqf
@@ -38,8 +38,25 @@ GVAR(MissionManagerBaseClass) = compileFinal createHashMapFromArray [
["getMaxConcurrentMissions", compileFinal {
private _maxConcurrent = _self getOrDefault ["maxConcurrentMissions", 1];
if (_maxConcurrent <= 0) then { _maxConcurrent = 1; };
+ private _attackLocationCount = _self call ["getAttackLocationCount", []];
+ if (_attackLocationCount > 0) then {
+ _maxConcurrent = _maxConcurrent min _attackLocationCount;
+ };
_maxConcurrent
}],
+ ["getAttackLocationCount", compileFinal {
+ private _locationsConfig = _self getOrDefault ["locationsConfig", configNull];
+ if (isNull _locationsConfig) exitWith { 0 };
+
+ private _count = 0;
+ {
+ if ("attack" in getArray (_x >> "suitable")) then {
+ _count = _count + 1;
+ };
+ } forEach ("true" configClasses _locationsConfig);
+
+ _count
+ }],
["getLocationReuseCooldown", compileFinal {
private _missionConfig = _self getOrDefault ["missionConfig", configNull];
private _cooldown = getNumber (_missionConfig >> "locationReuseCooldown");
@@ -352,7 +369,8 @@ GVAR(MissionManager) = createHashMapObject [GVAR(MissionManagerBaseClass)];
[{
{
private _status = GVAR(TaskStore) call ["getTaskStatus", [_x]];
- if (_status in ["succeeded", "failed"]) then {
+ private _hasCatalogEntry = GVAR(TaskStore) call ["hasTaskCatalogEntry", [_x]];
+ if (_status in ["succeeded", "failed"] || { _status isEqualTo "" && { !_hasCatalogEntry } }) then {
GVAR(MissionManager) call ["completeMission", [_x]];
GVAR(TaskStore) call ["clearTaskStatus", [_x]];
};
diff --git a/arma/server/extension/src/actor.rs b/arma/server/extension/src/actor.rs
index 53f3a68..7bbdf68 100644
--- a/arma/server/extension/src/actor.rs
+++ b/arma/server/extension/src/actor.rs
@@ -54,6 +54,7 @@ pub fn group() -> Group {
Group::new()
.command("init", init_hot_actor)
.command("get", get_hot_actor)
+ .command("keys", list_hot_actor_keys)
.command("override", override_hot_actor)
.command("save", save_hot_actor)
.command("remove", remove_hot_actor),
@@ -91,6 +92,16 @@ pub(crate) fn get_hot_actor(call_context: CallContext, key: String) -> String {
}
}
+pub(crate) fn list_hot_actor_keys() -> String {
+ match HOT_ACTOR_SERVICE.list_actor_keys() {
+ Ok(keys) => match serde_json::to_string(&keys) {
+ Ok(json) => json,
+ Err(error) => format!("Error: Failed to serialize actor hot-state keys: {}", error),
+ },
+ Err(error) => format!("Error: {}", error),
+ }
+}
+
pub(crate) fn override_hot_actor(
call_context: CallContext,
key: String,
diff --git a/arma/server/extension/src/cad.rs b/arma/server/extension/src/cad.rs
index 5984e99..d6c4223 100644
--- a/arma/server/extension/src/cad.rs
+++ b/arma/server/extension/src/cad.rs
@@ -3,6 +3,10 @@
//! The extension owns the in-memory CAD state store, while the shared service
//! layer handles mutation rules and hydrate shaping. This keeps the extension
//! surface thin and aligned with the workspace architecture.
+//!
+//! CAD state is intentionally transient operational state. It follows the
+//! active server or mission lifecycle and is not treated as durable player or
+//! organization persistence.
use arma_rs::Group;
use forge_repositories::InMemoryCadRepository;
diff --git a/arma/server/extension/src/org.rs b/arma/server/extension/src/org.rs
index e0456ad..9b370fb 100644
--- a/arma/server/extension/src/org.rs
+++ b/arma/server/extension/src/org.rs
@@ -7,8 +7,9 @@ use arma_rs::Group;
use forge_models::{
HotOrgRecord, OrgAssetGrantSeed, OrgCheckoutContext, OrgCreditLineContext,
OrgCreditLineRepaymentContext, OrgCreditLineRepaymentResult, OrgDisbandResult,
- OrgEnsureMemberContext, OrgFleetGrantSeed, OrgGrantContext, OrgLeaveContext, OrgLeaveResult,
- OrgRegisterContext,
+ OrgEnsureMemberContext, OrgFleetGrantSeed, OrgGrantContext, OrgInviteContext,
+ OrgInviteDecisionContext, OrgInviteDecisionResult, OrgInviteRecord, OrgInviteResult,
+ OrgLeaveContext, OrgLeaveResult, OrgRegisterContext,
};
use forge_repositories::{InMemoryOrgHotRepository, RedisOrgRepository};
use forge_services::{OrgHotStateService, OrgService};
@@ -58,7 +59,11 @@ pub fn group() -> Group {
.command("get", get_hot_org)
.command("override", override_hot_org)
.command("ensure_member", ensure_hot_org_member)
+ .command("member_invites", get_hot_org_member_invites)
.command("register", register_hot_org)
+ .command("invite_member", invite_hot_org_member)
+ .command("accept_invite", accept_hot_org_invite)
+ .command("decline_invite", decline_hot_org_invite)
.command("assign_credit_line", assign_credit_line_hot_org)
.command("repay_credit_line", repay_credit_line_hot_org)
.command("charge_checkout", charge_checkout_hot_org)
@@ -142,6 +147,13 @@ pub(crate) fn ensure_hot_org_member(json_data: String) -> String {
}
}
+pub(crate) fn get_hot_org_member_invites(member_uid: String) -> String {
+ match HOT_ORG_SERVICE.get_member_invites(member_uid) {
+ Ok(invites) => serialize_result::>(&invites, "org invite list"),
+ Err(error) => format!("Error: {}", error),
+ }
+}
+
pub(crate) fn register_hot_org(json_data: String) -> String {
let context: OrgRegisterContext = match serde_json::from_str(&json_data) {
Ok(data) => data,
@@ -154,6 +166,46 @@ pub(crate) fn register_hot_org(json_data: String) -> String {
}
}
+pub(crate) fn invite_hot_org_member(json_data: String) -> String {
+ let context: OrgInviteContext = match serde_json::from_str(&json_data) {
+ Ok(data) => data,
+ Err(error) => return format!("Error: Invalid org invite JSON: {}", error),
+ };
+
+ match HOT_ORG_SERVICE.invite_member(context) {
+ Ok(result) => serialize_result::(&result, "org invite result"),
+ Err(error) => format!("Error: {}", error),
+ }
+}
+
+pub(crate) fn accept_hot_org_invite(json_data: String) -> String {
+ let context: OrgInviteDecisionContext = match serde_json::from_str(&json_data) {
+ Ok(data) => data,
+ Err(error) => return format!("Error: Invalid org invite decision JSON: {}", error),
+ };
+
+ match HOT_ORG_SERVICE.accept_invite(context) {
+ Ok(result) => {
+ serialize_result::(&result, "org invite decision result")
+ }
+ Err(error) => format!("Error: {}", error),
+ }
+}
+
+pub(crate) fn decline_hot_org_invite(json_data: String) -> String {
+ let context: OrgInviteDecisionContext = match serde_json::from_str(&json_data) {
+ Ok(data) => data,
+ Err(error) => return format!("Error: Invalid org invite decision JSON: {}", error),
+ };
+
+ match HOT_ORG_SERVICE.decline_invite(context) {
+ Ok(result) => {
+ serialize_result::(&result, "org invite decision result")
+ }
+ Err(error) => format!("Error: {}", error),
+ }
+}
+
pub(crate) fn assign_credit_line_hot_org(json_data: String) -> String {
let context: OrgCreditLineContext = match serde_json::from_str(&json_data) {
Ok(data) => data,
diff --git a/arma/server/extension/src/task.rs b/arma/server/extension/src/task.rs
index 233d3dc..fbe2e40 100644
--- a/arma/server/extension/src/task.rs
+++ b/arma/server/extension/src/task.rs
@@ -2,6 +2,9 @@
//!
//! The extension owns portable task metadata while SQF keeps Arma-only runtime
//! state such as entity references and participant tracking.
+//!
+//! This state is intentionally transient and is reset during server task-store
+//! initialization so tasks start clean for each server or mission lifecycle.
use arma_rs::Group;
use forge_repositories::InMemoryTaskRepository;
diff --git a/arma/server/extension/src/transport.rs b/arma/server/extension/src/transport.rs
index 0232ba4..e2679a5 100644
--- a/arma/server/extension/src/transport.rs
+++ b/arma/server/extension/src/transport.rs
@@ -185,6 +185,10 @@ fn route_command(
expect_arg_count(function_name, &arguments, 1)?;
Ok(actor::get_hot_actor(call_context, arguments[0].clone()))
}
+ "actor:hot:keys" => {
+ expect_arg_count(function_name, &arguments, 0)?;
+ Ok(actor::list_hot_actor_keys())
+ }
"actor:hot:override" => {
expect_arg_count(function_name, &arguments, 2)?;
Ok(actor::override_hot_actor(
@@ -355,10 +359,26 @@ fn route_command(
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::ensure_hot_org_member(arguments[0].clone()))
}
+ "org:hot:member_invites" => {
+ expect_arg_count(function_name, &arguments, 1)?;
+ Ok(org::get_hot_org_member_invites(arguments[0].clone()))
+ }
"org:hot:register" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::register_hot_org(arguments[0].clone()))
}
+ "org:hot:invite_member" => {
+ expect_arg_count(function_name, &arguments, 1)?;
+ Ok(org::invite_hot_org_member(arguments[0].clone()))
+ }
+ "org:hot:accept_invite" => {
+ expect_arg_count(function_name, &arguments, 1)?;
+ Ok(org::accept_hot_org_invite(arguments[0].clone()))
+ }
+ "org:hot:decline_invite" => {
+ expect_arg_count(function_name, &arguments, 1)?;
+ Ok(org::decline_hot_org_invite(arguments[0].clone()))
+ }
"org:hot:assign_credit_line" => {
expect_arg_count(function_name, &arguments, 1)?;
Ok(org::assign_credit_line_hot_org(arguments[0].clone()))
diff --git a/lib/models/src/actor.rs b/lib/models/src/actor.rs
index b6e7c2b..203eed1 100644
--- a/lib/models/src/actor.rs
+++ b/lib/models/src/actor.rs
@@ -61,7 +61,7 @@ impl Actor {
state: "HEALTHY".to_string(),
holster: true,
rank: None,
- organization: "".to_string(),
+ organization: "default".to_string(),
};
actor.validate()?;
@@ -171,7 +171,7 @@ impl FromArma for Actor {
})?;
if actor.organization.trim().is_empty() {
- actor.organization = String::new();
+ actor.organization = "default".to_string();
}
Ok(actor)
diff --git a/lib/models/src/lib.rs b/lib/models/src/lib.rs
index 243a5a5..7c5a6f4 100644
--- a/lib/models/src/lib.rs
+++ b/lib/models/src/lib.rs
@@ -27,7 +27,9 @@ pub use org::{
OrgAssetEntry, OrgAssetGrantSeed, OrgCheckoutContext, OrgCreditLineContext,
OrgCreditLineRepaymentContext, OrgCreditLineRepaymentResult, OrgDisbandMemberResult,
OrgDisbandResult, OrgEnsureMemberContext, OrgFleetEntry, OrgFleetGrantSeed, OrgGrantContext,
- OrgLeaveContext, OrgLeaveResult, OrgMutationResult, OrgRegisterContext, OrgRegisterResult,
+ OrgInviteContext, OrgInviteDecisionContext, OrgInviteDecisionResult, OrgInviteRecord,
+ OrgInviteResult, OrgLeaveContext, OrgLeaveResult, OrgMutationResult, OrgRegisterContext,
+ OrgRegisterResult,
};
pub use store::{
StoreCheckoutContext, StoreCheckoutItemSeed, StoreCheckoutResult, StoreCheckoutVehicleSeed,
diff --git a/lib/models/src/org.rs b/lib/models/src/org.rs
index bda9235..5d05592 100644
--- a/lib/models/src/org.rs
+++ b/lib/models/src/org.rs
@@ -69,6 +69,17 @@ pub struct MemberSummary {
pub name: String,
}
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct OrgInviteRecord {
+ pub org_id: String,
+ pub org_name: String,
+ pub inviter_uid: String,
+ pub inviter_name: String,
+ pub target_uid: String,
+ pub target_name: String,
+}
+
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HotOrgRecord {
pub id: String,
@@ -84,6 +95,8 @@ pub struct HotOrgRecord {
pub fleet: HashMap,
#[serde(default)]
pub members: HashMap,
+ #[serde(default)]
+ pub pending_invites: HashMap,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -112,6 +125,44 @@ pub struct OrgRegisterResult {
pub message: String,
}
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct OrgInviteContext {
+ pub requester_uid: String,
+ pub requester_name: String,
+ pub org_id: String,
+ pub requester_is_default_org_ceo: bool,
+ pub target_uid: String,
+ pub target_name: String,
+ pub target_org_id: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct OrgInviteDecisionContext {
+ pub requester_uid: String,
+ pub requester_name: String,
+ pub org_id: String,
+ pub existing_org_id: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct OrgInviteResult {
+ pub org: HotOrgRecord,
+ pub target_uid: String,
+ pub message: String,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct OrgInviteDecisionResult {
+ pub invited_org: HotOrgRecord,
+ pub previous_org: Option,
+ pub actor_organization: String,
+ pub message: String,
+}
+
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrgCreditLineContext {
@@ -329,6 +380,7 @@ impl HotOrgRecord {
.into_iter()
.map(|member| (member.uid.clone(), member))
.collect(),
+ pending_invites: HashMap::new(),
}
}
diff --git a/lib/models/src/v_locker.rs b/lib/models/src/v_locker.rs
index 2f68ffc..f67de35 100644
--- a/lib/models/src/v_locker.rs
+++ b/lib/models/src/v_locker.rs
@@ -35,7 +35,7 @@ impl VLocker {
"ItemMap".to_string(),
"ItemRadio".to_string(),
"ItemWatch".to_string(),
- "U_IG_Guerrilla_6_1".to_string(),
+ "U_BG_Guerrilla_6_1".to_string(),
"V_TacVest_oli".to_string(),
],
weapons: vec!["arifle_MX_F".to_string(), "hgun_P07_F".to_string()],
diff --git a/lib/repositories/src/actor.rs b/lib/repositories/src/actor.rs
index cca7867..72107e1 100644
--- a/lib/repositories/src/actor.rs
+++ b/lib/repositories/src/actor.rs
@@ -34,6 +34,7 @@ pub trait ActorRepository: Send + Sync {
pub trait ActorHotRepository: Send + Sync {
fn get(&self, id: &str) -> Result