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
3 changes: 3 additions & 0 deletions GLOSSARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,9 @@ _Avoid_: completion email, alert

### Console

**Cron job**:
An entry in a machine account's operating-system crontab that mentions thirdshift. Its recorded schedule and command are shown even when its **Launch directory** or Command kind cannot be recognized, and a displayed entry need not offer a **Console** start choice.

**Console**:
The web page a machine serves that shows the serving OS account's **Commands**, opening on its **Inbox**, with each one's child **Ticket runs**, their stages and their logs, and from which a live Command can be stopped and an Inbox row given a **Dismissal**. The **User config** can include readable **Other-account Commands** on that machine, and the Commands of every machine in its **Fleet**, which it can stop and dismiss as their own Consoles can. Each machine serves its own Console, behind a sign-in, and every view and action it offers is also a CLI command.
_Avoid_: dashboard, floor, UI, web UI
Expand Down
2 changes: 1 addition & 1 deletion docs/code-navigation.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Start with the issue's terms in `GLOSSARY.md`, then use this index to find their
| Theme, hidden sections, Pause motion | `src/config/console.rs` — built-in names and restricted User config writes; `src/console/look.rs` — CLI/HTTP look actions; `src/console/assets/themes.css` — tokens; `src/console/assets/look.js` — picker and browser motion preference | `tests/console/look.rs`, `console/tests/inbox.test.cjs` |
| Custom Theme, Theme import/export, custom fonts | `src/config/console/themes.rs` — portable format, defaults, validation, folder and confined font reads; `src/console/look.rs` — shared saves and Console log; `src/console/assets/look.js` — JSON editor | `tests/console/look.rs`, `console/tests/custom-themes.test.cjs`; format in `console/README.md` |
| Console installation, proxy site, system service | `src/console/install.rs` — printed Linux installation script; `src/config/console.rs` — address and port | `tests/console_install.rs` |
| Console Cron jobs, crontab inventory | `src/cronjobs.rs` — read-only account crontab and CLI; `src/console/cronjobs.rs`, `src/console/cronjobs.html` — page rendering | `tests/console/cronjobs.rs`, `console/tests/cronjobs.test.cjs` |
| Cron job, Command kind, crontab inventory | `src/cronjobs.rs` — read-only account crontab and CLI; `src/cronjobs/command.rs` — owned `Interpretation`, recognition and independent Command-kind/Launch/start facts; `src/launch_directories.rs` — filesystem/origin resolution; `src/console/start_controls.rs` — supported Pass start choices; `src/console/cronjobs.rs`, `src/console/cronjobs.html` — page rendering | `src/cronjobs/command.rs` interface behavior tables; `tests/console/cronjobs.rs`, `tests/console/start.rs`, `console/tests/cronjobs.test.cjs`, `console/tests/start.test.cjs` |
| Pass last-fired time | `src/launch.rs` — shared Pass entry; `src/logs/recording.rs` — atomic per-kind start replacement; `src/logs/pass_starts.rs` — saved times and shared age display; `src/logs/reading.rs` — per-repository inventory shared by status and Cron jobs | `tests/activity_log.rs`, `tests/status.rs`, `tests/console/cronjobs.rs` |
| Stop | `src/logs/reading.rs` — `retained_work` reload by captured record; `src/logs/reading/selection.rs` — `Selection::stop_target`; `src/logs/stop.rs` — shared request, signaling, waiting and inspection; `src/console/stop.rs` — confirmation and authenticated Console action; `src/claim.rs` — Stop endings | `src/logs/reading/tests.rs`, `tests/stop.rs`, `tests/console/stop.rs`, `console/tests/stop.test.cjs`; inline Claim and Stop tests |
| Dismissal | `src/logs/dismiss.rs` — shared CLI/Console dismissal and take-back using `Evaluation::into_target`, record-side `*.dismiss.json` files, `Captured` normalizes legacy and accumulated kinds for one read per owning path; `src/console/dismiss.rs` — authenticated Dismiss/Undo form posts; `src/console/peek.rs` and `src/console/peek.html` — Command details actions and CLI forms; `src/logs/reading/inbox.rs` — target visibility before collapse (including explicit History Undo), captured coverage, return on an uncovered kind, pending Finding after a dismissed Failed row, waiting exit, Resolved retention from the dismissal and collapse partition; `src/logs/status.rs` and `src/logs/status/inbox.rs` — text, JSON (`dismissal.kinds`) and Console rows | `tests/dismiss.rs` — health transitions, live endings, Security findings, hidden History membership and Spec-wide kind coverage; `src/logs/reading/tests.rs`, `tests/console/dismiss.rs` |
Expand Down
8 changes: 6 additions & 2 deletions src/console/start_controls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,16 +178,20 @@ pub(super) async fn options() -> axum::response::Response {
/// Preserve the cron Pass's CLI options, while the API still decides admission.
pub(super) fn passes(config: &UserConfig, jobs: &[crate::cronjobs::CronJob]) -> Result<Vec<Pass>> {
let directories = crate::launch_directories::load()?;
let home = crate::config::home()?;
let mut passes = Vec::new();
for job in jobs {
let Some((path, arguments)) = crate::launch_directories::cron_arguments(&job.command)
let Some(facts) = crate::cronjobs::Interpretation::new(&job.command).into_launch(&home)
else {
continue;
};
let Ok(path) = facts.directory.canonicalize() else {
continue;
};
let Some(directory) = directories.directories.iter().find(|d| d.path == path) else {
continue;
};
let Ok(parsed) = args::parse(&arguments) else {
let Some(parsed) = facts.command else {
continue;
};
let (mut request, flags, security) = match parsed {
Expand Down
71 changes: 8 additions & 63 deletions src/cronjobs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use serde::{Deserialize, Serialize};
use crate::config::UserConfig;
use crate::logs::pass_starts::{self, PassStart};

mod command;
pub(crate) mod fleet;
pub(crate) use command::{Interpretation, LaunchFacts};

#[derive(Serialize, Deserialize)]
pub(crate) struct CronJobs {
Expand Down Expand Up @@ -89,48 +91,6 @@ where
Ok(serde_json::from_value(value).unwrap_or(None))
}

#[expect(
clippy::wildcard_enum_match_arm,
reason = "inventory recognizes work Commands only; inspection and administration commands have unknown kind"
)]
fn command_kind(command: &str) -> Option<CommandKind> {
use crate::args::{self, Command as Parsed};
let words = crate::launch_directories::shell_words(shell_command(command))?;
let invocation = match words.as_slice() {
[cd, path, and, rest @ ..] if cd == "cd" && and == "&&" && !path.contains(['$', '`']) => {
rest
}
rest => rest,
};
let (executable, arguments) = invocation.split_first()?;
if executable.contains(['$', '`'])
|| std::path::Path::new(executable).file_name()? != "thirdshift"
{
return None;
}
if arguments
.iter()
.any(|word| matches!(word.as_str(), "&&" | "||" | ";" | "|" | "&"))
{
return None;
}
let arguments = crate::launch_directories::literal_cli_arguments(arguments)?;
if arguments.is_empty() {
return None;
}
Some(match args::parse(&arguments).ok()? {
Parsed::Pickup(_) => CommandKind::Pickup,
Parsed::Secure(_) => CommandKind::Security,
Parsed::Architect(_) => CommandKind::Architect,
Parsed::Triage(_) => CommandKind::Triage,
Parsed::Fix(_) => CommandKind::Fix,
Parsed::Review(_) => CommandKind::Review,
Parsed::Process(args::ProcessAction::Run(_)) => CommandKind::Process,
// An Issue URL can select a Spec run or Ticket run; labels are not read here.
_ => return None,
})
}

/// One Fleet report field. A control character or Unicode line separator would
/// start another line or a terminal control sequence, so one machine's schedule
/// or error could look like another machine's entry. JSON keeps the original text.
Expand Down Expand Up @@ -168,7 +128,9 @@ pub(crate) fn load() -> Result<CronJobs> {
let mut jobs = jobs()?;
for job in &mut jobs {
job.repository = repository_label(
crate::launch_directories::from_cron(&job.command, &home)
Interpretation::new(&job.command)
.into_launch(&home)
.and_then(crate::launch_directories::from_facts)
.map(|directory| directory.repository),
);
}
Expand Down Expand Up @@ -249,32 +211,15 @@ fn entry(line: &str) -> Option<CronJob> {
schedule.push(&command[..end]);
command = command[end..].trim_start();
}
// Keep stdin text for display, but don't match it as a command.
let mentions_thirdshift = shell_command(command)
.split(|c: char| c.is_whitespace() || "'\";&|()<>".contains(c))
.any(|word| word.rsplit('/').next() == Some("thirdshift"));
mentions_thirdshift.then(|| CronJob {
let interpreted = Interpretation::new(command);
interpreted.recognized().then(|| CronJob {
schedule: schedule.join(" "),
command: command.into(),
repository: None,
command_kind: command_kind(command),
command_kind: interpreted.command_kind(),
})
}

/// Cron sends everything after its first unescaped % to stdin, even in quotes.
pub(crate) fn shell_command(command: &str) -> &str {
let mut escaped = false;
let command_end = command
.char_indices()
.find_map(|(index, character)| {
let separator = character == '%' && !escaped;
escaped = character == '\\' && !escaped;
separator.then_some(index)
})
.unwrap_or(command.len());
&command[..command_end]
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
Loading