Gantt
Perth.GanttTask — Type
GanttTask(; name, start, kwargs...)A single task (or milestone) on the Gantt chart.
Fields
id::String: short unique identifier (auto-generated).name::String: task label shown on the chart.start::Date: first day of the task.duration::Int: duration in calendar days (≥ 1). Milestones ignore this.progress::Int: completion percentage,0–100.dependencies::Vector{String}: predecessor references. Plain ids mean finish-to-start;"id+3"/"id-2"add lag/lead in days;"SS:id"and"FF:id"(optionally with lag) declare start-to-start / finish-to-finish.cost::Float64: planned cost (any unit).0= use duration (person-days) as the weight in S-curve analytics.color::String: hex color (e.g."#bd93f9"); empty string means automatic.assignee::String: person or resource responsible.notes::String: free-form notes.milestone::Bool: render as a diamond marker instead of a bar.parent::String: id of the parent task (WBS). A task with children is a summary: itsstart,durationandprogressare derived from its descendants on every save (seeset_parent!).baseline_start::Union{Nothing,Date}/baseline_duration::Int: snapshot taken byset_baseline!;nothing/0= no baseline.deadline::Union{Nothing,Date}: a date the task must not finish after. It never moves the task — it caps the late finish in the CPM backward pass, so busting it turns the slack of this task and of everything feeding it negative.nothing= no commitment.pinned::Bool: the start date is fixed (a contract date, a delivery window).schedule!leaves it where it is; the engine still computes where it would go, so a pin the plan can no longer honour shows up as anearly_startlater thanstartinslackinstead of the task silently moving.optimistic::Int/most_likely::Int/pessimistic::Int: the PERT three-point estimate, in the same days asduration. All three0(the default) means no estimate; seeset_estimate!andpert. They never move the task on their own — the expected duration(o + 4m + p)/6only reachesdurationthroughpert!, the same wayschedule!is what moves dates.
Perth.Project — Type
Project(; name, kwargs...)A project: a named collection of GanttTasks.
Perth.run — Function
Perth.run(; port = 8123, open_browser = true, data_dir = nothing,
share = false, host = nothing, key = "") -> StringStart the Perth server and (optionally) open the app in your browser. Returns the URL. The server does not block the REPL; stop it with Perth.stop.
By default only this machine can open the app. Pass share = true to let other machines on the local network open the same projects: every connected machine shows up as a labelled cursor with its name and IP address — exactly like Perth.kanban(share = true). key requires an access key from those machines.
Sharing is a live switch, not a startup-only decision: turn it on and off with the server running via Perth.share! or the UI (File → Share / QR…). To that end the socket binds to 0.0.0.0 and every connection is checked against the current setting — with sharing off, requests from other machines are refused with 403. Pass host to bind a fixed address instead (which disables the live switch).
If port is busy, the next free port is used (up to 20 attempts). data_dir overrides the project storage directory (default: $PERTH_DATA_DIR or ~/.perth).
Perth.stop — Function
Perth.stop()Stop the running Perth server, if any.
Perth.share! — Function
Perth.share!(on = true) -> BoolTurn network sharing on or off on the running gantt server, live — no restart, no Perth.stop. With sharing on, other machines on the local network can open the same projects (see Perth.run); with it off, only this machine can, and remote browsers already connected are disconnected immediately.
The same switch is in the UI (File → Share / QR…), available only from the machine running the server. Sharing can only be toggled when the server was started without an explicit host — see Perth.run.
Perth.key! — Function
Perth.key!(key = "") -> BoolSet (or drop, with "") the access key of the running gantt server, live — no restart, no Perth.stop. Returns whether a key is required from now on. Surrounding whitespace is trimmed, so a pasted key does not fail on a trailing space.
Machines on the network must send the key on every request and on the presence socket; the machine running the server never needs it. The LAN links (and the QR code) carry the key, so nobody has to type it — see Perth.run.
Changing the key disconnects every machine on the network immediately: the key they hold is now the wrong one, and each is asked for the new one on screen rather than left with a dead page. Dropping the key (key!()) disconnects nobody — nothing they hold became invalid.
The same control is in the UI (File → Share / QR…), available only from the machine running the server.
Perth.key!("obra-2026") # exige a chave de quem vem da rede
Perth.key!() # volta a aceitar qualquer um da redePerth.background! — Function
Perth.background!(path; opacity = 0.18, interval = 60) -> String
Perth.background!(dir; …) -> Vector{String}
Perth.background!(paths::AbstractVector; …) -> Vector{String}
Perth.background!(; opacity, interval) -> Float64Use local images as the background of the Perth UI — gantt and kanban, every connected browser. They are read from the machine running the server; the files themselves are never copied, so replacing one on disk changes the background on the next reload.
Point at one file, at a list of files, or at a directory: a directory is read once, right here, and the images in it (sorted by name, up to 60) become the rotation. Whatever is in there that is not a usable image is skipped, and the log line says how many were taken and how many were left out. The directory is not re-read later — the list is frozen at this call, so a file dropped in the folder afterwards is not published to your network behind your back. Add photos by calling background! on the folder again.
With more than one image the UI cycles through them, interval seconds apart, fading the current one out and the next one in. Every connected browser derives the current image from the wall clock, so all of them show the same photo without the server ticking. interval = 0 stops the rotation on the first image.
opacity (0–1) is how strongly the image shows through the page colour. The default is deliberately low: panels, cards and bars keep their solid surfaces, and the image fills the space around them. Pass opacity or interval alone to adjust them without changing the images.
The setting is persisted in settings.json in the Perth data directory. Clear it with Perth.background_clear!; read it back with Perth.background / Perth.backgrounds.
Accepted formats are PNG, JPEG, GIF and WebP, checked by content rather than by file extension, up to 12 MB each.
Every machine that can open Perth can see these images — they are served over the same port, behind the same access key as the rest of the data. Each browser can hide them locally (settings panel → Hide background), which is a rendering preference, not a way to keep an image private. Point at a folder you would be comfortable showing on the office wall.
Perth.background!("~/Imagens/escritorio.jpg")
Perth.background!("~/Imagens/fundos/"; interval = 90)
Perth.background!(["~/a.jpg", "~/b.png"])
Perth.background!(opacity = 0.35)
Perth.background_clear!()Perth.background — Function
Perth.background() -> Union{String,Nothing}Path of the first image used as the UI background, or nothing when none is set (or the files are gone). With a rotation, this is where it starts; Perth.backgrounds gives the whole list. See Perth.background!.
Perth.backgrounds — Function
Perth.backgrounds() -> Vector{String}Every image in the UI background rotation, in the order it cycles through them — the list frozen by Perth.background!, minus whatever has since been deleted from disk. Empty when no background is set.
Perth.background_clear! — Function
Perth.background_clear!()Drop the UI background, live on every connected browser. The image files themselves are left alone. See Perth.background!.
Perth.create_project — Function
create_project(name::AbstractString) -> ProjectCreate (and persist) a new empty project.
Perth.add_task! — Function
add_task!(p::Project, name; start = today(), duration = 1, kwargs...) -> GanttTaskAdd a task to p and persist. Keyword arguments are forwarded to GanttTask (progress, dependencies, color, assignee, notes, milestone).
Perth.update_task! — Function
update_task!(p::Project, id::AbstractString; kwargs...) -> GanttTaskUpdate fields of the task with the given id (any GanttTask field except id) and persist.
Perth.schedule! — Function
schedule!(p::Project) -> ProjectReschedule the project so that no task starts before all of its dependencies have finished. Each task's own start date acts as a start-no-earlier-than constraint: tasks are only pushed forward, never pulled back. Tasks marked pinned keep their start date — the engine still computes where they would have to go, so a pin the plan can no longer honour leaves slack's early_start later than the task's start, instead of the date silently moving. Persists the result.
Throws ArgumentError if the dependency graph has a cycle.
Perth.critical_path — Function
critical_path(p::Project) -> Vector{String}Ids of the tasks on the critical path (slack ≤ 0), in topological order. Delaying any of these delays the whole project.
Slack is only ever negative when a deadline is already missed, and those tasks are more critical than the zero-slack ones — leaving them out would hide exactly the chain that is late.
Perth.slack — Function
slack(p::Project) -> Vector{NamedTuple}Per-task CPM summary as Tables.jl-compatible rows: id, name, early_start, early_finish, slack_days, critical.
slack_days goes negative when a deadline cannot be met: the task and everything feeding it are late by that many days. critical is slack_days ≤ 0.
Perth.deadline_slip — Function
deadline_slip(p::Project) -> Vector{NamedTuple}The tasks whose planned finish is past their deadline, as Tables.jl rows: id, name, deadline, finish (calendar-aware) and slip_days — calendar days late, like slippage, not the business days the CPM slack is measured in.
Tasks without a deadline, and deadlines still being met, produce no row; an empty result means every commitment in the plan holds.
Perth.set_baseline! — Function
set_baseline!(p::Project) -> ProjectSnapshot the current plan: every task's start/duration is copied to its baseline_start/baseline_duration, and p.baseline_at records when. The web UI then draws the baseline as ghost bars and flags slipped tasks; slippage reports the deviation. Persists.
Perth.clear_baseline! — Function
clear_baseline!(p::Project) -> ProjectRemove the baseline snapshot from every task and from the project. Persists.
Perth.has_baseline — Function
has_baseline(t::GanttTask) -> BoolWhether the task carries a baseline snapshot.
Perth.slippage — Function
slippage(p::Project) -> Vector{NamedTuple}Tables.jl-compatible rows comparing the current plan against the baseline, for every task that has one: id, name, baseline_start, baseline_finish, start, finish and slip_days (positive = the task now ends later than planned; calendar days).
slippage(p::Project, id::AbstractString) -> IntSlip of one task in calendar days (positive = later than the baseline). Throws if the task has no baseline.
Perth.set_estimate! — Function
set_estimate!(p::Project, id, optimistic, most_likely, pessimistic;
apply = true) -> GanttTaskGive a task its PERT three-point estimate, in the same days as duration. With apply = true (the default) the task's duration becomes the expected duration (o + 4m + p)/6, rounded to whole days (a tie rounds up: 4.5 becomes 5) — pass apply = false to record the estimate without touching the plan. Persists.
The three numbers are pushed into order rather than sorted: the optimistic estimate is a floor and the pessimistic one a ceiling, so set_estimate!(p, id, 8, 5, 6) records 8, 8, 8 instead of silently swapping the fields you filled in. A partial estimate is allowed — pass 0 for what you don't know and the current duration fills the gap.
set_estimate!(p, t.id, 4, 6, 14) # duration becomes 7
pert(p) # the whole table, with σ per taskPerth.clear_estimate! — Function
clear_estimate!(p::Project, id) -> GanttTaskDrop a task's three-point estimate. The duration it produced stays — removing the estimate is not a reason to move the plan. Persists.
Perth.has_estimate — Function
has_estimate(t::GanttTask) -> BoolWhether the task carries a PERT three-point estimate.
Perth.expected_duration — Function
expected_duration(t::GanttTask) -> Float64The PERT expected duration (o + 4m + p)/6, in the same days as duration. Falls back to the task's plain duration when it has no estimate, so it is safe to call on any task.
Perth.pert — Function
pert(p::Project) -> Vector{NamedTuple}The project's three-point estimates as Tables.jl-compatible rows, in WBS display order, one per estimated task: id, name, optimistic, most_likely, pessimistic, expected (te, unrounded), sd (σ), variance and duration (what the plan currently uses — it differs from expected until pert! runs).
Tasks without an estimate produce no row, so an empty result means nothing in the plan has been estimated yet.
Perth.pert! — Function
pert!(p::Project) -> ProjectApply every three-point estimate on the project: each estimated task's duration becomes its expected duration (o + 4m + p)/6, rounded to whole days (a tie rounds up, so te = 4.5 schedules 5 days — never 4). Milestones and tasks without an estimate are left alone; WBS summaries derive their span from their children as always. Persists.
This is the only thing that turns estimates into a plan — deliberately, like schedule! being the only thing that moves dates. Estimate first, pert!, then schedule! to push the successors around the new durations.
Perth.pert_finish — Function
pert_finish(p::Project) -> NamedTupleThe probabilistic finish of the project under its three-point estimates: expected (the CPM finish computed with expected durations), sd_days (σ of that finish), variance, critical (how many tasks are on the critical path) and estimated (how many of those carry an estimate).
Nothing is written: the estimates feed the engine directly, so the answer is the same whether or not pert! has run.
σ is the textbook one — the square root of the variance summed along the critical path (the riskiest one, when several tie at zero slack). It is an estimate of the spread, not a forecast of the date: parallel chains that are nearly critical can become critical when they slip, and this formula cannot see them, so its expected is optimistic whenever the project has several fronts merging (the classic merge bias). When that matters, ask pert_simulate instead, which reschedules the whole project thousands of times and does see them.
Perth.finish_probability — Function
finish_probability(p::Project, date::Date) -> Float64Probability (0–1) that the project finishes on or before date, from the PERT estimates: Φ((date − expected) / σ).
With no uncertainty on the critical path (σ = 0 — nothing estimated, or every estimate a single point) the answer is the certainty of the plan itself: 1.0 from the expected finish onwards, 0.0 before it.
See pert_finish for what σ does and does not cover.
Perth.pert_date — Function
pert_date(p::Project, probability::Real) -> DateThe date the project finishes by with the given confidence — the P80 a sponsor asks for when the expected finish (P50) is not a promise anyone wants to make.
Inverse of finish_probability: expected + z·σ, rounded to whole days and shifted through the project's calendar, so under a business-day calendar the buffer is counted in working days too.
Perth.pert_simulate — Function
pert_simulate(p::Project; n = 10_000, rng = Random.default_rng()) -> NamedTupleMonte Carlo over the project's three-point estimates: draw a duration for every estimated task from its Beta-PERT distribution, re-run the CPM forward pass, and repeat n times. Returns runs, mean_days and sd_days (spread of the simulated finishes, in days around the mean), expected (the mean finish as a date) and the p10, p50, p80 and p90 finish dates.
This is pert_finish without its blind spot. The formula only propagates variance along today's critical path; here every path is sampled, so a parallel chain with one day of slack and a wide estimate shows up as the risk it is. Expect p50 at or later than the formula's expected finish, and the gap to grow with the number of near-critical chains — that gap is the merge bias, made visible.
Pass a seeded rng (Random.MersenneTwister(1)) for a reproducible run. Cost is one CPM pass per draw: a few hundred tasks × 10 000 draws is a fraction of a second.
Perth.set_calendar! — Function
set_calendar!(p::Project, name::AbstractString) -> ProjectSet the project's working-day calendar (a BusinessDays.jl calendar name, e.g. "Brazil", "BRSettlement", "USSettlement", "WeekendsOnly"). Durations are then interpreted as business days by the scheduling engine. Pass "" to revert to calendar days. Requires using BusinessDays for scheduling to run. Persists the change.
Perth.set_parent! — Function
set_parent!(p::Project, id, parent) -> GanttTaskMove the task id under parent in the WBS (pass nothing or "" to promote it to top level). Throws ArgumentError when the parent is a milestone or when the move would create a cycle. Persists — the parent becomes a summary and rolls up on save.
Perth.tasktable — Function
tasktable(p::Project) -> Vector{NamedTuple}The project's tasks as Tables.jl-compatible rows in WBS display order — ready for DataFrame(tasktable(p)), CSV.write, etc. Columns: id, name, wbs_depth, parent, summary, start, duration, finish (calendar-aware), deadline, deadline_slip (calendar days past it; missing without a deadline), pinned, progress, assignee, dependencies, color, notes, milestone, baseline_start, baseline_finish, slip_days (missing without a baseline), optimistic, most_likely, pessimistic and expected — the PERT three-point estimate and the duration it implies (all missing on a task without an estimate).
Perth.icalendar — Function
icalendar(p::Project) -> String
icalendar(p::Project, path::AbstractString) -> StringThe project's commitments as an iCalendar (.ics) document: one all-day event per milestone and one per task deadline, ready to import into any calendar app. Ordinary tasks are left out on purpose — a two-week bar is noise in a calendar; a point in time and a promise are not.
Milestone events are named after the task; deadline events are prefixed with Deadline: and carry the planned finish (and how late it is) in the description. Summary tasks are containers, not work, so they are skipped. Events are TRANSP:TRANSPARENT — they do not mark the day busy — and their UIDs are stable, so re-importing an updated file updates the events instead of duplicating them.
With path, writes the document there and returns the path; otherwise returns the document as a String.
Perth.overallocations — Function
overallocations(p::Project) -> Vector{NamedTuple}Pairs of leaf tasks assigned to the same person whose date ranges overlap — Tables.jl-compatible rows with assignee, task1, task1_name, task2, task2_name, from and to (the overlapping interval, calendar-aware ends). Summaries are containers, not work, so they are ignored.
Perth.workload — Function
workload(p::Project) -> Vector{NamedTuple}Daily load per person: one Tables.jl-compatible row for each (assignee, date) pair that has work on it, with tasks (how many run that day), effort (the day's share of the tasks' weight — cost when set, otherwise person-days, as in the S-curve) and task_ids. Rows are sorted by person, then date.
Only leaf tasks with an assignee count — summaries are containers, not work — and only working days: under a business-day calendar (set_calendar!) a holiday carries no load. Days with no work produce no row, so the result stays small on long projects.
tasks ≥ 2 is the same overlap overallocations reports as a pair, seen day by day instead of pair by pair.
Perth.ganttplot — Function
ganttplot(p::Project; kwargs...)Render p as a static Makie figure. Requires a Makie backend (e.g. CairoMakie) to be loaded — the method is provided by the PerthMakieExt package extension.
Perth.save_chart — Function
save_chart(p::Project, path; kwargs...) -> StringRender p with ganttplot and save the figure to path (.png, .svg, .pdf) at px_per_unit = 2. Requires a Makie backend (e.g. CairoMakie) — provided by the PerthMakieExt package extension.