diff --git a/Elternbeirat.Web.Tests/PocketBaseClientTests.cs b/Elternbeirat.Web.Tests/PocketBaseClientTests.cs
index b90033b..f5b1a87 100644
--- a/Elternbeirat.Web.Tests/PocketBaseClientTests.cs
+++ b/Elternbeirat.Web.Tests/PocketBaseClientTests.cs
@@ -7,7 +7,8 @@ namespace Elternbeirat.Web.Tests;
/// with a known seed (see ). Because the data is fixed,
/// the tests assert on exact values, and they need no network to the live instance.
///
-public sealed class PocketBaseClientTests(PocketBaseFixture pocketBase) : IClassFixture
+[Collection(PocketBaseTestGroup.Name)]
+public sealed class PocketBaseClientTests(PocketBaseFixture pocketBase)
{
[Fact]
public async Task Events_load_sorted_by_start()
diff --git a/Elternbeirat.Web.Tests/PocketBaseFixture.cs b/Elternbeirat.Web.Tests/PocketBaseFixture.cs
index 1c333ef..d6b3fe4 100644
--- a/Elternbeirat.Web.Tests/PocketBaseFixture.cs
+++ b/Elternbeirat.Web.Tests/PocketBaseFixture.cs
@@ -49,6 +49,14 @@ public sealed class PocketBaseFixture : IAsyncLifetime
/// Creates a pointed at this container.
public PocketBaseClient CreateClient() => new(_http);
+ ///
+ /// The base URL the container's PocketBase is reachable at. Set once the
+ /// container is up; used to point the web app's client at this instance in the
+ /// route smoke tests.
+ ///
+ public Uri BaseUrl => _http.BaseAddress
+ ?? throw new InvalidOperationException("PocketBase is not started yet.");
+
public async Task InitializeAsync()
{
// Clear any leftover from an earlier run that was aborted before DisposeAsync
diff --git a/Elternbeirat.Web.Tests/PocketBaseTestGroup.cs b/Elternbeirat.Web.Tests/PocketBaseTestGroup.cs
new file mode 100644
index 0000000..bccb367
--- /dev/null
+++ b/Elternbeirat.Web.Tests/PocketBaseTestGroup.cs
@@ -0,0 +1,14 @@
+namespace Elternbeirat.Web.Tests;
+
+///
+/// Groups every test that needs the seeded PocketBase container into one xUnit
+/// collection, so the single (one container, one
+/// seed) is shared across those classes instead of started once per class. The
+/// test project runs one PocketBase for the whole run under the fixed project
+/// name eb-test-stack; sharing avoids two classes fighting over it.
+///
+[CollectionDefinition(Name)]
+public sealed class PocketBaseTestGroup : ICollectionFixture
+{
+ public const string Name = "PocketBase";
+}
diff --git a/Elternbeirat.Web.Tests/RouteSmokeTests.cs b/Elternbeirat.Web.Tests/RouteSmokeTests.cs
index 919306d..f69f239 100644
--- a/Elternbeirat.Web.Tests/RouteSmokeTests.cs
+++ b/Elternbeirat.Web.Tests/RouteSmokeTests.cs
@@ -1,4 +1,5 @@
using System.Net;
+using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
namespace Elternbeirat.Web.Tests;
@@ -7,19 +8,40 @@ namespace Elternbeirat.Web.Tests;
/// Smoke tests: every known route must return 200, unknown routes must return
/// 404. This catches broken content files, renamed slugs or routing regressions
/// before a deploy.
+///
+/// The post routes now read from PocketBase, so the tests point the web app at the
+/// seeded container from (shared via the
+/// collection) and assert against its known slugs. Requires Docker, like the
+/// client tests.
+///
///
-public sealed class RouteSmokeTests : IClassFixture>
+[Collection(PocketBaseTestGroup.Name)]
+public sealed class RouteSmokeTests : IDisposable
{
+ // WithWebHostBuilder returns a new factory that wraps the base one; both are
+ // disposable, so both are held and disposed to avoid leaking either.
+ private readonly WebApplicationFactory _baseFactory = new();
private readonly WebApplicationFactory _factory;
- public RouteSmokeTests(WebApplicationFactory factory)
+ public RouteSmokeTests(PocketBaseFixture pocketBase)
{
- _factory = factory;
+ // Point the app's PocketBaseClient at the seeded test container instead of
+ // the value from appsettings. UseSetting (not ConfigureAppConfiguration)
+ // because it wins over appsettings.Development.json, which otherwise pins the
+ // client to localhost:8090 while the test container uses a random port.
+ _factory = _baseFactory.WithWebHostBuilder(builder =>
+ builder.UseSetting("PocketBase:BaseUrl", pocketBase.BaseUrl.ToString()));
+ }
+
+ public void Dispose()
+ {
+ _factory.Dispose();
+ _baseFactory.Dispose();
}
///
/// Every route that a visitor can reach through the navigation, the FAQ hub
- /// or the news section.
+ /// or the news section. The post slugs match the fixture's seed.
///
public static TheoryData KnownRoutes =>
[
@@ -35,11 +57,11 @@ public sealed class RouteSmokeTests : IClassFixtureStart
VorstandsteamFörderverein
- Beiträge
- Termine
+ Beiträge
+ TermineFAQDownloadsKontakt
diff --git a/Elternbeirat.Web/Elternbeirat.Web.csproj b/Elternbeirat.Web/Elternbeirat.Web.csproj
index f05d606..70c00f9 100644
--- a/Elternbeirat.Web/Elternbeirat.Web.csproj
+++ b/Elternbeirat.Web/Elternbeirat.Web.csproj
@@ -22,6 +22,12 @@
+
+
+
+
+
diff --git a/Elternbeirat.Web/Features/Events/Event.cs b/Elternbeirat.Web/Features/Events/Event.cs
deleted file mode 100644
index fe94c93..0000000
--- a/Elternbeirat.Web/Features/Events/Event.cs
+++ /dev/null
@@ -1,33 +0,0 @@
-namespace Elternbeirat.Web.Features.Events;
-
-///
-/// A single calendar entry (parents' evening, meeting, deadline). Read from
-/// Content/events.yml at startup. All display text is German because it
-/// is visitor-facing content.
-///
-public sealed class Event
-{
- /// Short title shown in the list and the calendar.
- public required string Title { get; init; }
-
- ///
- /// When the entry starts. A date-only entry (no time) is treated as an
- /// all-day event; see .
- ///
- public required DateTime Start { get; init; }
-
- ///
- /// True when carries a wall-clock time, false for an
- /// all-day entry. Drives both the list formatting and the ICS export.
- ///
- public required bool HasTime { get; init; }
-
- /// Optional end, for multi-hour or multi-day entries.
- public DateTime? End { get; init; }
-
- /// Optional location, e.g. "Aula" or "Mensa".
- public string? Location { get; init; }
-
- /// Optional free-text note shown below the entry.
- public string? Note { get; init; }
-}
diff --git a/Elternbeirat.Web/Features/Events/EventList.razor b/Elternbeirat.Web/Features/Events/EventList.razor
index 5d71ca1..def16d1 100644
--- a/Elternbeirat.Web/Features/Events/EventList.razor
+++ b/Elternbeirat.Web/Features/Events/EventList.razor
@@ -1,5 +1,4 @@
-@page "/termine"
-@inject EventService EventService
+@page "/events"
Termine
@@ -7,20 +6,20 @@
Alle Termine des Elternbeirats auf einen Blick. Sie können den Kalender auch
- abonnieren und in Ihrer Kalender-App automatisch
+ abonnieren und in Ihrer Kalender-App automatisch
aktuell halten.
- @foreach (var ev in EventService.Past)
+ @foreach (var ev in _past)
{
diff --git a/Elternbeirat.Web/Features/Events/EventList.razor.cs b/Elternbeirat.Web/Features/Events/EventList.razor.cs
index a17a7bd..e4d066c 100644
--- a/Elternbeirat.Web/Features/Events/EventList.razor.cs
+++ b/Elternbeirat.Web/Features/Events/EventList.razor.cs
@@ -1,5 +1,7 @@
using System.Globalization;
-using Elternbeirat.Web.Services;
+using Elternbeirat.Contracts;
+using Elternbeirat.PocketBase;
+using Microsoft.AspNetCore.Components;
namespace Elternbeirat.Web.Features.Events;
@@ -7,11 +9,55 @@ public partial class EventList
{
private static readonly CultureInfo German = CultureInfo.GetCultureInfo("de-DE");
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Could not load events from PocketBase.")]
+ private static partial void LogLoadFailed(ILogger logger, Exception exception);
+
+ [Inject]
+ private PocketBaseClient PocketBase { get; set; } = default!;
+
+ [Inject]
+ private ILogger Logger { get; set; } = default!;
+
+ private IReadOnlyList _upcoming = [];
+ private IReadOnlyList _past = [];
+
+ protected override async Task OnInitializedAsync()
+ {
+ IReadOnlyList events;
+ try
+ {
+ events = await PocketBase.GetEventsAsync();
+ }
+ catch (HttpRequestException ex)
+ {
+ // Source unreachable -> show empty sections instead of a 500. The page
+ // stays up; the "keine Termine" note is rendered.
+ LogLoadFailed(Logger, ex);
+ return;
+ }
+
+ // Split into upcoming and past by date, so an event earlier today still
+ // counts as upcoming. Upcoming earliest first, past most recent first.
+ var today = DateOnly.FromDateTime(DateTime.Today);
+ _upcoming = events
+ .Where(e => DateOnly.FromDateTime(e.Start) >= today)
+ .OrderBy(e => e.Start)
+ .ToList();
+ _past = events
+ .Where(e => DateOnly.FromDateTime(e.Start) < today)
+ .OrderByDescending(e => e.Start)
+ .ToList();
+ }
+
+ // An event counts as all-day when its start carries no wall-clock time
+ // (00:00); PocketBase has no separate flag.
+ private static bool HasTime(Event ev) => ev.Start.TimeOfDay != TimeSpan.Zero;
+
// Formats the entry's date range for display: a single day, a date with a
// time, or a span across days.
private static string Format(Event ev)
{
- var start = ev.HasTime
+ var start = HasTime(ev)
? ev.Start.ToString("dddd, d. MMMM yyyy, HH:mm", German) + " Uhr"
: ev.Start.ToString("dddd, d. MMMM yyyy", German);
@@ -24,12 +70,12 @@ public partial class EventList
// end date.
if (end.Date == ev.Start.Date)
{
- return ev.HasTime
+ return HasTime(ev)
? start + "–" + end.ToString("HH:mm", German) + " Uhr"
: start;
}
- var endText = ev.HasTime
+ var endText = HasTime(ev)
? end.ToString("d. MMMM yyyy, HH:mm", German) + " Uhr"
: end.ToString("d. MMMM yyyy", German);
return start + " – " + endText;
diff --git a/Elternbeirat.Web/Features/Events/EventService.cs b/Elternbeirat.Web/Features/Events/EventService.cs
deleted file mode 100644
index e813ce8..0000000
--- a/Elternbeirat.Web/Features/Events/EventService.cs
+++ /dev/null
@@ -1,138 +0,0 @@
-using System.Globalization;
-using Elternbeirat.Web.Services;
-using YamlDotNet.Serialization;
-using YamlDotNet.Serialization.NamingConventions;
-
-namespace Elternbeirat.Web.Features.Events;
-
-///
-/// Reads the calendar entries from Content/events.yml once at startup and
-/// keeps them in memory. Registered as a singleton because the content lives in
-/// the image and does not change at runtime.
-///
-public sealed class EventService
-{
- private readonly IReadOnlyList _events;
-
- ///
- /// Reads and parses Content/events.yml below the application root.
- /// A missing file yields an empty list rather than an error.
- ///
- public EventService(IWebHostEnvironment environment)
- {
- var path = Path.Combine(environment.ContentRootPath, "Content", "events.yml");
- _events = File.Exists(path)
- ? Parse(File.ReadAllText(path))
- : [];
- }
-
- ///
- /// Entries that start today or later, earliest first — the upcoming section.
- /// "Today" is compared by date, so an entry earlier today still counts as
- /// upcoming.
- ///
- public IReadOnlyList Upcoming =>
- _events
- .Where(e => DateOnly.FromDateTime(e.Start) >= DateOnly.FromDateTime(DateTime.Today))
- .OrderBy(e => e.Start)
- .ToList();
-
- ///
- /// Entries that already passed, most recent first — the past section shown
- /// below the upcoming ones.
- ///
- public IReadOnlyList Past =>
- _events
- .Where(e => DateOnly.FromDateTime(e.Start) < DateOnly.FromDateTime(DateTime.Today))
- .OrderByDescending(e => e.Start)
- .ToList();
-
- /// All entries in chronological order, for the ICS export.
- public IReadOnlyList All =>
- _events.OrderBy(e => e.Start).ToList();
-
- private static List Parse(string yaml)
- {
- // Keys are lowercase single words (title, start, ...), which the camel
- // case convention maps onto the PascalCase properties.
- var deserializer = new DeserializerBuilder()
- .WithNamingConvention(CamelCaseNamingConvention.Instance)
- .IgnoreUnmatchedProperties()
- .Build();
-
- var raw = deserializer.Deserialize?>(yaml) ?? [];
- return raw
- .Where(entry => entry.Title is not null && entry.Start is not null)
- .Select(ToEvent)
- .OfType()
- .ToList();
- }
-
- private static Event? ToEvent(EventEntry entry)
- {
- if (!TryParseWhen(entry.Start!, out var start, out var hasTime))
- {
- // A malformed date drops the entry rather than crashing startup.
- return null;
- }
-
- DateTime? end = null;
- if (entry.End is not null && TryParseWhen(entry.End, out var parsedEnd, out _))
- {
- end = parsedEnd;
- }
-
- return new Event
- {
- Title = entry.Title!,
- Start = start,
- HasTime = hasTime,
- End = end,
- Location = entry.Location,
- Note = entry.Note,
- };
- }
-
- ///
- /// Parses a value that is either a date (2026-03-15) or a date with a
- /// time (2026-03-15 19:30). Sets so the
- /// caller knows whether to treat the entry as all-day.
- ///
- private static bool TryParseWhen(string value, out DateTime when, out bool hasTime)
- {
- var trimmed = value.Trim();
-
- if (DateOnly.TryParseExact(trimmed, "yyyy-MM-dd",
- CultureInfo.InvariantCulture, DateTimeStyles.None, out var dateOnly))
- {
- when = dateOnly.ToDateTime(TimeOnly.MinValue);
- hasTime = false;
- return true;
- }
-
- string[] formats = ["yyyy-MM-dd HH:mm", "yyyy-MM-dd'T'HH:mm"];
- if (DateTime.TryParseExact(trimmed, formats,
- CultureInfo.InvariantCulture, DateTimeStyles.None, out when))
- {
- hasTime = true;
- return true;
- }
-
- when = default;
- hasTime = false;
- return false;
- }
-
- ///
- /// Raw shape of one YAML entry, before validation. String-typed on purpose so
- /// we control date parsing and can distinguish date-only from date-and-time.
- ///
- private sealed class EventEntry
- {
- public string? Title { get; set; }
- public string? Start { get; set; }
- public string? End { get; set; }
- public string? Location { get; set; }
- public string? Note { get; set; }
- }
-}
diff --git a/Elternbeirat.Web/Features/Events/IcsCalendar.cs b/Elternbeirat.Web/Features/Events/IcsCalendar.cs
index e3d3b03..2d85388 100644
--- a/Elternbeirat.Web/Features/Events/IcsCalendar.cs
+++ b/Elternbeirat.Web/Features/Events/IcsCalendar.cs
@@ -1,7 +1,7 @@
using System.Globalization;
using System.Security.Cryptography;
using System.Text;
-using Elternbeirat.Web.Services;
+using Elternbeirat.Contracts;
namespace Elternbeirat.Web.Features.Events;
@@ -45,7 +45,7 @@ public static class IcsCalendar
// between requests, so caches and clients do not see spurious changes.
AppendLine(sb, "DTSTAMP:20000101T000000Z");
- if (ev.HasTime)
+ if (HasTime(ev))
{
AppendLine(sb, $"DTSTART:{Local(ev.Start)}");
if (ev.End is { } end)
@@ -75,6 +75,12 @@ public static class IcsCalendar
AppendLine(sb, "END:VEVENT");
}
+ ///
+ /// An entry counts as all-day when its start has no wall-clock time (00:00).
+ /// PocketBase has no separate flag; the convention is a midnight start.
+ ///
+ private static bool HasTime(Event ev) => ev.Start.TimeOfDay != TimeSpan.Zero;
+
///
/// A UID that stays the same as long as the entry's title and start do, so a
/// re-subscribe updates the event instead of creating a duplicate.
diff --git a/Elternbeirat.Web/Features/Home/Home.razor b/Elternbeirat.Web/Features/Home/Home.razor
index b811c0e..48118f4 100644
--- a/Elternbeirat.Web/Features/Home/Home.razor
+++ b/Elternbeirat.Web/Features/Home/Home.razor
@@ -23,13 +23,13 @@
@foreach (var post in _latest)
{
- @foreach (var post in PostService.All)
+ @foreach (var post in _posts)
{
- @post.Title
+ @post.Title
diff --git a/Elternbeirat.Web/Features/Posts/PostList.razor.cs b/Elternbeirat.Web/Features/Posts/PostList.razor.cs
index f582b7e..38d358b 100644
--- a/Elternbeirat.Web/Features/Posts/PostList.razor.cs
+++ b/Elternbeirat.Web/Features/Posts/PostList.razor.cs
@@ -1,8 +1,36 @@
using System.Globalization;
+using Elternbeirat.Contracts;
+using Elternbeirat.PocketBase;
+using Microsoft.AspNetCore.Components;
namespace Elternbeirat.Web.Features.Posts;
public partial class PostList
{
+ [LoggerMessage(Level = LogLevel.Warning, Message = "Could not load posts from PocketBase.")]
+ private static partial void LogLoadFailed(ILogger logger, Exception exception);
+
private static readonly CultureInfo German = CultureInfo.GetCultureInfo("de-DE");
+
+ [Inject]
+ private PocketBaseClient PocketBase { get; set; } = default!;
+
+ [Inject]
+ private ILogger Logger { get; set; } = default!;
+
+ private IReadOnlyList _posts = [];
+
+ protected override async Task OnInitializedAsync()
+ {
+ try
+ {
+ _posts = await PocketBase.GetPostsAsync();
+ }
+ catch (HttpRequestException ex)
+ {
+ // Source unreachable -> show an empty list instead of a 500. The page
+ // stays up; the "keine Beiträge" note is rendered.
+ LogLoadFailed(Logger, ex);
+ }
+ }
}
diff --git a/Elternbeirat.Web/Program.cs b/Elternbeirat.Web/Program.cs
index ed34eb9..3ea5781 100644
--- a/Elternbeirat.Web/Program.cs
+++ b/Elternbeirat.Web/Program.cs
@@ -1,3 +1,5 @@
+using Elternbeirat.Contracts;
+using Elternbeirat.PocketBase;
using Elternbeirat.Web.Components;
using Elternbeirat.Web.Features.Events;
using Elternbeirat.Web.Services;
@@ -8,10 +10,17 @@ var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorComponents();
+// The typed client reads content from PocketBase over its REST API. The base URL
+// comes from configuration: appsettings for local dev, the PocketBase__BaseUrl
+// env var on the server (see compose.yaml).
+var pocketBaseUrl = builder.Configuration["PocketBase:BaseUrl"]
+ ?? throw new InvalidOperationException("PocketBase:BaseUrl is not configured.");
+builder.Services.AddHttpClient(client =>
+ client.BaseAddress = new Uri(pocketBaseUrl));
+
// Content is read once at startup and cached -> singletons.
builder.Services.AddSingleton();
builder.Services.AddSingleton();
-builder.Services.AddSingleton();
var app = builder.Build();
@@ -43,8 +52,21 @@ app.MapStaticAssets();
app.MapRazorComponents();
// Subscribable calendar feed of all events. A minimal API endpoint rather than
-// a Razor page because it returns text/calendar, not HTML.
-app.MapGet("/termine.ics", (EventService events) =>
- Results.Text(IcsCalendar.Build(events.All), "text/calendar; charset=utf-8"));
+// a Razor page because it returns text/calendar, not HTML. Reads from PocketBase
+// per request; an unreachable source yields an empty calendar, not a 500.
+app.MapGet("/events.ics", async (PocketBaseClient pocketBase) =>
+{
+ IReadOnlyList events;
+ try
+ {
+ events = await pocketBase.GetEventsAsync();
+ }
+ catch (HttpRequestException)
+ {
+ events = [];
+ }
+
+ return Results.Text(IcsCalendar.Build(events), "text/calendar; charset=utf-8");
+});
app.Run();
diff --git a/Elternbeirat.Web/Shared/Markdown.cs b/Elternbeirat.Web/Shared/Markdown.cs
new file mode 100644
index 0000000..cc4309a
--- /dev/null
+++ b/Elternbeirat.Web/Shared/Markdown.cs
@@ -0,0 +1,23 @@
+using Markdig;
+
+namespace Elternbeirat.Web.Shared;
+
+///
+/// Renders Markdown to HTML. Content now comes from PocketBase as raw Markdown
+/// (the body field), so the pages render it at display time instead of
+/// reading pre-rendered HTML from files.
+///
+public static class Markdown
+{
+ private static readonly MarkdownPipeline Pipeline =
+ new MarkdownPipelineBuilder().Build();
+
+ ///
+ /// Converts a Markdown string to HTML. Returns an empty string for
+ /// null or empty input, so callers can bind the result directly.
+ ///
+ public static string ToHtml(string? markdown) =>
+ string.IsNullOrEmpty(markdown)
+ ? ""
+ : Markdig.Markdown.ToHtml(markdown, Pipeline);
+}
diff --git a/Elternbeirat.Web/_Imports.razor b/Elternbeirat.Web/_Imports.razor
index 57b29e7..ac14ee8 100644
--- a/Elternbeirat.Web/_Imports.razor
+++ b/Elternbeirat.Web/_Imports.razor
@@ -8,3 +8,4 @@
@using Microsoft.JSInterop
@using Elternbeirat.Web
@using Elternbeirat.Web.Services
+@using Elternbeirat.Web.Shared
diff --git a/Elternbeirat.Web/appsettings.Development.json b/Elternbeirat.Web/appsettings.Development.json
index 0c208ae..b561eb0 100644
--- a/Elternbeirat.Web/appsettings.Development.json
+++ b/Elternbeirat.Web/appsettings.Development.json
@@ -4,5 +4,8 @@
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
+ },
+ "PocketBase": {
+ "BaseUrl": "http://localhost:8090"
}
}
diff --git a/Elternbeirat.Web/appsettings.json b/Elternbeirat.Web/appsettings.json
index 10f68b8..4ddd54e 100644
--- a/Elternbeirat.Web/appsettings.json
+++ b/Elternbeirat.Web/appsettings.json
@@ -5,5 +5,10 @@
"Microsoft.AspNetCore": "Warning"
}
},
- "AllowedHosts": "*"
+ "AllowedHosts": "*",
+ "PocketBase": {
+ // The PocketBase service name on the compose network. compose.yaml overrides
+ // this via the env var PocketBase__BaseUrl; local dev uses appsettings.Development.json.
+ "BaseUrl": "http://eb-pocketbase:8090"
+ }
}
diff --git a/compose.yaml b/compose.yaml
index f996039..b207665 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -1,6 +1,10 @@
-# Base compose: production-like. Both services, no host port mappings (NPM is the
-# only path to the web app; the editors reach PocketBase through an NPM subdomain).
-# Only PocketBase's data is a volume; everything else lives inside the images.
+# Base compose: production-like. Both services; only PocketBase's data is a volume,
+# everything else lives inside the images.
+#
+# The two `ports:` blocks below are TEMPORARY. The target state is NPM in front:
+# NPM terminates TLS and is the only path to the web app, and the editors reach
+# PocketBase through an NPM subdomain -- then both port mappings go away (#9). While
+# NPM is not set up yet, the ports are the only way onto the site and the admin UI.
#
# For local development use the dev overlay on top, which adds host ports and builds
# the web image from source:
@@ -25,6 +29,11 @@ services:
volumes:
# The only state that must survive a restart or redeploy.
- /mnt/user/appdata/pocketbase/pb_data:/pb_data
+ ports:
+ # TEMPORARY: exposes the admin UI on the LAN (http://:8090/_/) while
+ # NPM is not in front yet. Remove once the editors reach PocketBase through an
+ # NPM subdomain (#9) -- until then the admin has no other way in.
+ - "8090:8090"
healthcheck:
# The image ships this endpoint; the web app waits for it to pass.
test: ["CMD", "wget", "--spider", "-q", "http://localhost:8090/api/health"]
@@ -44,3 +53,8 @@ services:
TZ: Europe/Berlin
# The web app reads content from PocketBase over the compose network.
PocketBase__BaseUrl: http://eb-pocketbase:8090
+ ports:
+ # TEMPORARY: reach the site on the LAN (http://:5000) while NPM is not
+ # in front yet. Replace with the external npm network once NPM terminates TLS
+ # and is the only path in (#9).
+ - "5000:8080"