diff --git a/.badges/code_style-black-000000.svg b/.badges/code_style-black-000000.svg
deleted file mode 100644
index 0355916963e6ae96e60f1cf2a6aa3abce451cc9d_LmJhZGdlcy9jb2RlX3N0eWxlLWJsYWNrLTAwMDAwMC5zdmc=..0000000000000000000000000000000000000000
--- a/.badges/code_style-black-000000.svg
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<svg xmlns="http://www.w3.org/2000/svg" width="114" height="20">
-  <linearGradient id="b" x2="0" y2="100%">
-    <stop offset="0" stop-color="#bbb" stop-opacity=".1" />
-    <stop offset="1" stop-opacity=".1" />
-  </linearGradient>
-  <mask id="anybadge_1">
-    <rect width="114" height="20" rx="3" fill="#fff" />
-  </mask>
-  <g mask="url(#anybadge_1)">
-    <path fill="#555" d="M0 0h72v20H0z" />
-    <path fill="#000000" d="M72 0h42v20H72z" />
-    <path fill="url(#b)" d="M0 0h114v20H0z" />
-  </g>
-  <g
-    fill="#fff"
-    text-anchor="middle"
-    font-family="DejaVu Sans,Verdana,Geneva,sans-serif"
-    font-size="11"
-  >
-    <text x="37.0" y="15" fill="#010101" fill-opacity=".3">code style</text>
-    <text x="36.0" y="14">code style</text>
-  </g>
-  <g
-    fill="#fff"
-    text-anchor="middle"
-    font-family="DejaVu Sans,Verdana,Geneva,sans-serif"
-    font-size="11"
-  >
-    <text x="94.0" y="15" fill="#010101" fill-opacity=".3">black</text>
-    <text x="93.0" y="14">black</text>
-  </g>
-</svg>
diff --git a/.badges/code_style-ruff.svg b/.badges/code_style-ruff.svg
new file mode 100644
index 0000000000000000000000000000000000000000..d1e6e2024d3d882c89f27a1971b9202550abe19d_LmJhZGdlcy9jb2RlX3N0eWxlLXJ1ZmYuc3Zn
--- /dev/null
+++ b/.badges/code_style-ruff.svg
@@ -0,0 +1,49 @@
+<svg
+  xmlns="http://www.w3.org/2000/svg"
+  xmlns:xlink="http://www.w3.org/1999/xlink"
+  width="53"
+  height="20"
+  role="img"
+  aria-label="Ruff"
+>
+  <title>Ruff</title>
+  <linearGradient id="s" x2="0" y2="100%">
+    <stop offset="0" stop-color="#bbb" stop-opacity=".1" />
+    <stop offset="1" stop-opacity=".1" />
+  </linearGradient>
+  <clipPath id="r">
+    <rect width="53" height="20" rx="3" fill="#fff" />
+  </clipPath>
+  <g clip-path="url(#r)">
+    <rect width="20" height="20" fill="#555" />
+    <rect x="20" width="33" height="20" fill="#261230" />
+    <rect width="53" height="20" fill="url(#s)" />
+  </g>
+  <g
+    fill="#fff"
+    text-anchor="middle"
+    font-family="Verdana,Geneva,DejaVu Sans,sans-serif"
+    text-rendering="geometricPrecision"
+    font-size="110"
+  >
+    <image
+      x="5"
+      y="3"
+      width="10"
+      height="14"
+      xlink:href="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEwIiBoZWlnaHQ9IjYyMiIgdmlld0JveD0iMCAwIDUxMCA2MjIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yMDYuNzAxIDBDMjAwLjk2NCAwIDE5Ni4zMTQgNC42NDEzMSAxOTYuMzE0IDEwLjM2NjdWNDEuNDY2N0MxOTYuMzE0IDQ3LjE5MiAxOTEuNjYzIDUxLjgzMzMgMTg1LjkyNyA1MS44MzMzSDE1Ni44NDNDMTUxLjEwNyA1MS44MzMzIDE0Ni40NTYgNTYuNDc0NiAxNDYuNDU2IDYyLjJWMTQ1LjEzM0MxNDYuNDU2IDE1MC44NTkgMTQxLjgwNiAxNTUuNSAxMzYuMDY5IDE1NS41SDEwNi45ODZDMTAxLjI0OSAxNTUuNSA5Ni41OTg4IDE2MC4xNDEgOTYuNTk4OCAxNjUuODY3VjIyMi44ODNDOTYuNTk4OCAyMjguNjA5IDkxLjk0ODQgMjMzLjI1IDg2LjIxMTggMjMzLjI1SDU3LjEyODNDNTEuMzkxNyAyMzMuMjUgNDYuNzQxMyAyMzcuODkxIDQ2Ljc0MTMgMjQzLjYxN1YzMDAuNjMzQzQ2Ljc0MTMgMzA2LjM1OSA0Mi4wOTA5IDMxMSAzNi4zNTQ0IDMxMUgxMC4zODdDNC42NTA0IDMxMSAwIDMxNS42NDEgMCAzMjEuMzY3VjM1Mi40NjdDMCAzNTguMTkyIDQuNjUwNCAzNjIuODMzIDEwLjM4NyAzNjIuODMzSDE0NS40MThDMTUxLjE1NCAzNjIuODMzIDE1NS44MDQgMzY3LjQ3NSAxNTUuODA0IDM3My4yVjQzMC4yMTdDMTU1LjgwNCA0MzUuOTQyIDE1MS4xNTQgNDQwLjU4MyAxNDUuNDE4IDQ0MC41ODNIMTE2LjMzNEMxMTAuNTk3IDQ0MC41ODMgMTA1Ljk0NyA0NDUuMjI1IDEwNS45NDcgNDUwLjk1VjUwNy45NjdDMTA1Ljk0NyA1MTMuNjkyIDEwMS4yOTcgNTE4LjMzMyA5NS41NjAxIDUxOC4zMzNINjYuNDc2NkM2MC43NCA1MTguMzMzIDU2LjA4OTYgNTIyLjk3NSA1Ni4wODk2IDUyOC43VjYxMS42MzNDNTYuMDg5NiA2MTcuMzU5IDYwLjc0IDYyMiA2Ni40NzY2IDYyMkgxNDkuNTcyQzE1NS4zMDkgNjIyIDE1OS45NTkgNjE3LjM1OSAxNTkuOTU5IDYxMS42MzNWNTcwLjE2N0gyMDEuNTA3QzIwNy4yNDQgNTcwLjE2NyAyMTEuODk0IDU2NS41MjUgMjExLjg5NCA1NTkuOFY1MjguN0MyMTEuODk0IDUyMi45NzUgMjE2LjU0NCA1MTguMzMzIDIyMi4yODEgNTE4LjMzM0gyNTEuMzY1QzI1Ny4xMDEgNTE4LjMzMyAyNjEuNzUyIDUxMy42OTIgMjYxLjc1MiA1MDcuOTY3VjQ3Ni44NjdDMjYxLjc1MiA0NzEuMTQxIDI2Ni40MDIgNDY2LjUgMjcyLjEzOCA0NjYuNUgzMDEuMjIyQzMwNi45NTkgNDY2LjUgMzExLjYwOSA0NjEuODU5IDMxMS42MDkgNDU2LjEzM1Y0MjUuMDMzQzMxMS42MDkgNDE5LjMwOCAzMTYuMjU5IDQxNC42NjcgMzIxLjk5NiA0MTQuNjY3SDM1MS4wNzlDMzU2LjgxNiA0MTQuNjY3IDM2MS40NjYgNDEwLjAyNSAzNjEuNDY2IDQwNC4zVjM3My4yQzM2MS40NjYgMzY3LjQ3NSAzNjYuMTE3IDM2Mi44MzMgMzcxLjg1MyAzNjIuODMzSDQwMC45MzdDNDA2LjY3MyAzNjIuODMzIDQxMS4zMjQgMzU4LjE5MiA0MTEuMzI0IDM1Mi40NjdWMzIxLjM2N0M0MTEuMzI0IDMxNS42NDEgNDE1Ljk3NCAzMTEgNDIxLjcxMSAzMTFINDUwLjc5NEM0NTYuNTMxIDMxMSA0NjEuMTgxIDMwNi4zNTkgNDYxLjE4MSAzMDAuNjMzVjIxNy43QzQ2MS4xODEgMjExLjk3NSA0NTYuNTMxIDIwNy4zMzMgNDUwLjc5NCAyMDcuMzMzSDQyMC42NzJDNDE0LjkzNiAyMDcuMzMzIDQxMC4yODUgMjAyLjY5MiA0MTAuMjg1IDE5Ni45NjdWMTY1Ljg2N0M0MTAuMjg1IDE2MC4xNDEgNDE0LjkzNiAxNTUuNSA0MjAuNjcyIDE1NS41SDQ0OS43NTZDNDU1LjQ5MiAxNTUuNSA0NjAuMTQzIDE1MC44NTkgNDYwLjE0MyAxNDUuMTMzVjExNC4wMzNDNDYwLjE0MyAxMDguMzA4IDQ2NC43OTMgMTAzLjY2NyA0NzAuNTMgMTAzLjY2N0g0OTkuNjEzQzUwNS4zNSAxMDMuNjY3IDUxMCA5OS4wMjUzIDUxMCA5My4zVjEwLjM2NjdDNTEwIDQuNjQxMzIgNTA1LjM1IDAgNDk5LjYxMyAwSDIwNi43MDFaTTE2OC4yNjkgNDQwLjU4M0MxNjIuNTMyIDQ0MC41ODMgMTU3Ljg4MiA0NDUuMjI1IDE1Ny44ODIgNDUwLjk1VjUwNy45NjdDMTU3Ljg4MiA1MTMuNjkyIDE1My4yMzEgNTE4LjMzMyAxNDcuNDk1IDUxOC4zMzNIMTE4LjQxMUMxMTIuNjc1IDUxOC4zMzMgMTA4LjAyNCA1MjIuOTc1IDEwOC4wMjQgNTI4LjdWNTU5LjhDMTA4LjAyNCA1NjUuNTI1IDExMi42NzUgNTcwLjE2NyAxMTguNDExIDU3MC4xNjdIMTU5Ljk1OVY1MjguN0MxNTkuOTU5IDUyMi45NzUgMTY0LjYxIDUxOC4zMzMgMTcwLjM0NiA1MTguMzMzSDE5OS40M0MyMDUuMTY2IDUxOC4zMzMgMjA5LjgxNyA1MTMuNjkyIDIwOS44MTcgNTA3Ljk2N1Y0NzYuODY3QzIwOS44MTcgNDcxLjE0MSAyMTQuNDY3IDQ2Ni41IDIyMC4yMDQgNDY2LjVIMjQ5LjI4N0MyNTUuMDI0IDQ2Ni41IDI1OS42NzQgNDYxLjg1OSAyNTkuNjc0IDQ1Ni4xMzNWNDI1LjAzM0MyNTkuNjc0IDQxOS4zMDggMjY0LjMyNSA0MTQuNjY3IDI3MC4wNjEgNDE0LjY2N0gyOTkuMTQ1QzMwNC44ODEgNDE0LjY2NyAzMDkuNTMyIDQxMC4wMjUgMzA5LjUzMiA0MDQuM1YzNzMuMkMzMDkuNTMyIDM2Ny40NzUgMzE0LjE4MiAzNjIuODMzIDMxOS45MTkgMzYyLjgzM0gzNDkuMDAyQzM1NC43MzkgMzYyLjgzMyAzNTkuMzg5IDM1OC4xOTIgMzU5LjM4OSAzNTIuNDY3VjMyMS4zNjdDMzU5LjM4OSAzMTUuNjQxIDM2NC4wMzkgMzExIDM2OS43NzYgMzExSDM5OC44NTlDNDA0LjU5NiAzMTEgNDA5LjI0NiAzMDYuMzU5IDQwOS4yNDYgMzAwLjYzM1YyNjkuNTMzQzQwOS4yNDYgMjYzLjgwOCA0MDQuNTk2IDI1OS4xNjcgMzk4Ljg1OSAyNTkuMTY3SDMxOC44OEMzMTMuMTQzIDI1OS4xNjcgMzA4LjQ5MyAyNTQuNTI1IDMwOC40OTMgMjQ4LjhWMjE3LjdDMzA4LjQ5MyAyMTEuOTc1IDMxMy4xNDMgMjA3LjMzMyAzMTguODggMjA3LjMzM0gzNDcuOTYzQzM1My43IDIwNy4zMzMgMzU4LjM1IDIwMi42OTIgMzU4LjM1IDE5Ni45NjdWMTY1Ljg2N0MzNTguMzUgMTYwLjE0MSAzNjMuMDAxIDE1NS41IDM2OC43MzcgMTU1LjVIMzk3LjgyMUM0MDMuNTU3IDE1NS41IDQwOC4yMDggMTUwLjg1OSA0MDguMjA4IDE0NS4xMzNWMTE0LjAzM0M0MDguMjA4IDEwOC4zMDggNDEyLjg1OCAxMDMuNjY3IDQxOC41OTUgMTAzLjY2N0g0NDcuNjc4QzQ1My40MTUgMTAzLjY2NyA0NTguMDY1IDk5LjAyNTMgNDU4LjA2NSA5My4zVjYyLjJDNDU4LjA2NSA1Ni40NzQ2IDQ1My40MTUgNTEuODMzMyA0NDcuNjc4IDUxLjgzMzNIMjA4Ljc3OEMyMDMuMDQxIDUxLjgzMzMgMTk4LjM5MSA1Ni40NzQ2IDE5OC4zOTEgNjIuMlYxNDUuMTMzQzE5OC4zOTEgMTUwLjg1OSAxOTMuNzQxIDE1NS41IDE4OC4wMDQgMTU1LjVIMTU4LjkyMUMxNTMuMTg0IDE1NS41IDE0OC41MzQgMTYwLjE0MSAxNDguNTM0IDE2NS44NjdWMjIyLjg4M0MxNDguNTM0IDIyOC42MDkgMTQzLjg4MyAyMzMuMjUgMTM4LjE0NyAyMzMuMjVIMTA5LjA2M0MxMDMuMzI3IDIzMy4yNSA5OC42NzYyIDIzNy44OTEgOTguNjc2MiAyNDMuNjE3VjMwMC42MzNDOTguNjc2MiAzMDYuMzU5IDEwMy4zMjcgMzExIDEwOS4wNjMgMzExSDE5Ny4zNTJDMjAzLjA4OSAzMTEgMjA3LjczOSAzMTUuNjQxIDIwNy43MzkgMzIxLjM2N1Y0MzAuMjE3QzIwNy43MzkgNDM1Ljk0MiAyMDMuMDg5IDQ0MC41ODMgMTk3LjM1MiA0NDAuNTgzSDE2OC4yNjlaIiBmaWxsPSIjRDdGRjY0Ii8+PC9zdmc+"
+    />
+    <text
+      aria-hidden="true"
+      x="355"
+      y="150"
+      fill="#010101"
+      fill-opacity=".3"
+      transform="scale(.1)"
+      textLength="230"
+    >
+      Ruff
+    </text>
+    <text x="355" y="140" transform="scale(.1)" fill="#fff" textLength="230">Ruff</text>
+  </g>
+</svg>
diff --git a/.badges/pylint.svg b/.badges/pylint.svg
deleted file mode 100644
index 0355916963e6ae96e60f1cf2a6aa3abce451cc9d_LmJhZGdlcy9weWxpbnQuc3Zn..0000000000000000000000000000000000000000
--- a/.badges/pylint.svg
+++ /dev/null
@@ -1,33 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<svg xmlns="http://www.w3.org/2000/svg" width="103" height="20">
-  <linearGradient id="b" x2="0" y2="100%">
-    <stop offset="0" stop-color="#bbb" stop-opacity=".1" />
-    <stop offset="1" stop-opacity=".1" />
-  </linearGradient>
-  <mask id="anybadge_1">
-    <rect width="103" height="20" rx="3" fill="#fff" />
-  </mask>
-  <g mask="url(#anybadge_1)">
-    <path fill="#555" d="M0 0h44v20H0z" />
-    <path fill="#808080" d="M44 0h59v20H44z" />
-    <path fill="url(#b)" d="M0 0h103v20H0z" />
-  </g>
-  <g
-    fill="#fff"
-    text-anchor="middle"
-    font-family="DejaVu Sans,Verdana,Geneva,sans-serif"
-    font-size="11"
-  >
-    <text x="23.0" y="15" fill="#010101" fill-opacity=".3">pylint</text>
-    <text x="22.0" y="14">pylint</text>
-  </g>
-  <g
-    fill="#fff"
-    text-anchor="middle"
-    font-family="DejaVu Sans,Verdana,Geneva,sans-serif"
-    font-size="11"
-  >
-    <text x="74.5" y="15" fill="#010101" fill-opacity=".3">unknown</text>
-    <text x="73.5" y="14">unknown</text>
-  </g>
-</svg>
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 0355916963e6ae96e60f1cf2a6aa3abce451cc9d_LmdpdGxhYi1jaS55bWw=..d1e6e2024d3d882c89f27a1971b9202550abe19d_LmdpdGxhYi1jaS55bWw= 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,6 +1,3 @@
 include:
   - project: xcg/ci-templates
-    file: /odoo/15.0/gitlab-ci.yaml
-
-variables:
-  CI_TEMPLATE_NO_INSTALL_MODULE: ""
+    file: /odoo/17.0/gitlab-ci.yaml
diff --git a/NEWS.rst b/NEWS.rst
index 0355916963e6ae96e60f1cf2a6aa3abce451cc9d_TkVXUy5yc3Q=..d1e6e2024d3d882c89f27a1971b9202550abe19d_TkVXUy5yc3Q= 100644
--- a/NEWS.rst
+++ b/NEWS.rst
@@ -1,4 +1,3 @@
-=========
 Changelog
 =========
 
@@ -2,6 +1,6 @@
 Changelog
 =========
 
-15.0.1.0.1
+17.0.1.0.0
 ----------
 
@@ -6,9 +5,3 @@
 ----------
 
-Package the module so that it can be installed.
-
-15.0.1.0.0
-----------
-
-Port to Odoo 15
-
+Migration to Odoo 17.
diff --git a/README.rst b/README.rst
index 0355916963e6ae96e60f1cf2a6aa3abce451cc9d_UkVBRE1FLnJzdA==..d1e6e2024d3d882c89f27a1971b9202550abe19d_UkVBRE1FLnJzdA== 100644
--- a/README.rst
+++ b/README.rst
@@ -2,18 +2,9 @@
 Menu Redirector
 ===============
 
-.. |pipeline| image:: https://orus.io/xcg/odoo-modules/menuredir/badges/branch/15.0/pipeline.svg
-    :target: https://orus.io/xcg/odoo-modules/menuredir/commits/branch/15.0
-    :alt: pipeline status
-.. |coverage| image:: https://orus.io/xcg/odoo-modules/menuredir/badges/branch/15.0/coverage.svg
-    :target: https://orus.io/xcg/odoo-modules/menuredir/commits/branch/15.0
-    :alt: Coverage report
-.. |pylint| image:: .badges/pylint.svg
-    :target: https://orus.io/xcg/odoo-modules/menuredir/-/pipelines?ref=branch/15.0
-    :alt: pylint score
 .. |maturity| image:: .badges/maturity.svg
     :target: https://odoo-community.org/page/development-status
     :alt: Stable
 .. |license| image:: .badges/licence-AGPL--3-blue.svg
     :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
     :alt: License: AGPL-3
@@ -14,13 +5,13 @@
 .. |maturity| image:: .badges/maturity.svg
     :target: https://odoo-community.org/page/development-status
     :alt: Stable
 .. |license| image:: .badges/licence-AGPL--3-blue.svg
     :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
     :alt: License: AGPL-3
-.. |black| image:: .badges/code_style-black-000000.svg
-    :target: https://github.com/psf/black
-    :alt: Black
+.. |ruff| image:: .badges/code_style-ruff.svg
+    :target: https://github.com/astral-sh/ruff
+    :alt: Ruff
 .. |prettier| image:: .badges/code_style-prettier-ff69b4.svg
     :target: https://github.com/prettier/prettier
     :alt: Prettier
 
@@ -23,8 +14,8 @@
 .. |prettier| image:: .badges/code_style-prettier-ff69b4.svg
     :target: https://github.com/prettier/prettier
     :alt: Prettier
 
-|pipeline| |coverage| |pylint| |maturity| |license| |black| |prettier|
+|maturity| |license| |ruff| |prettier|
 
 A simple web controller that redirects the web browser to the proper
 application and menu_id after looking up for them in the database.
@@ -34,3 +25,18 @@
 avoids to use internal database ids that may change between database instances
 depending on the order in which modules were installed.
 
+Usage
+=====
+
+A new controller is added that redirect /menuredir/go?app=base&db=odoo&menu=menu_ir_access_act to the menu item whose external id is base.menu_ir_access_act in the database odoo.
+
+The db parameter can be omitted when the server is running a single database.
+
+When used with multiple database, the module needs to be added to the list of modules for server wide modules.
+
+When defined in server wide modules, the module does not need to be installed.
+
+If a db filter is defined in Odoo’s configuration file, the provided name is checked against the filter.
+
+If the database name is filtered or does not exist, or if the menu item is not found or is not a menu item, the module redirects to /web and log the error.
+
diff --git a/__init__.py b/__init__.py
index 0355916963e6ae96e60f1cf2a6aa3abce451cc9d_X19pbml0X18ucHk=..d1e6e2024d3d882c89f27a1971b9202550abe19d_X19pbml0X18ucHk= 100644
--- a/__init__.py
+++ b/__init__.py
@@ -1,1 +1,1 @@
-from . import controllers  # noqa: F401
+from . import controllers
diff --git a/__manifest__.py b/__manifest__.py
index 0355916963e6ae96e60f1cf2a6aa3abce451cc9d_X19tYW5pZmVzdF9fLnB5..d1e6e2024d3d882c89f27a1971b9202550abe19d_X19tYW5pZmVzdF9fLnB5 100644
--- a/__manifest__.py
+++ b/__manifest__.py
@@ -20,8 +20,8 @@
 {
     "name": "Menu Redirector",
     "license": "AGPL-3",
-    "version": "15.0.1.0.1",
+    "version": "17.0.1.0.0",
     "category": "Web",
     "author": "XCG Consulting",
     "website": "https://orbeet.io/",
     "depends": ["web"],
@@ -24,7 +24,6 @@
     "category": "Web",
     "author": "XCG Consulting",
     "website": "https://orbeet.io/",
     "depends": ["web"],
-    "data": [],
     "installable": True,
 }
diff --git a/controllers/main.py b/controllers/main.py
index 0355916963e6ae96e60f1cf2a6aa3abce451cc9d_Y29udHJvbGxlcnMvbWFpbi5weQ==..d1e6e2024d3d882c89f27a1971b9202550abe19d_Y29udHJvbGxlcnMvbWFpbi5weQ== 100644
--- a/controllers/main.py
+++ b/controllers/main.py
@@ -1,7 +1,8 @@
 ##############################################################################
 #
 #    Menu redirector for Odoo
-#    Copyright © 2015, 2018, 2019, 2022 XCG Consulting (https://xcg-consulting.fr/)
+#    Copyright © 2015, 2018, 2019, 2022, 2024 XCG Consulting
+#              (https://xcg-consulting.fr/)
 #
 #    This program is free software: you can redistribute it and/or modify
 #    it under the terms of the GNU Affero General Public License as
@@ -19,10 +20,10 @@
 ##############################################################################
 import logging
 
-import werkzeug.utils
-
-from odoo.http import Controller, request, route
-from odoo.modules.registry import Registry
+from odoo import SUPERUSER_ID, registry
+from odoo.api import Environment
+from odoo.http import Controller, db_filter, request, route
+from werkzeug import utils
 
 log = logging.getLogger(__name__)
 
@@ -57,8 +58,7 @@
         Find the correct menu id and redirect the client to the proper URL as if he had
         clicked on the menu.
         """
-        dbname, app, menu = (
-            kw.get("db", None),
+        app, menu = (
             kw.get("app", None),
             kw.get("menu", None),
         )
@@ -62,6 +62,13 @@
             kw.get("app", None),
             kw.get("menu", None),
         )
+        # Inspired by code in auth_oauth controller
+        dbname = kw.get("db", None)
+        if not dbname:
+            dbname = request.db
+        if dbname and not db_filter([dbname]):
+            # filter out any filtered dbname
+            dbname = None
 
         url = "/web"
         error_ = request_error(dbname, app, menu)
@@ -69,5 +76,5 @@
 
         if not error_:
             try:
-                registry = Registry(dbname)
+                odoo_registry = registry(dbname)
 
@@ -73,3 +80,3 @@
 
-                with registry.cursor():
+                with odoo_registry.cursor() as cr:
                     try:
@@ -75,7 +82,8 @@
                     try:
-                        log.info(
+                        env = Environment(cr, SUPERUSER_ID, {})
+                        log.debug(
                             "Searching menu item %s.%s for database %s",
                             app,
                             menu,
                             dbname,
                         )
@@ -77,11 +85,11 @@
                             "Searching menu item %s.%s for database %s",
                             app,
                             menu,
                             dbname,
                         )
-                        irmd = request.env["ir.model.data"]
+                        irmd = env["ir.model.data"]
                         # pylint: disable=protected-access
                         res_model, res_id = irmd._xmlid_to_res_model_res_id(
                             f"{app}.{menu}", True
                         )
                         if res_model == "ir.ui.menu":
@@ -83,10 +91,14 @@
                         # pylint: disable=protected-access
                         res_model, res_id = irmd._xmlid_to_res_model_res_id(
                             f"{app}.{menu}", True
                         )
                         if res_model == "ir.ui.menu":
-                            url = f"/web?&db={dbname}#menu_id={res_id}&action="
-                            log.info(REDIRECT_MSG, url)
+                            url = (
+                                "/web"
+                                + (f"?db={dbname}" if "db" in kw else "")
+                                + f"#menu_id={res_id}&action="
+                            )
+                            log.debug(REDIRECT_MSG, url)
                         else:
                             log.error("%s.%s is not a menu xmlid", app, menu)
                         # this almost could be a permanent redirect, but if the menu
@@ -102,5 +114,5 @@
                         log.exception("Unknown error searching for menu item")
 
             except Exception:
-                log.error("Invalid dbname provided: redirecting to /")
+                log.exception("Invalid db provided: redirecting to /")
 
@@ -106,5 +118,5 @@
 
-        redirect = werkzeug.utils.redirect(url, 302)
+        redirect = utils.redirect(url, 302)
         redirect.autocorrect_location_header = True
         if cache_header:
             redirect.headers.add("Cache-Control", cache_header)
diff --git a/pyproject.toml b/pyproject.toml
index 0355916963e6ae96e60f1cf2a6aa3abce451cc9d_cHlwcm9qZWN0LnRvbWw=..d1e6e2024d3d882c89f27a1971b9202550abe19d_cHlwcm9qZWN0LnRvbWw= 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,4 +11,5 @@
   "Programming Language :: Python :: 3",
   "Programming Language :: Python :: 3.9",
   "Programming Language :: Python :: 3.10",
+  "Programming Language :: Python :: 3.11",
   "Framework :: Odoo",
@@ -14,4 +15,4 @@
   "Framework :: Odoo",
-  "Framework :: Odoo :: 15.0",
+  "Framework :: Odoo :: 17.0",
   "License :: OSI Approved :: GNU Affero General Public License v3"
 ]
@@ -16,6 +17,6 @@
   "License :: OSI Approved :: GNU Affero General Public License v3"
 ]
-dependencies = ["odoo==15.0.*"]
+dependencies = ["odoo==17.0.*"]
 
 [project.optional-dependencies]
 doc = ["sphinx", "sphinx-odoo-autodoc", "odoo-scripts"]
@@ -23,7 +24,7 @@
 
 [project.urls]
 repository = "https://orus.io/xcg/odoo-modules/menuredir"
-changelog = "https://orus.io/xcg/odoo-modules/menuredir/-/blob/branch/15.0/NEWS.rst"
+changelog = "https://orus.io/xcg/odoo-modules/menuredir/-/blob/branch/17.0/NEWS.rst"
 
 [build-system]
 requires = ["hatchling >=1.19", "hatch-vcs"]
@@ -60,7 +61,18 @@
 [tool.hatch.version]
 source = "vcs"
 
-[tool.black]
-target-version = ["py39", "py310", "py311"]
-required-version = "22"
+[tool.isort]
+section-order = [
+  "future",
+  "standard-library",
+  "third-party",
+  "odoo",
+  "odoo-addons",
+  "first-party",
+  "local-folder"
+]
+
+[tool.isort.sections]
+"odoo" = ["odoo"]
+"odoo-addons" = ["odoo.addons"]
 
@@ -66,15 +78,16 @@
 
-[tool.isort]
-py_version = 39
-profile = "black"
-known_odoo = ['odoo']
-known_odoo_addons = ['odoo.addons']
-sections = [
-  'FUTURE',
-  'STDLIB',
-  'THIRDPARTY',
-  'ODOO',
-  'ODOO_ADDONS',
-  'FIRSTPARTY',
-  'LOCALFOLDER'
+[tool.ruff.lint.mccabe]
+max-complexity = 16
+
+[tool.ruff]
+target-version = "py39"
+
+[tool.ruff.lint]
+extend-select = [
+  "B",
+  "C90",
+  "E501", # line too long (default 88)
+  "I", # isort
+  "UP", # pyupgrade
+
 ]
@@ -80,1 +93,5 @@
 ]
+
+[tool.ruff.lint.per-file-ignores]
+"__init__.py" = ["F401", "I001"] # ignore unused and unsorted imports in __init__.py
+"__manifest__.py" = ["B018"] # useless expression
diff --git a/tests/test_controller.py b/tests/test_controller.py
index 0355916963e6ae96e60f1cf2a6aa3abce451cc9d_dGVzdHMvdGVzdF9jb250cm9sbGVyLnB5..d1e6e2024d3d882c89f27a1971b9202550abe19d_dGVzdHMvdGVzdF9jb250cm9sbGVyLnB5 100644
--- a/tests/test_controller.py
+++ b/tests/test_controller.py
@@ -1,4 +1,4 @@
-from odoo.tests.common import HttpCase, tagged
+from odoo.tests import HttpCase, tagged
 from odoo.tools import mute_logger
 
 
@@ -11,9 +11,10 @@
         dbname = self.env.cr.dbname
         menu = "menu_action_res_users"
         url = f"/menuredir/go?db={dbname}&app=base&menu={menu}"
-        res = self.url_open(url, allow_redirects=False)
-        self.assertEqual(res.status_code, 302, "Response should be a redirect")
-        res = self.url_open(url, allow_redirects=True)
+        with mute_logger("odoo.addons.menuredir.controllers.main", "werkzeug"):
+            res = self.url_open(url, allow_redirects=False)
+            self.assertEqual(res.status_code, 302, "Response should be a redirect")
+            res = self.url_open(url, allow_redirects=True)
         self.assertEqual(res.status_code, 200, "Response should = OK")
         menu_id = self.env.ref(f"base.{menu}").id
         self.assertTrue(res.url.endswith(f"#menu_id={menu_id}&action="))
@@ -22,8 +23,8 @@
         """Test the redirect when the database name is missing"""
         menu = "menu_action_res_users"
         url = f"/menuredir/go?app=base&menu={menu}"
-        with mute_logger("odoo.addons.menuredir.controllers.main"):
+        with mute_logger("odoo.addons.menuredir.controllers.main", "werkzeug"):
             res = self.url_open(url, allow_redirects=False)
             self.assertEqual(res.status_code, 302, "Response should be a redirect")
             res = self.url_open(url, allow_redirects=True)
             self.assertEqual(res.status_code, 200, "Response should = OK")
@@ -26,10 +27,26 @@
             res = self.url_open(url, allow_redirects=False)
             self.assertEqual(res.status_code, 302, "Response should be a redirect")
             res = self.url_open(url, allow_redirects=True)
             self.assertEqual(res.status_code, 200, "Response should = OK")
+            # Tests are run with a single database, so it should redirect correctly
+            menu_id = self.env.ref(f"base.{menu}").id
+            self.assertTrue(res.url.endswith(f"#menu_id={menu_id}&action="))
+
+    def test_invalid_db(self):
+        """Test the redirect when the database name is incorrect"""
+        # Assumes <dbname>-invalid is not a valid database name
+        dbname = self.env.cr.dbname + "-invalid"
+        menu = "menu_action_res_users"
+        url = f"/menuredir/go?db={dbname}&app=base&menu={menu}"
+        with mute_logger("odoo.addons.menuredir.controllers.main", "werkzeug"):
+            res = self.url_open(url, allow_redirects=False)
+            self.assertEqual(res.status_code, 302, "Response should be a redirect")
+            res = self.url_open(url, allow_redirects=True)
+            self.assertEqual(res.status_code, 200, "Response should = OK")
+            self.assertTrue(res.url.endswith("/web/login"))
 
     def test_missing_app(self):
         """Test the redirect when the module name is missing"""
         dbname = self.env.cr.dbname
         menu = "menu_action_res_users"
         url = f"/menuredir/go?db={dbname}&menu={menu}"
@@ -30,11 +47,11 @@
 
     def test_missing_app(self):
         """Test the redirect when the module name is missing"""
         dbname = self.env.cr.dbname
         menu = "menu_action_res_users"
         url = f"/menuredir/go?db={dbname}&menu={menu}"
-        with mute_logger("odoo.addons.menuredir.controllers.main"):
+        with mute_logger("odoo.addons.menuredir.controllers.main", "werkzeug"):
             res = self.url_open(url, allow_redirects=False)
             self.assertEqual(res.status_code, 302, "Response should be a redirect")
             res = self.url_open(url, allow_redirects=True)
             self.assertEqual(res.status_code, 200, "Response should = OK")
@@ -37,9 +54,10 @@
             res = self.url_open(url, allow_redirects=False)
             self.assertEqual(res.status_code, 302, "Response should be a redirect")
             res = self.url_open(url, allow_redirects=True)
             self.assertEqual(res.status_code, 200, "Response should = OK")
+            self.assertTrue(res.url.endswith("/web/login"))
 
     def test_messing_menu(self):
         """Test the redirect when the menu xmlid is missing"""
         dbname = self.env.cr.dbname
         url = f"/menuredir/go?db={dbname}&app=base"
@@ -41,10 +59,10 @@
 
     def test_messing_menu(self):
         """Test the redirect when the menu xmlid is missing"""
         dbname = self.env.cr.dbname
         url = f"/menuredir/go?db={dbname}&app=base"
-        with mute_logger("odoo.addons.menuredir.controllers.main"):
+        with mute_logger("odoo.addons.menuredir.controllers.main", "werkzeug"):
             res = self.url_open(url, allow_redirects=False)
             self.assertEqual(res.status_code, 302, "Response should be a redirect")
             res = self.url_open(url, allow_redirects=True)
             self.assertEqual(res.status_code, 200, "Response should = OK")
@@ -47,10 +65,11 @@
             res = self.url_open(url, allow_redirects=False)
             self.assertEqual(res.status_code, 302, "Response should be a redirect")
             res = self.url_open(url, allow_redirects=True)
             self.assertEqual(res.status_code, 200, "Response should = OK")
+            self.assertTrue(res.url.endswith("/web/login"))
 
     def test_invalid_menu(self):
         """Test the redirect when the menu xmlid is incorrect"""
         dbname = self.env.cr.dbname
         menu = "made_up_id"
         url = f"/menuredir/go?db={dbname}&app=base&menu={menu}"
@@ -51,11 +70,11 @@
 
     def test_invalid_menu(self):
         """Test the redirect when the menu xmlid is incorrect"""
         dbname = self.env.cr.dbname
         menu = "made_up_id"
         url = f"/menuredir/go?db={dbname}&app=base&menu={menu}"
-        with mute_logger("odoo.addons.menuredir.controllers.main"):
+        with mute_logger("odoo.addons.menuredir.controllers.main", "werkzeug"):
             res = self.url_open(url, allow_redirects=False)
             self.assertEqual(res.status_code, 302, "Response should be a redirect")
             res = self.url_open(url, allow_redirects=True)
             self.assertEqual(res.status_code, 200, "Response should = OK")
@@ -58,10 +77,11 @@
             res = self.url_open(url, allow_redirects=False)
             self.assertEqual(res.status_code, 302, "Response should be a redirect")
             res = self.url_open(url, allow_redirects=True)
             self.assertEqual(res.status_code, 200, "Response should = OK")
+            self.assertTrue(res.url.endswith("/web/login"))
 
     def test_not_a_menu(self):
         """Test the redirect when the menu xmlid is not a menu"""
         dbname = self.env.cr.dbname
         menu = "group_user"
         url = f"/menuredir/go?db={dbname}&app=base&menu={menu}"
@@ -62,11 +82,11 @@
 
     def test_not_a_menu(self):
         """Test the redirect when the menu xmlid is not a menu"""
         dbname = self.env.cr.dbname
         menu = "group_user"
         url = f"/menuredir/go?db={dbname}&app=base&menu={menu}"
-        with mute_logger("odoo.addons.menuredir.controllers.main"):
+        with mute_logger("odoo.addons.menuredir.controllers.main", "werkzeug"):
             res = self.url_open(url, allow_redirects=False)
             self.assertEqual(res.status_code, 302, "Response should be a redirect")
             res = self.url_open(url, allow_redirects=True)
             self.assertEqual(res.status_code, 200, "Response should = OK")
@@ -69,4 +89,5 @@
             res = self.url_open(url, allow_redirects=False)
             self.assertEqual(res.status_code, 302, "Response should be a redirect")
             res = self.url_open(url, allow_redirects=True)
             self.assertEqual(res.status_code, 200, "Response should = OK")
+            self.assertTrue(res.url.endswith("/web/login"))