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: its start, duration and progress are derived from its descendants on every save (see set_parent!).
  • baseline_start::Union{Nothing,Date} / baseline_duration::Int: snapshot taken by set_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 an early_start later than start in slack instead of the task silently moving.
  • optimistic::Int / most_likely::Int / pessimistic::Int: the PERT three-point estimate, in the same days as duration. All three 0 (the default) means no estimate; see set_estimate! and pert. They never move the task on their own — the expected duration (o + 4m + p)/6 only reaches duration through pert!, the same way schedule! is what moves dates.
source
Perth.run — Function
Perth.run(; port = 8123, open_browser = true, data_dir = nothing,
          share = false, host = nothing, key = "") -> String

Start 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).

Warning

With share = true and no key, anyone on the local network who knows the port can edit the projects. Never expose the port to the internet.

source
Perth.share! — Function
Perth.share!(on = true) -> Bool

Turn 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.

source
Perth.key! — Function
Perth.key!(key = "") -> Bool

Set (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 rede
source
Perth.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) -> Float64

Use 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.

Note

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!()
source
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!.

source
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.

source
Perth.add_task! — Function
add_task!(p::Project, name; start = today(), duration = 1, kwargs...) -> GanttTask

Add a task to p and persist. Keyword arguments are forwarded to GanttTask (progress, dependencies, color, assignee, notes, milestone).

source
Perth.update_task! — Function
update_task!(p::Project, id::AbstractString; kwargs...) -> GanttTask

Update fields of the task with the given id (any GanttTask field except id) and persist.

source
Perth.schedule! — Function
schedule!(p::Project) -> Project

Reschedule 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.

source
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.

source
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.

source
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.

source
Perth.set_baseline! — Function
set_baseline!(p::Project) -> Project

Snapshot 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.

source
Perth.clear_baseline! — Function
clear_baseline!(p::Project) -> Project

Remove the baseline snapshot from every task and from the project. Persists.

source
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).

source
slippage(p::Project, id::AbstractString) -> Int

Slip of one task in calendar days (positive = later than the baseline). Throws if the task has no baseline.

source
Perth.set_estimate! — Function
set_estimate!(p::Project, id, optimistic, most_likely, pessimistic;
              apply = true) -> GanttTask

Give 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 task
source
Perth.clear_estimate! — Function
clear_estimate!(p::Project, id) -> GanttTask

Drop a task's three-point estimate. The duration it produced stays — removing the estimate is not a reason to move the plan. Persists.

source
Perth.has_estimate — Function
has_estimate(t::GanttTask) -> Bool

Whether the task carries a PERT three-point estimate.

source
Perth.expected_duration — Function
expected_duration(t::GanttTask) -> Float64

The 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.

source
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.

source
Perth.pert! — Function
pert!(p::Project) -> Project

Apply 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.

source
Perth.pert_finish — Function
pert_finish(p::Project) -> NamedTuple

The 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.

source
Perth.finish_probability — Function
finish_probability(p::Project, date::Date) -> Float64

Probability (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.

source
Perth.pert_date — Function
pert_date(p::Project, probability::Real) -> Date

The 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.

source
Perth.pert_simulate — Function
pert_simulate(p::Project; n = 10_000, rng = Random.default_rng()) -> NamedTuple

Monte 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.

source
Perth.set_calendar! — Function
set_calendar!(p::Project, name::AbstractString) -> Project

Set 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.

source
Perth.set_parent! — Function
set_parent!(p::Project, id, parent) -> GanttTask

Move 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.

source
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).

source
Perth.icalendar — Function
icalendar(p::Project) -> String
icalendar(p::Project, path::AbstractString) -> String

The 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.

source
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.

source
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.

source
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.

source
Perth.save_chart — Function
save_chart(p::Project, path; kwargs...) -> String

Render 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.

source