# ACE — Aggregated Calendar Explorer

A public, embeddable event calendar for iCalendar (`.ics`), jCal, and custom JSON feeds. Configure each page through URL parameters, HTML attributes, or JavaScript, following the approach used by BibTeX Insight Browser.

ACE provides month, week, and day calendars and an upcoming-events list; filters for source, event type, keywords, attendance mode, location, dates, and text; German and English interfaces; light and dark themes; and a configurable accent colour. Event cards can be title-only, compact, or detailed. Each event opens a configurable detail dialog with an individual `.ics` download.

The repository includes a working demonstration with **fictional September 2026 events**. Real source URLs are not configured yet. The demo opens September 2026 deliberately; configured calendars open the current month unless `month` is specified.

## Run locally

```sh
npm ci
npm run dev
```

Open [the calendar](http://127.0.0.1:8080/) or the [live embedding examples](http://127.0.0.1:8080/examples/embedding.html).

The [busy-day examples](http://127.0.0.1:8080/examples/busy-days.html) demonstrate 3 events on 10 September 2026, 5 on 15 September, and 8 on 17 September. They include overlapping sessions, merged sources, and all-day events spanning two days. The same events are also available in the main demo. Days with more than three events show a button for the remaining events, opening the complete list for that day.

The generated `dist/ace.js` and `dist/ace.css` are included. A normal deployment only needs a static web server. Node.js is needed for development and tests, not for running the webpage. After editing `src/`, run `npm run build` and include the updated `dist/` files.

## Configure an individual page

Each widget defines its own feeds. Visitors can browse and filter; there is no visitor source-entry field, upload control, login, or central source catalogue.

### HTML embedding

ACE’s host is `https://ace.tools.rpdm.rwth-aachen.de`. Replace the two feed URLs below with your sources. **The `data-*` attributes configure this calendar; the objects inside `data-sources` configure each source.** Optional attributes can be removed to use their defaults.

This complete example uses a purple calendar with blue/green source indicators. It displays compact cards with 25% larger event text, hides keywords, and opens a day popup when there are more than three events.

```html
<!-- Load these assets once per page, even if you embed several calendars. -->
<link rel="stylesheet" href="https://ace.tools.rpdm.rwth-aachen.de/dist/ace.css">

<!-- Calendar appearance, card content, and controls are configured below. -->
<div class="ace-calendar"
  data-lang="de"
  data-timezone="Europe/Berlin"
  data-theme="auto"
  data-theme-fallback="light"
  data-accent="#7455a3"

  data-view="month"
  data-views="month,week,day,list"
  data-week-numbers="true"

  data-density="compact"
  data-event-font-size="125%"
  data-fields="time,source"
  data-hide="keywords"
  data-max-events-per-day="3"
  data-overflow="popup"

  data-filters="source,type,attendance,venue,dates"
  data-controls="all"
  data-refresh="900"

  data-sources='[
    {
      "id": "research",
      "name": "Research Data Network",
      "url": "https://example.org/calendar.ics",
      "color": "#00549f"
    },
    {
      "id": "campus",
      "name": "Campus Events",
      "url": "/events.json",
      "color": "#247867"
    }
  ]'>
</div>

<script src="https://ace.tools.rpdm.rwth-aachen.de/dist/ace.js" defer></script>
```

Change the following attributes to adapt that example:

| I want to… | Change in the `<div>` |
| --- | --- |
| Change the calendar colour, including its grid lines and weekday header | `data-accent="#00549f"` (blue), `"#247867"` (teal), or any six-digit hex colour. |
| Use English or a dark background | `data-lang="en"` / `data-theme="dark"`. |
| Follow the surrounding page's theme | `data-theme="auto"` and `data-theme-fallback="light"` (or `"dark"` when the page supplies no theme). |
| Start in another view | `data-view="week"`, `"day"`, or `"list"`. Use `data-date="2026-09-17"` to choose a particular date. |
| Offer only month and list buttons | `data-views="month,list"`. |
| Hide week numbers | `data-week-numbers="false"`. |
| Enlarge event text | `data-event-font-size="125%"` for 25% larger text, `"150%"` for 50% larger, or `"100%"` for the original size. |
| Display only titles and source colours | `data-density="title"` and remove `data-fields`. |
| Keep time but hide source names on cards | `data-fields="time"`. Source colours and full popup attribution remain. |
| Add venue and event type to cards | `data-fields="time,source,type,venue"`. |
| Show fuller cards with keywords and a description excerpt | `data-density="detailed"` and remove `data-fields` and `data-hide`. |
| Hide keywords in cards **and** event popups | `data-hide="keywords"`. This example also omits the keyword filter from `data-filters`. |
| Show five cards per day, or all cards | `data-max-events-per-day="5"` or `"0"` (unlimited). |
| Open the day's list instead of a popup | `data-overflow="list"`. |
| Offer only source and type filters | `data-filters="source,type"`; use `"none"` for no sidebar. |
| Keep only calendar navigation and view buttons | `data-controls="navigation,views"`. |
| Disable automatic feed refresh | `data-refresh="0"`. |

Inside **each `data-sources` object**, change `name` for the attribution label, `url` for the feed address, and `color` for that source’s event markers. Keep `id` unique and stable. A source’s `color` does not change the calendar grid; `data-accent` does. Source data remains available in downloads regardless of display settings.

Try the [interactive display examples](examples/display-options.html), including an event text-size slider, an accent-colour picker, and ready-made layouts. The [vocabulary example](examples/vocabulary.html) demonstrates normalization and spelling review across three feed formats.

Relative source URLs resolve against the **embedding page**, not the ACE script host. Multiple widgets on one page have independent sources, themes, languages, filters, dialogs, and timers. URL synchronization is disabled for embedded widgets unless explicitly enabled with `data-url-sync="true"`.

### Embedded language and theme

Set `data-lang="de"` or `data-lang="en"` to choose the interface language. Embedded calendars hide their language and theme switches by default, including when `data-controls="all"`. The standalone page keeps both switches. To offer either switch explicitly, set `data-language-switch="true"` or `data-theme-switch="true"`; use `false` to hide them on a standalone page too.

```html
<div class="ace-calendar"
  data-lang="de"
  data-theme="auto"
  data-theme-fallback="light"
  data-src="https://example.org/calendar.ics">
</div>
```

For direct HTML/JavaScript embeds, `auto` follows the host page in this order:

1. The nearest ancestor declaring `data-theme="light|dark"`, `data-bs-theme="light|dark"`, `data-color-mode="light|dark"`, or a `light`/`dark` class. If an element has multiple markers, attributes take precedence in that order, then classes.
2. The parent element's computed CSS `color-scheme`. A single `light` or `dark` is used directly. With `light dark`, ACE follows the browser's colour preference.
3. `themeFallback`, which defaults to `light`. Set it to `dark` if that suits the page.

Changes to these ancestor attributes/classes/inline styles and to the browser's colour preference update the calendar without reloading feeds or closing event popups. If a site uses a different theme convention, have its theme switch also update a supported marker, for example `document.documentElement.dataset.theme = 'dark'`. See the [live embedding example](examples/embedding.html#host-theme).

Use `data-theme="light"` or `data-theme="dark"` to keep a fixed appearance regardless of the host. If you enable ACE's own theme switch, clicking it selects a fixed theme for that calendar. JavaScript options use `lang`, `theme`, `themeFallback`, `languageSwitch`, and `themeSwitch`.

### JavaScript initialization

Load the CSS and script, then initialize an existing target. Leave off `class="ace-calendar"` when initializing manually to avoid automatic initialization taking precedence.

```html
<div id="project-events"></div>
<script src="https://ace.tools.rpdm.rwth-aachen.de/dist/ace.js"></script>
<script>
  const calendar = ACE.init({
    target: '#project-events',
    lang: 'en',
    theme: 'auto',
    themeFallback: 'light',
    accent: '#00549f',
    eventFontSize: '125%',
    timezone: 'Europe/Berlin',
    sources: [{
      id: 'project',
      name: 'Project events',
      url: '/project-events.ics'
    }]
  });
  // Optional: await calendar.ready; calendar.load(); calendar.destroy();
</script>
```

Calling `ACE.init` again for the same target returns the existing instance. Call `destroy()` before initializing that target with a different configuration.

### Standalone URLs and iframes

For a simple feed, use `src`. Repeat it for multiple feeds:

```text
/?src=https%3A%2F%2Fexample.org%2Fevents.ics&lang=en&view=month
/?src=https%3A%2F%2Fexample.org%2Fa.ics&src=https%3A%2F%2Fexample.org%2Fb.json
```

For names, colours, and mappings, encode the full `sources` array using `URLSearchParams`:

```js
const params = new URLSearchParams({
  sources: JSON.stringify([
    { id: 'research', name: 'Research events', url: 'https://example.org/events.ics', color: '#00549f' }
  ]),
  lang: 'de',
  view: 'list',
  keyword: 'FAIR'
});
const calendarURL = 'https://ace.tools.rpdm.rwth-aachen.de/?' + params;
```

Embed that URL in an iframe with a descriptive title:

```html
<iframe title="Project events"
  src="https://ace.tools.rpdm.rwth-aachen.de/?src=https%3A%2F%2Fexample.org%2Fevents.ics&amp;lang=en&amp;theme=light"
  loading="lazy"
  style="width:100%;height:900px;border:0">
</iframe>
```

Inside an iframe, language and theme switches are hidden by default too. Supply `lang=de|en` and `theme=light|dark` in its URL. Theme detection operates within the calendar's own document; it does not inspect the parent page across an iframe boundary. Browsers restrict cross-origin parent access through the [same-origin policy](https://developer.mozilla.org/en-US/docs/Web/Security/Defenses/Same-origin_policy). For automatic host-theme updates, use the direct HTML/JavaScript embed above; alternatively the host can update the iframe's theme URL when its theme changes, which reloads the calendar.

The standalone page synchronizes filters, month, selected date, view, language, and theme into its URL, which can be copied using the link button. HTML-defined sources remain part of that page’s configuration. Explicit URL settings take precedence when URL synchronization is enabled; otherwise JavaScript options override HTML attributes and defaults.

URLs containing `src` or `sources` automatically hide the page footer linking to sample pages and configuration examples. That footer appears only on the built-in demo; no extra URL option is needed.

## Page settings

For HTML, prefix each setting with `data-`; camelCase options use hyphens (`urlSync` → `data-url-sync`). For URL settings, use the names below except `urlSync`, which is controlled by the page itself.

| Setting | Default | Meaning |
| --- | --- | --- |
| `sources` | `[]` | JSON array of source descriptors. |
| `src` | — | A simple source URL; repeat in URLs to load several. Use `sources` for descriptive names. |
| `lang` | `en` | `de` or `en`. The standalone demo initially follows browser language. |
| `languageSwitch` | `false` for embeds; `true` standalone | Show DE/EN buttons when `controls` includes `header,language`. HTML: `data-language-switch`. |
| `timezone` | `Europe/Berlin` | Display timezone and fallback for floating event times. |
| `theme` | `auto` for embeds; `light` standalone | `auto` follows the host page; `light` or `dark` fixes the appearance. |
| `themeFallback` | `light` | `light` or `dark` when `theme=auto` finds no host theme. HTML: `data-theme-fallback`. |
| `themeSwitch` | `false` for embeds; `true` standalone | Show the theme button when `controls` includes `header,theme`. HTML: `data-theme-switch`. |
| `accent` | `#00549f` | Six-digit hex colour for buttons, the current date, grid lines, and weekday headers. Encode `#` as `%23` in URLs. |
| `view` | `month` | `month`, `week`, `day`, or `list`. |
| `views` | `month,week,day,list` | Views offered by the selector. If the initial view is unavailable, the first listed view opens. |
| `month` | Current month | Initial month, `YYYY-MM`. |
| `date` | Today within the initial month | Selected date, `YYYY-MM-DD`, used by week/day views. An explicit date also chooses the month. |
| `refresh` | `900` | Refresh interval in seconds; `0` disables automatic refresh. Minimum nonzero interval: 60 seconds. |
| `title` | Localized tagline | Page-specific subtitle below the ACE name. |
| `plain` | `false` | Hide header, controls, and filters; apply configured filters to the chosen view. |
| `urlSync` | `false` for embeds | Read and update the host page URL. Enable for at most one widget per page. |
| `proxy` | — | Optional fetch endpoint URL, e.g. `https://ace.tools.rpdm.rwth-aachen.de/api/fetch-calendar`. |
| `vocabulary` | Built-in event types; open keywords | Page-specific preferred labels and aliases. JSON object in HTML/URLs. See below. |
| `vocabularyReport` | `false` | Show a collapsible vocabulary review with source labels and spelling suggestions. HTML: `data-vocabulary-report="true"`. |

The HTML attribute `data-source-name` can name a simple `data-src` feed. For multiple sources, use `sources` and provide names individually.

### Display settings

Lists below accept comma-separated values in HTML and URLs, or arrays in JavaScript. Use `none` or an empty string for an empty list, and `all` for every supported value. Event titles, status markers, source colours, and the download action remain available.

| Setting | Default | Meaning |
| --- | --- | --- |
| `density` | `compact` | `title` for single-line titles, `compact`, or `detailed` for additional fields and description excerpts. |
| `eventFontSize` | `100%` | Scale event titles and supporting text in every view and in day/event popups. Accepts `75%`–`200%` (or a number such as `125`). Invalid values use `100%`. HTML: `data-event-font-size="125%"`. |
| `fields` | Automatic for density/view | Visible card fields: `time,source,type,venue,keywords,description,attendance`. Title always appears. An explicit list overrides the density's field selection. |
| `detailFields` | All fields | Fields in the event popup, using the same names. HTML: `data-detail-fields`. |
| `hide` | None | Remove the listed fields from cards **and** event popups. Takes precedence over `fields` and `detailFields`. |
| `maxEventsPerDay` | `3` | Cards per day in month/week views; `0` or `all` displays every event. HTML: `data-max-events-per-day`. |
| `overflow` | `list` | `popup` opens a complete day over the current calendar; `list` switches to the day’s list. If the list view is disabled, a popup is used. |
| `weekNumbers` | `false` | ISO week numbers in month/week views. HTML: `data-week-numbers`. |
| `filters` | `source,type,attendance,keyword,venue,dates` | Which sidebar controls appear. This changes visibility; configured event filters still apply. |
| `controls` | `all` | Visible controls: `header,language,theme,share,search,views,navigation,refresh,updated`. |

Compact month/week cards show time, title, and source name. Compact list/day cards also show type and venue. Detailed cards add keywords, attendance, and a description excerpt. Title-only cards retain source colours, with full details in the popup unless separately hidden. Use `fields="time"` for title, time, and source colour without source names on cards.

`eventFontSize` scales the text relative to each view's original sizes, preserving the hierarchy between titles, times, and source labels. Cards grow vertically as needed, with the chosen density's title and description line limits. The setting belongs to each calendar independently. For a URL, use `eventFontSize=125` (equivalent to `125%`).

`header` includes the brand and its language/theme/share controls, so those controls require `header` too. Language and theme buttons additionally require `languageSwitch=true` and `themeSwitch=true` respectively; both default to false for embeds, even with `controls=all`. `share` additionally requires URL synchronization. `navigation` governs previous/next/today buttons; `views` governs the view selector. `refresh` hides or shows the manual refresh button without changing automatic refresh. `updated` controls the last-update timestamp. `plain=true` overrides these settings to hide the header, controls, and filters.

Hiding `keywords` does not remove them from search or downloads. Hide the keyword **filter** separately by omitting singular `keyword` from `filters`. “Clear filters” resets visible filter controls and visible search only, preserving filters that the page intentionally keeps hidden.

Examples of equivalent HTML / URL settings:

```html
<!-- Compact view with time and source names, and a day popup. -->
<div class="ace-calendar" data-fields="time,source"
  data-max-events-per-day="3" data-overflow="popup"
  data-sources='[{"url":"/events.ics"}]'></div>

<!-- Only navigation and view controls; no filter sidebar or keywords. -->
<div class="ace-calendar" data-fields="time" data-hide="keywords"
  data-filters="none" data-controls="navigation,views"
  data-sources='[{"url":"/events.ics"}]'></div>
```

```text
/?src=https%3A%2F%2Fexample.org%2Fevents.ics&fields=time,source&maxEventsPerDay=3&overflow=popup
/?src=https%3A%2F%2Fexample.org%2Fevents.ics&view=week&date=2026-09-17&weekNumbers=true
```

### Filters

| Setting | Meaning |
| --- | --- |
| `q` | Text search across titles, descriptions, locations, types, keywords, and source names. |
| `source` | Source ID or exact source name. |
| `type` | Event type. |
| `keyword` | Keyword/category. |
| `attendance` | `online`, `in-person`, `hybrid`, or `unknown`. |
| `venue` | Location text. |
| `from`, `to` | Inclusive display date bounds in `YYYY-MM-DD`. |

Repeat `source`, `type`, `keyword`, `attendance`, or `venue` in a URL to select multiple values. In JavaScript pass arrays. Multiple values within one field use **OR**; different fields use **AND**. Matching is case-insensitive. All text-search words must occur somewhere in the event.

### Controlled vocabulary

ACE normalizes event types and keywords before merging events and building filter choices. Case, repeated spaces, Unicode compatibility variants, and typographic hyphens are normalized. Known aliases resolve to one preferred label. Unlisted keywords use lowercase; an explicit preferred label preserves spellings such as `FAIR data`, `GitHub`, or `R`.

Add this optional attribute to your calendar's `<div>` (or pass the object as `vocabulary` to `ACE.init`):

```html
data-vocabulary='{
  "types": [
    {
      "id": "workshop",
      "prefLabel": "Workshop",
      "altLabel": ["workshops", "Werkstatt"],
      "hiddenLabel": ["wrokshop"],
      "schemaType": "https://schema.org/EducationEvent"
    }
  ],
  "keywords": [
    { "prefLabel": "FAIR data", "altLabel": ["FAIR-data"] },
    {
      "prefLabel": "open science",
      "altLabel": ["open-science"],
      "hiddenLabel": ["open sciance"]
    },
    { "prefLabel": "metadata", "hiddenLabel": ["metdata"] }
  ]
}'
```

| Vocabulary entry | Purpose |
| --- | --- |
| `prefLabel` | The one displayed label. Required. A plain string entry is shorthand, e.g. `"metadata"`. |
| `altLabel` | Array of approved synonyms, translations, or spelling variants. Matching ignores case and spacing. |
| `hiddenLabel` | Array of approved misspellings that resolve to the preferred label. |
| `id` | Stable identifier, defaulting to the lowercase preferred label. A type entry replaces the built-in entry with the same ID; include all aliases you want to keep. |
| `schemaType` | Optional Schema.org class URL for a type. Available as `event.schemaType` alongside `event.typeId`. |

Type entries extend the built-in list. Supplying `keywords` enables a controlled list: unlisted keywords are flagged as unknown. Omitting it keeps keywords open, while still merging case/spacing variants and checking for similar spellings among the loaded labels. Ambiguous aliases assigned to different concepts are rejected during configuration.

Enable `data-vocabulary-report="true"` while reviewing feeds. The report shows source, original term, applied label, representative event, occurrence count, and possible spelling matches. Unknown terms and events stay visible. Suggested matches are **not automatically merged**: add an approved `altLabel` or `hiddenLabel` to make that decision permanent in the page configuration. No external spelling service is called. This is vocabulary-based review, not a general German/English dictionary check. Similarity checks skip labels shorter than four or longer than 80 characters; open keyword matching considers up to 1,000 distinct loaded labels.

The review panel is hidden by default and is public when enabled. The full structured report is also available as `calendar.vocabularyReport` after `await calendar.ready`; it covers the currently parsed events, including the expanded recurrence range. `event.originalTerms` retains the source IDs and original type/keywords for auditing. Canonical terms appear in cards, filters, and `.ics` downloads. Alias filters and text searches also match the corresponding preferred terms.

#### Event-type standards

[RFC 5545 §3.8.1.2](https://www.rfc-editor.org/rfc/rfc5545#section-3.8.1.2) defines `CATEGORIES` as free text; it does not prescribe a closed event-type vocabulary. [jCal (RFC 7265)](https://www.rfc-editor.org/rfc/rfc7265) carries the same iCalendar semantics. ACE therefore maps its finer event labels to [Schema.org Event classes](https://schema.org/Event). These are ACE mappings, not a claim that Workshop or Seminar are Schema.org classes.

| ACE labels | Schema.org mapping |
| --- | --- |
| Workshop, Seminar, Training, Course, Lecture, Education event | [EducationEvent](https://schema.org/EducationEvent) |
| Conference | [ConferenceEvent](https://schema.org/ConferenceEvent), currently in Schema.org’s “new” area |
| Hackathon | [Hackathon](https://schema.org/Hackathon) |
| Exhibition | [ExhibitionEvent](https://schema.org/ExhibitionEvent) |
| Webinar, Meetup, Meeting, Consultation, Discussion, Event | [Event](https://schema.org/Event); these formats alone do not establish a narrower class |

Common English plurals and German labels are included. A broad value such as `https://schema.org/EducationEvent` stays “Education event”; it is not guessed to mean Workshop. For ICS/jCal with no explicit type, a single recognized type in `CATEGORIES` supplies the type. Conflicting type categories are flagged for review. A category used as the type is removed from keyword choices, and downloads include both type and keywords in standard `CATEGORIES` plus the optional `X-EVENT-TYPE` extension for a precise ACE round trip.

Preferred, alternative, and hidden labels follow the terminology of [SKOS](https://www.w3.org/TR/skos-reference/#labels). ACE’s small JSON configuration is not a complete SKOS/RDF importer. Subject keywords remain page-specific because a single subject taxonomy would not fit all source collections.

The month view shows six weeks, starting on Monday. Week view shows seven day columns; simultaneous events are stacked rather than placed on an hourly timeline. Day view lists all matching events for the selected date. Multi-day events appear on each applicable day. Dates are clickable to open a day view (or a popup when day view is disabled). Previous/next navigation moves by one month, week, or day. Week numbers follow ISO week years at December/January boundaries.

The default overflow threshold is three cards per day; change `maxEventsPerDay` and `overflow` to adjust it. Day popups preserve the calendar and filters; opening event details and closing them returns to that day popup. The upcoming list initially covers today through the next 365 days; setting `from` and `to` exposes other periods, including past events still supplied by feeds. ACE does not maintain a server archive.

## Source settings and data formats

### Guidance for data providers

**Organizations publishing event feeds for ACE should use Schema.org event classes where applicable and maintain consistent event-type and keyword vocabularies using the SKOS approach.** These recommendations improve interoperability across providers. Schema.org supplies event classes; SKOS supplies concepts, preferred labels, and alternative labels. They serve complementary purposes.

| Providers should… | What to publish |
| --- | --- |
| Reuse an appropriate event class | Use a [Schema.org Event](https://schema.org/Event) class such as `EducationEvent` for educational events. Keep finer types such as Workshop or Seminar consistent and document their mapping to the broader class. See the [event-type mappings](#event-type-standards), including the status of ConferenceEvent. |
| Use a controlled subject vocabulary | Reuse a suitable published vocabulary when available. For a maintained vocabulary, follow [SKOS](https://www.w3.org/TR/skos-reference/#labels): identify concepts consistently, define a `prefLabel`, list synonyms as `altLabel`, and record known misspellings as `hiddenLabel`. Full SKOS vocabularies should use stable concept URIs and language-tagged labels, with at most one preferred label per language. |
| Publish preferred labels consistently | Use the same spelling and spacing in every record. Prefer lowercase for ordinary keywords such as `open science`, while preserving established names and acronyms such as `GitHub`, `FAIR data`, and `R`. This casing convention is an ACE recommendation, not a requirement imposed by SKOS. |
| Keep type and subject separate | Publish Workshop as the event type and `open science` as a subject keyword. Online, in-person, and hybrid describe attendance mode and belong in a separate field. |
| Share vocabulary definitions with the page administrator | Provide the preferred labels, aliases, concept IDs/URIs, and type-to-class mappings alongside the feed documentation. Agree on aliases before merging terms that may have different meanings. |
| Provide stable event identity and usable dates | Supply a stable event UID, title, start date/time, timezone or UTC offset. Reuse the same UID when updating an event. See the format-specific date and recurrence rules below. |
| Link back to the published entry | Supply the event's page on the feed provider's website as JSON `page_url` or an iCalendar/jCal `LINK;LINKREL=SOURCE;VALUE=URI`. Keep any linked organizer/event website separate in `url`/`event_url` or iCalendar `URL`. See [event page links](#event-page-links). |

For **iCalendar and jCal**, publish types and keywords through standard `CATEGORIES`. ACE recognizes an unambiguous known type there; `X-EVENT-TYPE` can optionally make the finer type explicit. That `X-` property is an ACE-compatible extension, not a standard Schema.org or iCalendar type property. This complete iCalendar example uses preferred labels:

```ical
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Organization//Events//EN
BEGIN:VEVENT
UID:research-workshop-20260917@example.org
DTSTAMP:20260909T100000Z
DTSTART:20260917T080000Z
DTEND:20260917T100000Z
SUMMARY:Research data workshop
LOCATION:Online
URL:https://example.org/events/research-workshop
LINK;LINKREL=SOURCE;VALUE=URI:https://example.org/events/research-workshop
CATEGORIES:Workshop,FAIR data,open science
X-EVENT-TYPE:Workshop
END:VEVENT
END:VCALENDAR
```

For **ACE-compatible JSON**, a simple feed can provide the finer type and preferred keyword strings directly:

```json
[
  {
    "uid": "research-workshop-20260917@example.org",
    "title": "Research data workshop",
    "start": "2026-09-17T08:00:00Z",
    "end": "2026-09-17T10:00:00Z",
    "type": "Workshop",
    "keywords": ["FAIR data", "open science"],
    "attendance": "online",
    "location": "Online",
    "page_url": "https://example.org/events/research-workshop",
    "url": "https://organizer.example/workshop"
  }
]
```

The built-in Workshop vocabulary entry maps this label to `https://schema.org/EducationEvent`. A provider that only knows the broader class can instead supply `"type": "https://schema.org/EducationEvent"`; ACE then displays “Education event” without guessing a finer type. Additional classes and concept URIs require matching entries in the page's vocabulary. To preserve the preferred casing of `FAIR data`, the page administrator should include that label in `vocabulary.keywords`.

**Current integration:** ACE accepts iCalendar, jCal, and mapped JSON feeds. It does not automatically discover provider vocabularies, parse arbitrary Schema.org JSON-LD graphs, or import SKOS/RDF files. The page administrator transfers the agreed labels and mappings into `data-vocabulary`; `schemaType` is a property of that ACE vocabulary configuration, not an automatically imported event field. JSON keyword values must be strings, rather than embedded SKOS/DefinedTerm objects. Providers may use concept URI strings if the administrator configures corresponding vocabulary IDs or aliases.

Existing feeds with free-text labels remain supported. ACE normalizes known variants and reports unknown or similar terms for review. Providers should correct confirmed errors in their feeds; administrators can use aliases while those corrections are being made. The [vocabulary example](examples/vocabulary.html) demonstrates this workflow.

### Source descriptors

| Source property | Meaning |
| --- | --- |
| `id` | Stable, unique source identifier for filtering. Defaults to `source-1`, etc. |
| `name` | Public attribution label. Defaults to the feed hostname. |
| `url` | Public HTTP(S) feed endpoint; required. |
| `color` | Six-digit hex colour for source indicators. |
| `timezone` | Fallback timezone for this source’s floating timestamps. |
| `type` | Default event type for the source, if individual records have none. |
| `attendance` | Default attendance mode when the record has none. |
| `mapping` | Custom JSON field paths, described below. |
| `itemsPath` | Dot-separated path to a nested JSON array, e.g. `data.events`. |
| `allDay` | Set `true` only when this JSON source represents all-day events. |
| `endInclusive` | Set `true` to convert a JSON all-day inclusive end date into an exclusive end. |
| `proxy` | Override the page proxy for this source. |

Feed contents are detected automatically. `.ics` and jCal use [ICAL.js](https://github.com/kewisch/ical.js), based on [iCalendar (RFC 5545)](https://www.rfc-editor.org/rfc/rfc5545) and [jCal (RFC 7265)](https://www.rfc-editor.org/rfc/rfc7265). Custom JSON is a separate input adapter; a JSON array of event objects is **not** jCal.

### Event page links

**“Published by” links to the event's entry on the website providing the feed. “Event page” opens the linked event, organizer, or registration website when supplied.** For example:

```json
{
  "title": "Research workshop",
  "start": "2026-09-17T08:00:00Z",
  "page_url": "https://publisher.example/events/research-workshop/",
  "url": "https://organizer.example/workshop/"
}
```

ACE recognizes `page_url`, `pageUrl`, `source_url`, `sourceUrl`, and `permalink` as source-entry fields, in that order. For another JSON structure, configure `"mapping": { "pageUrl": "links.entry" }`. Relative URLs resolve against the feed URL. The `url`/`event_url` field and `mapping.url` supply the “Event page” button. If a provider uses its `url` field for the source entry itself, configure `"mapping": { "pageUrl": "url" }` explicitly.

For iCalendar/jCal, publish the source entry using the standard `LINK` relationship from [RFC 9253 §8.2](https://www.rfc-editor.org/rfc/rfc9253.html#section-8.2):

```ical
URL:https://organizer.example/workshop/
LINK;LINKREL=SOURCE;VALUE=URI:https://publisher.example/events/workshop/
```

The equivalent jCal property is `["link", {"linkrel":"SOURCE"}, "uri", "https://publisher.example/events/workshop/"]`. A generic `URL` alone does not identify the publisher's entry. Providers may set both fields to the same URL when that page serves both purposes.

Each “Published by” attribution links to that publisher's entry when supplied; otherwise it links to the configured feed. The “Event page” button uses the linked event URL, taking the first available one in configured source order when duplicates merge. With no valid event URL, the button is omitted; the source-entry URL is not substituted. The button works even when the source entry is missing. Individual calendar downloads retain the linked event website in `URL` and carry the available publisher pages as separate `LINK;LINKREL=SOURCE` properties and in their attribution text.

### iCalendar / jCal

Standard event fields include `UID`, `SUMMARY`, `DTSTART`, `DTEND`/`DURATION`, `DESCRIPTION`, `LOCATION`, `URL`, `CATEGORIES`, and `STATUS`. `CATEGORIES` supplies keywords and, when unambiguous, a recognized event type. Optional `X-EVENT-TYPE` and `X-ATTENDANCE-MODE` extensions supply explicit type and attendance information; source defaults are also supported. Online/hybrid locations are recognized conservatively when explicit attendance data is absent.

Recurring events use `RRULE`, `RDATE`, `EXDATE`, and recurrence exceptions, including moved occurrences. Date-only events stay date-only. Their `DTEND` is **exclusive**: an event covering 17 and 18 September ends on 19 September. Timed midnight events are not automatically treated as all-day.

Embedded `VTIMEZONE` definitions are used when supplied. Recognized IANA timezone identifiers also work through the browser’s timezone database. Unzoned dates use the source’s timezone or the page’s `Europe/Berlin` default. Nonexistent local times at a DST transition are reported instead of silently shifted.

To keep unbounded recurrence feeds responsive, expansion is restricted to the displayed range and capped at 10,000 iterations per series / 50,000 per source. Hitting a cap produces a source warning. Feed authors should bound very old or very frequent recurrence rules. Individual downloads contain the chosen occurrence, not an entire recurring series.

### Custom JSON

The shape discussed during planning works directly:

```json
[
  {
    "title": "DKZ.2R workshop",
    "date": "2026-08-28T05:00:00Z",
    "event_category": "Workshop",
    "event_start": "2026-09-11T08:00:00Z",
    "event_end": "2026-09-11T10:00:00Z",
    "event_location": "Aachen",
    "page_url": "https://publisher.example/events/dkz2r-workshop/",
    "event_url": "https://example.org/workshop",
    "tags": ["Research data"],
    "draft": false
  }
]
```

`date` is treated as publication metadata, not the event start. Draft records are omitted. Common aliases are recognized (`summary`, `start`, `end`, `location`, `url`, `type`, `keywords`). For other formats configure mappings:

```json
{
  "id": "project",
  "name": "Project events",
  "url": "https://example.org/api/events",
  "itemsPath": "results",
  "mapping": {
    "uid": "identifier",
    "title": "heading",
    "start": "schedule.begins",
    "end": "schedule.finishes",
    "description": "details",
    "location": "place",
    "url": "link",
    "pageUrl": "links.sourceEntry",
    "type": "kind",
    "keywords": "topics",
    "attendance": "participation.mode",
    "allDay": "whole_day",
    "status": "state",
    "draft": "unpublished"
  }
}
```

Supply ISO dates (`2026-09-11`) or ISO timestamps (`2026-09-11T10:00:00+02:00`, `2026-09-11T08:00:00Z`, or unzoned `2026-09-11T10:00:00`). Omit an unknown end time instead of inventing one. JSON pagination and provider-specific authentication are outside the initial scope: each configured URL should return a complete public event collection.

### Duplicates and attribution

Events are combined using a matching UID and occurrence, a matching event URL and start, or an exact normalized title/start/location combination when a location is present. Titles alone do not merge events. The first configured source provides the primary event details; all matching source labels and keywords are retained. Filtering by any contributing source finds the merged event. Put the preferred source first when feeds disagree. This is deterministic matching, not fuzzy identity detection.

## Refresh and error handling

Sources load on opening the page, with up to four concurrent requests. Automatic refresh runs every 15 minutes while the document is visible and checks for stale data on returning to the tab. No cookies or authorization headers are sent. Each request has a 15-second timeout and a 5 MiB response limit.

One failing source does not hide the other sources. A failed refresh keeps that source’s last successful data in memory and shows a warning. A later successful response replaces it, including removals and cancellations. Malformed individual events are reported while valid events remain available. Reloading the page clears this in-memory fallback.

Descriptions and labels are displayed as text, and event links are restricted to HTTP(S). Personal-calendar downloads include source attribution and are generated locally.

## Static hosting and Docker

Serve `index.html`, `dist/`, `assets/`, `examples/`, and this README on ordinary web hosting, or build the nginx container:

```sh
docker build -t ace-calendar-explorer .
docker run --rm -p 8080:8080 ace-calendar-explorer
```

The configured public host is `https://ace.tools.rpdm.rwth-aachen.de`. The container runs unprivileged on port 8080 and serves browser assets with CORS headers for cross-origin embedding. Route that hostname to the container through the deployment's HTTPS reverse proxy; TLS termination is external to this container. The Dockerfile uses the upstream `stable-alpine` image tag; pin its digest in a deployment that requires an immutable base image.

### Optional feed proxy

Direct browser requests require the source to allow CORS. For sources that do not, nginx offers `/api/fetch-calendar?url=...`. It is disabled for all upstreams by default.

Allow only specific public feed URLs in `deploy/proxy-sources.map`. Each entry maps the **URL-encoded** request value to the exact upstream URL:

```nginx
"https%3A%2F%2Fevents.example.org%2Fcalendar.ics" "https://events.example.org/calendar.ics";
```

Rebuild the image or mount that map at `/etc/nginx/ace-proxy-sources.map`, then reload nginx. Set the relevant source’s `proxy` to `https://ace.tools.rpdm.rwth-aachen.de/api/fetch-calendar`. ACE appends the encoded `url` query parameter itself. Use the final feed URL: redirects are blocked. The proxy verifies TLS, strips incoming request headers and upstream cookies, and caches successful responses for five minutes.

The allowlist controls server-side network access only. Source names, colours, field mappings, filters, and which sources appear on each page remain page-specific. There is no admin application or database. Large upstream transfer limits beyond the browser’s 5 MiB limit should be applied by the deployment’s gateway if needed.

### GitLab CI

The Git origin is `git@gitlab.git.nrw:rpdm/projects-and-services/ace.git`. `.gitlab-ci.yml` installs Git in the Node Alpine validation container, runs unit tests, rebuilds assets, and checks that committed bundles are current. The build job uses `gcr.io/kaniko-project/executor:v1.23.2-debug`, matching the BibTeX browser and DataHub pipelines on the `runner:docker` runner. It authenticates through the GitLab registry variables and publishes the commit tag; the default branch also publishes `latest`. The project’s container registry and matching runner must be available. The pipeline builds an image; deployment to the public host is configured separately.

The RPDM runner rejects RootlessKit’s UID/GID mapping (`newuidmap: write to uid_map failed: Operation not permitted`), so rootless BuildKit cannot start there. Kaniko builds directly in its job container and avoids that startup requirement. The original [Kaniko project is archived](https://github.com/GoogleContainerTools/kaniko); the pinned version is retained for compatibility with these existing RPDM pipelines. Revisit the builder when the shared runner or the other projects migrate to a maintained alternative. Running rootless BuildKit would require the runner to permit its user-namespace operations, as described in [GitLab’s runner requirements](https://docs.gitlab.com/ci/docker/using_buildkit/#buildkit-rootless).

## Development and validation

```sh
npm ci
npm run check
npx playwright install chromium
npm run dev
# In another terminal:
npm run test:browser
npm run test:display
npm run test:vocabulary
npm run test:theme
```

Browser checks use `http://127.0.0.1:8080` by default; override with `ACE_TEST_URL`. Screenshots and downloaded test events are saved to ignored `test-results/`.

Tests cover custom JSON mappings, date semantics, Berlin DST, embedded timezones, recurrence exceptions, source merging/filtering, safe calendar serialization, URL precedence, vocabulary normalization and ambiguity checks, and isolated embedding. Browser checks exercise search, filters and reloadable links, dialogs and downloads, both languages/themes, mobile layout, enlarged event text, vocabulary review and accepted corrections, source failures, and untrusted feed text.

The browser bundle includes ICAL.js under MPL-2.0. Its source remains available through the pinned npm dependency; notices and a copy of the license are shipped with the generated bundle. See `THIRD_PARTY_NOTICES.md`.
