diff --git a/Project.toml b/Project.toml
index 82ad0ca25a63ec9961f978f9a2295056eef2c008..807476964d4245ed2f5fce02d49c7554d39bab2d 100644
--- a/Project.toml
+++ b/Project.toml
@@ -1,7 +1,7 @@
 name = "LarvaTagger"
 uuid = "8b3b36f1-dfed-446e-8561-ea19fe966a4d"
 authors = ["François Laurent", "Institut Pasteur"]
-version = "0.20"
+version = "0.20.1"
 
 [deps]
 Bonito = "824d6782-a2ef-11e9-3a09-e5662e0c26f8"
diff --git a/recipes/release.sh b/recipes/release.sh
index 396d85810578e9f7385bedc97fcbf336869d495b..dbeda37a41715412c4ea286124967f51c101508d 100755
--- a/recipes/release.sh
+++ b/recipes/release.sh
@@ -14,7 +14,7 @@ docker=docker LARVATAGGER_IMAGE=flaur/larvatagger:$RELEASE-20230311 scripts/larv
 docker tag flaur/larvatagger:$RELEASE-20230311 flaur/larvatagger:latest
 docker build -t flaur/larvatagger:$RELEASE-bigfat -f recipes/Dockerfile.pasteurjanelia --no-cache .
 
-test/predict_and_retrain.sh
+#test/predict_and_retrain.sh
 
 cat <<EOF
 Next steps are:
diff --git a/scripts/install.sh b/scripts/install.sh
index 30f8023200a156b958b4dbe5704a46419b4f553e..cdd3dcb9c8fcf1981effec09fcf03c4448aabbd9 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -50,7 +50,7 @@ fi
 if [ "$1" = "--uninstall" ]; then
   for pkg in MaggotUBA TaggingBackends; do
     if [ -d "$LARVATAGGER_PATH/$pkg" ]; then
-      env=$(cd "$LARVATAGGER_PATH/$pkg" && poetry env info -p)
+      env=$(cd "$LARVATAGGER_PATH/$pkg" && poetry env info -p 2>/dev/null)
       [ -d "$env" ] && rm -rf "$env"
     fi
   done
@@ -278,9 +278,6 @@ EOF
 }
 
 if [ -n "$WITH_BACKEND" ]; then
-  if [ "`uname`" = "Darwin" ]; then
-    echo "WARNING: the default tagging backend is not supported by macOS"
-  fi
   if ! command -v python$PYTHON_VERSION &>/dev/null; then
     if command -v pyenv &>/dev/null; then
       [ `pyenv versions | grep $PYTHON_VERSION` ] || pyenv install $PYTHON_VERSION
@@ -370,7 +367,7 @@ else
 activate() {
   # pyenv activation is necessary on WSL
   command -v pyenv &>/dev/null && [ -n "`pyenv versions | grep '  $PYTHON_VERSION'`" ] && pyenv local $PYTHON_VERSION
-  poetry env use $PYTHON_VERSION
+  poetry env use $PYTHON_VERSION 2>/dev/null
 }
 
 if [ -d TaggingBackends ]; then
diff --git a/src/REST/Client.jl b/src/REST/Client.jl
index 93ca3c3acae716775bd69e719849f5162ce36be2..106415c1e0d138349979c22c3d4a2d9b16012c8c 100644
--- a/src/REST/Client.jl
+++ b/src/REST/Client.jl
@@ -219,6 +219,7 @@ end
 function listmodels(back::LTBackend, ::Val{true})
     map(back.active_tagging_backend) do tagging_backend
         models = OrderedDict{String, String}[]
+        isnothing(tagging_backend) && return models
         for name in keys(back.taggers[tagging_backend])
             metadata = back.metadata[tagging_backend][:models][name]
             push!(models, OrderedDict("name" => name,
diff --git a/src/REST/Model.jl b/src/REST/Model.jl
index d7122356c9149b52041b06dc85b14db29811a485..1450f65a93fc888d7865553390c92233b0633036 100644
--- a/src/REST/Model.jl
+++ b/src/REST/Model.jl
@@ -14,13 +14,15 @@ struct LTBackend
     root
     tokens
     lock
+    token_expiry
 end
 
 function LTBackend()
     root = Ref{AbstractString}("")
     tokens = Dict{String, Dict{String, Dict{String, Float64}}}()
     lock = ReentrantLock()
-    LTBackend(root, tokens, lock)
+    token_expiry = Ref{Union{Nothing, Real}}(nothing)
+    LTBackend(root, tokens, lock, token_expiry)
 end
 
 Base.lock(f::Function, backend::LTBackend) = lock(f, backend.lock)
@@ -66,6 +68,7 @@ end
 
 function gettoken(lt_backend, backend_dir, model_instance)
     tagger = gettagger(lt_backend, backend_dir, model_instance)
+    resetdata(lt_backend) # perform server-wide maintenance
     return tagger.sandbox
 end
 
@@ -88,6 +91,29 @@ function resetdata(lt_backend, backend_dir, model_instance, token, datadir=nothi
     nothing
 end
 
+function resetdata(lt_backend, min_age)
+    isnothing(min_age) && return
+    @assert min_age isa Real
+    lock(lt_backend) do
+        for (backend_dir, instances) in pairs(lt_backend.tokens)
+            for (model_instance, tokens) in pairs(instances)
+                for (token, created) in pairs(copy(tokens))
+                    age = time() - created
+                    if min_age <= age
+                        @info "resetdata" backend_dir model_instance token age
+                        tagger = gettagger(lt_backend, backend_dir, model_instance, token)
+                        Taggers.resetdata(tagger)
+                        pop!(tokens, token)
+                    end
+                end
+            end
+        end
+    end
+    nothing
+end
+
+resetdata(lt_backend) = resetdata(lt_backend, lt_backend.token_expiry[])
+
 function listfiles(lt_backend, backend_dir, model_instance, token, data_dir)
     tagger = gettagger(lt_backend, backend_dir, model_instance, token)
     dir = Taggers.datadir(tagger, data_dir)
diff --git a/src/REST/Server.jl b/src/REST/Server.jl
index 4bd2bf57fe951ea4acd3d8f00a1b6b403b06bdca..bda84596052d7f54ee77cbc273f6a7c5ef0a6d24 100644
--- a/src/REST/Server.jl
+++ b/src/REST/Server.jl
@@ -17,8 +17,9 @@ end
 # state
 const lt_backend = LTBackend()
 
-function run_backend(root::AbstractString; kwargs...)
+function run_backend(root::AbstractString, token_expiry=nothing; kwargs...)
     lt_backend.root[] = root
+    lt_backend.token_expiry[] = token_expiry
     run_backend(; kwargs...)
 end
 
@@ -49,6 +50,14 @@ end
 end
 
 
+@get "/reset-data/{min_age}" function (
+        request,
+        min_age::Int,
+    )
+    resetdata(lt_backend, min_age)
+end
+
+
 @get "/reset-data/{backend_dir}/{model_instance}/{token}" function(
         request,
         backend_dir::String,
diff --git a/src/backends.jl b/src/backends.jl
index a3ad720f5bd8d784e765da36f3ca4fef24d79cd0..d5f077557b0a107efb07e98087ef3e9d4e9fb762 100644
--- a/src/backends.jl
+++ b/src/backends.jl
@@ -51,7 +51,11 @@ function getbackends(controller, location=nothing)
     else
         if !isnothing(location) && startswith(location, "http://")
             back = REST.Client.LTBackend(location)
-            REST.Client.connect(back; preselect_tagger=true)
+            try
+                REST.Client.connect(back; preselect_tagger=true)
+            catch
+                @error "Failed to connect to backend"
+            end
             controller[:backends] = back
         else
             backends = Backends(controller, location)
diff --git a/src/cli_open.jl b/src/cli_open.jl
index cdb08e21b4551d50da4ae9c5ea35e5d5c3b90575..900d9a4d1f1a7ff8865a98ce8f88108f144468c8 100644
--- a/src/cli_open.jl
+++ b/src/cli_open.jl
@@ -54,8 +54,14 @@ function main(args=ARGS; exit_on_error=false)
     infile = parsed_args["<file-path>"]
     if isempty(infile)
         infile = nothing
-    elseif !(startswith(infile, "http://") || isfile(infile))
-        if isdir(infile)
+    elseif !startswith(infile, "http://")
+        if isfile(infile)
+            dataroot = dirname(infile)
+            if !isempty(dataroot)
+                cd(dataroot)
+                infile = basename(infile)
+            end
+        elseif isdir(infile)
             dataroot = infile
             infile = nothing
             cd(dataroot)
diff --git a/src/files.jl b/src/files.jl
index a9f9a120dc725c2462f772e85b89e2015e49ddc8..2dce232a5ebe426ad9729a477edd3e358ec5107a 100644
--- a/src/files.jl
+++ b/src/files.jl
@@ -249,6 +249,25 @@ end
 
 getoutput(controller) = gethub(controller)[:output]
 
+function valid_filename(name)
+    if startswith(name, ".")
+        return false
+    end
+    # adapted from NyxUI.jl (MIT license, same author)
+    windows_extra = "|:*?<>"
+    for c in "/\\'\"`" * windows_extra
+        if c in name
+            return false
+        end
+    end
+    for nonprintable in 0x0:0x31
+        if nonprintable in name
+            return false
+        end
+    end
+    return true
+end
+
 interpolate(s="yyyymmdd_HHMMSS") = Dates.format(Dates.now(), s)
 
 function savetofile(controller, file; datafile=nothing, merge=false)
@@ -474,6 +493,11 @@ function getoutputfile(controller)
             dir = cwd(controller)
             if isnothing(file)
                 outputfile.name.val = "{yyyymmdd_HHMMSS}.label"
+            elseif !valid_filename(file)
+                @warn "Invalid filename; saving to date_time format instead" file
+                file = "{yyyymmdd_HHMMSS}.label"
+                savetofile(hub, file)
+                reset!(outputfile)
             elseif isfile(joinpath(dir, file))
                 twooptiondialog(hub, outputfile.merge,
                     "File already exists",
diff --git a/src/larvatagger.js b/src/larvatagger.js
index 8437424d0d5ae6ebd80506e8f9a72dae5a256b65..680b6cdc448ef544b3739d25236b252c3a6c6828 100644
--- a/src/larvatagger.js
+++ b/src/larvatagger.js
@@ -118,6 +118,7 @@ const LarvaTagger = (function () {
 		return false;
 	}
 
+	// TODO: validate filepath similarly to valid_filename in files.jl
 	function setOutputFilename(obs) {
 		var defaultfilepath = obs.value;
 		if (defaultfilepath === null) {
diff --git a/test/rest_client.sh b/test/rest_client.sh
index 9763454f8a6cb2ac4b5a3b7d71e10ef3c1fc308e..1213d5a78def51c709bf8a712fa815cb5a735f8b 100755
--- a/test/rest_client.sh
+++ b/test/rest_client.sh
@@ -24,7 +24,7 @@ julia "+$JULIA_VERSION" --project="${larvatagger_jl_project_root}" -q -e "using
 # run and background the backend server
 JULIA_PROJECT="${larvatagger_project_root}/TaggingBackends" \
   julia "+$JULIA_VERSION" --project="${larvatagger_jl_project_root}" -i \
-  -e "using LarvaTagger.REST.Server; run_backend(\"${larvatagger_project_root}\"; port=${lt_backend_port})" &
+  -e "using LarvaTagger.REST.Server; run_backend(\"${larvatagger_project_root}\", 300; port=${lt_backend_port})" &
 lt_backend_pid=$!
 
 # run the frontend server
@@ -36,6 +36,8 @@ JULIA="julia +$JULIA_VERSION" ${larvatagger_jl_project_root}/scripts/larvatagger
 #   expected: a predicted.label is generated and the GUI reloads
 # * load a second tracking data file (binary if first was ascii or vice versa), select another model instance, click again on "Autotag";
 #   expected: a new token was issued + similar outcome as previous step, with tracking data file and tagging model properly identified in the predicted.label file
+# * wait for 5 min and click again on "Autotag";
+#   expected: the data directories corresponding to the previous tokens are empty
 
 kill $lt_backend_pid
 wait $lt_backend_pid