diff --git a/html/static/js/time-relative.js b/html/static/js/time-relative.js
new file mode 100644
index 00000000..6d68e24c
--- /dev/null
+++ b/html/static/js/time-relative.js
@@ -0,0 +1,92 @@
+/**
+ * Custom HTML element that displays relative time
+ * Usage:
+ */
+
+class TimeRelative extends HTMLElement {
+ constructor() {
+ super();
+ this.span = null;
+ }
+
+ static get observedAttributes() {
+ return ["time"];
+ }
+
+ connectedCallback() {
+ // Create the span element if it doesn't exist
+ if (!this.span) {
+ this.span = document.createElement("span");
+ this.span.className = "time-relative";
+ this.appendChild(this.span);
+ }
+ this.updateTime();
+ }
+
+ attributeChangedCallback(name, oldValue, newValue) {
+ if (name === "time" && oldValue !== newValue) {
+ this.updateTime();
+ }
+ }
+
+ updateTime() {
+ if (this.span) {
+ const timeValue = this.getAttribute("time");
+ if (timeValue) {
+ this.span.textContent = this.formatRelativeTime(timeValue);
+ }
+ }
+ }
+
+ formatRelativeTime(timestamp) {
+ const now = new Date();
+ const date = new Date(timestamp);
+ const diffInSeconds = Math.floor((now - date) / 1000);
+
+ // Time units in seconds
+ const minute = 60;
+ const hour = minute * 60;
+ const day = hour * 24;
+ const week = day * 7;
+ const month = day * 30;
+ const year = day * 365;
+
+ if (diffInSeconds < minute) {
+ return "just now";
+ } else if (diffInSeconds < hour) {
+ const minutes = Math.floor(diffInSeconds / minute);
+ return `${minutes} ${minutes === 1 ? "minute" : "minutes"} ago`;
+ } else if (diffInSeconds < day) {
+ const hours = Math.floor(diffInSeconds / hour);
+ return `${hours} ${hours === 1 ? "hour" : "hours"} ago`;
+ } else if (diffInSeconds < week) {
+ const days = Math.floor(diffInSeconds / day);
+ return `${days} ${days === 1 ? "day" : "days"} ago`;
+ } else if (diffInSeconds < month) {
+ const weeks = Math.floor(diffInSeconds / week);
+ return `${weeks} ${weeks === 1 ? "week" : "weeks"} ago`;
+ } else if (diffInSeconds < year) {
+ const months = Math.floor(diffInSeconds / month);
+ return `${months} ${months === 1 ? "month" : "months"} ago`;
+ } else {
+ const years = Math.floor(diffInSeconds / year);
+ return `${years} ${years === 1 ? "year" : "years"} ago`;
+ }
+ }
+
+ // Property getter and setter for JavaScript access
+ get time() {
+ return this.getAttribute("time");
+ }
+
+ set time(value) {
+ if (value) {
+ this.setAttribute("time", value);
+ } else {
+ this.removeAttribute("time");
+ }
+ }
+}
+
+// Register the custom element
+customElements.define("time-relative", TimeRelative);
diff --git a/html/template/sync/planning-root.html b/html/template/sync/planning-root.html
index 8b1fd502..8940ee5c 100644
--- a/html/template/sync/planning-root.html
+++ b/html/template/sync/planning-root.html
@@ -6,13 +6,14 @@
type="text/javascript"
src="//unpkg.com/maplibre-gl@5.0.1/dist/maplibre-gl.js"
>
-
-
+
+
+