Cronboard is a terminal-based application built with Python and the Textual framework that allows users to manage and schedule cron jobs both locally and on remote servers. The application provides an intuitive user interface for creating, editing, pausing, resuming, and deleting cron jobs with validation and human-readable feedback.
- Simplify cron job management through a user-friendly terminal interface
- Support both local and remote SSH-based cron administration
- Provide real-time validation and human-readable descriptions of cron expressions
- Secure passwords and sensitive information with encryption
The project is structured into three main modules:
Purpose: Main application that coordinates all components and handles user interaction.
- Input: None
- Output: ComposeResult with UI components
- Steps:
- Retrieves version number from package
- Sets up configuration path (
~/.config/cronboard/config.toml) - Creates title label with version
- Creates footer for keyboard commands
- Creates tabs for "Local" and "Servers"
- Creates container for content
- Goal: Build the basic UI structure
- Input: None
- Output: None (side effects)
- Steps:
- Loads configuration from TOML file
- Sets theme based on saved preference (default: "catppuccin-mocha")
- Initializes local CronTable
- Mounts and displays local table
- Goal: Initialize application state at startup
- Input: None
- Output: Dictionary with configuration data
- Steps:
- Checks if configuration file exists
- Opens and parses TOML file with
tomllib - Returns empty dictionary on error
- Goal: Load user preferences
- Input:
theme- Theme name (string) - Output: None (saves to file)
- Steps:
- Creates configuration directory if it doesn't exist
- Writes theme to configuration file with
tomlkit
- Goal: Persist theme selection
- Input:
event- Tabs.TabActivated event - Output: None (displays correct content)
- Steps:
- Gets label from activated tab
- Calls
show_tab_content()with correct index
- Goal: Handle tab switching
- Input:
index- Tab index (0=Local, 1=Servers) - Output: None (shows/hides components)
- Steps:
- If index 0: Show local table, hide server view
- If index 1: Create server widget if needed, show it
- Goal: Manage visibility of tab content
- Input:
cron- CronTab objectremote- Boolean (if remote server)ssh_client- Paramiko SSH client (optional)crontab_user- Username for crontab (optional)
- Output: None (opens modal)
- Steps:
- Defines callback function
check_save() - Opens CronCreator modal with screen push
- On save: Updates relevant tables
- Defines callback function
- Goal: Start process for creating new cron job
- Input:
job- CronJob object to delete- Other parameters for context
- Output: None (opens confirmation dialog)
- Steps:
- Defines callback function
check_delete() - Opens CronDeleteConfirmation modal
- On confirmation: Updates tables
- Defines callback function
- Goal: Delete cron job with user confirmation
- Input:
cron- CronTab objectidentificator- Job IDexpression- Cron expressioncommand- Command to execute- Context parameters
- Output: None (opens edit modal)
- Steps:
- Defines callback function
check_save() - Opens CronCreator in edit mode with existing values
- On save: Updates tables
- Defines callback function
- Goal: Edit existing cron job
Purpose: Display and manage cron jobs in a tabular view.
- Input:
remote- Boolean (local or remote)ssh_client- SSH clientcrontab_user- Username
- Output: CronTable instance
- Goal: Initialize table with context
- Input: None
- Output: None (builds table)
- Steps:
- Initializes local CronTab with
CronTab(user=True) - Adds columns: Identificator, Expression, Command, Last Run, Next Run, Status
- If remote: Fetches crontab via SSH command
crontab -lorcrontab -u <user> -l - Handles exit status 1 (no crontab) as empty string
- Parses SSH crontab content with
CronTab(tab=content) - Calls
load_crontabs()to populate table
- Initializes local CronTab with
- Goal: Set up table with data
- Input:
cron- CronTab object - Output: None (adds rows to table)
- Steps:
- Iterates through each job in cron
- Extracts expression with
job.slices.render() - Gets command and identificator (comment)
- Checks if job is active with
job.is_enabled() - Calculates next run time with
job.schedule().get_next() - Calculates previous run time with
schedule.get_prev() - Formats dates as "dd.mm.yyyy at HH:MM"
- Handles ValueError for invalid expressions
- Adds row to table with all values
- Goal: Convert cron data to table rows
- Input: None
- Output: None (updates display)
- Steps:
- Clears existing table content
- If remote: Parses SSH cron
- Otherwise: Parses local cron
- Goal: Load/reload cron data
- Input: None
- Output: None (updates data)
- Steps:
- If remote: Runs
crontab -lvia SSH - Reads output and parses with CronTab
- If local: Reinitialize CronTab(user=True)
- Calls
load_crontabs()
- If remote: Runs
- Goal: Refresh table with fresh data
- Input: None (uses cursor_row)
- Output: None (toggles pause status)
- Steps:
- Gets row at cursor position
- Extracts identificator, expression and command
- Finds matching job in crontab
- Toggles enabled status with
job.enable(False/True) - If remote: Writes to remote crontab with
write_remote_crontab() - If local: Writes with
cron.write() - Reloads table
- Goal: Pause or resume a cron job
- Input: None (uses selected row)
- Output: None (opens edit modal)
- Steps:
- Gets row at cursor
- Extracts identificator, expression and command
- Finds job with
find_if_cronjob_exists() - Calls
action_edit_cronjob_keybind()with values
- Goal: Start editing selected job
- Input: None (uses selected row)
- Output: None (opens delete confirmation)
- Steps:
- Gets row at cursor
- Finds job with
find_if_cronjob_exists() - Calls
action_delete_cronjob_keybind()with job
- Goal: Start deletion process for selected job
- Input:
identificator- Job IDcmd- Command
- Output: CronJob or None
- Steps:
- Iterates through all jobs in crontab
- Compares comment and command
- Returns match or None
- Goal: Find specific job in crontab
- Input: None (uses instance variables)
- Output: Boolean (success)
- Steps:
- Renders crontab content with
ssh_cron.render() - Constructs command:
crontab -u <user> -orcrontab - - Executes SSH command with
exec_command() - Writes content to stdin
- Closes write channel with
shutdown_write() - Checks exit status and stderr
- Returns True on success, False on error
- Renders crontab content with
- Goal: Update crontab on remote server
Purpose: Modal for creating or editing cron jobs with validation.
Dictionary mapping special cron expressions to standard format:
@reboot→ None (special handling)@hourly→ "0 * * * *"@daily→ "0 0 * * *"@weekly→ "0 0 * * 0"@monthly→ "0 0 1 * *"@yearly→ "0 0 1 1 *"@annually→ "0 0 1 1 *"@midnight→ "0 0 * * *"
- Input:
cron- CronTab objectexpression- Existing expression (for editing)command- Existing commandidentificator- Existing ID- Context parameters
- Output: CronCreator instance
- Goal: Initialize creator with context
- Input: None
- Output: ComposeResult with form elements
- Steps:
- Creates Grid container
- Adds information labels about special characters (*, ,, -, /)
- Adds Input for cron expression with placeholder "* * * * *"
- Adds Label for description (label_desc)
- Adds Input for command
- Adds Input for identificator
- Adds Save and Cancel buttons
- Goal: Build input form
- Input:
event- Input.Changed event - Output: None (updates description)
- Steps:
- Checks if input is "expression" field
- Gets description label
- Calls
expression_description()with expression - Removes existing error messages
- Goal: Provide real-time validation and feedback
- Input:
expr- Cron expression (string)label_desc- Label widget
- Output: None (updates label)
- Steps:
- If empty: Clears label and removes classes
- If "@reboot": Shows "Runs at system startup"
- Converts alias with
CRON_ALIASES.get() - Creates ExpressionDescriptor with options:
- locale_code = "en"
- use_24hour_time_format = True
- Generates description with
get_description() - On success: Updates label with green color (success class)
- On Exception: Shows "Invalid cron expression" with red color (error class)
- Goal: Convert cron expression to human-readable text
- Input:
event- Button.Pressed event - Output: None (saves or cancels)
- Steps:
- If Cancel: Dismiss with False
- If Save:
- Gets values from all input fields
- Validates that identificator is not empty
- Finds existing job with
find_if_cronjob_exists() - If job exists:
- Updates command with
job.set_command() - Updates expression with
job.setall()
- Updates command with
- If new job:
- Creates with
cron.new(command, comment) - Sets expression with
setall()
- Creates with
- Calls
write_cron_changes()to save - Dismiss with True on success
- Shows error message on ValueError/KeyError
- Goal: Save cron job with validation
- Input: None
- Output: None (writes changes)
- Steps:
- If remote and SSH client:
- Renders crontab with
cron.render() - Constructs command based on crontab_user
- Executes SSH command
- Writes content to stdin
- Closes write channel
- Checks exit status and errors
- Shows notification on error
- Renders crontab with
- If local:
- Writes with
cron.write()
- Writes with
- If remote and SSH client:
- Goal: Persist cron changes
- Input:
identificator- Job IDcmd- Command
- Output: CronJob or None
- Steps:
- Iterates through cron
- Compares comment and command
- Returns match or None
- Goal: Check if job already exists
Purpose: Manage and connect to remote servers via SSH.
- Input: None
- Output: CronServers instance
- Steps:
- Sets servers_path to
~/.config/cronboard/servers.toml - Loads servers with
load_servers() - Initializes SSH client and table as None
- Sets servers_path to
- Goal: Set up server management
- Input: None
- Output: ComposeResult with tree and content area
- Steps:
- Creates CronTree with "Servers" root
- Expands root node
- Creates content_area Label with instructions
- Mounts in Grid layout
- Goal: Build server UI
- Input: None
- Output: None (populates tree)
- Steps:
- Gets servers tree
- Iterates through saved servers
- Adds leaf node for each server with format "name: crontab_user"
- Refreshes tree
- Goal: Display saved servers
- Input: None (uses selected node)
- Output: None (connects to server)
- Steps:
- Gets cursor_node from tree
- Checks that it's not root node
- Gets server_info based on server_id
- Calls
connect_to_server()with server_info
- Goal: Start connection to selected server
- Input:
server_info- Dictionary with server details - Output: None (establishes connection)
- Steps:
- Creates Paramiko SSHClient
- Loads system host keys with
load_system_host_keys() - Sets missing host key policy to WarningPolicy
- Extracts host, port, username, password
- If ssh_key: Connects without password
- Otherwise: Connects with password
- Closes existing connection if active
- Saves new ssh_client
- Calls
show_cron_table_for_server() - Sets connected=True and saves
- Shows success notification
- Handles AuthenticationException and other errors
- Goal: Establish SSH connection to server
- Input:
ssh_client- Active SSH clientserver_info- Server detailscrontab_user- Username for crontab
- Output: None (displays table)
- Steps:
- If table exists:
- Updates ssh_client, remote and crontab_user
- Refreshes table
- If new table:
- Creates CronTable with remote=True
- Finds container and horizontal layout
- Removes old content
- Mounts new table
- Updates content_area
- If table exists:
- Goal: Display cron jobs from remote server
- Input: None
- Output: None (shows message)
- Steps:
- Removes existing table if active
- Creates disconnected Label
- Finds container
- Removes old content
- Mounts new label
- Updates content_area
- Goal: Display disconnected state
- Input: None
- Output: None (disconnects)
- Steps:
- Closes SSH client if active
- Resets current_ssh_client
- Calls
show_disconnected_message() - Sets connected=False for all servers
- Shows notification
- Saves changes
- Goal: Terminate SSH connection
- Input: None
- Output: Dictionary with servers
- Steps:
- Checks if servers.toml exists
- Opens and parses with tomllib
- For each server:
- Decrypts password with
decrypt_password() - Handles missing fields
- Sets password to None if empty
- Ensures crontab_user exists
- Decrypts password with
- Returns empty dict on error
- Goal: Load saved servers with decrypted passwords
- Input: None
- Output: None (saves to file)
- Steps:
- Creates configuration directory
- For each server:
- Encrypts password with
encrypt_password() - Builds TOML-safe dictionary with:
- name, host, port, username
- encrypted_password
- ssh_key, connected
- crontab_user
- Encrypts password with
- Writes to file with tomlkit.dump()
- Handles errors with notification
- Goal: Persist server configuration securely
- Input: None
- Output: None (opens modal)
- Steps:
- Defines callback
on_server_added() - Opens CronSSHModal
- On result: Calls
add_server_to_tree()with values
- Defines callback
- Goal: Start process for adding server
- Input: Server details
- Output: None (adds to tree and storage)
- Steps:
- Generates server_id:
username@host:crontab_user - Checks if server_id exists
- Builds server_info dictionary
- Adds leaf to tree
- Refreshes tree
- Calls
save_servers()
- Generates server_id:
- Goal: Add new server to configuration
- Input: None (uses selected node)
- Output: None (deletes server)
- Steps:
- Gets cursor_node from tree
- Validates it's a server node
- Gets server_info
- Defines callback
on_delete_confirmed() - Opens CronDeleteConfirmation modal
- On confirmation:
- Disconnects if connected
- Deletes from dictionary
- Removes node from tree
- Saves changes
- Goal: Delete server with confirmation
Purpose: Modal for confirming deletion of jobs or servers.
- Input: Context for deletion
- Output: Modal instance
- Goal: Initialize confirmation dialog
- Input: None
- Output: ComposeResult with message and buttons
- Steps:
- Determines message based on:
- Custom message if provided
- Server deletion if server provided
- Job deletion if job provided
- Generic message otherwise
- Creates Grid with Label and buttons (Delete/Cancel)
- Determines message based on:
- Goal: Display confirmation dialog
- Input:
event- Button.Pressed event - Output: None (deletes or cancels)
- Steps:
- If Cancel: Dismiss with False
- If Delete and job exists:
- Removes job from cron with
cron.remove(job) - If remote: Calls
write_remote_crontab() - Otherwise: Writes with
cron.write()
- Removes job from cron with
- Dismiss with True
- Goal: Execute deletion on confirmation
- Input: None
- Output: Boolean (success)
- Steps:
- Renders crontab
- Constructs SSH command
- Writes to remote crontab
- Checks exit status
- Returns success/failure
- Goal: Update remote crontab after deletion
Purpose: Encrypt and decrypt passwords for secure storage.
- Input: None
- Output: Encryption key (bytes)
- Steps:
- Creates
~/.config/cronboard/if doesn't exist - Checks if
secret.keyexists - If not:
- Generates new Fernet key with
Fernet.generate_key() - Writes to file
- Sets file permissions to 0o600 (owner read/write only)
- Generates new Fernet key with
- If exists:
- Reads key from file
- Returns key
- Creates
- Goal: Ensure persistent encryption key per user
- Input:
password- Plaintext password (string) - Output: Encrypted token (string)
- Steps:
- Checks if password is empty
- If empty: Returns empty string
- Encodes password to bytes
- Encrypts with Fernet
- Decodes to string for storage
- Returns encrypted token
- Algorithm: Fernet (symmetric encryption with AES-128-CBC)
- Goal: Convert plaintext to encrypted format
- Input:
token- Encrypted password (string) - Output: Decrypted password (string)
- Steps:
- Checks if token is empty
- If empty: Returns empty string
- Encodes token to bytes
- Decrypts with Fernet
- Decodes to string
- Returns decrypted password
- Goal: Convert encrypted format to plaintext
Security Features:
- Uses Fernet (symmetric encryption based on AES-128-CBC)
- Key file protected with 0o600 permissions
- Passwords never stored in plaintext on disk
- Each installation has unique key
Purpose: Modal for adding new SSH servers.
- Input: None
- Output: ComposeResult with input fields
- Steps:
- Creates Grid container
- Adds Input fields for:
- Hostname (format: host:port)
- Username
- Password (with password=True for masking)
- Crontab user (optional)
- Adds information labels
- Adds buttons (Add Server/Cancel)
- Goal: Collect server details
- Input:
event- Input.Changed event - Output: None (removes error messages)
- Steps:
- Removes existing error labels
- Goal: Reset error messages on change
- Input:
event- Button.Pressed event - Output: None (returns server data or cancels)
- Steps:
- If Cancel: Dismiss with False
- If Add Server:
- Gets values from all input fields
- Parses hostname:port with
split(':') - Converts port to int
- On ValueError: Shows error "Invalid host format"
- Builds server dictionary with:
- hostname, port, username, password
- ssh_key=True if no password
- crontab_user if provided
- Dismiss with server dictionary
- Goal: Validate and return server configuration
Purpose: Custom Tree widget with Vim-like navigation.
- Input: Standard Tree arguments
- Output: CronTree instance
- Goal: Initialize tree with custom bindings
Key Bindings:
j→ cursor_down (Down)k→ cursor_up (Up)
Purpose: Provide Vim users with familiar navigation
- User presses 'c' in CronTable
- CronTable calls
app.action_create_cronjob()with local cron - App opens CronCreator modal
- User fills in fields (expression, command, identificator)
- Real-time validation with
expression_description() - On Save:
on_button_pressed()validates input - Creates new job with
cron.new() - Sets expression with
job.setall() - Writes with
cron.write() - Modal closes and table refreshes
1-3. Same as local, but with SSH parameters
4-6. Same as local 7. Creates job in SSH cron object 8. Sets expression 9. write_cron_changes() renders crontab 10. Executes crontab - via SSH 11. Writes content to stdin 12. Checks exit status 13. Modal closes and remote table refreshes
- User adds server with 'a' in CronServers
- CronSSHModal collects host, port, username, password/key
- Server saved in servers.toml with encrypted password
- User presses 'c' to connect
connect_to_server()creates Paramiko SSHClient- Loads system host keys
- Connects with password or key
show_cron_table_for_server()creates remote CronTable- CronTable executes
crontab -lvia SSH - Parses output and displays in table
- User selects job and presses 'p'
action_pause_cronjob()gets row data- Finds matching job in crontab
- Toggles status with
job.enable(False/True) - If local: Writes with
cron.write() - If remote: Calls
write_remote_crontab() - Table reloads with updated status
- Cron expressions: Validated with python-crontab's
setall(), ValueError raised on invalid format - SSH connection: Handles AuthenticationException and general exceptions
- Decryption: Try-catch on decryption, fallback to None
- Remote crontab: Checks exit status and stderr for errors
- Real-time cron description (green for valid, red for invalid)
- Notifications for connections, errors and success
- Error labels in modals on invalid input
- Encrypted with Fernet (AES-128-CBC)
- Key stored in
~/.config/cronboard/secret.keywith 0o600 - Never stored in plaintext
- Uses system host keys (
~/.ssh/known_hosts) - WarningPolicy for unknown hosts
- Supports both password and key-based authentication
- Stored in
~/.config/cronboard/ - TOML format for readability
- Per-user configuration
- textual (6.2.1+): TUI framework for terminal UI
- python-crontab (3.3.0+): Crontab parsing and manipulation
- cron-descriptor (2.0.6+): Conversion to human-readable text
- paramiko (4.0.0+): SSH client for remote connections
- cryptography (via Fernet): Password encryption
- croniter (6.0.0+): Cron schedule calculations
- tomlkit (0.13.3+): TOML parsing and writing
- bcrypt (5.0.0+): Password hashing (dependency)
- dt-croniter (6.0.1+): Datetime support for croniter
The project includes pytest setup with:
- pytest (8.4.2+)
- pytest-asyncio (1.2.0+)
Test files located in src/test/cronboard_test.py.
Ctrl+Q- Quit applicationTab- Switch panel/focus
h/l- Left/right (Vim-style)j/k- Down/up (Vim-style)c- Create new cron jobe- Edit selected jobD- Delete selected jobp- Pause/resume jobr- Refresh table
a- Add serverc- Connect to selected serverd- Disconnect from serverD- Delete serverj/k- Navigate tree
theme = "catppuccin-mocha"[username@hostname:crontab_user]
name = "username@hostname"
host = "hostname"
port = 22
username = "username"
encrypted_password = "encrypted_token_here"
ssh_key = false
connected = false
crontab_user = "username"Binary file with Fernet encryption key (auto-generated).
Cronboard is a comprehensive solution for cron management that combines simple local handling with powerful remote SSH functionality. Through proper validation, real-time feedback and secure password storage, it provides a user-friendly experience while maintaining security and reliability.
The architecture is modular with clear responsibilities: app.py coordinates, widgets handle UI, and encryption secures data. All components work together through well-defined interfaces and callback patterns.