diff --git a/.github/workflows/pylint.yml b/.github/workflows/pylint.yml
new file mode 100644
index 0000000..645daa2
--- /dev/null
+++ b/.github/workflows/pylint.yml
@@ -0,0 +1,26 @@
+name: Pylint
+
+on: [ push ]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: [ "3.10" ]
+    steps:
+      - uses: actions/checkout@v3
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v3
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install -r requirements.txt
+      - name: Analysing the code with pylint
+        run: |
+          pylint app --disable=C0116,C0114,C0115,C0411,E0401,W0611,W0622,W0719,C0103,W1514,R0903,R1732,W0718
+      - name: Analysing the code with pycodestyle
+        run: |
+          pycodestyle app
diff --git a/.gitignore b/.gitignore
index 3e46e49..c615a4c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,7 @@
 # Project-specific files
-config.cfg
+external/
+log.txt
+
 # Byte-compiled / optimized / DLL files
 __pycache__/
 *.py[cod]
@@ -151,4 +153,6 @@ out/
 .vscode/
 
 .fleet
+.DS_Store
+**/.DS_Store
 
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..650f0b1
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,11 @@
+FROM python:3.10-slim
+
+WORKDIR /app
+
+COPY requirements.txt /app
+RUN --mount=type=cache,target=/root/.cache/pip \
+    pip3 install -r requirements.txt
+
+COPY . /app
+
+CMD ["gunicorn", "--bind", "0.0.0.0:5000", "application:app"]
\ No newline at end of file
diff --git a/README.md b/README.md
index bd545a7..39781a6 100644
--- a/README.md
+++ b/README.md
@@ -3,122 +3,125 @@
 PRISMO Admin Panel
 ===================
 
-The goal of this webtool is provide basic management capabilities for hackerspaces, like:
+Prismo is fully open source and easy to install access system for control access of tools and equipment for maker
+spaces.
 
-1. Presence management basing on MAC address monitoring
-2. RFID access system management
-3. Payments monitoring
-4. Internal information storage(wiki based)
+The gold for the project to create a system which any maker space in the world can setup for own use. The system fully
+open source, include the backend, readers firmware and PCB schema.
 
-## Prepare database
+## Installation by docker
 
-Install docker on your system. 
+- Install docker on the host machine.
+  Check [the tutorial for Raspberry Pi 4](https://github.com/codingforentrepreneurs/Pi-Awesome/blob/main/how-tos/Docker%20%26%20Docker%20Compose%20on%20Raspberry%20Pi.md)
+- Create a folder `data` - the folder use for keep all persistent data, like a database.
+- Run docker container:
 
-1. Pull PostgreSQL docker image
+```bash
+docker run --name=prismo-app -p 80:5000 --restart always --detach -v "$(pwd)/data/:/app/external/" hacklabkyiv/prismo-app:0.1.7
+```
+
+Add docker to autostart:
 
-   ```bash
-   $ docker pull postgres
-   ```
+```bash
+sudo systemctl enable docker
+```
 
-2. Add user to group docker, user this instructions https://docs.docker.com/install/linux/linux-postinstall/. This will allow to use docker without sudo. TODO: update step for MacOS users
+The application ready to work and available on `http://localhost:5000`
 
-#### Optional steps
+### The reader firmware
 
-By default, this should be run by Prismo admin process, but for debugging purpose you should run this commands by yourself.
+The reader is a device which connected to the network and read RFID cards. The reader firmware is stored in
+the `prismo-reader` [repository](https://github.com/hacklabkyiv/prismo-reader/tree/micropython_pn532).
 
-1. Run docker with. Here we will create database with name `prismo-db` inside docker container.
+### Configuration
 
-   ```bash
-   $ docker run -d --name prismo-db -e POSTGRES_PASSWORD=12345678 -e POSTGRES_DB=visitors -e POSTGRES_USER=prismo -p 5432:5432 -v $(pwd)/data:/var/lib/postgresql/data postgres
-   ```
+Config file name is `config.cfg`, the file located in the root directory of the project. Configs stored in YAML format.
 
-2. Let's connect to database
+```
+logging:
+    logfile: log.txt
+    logsize_kb: 1000
+    rolldepth: 3
+```
 
-   ```bash
-   $ $ docker exec -it prismo-db psql -h localhost -U prismo -d visitors
-   psql (12.2 (Debian 12.2-2.pgdg100+1))
-   Type "help" for help.
-   
-   visitors=# 
-   ```
+## Development
 
-3. Now you are in SQL console, basic commands are
+### Preconditions
 
-   ```
-   \? # Get help
-   \d # Describe table
-   \q # Quit psql
-   ```
+- Python 3.10+ with pip
+- git
+- supervisor(optional)
 
-4. Let's create table with users. Also we will create two columns with access to door and lathe.
+### Step-by-step installation
 
-   ```bash
-   visitors=# CREATE TABLE users ( id serial primary key, name text, key text, last_enter timestamp, door boolean, lathe boolean);
-   ```
-   ```bash
-   visitors=# CREATE TABLE logs(device_name text, key text, time integer);
-   ```
-   Alternatively you can add column to already existed table by command: `ALTER TABLE users ADD COLUMN last_enter TIMESTAMP;` 
+1. Clone the repository:
 
-5. Show contents of table:
+    ```sh
+    git clone git@github.com:hacklabkyiv/prismo.git
+    ```
+   or by https:
+    ```sh
+    git clone https://github.com/hacklabkyiv/prismo.git
+    ```
 
-   ```bash
-   # SELECT * FROM users;
-    id | name | key | door | lathe 
-   ----+------+-----+------+-------
-   (0 rows)
-   ```
+2. Install virtualenv in project's directory:
 
-6. Quit database with `\q`
+    ```sh
+    $ python3 -m venv ./virtualenv
+    ```
 
-If you want to stop docker container just run `docker stop prismo-db`, to start it again use `docker start prismo-db`
+3. Activate virtual environment
 
-## Installation
+    ```
+    source ./virtualenv/bin/activate
+    ```
 
-1. Install virtualenv in project's directory:
-	```sh
-    $ python3 -m venv ./virtualenv
-   ```
-   
-2. Activate virtual environment
-	
-   ```
-   source ./virtualenv/bin/activate
-   ```
-   
-3. Install required packages:
-
-  ```sh
-   $ pip3 install -r requirements.txt
-  ```
-
-4. Run app:
-  ```sh
-   $ export FLASK_APP=application.py 
-   $ flask run
-  ```
-  table.sql contains create statements for database tables
-
-Configuration
-=============
-
-Currently config is stored in YAML file. Example of config:
+4. Install required packages:
 
+    ```sh
+    pip3 install -r requirements.txt
+    ```
+
+5. Run for debugging and development: (it will reload app on code changes and enable debug mode)
+
+```sh
+export FLASK_APP=application.py
+flask run --debug
 ```
-# Example config file
-data:
-    user: prismo
-    password: 12345678 
-    host: localhost
-    port: 5432
-    name: visitors
-    latest-key-file: ./key.txt
-logging:
-    debug: Yes
-    logfile: log.txt
-    logsize_kb: 1000
-    rolldepth: 3
-```
 
-path to config file is set in `applicaiton.py`. By default, config file name is `config.cfg`
+By default, this should be run by Prismo admin process, but for debugging purpose you should run this commands by
+yourself.
+
+## Database
+
+All information about the database is stored in [doc/database.md](docs/database.md) file.
+
+### Logging
+
+All logs are stored in `log.txt` file.
+
+## API
+
+The docs for API is stored in [docs/api.md](docs/api.md) file.
 
+## Slack
+
+Slack integration works with slack bot. You need to create slack bot in your slack workspace and get token for it.
+Scope:
+
+- chat:write
+- files:write
+- incoming-webhook
+
+## Build docker image
+
+The main target platform is `linux/arm64/v8` (Raspberry Pi 4). To build docker image for this platform you should use
+buildx.
+
+Execute `docker login` with hacklabkyiv credentials.
+Execute this commands in the root directory of the project:
+
+```
+docker buildx create --use
+docker buildx build --platform linux/arm64/v8 -t hacklabkyiv/prismo-app:<version> --push .
+```
diff --git a/app/config.py b/app/config.py
new file mode 100644
index 0000000..97b9e6c
--- /dev/null
+++ b/app/config.py
@@ -0,0 +1,65 @@
+import json
+import logging
+import os
+import sys
+import secrets
+from pathlib import Path
+
+import yaml
+
+try:
+    from yaml import CLoader as Loader, CDumper
+except ImportError:
+    from yaml import Loader
+
+# Configuration file
+CONFIG_FILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../config.cfg')
+
+# Initial setup
+try:
+    cfg = yaml.load(open(CONFIG_FILE, 'r'), Loader=Loader)
+except IOError as e:
+    logging.error("Config file not found!")
+    logging.error("Exception: %s", str(e))
+    sys.exit(1)
+
+os.makedirs("external", exist_ok=True)
+database_file = Path("external/database.db")
+
+UPLOAD_FOLDER = '/uploads'
+
+internal_config_file = Path("external/internal_config.json")
+slat_key = 'slat'
+key_slack_token = 'key_slack_token'
+key_slack_backup_channel = 'key_slack_backup_channel'
+key_secret_key = 'key_secret_key'
+key_database_version = 'key_database_version'
+
+
+def get_setting(key: str):
+    with open(internal_config_file, 'r') as config_file:
+        config = json.load(config_file)
+        config_file.close()
+        return config.get(key, None)
+
+
+def set_setting(key: str, value: str):
+    with open(internal_config_file, 'r') as config_file:
+        config = json.load(config_file)
+        config_file.close()
+
+    config[key] = value
+
+    with open(internal_config_file, 'w') as config_file:
+        json.dump(config, config_file, indent=4)
+        config_file.close()
+
+
+def create_internal_config_file():
+    if not internal_config_file.is_file():
+        with open(internal_config_file, 'w') as config_file:
+            json.dump({
+                key_database_version: 1,
+                key_secret_key: secrets.token_hex(32),
+            }, config_file, indent=4)
+            config_file.close()
diff --git a/app/data/device_dto.py b/app/data/device_dto.py
new file mode 100644
index 0000000..c18e3f1
--- /dev/null
+++ b/app/data/device_dto.py
@@ -0,0 +1,11 @@
+from dataclasses import dataclass
+
+
+@dataclass
+class DeviceDto:
+    id: str
+    name: str
+
+    def __init__(self, id: str, name: str):
+        self.id = id
+        self.name = name
diff --git a/app/data/dtos.py b/app/data/dtos.py
new file mode 100644
index 0000000..fc457d6
--- /dev/null
+++ b/app/data/dtos.py
@@ -0,0 +1,21 @@
+from dataclasses import dataclass
+
+
+@dataclass
+class UserDto:
+    key: str
+    name: str
+
+    def __init__(self, user_key, user_name):
+        self.key = user_key
+        self.name = user_name
+
+
+@dataclass
+class OperationDto:
+    time: int
+    type: str
+
+    def __init__(self, operation_time, operation_type):
+        self.time = operation_time
+        self.type = operation_type
diff --git a/app/data/work_logs_repository.py b/app/data/work_logs_repository.py
new file mode 100644
index 0000000..0124ac4
--- /dev/null
+++ b/app/data/work_logs_repository.py
@@ -0,0 +1,84 @@
+from sqlite3 import Row
+
+from app.features.admin.init_app import get_db_connection
+
+
+def get_latest_key() -> str | None:
+    """
+    Get last triggered key, to add new users by clicking on any reader
+    """
+    connection = get_db_connection()
+    rows = (
+        connection.cursor()
+        .execute(
+            "SELECT user_key "
+            "FROM event_logs "
+            "WHERE user_key IS NOT NULL AND operation_type = 'deny_access' "
+            "ORDER BY operation_time DESC LIMIT 1")
+        .fetchone()
+    )
+
+    if rows is None:
+        return None
+
+    return rows[0]
+
+
+def query_event_logs(start_time=None, end_time=None, limit=100, offset=0):
+    """
+    Retrieve event logs from a SQLite database within a specified time range and limit the number
+    of results.
+
+    Args:
+        start_time (str, optional): The start time of the time range to filter the logs. Should
+be in the format 'YYYY-MM-DD HH:MM:SS'.
+        end_time (str, optional): The end time of the time range to filter the logs. Should be
+in the format 'YYYY-MM-DD HH:MM:SS'.
+        limit (int, optional): The maximum number of log entries to retrieve. Default is 100.
+        offset (int, optional): The offset from the beginning of the log entries to start
+retrieving results. Default is 0.
+
+    Returns:
+        list of dict: A list of dictionaries representing the retrieved log entries.
+        Each dictionary contains the following keys:
+            - 'name' (str): Username.
+            - 'key' (str): User key.
+            - 'device_name' (str): Device(Reader) name.
+            - 'device_id' (int): Device(Reader) ID.
+            - 'operation_type' (str): Type of operation.
+            - 'operation_time' (str): Time of the operation in 'YYYY-MM-DD HH:MM:SS' format.
+
+    Example:
+        Retrieve logs for a specific time range and limit the results
+
+        logs = query_event_logs(start_time='2023-01-01 00:00:00', end_time='2023-01-31 23:59:59',
+        limit=50)
+    """
+    connection = get_db_connection()
+    connection.row_factory = Row
+    cursor = connection.cursor()
+
+    query = """
+        SELECT u.name, u.key, d.name, d.id, operation_type, operation_time
+        FROM event_logs
+        LEFT JOIN users u ON event_logs.user_key = u.key
+        LEFT JOIN devices d ON d.id = event_logs.device_id
+    """
+
+    if start_time is not None and end_time is not None:
+        query += "WHERE operation_time >= ? AND operation_time <= ?"
+        cursor.execute(query + " ORDER BY operation_time DESC LIMIT ? OFFSET ?",
+                       (start_time, end_time, limit, offset))
+    else:
+        query += "ORDER BY operation_time DESC LIMIT ? OFFSET ?"
+        cursor.execute(query, (limit, offset))
+
+    results = cursor.fetchall()
+
+    # Convert the results to a list of dictionaries
+    result_dicts = [dict(row) for row in results]
+
+    # Don't forget to close the cursor and the connection when done
+    cursor.close()
+
+    return result_dicts
diff --git a/app/features/admin/admin_routrers.py b/app/features/admin/admin_routrers.py
new file mode 100644
index 0000000..77edf8c
--- /dev/null
+++ b/app/features/admin/admin_routrers.py
@@ -0,0 +1,48 @@
+import flask
+import flask_login
+from flask import Blueprint, render_template, request
+
+from app.features.admin.admins_repository import get_flask_admin_user_by_credentials
+from app.features.admin.init_app import init_app
+
+admin_blue_print = Blueprint('admin', __name__)
+
+
+@admin_blue_print.route('/init_app', methods=['GET', 'POST'])
+def init_app_route():
+    if request.method == 'GET':
+        return render_template('init_app.html')
+
+    username = request.form['username']
+    password = request.form['password']
+    slat = flask.request.form['slat']
+    if 'file' in flask.request.files:
+        file = request.files['file']
+    else:
+        file = None
+
+    init_app(username, password, slat, file)
+
+    return flask.redirect(flask.url_for('admin.login'))
+
+
+@admin_blue_print.route('/login', methods=['GET', 'POST'])
+def login():
+    if flask.request.method == 'GET':
+        return render_template('login.html')
+
+    username = flask.request.form['username']
+    password = flask.request.form['password']
+    flask_admin_user = get_flask_admin_user_by_credentials(username, password)
+
+    if flask_admin_user is None:
+        return "Bad login"
+
+    flask_login.login_user(flask_admin_user)
+    return flask.redirect(flask.url_for('access_panel'))
+
+
+@admin_blue_print.route('/logout')
+def logout():
+    flask_login.logout_user()
+    return flask.redirect(flask.url_for('admin.login'))
diff --git a/app/features/admin/admins_repository.py b/app/features/admin/admins_repository.py
new file mode 100644
index 0000000..aca8d24
--- /dev/null
+++ b/app/features/admin/admins_repository.py
@@ -0,0 +1,92 @@
+# pylint: disable=attribute-defined-outside-init
+import flask_login
+
+from app.features.admin.init_app import get_db_connection
+from app.features.admin.password import hash_password
+
+
+class FlaskAdminUser(flask_login.UserMixin):
+    pass
+
+
+class AdminUser:
+    id: int
+    username: str
+    password: str
+
+    def __init__(self, id, username, password):
+        self.id = id
+        self.username = username
+        self.password = password
+
+
+def get_flask_admin_user_by_credentials(username: str, password: str) -> FlaskAdminUser | None:
+    connection = get_db_connection()
+    rows = connection.execute(
+        "SELECT id, password FROM admins WHERE username=?", (username,)
+    ).fetchall()
+
+    if len(rows) == 0:
+        return None
+
+    db_admin_id, db_password = rows[0]
+    if db_password == hash_password(password):
+        user = FlaskAdminUser()
+        user.id = db_admin_id
+        return user
+
+    return None
+
+
+def get_flask_admin_user_by_id(user_id: str) -> FlaskAdminUser | None:
+    user = get_admin_user_by_id(user_id)
+    if user is None:
+        return None
+
+    flask_user = FlaskAdminUser()
+    flask_user.id = user.id
+    return flask_user
+
+
+def get_flask_admin_user_by_user_name(user_name: str) -> FlaskAdminUser | None:
+    user = get_admin_user_by_user_name(user_name)
+    if user is None:
+        return None
+
+    flask_user = FlaskAdminUser()
+    flask_user.id = user.id
+    return flask_user
+
+
+def get_admin_user_by_flask_user(flask_user) -> AdminUser | None:
+    if flask_user.is_anonymous:
+        return None
+    return get_admin_user_by_id(flask_user.id)
+
+
+def get_admin_user_by_id(user_id: str) -> AdminUser | None:
+    connection = get_db_connection()
+    rows = connection.execute(
+        "SELECT id, username, password FROM admins WHERE id=?", (user_id,)
+    ).fetchall()
+
+    if len(rows) == 0:
+        return None
+
+    admin_id, username, password = rows[0]
+    user = AdminUser(admin_id, username, password)
+    return user
+
+
+def get_admin_user_by_user_name(user_name: str) -> AdminUser | None:
+    connection = get_db_connection()
+    rows = connection.execute(
+        "SELECT id, username, password FROM admins WHERE username=?", (user_name,)
+    ).fetchall()
+
+    if len(rows) == 0:
+        return None
+
+    admin_id, username, password = rows[0]
+    user = AdminUser(admin_id, username, password)
+    return user
diff --git a/app/features/admin/init_app.py b/app/features/admin/init_app.py
new file mode 100644
index 0000000..c48841b
--- /dev/null
+++ b/app/features/admin/init_app.py
@@ -0,0 +1,105 @@
+import os
+import sqlite3
+
+from werkzeug.utils import secure_filename
+
+from app.config import database_file, internal_config_file, slat_key, set_setting
+from app.features.admin.password import hash_password
+
+_database_connection = None
+
+
+def is_app_inited():
+    return database_file.is_file() and internal_config_file.is_file()
+
+
+def init_app(admin_username: str, admin_password: str, slat: str, file):
+    if is_app_inited():
+        raise Exception("App already initialized")
+
+    set_setting(slat_key, slat)
+
+    if (file is not None) and (not file.filename == ''):
+        init_database_from_backup(admin_username, admin_password, file)
+    else:
+        init_database(admin_username, admin_password)
+
+
+def init_database_from_backup(admin_username: str, admin_password: str, file):
+    if file.filename == '':
+        raise Exception("Invalid file name")
+
+    filename = secure_filename(file.filename)
+    if not filename.endswith(".db"):
+        raise Exception("Invalid file extension")
+    file.save(database_file)
+
+    verify_is_secrets_from_backup(admin_password, admin_username)
+
+
+def verify_is_secrets_from_backup(admin_password, admin_username):
+    cursor = get_db_connection().cursor()
+    rows = cursor.execute(
+        "SELECT * FROM admins where username = ? and password= ?",
+        (admin_username, hash_password(admin_password))
+    ).fetchone()
+    if (rows is None) or (len(rows) == 0):
+        os.remove(database_file)
+        raise Exception("Invalid admin credentials")
+
+
+def init_database(admin_username: str, admin_password: str):
+    sqlite3.connect(database_file)
+    connection = get_db_connection()
+    connection.executescript("""
+    CREATE TABLE admins(
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        username TEXT NOT NULL,
+        password TEXT NOT NULL
+    );
+
+    CREATE TABLE users(
+        name TEXT NOT NULL,
+        key  TEXT NOT NULL,
+        slack_id TEXT DEFAULT NULL
+    );
+
+    CREATE TABLE devices(
+        id   TEXT NOT NULL,
+        name TEXT NOT NULL,
+        slack_channel_id TEXT DEFAULT NULL
+    );
+
+    CREATE TABLE permissions(
+        device_id TEXT NOT NULL,
+        user_key  TEXT NOT NULL
+    );
+
+    CREATE TABLE event_logs(
+        device_id  TEXT NOT NULL,
+        user_key   TEXT,
+        operation_type TEXT NOT NULL,
+        operation_time       TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
+    );
+    """)
+    connection.commit()
+
+    connection = get_db_connection()
+    connection.execute(
+        "INSERT INTO admins(username, password) VALUES (?, ?)",
+        (admin_username, hash_password(admin_password))
+    )
+    connection.commit()
+
+
+def get_db_connection():
+    # pylint: disable=global-statement
+    if not is_app_inited():
+        return None
+
+    global _database_connection
+
+    if not _database_connection:
+        _database_connection = sqlite3.connect(database_file, check_same_thread=False)
+        _database_connection.row_factory = sqlite3.Row
+    return _database_connection
diff --git a/app/features/admin/password.py b/app/features/admin/password.py
new file mode 100644
index 0000000..e5f0e91
--- /dev/null
+++ b/app/features/admin/password.py
@@ -0,0 +1,9 @@
+import hashlib
+
+from app.config import get_setting, slat_key
+
+
+def hash_password(password: str):
+    final_slat = get_setting(slat_key) + str(len(password))
+    hashed_pass = hashlib.sha256((password + final_slat).encode('utf-8')).hexdigest()
+    return hashed_pass
diff --git a/app/features/backup_database.py b/app/features/backup_database.py
new file mode 100644
index 0000000..e51c34c
--- /dev/null
+++ b/app/features/backup_database.py
@@ -0,0 +1,24 @@
+import logging
+
+from slack_sdk import WebClient
+
+from app.config import get_setting, key_slack_token, key_slack_backup_channel, database_file
+
+
+def backup_data_base():
+    slack_token = get_setting(key_slack_token)
+    channel_id = get_setting(key_slack_backup_channel)
+
+    if (slack_token is None) or (channel_id is None):
+        return
+
+    client = WebClient(token=slack_token)
+
+    response = client.files_upload_v2(
+        channel=channel_id,
+        file=open(database_file, "rb").read(),
+        title="Database backup",
+        initial_comment="Here is the latest version of the database.",
+    )
+
+    logging.info(response)
diff --git a/app/features/permissions/access_pannel.py b/app/features/permissions/access_pannel.py
new file mode 100644
index 0000000..0fd4875
--- /dev/null
+++ b/app/features/permissions/access_pannel.py
@@ -0,0 +1,66 @@
+from dataclasses import dataclass
+from typing import List
+
+from app.features.readers.device_repository import get_all_devices
+from app.data.dtos import UserDto
+from app.features.users.user_repository import get_all_users
+from app.features.permissions.permissions_repository import get_user_permissions
+
+
+@dataclass
+class PermissionUiModel:
+    is_granted: bool
+    device_key: str
+
+    def __init__(self, is_granted, device_key):
+        self.is_granted = is_granted
+        self.device_key = device_key
+
+
+@dataclass
+class AccessControlPanelRow:
+    user_name: str
+    user_key: str
+    permissions: List[PermissionUiModel]
+
+    def __init__(self, user_name, user_key, permissions):
+        self.user_name = user_name
+        self.user_key = user_key
+        self.permissions = permissions
+
+
+@dataclass
+class AccessControlPanel:
+    header: List[str]
+    rows: List[AccessControlPanelRow]
+
+    def __init__(self, header, rows):
+        self.header = header
+        self.rows = rows
+
+
+def get_access_control_panel() -> AccessControlPanel:
+    users = get_all_users()
+    devices = get_all_devices()
+
+    header = ['User name', 'User key']
+    for device in devices:
+        header.append(device.name)
+
+    rows = []
+
+    for user in users:
+        access_control_panel_row = build_user_access_control_permissions_row(user, devices)
+        rows.append(access_control_panel_row)
+
+    return AccessControlPanel(header, rows)
+
+
+def build_user_access_control_permissions_row(user: UserDto, devices):
+    grated_user_device = get_user_permissions(user.key)
+    user_permissions_ui_models = []
+    for device in devices:
+        permission_model = PermissionUiModel(device.id in grated_user_device, device.id)
+        user_permissions_ui_models.append(permission_model)
+
+    return AccessControlPanelRow(user.name, user.key, user_permissions_ui_models)
diff --git a/app/features/permissions/permission_routers.py b/app/features/permissions/permission_routers.py
new file mode 100644
index 0000000..ae65599
--- /dev/null
+++ b/app/features/permissions/permission_routers.py
@@ -0,0 +1,24 @@
+import flask_login
+from flask import Blueprint, request
+
+from app.features.permissions.permissions_repository import grant_permission, reject_permission
+
+permissions_blue_print = Blueprint('permission', __name__, url_prefix='/permission')
+
+
+@permissions_blue_print.route('', methods=['POST'])
+@flask_login.login_required
+def grant_permission_route():
+    user_key = request.form['user_key']
+    permission = request.form['device_id']
+    grant_permission(user_key, permission)
+    return 'OK'
+
+
+@permissions_blue_print.route('', methods=['DELETE'])
+@flask_login.login_required
+def reject_permission_route():
+    user_key = request.form['user_key']
+    permission = request.form['device_id']
+    reject_permission(user_key, permission)
+    return 'OK'
diff --git a/app/features/permissions/permissions_repository.py b/app/features/permissions/permissions_repository.py
new file mode 100644
index 0000000..a8636a1
--- /dev/null
+++ b/app/features/permissions/permissions_repository.py
@@ -0,0 +1,61 @@
+import logging
+from dataclasses import dataclass
+from typing import List
+
+from app.features.admin.init_app import get_db_connection
+
+
+@dataclass
+class UserPermission:
+    user_key: str
+    permissions: List[str]
+
+    def __init__(self, user_key, permissions):
+        self.user_key = user_key
+        self.permissions = permissions
+
+
+def get_user_permissions(user_key) -> List[str]:
+    connection = get_db_connection()
+    rows = connection.cursor().execute(
+        "SELECT device_id FROM permissions WHERE user_key = ?", (user_key,)
+    ).fetchall()
+
+    user_permissions = []
+    for row in rows:
+        key, = row
+        user_permissions.append(key)
+
+    logging.info('user with id %s, permissions: %s', user_key, user_permissions)
+    return user_permissions
+
+
+def get_user_with_permission_to_device(device_id):
+    connection = get_db_connection()
+    rows = connection.cursor().execute(
+        "SELECT user_key FROM permissions WHERE device_id = ?", (device_id,)
+    ).fetchall()
+    users = []
+    for row in rows:
+        key, = row
+        users.append(key)
+
+    return users
+
+
+def grant_permission(user_key, device_id):
+    connection = get_db_connection()
+    connection.cursor().execute(
+        "INSERT INTO permissions(user_key, device_id) VALUES (?, ?)", (user_key, device_id)
+    )
+    logging.info('Grant permission for user with id %s to device %s', user_key, device_id)
+    connection.commit()
+
+
+def reject_permission(user_key, device_id):
+    connection = get_db_connection()
+    connection.cursor().execute(
+        "delete from permissions where user_key=? and device_id=?", (user_key, device_id)
+    )
+    connection.commit()
+    logging.info('Reject permission for user with id %s to device %s', user_key, device_id)
diff --git a/app/features/readers/device_repository.py b/app/features/readers/device_repository.py
new file mode 100644
index 0000000..ecd71bd
--- /dev/null
+++ b/app/features/readers/device_repository.py
@@ -0,0 +1,75 @@
+import logging
+from dataclasses import dataclass
+from typing import List
+
+from app.data.device_dto import DeviceDto
+from app.data.dtos import UserDto, OperationDto
+from app.features.admin.init_app import get_db_connection
+
+
+def get_all_devices() -> list[DeviceDto]:
+    connection = get_db_connection()
+    rows = connection.cursor().execute("SELECT id, name FROM devices order by name").fetchall()
+
+    devices = []
+    for row in rows:
+        device_id, device_name = row
+        device = DeviceDto(device_id, device_name)
+        devices.append(device)
+
+    return devices
+
+
+def get_full_device(device_id):
+    connection = get_db_connection()
+    row = connection.cursor().execute("SELECT id, name, slack_channel_id FROM devices WHERE id=?",
+                                      (device_id,)).fetchall()
+
+    if len(row) == 0:
+        return None
+
+    device_id, device_name, slack_channel_id = row[0]
+
+    rows = connection.cursor().execute(
+        "SELECT u.key, u.name, operation_type, operation_time "
+        "FROM event_logs "
+        "join users u on u.key = user_key "
+        "WHERE device_id=? order by operation_time desc", (device_id,)
+    ).fetchall()
+
+    logs = []
+    for row in rows:
+        user_key, user_name, operation_type, operation_time = row
+        user = UserDto(user_key, user_name)
+        operation = OperationDto(operation_time, operation_type)
+
+        logs.append({
+            "user": user,
+            "operation": operation,
+        })
+
+    rows = connection.cursor().execute(
+        "SELECT u.key, u.name FROM permissions JOIN users u ON u.key = permissions.user_key WHERE "
+        "permissions.device_id=?", (device_id,)
+    ).fetchall()
+    user_with_access = []
+    for row in rows:
+        user_key, user_name = row
+        user_with_access.append(UserDto(user_key, user_name))
+
+    return {
+        "device": {
+            "name": device_name,
+            "id": device_id,
+            "slack_channel_id": slack_channel_id,
+        },
+        "logs": logs,
+        "user_with_access": user_with_access,
+    }
+
+
+def add_device(device_id, device_name):
+    connection = get_db_connection()
+    connection.execute("INSERT INTO devices(id, name) VALUES(?,?)", (device_id, device_name))
+    logging.info('Device added: %s, %s', device_id, device_name)
+    connection.commit()
diff --git a/app/features/readers/manage_device.py b/app/features/readers/manage_device.py
new file mode 100644
index 0000000..cb744ce
--- /dev/null
+++ b/app/features/readers/manage_device.py
@@ -0,0 +1,52 @@
+import flask_login
+from flask import Blueprint, render_template, request
+
+from app.features.admin.init_app import get_db_connection
+from app.features.readers.device_repository import get_all_devices, get_full_device, add_device
+from app.features.slack_notifier import send_channel_message
+
+manage_device_blue_print = Blueprint('manage_device', __name__)
+
+
+@manage_device_blue_print.route('/devices')
+def devices():
+    return render_template('devices.html', devices=get_all_devices())
+
+
+@manage_device_blue_print.route("/device/<device_id>", methods=["GET", "POST"])
+def device_page(device_id):
+    if request.method == 'POST':
+        slack_channel_id = request.form['slack_channel_id']
+
+        connection = get_db_connection()
+        connection.execute(
+            "UPDATE devices SET slack_channel_id=? WHERE id=?",
+            (slack_channel_id, device_id)
+        )
+        connection.commit()
+    return render_template("device_page.html", full_device=get_full_device(device_id))
+
+
+@manage_device_blue_print.route('/device', methods=['POST'])
+@flask_login.login_required
+def add_device_route():
+    device_id = request.form['device_id']
+    device_name = request.form['device_name']
+    add_device(device_id, device_name)
+    return 'OK'
+
+
+@manage_device_blue_print.route('/send-test-message-to-channel', methods=['POST'])
+@flask_login.login_required
+def send_test_message_to_dm():
+    json = request.json
+    device_id = json['device_id']
+    cursor = get_db_connection().cursor()
+    cursor.execute("SELECT slack_channel_id, name FROM devices WHERE id=?", (device_id,))
+    slack_channel_id, name, = cursor.fetchall()[0]
+    if slack_channel_id is None:
+        return 'No slack channel id for device ' + device_id
+
+    send_channel_message(slack_channel_id, "Test message for device " + name)
+
+    return 'OK'
diff --git a/app/features/readers/reader_routers.py b/app/features/readers/reader_routers.py
new file mode 100644
index 0000000..2e861c4
--- /dev/null
+++ b/app/features/readers/reader_routers.py
@@ -0,0 +1,124 @@
+import logging
+
+from flask import Blueprint, request
+
+from app.features.admin.init_app import get_db_connection
+from app.features.permissions.permissions_repository import (
+    get_user_with_permission_to_device,
+)
+from app.features.slack_notifier import send_dm_message, send_channel_message
+
+import json
+
+reader_blue_print = Blueprint("reader", __name__, url_prefix="/reader")
+
+
+@reader_blue_print.route("/<device_id>/accesses/", methods=["GET"])
+def accesses(device_id):
+    return {"keys": get_user_with_permission_to_device(device_id)}
+
+
+@reader_blue_print.route("/<device_id>/log_operation", methods=["POST"])
+def log_operation(device_id):
+    json_data = json.loads(request.get_json())
+
+    logging.info("Received request: %s", str(json_data))
+
+    operation = json_data["operation"]
+    if operation not in ["lock", "unlock", "deny_access"]:
+        raise Exception("Invalid operation")
+
+    try:
+        user_key = json_data["key"]
+    except KeyError:
+        user_key = None
+
+    if (operation == "unlock") and user_key is None:
+        raise Exception("Invalid operation")
+
+    if operation == "unlock":
+        send_log_of_last_usage(device_id, user_key)
+        send_message_of_unlocking(device_id, user_key)
+
+    if operation == "lock":
+        send_message_of_locking(device_id)
+
+    connection = get_db_connection()
+    connection.execute(
+        "INSERT INTO event_logs(device_id, user_key, operation_type) VALUES (?, ?, ?)",
+        (device_id, user_key, operation),
+    )
+    connection.commit()
+    return "OK", 201
+
+
+def send_message_of_locking(device_id):
+    cursor = get_db_connection().cursor()
+    cursor.execute(
+        "SELECT slack_channel_id, name FROM devices WHERE id=?", (device_id,)
+    )
+    try:
+        slack_channel_id, device_name, = cursor.fetchall()[0]
+        if slack_channel_id is None:
+            raise Exception("No slack channel id for device " + device_id)
+
+        message = f"The {device_name} is free now"
+
+        send_channel_message(slack_channel_id, message)
+    except Exception as err:
+        print("Cannot send slack message.")
+        print(f"Unexpected {err=}, {type(err)=}")
+        # raise
+
+
+def send_message_of_unlocking(device_id, user_key):
+    cursor = get_db_connection().cursor()
+    cursor.execute(
+        "SELECT slack_channel_id, name FROM devices WHERE id=?", (device_id,)
+    )
+    try:
+        slack_channel_id, device_name, = cursor.fetchall()[0]
+        if slack_channel_id is None:
+            raise Exception("No slack channel id for device " + device_id)
+
+        user_cursor = get_db_connection().cursor()
+        user_cursor.execute("SELECT slack_id, name FROM users WHERE key=?", (user_key,))
+        slack_id, user_name = user_cursor.fetchall()[0]
+        if slack_id is None:
+            message = f"{user_name} start using the {device_name}"
+        else:
+            message = f"<@{slack_id}> start using the {device_name}"
+
+        send_channel_message(slack_channel_id, message)
+    except Exception as err:
+        print("Cannot send slack message.")
+        print(f"Unexpected {err=}, {type(err)=}")
+        # raise
+
+
+def send_log_of_last_usage(device_id, user_key):
+    cursor = get_db_connection().cursor()
+    cursor.execute(
+        "SELECT slack_id, name, operation_time "
+        "FROM event_logs "
+        "JOIN users user ON event_logs.user_key = user.key "
+        "WHERE device_id = ? and operation_type = 'unlock' "
+        "ORDER BY operation_time DESC "
+        "LIMIT 3",
+        (device_id,),
+    )
+    rows = cursor.fetchall()
+    message = "The last 3 people who unlocked the door were: \n"
+    try:
+        for row in rows:
+            slack_id, name, operation_time = row
+            if slack_id is None:
+                message += f"{name} at {operation_time}\n"
+            else:
+                message += f"<@{slack_id}> at {operation_time}\n"
+
+        send_dm_message(user_key, message)
+    except Exception as err:
+        print("Cannot send slack message.")
+        print(f"Unexpected {err=}, {type(err)=}")
+        # raise
diff --git a/app/features/slack_notifier.py b/app/features/slack_notifier.py
new file mode 100644
index 0000000..1e5d4fd
--- /dev/null
+++ b/app/features/slack_notifier.py
@@ -0,0 +1,44 @@
+import logging
+
+from slack_sdk import WebClient
+
+from app.config import get_setting, key_slack_token
+from app.features.admin.init_app import get_db_connection
+
+
+def send_dm_message(user_key, message):
+    slack_token = get_setting(key_slack_token)
+    connection = get_db_connection()
+    row = connection.cursor().execute("SELECT slack_id FROM users WHERE key=?",
+                                      (user_key,)).fetchall()
+    if len(row) != 1:
+        raise Exception('No user with such key')
+    slack_id = row[0][0]
+    print("slack id", slack_id)
+
+    if (slack_token is None) or (slack_id is None):
+        return
+
+    client = WebClient(token=slack_token)
+
+    response = client.chat_postMessage(
+        channel=slack_id,
+        text=message,
+    )
+
+    logging.info(response)
+
+
+def send_channel_message(channel_id, message):
+    slack_token = get_setting(key_slack_token)
+    if (slack_token is None) or (channel_id is None):
+        return
+
+    client = WebClient(token=slack_token)
+
+    response = client.chat_postMessage(
+        channel=channel_id,
+        text=message,
+    )
+
+    logging.info(response)
diff --git a/app/features/users/user_repository.py b/app/features/users/user_repository.py
new file mode 100644
index 0000000..fc9280c
--- /dev/null
+++ b/app/features/users/user_repository.py
@@ -0,0 +1,85 @@
+import logging
+
+from app.data.device_dto import DeviceDto
+from app.data.dtos import UserDto, OperationDto
+from app.features.admin.init_app import get_db_connection
+
+
+def get_full_user(user_key):
+    connection = get_db_connection()
+    row = connection.execute(
+        "SELECT key, name, slack_id FROM users WHERE key=?", (user_key,)
+    ).fetchall()
+
+    if len(row) == 0:
+        return None
+
+    user_key, user_name, slack_id = row[0]
+
+    rows = connection.execute(
+        "SELECT d.id, d.name FROM permissions JOIN devices d ON d.id = permissions.device_id "
+        "WHERE permissions.user_key = ?", (user_key,)
+    ).fetchall()
+
+    user_devices = []
+    for row in rows:
+        device_id, device_name = row
+        user_devices.append(DeviceDto(device_name, device_id))
+
+    rows = connection.execute(
+        "select d.id, d.name, operation_type, operation_time from event_logs "
+        "join devices d on d.id = event_logs.device_id "
+        "WHERE event_logs.user_key=? "
+        "ORDER BY operation_time DESC", (user_key,)
+    ).fetchall()
+
+    user_logs = []
+    for row in rows:
+        device_id, device_name, operation_type, operation_time = row
+        device = DeviceDto(device_id, device_name)
+
+        log = {
+            "device": device,
+            "operation": OperationDto(operation_time, operation_type),
+        }
+
+        user_logs.append(log)
+
+    return {
+        "user": {
+            "key": user_key,
+            "name": user_name,
+            "slack_id": slack_id
+        },
+        "logs": user_logs,
+        "devices": user_devices
+    }
+
+
+def delete_user(user_key):
+    connection = get_db_connection()
+    connection.execute("DELETE FROM users WHERE key=?", (user_key,))
+    connection.commit()
+    logging.info('User with id %s was deleted', user_key)
+
+
+def add_user(user_name, user_key):
+    connection = get_db_connection()
+    connection.execute("INSERT INTO users(name, key) VALUES(?,?)", (user_name, user_key))
+    logging.info('User added: %s, %s', user_name, user_key)
+    connection.commit()
+
+
+def get_all_users() -> list[UserDto]:
+    connection = get_db_connection()
+    cursor = connection.cursor()
+    cursor.execute("SELECT key, name FROM users")
+    rows = cursor.fetchall()
+
+    users = []
+    for row in rows:
+        key, name = row
+        user = UserDto(key, name)
+        users.append(user)
+
+    return users
diff --git a/app/features/users/user_routers.py b/app/features/users/user_routers.py
new file mode 100644
index 0000000..57cb519
--- /dev/null
+++ b/app/features/users/user_routers.py
@@ -0,0 +1,52 @@
+import flask_login
+from flask import (
+    Blueprint, render_template
+)
+from flask import request
+
+from app.features.admin.init_app import get_db_connection
+from app.features.slack_notifier import send_dm_message
+from app.features.users.user_repository import delete_user, add_user, get_full_user
+
+user_blue_print = Blueprint('users', __name__, url_prefix='/user')
+
+
+@user_blue_print.route('/', methods=['POST'])
+@flask_login.login_required
+def add_user_route():
+    user_name = request.form['nick']
+    user_key = request.form['key']
+    add_user(user_name, user_key)
+    return 'OK'
+
+
+@user_blue_print.route('/', methods=['DELETE'])
+@flask_login.login_required
+def delete_user_route():
+    user_key = request.form['user_key']
+    delete_user(user_key)
+    return 'OK'
+
+
+@user_blue_print.route('/<user_key>', methods=['GET', 'POST'])
+def user_page(user_key):
+    if request.method == 'POST':
+        slack_id = request.form['slack_id']
+
+        connection = get_db_connection()
+        connection.execute(
+            "UPDATE users SET slack_id=? WHERE key=?",
+            (slack_id, user_key)
+        )
+        connection.commit()
+
+    return render_template("user_page.html", full_user=get_full_user(user_key))
+
+
+@user_blue_print.route('/send-test-message-to-user', methods=['POST'])
+@flask_login.login_required
+def send_test_message_to_dm():
+    json = request.json
+    user_key = json['user_key']
+    send_dm_message(user_key, 'Test message')
+    return 'OK'
diff --git a/app/routers/settings_routers.py b/app/routers/settings_routers.py
new file mode 100644
index 0000000..84b4505
--- /dev/null
+++ b/app/routers/settings_routers.py
@@ -0,0 +1,41 @@
+import flask_login
+from flask import Blueprint, request, render_template
+
+from app.config import set_setting, key_slack_token, key_slack_backup_channel, get_setting
+from app.features.backup_database import backup_data_base
+
+settings_blue_print = Blueprint('settings', __name__)
+
+
+@settings_blue_print.route('/settings', methods=['GET', 'POST'])
+@flask_login.login_required
+def index():
+    if request.method == 'POST':
+        slack_token = request.form.get('slack_token')
+        channel_id = request.form.get('channel_id')
+
+        if slack_token is not None:
+            set_setting(key_slack_token, slack_token)
+
+        if channel_id is not None:
+            set_setting(key_slack_backup_channel, channel_id)
+
+    settings = {}
+    saved_slack_token = get_setting(key_slack_token)
+
+    if saved_slack_token is not None:
+        settings['slack_token'] = get_setting(key_slack_token)
+
+    saved_channel_id = get_setting(key_slack_backup_channel)
+    if saved_channel_id is not None:
+        settings['channel_id'] = get_setting(key_slack_backup_channel)
+
+    return render_template('settings.html', settings=settings)
+
+
+@settings_blue_print.route('/send-backup-to-slack', methods=['POST'])
+@flask_login.login_required
+def send_backup_to_slack():
+    print("Sending backup to slack")
+    backup_data_base()
+    return 'OK'
diff --git a/app/utils/fimware_updater.py b/app/utils/fimware_updater.py
new file mode 100644
index 0000000..af09618
--- /dev/null
+++ b/app/utils/fimware_updater.py
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+
+# Disable pylint for the file
+# pylint: disable=all
+
+"""Reader firmware update tool. It erases memory, flashes Micropython and
+working scripts itself to connected device.
+
+@author: Artem Synytsyn <a.synytsyn@gmail.com>
+"""
+from flask import Flask, render_template
+from flask_sock import Sock
+import subprocess
+import shlex
+import pathlib
+
+# TODO: Read all these pathes from config file
+ESPTOOL_PATH = "/home/artsin/Dev/esptool/esptool.py"
+AMPY_PATH = "/home/artsin/.local/bin/ampy"
+MICROPYTHON_DISTRO_PATH = "/home/artsin/Downloads/ESP32_GENERIC-20231005-v1.21.0.bin"
+
+READER_FW_PATH = "/home/artsin/Dev/prismo-reader/src/"
+
+command_erase = ["python3", ESPTOOL_PATH, "erase_flash"]
+command_flash_micropython = [
+    "python3",
+    ESPTOOL_PATH,
+    "--baud",
+    "460800",
+    "write_flash",
+    "-z",
+    "0x1000",
+    MICROPYTHON_DISTRO_PATH,
+]
+# TODO: Serial port autodetect
+commands_upload_scripts = [AMPY_PATH, "-p", "/dev/ttyUSB0", "put"]
+
+
+def erase_flash(socket):
+    process = subprocess.Popen(command_erase, stdout=subprocess.PIPE)
+    while True:
+        output = process.stdout.readline()
+        if output == "" and process.poll() is not None:
+            break
+        if output:
+            data = output.strip()
+            print(data)
+            socket.send(str(data))
+            if b"Hard resetting via RTS pin" in data:
+                return True
+    return False
+
+
+def flash_micropython_binary(socket):
+    process = subprocess.Popen(command_flash_micropython, stdout=subprocess.PIPE)
+    while True:
+        output = process.stdout.readline()
+        if output == "" and process.poll() is not None:
+            break
+        if output:
+            data = output.strip()
+            print(data)
+            socket.send(str(data))
+            if b"Hard resetting via RTS pin" in data:
+                return True
+    return False
+
+
+def upload_python_scripts(socket):
+    firmware_files = [f for f in pathlib.Path(READER_FW_PATH).iterdir() if f.is_file()]
+    for file in firmware_files:
+        socket.send("Download: " + str(file.name))
+        # Make a copy, since we do not want to change original command template
+        command = commands_upload_scripts[:]
+        command.append(str(file))
+        process = subprocess.Popen(command, stdout=subprocess.PIPE)
+        process.wait()
+        socket.send("OK")
+    return True
+
+
+def update_firmware_full(socket):
+    socket.send("--------- FLASH ERASE ----------------")
+    result = erase_flash(socket)
+    print("Erase flash result: ", result)
+    socket.send("------- MICROPYTHON INSTALL ----------")
+    result = flash_micropython_binary(socket)
+    print("upload script")
+    socket.send("----------- UPLOAD FIRMWARE ----------")
+    upload_python_scripts(socket)
+    socket.send("----------- DONE! ----------")
diff --git a/application.py b/application.py
index 962cf57..cb2f87c 100755
--- a/application.py
+++ b/application.py
@@ -1,170 +1,153 @@
-#!/usr/bin/python
-# -*- coding: utf-8 -*-
-
-"""
-Core of Hacklab Admin Panel
-
-@author: Artem Synytsyn
-"""
-
-from flask import Flask, render_template, request, abort
-import time
-import json
-import yaml
-import sys
-import os
 import logging
+import threading
+import time
 from logging.handlers import RotatingFileHandler
-from collections import namedtuple
-from threading import Thread
-from os.path import getmtime
-import datetime
-import psycopg2 as psycopg
-import requests
-import txt_log_reader
-try:
-    from yaml import CLoader as Loader, CDumper
-except ImportError:
-    from yaml import Loader
-
-# Configuration file
-CONFIG_FILE = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'config.cfg')
-
-# Initial setup
-try:
-    cfg = yaml.load(open(CONFIG_FILE, 'r'), Loader=Loader)
-except IOError as e:
-    logger.error("Config file not found!")
-    logger.error("Exception: %s" % str(e))
-    sys.exit(1)
-
-LATEST_KEY_FILE = cfg['data']['latest-key-file']
+
+import flask
+import flask_login
+import schedule
+from flask import Flask, render_template
+from flask_login import LoginManager
+from flask_sock import Sock
+from flask import jsonify, request
+
+from app.config import cfg, UPLOAD_FOLDER, get_setting, key_secret_key, set_setting, \
+    create_internal_config_file
+from app.data.work_logs_repository import get_latest_key
+from app.features.admin.admin_routrers import admin_blue_print
+from app.features.admin.admins_repository import get_admin_user_by_flask_user, \
+    get_flask_admin_user_by_id, \
+    get_flask_admin_user_by_user_name
+from app.features.admin.init_app import database_file
+from app.features.backup_database import backup_data_base
+from app.features.permissions.access_pannel import get_access_control_panel
+from app.features.permissions.permission_routers import permissions_blue_print
+from app.features.readers.manage_device import manage_device_blue_print
+from app.features.readers.reader_routers import reader_blue_print
+from app.routers.settings_routers import settings_blue_print
+from app.features.users.user_routers import user_blue_print
+from app.utils.fimware_updater import update_firmware_full
+
+from app.data.work_logs_repository import query_event_logs
 
 app = Flask(__name__)
-app.config['SECRET_KEY'] = 'secret!'
+
+create_internal_config_file()
+
+secret_key = get_setting(key_secret_key)
+
+app.config['SECRET_KEY'] = secret_key
+websocket = Sock(app)
 logger = logging.getLogger(__name__)
-if cfg['logging']['debug'] is True:
-    app.config['DEBUG'] = True
-    logging.basicConfig(level=logging.DEBUG)
-    # Create logger to be able to use rolling logs
-    logger.setLevel(logging.DEBUG)
-    log_handler = RotatingFileHandler(cfg['logging']['logfile'],mode='a',
-                                      maxBytes=int(cfg['logging']['logsize_kb'])*1024,
-                                      backupCount=int(cfg['logging']['rolldepth']),
-                                      delay=0)
-    formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
-    log_handler.setFormatter(formatter)
-    logger.addHandler(log_handler)
-
-def get_latest_key_info():
+
+app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
+
+login_manager = LoginManager()
+login_manager.init_app(app)
+
+logging.basicConfig(level=logging.DEBUG)
+# Create logger to be able to use rolling logs
+logger.setLevel(logging.DEBUG)
+log_handler = RotatingFileHandler(cfg['logging']['logfile'], mode='a',
+                                  maxBytes=int(cfg['logging']['logsize_kb']) * 1024,
+                                  backupCount=int(cfg['logging']['rolldepth']),
+                                  delay=0)
+formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+log_handler.setFormatter(formatter)
+logger.addHandler(log_handler)
+
+app.register_blueprint(reader_blue_print)
+app.register_blueprint(permissions_blue_print)
+app.register_blueprint(user_blue_print)
+app.register_blueprint(admin_blue_print)
+app.register_blueprint(settings_blue_print)
+app.register_blueprint(manage_device_blue_print)
+
+
+def scheduler_thread():
+    while True:
+        schedule.run_pending()
+        time.sleep(1)
+
+
+schedule.every().day.at("22:17").do(backup_data_base)
+
+scheduler = threading.Thread(target=scheduler_thread)
+scheduler.daemon = True
+scheduler.start()
+
+
+# noinspection PyBroadException
+@login_manager.user_loader
+def loader_user(user_id):
+    # pylint: disable=broad-exception-caught
     try:
-        with open(LATEST_KEY_FILE, 'r') as f:
-            key_value = f.read()
-    except FileNotFoundError:
-        key_value = '<absent>'
-    # Getting modification datetime
+        return get_flask_admin_user_by_id(user_id)
+    except Exception:
+        return None
+
+
+# noinspection PyBroadException
+@login_manager.request_loader
+def request_loader(request):
+    # pylint: disable=broad-exception-caught
+    username = request.form.get('username')
     try:
-        mod_time = getmtime(LATEST_KEY_FILE)
-        mod_time_converted = datetime.datetime.fromtimestamp(
-                mod_time).strftime('%Y-%m-%d %H:%M:%S')
-    except OSError:
-        mod_time_converted = '<unknown>'
-    return ("%s updated at: %s" % (key_value, mod_time_converted))
+        print(f"user name: {username}")
+        return get_flask_admin_user_by_user_name(username)
+    except Exception:
+        print(f"none: {username}")
+        return None
 
 
-@app.route('/', methods=['GET', 'POST'])
+@app.route('/', methods=['GET'])
 def index():
-    try:
-        conn = psycopg.connect(user = cfg['data']['user'],
-                                  password = cfg['data']['password'],
-                                  host = cfg['data']['host'],
-                                  port = cfg['data']['port'],
-                                  database = cfg['data']['name'])
-    except (Exception, psycopg.DatabaseError) as error :
-        logger.error("Error while connecting to PostgreSQL: %s" % error)
-        abort(500)
-    cursor = conn.cursor()
-    cursor.execute('SELECT * FROM users')
-    all_column_names = list(description[0] for description in
-                          cursor.description)
-
-    # Get column names for devices, needed access control. Exclude the others
-    access_info_columns = list(filter(lambda value: not value in ['id', 'name', 'key', 'last_enter'], all_column_names))
-    ordered_column_names = ['id', 'name', 'key', 'last_enter']
-    ordered_column_names.extend(access_info_columns)                   
-    
-    cursor.execute('SELECT id, name, key, last_enter FROM users')
-    user_info = cursor.fetchall()
-    cursor.execute('SELECT %s FROM users'
-                        % ','.join(access_info_columns))
-    user_access_info = cursor.fetchall()
-    template_data = zip(user_info, user_access_info)
-
-    # Updating latest key information
-    latest_key_info = get_latest_key_info()
-    # Parsing data from frontend: editing access, user additioin and deletion
-    if request.method == 'POST':
-        operation = request.form['operation']
-        if operation == 'edit':
-            user_id = request.form['id']
-            user_device = request.form['device']
-            user_state = request.form['state']
-            if user_state == 'true':
-                user_state = '1'
-            elif user_state == 'false':
-                user_state = '0'
-
-            logger.info('Updated user info: %s, %s, %s' % (user_id,
-                         access_info_columns[int(user_device)],
-                         user_state))
-            command = "UPDATE users SET %s = '%s' WHERE id = %s" \
-                % (access_info_columns[int(user_device)], user_state,
-                   user_id)
-            cursor.execute(command)
-            conn.commit()
-        elif operation == 'delete':
-            user_id = request.form['id']
-            user_device = request.form['device']
-            user_state = request.form['state']
-            cursor.execute('DELETE FROM users WHERE id=%s', (user_id, ))
-            conn.commit()
-            logger.info('User deleted, id: %s' % user_id)
-        elif operation == 'add':
-            user_name = request.form['nick']
-            user_key = request.form['key']
-            cursor.execute('INSERT INTO users(name, key) VALUES(%s, %s)',(user_name, user_key))
-            conn.commit()
-            logger.info('User added: %s, %s' % (user_name, user_key))
-    cursor.close()
-    conn.close()
-    return render_template('index.html', data=template_data,
-                           column_names=ordered_column_names,
-                           latest_key_info=latest_key_info)
-
-@app.route("/log_viewer")
-def log_reader_wrapper():
-    return txt_log_reader.render_logs_to_html()
-
-
-@app.route('/log_view_2')
-def log_view_2():
-    try:
-        conn = psycopg.connect(user=cfg['data']['user'],
-                               password=cfg['data']['password'],
-                               host=cfg['data']['host'],
-                               port=cfg['data']['port'],
-                               database=cfg['data']['name'])
-    except (Exception, psycopg.DatabaseError) as error:
-        logger.error("Error while connecting to PostgreSQL: %s" % error)
-        abort(500)
-    cursor = conn.cursor()
+    if not database_file.is_file():
+        return flask.redirect(flask.url_for('admin.init_app_route'))
+    if flask_login.current_user.is_authenticated:
+        return flask.redirect(flask.url_for('access_panel'))
+    return flask.redirect(flask.url_for('admin.login'))
+
+
+@app.route('/access_panel', methods=['GET'])
+def access_panel():
+    access_control_panel = get_access_control_panel()
+    latest_key = get_latest_key()
+
+    user = get_admin_user_by_flask_user(flask_login.current_user)
+    if user is None:
+        current_username = "Anonymous"
+    else:
+        current_username = user.username
+
+    logger.info('Access control panel data: %s', access_control_panel)
+    logger.info('Latest key: %s', latest_key)
+
+    return render_template("access_panel.html",
+                           latest_key=latest_key,
+                           access_control_panel=access_control_panel,
+                           current_user=current_username)
+
+
+@app.route('/full_log_view')
+def full_log_view():
+    return render_template('full_log_view.html')
+
 
-    cursor.execute('select device_name, name, to_timestamp(time) from logs join users u on logs.key = u.key;')
+# TODO all API calls, including API for readers move to separate module.
+@app.route('/api/logs', methods=['GET'])
+def api_get_logs():
+    # TODO: error handling, input data validation etc.
+    # Retrieve parameters from the query string
+    start_time = request.args.get('start_time', default=None)
+    end_time = request.args.get('end_time', default=None)
+    limit = request.args.get('limit', default=100, type=int)
+    offset = request.args.get('offset', default=0, type=int)
 
-    logs = cursor.fetchall()
+    return jsonify(query_event_logs(start_time, end_time, limit, offset))
 
-    print(logs)
 
-    cursor.close()
-    conn.close()
-    return render_template('log_view_2.html', logs=logs)
+@websocket.route('/updater_socket')
+def updater(websocket):
+    # pylint: disable=redefined-outer-name
+    update_firmware_full(websocket)
diff --git a/config.cfg b/config.cfg
new file mode 100644
index 0000000..e944619
--- /dev/null
+++ b/config.cfg
@@ -0,0 +1,4 @@
+logging:
+    logfile: log.txt
+    logsize_kb: 1000
+    rolldepth: 3
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 0000000..b6b49d8
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,82 @@
+## Api for readers
+
+#### Get all keys with access to device
+
+Method: GET
+Path: `/reader/<device_id>/accesses/`
+Response:
+
+```json
+{
+  "keys": [
+    "value_1",
+    "value_2"
+  ]
+}
+```
+
+#### Log an operation
+
+Method: POST
+Path: `/reader/<device_id>/log_operation`
+List of operations: `lock`, `unlock`, `deny_access`
+Body for lock:
+
+```json
+{
+  "operation": "lock"
+}
+```
+
+Body for unlock:
+
+```json
+{
+  "operation": "unlock",
+  "data": {
+    "key": "<user key>"
+  }
+}
+```
+
+
+Body for deny:
+
+```json
+{
+  "operation": "deny_access",
+  "data": {
+    "key": "<user key>"
+  }
+}
+```
+
+#### Get event log data
+
+Method: GET
+Path: `/api/logs`
+Query Parameters:
+
+1. start_time (optional): The start time of the time range (format: 'YYYY-MM-DD HH:MM:SS').
+2. end_time (optional): The end time of the time range (format: 'YYYY-MM-DD HH:MM:SS').
+3. limit (optional): The maximum number of log entries to retrieve (default: 100).
+
+Response:
+
+```json
+[
+  {
+    "id": "d2db5ec4-6e7a-11ee-b962-0242ac120002",
+    "key": "2fc49ee397fc41a2c8721f86d7f87bb2c560c01d7b19bc1654fb5db9beaa19ad",
+    "name": "MyTestCard",
+    "operation_time": "2023-11-07 10:17:12",
+    "operation_type": "lock"
+  },
+  // More log entries
+}
+```
+Example usage
+
+```
+GET /api/log?start_time=2023-01-01%2012:00:00&end_time=2023-01-02%2012:00:00&limit=50
+```
diff --git a/docs/database.md b/docs/database.md
new file mode 100644
index 0000000..d4ef39e
--- /dev/null
+++ b/docs/database.md
@@ -0,0 +1,56 @@
+## The database structure
+
+The application creates the database by itself. The code for creating database is in `app/data/database.py` file.
+
+### Project data structure
+
+All data stored in postgres database. Currently, we have three tables:
+
+- users - the table with users data
+- permissions - the table with permissions. It contains two columns: device_id and user_key. Device_id is unique id of
+  device, user_key is key of user, who has access to this device.
+- event_logs - the jornal of all events. It contains four columns: user_key, device_id, operation and time. Operation
+  can be `lock`, `unlock`, ...
+
+#### Database
+
+We use the sqlite database, the file for database is stored in `database.db` file. The file is not created by default.
+You need to create it manually. See section "Prepare database"
+
+Schema of database:
+
+```mermaid
+classDiagram
+    direction BT
+    class devices {
+        text id
+        text name
+    }
+    class permissions {
+        text device_id
+        text user_key
+    }
+
+    class users {
+        text name
+        text key
+    }
+
+    class event_long {
+        text user_key
+        text device_id
+        text operation
+        text time
+    }
+
+    class admins {
+        integer id
+        text username
+        text password
+    }
+
+    permissions --> devices
+    work_logs --> devices
+    work_logs --> users
+    permissions --> users
+```
diff --git a/readres.http b/readres.http
new file mode 100644
index 0000000..ea97fd7
--- /dev/null
+++ b/readres.http
@@ -0,0 +1,24 @@
+GET http://localhost:5000/reader/name1/accesses
+
+
+###
+POST http://127.0.0.1:5000/reader/test_slack_device/log_operation
+Content-Type: application/json
+
+{
+  "operation": "lock",
+  "data": {
+    "key": "my_slack_key"
+  }
+}
+
+###
+POST http://127.0.0.1:5000/reader/test_slack_device/log_operation
+Content-Type: application/json
+
+{
+  "operation": "unlock",
+  "data": {
+    "key": "my_slack_key"
+  }
+}
diff --git a/requirements.txt b/requirements.txt
index 88cac4d..850521c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,11 @@
-Flask
-pyyaml
-psycopg2-binary
+Flask~=2.3.2
+pyyaml~=6.0
 requests
+slack_sdk
+Werkzeug==2.3.7
+flask-sock
+flask-login
+schedule
+pylint
+pycodestyle
+gunicorn==20.0.4
\ No newline at end of file
diff --git a/router/mac b/router/mac
deleted file mode 100644
index bf2fcb9..0000000
--- a/router/mac
+++ /dev/null
@@ -1,26 +0,0 @@
-#!/usr/bin/env lua
-
-print("Content-type: application/json")
-print("")
-
-local ip = os.getenv("REMOTE_HOST")
-local first = true
-
-io.write('{"ip": "', ip, '", "macs": [')
-for line in io.lines('/proc/net/arp') do
-  fields = {}
-  line:gsub("([^ ]*)".." ", function(c)
-    if c ~= "" then
-      table.insert(fields, c)
-    end
-  end)
-  if fields[1] == ip then
-    if first then
-      first = false
-    else
-      io.write(", ")
-    end
-    io.write('"', fields[4], '"')
-  end
-end
-io.write("]}")
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..2776ed3
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,4 @@
+[pycodestyle]
+count = False
+max-line-length = 100
+statistics = True
\ No newline at end of file
diff --git a/static/css/auth.css b/static/css/auth.css
new file mode 100644
index 0000000..86c53ef
--- /dev/null
+++ b/static/css/auth.css
@@ -0,0 +1,88 @@
+body {
+    font-family: Arial, sans-serif;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 100vh;
+    margin: 0;
+    background-color: #f0f0f0;
+}
+
+.login-container {
+    background: #fff;
+    padding: 20px;
+    border-radius: 5px;
+    box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
+    width: 300px;
+    text-align: center;
+}
+
+.login-container h2 {
+    margin-top: 0;
+}
+
+.login-form {
+    text-align: left;
+}
+
+.login-form label,
+.login-form input {
+    display: block;
+    margin-bottom: 10px;
+}
+
+.login-form input {
+    width: 100%;
+    padding: 10px;
+    border: 1px solid #ccc;
+    border-radius: 5px;
+}
+
+.login-form button {
+    background-color: #007bff;
+    color: #fff;
+    padding: 10px 20px;
+    border: none;
+    border-radius: 5px;
+    cursor: pointer;
+}
+
+/* Style the checkbox and its label */
+#toggleCheckbox {
+    display: none; /* Hide the default checkbox */
+}
+
+#toggleCheckbox + label {
+    display: flex;
+    align-items: center;
+    margin-right: 10px;
+}
+
+#toggleCheckbox + label::before {
+    content: "";
+    display: inline-block;
+    width: 20px;
+    height: 20px;
+    border: 1px solid #ccc;
+    border-radius: 3px;
+    background-color: #fff;
+    margin-right: 10px; /* Adjust the spacing between checkbox and label text */
+}
+
+#toggleCheckbox:checked + label::before {
+    background-color: #007bff; /* Change background color when checked */
+    border: 1px solid #007bff;
+}
+
+/* Style the content to be hidden or shown */
+#contentToToggle {
+    display: none;
+    padding: 10px;
+    border: 1px solid #ccc;
+    border-radius: 5px;
+    background-color: #fff;
+}
+
+#toggleCheckbox:checked + label + #contentToToggle {
+    display: block;
+}
\ No newline at end of file
diff --git a/static/js/application.js b/static/js/application.js
index c0dada7..7b053ee 100644
--- a/static/js/application.js
+++ b/static/js/application.js
@@ -1,170 +1,11 @@
-/**
- * Javascript part of Hacklab Admin Panel.
- * includes SocketIO communication with backend and
- * implementation of general visual logic
- * 
- * TODO:
- *    * Implement save, discard, delete logic for user data table
- *    * Add comments for functions
- *    * Code review
- */
-var DELETE_EDIT_BUTTON_GROUP = '<td class="buttonToolbox">' +
-    '<div class="btn-group"><a class="btn btn-danger btn-sm" onclick="deleteRow(this)">' +
-    '<i class="fa fa-trash-o"></i></a>' +
-    '<a class="btn btn-info btn-sm" onclick="editRow(this)">' +
-    '<i class="fa fa-pencil"></i></a></div></td>';
-
-var DISCARD_APPLY_BUTTON_GROUP = '<div class="btn-group">' +
-    '<a class="btn btn-danger btn-sm" onclick="discardChanges(this)">' +
-    '<i class="fa fa-undo" aria-hidden="true"></i></a>' +
-    '<a class="btn btn-primary btn-sm" onclick="applyChanges(this)">' +
-    '<i class="fa fa-floppy-o" aria-hidden="true"></i></a></div>';
-
-var MAC_CONVERSION_SERVICE = "http://192.168.1.1/cgi-bin/mac";
-
-var macs = [];
-
-$(function() {
-  $.getJSON( MAC_CONVERSION_SERVICE, function( data ) {
-    macs = data.macs;
-  });
-});
-
-$(function() {
-    //connect to the socket server.
-    var socket = io.connect('http://' + document.domain + ':' + location.port + '/log');
-    var msgReceived = [];
-
-    //receive details from server
-    socket.on('newmessage', function(msg) {
-        msgReceived.push(msg);
-        numbers_string = '';
-        // TODO: Maybe here we could use append function?
-        $('#log').html(numbers_string);
-        for (var i = 0; i < msgReceived.length; i++) {
-            numbers_string = numbers_string + msgReceived[i] + '<br>';
-            console.log(i);
-        }
-        $('#log').html(numbers_string);
-    });
-
-});
-
-$(function() {
-    var socket = io.connect('http://' + document.domain + ':' + location.port + '/tablemgr');
-    socket.on('tabledata', function(msg) {
-        var table_data = JSON.parse(msg);
-        var index;
-        $("#table_body").empty();
-        $("#table_full_status_body").empty();
-        for (index = 0; index < table_data.length; ++index) {
-            console.log(table_data[index]);
-            addTableRow(table_data[index]);
-        }
-    });
-});
-
-function addTableRow(data) {
-    if (data.hasOwnProperty('id')) {
-        var decorated = (data.status == 'online' ? '<strong>online</strong>' : 'offline');
-        $("#tblData tbody").append(
-            "<tr>" +
-            '<td class="id_entry">' + data.id + "</td>" +
-            '<td class="mac_entry" data-mac="' + data.mac + '">' + 
-            data.mac.replace(/;/g, '<br>') + "</td>" +
-            '<td class="status_entry">' + decorated + "</td>" +
-            '<td class="nick_entry">' + data.nick + "</td>" +
-            DELETE_EDIT_BUTTON_GROUP +
-            "</tr>");
-    }
-    if (data.status === 'online') {
-        var decorated = ($.inArray(data.mac, macs) == -1 ? '' : ' (<strong>you!</strong>)');
-        $("#tblDataFullStatus tbody").append(
-            '<tr>' +
-            '<td class="mac_entry">' + data.mac + decorated + "</td>" +
-            '<td class="nick_entry">' + data.nick + "</td>" +
-            '</tr>');
-    }
-};
-
-function discardChanges(context) {
-    var socket = io.connect('http://' + document.domain + ':' + location.port + '/tablemgr');
-    // Just ask server resend the table
-     socket.emit('uploadTable', {});
-};
-
-function editRow(context) {
-    var $row = $(context).closest("tr");
-
-    var id = $row.find(".id_entry");
-    var mac = $row.find(".mac_entry");
-    var nick = $row.find(".nick_entry");
-    var buttonToolbox = $row.find(".buttonToolbox");
-    // Store previous values for undo
-    var prevNick = nick.text();
-    var prevMac = mac.attr("data-mac");
-    
-    console.log(prevNick);
-    mac.html("<input type='text' id='newmac' data-toggle='tooltip'" + 
-             "data-placement='top' title='Use &quot ; &quot separator to add multiple MACs' value='" + 
-             mac.attr("data-mac") + "'/>");
-    nick.html("<input type='text' id='newnick' value='" + nick.text() + "'/>");
-
-    // Change buttons to "Save" and "Apply"
-    buttonToolbox.html(DISCARD_APPLY_BUTTON_GROUP);
-};
-
-function applyChanges(context) {
-    var socket = io.connect('http://' + document.domain + ':' + location.port + '/tablemgr');
-    var $row = $(context).closest("tr");
-    var id = $row.find(".id_entry").text();
-    var mac = $row.find(".mac_entry");
-    var nick = $row.find(".nick_entry");
-    var buttonToolbox = $row.find(".buttonToolbox");
-
-    var newMacValue = document.getElementById("newmac").value;
-    var newNickValue = document.getElementById("newnick").value;
-
-    // Convert all symbols to upper, just to convenience
-    newMacValue = newMacValue.toUpperCase();
-    
-    mac.html(newMacValue);
-    nick.html(newNickValue);
-
-    buttonToolbox.html(DELETE_EDIT_BUTTON_GROUP);
-    
-    socket.emit('edituser', {
-        id: id,
-        mac: newMacValue,
-        nick: newNickValue,
-    }); 
-};
-
-function deleteRow(context) {
-    var socket = io.connect('http://' + document.domain + ':' + location.port + '/tablemgr');
-    var $row = $(context).closest("tr");
-    var id = $row.find(".id_entry").text();
-
-    socket.emit('deleteuser', {
-        id: id,
-    });    
-};
-
-function addUser() {
-    var socket = io.connect('http://' + document.domain + ':' + location.port + '/tablemgr');
-    var nickname = $("#nickname").val();
-    var macaddress = $("#macaddress").val();
-
-    if (nickname == "") {
-        alert("Please specify nickname!");
-        return 0;
-    }
-    if (macaddress == "") {
-        alert("Please specify MAC address!");
-        return 0;
-    }
-    socket.emit('adduser', {
-        macaddress: macaddress,
-        nickname: nickname
+function flashFirmware() {
+    console.log("Test!");
+    const socket = new WebSocket('ws://' + location.host + '/updater_socket');
+    const log = (text, color) => {
+        document.getElementById('flashLog').innerHTML += `<span style="color: ${color}">${text}</span><br>`;
+    };
+
+    socket.addEventListener('message', ev => {
+        log('<<< ' + ev.data, 'blue');
     });
-};
+}
diff --git a/static/js/macs.js b/static/js/macs.js
deleted file mode 100644
index b6557f6..0000000
--- a/static/js/macs.js
+++ /dev/null
@@ -1,53 +0,0 @@
-$(document).ready(function() {
-    $('.select2').select2();
-});
-
-function addMac() {
-	var user_id = $("#user_id").val();
-	var mac = $("#mac").val();
-	var data = new FormData();
-
-	if (user_id == "") {
-		alert("Please select a person!");
-		return 0;
-	}
-	if (mac == "") {
-		alert("Please specify MAC!");
-		return 0;
-	}
-
-	data.append('operation', 'add');
-	data.append('user_id', user_id);
-	data.append('mac', mac);
-
-	var xhr = new XMLHttpRequest();
-	xhr.onreadystatechange = function () {
-		if (xhr.readyState == 4) {
-			if (xhr.status != 200) {
-				alert("Cannot get updated table!");
-			} else {
-				location.reload(true);
-			}
-		}
-	}
-	xhr.open('POST', '/macs', true);
-	xhr.send(data);
-}
-
-function deleteMac(mac){
-  var data = new FormData();
-	data.append('operation', 'delete');
-	data.append('mac', mac);
-	var xhr = new XMLHttpRequest();
-	xhr.onreadystatechange = function () {
-		if (xhr.readyState == 4) {
-			if (xhr.status != 200) {
-				alert("Cannot get updated table!");
-			} else {
-				location.reload(true);
-			}
-		}
-	}
-	xhr.open('POST', '/macs', true);
-	xhr.send(data);
-}
diff --git a/static/js/rfid.js b/static/js/rfid.js
index 176c9b4..d29cbaf 100644
--- a/static/js/rfid.js
+++ b/static/js/rfid.js
@@ -1,73 +1,116 @@
-function onCheckboxChange(context) {
-	var data = new FormData();
-	data.append('operation', 'edit');
-	data.append('id', context.value.split(",")[1]);
-	data.append('device', context.value.split(",")[0]);
-	data.append('state', context.checked);
-
-	var xhr = new XMLHttpRequest();
-	xhr.onreadystatechange = function () {
-		if (xhr.readyState == 4) {
-			if (xhr.status != 200) {
-				alert("Cannot get updated table!");
-			}
-		}
-	}
-	xhr.open('POST', '/', true);
-	xhr.send(data);
+function onUserPermissionChange(user_key, device_id, context) {
+    console.log('change permission for user ' + user_key + ' on device ' + device_id);
+
+    var data = new FormData();
+    data.append('user_key', user_key);
+    data.append('device_id', device_id);
+
+    if (context.checked) {
+        method = 'POST';
+    } else {
+        method = 'DELETE';
+    }
+
+    console.log('method: ' + method);
+
+    var xhr = new XMLHttpRequest();
+    xhr.onreadystatechange = function () {
+        if (xhr.readyState == 4) {
+            if (xhr.status != 200) {
+                alert("Cannot get updated table!");
+            } else {
+                location.reload();
+            }
+        }
+    }
+    xhr.open(method, '/permission', true);
+    xhr.send(data);
 }
 
-function onDeleteClick(id_value, a) {
-	var data = new FormData();
-	data.append('operation', 'delete');
-	data.append('id', id_value);
-	data.append('device', '');
-	data.append('state', '');
-
-	var xhr = new XMLHttpRequest();
-	var table = document.getElementById('tblData');
-	var rowIndex = a.parentNode.parentNode.rowIndex;
-	table.deleteRow(rowIndex);
-	xhr.onreadystatechange = function () {
-		if (xhr.readyState == 4) {
-			if (xhr.status != 200) {
-				alert("Cannot get updated table!");
-			}
-		}
-	}
-	xhr.open('POST', '/', true);
-	xhr.send(data);
+function onUserDeleteClick(user_key, a) {
+    console.log('onUserDeleteClick ' + user_key);
+
+    var data = new FormData();
+    data.append('user_key', user_key);
+
+    var xhr = new XMLHttpRequest();
+    xhr.onreadystatechange = function () {
+        if (xhr.readyState == 4) {
+            if (xhr.status != 200) {
+                alert("Cannot get updated table!");
+            } else {
+                // Reload page when we received response
+                location.reload();
+            }
+        }
+    }
+    xhr.open('DELETE', '/user', true);
+    xhr.send(data);
 }
 
 function addUser() {
-	var name = $("#user_name").val();
-	var key = $("#user_key").val();
-	var data = new FormData();
-
-	if (name == "") {
-		alert("Please specify nickname!");
-		return 0;
-	}
-	if (key == "") {
-		alert("Please specify access key!");
-		return 0;
-	}
-
-	data.append('operation', 'add');
-	data.append('nick', name);
-	data.append('key', key);
-
-	var xhr = new XMLHttpRequest();
-	xhr.onreadystatechange = function () {
-		if (xhr.readyState == 4) {
-			if (xhr.status != 200) {
-				alert("Cannot get updated table!");
-			} else {
-				// Reload page when we received response
-				location.reload(true);
-			}
-		}
-	}
-	xhr.open('POST', '/', true);
-	xhr.send(data);
+    var name = $("#user_name").val();
+    var key = $("#user_key").val();
+    var data = new FormData();
+
+    if (name == "") {
+        alert("Please specify nickname!");
+        return 0;
+    }
+    if (key == "") {
+        alert("Please specify access key!");
+        return 0;
+    }
+
+    data.append('nick', name);
+    data.append('key', key);
+
+    var xhr = new XMLHttpRequest();
+    xhr.onreadystatechange = function () {
+        if (xhr.readyState == 4) {
+            if (xhr.status != 200) {
+                alert("Cannot get updated table!");
+            } else {
+                // Reload page when we received response
+                location.reload(true);
+            }
+        }
+    }
+    xhr.open('POST', '/user', true);
+    xhr.send(data);
 }
+
+function addDevice() {
+    console.log("Add device called");
+    var device_name = $("#device_name").val();
+    var device_id = $("#device_id").val();
+    var data = new FormData();
+
+    if (device_name == "") {
+        alert("Please specify device_name!");
+        return 0;
+    }
+    if (device_id == "") {
+        alert("Please specify device_id!");
+        return 0;
+    }
+
+    data.append('device_name', device_name);
+    data.append('device_id', device_id);
+
+    var xhr = new XMLHttpRequest();
+    xhr.onreadystatechange = function () {
+        if (xhr.readyState == 4) {
+            if (xhr.status != 200) {
+                alert("Cannot get updated table!");
+            } else {
+                // Reload page when we received response
+                location.reload(true);
+            }
+        }
+    }
+    xhr.open('POST', '/device', true);
+    xhr.send(data);
+
+    console.log(data);
+}
\ No newline at end of file
diff --git a/templates/access_panel.html b/templates/access_panel.html
new file mode 100644
index 0000000..64372c8
--- /dev/null
+++ b/templates/access_panel.html
@@ -0,0 +1,67 @@
+{% extends "layout.html" %}
+{% block content %}
+<div class="container-fluid px-md-5">
+    <div class="row">
+        <div class="form-group col-md-5 col-sm-12" data-step="2">
+            <h1 class="my-4">Access control</h1>
+        </div>
+        <div class="form-group col-md-5 col-sm-12" data-step="3">
+            <h1 class="my-4">{{ current_user }}</h1>
+        </div>
+    </div>
+
+    <div class="well well-sm pb-4" data-step="1"
+         data-intro="Latest used key on DOOR is shown here. Put new key to door reader and check modify datetime">Latest
+        triggered door key: {{ latest_key }}
+    </div>
+    <div class="row">
+        <div class="form-group col-md-5 col-sm-12" data-step="2" data-intro="Add this new key here.">
+            <label for="email">KEY Value: </label>
+            <input type="text" class="form-control" id="user_key" value={{ latest_key }}>
+        </div>
+        <div class="form-group col-md-5 col-sm-12" data-step="3" data-intro="Fill the nickname">
+            <label for="pwd">Nickname: </label>
+            <input type="text" class="form-control" id="user_name" placeholder="Nickname">
+        </div>
+        <div class="col-md-2 col-sm-10 d-flex align-items-end">
+            <button type="submit" class="btn btn-primary" onclick="addUser()" data-step="4"
+                    data-intro="Finish addition">Add user
+            </button>
+        </div>
+    </div>
+    <div class="py-3">
+        <div class="py-3 table-responsive">
+            <table id="tblData" class="table stripe hover align-middle dataTable no-footer compact pt-3">
+                <thead>
+                <tr>
+                    {% for column_name in access_control_panel.header %}
+                    <th>{{ column_name }}</th>
+                    {% endfor %}
+                    <th></th>
+                </tr>
+                </thead>
+                <tbody id="table_body">
+                {% for row in access_control_panel.rows %}
+                <tr>
+                    <td><a href="/user/{{row.user_key}}">{{row.user_name}}</a></td>
+                    <td>{{row.user_key}}</td>
+                    {% for permission in row.permissions %}
+                    <td>
+                        <input type="checkbox" name="box1"
+                               onChange="onUserPermissionChange('{{row.user_key}}', '{{permission.device_key}}', this)"
+                               value="box1" {{ 'checked="checked"' if permission.is_granted else ""}}>
+                    </td>
+                    {% endfor %}
+                    <td>
+                        <a class="btn btn-danger btn-sm" onclick="onUserDeleteClick('{{row.user_key}}', this)">
+                            <i style="color: white" class="fa fa-trash-o"></i>
+                        </a>
+                    </td>
+                </tr>
+                {% endfor %}
+                </tbody>
+            </table>
+        </div>
+    </div>
+</div>
+{% endblock %}
diff --git a/templates/device_page.html b/templates/device_page.html
new file mode 100644
index 0000000..2f7af25
--- /dev/null
+++ b/templates/device_page.html
@@ -0,0 +1,66 @@
+{% extends "layout.html" %}
+
+{% block content %}
+
+<div class="container-fluid px-md-5">
+    <h1 class="my-4">Profile for device : {{ full_device.device.name }}</h1>
+    <div>
+        <form method="post" action="#">
+            <label for="slack_channel_id">Slack ID:</label>
+            <input type="text" id="slack_channel_id" name="slack_channel_id" required value={{
+                   full_device.device.slack_channel_id }}>
+
+            <input type="submit" value="Set device slack channel id for notification">
+        </form>
+
+        <button id="execute-button">Send sent notification</button>
+    </div>
+    <div class="py-3">
+        <div style="width: 100%;">
+            <div style="float:left; width: 30%">
+                <h4>User with access to {{ full_device.device.name }}</h4>
+                <ul>
+                    {% for user in full_device.user_with_access %}
+                    <li><a href="/user/{{user.key}}">{{ user.name }}</a></li>
+                    {% endfor %}
+                </ul>
+            </div>
+            <div style="float:right; width: 70%">
+                <table class="table stripe hover align-middle dataTable no-footer compact pt-3">
+                    <tr>
+                        <th>User name</th>
+                        <th>Time</th>
+                        <th>Operation</th>
+                    </tr>
+                    {% for log in full_device.logs %}
+                    <tr>
+                        <td><a href="/user/{{ log.user.key }}">{{ log.user.name }}</a></td>
+                        <td>{{ log.operation.time }}</td>
+                        <td>{{ log.operation.type }}</td>
+                    </tr>
+                    {% endfor %}
+                </table>
+            </div>
+        </div>
+        <div style="clear:both"></div>
+    </div>
+</div>
+
+<script>
+    $(document).ready(function () {
+        let jsonData = {
+            device_id: "{{ full_device.device.id }}",
+        };
+        $('#execute-button').click(function () {
+            $.ajax({
+                type: 'POST',
+                url: '/send-test-message-to-channel',
+                data: JSON.stringify(jsonData),
+                contentType: 'application/json',
+            });
+        });
+    });
+</script>
+
+
+{% endblock %}
diff --git a/templates/devices.html b/templates/devices.html
new file mode 100644
index 0000000..c2edbcc
--- /dev/null
+++ b/templates/devices.html
@@ -0,0 +1,63 @@
+{% extends "layout.html" %}
+
+{% block content %}
+
+<div class="container-fluid px-md-5">
+    <div class="row">
+        <div class="form-group col-md-5 col-sm-12" data-step="2" data-intro="Add this new key here.">
+            <label for="email">Device name: </label>
+            <input type="text" class="form-control" id="device_id" placeholder="c32d8b45-92fe-44f6-8b61-42c2107dfe87">
+        </div>
+        <div class="form-group col-md-5 col-sm-12" data-step="3" data-intro="Fill the nickname">
+            <label for="pwd">Reader Name: </label>
+            <input type="text" class="form-control" id="device_name" placeholder="Device name, eg Lathe or Door">
+        </div>
+        <div class="col-md-2 col-sm-10 d-flex align-items-end">
+            <button type="submit" class="btn btn-primary" onclick="addDevice()" data-step="4"
+                    data-intro="Finish addition">Add device
+            </button>
+        </div>
+    </div>
+    <h1 class="my-4">All devices</h1>
+    <div class="py-3">
+        <div style="float:left; width: 30%">
+            <ul>
+                {% for device in devices %}
+                <li><a href="/device/{{device.id}}">{{ device.name }}:{{device.id}}</a></li>
+                {% endfor %}
+            </ul>
+        </div>
+        <div style="clear:both"></div>
+    </div>
+    <div class="py-3">
+        <div style="float:left; width: 30%">
+            <!-- Button trigger modal -->
+            <button type="button" class="btn btn-primary" data-toggle="modal" data-target="#flashDeviceModal">
+            Flash Connected Device
+            </button>
+            <!-- Modal -->
+            <div class="modal fade" id="flashDeviceModal" tabindex="-1" role="dialog" aria-labelledby="exampleModalLabel" aria-hidden="true">
+            <div class="modal-dialog modal-dialog-scrollable" role="document">
+                <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title" id="exampleModalLabel">Flash Device</h5>
+                    <button type="button" class="close" data-dismiss="modal" aria-label="Close">
+                    <span aria-hidden="true">&times;</span>
+                    </button>
+                </div>
+                <div class="modal-body" id="flashLog">
+                    ...
+                </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
+                    <button type="button" class="btn btn-primary" onclick="flashFirmware()">Flash Firmware</button>
+                </div>
+                </div>
+            </div>
+            </div>
+        </div>
+        <div style="clear:both"></div>
+    </div>
+
+</div>
+{% endblock %}
diff --git a/templates/full_log_view.html b/templates/full_log_view.html
new file mode 100644
index 0000000..f48d669
--- /dev/null
+++ b/templates/full_log_view.html
@@ -0,0 +1,69 @@
+{% extends "layout.html" %}
+
+{% block content %}
+
+<div class="container-fluid px-md-5">
+    <h1 class="my-4">Logs</h1>
+    <div class="row">
+        <div class="form-group col-md-3 col-sm-12" data-step="1">
+            <label>Start time:</label>
+            <input type="text" class="form-control" id="start_time" type="datetime">
+        </div>
+        <div class="form-group col-md-3 col-sm-12" data-step="2">
+            <label>End time:</label>
+            <input type="text" class="form-control" id="end_time" type="datetime">
+        </div>
+        <div class="col-md-2 col-sm-10 d-flex align-items-end" data-step="3">
+            <button type="submit" class="btn btn-primary" onclick="addTimeRange()">
+                Search
+            </button>
+        </div>
+    </div>
+    <div class="py-3">
+        <table id="logsTable" class="table stripe hover align-middle dataTable no-footer compact pt-3">
+        </table>
+    </div>
+
+</div>
+
+<script>
+    function addTimeRange() {
+        let startTime = $('#start_time').val();
+        let endTime = $('#end_time').val();
+        let url = '/api/logs?limit=5000';
+        if (startTime) {
+            url += '&start_time=' + startTime;
+        }
+        if (endTime) {
+            url += '&end_time=' + endTime;
+        }
+        $('#logsTable').DataTable().ajax.url(url).load();
+    }
+    $('#logsTable').DataTable({
+        ajax: {
+            url: '/api/logs?limit=5000',
+            dataSrc: ''
+        },
+        columns: [
+            {
+                data: 'name',
+                title: 'Name',
+                render: (data, type, row) => {
+                    if (data === null) return '–'
+                    return '<a href="/user/' + row.key + '">' + data + '</a>'
+                }
+            },
+            {
+                data: 'id',
+                title: 'Device',
+                render: (data, type, row) => '<a href="/device/' + data + '">' + data + '</a>'
+            },
+            { data: 'operation_type', title: 'Operation Type' },
+            { data: 'operation_time', title: 'Time' },
+        ],
+        order: [[3, 'desc']],
+        pageLength: 100
+    });
+</script>
+
+{% endblock %}
diff --git a/templates/index.html b/templates/index.html
deleted file mode 100644
index ee0f2fb..0000000
--- a/templates/index.html
+++ /dev/null
@@ -1,69 +0,0 @@
-{% extends "layout.html" %}
-{% block content %}
-<div class="container-fluid px-md-5">
-        <h1 class="my-4">Access control</h1>
-        <div class="well well-sm pb-4" data-step="1"
-        data-intro="Latest used key on DOOR is shown here. Put new key to door reader and check modify datetime"
-          >Latest triggered door key: {{ latest_key_info }}
-        </div>
-        <div class="row">
-          <div class="form-group col-md-5 col-sm-12" data-step="2" data-intro="Add this new key here.">
-              <label for="email">KEY Value:  </label>
-              <input type="text" class="form-control" id="user_key" placeholder="123ABC" >
-          </div>
-          <div class="form-group col-md-5 col-sm-12" data-step="3" data-intro="Fill the nickname">
-              <label for="pwd">Nickname:  </label>
-              <input type="text" class="form-control" id="user_name" placeholder="Nickname">
-          </div>
-          <div class="col-md-2 col-sm-10 d-flex align-items-end">
-            <button type="submit" class="btn btn-primary" onclick="addUser()" data-step="4" data-intro="Finish addition">Add user</button>
-          </div>
-        </div>
-        <div class="py-3">
-            <!-- Table with database -->
-            <div class="py-3 table-responsive">
-                <table id="tblData" class="table stripe hover align-middle dataTable no-footer compact pt-3">
-                    <thead>
-                        <tr>
-                            {% for column_name in column_names %}
-                                <th>{{ column_name }}</th>
-                            {% endfor %}
-                            <th></th>
-                        </tr>
-                    </thead>
-                    <tbody id="table_body">
-                    {% for item in data %}
-                        <!-- User information part -->
-                        <tr>
-                        {% for subitem in item[0][:-1] %}
-                            <td>{{ subitem }}</td>
-                        {% endfor %}
-                        <!-- Last enter part -->
-                        <td>
-                        {% if item[0][3] %}
-                            {{ item[0][3].strftime('%Y-%m-%d %H:%M') }}
-                        {% else %}
-                            –
-                        {% endif %}
-                        </td>
-                        <!-- Access information part -->
-                        {% for subitem in item[1] %}
-                         <td>
-                         <!-- Value is encoded like: (x,y), where x - is index of access
-                         devices(door, cnc, etc), y - user ID value -->
-                            <input type="checkbox" onChange='onCheckboxChange(this)'
-                                name="accessBox" value={{loop.index - 1}},{{item[0][0]}}
-                                {% if subitem==1 %}checked{% endif %}>
-                        </td>
-                        {% endfor %}
-                        <td>
-                            <a class="btn btn-danger btn-sm" onclick="onDeleteClick({{item[0][0]}}, this)"><i style="color: white" class="fa fa-trash-o"></i></a>
-                        </td>
-                        </tr>
-                    {% endfor %}
-                    </tbody>
-                </table>
-            </div>
-        </div>
-</div>
-{% endblock %}
diff --git a/templates/init_app.html b/templates/init_app.html
new file mode 100644
index 0000000..f3912d2
--- /dev/null
+++ b/templates/init_app.html
@@ -0,0 +1,50 @@
+{% block content %}
+
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Login Form</title>
+    <link rel="stylesheet" href="/static/css/auth.css">
+</head>
+<body>
+<div class="login-container">
+    <h2>Create your admin user</h2>
+    <p>We suggest and highly recommend to create a strong password for your admin user</p>
+    <form class="login-form" action="#" method="post" enctype="multipart/form-data">
+        <label for="username">Admin username:</label>
+        <input type="text" id="username" name="username" required>
+
+        <label for="password">Admin Password:</label>
+        <input type="password" id="password" name="password" required>
+
+        <label for="slat">Slat, the secure string using for hash the admin password</label>
+        <label for="slat">We recommend to generate random string for the field, and keep it save</label>
+        <input type="text" id="slat" name="slat" required>
+
+        <input type="checkbox" id="toggleCheckbox">
+        <label for="toggleCheckbox">Restore from database backup</label>
+
+        <div id="contentToToggle">
+            <input type=file name=file>
+        </div>
+
+        <div>
+            <button type="submit">Init Prismo</button>
+        </div>
+    </form>
+</div>
+<script>
+    const toggleCheckbox = document.getElementById("toggleCheckbox");
+    const contentToToggle = document.getElementById("contentToToggle");
+
+    toggleCheckbox.addEventListener("change", function () {
+        console.log("test")
+        contentToToggle.style.display = this.checked ? "block" : "none";
+    });
+    contentToToggle.style.display = "none";
+</script>
+</body>
+</html>
+
+{% endblock %}
diff --git a/templates/layout.html b/templates/layout.html
index 25023ff..253b3f7 100644
--- a/templates/layout.html
+++ b/templates/layout.html
@@ -1,52 +1,51 @@
 <!DOCTYPE html>
-<html>
+<html lang="en">
 
 <head>
     <title>Prismo</title>
     <script src="//ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
     <script src="//cdnjs.cloudflare.com/ajax/libs/intro.js/2.7.0/intro.js"></script>
     <script src="static/js/rfid.js"></script>
-    <script src="static/js/macs.js"></script>
-    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css">
+    <script src="static/js/application.js"></script>
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
     <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/intro.js/2.7.0/introjs.css">
-    <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.8/css/select2.min.css" rel="stylesheet" />
+    <link href="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.8/css/select2.min.css" rel="stylesheet"/>
     <script src="https://cdnjs.cloudflare.com/ajax/libs/select2/4.0.8/js/select2.min.js"></script>
     <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/dt/dt-1.13.1/datatables.min.css"/>
     <script type="text/javascript" src="https://cdn.datatables.net/v/dt/dt-1.13.1/datatables.min.js"></script>
+    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
 </head>
 
 <body>
-  <nav class="navbar navbar-expand-lg navbar-light bg-dark px-5" role="navigation">
-      <a class="navbar-brand text-light" href="/">Hacklab Admin Panel</a>
-          <div class="collapse navbar-collapse" id="navbarSupportedContent">
-              <ul class="navbar-nav mr-auto">
-                  <li class="nav-item active"><a class="nav-link text-light" href="/">Access control</a></li>
-                  <li class="nav-item"><a class="nav-link text-light" href="macs">MAC addresses</a></li>
-                  <li class="nav-item"><a class="nav-link text-light" href="#">Payments</a></li>
-                  <li class="nav-item"><a class="nav-link text-light" href="log_viewer">Machine Log Viewer</a></li>
-                  <li class="nav-item active"><a class="nav-link text-light" href="/log_view_2">Log viewer 2</a></li>
-              </ul>
-          </div>
-  </nav>
-    {% block content %}{% endblock %}
-    <div id="footer">
+<nav class="navbar navbar-expand-lg navbar-light bg-dark px-5" role="navigation">
+    <a class="navbar-brand text-light" href="/">Hacklab Admin Panel</a>
+    <div class="collapse navbar-collapse" id="navbarSupportedContent">
+        <ul class="navbar-nav mr-auto">
+            <li class="nav-item active"><a class="nav-link text-light" href="/access_panel">Access control</a></li>
+            <li class="nav-item active"><a class="nav-link text-light" href="/full_log_view">Full work log viewer</a>
+            </li>
+            <li class="nav-item active"><a class="nav-link text-light" href="/devices">Devices</a></li>
+            <li class="nav-item active"><a class="nav-link text-light" href="/settings">Settings</a></li>
+            <li class="nav-item active"><a class="nav-link text-light" href="/logout">Log out</a></li>
+
+        </ul>
     </div>
-    <footer class="footer px-2">
-        <p>&copy; Hacklab 2017</p>
-    </footer>
-    <script>
-        $(document).ready(function(){
-            $('#tblData').dataTable({
-                paging: false,
-                columnDefs: [
-                    { targets: [0], visible: false, searchable: false },
-                    { targets: [5], searchable: false, orderable: false }
-                ]
-            });
+</nav>
+{% block content %}{% endblock %}
+<div id="footer">
+</div>
+<footer class="footer px-2">
+    <p>&copy; Hacklab 2017</p>
+</footer>
+<script>
+    $(document).ready(function () {
+        $('#tblData').dataTable({
+            paging: false,
         });
-    </script>
+    });
+</script>
 </body>
 
 </html>
diff --git a/templates/log_view_2.html b/templates/log_view_2.html
deleted file mode 100644
index d68651e..0000000
--- a/templates/log_view_2.html
+++ /dev/null
@@ -1,25 +0,0 @@
-{% extends "layout.html" %}
-
-{% block content %}
-<h1>Logs</h1>
-
-<div class="py-3">
-    <table class="table stripe hover align-middle dataTable no-footer compact pt-3">
-        <tr>
-            <th>Device name</th>
-            <th>User Name</th>
-            <th>Time</th>
-        </tr>
-        {% for log in logs %}
-        <tr>
-            <td>{{ log[0] }}</td>
-            <td>{{ log[1] }}</td>
-            <td>{{ log[2] }}</td>
-        </tr>
-        {% endfor %}
-
-    </table>
-</div>
-
-
-{% endblock %}
\ No newline at end of file
diff --git a/templates/log_viewer.html b/templates/log_viewer.html
deleted file mode 100644
index 8cb4c0d..0000000
--- a/templates/log_viewer.html
+++ /dev/null
@@ -1,66 +0,0 @@
-{% extends "layout.html" %}
-{% block content %}
-    <nav class="navbar navbar-dark bg-dark">
-        <div class="navbar navbar-inverse container">
-            <ul class="nav navbar-nav navbar-center">
-
-                {% if equipment == 'mill' %} <li class="active"> {% else %}
-                <li> {% endif %}
-                    <a href="/log_viewer?equipment=mill">Mill</a></li>
-
-                {% if equipment == 'lathe' %} <li class="active"> {% else %}
-                <li> {% endif %}
-                    <a href="/log_viewer?equipment=lathe">Lathe</a></li>
-
-                {% if equipment == 'laser' %} <li class="active"> {% else %}
-                <li> {% endif %}
-                    <a href="/log_viewer?equipment=laser">Laser</a></li>
-
-                {% if equipment == 'cnc' %} <li class="active"> {% else %}
-                <li> {% endif %}
-                    <a href="/log_viewer?equipment=cnc">CNC</a></li>
-
-                {% if equipment == 'big_cnc' %} <li class="active"> {% else %}
-                <li> {% endif %}
-                    <a href="/log_viewer?equipment=big_cnc">Big CNC</a></li>
-
-                {% if equipment == 'band_saw' %} <li class="active"> {% else %}
-                <li> {% endif %}
-                    <a href="/log_viewer?equipment=band_saw">Band saw</a></li>
-
-            </ul>
-
-        </div>
-    </nav>
-
-    <div class="container">
-        <div class="row">
-
-            <div class="col-xs-4 col-sm-offset-4 text-center">
-
-                <table class="table table-hover">
-                    <thead>
-                        <tr>
-                            <th class="text-center">Nick</th>
-                            <th class="text-center">Date</th>
-                            <th class="text-center">Time</th>
-                            <th class="text-center">Action</th>
-                        </tr>
-                    </thead>
-                    <tbody>
-
-                        {% for record in log_records %}
-                        <tr>
-                            {% for data in record %}
-                            <td>{{data}}</td>
-                            {% endfor %}
-                        </tr>
-                        {% endfor %}
-
-                    </tbody>
-                </table>
-
-            </div>
-        </div>
-    </div>
-{% endblock %}
diff --git a/templates/login.html b/templates/login.html
new file mode 100644
index 0000000..659a9b8
--- /dev/null
+++ b/templates/login.html
@@ -0,0 +1,26 @@
+{% block content %}
+
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Login Form</title>
+    <link rel="stylesheet" href="/static/css/auth.css">
+</head>
+<body>
+<div class="login-container">
+    <h2>Login</h2>
+    <form class="login-form" action="#" method="post">
+        <label for="username">Username:</label>
+        <input type="text" id="username" name="username" required>
+
+        <label for="password">Password:</label>
+        <input type="password" id="password" name="password" required>
+
+        <button type="submit">Login</button>
+    </form>
+</div>
+</body>
+</html>
+
+{% endblock %}
diff --git a/templates/macs.html b/templates/macs.html
deleted file mode 100644
index ff81ad3..0000000
--- a/templates/macs.html
+++ /dev/null
@@ -1,50 +0,0 @@
-<style>
-  .select2-container .select2-selection--single { height: 32px !important}
-</style>
-{% extends "layout.html" %}
-{% block content %}
-<div class="container-fluid p-5">
-  <div class="form-group">
-      <input type="text" class="form-control" id="mac" placeholder="Enter MAC address" style="width:300px; max-width:100%">
-  </div>
-  <div class="form-group">
-      <select id="user_id" class="select2">
-          <option value="">Select a user</option>
-          {% for option in user_options %}
-              <option value="{{option[1]}}">{{option[0]}}</option>
-          {% endfor %}
-      </select>
-  </div>
-  <button type="submit" class="btn btn-primary" onclick="addMac()" >Add MAC</button>
-</div>
-<div class="container-fluid px-5">
-  <div class="py-3">
-      <table id="tblData" class="table table-bordered">
-          <thead>
-              <tr>
-                  <th>Name</th>
-                  <th>MAC</th>
-              </tr>
-          </thead>
-          <tbody id="table_body">
-          {% for item in mac_info %}
-              <tr>
-                  <td>{{item[0]}}</td>
-                  <td>
-                    {% for i in item[1] %}
-                        {% if i %}
-                            <span style="margin-right:10px">{{i}}
-                                <a class="btn btn-danger btn-sm" onclick='deleteMac("{{i}}")' href="#">
-                                    <i class="fa-trash-o fa" style="color: white"></i>
-                                </a>
-                            </span>
-                        {% endif %}
-                    {% endfor %}
-                  </td>
-              </tr>
-          {% endfor %}
-          </tbody>
-      </table>
-  </div>
-</div>
-{% endblock %}
diff --git a/templates/settings.html b/templates/settings.html
new file mode 100644
index 0000000..2e76aa9
--- /dev/null
+++ b/templates/settings.html
@@ -0,0 +1,37 @@
+{% extends "layout.html" %}
+
+{% block content %}
+
+<div class="container-fluid px-md-5">
+    <h1 class="my-4">Settings</h1>
+    <div class="py-3">
+        <form method="post" action="#">
+            <label for="slack_token">Slack Token:</label>
+            <input type="text" id="slack_token" name="slack_token" required value={{ settings.slack_token }}>
+
+            <input type="submit" value="Save slack token">
+        </form>
+
+        <form method="post" action="#">
+            <label for="channel_id">Channel ID:</label>
+            <input type="text" id="channel_id" name="channel_id" required value={{ settings.channel_id }}>
+
+            <input type="submit" value="Save channel id">
+        </form>
+
+        <button id="execute-button">Send backup to slack now</button>
+    </div>
+</div>
+
+<script>
+    $(document).ready(function() {
+        $('#execute-button').click(function() {
+            $.ajax({
+                type: 'POST',
+                url: 'send-backup-to-slack',
+            });
+        });
+    });
+</script>
+
+{% endblock %}
diff --git a/templates/user_page.html b/templates/user_page.html
new file mode 100644
index 0000000..96627af
--- /dev/null
+++ b/templates/user_page.html
@@ -0,0 +1,65 @@
+{% extends "layout.html" %}
+
+{% block content %}
+
+<div class="container-fluid px-md-5">
+    <h1 class="my-4">Profile for user : {{ full_user.user.name }}</h1>
+    <div>
+        <form method="post" action="#">
+            <label for="slack_id">Slack ID:</label>
+            <input type="text" id="slack_id" name="slack_id" required value={{ full_user.user.slack_id }}>
+
+            <input type="submit" value="Set user slack id">
+        </form>
+
+        <button id="execute-button">Send sent notification</button>
+    </div>
+    <div class="py-3">
+        <div style="width: 100%;">
+            <div style="float:left; width: 30%">
+                <ul>
+                    {% for device in full_user.devices %}
+                    <li><a href="/device/{{device.device_id}}">{{ device.device_name }}</a></li>
+                    {% endfor %}
+                </ul>
+            </div>
+            <div style="float:right; width: 70%">
+                <table class="table stripe hover align-middle dataTable no-footer compact pt-3">
+                    <tr>
+                        <th>Device name</th>
+                        <th>Operation type</th>
+                        <th>Time</th>
+                    </tr>
+                    {% for log in full_user.logs %}
+                    <tr>
+                        <td><a href="/device/{{ log.device.id }}">{{ log.device.name }}</a></td>
+                        <td>{{ log.operation.type }}</td>
+                        <td>{{ log.operation.time }}</td>
+                    </tr>
+                    {% endfor %}
+                </table>
+            </div>
+        </div>
+        <div style="clear:both"></div>
+    </div>
+
+</div>
+
+<script>
+    $(document).ready(function() {
+        let jsonData = {
+            user_key: "{{ full_user.user.key }}",
+        };
+        $('#execute-button').click(function() {
+            $.ajax({
+                type: 'POST',
+                url: 'send-test-message-to-user',
+                data: JSON.stringify(jsonData),
+                contentType: 'application/json',
+            });
+        });
+    });
+</script>
+
+
+{% endblock %}
diff --git a/txt_log_reader.py b/txt_log_reader.py
deleted file mode 100644
index 18d7049..0000000
--- a/txt_log_reader.py
+++ /dev/null
@@ -1,52 +0,0 @@
-from flask import render_template, request
-import os
-
-log_file_mill  = "/home/pi/toolsmonitor/logs/mill.txt"
-log_file_lathe = "/home/pi/toolsmonitor/logs/lathelog.txt"
-log_file_laser = "/home/pi/toolsmonitor/logs/laserlog.txt"
-log_file_cnc   = "/home/pi/toolsmonitor/logs/cnclog.txt"
-log_file_b_cnc = "/home/pi/toolsmonitor/logs/bigcnclog.txt"
-log_file_b_saw = "./device_logs/bandsawlog.txt"
-
-def render_logs_to_html():
-    equipment = request.args.get("equipment")
-    
-    if   equipment == "mill":
-            log_file = log_file_mill
-    elif equipment == "lathe":
-            log_file = log_file_lathe
-    elif equipment == "laser":
-            log_file = log_file_laser
-    elif equipment == "cnc":
-            log_file = log_file_cnc
-    elif equipment == "big_cnc":
-            log_file = log_file_b_cnc
-    elif equipment == "band_saw":
-            log_file = log_file_b_saw
-    else:
-            # show mill logs by default
-            log_file = log_file_mill
-            equipment = "mill"
-    
-    log_records = read_log_file(log_file)
-    
-    return render_template("log_viewer.html", log_records=log_records, equipment=equipment)
-
-def read_log_file(log_file):
-    log_records = []
-    
-    # render "we are doomed" error styled message if log file is missing
-    if not os.path.exists(log_file):
-            return ["I need", "to know", "path to", "your logs darling!"]
-    
-    # .txt to [list] heavy lifting
-    with open(log_file, 'r') as raw_log_source:
-            for record in raw_log_source:
-    
-                    # drop '\n', blank space and  and parse record into list
-                    log_records.append(record.split())
-    
-    # reorder log records from newest to oldest
-    log_records.reverse()
-    
-    return log_records
\ No newline at end of file
diff --git a/wsgi.py b/wsgi.py
new file mode 100644
index 0000000..469d103
--- /dev/null
+++ b/wsgi.py
@@ -0,0 +1,6 @@
+import os
+
+from application import app
+
+if __name__ == "__main__":
+    app.run(host='0.0.0.0', port=os.environ.get("FLASK_SERVER_PORT"), debug=True)
\ No newline at end of file