diff --git a/internal/dis/component/server/admin/admin.go b/internal/dis/component/server/admin/admin.go
index 128db04..4f18cf7 100644
--- a/internal/dis/component/server/admin/admin.go
+++ b/internal/dis/component/server/admin/admin.go
@@ -75,6 +75,10 @@ var (
menuGrants = component.DummyMenuItem()
menuPurge = component.DummyMenuItem()
menuSnapshots = component.DummyMenuItem()
+ menuSSH = component.DummyMenuItem()
+ menuStats = component.DummyMenuItem()
+ menuData = component.DummyMenuItem()
+ menuDrupal = component.DummyMenuItem()
)
func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http.Handler, err error) {
@@ -145,6 +149,26 @@ func (admin *Admin) HandleRoute(ctx context.Context, route string) (handler http
router.Handler(http.MethodGet, route+"instance/:slug/snapshots", snapshots)
}
+ {
+ ssh := admin.instanceSSH(ctx)
+ router.Handler(http.MethodGet, route+"instance/:slug/ssh", ssh)
+ }
+
+ {
+ stats := admin.instanceStats(ctx)
+ router.Handler(http.MethodGet, route+"instance/:slug/stats", stats)
+ }
+
+ {
+ data := admin.instanceData(ctx)
+ router.Handler(http.MethodGet, route+"instance/:slug/data", data)
+ }
+
+ {
+ drupal := admin.instanceDrupal(ctx)
+ router.Handler(http.MethodGet, route+"instance/:slug/drupal", drupal)
+ }
+
// add a router for the login page
router.Handler(http.MethodPost, route+"login", admin.loginHandler(ctx))
diff --git a/internal/dis/component/server/admin/html/instance.html b/internal/dis/component/server/admin/html/instance.html
index 1efa196..8251d1f 100644
--- a/internal/dis/component/server/admin/html/instance.html
+++ b/internal/dis/component/server/admin/html/instance.html
@@ -7,8 +7,6 @@
Info & Status
Drupal Status Report
WissKI Data
- Statistics
- SSH Keys
@@ -126,39 +124,6 @@
-
-
-
-
-
-
- |
- Drupal Info
- |
-
-
-
-
- |
- Drupal Version
- |
-
- {{ .Info.DrupalVersion }}
- |
-
-
- |
- Default Theme
- |
-
- {{ .Info.Theme }}
- |
-
-
-
-
-
-
-
-
-
Drupal Status Report
-
-
-
-
-
-
-
-
- |
- ID
- |
-
- Severity
- |
-
- Title
- |
-
- Value
- |
-
- Description
- |
-
-
-
-
- {{ range $name, $req := .Info.Requirements }}
-
-
- {{ $req.ID }}
- |
-
- {{ $req.Level }}
- |
-
- {{ $req.Title }}
- |
-
- {{ $req.Value }}
- |
-
- {{ $req.Description }}
- |
-
- {{ end }}
-
-
-
-
-
-
-
-
WissKI Data
-
-
-
-
-
-
-
-
- |
- Pathbuilders
- |
-
-
-
-
- {{ range $name, $xml := .Info.Pathbuilders }}
-
-
- {{ $name }}
- |
-
- {{ $xml }}
- |
-
- {{ end }}
-
-
-
-
-
-
-
-
-
-
-
- |
- URI Prefixes
-
- {{ if .Info.NoPrefixes }}
- (excluded from resolver)
- {{ end }}
- |
-
-
-
- {{ range $index, $prefix := .Info.Prefixes }}
-
-
- {{ $prefix }}
- |
-
- {{ end }}
-
-
-
-
-
-
-
-
Statistics
-
-
-
-
-
-
-
-
- |
- Bundles
- |
-
-
- |
- Label
- |
-
- Machine Name
- |
-
- Count
- |
-
- LastEdit
- |
-
- MainBundle
- |
-
-
-
- {{ range .Info.Statistics.Bundles.Bundles }}
-
-
- {{ .Label }}
- |
-
- {{ .MachineName }}
- |
-
- {{ .Count }}
- |
-
- {{ .LastEdit.Time.Format "2006-01-02T15:04:05Z07:00" }}
- |
-
- {{ .MainBundle }}
- |
-
- {{ end }}
-
-
-
-
-
-
-
-
-
-
-
-
-
- |
- Triplestore
- |
-
-
- |
- URI
- |
-
- Count
- |
-
-
-
- {{ range .Info.Statistics.Triplestore.Graphs }}
-
-
- {{ .URI }}
- |
-
- {{ .Count }}
- |
-
- {{ end }}
-
-
-
-
-
-
-
-
SSH Keys
-
-
- {{ range .Info.SSHKeys }}
-
-
- {{ . }}
- |
-
- {{ end }}
-
-
-
diff --git a/internal/dis/component/server/admin/html/instance_data.html b/internal/dis/component/server/admin/html/instance_data.html
new file mode 100644
index 0000000..28b8bf9
--- /dev/null
+++ b/internal/dis/component/server/admin/html/instance_data.html
@@ -0,0 +1,61 @@
+
+
+
WissKI Data
+
+
+
+
+
+
+
+
+ |
+ Pathbuilders
+ |
+
+
+
+
+ {{ range $name, $xml := .Pathbuilders }}
+
+
+ {{ $name }}
+ |
+
+ {{ $xml }}
+ |
+
+ {{ end }}
+
+
+
+
+
+
+
+
+
+
+
+ |
+ URI Prefixes
+
+ {{ if .NoPrefixes }}
+ (excluded from resolver)
+ {{ end }}
+ |
+
+
+
+ {{ range $index, $prefix := .Prefixes }}
+
+
+ {{ $prefix }}
+ |
+
+ {{ end }}
+
+
+
+
+
diff --git a/internal/dis/component/server/admin/html/instance_drupal.html b/internal/dis/component/server/admin/html/instance_drupal.html
new file mode 100644
index 0000000..1f255aa
--- /dev/null
+++ b/internal/dis/component/server/admin/html/instance_drupal.html
@@ -0,0 +1,94 @@
+
+
Drupal Information
+
+
+
+
+
+
+
+
+ |
+ Drupal Info
+ |
+
+
+
+
+ |
+ Drupal Version
+ |
+
+ {{ .DrupalVersion }}
+ |
+
+
+ |
+ Default Theme
+ |
+
+ {{ .DefaultTheme }}
+ |
+
+
+
+
+
+
+
+
+
Status Report
+
+ This mirrors the Status Report page found inside drupal.
+
+
+
+
+
+
+
+
+
+ |
+ ID
+ |
+
+ Severity
+ |
+
+ Title
+ |
+
+ Value
+ |
+
+ Description
+ |
+
+
+
+
+ {{ range $name, $req := .Requirements }}
+
+
+ {{ $req.ID }}
+ |
+
+ {{ $req.Level }}
+ |
+
+ {{ $req.Title }}
+ |
+
+ {{ $req.Value }}
+ |
+
+ {{ $req.Description }}
+ |
+
+ {{ end }}
+
+
+
+
+
diff --git a/internal/dis/component/server/admin/html/instance_ssh.html b/internal/dis/component/server/admin/html/instance_ssh.html
new file mode 100644
index 0000000..6e466a2
--- /dev/null
+++ b/internal/dis/component/server/admin/html/instance_ssh.html
@@ -0,0 +1,38 @@
+
+
SSH Access
+
+ Every WissKI Instance comes with access via SSH.
+ Every distillery user that has administrative access to this instance can access it.
+ Furthermore, every distillery administrator has access.
+
+
+ To access the ssh server of this instance, use:
+
+
+ ssh -J {{ .PanelDomain }}:{{ .Port }} www-data@{{ .Hostname }}
+
+
+ For this to work, configure an ssh key on your Personal SSH Page.
+
+
+
+
+
SSH Keys
+
+ This page lists all SSH Keys that have access to this system.
+
+
+
+
+
+
+ {{ range .SSHKeys }}
+
+
+ {{ . }}
+ |
+
+ {{ end }}
+
+
+
\ No newline at end of file
diff --git a/internal/dis/component/server/admin/html/instance_stats.html b/internal/dis/component/server/admin/html/instance_stats.html
new file mode 100644
index 0000000..a0a966a
--- /dev/null
+++ b/internal/dis/component/server/admin/html/instance_stats.html
@@ -0,0 +1,98 @@
+
+
+ This page contains statistics generated by the WissKI Statistics module.
+ If the module is not available, this page may be empty.
+
+
+
+
+
+
+
+
+
+
+ |
+ Bundles
+ |
+
+
+ |
+ Label
+ |
+
+ Machine Name
+ |
+
+ Count
+ |
+
+ LastEdit
+ |
+
+ MainBundle
+ |
+
+
+
+ {{ range .Statistics.Bundles.Bundles }}
+
+
+ {{ .Label }}
+ |
+
+ {{ .MachineName }}
+ |
+
+ {{ .Count }}
+ |
+
+ {{ .LastEdit.Time.Format "2006-01-02T15:04:05Z07:00" }}
+ |
+
+ {{ .MainBundle }}
+ |
+
+ {{ end }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ |
+ Triplestore
+ |
+
+
+ |
+ URI
+ |
+
+ Count
+ |
+
+
+
+ {{ range .Statistics.Triplestore.Graphs }}
+
+
+ {{ .URI }}
+ |
+
+ {{ .Count }}
+ |
+
+ {{ end }}
+
+
+
+
+
diff --git a/internal/dis/component/server/admin/instance.go b/internal/dis/component/server/admin/instance.go
index aaea17c..a739a33 100644
--- a/internal/dis/component/server/admin/instance.go
+++ b/internal/dis/component/server/admin/instance.go
@@ -79,19 +79,12 @@ func (admin *Admin) instanceTabs(slug string, active string) templating.FlagFunc
{Title: "Overview", Path: template.URL("/admin/instance/" + slug), Active: active == "overview"},
{Title: "Rebuild", Path: template.URL("/admin/instance/" + slug + "/rebuild"), Active: active == "rebuild"},
{Title: "Users & Grants", Path: template.URL("/admin/instance/" + slug + "/users"), Active: active == "users"},
+ {Title: "Drupal Status", Path: template.URL("/admin/instance/" + slug + "/drupal"), Active: active == "drupal"},
+ {Title: "WissKI Data", Path: template.URL("/admin/instance/" + slug + "/data"), Active: active == "data"},
+ {Title: "WissKI Stats", Path: template.URL("/admin/instance/" + slug + "/stats"), Active: active == "stats"},
+ {Title: "SSH", Path: template.URL("/admin/instance/" + slug + "/ssh"), Active: active == "ssh"},
{Title: "Snapshots", Path: template.URL("/admin/instance/" + slug + "/snapshots"), Active: active == "snapshots"},
{Title: "Purge", Path: template.URL("/admin/instance/" + slug + "/purge"), Active: active == "purge"},
-
- // TODO: These still need to be migrated to their own tabs
- // Then we also need to redo the main page
- /*
- {Title: "Status", Path: template.URL("/instance/" + slug + "/status"), Active: active == "status"},
- {Title: "Database", Path: template.URL("/instance/" + slug + "/database"), Active: active == "database"},
- {Title: "Drupal", Path: template.URL("/instance/" + slug + "/drupal"), Active: active == "drupal"},
- {Title: "Users & Grants", Path: template.URL("/instance/" + slug + "/users"), Active: active == "users"},
- {Title: "Stats", Path: template.URL("/instance/" + slug + "/stats"), Active: active == "stats"},
- {Title: "SSH", Path: template.URL("/instance/" + slug + "/ssh"), Active: active == "ssh"},
- */
}
return flags
}
diff --git a/internal/dis/component/server/admin/instance_data.go b/internal/dis/component/server/admin/instance_data.go
new file mode 100644
index 0000000..adf83f0
--- /dev/null
+++ b/internal/dis/component/server/admin/instance_data.go
@@ -0,0 +1,77 @@
+package admin
+
+import (
+ "context"
+ _ "embed"
+ "html/template"
+ "net/http"
+
+ "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
+ "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
+ "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
+ "github.com/FAU-CDI/wisski-distillery/internal/wisski"
+ "github.com/tkw1536/pkglib/httpx"
+
+ "github.com/julienschmidt/httprouter"
+)
+
+//go:embed "html/instance_data.html"
+var instanceDataHTML []byte
+var instanceDataTemplate = templating.Parse[instanceDataContext](
+ "instance_data.html", instanceDataHTML, nil,
+
+ templating.Assets(assets.AssetsAdmin),
+)
+
+type instanceDataContext struct {
+ templating.RuntimeFlags
+
+ Instance *wisski.WissKI
+ Pathbuilders map[string]string
+ NoPrefixes bool
+ Prefixes []string
+}
+
+func (admin *Admin) instanceData(ctx context.Context) http.Handler {
+ tpl := instanceDataTemplate.Prepare(
+ admin.dependencies.Templating,
+ templating.Crumbs(
+ menuAdmin,
+ menuInstances,
+ menuInstance,
+ menuData,
+ ),
+ )
+
+ return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ctx instanceDataContext, funcs []templating.FlagFunc, err error) {
+ slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
+
+ // setup the context with just the instance
+ ctx.Instance, err = admin.dependencies.Instances.WissKI(r.Context(), slug)
+ if err != nil {
+ return ctx, nil, httpx.ErrNotFound
+ }
+
+ server := ctx.Instance.PHP().NewServer()
+ defer server.Close()
+
+ ctx.Pathbuilders, err = ctx.Instance.Pathbuilder().GetAll(r.Context(), server)
+ if err != nil {
+ return ctx, nil, httpx.ErrInternalServerError
+ }
+
+ prefixes := ctx.Instance.Prefixes()
+ ctx.NoPrefixes = prefixes.NoPrefix()
+ ctx.Prefixes, err = prefixes.All(r.Context(), server)
+ if err != nil {
+ return ctx, nil, httpx.ErrInternalServerError
+ }
+
+ return ctx, []templating.FlagFunc{
+ templating.ReplaceCrumb(menuInstance, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + ctx.Instance.Slug)}),
+ templating.ReplaceCrumb(menuData, component.MenuItem{Title: "SSH", Path: template.URL("/admin/instance/" + ctx.Instance.Slug + "/data")}),
+ templating.Title(ctx.Instance.Slug + " - Data"),
+ admin.instanceTabs(slug, "data"),
+ }, nil
+ })
+}
diff --git a/internal/dis/component/server/admin/instance_drupal.go b/internal/dis/component/server/admin/instance_drupal.go
new file mode 100644
index 0000000..41d7904
--- /dev/null
+++ b/internal/dis/component/server/admin/instance_drupal.go
@@ -0,0 +1,86 @@
+package admin
+
+import (
+ "context"
+ _ "embed"
+ "html/template"
+ "net/http"
+
+ "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
+ "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
+ "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
+ "github.com/FAU-CDI/wisski-distillery/internal/status"
+ "github.com/FAU-CDI/wisski-distillery/internal/wisski"
+ "github.com/tkw1536/pkglib/httpx"
+
+ "github.com/julienschmidt/httprouter"
+)
+
+//go:embed "html/instance_drupal.html"
+var instanceDrupalHTML []byte
+var instanceDrupalTemplate = templating.Parse[instanceDrupalContext](
+ "instance_drupal.html", instanceDrupalHTML, nil,
+
+ templating.Assets(assets.AssetsAdmin),
+)
+
+type instanceDrupalContext struct {
+ templating.RuntimeFlags
+
+ Instance *wisski.WissKI
+
+ DrupalVersion string
+ DefaultTheme string
+
+ Requirements []status.Requirement
+}
+
+func (admin *Admin) instanceDrupal(ctx context.Context) http.Handler {
+ tpl := instanceDrupalTemplate.Prepare(
+ admin.dependencies.Templating,
+ templating.Crumbs(
+ menuAdmin,
+ menuInstances,
+ menuInstance,
+ menuDrupal,
+ ),
+ )
+
+ return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ctx instanceDrupalContext, funcs []templating.FlagFunc, err error) {
+ slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
+
+ // setup the context with just the instance
+ ctx.Instance, err = admin.dependencies.Instances.WissKI(r.Context(), slug)
+ if err != nil {
+ return ctx, nil, httpx.ErrNotFound
+ }
+
+ server := ctx.Instance.PHP().NewServer()
+ defer server.Close()
+
+ // get the requirements
+ ctx.Requirements, err = ctx.Instance.Requirements().Get(r.Context(), server)
+ if err != nil {
+ return ctx, nil, httpx.ErrInternalServerError
+ }
+
+ // get the drupal version
+ ctx.DrupalVersion, err = ctx.Instance.Version().Get(r.Context(), server)
+ if err != nil {
+ return ctx, nil, httpx.ErrInternalServerError
+ }
+
+ // get the default theme
+ ctx.DefaultTheme, err = ctx.Instance.Theme().Get(r.Context(), server)
+ if err != nil {
+ return ctx, nil, httpx.ErrInternalServerError
+ }
+
+ return ctx, []templating.FlagFunc{
+ templating.ReplaceCrumb(menuInstance, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + ctx.Instance.Slug)}),
+ templating.ReplaceCrumb(menuDrupal, component.MenuItem{Title: "Drupal Status", Path: template.URL("/admin/instance/" + ctx.Instance.Slug + "/drupal")}),
+ templating.Title(ctx.Instance.Slug + " - Drupal Status"),
+ admin.instanceTabs(slug, "drupal"),
+ }, nil
+ })
+}
diff --git a/internal/dis/component/server/admin/instance_ssh.go b/internal/dis/component/server/admin/instance_ssh.go
new file mode 100644
index 0000000..2fb691d
--- /dev/null
+++ b/internal/dis/component/server/admin/instance_ssh.go
@@ -0,0 +1,80 @@
+package admin
+
+import (
+ "context"
+ _ "embed"
+ "html/template"
+ "net/http"
+
+ "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
+ "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
+ "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
+ "github.com/FAU-CDI/wisski-distillery/internal/wisski"
+ "github.com/tkw1536/pkglib/httpx"
+
+ "github.com/julienschmidt/httprouter"
+
+ gossh "golang.org/x/crypto/ssh"
+)
+
+//go:embed "html/instance_ssh.html"
+var instanceSSHHTML []byte
+var instanceSSHTemplate = templating.Parse[instanceSSHContext](
+ "instance_ssh.html", instanceSSHHTML, nil,
+
+ templating.Assets(assets.AssetsAdmin),
+)
+
+type instanceSSHContext struct {
+ templating.RuntimeFlags
+
+ Instance *wisski.WissKI
+ SSHKeys []string
+
+ Hostname string
+ PanelDomain string
+ Port uint16
+}
+
+func (admin *Admin) instanceSSH(ctx context.Context) http.Handler {
+ tpl := instanceSSHTemplate.Prepare(
+ admin.dependencies.Templating,
+ templating.Crumbs(
+ menuAdmin,
+ menuInstances,
+ menuInstance,
+ menuSSH,
+ ),
+ )
+
+ return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ctx instanceSSHContext, funcs []templating.FlagFunc, err error) {
+ slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
+
+ // setup the context with just the instance
+ ctx.Instance, err = admin.dependencies.Instances.WissKI(r.Context(), slug)
+ if err != nil {
+ return ctx, nil, httpx.ErrNotFound
+ }
+
+ ctx.Hostname = ctx.Instance.Domain()
+ ctx.PanelDomain = admin.Config.HTTP.PanelDomain()
+ ctx.Port = admin.Config.Listen.SSHPort
+
+ keys, err := ctx.Instance.SSH().Keys(r.Context())
+ if err != nil {
+ return ctx, nil, httpx.ErrInternalServerError
+ }
+
+ ctx.SSHKeys = make([]string, len(keys))
+ for i, key := range keys {
+ ctx.SSHKeys[i] = string(gossh.MarshalAuthorizedKey(key))
+ }
+
+ return ctx, []templating.FlagFunc{
+ templating.ReplaceCrumb(menuInstance, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + ctx.Instance.Slug)}),
+ templating.ReplaceCrumb(menuSSH, component.MenuItem{Title: "SSH", Path: template.URL("/admin/instance/" + ctx.Instance.Slug + "/ssh")}),
+ templating.Title(ctx.Instance.Slug + " - SSH"),
+ admin.instanceTabs(slug, "ssh"),
+ }, nil
+ })
+}
diff --git a/internal/dis/component/server/admin/instance_stats.go b/internal/dis/component/server/admin/instance_stats.go
new file mode 100644
index 0000000..3f75fd4
--- /dev/null
+++ b/internal/dis/component/server/admin/instance_stats.go
@@ -0,0 +1,67 @@
+package admin
+
+import (
+ "context"
+ _ "embed"
+ "html/template"
+ "net/http"
+
+ "github.com/FAU-CDI/wisski-distillery/internal/dis/component"
+ "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/assets"
+ "github.com/FAU-CDI/wisski-distillery/internal/dis/component/server/templating"
+ "github.com/FAU-CDI/wisski-distillery/internal/status"
+ "github.com/FAU-CDI/wisski-distillery/internal/wisski"
+ "github.com/tkw1536/pkglib/httpx"
+
+ "github.com/julienschmidt/httprouter"
+)
+
+//go:embed "html/instance_stats.html"
+var instanceStatsHTML []byte
+var instanceStatsTemplate = templating.Parse[instanceStatsContext](
+ "instance_stats.html", instanceStatsHTML, nil,
+
+ templating.Assets(assets.AssetsAdmin),
+)
+
+type instanceStatsContext struct {
+ templating.RuntimeFlags
+
+ Instance *wisski.WissKI
+ Statistics status.Statistics
+}
+
+func (admin *Admin) instanceStats(ctx context.Context) http.Handler {
+ tpl := instanceStatsTemplate.Prepare(
+ admin.dependencies.Templating,
+ templating.Crumbs(
+ menuAdmin,
+ menuInstances,
+ menuInstance,
+ menuStats,
+ ),
+ )
+
+ return tpl.HTMLHandlerWithFlags(func(r *http.Request) (ctx instanceStatsContext, funcs []templating.FlagFunc, err error) {
+ slug := httprouter.ParamsFromContext(r.Context()).ByName("slug")
+
+ // setup the context with just the instance
+ ctx.Instance, err = admin.dependencies.Instances.WissKI(r.Context(), slug)
+ if err != nil {
+ return ctx, nil, httpx.ErrNotFound
+ }
+
+ // read statistics
+ ctx.Statistics, err = ctx.Instance.Stats().Get(r.Context(), nil)
+ if err != nil {
+ return ctx, nil, httpx.ErrInternalServerError
+ }
+
+ return ctx, []templating.FlagFunc{
+ templating.ReplaceCrumb(menuInstance, component.MenuItem{Title: "Instance", Path: template.URL("/admin/instance/" + ctx.Instance.Slug)}),
+ templating.ReplaceCrumb(menuStats, component.MenuItem{Title: "SSH", Path: template.URL("/admin/instance/" + ctx.Instance.Slug + "/stats")}),
+ templating.Title(ctx.Instance.Slug + " - Stats"),
+ admin.instanceTabs(slug, "stats"),
+ }, nil
+ })
+}
diff --git a/internal/wisski/wisski.go b/internal/wisski/wisski.go
index c40d77a..f36ac12 100644
--- a/internal/wisski/wisski.go
+++ b/internal/wisski/wisski.go
@@ -122,6 +122,22 @@ func (wisski *WissKI) Blocks() *extras.Blocks {
return export[*extras.Blocks](wisski)
}
+func (wisski *WissKI) Stats() *extras.Stats {
+ return export[*extras.Stats](wisski)
+}
+
+func (wisski *WissKI) Requirements() *extras.Requirements {
+ return export[*extras.Requirements](wisski)
+}
+
+func (wisski *WissKI) Version() *extras.Version {
+ return export[*extras.Version](wisski)
+}
+
+func (wisski *WissKI) Theme() *extras.Theme {
+ return export[*extras.Theme](wisski)
+}
+
//
// All components
// THESE SHOULD NEVER BE CALLED DIRECTLY