fix session
This commit is contained in:
parent
b60d4c91ae
commit
d71c9900de
File diff suppressed because one or more lines are too long
|
|
@ -175,7 +175,7 @@ export function CallOverlay({
|
||||||
<button type="button" className="call-hangup" onClick={hangUp}>{t("hangUp")}</button>
|
<button type="button" className="call-hangup" onClick={hangUp}>{t("hangUp")}</button>
|
||||||
</div>
|
</div>
|
||||||
{showTakeover ? (
|
{showTakeover ? (
|
||||||
<button type="button" className="primary call-takeover" onClick={onTakeOver}>{t("loginOpenScreen")}</button>
|
<button type="button" className="primary call-takeover" onClick={onTakeOver}>{t("takeOverNow")}</button>
|
||||||
) : null}
|
) : null}
|
||||||
<p className="call-hint">{t("callShortcuts")}</p>
|
<p className="call-hint">{t("callShortcuts")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@
|
||||||
.computer-caption{display:flex;align-items:center;justify-content:space-between;padding:14px 0;color:var(--muted)}
|
.computer-caption{display:flex;align-items:center;justify-content:space-between;padding:14px 0;color:var(--muted)}
|
||||||
.control-bar{display:flex;align-items:center;justify-content:flex-end;gap:8px;border-top:1px solid var(--hairline);padding:16px 0}
|
.control-bar{display:flex;align-items:center;justify-content:flex-end;gap:8px;border-top:1px solid var(--hairline);padding:16px 0}
|
||||||
.computer-overlay{position:fixed;inset:0;z-index:50;display:flex;flex-direction:column;background:rgba(4,4,5,.98)}
|
.computer-overlay{position:fixed;inset:0;z-index:50;display:flex;flex-direction:column;background:rgba(4,4,5,.98)}
|
||||||
.computer-overlay>header{height:64px;display:flex;align-items:center;justify-content:space-between;padding:0 18px;border-bottom:1px solid var(--line)}
|
.computer-overlay>header{min-height:64px;height:auto;display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:8px;padding:8px 18px;border-bottom:1px solid var(--line);overflow:visible}
|
||||||
.computer-overlay>header>div{display:flex;align-items:center;gap:10px}
|
.computer-overlay>header>div{display:flex;align-items:center;flex-wrap:wrap;gap:10px;min-width:0}
|
||||||
.computer-overlay .avatar{width:30px;height:30px}
|
.computer-overlay .avatar{width:30px;height:30px}
|
||||||
.control-badge{padding:5px 10px;border-radius:999px;background:rgba(48,162,75,.14);color:var(--success);font-size:13px}
|
.control-badge{padding:5px 10px;border-radius:999px;background:rgba(48,162,75,.14);color:var(--success);font-size:13px}
|
||||||
.overlay-screen{flex:1;min-height:0;padding:20px;display:flex;justify-content:center}
|
.overlay-screen{flex:1;min-height:0;padding:20px;display:flex;justify-content:center}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export const en: { [K in keyof typeof zhTW]: string } = {
|
||||||
results: "Results",
|
results: "Results",
|
||||||
advancedSettings: "Advanced settings",
|
advancedSettings: "Advanced settings",
|
||||||
sharedDesktop: "Shared control",
|
sharedDesktop: "Shared control",
|
||||||
sharedNeedsUser: "{name} is waiting for you to finish the steps on screen. Then press “Done, continue”.",
|
sharedNeedsUser: "{name} is waiting for you to finish the steps on screen. Press “Take over” to open it, then “Done, continue”.",
|
||||||
doneContinue: "Done, continue",
|
doneContinue: "Done, continue",
|
||||||
search: "Search",
|
search: "Search",
|
||||||
sharedComputer: "Shared computer",
|
sharedComputer: "Shared computer",
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ export const zhTW = {
|
||||||
results: "成果",
|
results: "成果",
|
||||||
advancedSettings: "進階設定",
|
advancedSettings: "進階設定",
|
||||||
sharedDesktop: "共同操作",
|
sharedDesktop: "共同操作",
|
||||||
sharedNeedsUser: "{name} 已暫停等你完成畫面上的步驟。完成後按「完成,繼續」。",
|
sharedNeedsUser: "{name} 已暫停等你完成畫面上的步驟。按「接手操作」打開畫面,完成後按「完成,繼續」。",
|
||||||
doneContinue: "完成,繼續",
|
doneContinue: "完成,繼續",
|
||||||
search: "搜尋", sharedComputer: "共用電腦", privateComputer: "私人電腦",
|
search: "搜尋", sharedComputer: "共用電腦", privateComputer: "私人電腦",
|
||||||
stopped: "已關閉", booting: "啟動中", suspended: "休眠中", error: "發生錯誤",
|
stopped: "已關閉", booting: "啟動中", suspended: "休眠中", error: "發生錯誤",
|
||||||
|
|
|
||||||
|
|
@ -468,7 +468,8 @@
|
||||||
.error-banner{z-index:8;bottom:118px}
|
.error-banner{z-index:8;bottom:118px}
|
||||||
|
|
||||||
|
|
||||||
.pause-banner{display:flex;align-items:center;gap:14px;width:min(760px,100%);margin:4px auto 18px;padding:12px 14px;border:1px solid var(--warn-border);border-radius:12px;background:var(--warn-bg);color:var(--warn-fg);font-size:13px;line-height:1.5}
|
.pause-banner{display:flex;align-items:center;flex-wrap:wrap;gap:14px;width:min(760px,100%);margin:4px auto 18px;padding:12px 14px;border:1px solid var(--warn-border);border-radius:12px;background:var(--warn-bg);color:var(--warn-fg);font-size:13px;line-height:1.5}
|
||||||
|
.pause-banner-actions{display:flex;flex-wrap:wrap;gap:8px;flex:0 0 auto}
|
||||||
|
|
||||||
|
|
||||||
.pause-banner span{flex:1}
|
.pause-banner span{flex:1}
|
||||||
|
|
|
||||||
|
|
@ -34,5 +34,6 @@
|
||||||
.sched-when>span{display:flex;flex-wrap:wrap;gap:6px;overflow-wrap:anywhere}
|
.sched-when>span{display:flex;flex-wrap:wrap;gap:6px;overflow-wrap:anywhere}
|
||||||
.sched-editor .dialog-actions{flex-wrap:wrap}
|
.sched-editor .dialog-actions{flex-wrap:wrap}
|
||||||
.login-chip,.sched-chip{width:100%;overflow-wrap:anywhere}
|
.login-chip,.sched-chip{width:100%;overflow-wrap:anywhere}
|
||||||
.message:has(>.sched-chip),.message:has(>.login-chip){flex-direction:column;gap:8px}
|
.login-chip>button{justify-self:start}
|
||||||
|
.message:has(>.sched-chip),.message:has(>.login-chip){flex-direction:column;gap:8px;width:100%}
|
||||||
.message.spoken>.sched-chip,.message.spoken>.login-chip{grid-column:2;max-width:100%;min-width:0}
|
.message.spoken>.sched-chip,.message.spoken>.login-chip{grid-column:2;max-width:100%;min-width:0}
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export function TaskStatus({tasks,onTakeover}:{tasks:TaskSnapshot[];onTakeover:(
|
||||||
const state=taskState(task,Date.now());const report=task.report;
|
const state=taskState(task,Date.now());const report=task.report;
|
||||||
return <div className="task-status" key={task.runId}>
|
return <div className="task-status" key={task.runId}>
|
||||||
<div className="task-status-heading" role="status"><i className={`task-dot ${state}`} aria-hidden="true"/><strong>{t(state)}</strong><span className="task-summary">{report?.summary}</span><RunProbe runId={task.runId} align="end" label={t("taskDetails")}><span className="task-probe" tabIndex={0}>{t("taskDetails")}</span></RunProbe></div>
|
<div className="task-status-heading" role="status"><i className={`task-dot ${state}`} aria-hidden="true"/><strong>{t(state)}</strong><span className="task-summary">{report?.summary}</span><RunProbe runId={task.runId} align="end" label={t("taskDetails")}><span className="task-probe" tabIndex={0}>{t("taskDetails")}</span></RunProbe></div>
|
||||||
{task.status==="waiting_takeover"&&<button className="outline" onClick={onTakeover}>{t("loginOpenScreen")}</button>}
|
{task.status==="waiting_takeover"&&<button className="primary" onClick={onTakeover}>{t("takeOverNow")}</button>}
|
||||||
{task.status==="waiting_input"&&report?.request&&<p>{report.request}</p>}
|
{task.status==="waiting_input"&&report?.request&&<p>{report.request}</p>}
|
||||||
{report&&Boolean(report.remaining?.length||report.completed?.length)&&<details><summary>{t("taskDetails")}</summary>{report.completed?.length>0&&<p>{t("taskCompleted")}:{report.completed.join(";")}</p>}{report.remaining?.length>0&&<p>{t("taskFailed")}:{report.remaining.join(";")}</p>}{report.attempts?.map((attempt,i)=><p key={i}>{attempt}</p>)}{report.verification&&<p>{report.verification}</p>}</details>}
|
{report&&Boolean(report.remaining?.length||report.completed?.length)&&<details><summary>{t("taskDetails")}</summary>{report.completed?.length>0&&<p>{t("taskCompleted")}:{report.completed.join(";")}</p>}{report.remaining?.length>0&&<p>{t("taskFailed")}:{report.remaining.join(";")}</p>}{report.attempts?.map((attempt,i)=><p key={i}>{attempt}</p>)}{report.verification&&<p>{report.verification}</p>}</details>}
|
||||||
</div>;
|
</div>;
|
||||||
|
|
|
||||||
|
|
@ -311,7 +311,12 @@ pub async fn ensure_bot_screen(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
tx.commit().await.map_err(|e| e.to_string())?;
|
tx.commit().await.map_err(|e| e.to_string())?;
|
||||||
crate::gui_publications::publish_screen(state, actor, computer, &row).await?;
|
// A stale GUI fence must not block starting the desktop. Extra Team bots
|
||||||
|
// otherwise get a screen row and no X server, so they cannot connect.
|
||||||
|
if let Err(error) = crate::gui_publications::publish_screen(state, actor, computer, &row).await
|
||||||
|
{
|
||||||
|
tracing::warn!("GUI publication for {} deferred: {error}", row.display);
|
||||||
|
}
|
||||||
let mut ctx = adapter_context_for(actor, bot_id, "screen", Some(&row), run_id);
|
let mut ctx = adapter_context_for(actor, bot_id, "screen", Some(&row), run_id);
|
||||||
ctx.computer_generation = Some(computer.generation);
|
ctx.computer_generation = Some(computer.generation);
|
||||||
let bot = state
|
let bot = state
|
||||||
|
|
@ -411,7 +416,14 @@ async fn restore_one_screen(
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
.ok_or("screen bot is unavailable")?;
|
.ok_or("screen bot is unavailable")?;
|
||||||
crate::gui_publications::publish_screen(state, actor, computer, screen).await?;
|
if let Err(error) =
|
||||||
|
crate::gui_publications::publish_screen(state, actor, computer, screen).await
|
||||||
|
{
|
||||||
|
tracing::warn!(
|
||||||
|
"GUI publication restore for {} deferred: {error}",
|
||||||
|
screen.display
|
||||||
|
);
|
||||||
|
}
|
||||||
let mut ctx = adapter_context_for(actor, &screen.bot_id, "screen-restore", Some(screen), None);
|
let mut ctx = adapter_context_for(actor, &screen.bot_id, "screen-restore", Some(screen), None);
|
||||||
ctx.computer_generation = Some(computer.generation);
|
ctx.computer_generation = Some(computer.generation);
|
||||||
let result = state
|
let result = state
|
||||||
|
|
@ -653,6 +665,25 @@ pub async fn take_profile_lock(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn attached_screen(
|
||||||
|
state: &AppState,
|
||||||
|
actor: &Actor,
|
||||||
|
bot_id: &str,
|
||||||
|
computer: &ComputerRow,
|
||||||
|
need_gui: bool,
|
||||||
|
) -> Result<Option<ScreenRow>, String> {
|
||||||
|
if !attach_display_for_tool(need_gui) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let bound = ensure_bot_screen(state, actor, bot_id, computer, None).await?;
|
||||||
|
match bound.row {
|
||||||
|
Some(row) => Ok(Some(row)),
|
||||||
|
None => Err(bound
|
||||||
|
.gui_block
|
||||||
|
.unwrap_or_else(|| "screen unavailable".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn boot(state: &AppState, actor: &Actor, bot_id: &str) -> Result<ComputerStatus, String> {
|
pub async fn boot(state: &AppState, actor: &Actor, bot_id: &str) -> Result<ComputerStatus, String> {
|
||||||
boot_for(state, actor, bot_id, true).await
|
boot_for(state, actor, bot_id, true).await
|
||||||
}
|
}
|
||||||
|
|
@ -708,14 +739,7 @@ pub async fn boot_for(
|
||||||
return Err("Computer health could not be verified; existing provider retained".into());
|
return Err("Computer health could not be verified; existing provider retained".into());
|
||||||
}
|
}
|
||||||
if probe == ContainerProbe::Alive {
|
if probe == ContainerProbe::Alive {
|
||||||
let screen = if attach_display_for_tool(need_gui) {
|
let screen = attached_screen(state, actor, bot_id, &computer, need_gui).await?;
|
||||||
ensure_bot_screen(state, actor, bot_id, &computer, None)
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.and_then(|bound| bound.row)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
if attach_display_for_tool(need_gui) {
|
if attach_display_for_tool(need_gui) {
|
||||||
restore_computer_screens(state, actor, &computer, bot_id).await;
|
restore_computer_screens(state, actor, &computer, bot_id).await;
|
||||||
}
|
}
|
||||||
|
|
@ -846,14 +870,7 @@ pub async fn boot_for(
|
||||||
.await
|
.await
|
||||||
.map_err(|error| error.to_string())?
|
.map_err(|error| error.to_string())?
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let screen = if attach_display_for_tool(need_gui) {
|
let screen = attached_screen(state, actor, bot_id, &computer, need_gui).await?;
|
||||||
ensure_bot_screen(state, actor, bot_id, &computer, None)
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.and_then(|bound| bound.row)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
if attach_display_for_tool(need_gui) {
|
if attach_display_for_tool(need_gui) {
|
||||||
restore_computer_screens(state, actor, &computer, bot_id).await;
|
restore_computer_screens(state, actor, &computer, bot_id).await;
|
||||||
}
|
}
|
||||||
|
|
@ -942,14 +959,7 @@ async fn resume_paused(
|
||||||
.map_err(|error| error.to_string())?
|
.map_err(|error| error.to_string())?
|
||||||
.ok_or_else(|| "computer not found".to_string())?;
|
.ok_or_else(|| "computer not found".to_string())?;
|
||||||
if current.state == "running" {
|
if current.state == "running" {
|
||||||
let screen = if attach_display_for_tool(need_gui) {
|
let screen = attached_screen(state, actor, bot_id, ¤t, need_gui).await?;
|
||||||
ensure_bot_screen(state, actor, bot_id, ¤t, None)
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.and_then(|bound| bound.row)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
return Ok(status_from(bot_id, ¤t, screen.as_ref(), None));
|
return Ok(status_from(bot_id, ¤t, screen.as_ref(), None));
|
||||||
}
|
}
|
||||||
return Err("computer resume was superseded".into());
|
return Err("computer resume was superseded".into());
|
||||||
|
|
@ -960,14 +970,7 @@ async fn resume_paused(
|
||||||
.await
|
.await
|
||||||
.map_err(|error| error.to_string())?
|
.map_err(|error| error.to_string())?
|
||||||
.ok_or_else(|| "computer not found".to_string())?;
|
.ok_or_else(|| "computer not found".to_string())?;
|
||||||
let screen = if attach_display_for_tool(need_gui) {
|
let screen = attached_screen(state, actor, bot_id, &computer, need_gui).await?;
|
||||||
ensure_bot_screen(state, actor, bot_id, &computer, None)
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.and_then(|bound| bound.row)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
if attach_display_for_tool(need_gui) {
|
if attach_display_for_tool(need_gui) {
|
||||||
restore_computer_screens(state, actor, &computer, bot_id).await;
|
restore_computer_screens(state, actor, &computer, bot_id).await;
|
||||||
}
|
}
|
||||||
|
|
@ -1141,12 +1144,16 @@ pub async fn takeover(
|
||||||
if computer.state != "running" || computer.provider_ref.is_none() {
|
if computer.state != "running" || computer.provider_ref.is_none() {
|
||||||
return Err("computer must be running".into());
|
return Err("computer must be running".into());
|
||||||
}
|
}
|
||||||
let bound = ensure_bot_screen(state, actor, bot_id, &computer, None).await?;
|
let screen = match attached_screen(state, actor, bot_id, &computer, true).await {
|
||||||
let screen = bound.row.ok_or_else(|| {
|
Ok(Some(row)) => row,
|
||||||
bound
|
Ok(None) => return Err("screen unavailable".into()),
|
||||||
.gui_block
|
Err(error) => state
|
||||||
.unwrap_or_else(|| "screen unavailable".into())
|
.db
|
||||||
})?;
|
.get_screen(&computer.id, bot_id)
|
||||||
|
.await
|
||||||
|
.map_err(|error| error.to_string())?
|
||||||
|
.ok_or(error)?,
|
||||||
|
};
|
||||||
let active = state
|
let active = state
|
||||||
.db
|
.db
|
||||||
.active_run(bot_id)
|
.active_run(bot_id)
|
||||||
|
|
@ -2482,6 +2489,76 @@ mod idle_job_tests {
|
||||||
server.abort();
|
server.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[sqlx::test(migrations = "../../migrations")]
|
||||||
|
async fn screen_starts_when_gui_publication_is_deferred(pool: sqlx::PgPool) {
|
||||||
|
use axum::{Json, Router, http::StatusCode, routing::post};
|
||||||
|
use std::sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicUsize, Ordering},
|
||||||
|
};
|
||||||
|
for statement in [
|
||||||
|
"INSERT INTO users(id,name) VALUES ('u','test')",
|
||||||
|
"INSERT INTO spaces(id,user_id,name) VALUES ('s','u','test')",
|
||||||
|
"INSERT INTO computers(id,space_id,user_id,scope,scope_key,home_key,state,provider_ref) VALUES ('c','s','u','team','team:s','home','running','provider')",
|
||||||
|
"INSERT INTO bots(id,space_id,user_id,name,computer_id) VALUES ('a','s','u','alpha','c'),('b','s','u','beta','c')",
|
||||||
|
] {
|
||||||
|
sqlx::query(statement).execute(&pool).await.unwrap();
|
||||||
|
}
|
||||||
|
let screens = Arc::new(AtomicUsize::new(0));
|
||||||
|
let seen = screens.clone();
|
||||||
|
let router = Router::new()
|
||||||
|
.route(
|
||||||
|
"/computers/provider/screens",
|
||||||
|
post(move |Json(request): Json<EnsureScreenRequest>| {
|
||||||
|
let seen = seen.clone();
|
||||||
|
async move {
|
||||||
|
seen.fetch_add(1, Ordering::SeqCst);
|
||||||
|
Json(json!({
|
||||||
|
"slot": request.slot,
|
||||||
|
"display": format!(":{}", request.slot + 1),
|
||||||
|
"viewPort": 6080 + request.slot
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/computers/provider/gui/publish",
|
||||||
|
post(|| async { StatusCode::BAD_GATEWAY }),
|
||||||
|
);
|
||||||
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||||
|
let mut state = app(pool.clone());
|
||||||
|
state.sandbox = Arc::new(lazyboy_sandbox::DockerSandbox::new(
|
||||||
|
format!("http://{}", listener.local_addr().unwrap()),
|
||||||
|
"fixture".into(),
|
||||||
|
));
|
||||||
|
let server = tokio::spawn(async move {
|
||||||
|
axum::serve(listener, router).await.unwrap();
|
||||||
|
});
|
||||||
|
let actor = Actor {
|
||||||
|
user_id: "u".into(),
|
||||||
|
space_id: "s".into(),
|
||||||
|
};
|
||||||
|
let computer = state.db.get_computer("c").await.unwrap().unwrap();
|
||||||
|
let first = ensure_bot_screen(&state, &actor, "a", &computer, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
first.row.is_some(),
|
||||||
|
"publication failure must not skip desktop start"
|
||||||
|
);
|
||||||
|
assert_eq!(first.row.as_ref().unwrap().slot, 0);
|
||||||
|
let second = ensure_bot_screen(&state, &actor, "b", &computer, None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
second.row.unwrap().slot,
|
||||||
|
1,
|
||||||
|
"peer bot must get its own screen"
|
||||||
|
);
|
||||||
|
assert_eq!(screens.load(Ordering::SeqCst), 2);
|
||||||
|
server.abort();
|
||||||
|
}
|
||||||
|
|
||||||
#[sqlx::test(migrations = "../../migrations")]
|
#[sqlx::test(migrations = "../../migrations")]
|
||||||
async fn abandoned_boot_expires_without_retiring_jobs_and_fences_late_replies(
|
async fn abandoned_boot_expires_without_retiring_jobs_and_fences_late_replies(
|
||||||
pool: sqlx::PgPool,
|
pool: sqlx::PgPool,
|
||||||
|
|
|
||||||
|
|
@ -51,13 +51,15 @@ pub async fn publish_screen(
|
||||||
let now = Utc::now().timestamp_millis();
|
let now = Utc::now().timestamp_millis();
|
||||||
let paused = agent_paused || screen.control_holder == "user";
|
let paused = agent_paused || screen.control_holder == "user";
|
||||||
let mut expires = now + 120_000;
|
let mut expires = now + 120_000;
|
||||||
if !paused && screen.execution_run_id.is_some() {
|
// An expired execution lease must not make the whole desktop unpublished:
|
||||||
expires = expires.min(
|
// other bots still need their screens, and takeover still needs a live view.
|
||||||
screen
|
if !paused {
|
||||||
.execution_lease_expires_at
|
if let Some(lease_expires) = screen.execution_lease_expires_at {
|
||||||
.ok_or("GUI execution lease missing")?
|
let lease_ms = lease_expires.timestamp_millis();
|
||||||
.timestamp_millis(),
|
if lease_ms > now {
|
||||||
);
|
expires = expires.min(lease_ms);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let mut request = GuiPublicationRequest {
|
let mut request = GuiPublicationRequest {
|
||||||
home: captured.home_key.clone(),
|
home: captured.home_key.clone(),
|
||||||
|
|
@ -152,9 +154,10 @@ async fn deliver(
|
||||||
|| request.paused != (paused || screen.control_holder == "user")
|
|| request.paused != (paused || screen.control_holder == "user")
|
||||||
|| (!request.paused
|
|| (!request.paused
|
||||||
&& screen.execution_run_id.is_some()
|
&& screen.execution_run_id.is_some()
|
||||||
&& screen
|
&& screen.execution_lease_expires_at.is_some_and(|expires| {
|
||||||
.execution_lease_expires_at
|
let lease_ms = expires.timestamp_millis();
|
||||||
.is_none_or(|e| request.lease_expires_at_ms > e.timestamp_millis()))
|
lease_ms > Utc::now().timestamp_millis() && request.lease_expires_at_ms > lease_ms
|
||||||
|
}))
|
||||||
{
|
{
|
||||||
return Err("GUI publication no longer authorized".into());
|
return Err("GUI publication no longer authorized".into());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -417,7 +417,7 @@ async fn room_status(
|
||||||
JOIN bots b ON b.id=r.bot_id
|
JOIN bots b ON b.id=r.bot_id
|
||||||
JOIN threads t ON t.id=r.thread_id
|
JOIN threads t ON t.id=r.thread_id
|
||||||
WHERE t.room_id=$1 AND t.space_id=$2 AND t.user_id=$3
|
WHERE t.room_id=$1 AND t.space_id=$2 AND t.user_id=$3
|
||||||
AND r.status IN ('queued','leased','running')
|
AND r.status IN ('queued','leased','running','waiting_input','waiting_takeover')
|
||||||
ORDER BY b.name",
|
ORDER BY b.name",
|
||||||
)
|
)
|
||||||
.bind(&id)
|
.bind(&id)
|
||||||
|
|
|
||||||
|
|
@ -835,15 +835,18 @@ test('taking the screen flips the mouse on the click, not on the reply',()=>{
|
||||||
assert.equal((app.match(/holder==="user"\?"takeover":"release"/g)||[]).length,1,'one request path, one owner of it');
|
assert.equal((app.match(/holder==="user"\?"takeover":"release"/g)||[]).length,1,'one request path, one owner of it');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('shared CUA desktops hide takeover and keep the mouse live',()=>{
|
test('shared CUA desktops keep the mouse live and still show takeover',()=>{
|
||||||
const app=fs.readFileSync('apps/web/src/App.tsx','utf8');
|
const app=fs.readFileSync('apps/web/src/App.tsx','utf8');
|
||||||
assert.match(app,/sharedInput:true/,'the first paint already assumes shared input');
|
assert.match(app,/sharedInput:true/,'the first paint already assumes shared input');
|
||||||
assert.match(app,/if\(!computer\.sharedInput\)await setControl\("user",id\)/,'opening the login screen must not pause a shared run');
|
assert.match(app,/await setControl\("user",id\)/,'opening the login screen pauses so the human can type');
|
||||||
assert.match(app,/if\(active&&!computer\.sharedInput\)void setControl\("user",active\.id\)/,'a call only takes exclusive control on older exclusive desktops');
|
assert.doesNotMatch(app,/if\(!computer\.sharedInput\)await setControl\("user",id\)/,'shared desktops must not hide the pause');
|
||||||
assert.match(app,/computer\.sharedInput\?\(computer\.takeoverRequested\|\|computer\.controlHolder==="user"\?<button className="primary" disabled=\{busy\|\|pending\} onClick=\{onRelease\}>\{t\("doneContinue"\)\}<\/button>:null\)/);
|
assert.match(app,/onTakeOver=\{\(\)=>void openLoginScreen\(\)\}/);
|
||||||
|
assert.match(app,/computer\.takeoverRequested\|\|computer\.controlHolder==="user"\?<button className="primary" disabled=\{busy\|\|pending\} onClick=\{onRelease\}>\{computer\.sharedInput\?t\("doneContinue"\):t\("releaseControl"\)\}<\/button>/);
|
||||||
|
assert.match(app,/<button className="primary" disabled=\{busy\|\|pending\} onClick=\{onTakeOver\}>\{working\|\|computer\.sharedInput\?t\("takeOverNow"\):t\("takeControl"\)\}<\/button>/);
|
||||||
assert.match(app,/computer\.sharedInput\?t\("sharedDesktop"\)/);
|
assert.match(app,/computer\.sharedInput\?t\("sharedDesktop"\)/);
|
||||||
assert.match(app,/computer\.sharedInput\?t\("sharedNeedsUser"/);
|
assert.match(app,/computer\.sharedInput\?t\("sharedNeedsUser"/);
|
||||||
assert.match(app,/takeover=\{computer\.takeoverRequested\|\|\(!computer\.sharedInput&&computer\.controlHolder==="user"\)\}/);
|
assert.match(app,/\{t\("takeOverNow"\)\}/);
|
||||||
|
assert.match(app,/takeover=\{computer\.takeoverRequested\|\|computer\.controlHolder==="user"\}/);
|
||||||
const vnc=fs.readFileSync('apps/web/vnc.html','utf8');
|
const vnc=fs.readFileSync('apps/web/vnc.html','utf8');
|
||||||
assert.match(vnc,/rfb\.viewOnly = true;/,'a dropped RFB is muted locally');
|
assert.match(vnc,/rfb\.viewOnly = true;/,'a dropped RFB is muted locally');
|
||||||
assert.doesNotMatch(vnc,/disconnect[\s\S]{0,400}applyViewOnly\(true\)/,'disconnect must not overwrite host intent');
|
assert.doesNotMatch(vnc,/disconnect[\s\S]{0,400}applyViewOnly\(true\)/,'disconnect must not overwrite host intent');
|
||||||
|
|
@ -1138,6 +1141,8 @@ test('task presence expires from server clock without inventing progress',()=>{
|
||||||
assert.equal(taskState({...base,status:'completed',report:{state:'complete'}},2000),'taskCompleted');
|
assert.equal(taskState({...base,status:'completed',report:{state:'complete'}},2000),'taskCompleted');
|
||||||
assert.equal(taskState({...base,status:'queued',aliveUntil:null},1000),'taskQueued');
|
assert.equal(taskState({...base,status:'queued',aliveUntil:null},1000),'taskQueued');
|
||||||
assert.equal(taskState({...base,status:'waiting_takeover'},999999),'taskWaiting');
|
assert.equal(taskState({...base,status:'waiting_takeover'},999999),'taskWaiting');
|
||||||
|
const taskSource=fs.readFileSync('apps/web/src/task-status.tsx','utf8');
|
||||||
|
assert.match(taskSource,/waiting_takeover.*takeOverNow/s,'the waiting task exposes Take over, not a hidden screen link');
|
||||||
assert.equal(taskState({...base,status:'failed',report:{state:'complete'}},2000),'taskFailed');
|
assert.equal(taskState({...base,status:'failed',report:{state:'complete'}},2000),'taskFailed');
|
||||||
assert.equal(taskState({...base,report:{state:'recovering'}},2000),'taskRecovering');
|
assert.equal(taskState({...base,report:{state:'recovering'}},2000),'taskRecovering');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue