A need may be paused #14
@@ -4,6 +4,10 @@ One line per change, grouped by the release that shipped it. Newest
|
||||
first. A release is a Cargo.toml bump plus a `v<version>` tag; the
|
||||
content repos pin the tag they run (see the end of this file).
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Trainer: `paused: true` on a need. Its card stays on the front door with what the business does and a sentence saying it is not taking new ones right now, with no form and a button that does not press; nothing is recorded. The lint reports a paused need as a warning ("paused, not open now") and counts it on its own in the summary, as `planned` already did for needs not open yet. Klingenberg Bygg's building applications are paused for capacity.
|
||||
|
||||
## 0.6.4 (2026-09-30)
|
||||
|
||||
- The rate limit is a hundred at once, refilled at ten a second (it was 30, one every two seconds, which a page with a picture strip used up in one load: a few reloads and the stylesheet was refused). The files a page is made of, from portal's own disk, are not counted at all. `PORTAL_RATE_PER_SECOND` replaces `PORTAL_RATE_SECONDS`; no site set it.
|
||||
|
||||
@@ -100,7 +100,9 @@ needs:
|
||||
- { name: oppstart, type: date, optional: true }
|
||||
- { name: skisser, type: file, optional: true }
|
||||
|
||||
# På pause: fullt hos oss. Ta linjen ut for å åpne igjen.
|
||||
- id: soknad
|
||||
paused: true
|
||||
who: en byggherre med et ferdig prosjektert bygg som trenger offentlig godkjenning før første spadetak
|
||||
wants: en godkjent byggesøknad, med ansvarsrett der det kreves
|
||||
lands_in: soknader
|
||||
|
||||
@@ -109,7 +109,8 @@
|
||||
"telefon": "Telefonnummer",
|
||||
"adresse": "Adresse",
|
||||
"tiltak": "Tilbygg, ombygging, nybygg…"
|
||||
}
|
||||
},
|
||||
"paused": "Akkurat nå har vi ikke kapasitet til nye søknadsoppdrag; vi åpner igjen så snart vi kan."
|
||||
}
|
||||
},
|
||||
"who": {
|
||||
|
||||
+5
-3
@@ -180,11 +180,13 @@ fn check_needs(
|
||||
}
|
||||
}
|
||||
if failed == 0 {
|
||||
let planned = file.needs.iter().filter(|n| n.planned).count();
|
||||
let planned = file.needs.iter().filter(|n| n.planned && !n.paused).count();
|
||||
let paused = file.needs.iter().filter(|n| n.paused).count();
|
||||
println!(
|
||||
"OK: {} need(s) structurally met, {} planned, {} persona(s)",
|
||||
file.needs.len() - planned,
|
||||
"OK: {} need(s) structurally met, {} planned, {} paused, {} persona(s)",
|
||||
file.needs.len() - planned - paused,
|
||||
planned,
|
||||
paused,
|
||||
file.personas.len()
|
||||
);
|
||||
}
|
||||
|
||||
+9
-5
@@ -78,6 +78,10 @@ pub struct Need {
|
||||
/// as a warning, so the gap stays visible without failing CI.
|
||||
#[serde(default)]
|
||||
pub planned: bool,
|
||||
/// Open before and closed for now (full up, out of season): its
|
||||
/// card is on the page with no form. Reports like `planned`.
|
||||
#[serde(default)]
|
||||
pub paused: bool,
|
||||
/// Each state of the bucket in plain words, in the site's
|
||||
/// language, for the scorer: state ids are ASCII, and a Norwegian
|
||||
/// `ikke_lost` reads as English to a model.
|
||||
@@ -305,11 +309,11 @@ pub fn check(
|
||||
let mut report = |level: Level, message: String| {
|
||||
findings.push(Finding {
|
||||
need: need.id.clone(),
|
||||
level: if need.planned { Level::Warn } else { level },
|
||||
message: if need.planned && level == Level::Fail {
|
||||
format!("planned, not met yet: {message}")
|
||||
} else {
|
||||
message
|
||||
level: if need.planned || need.paused { Level::Warn } else { level },
|
||||
message: match (need.paused, need.planned, level == Level::Fail) {
|
||||
(true, _, true) => format!("paused, not open now: {message}"),
|
||||
(false, true, true) => format!("planned, not met yet: {message}"),
|
||||
_ => message,
|
||||
},
|
||||
})
|
||||
};
|
||||
|
||||
+28
-3
@@ -425,6 +425,14 @@ pub struct PlanNeed {
|
||||
/// picture.
|
||||
#[serde(default)]
|
||||
pub presents: bool,
|
||||
/// Not taking new cases for now: full up, a season closed. The
|
||||
/// card stays where it is, saying what the business does and that
|
||||
/// it is paused, with no form to fill in and a button that does not
|
||||
/// press; the server refuses a submission. Remove the line to open
|
||||
/// again - the desk, the stages and every case under way are
|
||||
/// untouched either way.
|
||||
#[serde(default)]
|
||||
pub paused: bool,
|
||||
pub collects: Vec<Field>,
|
||||
/// How many required fields this person will tolerate: what the
|
||||
/// business said about them, not what the form happens to ask.
|
||||
@@ -1082,6 +1090,8 @@ pub fn fallback_words(plan: &Plan) -> BTreeMap<String, Value> {
|
||||
"button": "Send",
|
||||
"encouragement": "",
|
||||
"icon": "",
|
||||
// Said after the description, on a paused need's card only.
|
||||
"paused": if need.paused { loc(lang, "We are not taking new ones right now, and open again as soon as we can.", "Akkurat nå tar vi ikke imot nye; vi åpner igjen så snart vi kan.") } else { String::new() },
|
||||
// Three sections of context the plan already knows: what
|
||||
// the person must have for this to work, the road a case
|
||||
// travels, and who is at the other end. The fallbacks are
|
||||
@@ -1957,15 +1967,29 @@ fn form_alternative(plan: &Plan, need: &PlanNeed, w: &Value, action: &str) -> Va
|
||||
if !s(&w["who_description"]).trim().is_empty() {
|
||||
features.push(strip(&w["who_title"], &w["who_description"], s(&w["who_icon"])));
|
||||
}
|
||||
// A paused need asks for nothing: a form nobody can send has
|
||||
// already wasted the time it took to fill in.
|
||||
if !need.paused {
|
||||
features.push(feature);
|
||||
}
|
||||
let description = if need.paused {
|
||||
text_map(&w["description"], |d| format!("{} {}", d.trim(), s(&w["paused"]).trim()).trim().to_string())
|
||||
} else {
|
||||
text(&w["description"])
|
||||
};
|
||||
let mut alt = json!({
|
||||
"name": text(&w["heading"]),
|
||||
"description": text(&w["description"]),
|
||||
"description": description,
|
||||
"action": action,
|
||||
"consequence": button(&w["button"]),
|
||||
"record_as": need.lands_in,
|
||||
"features": features,
|
||||
});
|
||||
// Paused, it records nothing and says where nothing goes.
|
||||
if need.paused {
|
||||
alt["disabled"] = json!(true);
|
||||
alt.as_object_mut().map(|a| a.remove("record_as"));
|
||||
}
|
||||
if !s(&w["encouragement"]).trim().is_empty() {
|
||||
alt["encouragements"] = button(&w["encouragement"]);
|
||||
}
|
||||
@@ -2452,6 +2476,7 @@ pub fn render(plan: &Plan, words: &BTreeMap<String, Value>) -> anyhow::Result<BT
|
||||
"stage_words": all_stages(n).iter().map(|s| (s.clone(), json!(stage_text(n, s)))).collect::<Map<_, _>>(),
|
||||
});
|
||||
if let Some(group) = &n.enters_as { need["enters_as"] = json!(group); }
|
||||
if n.paused { need["paused"] = json!(true); }
|
||||
let others: BTreeSet<&String> = n.moves_by.values().flatten().filter(|m| *m != SUBMITTER && **m != n.handled_by).collect();
|
||||
if !others.is_empty() { need["also_moved_by"] = json!(others); }
|
||||
need
|
||||
@@ -2525,7 +2550,7 @@ fn words_prompt(plan: &Plan, path: &str, fallback: &Value, tone: Option<&str>) -
|
||||
context.extend(
|
||||
plan.needs
|
||||
.iter()
|
||||
.map(|n| format!("- {}: {} - wants: {}. Handled by {}. Stages: {}. Lands in `{}`; its form collects: {}.", n.id, n.who, n.wants, n.handled_by, n.stages.join(" -> "), n.lands_in, n.collects.iter().map(|f| format!("{{{}}}{}{}", f.name, if f.kind == "textarea" { " (long)" } else { "" }, if f.optional { " (optional)" } else { "" })).collect::<Vec<_>>().join(", "))),
|
||||
.map(|n| format!("- {}{}: {} - wants: {}. Handled by {}. Stages: {}. Lands in `{}`; its form collects: {}.", n.id, if n.paused { " (PAUSED: not taking new cases right now; its card shows no form and its button does not press, and `paused` is the sentence after its description that says so)" } else { "" }, n.who, n.wants, n.handled_by, n.stages.join(" -> "), n.lands_in, n.collects.iter().map(|f| format!("{{{}}}{}{}", f.name, if f.kind == "textarea" { " (long)" } else { "" }, if f.optional { " (optional)" } else { "" })).collect::<Vec<_>>().join(", "))),
|
||||
);
|
||||
format!(
|
||||
"You write the words for one page of a website for this business.\n\nBusiness: {}\nAbout: {}\nContact: {} <{}>\nLanguage to write in: {}\n\nEveryone the site serves:\n{}\n\nThe page: {path}\nIt is {purpose}\n\nBelow is the page as JSON. Every value is a slot that says what to write there. Return the same JSON with every slot replaced by your words. Keep every key exactly as it is. A slot left as it is counts as no answer.\n\nRules for the words:\n- `question` is a question the reader could ask themselves.\n- A heading says what the reader gets, in the reader's own words, and must work without its description.\n- Lead with what a thing enables. No negations in headings. No trade jargon the reader would not use.\n- Descriptions are one or two short sentences. Labels and buttons are a few words.\n- The button says what pressing it does; the encouragement says what happens next or how little it takes; the form's guidance line says what to have ready. Placeholders are example answers in the reader's words.\n- An icon is one name from this list, chosen for what the reader is doing, or empty: {}\n- Write the way people in this trade and country actually speak.{voice}\n\n{}\n\nReply with the JSON object only.",
|
||||
@@ -2682,7 +2707,7 @@ mod tests {
|
||||
stages: v(stages), done_when: v(done), success: v(success), skippable: v(skippable),
|
||||
moves_by: BTreeMap::new(), watched_by: vec![],
|
||||
events: BTreeMap::new(), attended_by: None, mails: BTreeMap::new(), stages_text: BTreeMap::new(), turned_down_from: vec![], reached_from: BTreeMap::new(), requires: String::new(), followed_by: vec![],
|
||||
grants: BTreeMap::new(), deadlines: BTreeMap::new(), escalate: BTreeMap::new(), public_states: vec![], presents: false,
|
||||
grants: BTreeMap::new(), deadlines: BTreeMap::new(), escalate: BTreeMap::new(), public_states: vec![], presents: false, paused: false,
|
||||
collects: vec![], max_fields: 3,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,6 +191,11 @@ rest. Each is one line in the plan, and the builder does the wiring:
|
||||
of them, as cards: the people who work there, entered from inside
|
||||
(`enters_as`), each with a `picture` field that shows as their
|
||||
portrait.
|
||||
- **Pause what you cannot take on.** `paused: true` on a need keeps
|
||||
its card on the page, saying what the business does and that it is
|
||||
not taking new ones right now, with no form and a button that does
|
||||
not press. Take the line out to open again; the desk and every case
|
||||
under way are untouched.
|
||||
- **Show the work.** `slideshow` on the plan is portal's own block,
|
||||
carried onto the front door as written: a strip of pictures under
|
||||
the question, each with its caption.
|
||||
|
||||
Reference in New Issue
Block a user