Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,9 @@ page, each with a temporary password that works for 72 hours and is changed at t
The panel's pages are grouped by what one comes for: **Players** (the players, the live map, the
bans), **Server** (restarts, the cfg, builds, backups, file changes), **Insights** (statistics, drops,
diagnostics) and **Maintenance** (tools, the audit log). The search button beside the logo, or Ctrl K
(⌘ K on a Mac) from any page, opens a search of the pages, of the settings by their name, and of the
connected players by name or ID. Each account finds only what it may open.
(⌘ K on a Mac) from any page, opens a search of the pages, of the settings by their name, of the
events and exports the **Documentation** page describes (see [Integrations](#integrations)), and of
the connected players by name or ID. Each account finds only what it may open.

From the host, even while Observer runs:

Expand Down Expand Up @@ -997,6 +998,13 @@ which the `observer_bridge` server convar gives: do not ensure it yourself. Scri
fixed name, as they do txAdmin's `monitor`, find nothing. [`resource/observer_example`](resource/observer_example)
listens to each event and marks where a team plugs in its notifications and its saves.

The panel's **Documentation** page shows that contract as the build that runs ships it: each event with
when it comes, its fields and a handler to copy, each export where the contract shows it called, and the
files of the example resource. Every section has a link of its own, and the panel's search (Ctrl K) finds
an event, an export or one of txAdmin's events by its name. Any account reads it. To the accounts that
see the dashboard, it also says what this server does now: the contract's version, the name the bridge
took for this start, and whether txAdmin's events are raised, now and from the next start.

Your resources can also give each connected player a label, a roleplay character's name or an ID of
your own, through the bridge's `setPlayerLabel` export. The panel shows it beside the player's FiveM
name, marked as your resources' word, keeps each label on the player's profile as long as their names,
Expand Down
33 changes: 33 additions & 0 deletions crates/fake-fxserver/tests/supervisor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,39 @@ async fn a_new_launch_applies_from_the_next_start() {
lab.shut_down(false).await;
}

#[tokio::test]
async fn the_status_says_whether_the_running_server_raises_txadmins_events() {
let mut lab = Lab::online(&[]).await;
let mut status = lab.supervisor.status();
assert_eq!(status.borrow().txadmin_events, Some(false));
let mut launch = lab.launch.clone();
launch.settings.txadmin_events = true;

lab.supervisor.configure(launch).await;
tokio::time::sleep(Duration::from_millis(500)).await;
assert_eq!(
status.borrow().txadmin_events,
Some(false),
"the running server keeps its launch"
);
lab.supervisor.restart("test").await.unwrap();

timeout(
WAIT,
status.wait_for(|status| status.txadmin_events == Some(true)),
)
.await
.expect("the restart did not take the new launch")
.unwrap();
lab.supervisor.stop("test").await.unwrap();
timeout(WAIT, status.wait_for(|status| status.pid.is_none()))
.await
.expect("the server did not stop")
.unwrap();
assert_eq!(status.borrow().txadmin_events, None);
lab.shut_down(false).await;
}

#[tokio::test]
async fn a_started_server_goes_online_then_stops_gracefully() {
let mut lab = Lab::new(&[]).await;
Expand Down
59 changes: 59 additions & 0 deletions crates/observer-app/src/docs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//! What the panel's Documentation page is built from: the integration contract as this build
//! ships it, and what the server Observer runs does of it.

use observer_domain::docs::{BridgeExport, ExampleFile, IntegrationDocs, IntegrationServer};
use observer_domain::permissions::Permissions;
use observer_fxserver::supervisor::State;
use observer_fxserver::{bridge, contract};

use crate::{Caller, Error, Servers, Supervision};

/// The contract with its example, what the bridge raises and exports, and this server's side of
/// it. The contract holds no secret, so any account reads it. What the server does now goes to
/// those who see its state elsewhere in the panel, the holders of the dashboard's permission.
pub async fn integration_docs(
caller: &Caller,
supervision: &Supervision,
servers: &Servers,
) -> Result<IntegrationDocs, Error> {
let server = if caller.permissions.contains(Permissions::DASHBOARD_VIEW) {
Some(server_side(supervision, servers).await?)
} else {
None
};
Ok(IntegrationDocs {
contract: contract::TEXT.to_owned(),
contract_version: bridge::CONTRACT_VERSION,
events: contract::events(),
exports: contract::exports()
.into_iter()
.map(|export| BridgeExport {
name: export.name.to_owned(),
parameters: export.parameters.into_iter().map(str::to_owned).collect(),
})
.collect(),
example: contract::EXAMPLE
.into_iter()
.map(|(name, text)| ExampleFile {
name: name.to_owned(),
text: text.to_owned(),
})
.collect(),
server,
})
}

async fn server_side(supervision: &Supervision, servers: &Servers) -> Result<IntegrationServer, Error> {
let next = servers
.launch_settings(Servers::DEFAULT)
.await?
.map(|stored| stored.settings.txadmin_events);
let status = supervision.supervisor().status();
let status = status.borrow();
Ok(IntegrationServer {
bridge: status.bridge.clone(),
starting: status.state == State::Starting,
txadmin_events: status.txadmin_events,
txadmin_events_next: next,
})
}
2 changes: 2 additions & 0 deletions crates/observer-app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ mod changes;
mod clock;
mod debug;
mod diagnostics;
mod docs;
mod drops;
mod error;
mod features;
Expand Down Expand Up @@ -49,6 +50,7 @@ pub use changes::{Changes, ChangesSetup, ChangesTask, ComparedTo, DiffOptions};
pub use clock::Clock;
pub use debug::{DebugCounts, DebugSetup, DebugTask};
pub use diagnostics::{Diagnostics, DiagnosticsSetup, DiagnosticsTask, MAX_DOWNLOAD};
pub use docs::integration_docs;
pub use drops::{DayRates, Drops, DropsSetup, DropsTask, LogQuery};
pub use error::Error;
pub use features::{Features, FeaturesSetup, Switched};
Expand Down
58 changes: 58 additions & 0 deletions crates/observer-domain/src/docs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//! What the panel's Documentation page is built from: the integration contract as this build
//! ships it, and what the server Observer runs does of it.

use serde::Serialize;
use ts_rs::TS;

/// `GET /api/docs/integration`.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct IntegrationDocs {
/// The contract in Markdown, as this build ships it: the page is drawn from it.
pub contract: String,
pub contract_version: u32,
/// Every server event the bridge raises, as resources hear it: `observer:shuttingDown`.
pub events: Vec<String>,
pub exports: Vec<BridgeExport>,
/// The example resource, file by file.
pub example: Vec<ExampleFile>,
/// Null for an account that does not see the server's state, which takes the dashboard's
/// permission: the contract is anyone's to read, what the server does now is not.
pub server: Option<IntegrationServer>,
}

/// An export of the bridge, as its script declares it.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct BridgeExport {
pub name: String,
pub parameters: Vec<String>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct ExampleFile {
/// From the contract's own folder, as its links name it: `observer_example/server.lua`.
pub name: String,
pub text: String,
}

/// What the server Observer runs does of the contract.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export)]
pub struct IntegrationServer {
/// The bridge's name in the server that runs now, drawn for its start. Null while no server
/// runs.
pub bridge: Option<String>,
/// That server is still starting: its bridge may not have reported yet.
pub starting: bool,
/// Whether the server that runs now raises txAdmin's events too. Null while no server runs.
pub txadmin_events: Option<bool>,
/// Whether the next start will, as the launch settings have it. Null until the server is set
/// up.
pub txadmin_events_next: Option<bool>,
}
1 change: 1 addition & 0 deletions crates/observer-domain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ pub mod convars;
pub mod debug;
pub mod delegation;
pub mod diagnostics;
pub mod docs;
pub mod drops;
pub mod error;
pub mod features;
Expand Down
5 changes: 5 additions & 0 deletions crates/observer-fxserver/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ const FILES: [(&str, &str); 3] = [
("heap.js", include_str!("../../../resource/observer/heap.js")),
];

/// The bridge's script, which the contract's documentation reads its events and exports from.
pub(crate) fn script() -> &'static str {
FILES[1].1
}

/// Draws the bridge's resource name for one start of FXServer, so that no script can target the
/// bridge by a name known before the start.
pub fn draw_name() -> io::Result<String> {
Expand Down
Loading
Loading