diff --git a/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/PoolAccess-l10nBundles.ftl.js b/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/PoolAccess-l10nBundles.ftl.js
new file mode 100644
index 000000000..96928ef9f
--- /dev/null
+++ b/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/PoolAccess-l10nBundles.ftl.js
@@ -0,0 +1,31 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+"use strict";
+
+const l10nBundles = new Map();
+let entries = null;
+// <#list supportedLanguages() as l>
+entries = new Map();
+l10nBundles.set("${l.locale.toLanguageTag()}", entries);
+// <#list l.l10nBundle.keys as key>
+entries.set("${key}", "${l.l10nBundle.getString(key)}");
+// #list>
+// #list>
+
+export default l10nBundles;
diff --git a/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/PoolAccess-preview.ftl.html b/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/PoolAccess-preview.ftl.html
new file mode 100644
index 000000000..f34328c0f
--- /dev/null
+++ b/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/PoolAccess-preview.ftl.html
@@ -0,0 +1,7 @@
+
+
diff --git a/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/computer-in-use.svg b/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/computer-in-use.svg
new file mode 100644
index 000000000..00e4cc0f2
--- /dev/null
+++ b/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/computer-in-use.svg
@@ -0,0 +1,90 @@
+
+
+
+
diff --git a/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/computer-off.svg b/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/computer-off.svg
new file mode 100644
index 000000000..27c11aef7
--- /dev/null
+++ b/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/computer-off.svg
@@ -0,0 +1,74 @@
+
+
+
+
diff --git a/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/computer.svg b/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/computer.svg
new file mode 100644
index 000000000..f7a6b94a9
--- /dev/null
+++ b/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/computer.svg
@@ -0,0 +1,74 @@
+
+
+
+
diff --git a/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/l10n.properties b/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/l10n.properties
new file mode 100644
index 000000000..c63009dd3
--- /dev/null
+++ b/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/l10n.properties
@@ -0,0 +1,3 @@
+conletName = Pool Access
+
+okayLabel = Apply and Close
diff --git a/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/l10n_de.properties b/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/l10n_de.properties
new file mode 100644
index 000000000..b117a31d5
--- /dev/null
+++ b/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/l10n_de.properties
@@ -0,0 +1,9 @@
+conletName = Pool-Zugriff
+
+okayLabel = Anwenden und Schließen
+Select\ pool = Pool auswählen
+
+Start\ VM = Pool starten
+Stop\ VM = Pool anhalten
+Reset\ VM = Pool zurücksetzen
+Open\ console = Konsole anzeigen
diff --git a/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/l10n_en.properties b/org.jdrupes.vmoperator.poolaccess/resources/org/jdrupes/vmoperator/poolaccess/l10n_en.properties
new file mode 100644
index 000000000..e69de29bb
diff --git a/org.jdrupes.vmoperator.poolaccess/rollup.config.mjs b/org.jdrupes.vmoperator.poolaccess/rollup.config.mjs
new file mode 100644
index 000000000..3ad199e41
--- /dev/null
+++ b/org.jdrupes.vmoperator.poolaccess/rollup.config.mjs
@@ -0,0 +1,35 @@
+import typescript from 'rollup-plugin-typescript2';
+import postcss from 'rollup-plugin-postcss';
+
+let packagePath = "org/jdrupes/vmoperator/poolaccess";
+let baseName = "PoolAccess"
+let module = "build/generated/resources/" + packagePath
+ + "/" + baseName + "-functions.js";
+
+let pathsMap = {
+ "aash-plugin": "../../page-resource/aash-vue-components/lib/aash-vue-components.js",
+ "jgconsole": "../../console-base-resource/jgconsole.js",
+ "jgwc": "../../page-resource/jgwc-vue-components/jgwc-components.js",
+ "l10nBundles": "./" + baseName + "-l10nBundles.ftl.js",
+ "vue": "../../page-resource/vue/vue.esm-browser.js"
+}
+
+export default {
+ external: ['aash-plugin', 'jgconsole', 'jgwc', 'l10nBundles', 'vue', 'chartjs'],
+ input: "src/" + packagePath + "/browser/" + baseName + "-functions.ts",
+ output: [
+ {
+ format: "esm",
+ file: module,
+ sourcemap: true,
+ sourcemapPathTransform: (relativeSourcePath, _sourcemapPath) => {
+ return relativeSourcePath.replace(/^([^/]*\/){12}/, "./");
+ },
+ paths: pathsMap
+ }
+ ],
+ plugins: [
+ typescript(),
+ postcss()
+ ]
+};
diff --git a/org.jdrupes.vmoperator.poolaccess/src/org/jdrupes/vmoperator/poolaccess/PoolAccess.java b/org.jdrupes.vmoperator.poolaccess/src/org/jdrupes/vmoperator/poolaccess/PoolAccess.java
new file mode 100644
index 000000000..028ef0b0b
--- /dev/null
+++ b/org.jdrupes.vmoperator.poolaccess/src/org/jdrupes/vmoperator/poolaccess/PoolAccess.java
@@ -0,0 +1,848 @@
+/*
+ * VM-Operator
+ * Copyright (C) 2023,2024 Michael N. Lipp
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package org.jdrupes.vmoperator.poolaccess;
+
+import com.fasterxml.jackson.annotation.JsonGetter;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.google.gson.JsonSyntaxException;
+import freemarker.core.ParseException;
+import freemarker.template.MalformedTemplateNameException;
+import freemarker.template.Template;
+import freemarker.template.TemplateNotFoundException;
+import io.kubernetes.client.util.Strings;
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.time.Duration;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.ResourceBundle;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.stream.Collectors;
+import org.bouncycastle.util.Objects;
+import org.jdrupes.vmoperator.common.K8sObserver;
+import org.jdrupes.vmoperator.common.VmDefinition;
+import org.jdrupes.vmoperator.common.VmDefinition.Permission;
+import org.jdrupes.vmoperator.common.VmPool;
+import org.jdrupes.vmoperator.manager.events.ChannelTracker;
+import org.jdrupes.vmoperator.manager.events.GetDisplayPassword;
+import org.jdrupes.vmoperator.manager.events.ModifyVm;
+import org.jdrupes.vmoperator.manager.events.ResetVm;
+import org.jdrupes.vmoperator.manager.events.VmChannel;
+import org.jdrupes.vmoperator.manager.events.VmDefChanged;
+import org.jdrupes.vmoperator.manager.events.VmPoolChanged;
+import org.jgrapes.core.Channel;
+import org.jgrapes.core.Components;
+import org.jgrapes.core.Event;
+import org.jgrapes.core.Manager;
+import org.jgrapes.core.annotation.Handler;
+import org.jgrapes.http.Session;
+import org.jgrapes.util.events.ConfigurationUpdate;
+import org.jgrapes.util.events.KeyValueStoreQuery;
+import org.jgrapes.util.events.KeyValueStoreUpdate;
+import org.jgrapes.webconsole.base.Conlet.RenderMode;
+import org.jgrapes.webconsole.base.ConletBaseModel;
+import org.jgrapes.webconsole.base.ConsoleConnection;
+import org.jgrapes.webconsole.base.ConsoleRole;
+import org.jgrapes.webconsole.base.ConsoleUser;
+import org.jgrapes.webconsole.base.WebConsoleUtils;
+import org.jgrapes.webconsole.base.events.AddConletRequest;
+import org.jgrapes.webconsole.base.events.AddConletType;
+import org.jgrapes.webconsole.base.events.AddPageResources.ScriptResource;
+import org.jgrapes.webconsole.base.events.ConletDeleted;
+import org.jgrapes.webconsole.base.events.ConsoleConfigured;
+import org.jgrapes.webconsole.base.events.ConsolePrepared;
+import org.jgrapes.webconsole.base.events.ConsoleReady;
+import org.jgrapes.webconsole.base.events.DeleteConlet;
+import org.jgrapes.webconsole.base.events.NotifyConletModel;
+import org.jgrapes.webconsole.base.events.NotifyConletView;
+import org.jgrapes.webconsole.base.events.OpenModalDialog;
+import org.jgrapes.webconsole.base.events.RenderConlet;
+import org.jgrapes.webconsole.base.events.RenderConletRequestBase;
+import org.jgrapes.webconsole.base.events.SetLocale;
+import org.jgrapes.webconsole.base.events.UpdateConletType;
+import org.jgrapes.webconsole.base.freemarker.FreeMarkerConlet;
+
+/**
+ * The Class {@link PoolAccess}. The component supports the following
+ * configuration properties:
+ *
+ * * `displayResource`: a map with the following entries:
+ * - `preferredIpVersion`: `ipv4` or `ipv6` (default: `ipv4`).
+ * Determines the IP addresses uses in the generated
+ * connection file.
+ * * `deleteConnectionFile`: `true` or `false` (default: `true`).
+ * If `true`, the downloaded connection file will be deleted by
+ * the remote viewer when opened.
+ * * `syncPreviewsFor`: a list objects with either property `user` or
+ * `role` and the associated name (default: `[]`).
+ * The remote viewer will synchronize the previews for the specified
+ * users and roles.
+ *
+ */
+@SuppressWarnings({ "PMD.DataflowAnomalyAnalysis", "PMD.ExcessiveImports",
+ "PMD.CouplingBetweenObjects", "PMD.GodClass", "PMD.TooManyMethods",
+ "PMD.CyclomaticComplexity" })
+public class PoolAccess extends FreeMarkerConlet {
+
+ private static final String VM_NAME_PROPERTY = "vmName";
+ private static final String RENDERED
+ = PoolAccess.class.getName() + ".rendered";
+ private static final String PENDING
+ = PoolAccess.class.getName() + ".pending";
+ private static final Set MODES = RenderMode.asSet(
+ RenderMode.Preview, RenderMode.Edit);
+ private static final Set MODES_FOR_GENERATED = RenderMode.asSet(
+ RenderMode.Preview, RenderMode.StickyPreview);
+ private final ChannelTracker channelTracker = new ChannelTracker<>();
+ private static ObjectMapper objectMapper
+ = new ObjectMapper().registerModule(new JavaTimeModule());
+ private Class> preferredIpVersion = Inet4Address.class;
+ private Set syncUsers = Collections.emptySet();
+ private Set syncRoles = Collections.emptySet();
+ private boolean deleteConnectionFile = true;
+ @SuppressWarnings("PMD.UseConcurrentHashMap")
+ private final Map vmPools = new HashMap<>();
+
+ /**
+ * The periodically generated update event.
+ */
+ public static class Update extends Event {
+ }
+
+ /**
+ * Creates a new component with its channel set to the given channel.
+ *
+ * @param componentChannel the channel that the component's handlers listen
+ * on by default and that {@link Manager#fire(Event, Channel...)}
+ * sends the event to
+ */
+ public PoolAccess(Channel componentChannel) {
+ super(componentChannel);
+ }
+
+ /**
+ * Configure the component.
+ *
+ * @param event the event
+ */
+ @SuppressWarnings({ "unchecked", "PMD.AvoidDuplicateLiterals" })
+ @Handler
+ public void onConfigurationUpdate(ConfigurationUpdate event) {
+ event.structured(componentPath()).ifPresent(c -> {
+ try {
+ var dispRes = (Map) c
+ .getOrDefault("displayResource",
+ Collections.emptyMap());
+ switch ((String) dispRes.getOrDefault("preferredIpVersion",
+ "")) {
+ case "ipv6":
+ preferredIpVersion = Inet6Address.class;
+ break;
+ case "ipv4":
+ default:
+ preferredIpVersion = Inet4Address.class;
+ break;
+ }
+
+ // Delete connection file
+ deleteConnectionFile
+ = Optional.ofNullable(c.get("deleteConnectionFile"))
+ .filter(v -> v instanceof String)
+ .map(v -> (String) v)
+ .map(Boolean::parseBoolean).orElse(true);
+
+ // Users or roles for which previews should be synchronized
+ syncUsers = ((List