Event data: source-of-truth YAML, locations, archive policy, naming convention, derived active camp, QA isolation, the camps.yaml validator.
Part of the requirements index. Section IDs (02-§N.M) are stable and cited from code; they do not encode the file path.
Past camps must remain accessible.
Participants who submit events should be able to edit those events for as long as the event is in the future. This uses a lightweight, cookie-based ownership model that requires no login.
Max-Age of 7 days; submitting another event updates (extends) it. Secure flag (HTTPS only) and SameSite=Strict to prevent
cross-site misuse. httpOnly).
Because the schedule pages are static HTML pre-rendered at build time, client-side
JavaScript is the only layer that can read the cookie and show or hide edit links
per event. Server-side validation on every edit request compensates for the
absence of httpOnly. This trade-off is explicit and documented. sb_session. api.sommar.example.com and sommar.example.com), the session cookie
must be set with a Domain attribute covering the shared parent domain so that
client-side JavaScript running on the static site can read the cookie via
document.cookie. The domain is supplied via the COOKIE_DOMAIN environment
variable. If the variable is not set, no Domain attribute is included and the
cookie is scoped to the API host only (acceptable for single-origin deployments). localStorage as sb_cookie_consent.
A declined decision is not persisted — the prompt will appear again next time
the user submits an event, allowing them to change their mind. Domain attribute the server used. The domain value is injected at build time
via a data-cookie-domain attribute on the <body> element. If the attribute
is absent or empty, no Domain is included (single-origin fallback). COOKIE_DOMAIN environment variable and inject
it as a data-cookie-domain attribute on the <body> element of every page
that loads session.js. events.json must be
kept — not removed. A newly-submitted event may not yet appear in the JSON
because the event-data deploy is still in progress. Removing unknown IDs would
silently discard the session cookie the server just set. data-event-id attribute
containing the event’s stable ID so JavaScript can identify it. /redigera.html?id={eventId}. /idag.html) also shows a “Redigera” link next to
each event the visitor owns or for which the visitor holds a valid admin
token (§91), and that has not passed — using the same rule and link text
as the weekly schedule. idag.html at build time includes the event’s
id field so client-side JavaScript can associate rendered rows with their
stable IDs. idag.html carry data-event-id and
data-event-date HTML attributes so session.js can inject the edit link
using the same mechanism as on schema.html. /redigera.html. id query parameter, checks the session cookie,
and fetches /events.json to pre-populate the form with the event’s current
values. id must not change after creation, even when mutable
fields are edited. /events.json is generated at build time containing all events
for the active camp. owner, meta) are excluded. POST /edit-event endpoint accepts edit requests. sb_session cookie from the request, parses the event
ID array, and verifies the target event ID is present — or that the request
body contains a valid adminToken (§91). meta.updated_at field is updated on every successful edit. credentials: 'include' so that
Set-Cookie response headers from the cross-origin API are applied by the
browser. Without this, the cookie is silently discarded. form.elements
returns undefined; calling .value on it throws a TypeError that aborts
the submission and breaks the consent banner interaction. /edit-event endpoint. The build step derives
the edit URL from the API_URL environment variable by replacing a trailing
/add-event path segment with /edit-event; if API_URL does not end with
/add-event, the edit URL falls back to /edit-event. credentials: 'include' so that the
sb_session cookie is sent to the cross-origin API. Without this the server
cannot verify ownership and will reject the request with HTTP 403. adminToken so the server can verify admin
status. The archive page (/arkiv.html) presents past camps as an interactive vertical
timeline. Each camp is a point on the timeline that expands to show details.
archived: true in camps.yaml are shown. start_date). <button> element with aria-expanded and
aria-controls attributes so screen readers announce the state. Each expanded accordion shows, in order:
camps.yaml) link is non-empty in camps.yaml) Date range and location are already displayed in the accordion header (§21.4) and must not be repeated inside the panel.
Fields that are empty or absent must not produce blank rows or placeholder text.
The accordion header must display the camp name as the primary text, followed by the date range and location in subdued (gray) text to the right.
D–D månadsnamn YYYY (or spanning months
when the camp crosses a month boundary). ·). link field, the expanded panel must show
the Facebook logo image (images/facebook-ikon.webp)
as a clickable link to the Facebook group, replacing the previous text
button. target="_blank",
rel="noopener noreferrer"). alt text (e.g.
“Facebookgrupp”). Each expanded accordion must display the camp’s events below the description and Facebook link. Events are loaded from the camp’s event YAML file at build time.
description or link field must be rendered as
expandable <details> elements — identical to the weekly schedule page
(schema.html). The ℹ️ icon and dotted-underline title style must
appear on these rows. description or link remain flat (plain <div>),
showing only time, title, and metadata. The homepage must show a list of upcoming (and recently past) camps so that visitors can see what is planned and what has already happened this year.
archived is false OR the camp’s
start_date year matches the current year. start_date ascending (nearest date first). end_date is strictly before today. Europe/Stockholm). ✔) and strikethrough
text, making it immediately clear they have already taken place. ☐) and
normal text. camps.yaml. sections.yaml like other sections. var(--color-terracotta). information field, the information text is
shown below the camp entry. data-end attributes; JavaScript applies the visual state. docs/07-DESIGN.md §7.
No hardcoded colors, spacing, or typography. The name field in camps.yaml follows a fixed format so that camp
titles are consistent across the archive, upcoming camps section, and
any other display.
{type} {year} {month}, where {type} is the camp
series name (e.g. “SB sommar”, “SB vinter”), {year} is the four-digit
year, and {month} is the Swedish month name in lowercase. The active camp must be derived automatically from camp dates at build time
and at API request time. The manual active field is removed from the data
model. This ensures exactly one camp is active at all times without manual
intervention.
Exactly one camp is active at any time, selected by a date-based priority
applied at both build time and at API request time — today on dates →
nearest future start_date → latest end_date, with the earlier
start_date winning any overlap. See 03-architecture/data-layer.md §2 "Metadata
Layer" for the canonical rules and the shared resolver
(source/scripts/resolve-active-camp.js).
active field is removed from camps.yaml entries. active field is removed from the data contract (05-DATA_CONTRACT.md). active + archived conflict check is removed from lint-yaml.js
(the conflict is impossible when active no longer exists). build.js resolves the active camp by applying the derivation rules
at build time using the current date. github.js resolves the active camp using the same derivation rules
when handling add-event and edit-event requests. lint-yaml.js no longer checks for the active field or the
active + archived conflict. active field are updated or
removed. A validation and sync tool that enforces camps.yaml as the single source of truth
for camp metadata, ensures referenced camp files exist, and keeps camp headers in
sync.
camps.yaml has all required fields:
id, name, start_date, end_date, opens_for_editing, location, file,
archived. start_date, end_date, and opens_for_editing must be valid YYYY-MM-DD
dates. end_date must be on or after start_date. archived must be a boolean. id values must be unique across all entries. file values must be unique across all entries. file does not exist in source/data/, the validator must
create it automatically. camp: header with id, name, location,
start_date, and end_date — all sourced from camps.yaml. events: [] section. camp: header must be: id, name, location,
start_date, end_date. camps.yaml is the single source of truth for camp metadata. camp: header fields
(id, name, location, start_date, end_date) against camps.yaml. camps.yaml, preserving the events: section unchanged. id, name, location, start_date,
end_date. npm run validate:camps. QA and Production share the same camps.yaml registry and the same git branch.
A dedicated QA camp allows testing the full event flow without polluting
production data: it is invisible to production builds and APIs, and is always
the active camp in QA environments. See 03-architecture/data-layer.md §2 "QA camp
isolation" for the canonical filter mechanism (driven by BUILD_ENV) and the
seasonal QA-camp model. The subsections below capture the operational
requirements that flow from that design.
camps.yaml entries may include an optional qa field of type
boolean. qa is omitted or false, the camp is a normal production
camp. qa is true, the camp is a QA-only camp. 2026-02-testar camp must be renamed to
id: qa-testcamp. file must be renamed to qa-testcamp.yaml. start_date: 2026-01-01, end_date: 2026-12-31) so that events
submitted on any day of the year pass date validation. opens_for_editing must be set to the start of the year so the
form is always open. qa: true. 2026-02-testar.yaml to qa-testcamp.yaml, with the camp header
updated to match. BUILD_ENV=production), the build must exclude all
camps with qa: true before resolving the active
camp. qa: true before
resolving the active camp for add-event and edit-event
requests. build.js must filter qa: true camps from the
camps array before passing it to any rendering function, so that
all downstream output is QA-free without per-renderer
checks. BUILD_ENV=qa), if a camp with qa: true exists and its
dates cover today, it must win the active camp resolution regardless
of other camps’ dates. BUILD_ENV environment variable to determine
the environment (qa or production). deploy-reusable.yml must pass the environment name as BUILD_ENV
to the build step. app.js) must read BUILD_ENV from its environment to
apply the correct filtering. .env.example must document the BUILD_ENV variable. BUILD_ENV is not set (local development), no filtering is
applied — all camps are included, and normal derivation rules
apply. resolveActiveCamp() must accept an optional environment
parameter (e.g. 'qa', 'production'). environment is 'production', camps with qa: true are
filtered out before resolution. environment is 'qa', QA camps that are on dates take
priority over non-QA camps. environment is not set, the function behaves as it does
today (no filtering, no QA priority). lint-yaml.js must accept qa as a valid optional boolean field
in camps.yaml entries. validate-camps.js) must accept qa as
a valid optional boolean field. camps.yaml — no automation
is required. The active QA camp must close before the upcoming real camp’s pre-camp
preparation period and reopen after the real-camp season ends, so QA
testing never overlaps the real-camp window. See
03-architecture/data-layer.md §2 "QA camp isolation" for the spring/autumn camp
descriptions and the off-season window.
camps.yaml at any time: one “spring” camp
active in the period leading up to the next real camp, and one “autumn”
camp active after the real-camp season. end_date is two weeks before the
start_date of the next non-QA, non-archived camp. end_date (exclusive) until the autumn QA camp’s start_date (exclusive),
so no QA camp is active in QA during the real-camp season. start_date is YYYY-10-01 and its end_date
is YYYY-12-31, where YYYY is the current calendar year.