Refactor everything
authorVincent Tondellier <tonton+hg@team1664.org>
Wed, 04 Nov 2015 17:43:00 +0100
changeset 78 0ebef32c34af
parent 77 e408da1419cd
child 79 4ae8bb6f8a96
Refactor everything - change db access method, use Mojo::{Pg,SQLite,...} instead of DBIx::Class - use Minion as job queue - refactor into a non-Lite Mojolicious app
CrashTest.conf
CrashTest.pl
bin/fs_to_db.pl
crash_test.conf
dbicdh/PostgreSQL/deploy/1/001-auto-__VERSION.sql
dbicdh/PostgreSQL/deploy/1/001-auto.sql
dbicdh/PostgreSQL/deploy/2/002-extract_crashing_functions.sql
dbicdh/SQLite/deploy/1/001-auto-__VERSION.sql
dbicdh/SQLite/deploy/1/001-auto.sql
dbicdh/_source/deploy/1/001-auto-__VERSION.yml
dbicdh/_source/deploy/1/001-auto.yml
dist.ini
lib/CrashTest.pm
lib/CrashTest/Command/insert.pm
lib/CrashTest/Commands/db.pm
lib/CrashTest/Controller/CrashInserter.pm
lib/CrashTest/Controller/CrashReports.pm
lib/CrashTest/Helper/Backtrace.pm
lib/CrashTest/Helper/DateTime.pm
lib/CrashTest/Helper/XmlEscape.pm
lib/CrashTest/Helpers/CrashTestHelpers.pm
lib/CrashTest/Model/CrashProcessor.pm
lib/CrashTest/Model/CrashReport.pm
lib/CrashTest/Model/Frame.pm
lib/CrashTest/Model/StackFilter.pm
lib/CrashTest/Model/Storage.pm
lib/CrashTest/Model/Thread.pm
lib/CrashTest/Models/Frame.pm
lib/CrashTest/Models/Thread.pm
lib/CrashTest/Plugin/CrashProcessor/Breakpad.pm
lib/CrashTest/Plugin/StackFilter/FileLink.pm
lib/CrashTest/Plugin/StackFilter/FrameTrust.pm
lib/CrashTest/Plugin/StackFilter/HideArgs.pm
lib/CrashTest/Plugin/Storage/Base.pm
lib/CrashTest/Plugin/Storage/File.pm
lib/CrashTest/Plugin/Storage/Sql.pm
lib/CrashTest/Plugin/Storage/Sql/Command/db.pm
lib/CrashTest/Plugin/Storage/Sql/Model/CrashReport.pm
lib/CrashTest/Plugin/Storage/Sql/migrations_pg.sql
lib/CrashTest/StackFilter.pm
lib/CrashTest/StackFilters/FileLink.pm
lib/CrashTest/StackFilters/FrameTrust.pm
lib/CrashTest/StackFilters/HideArgs.pm
lib/CrashTest/Storage/FileSystem.pm
lib/CrashTest/Storage/Sql.pm
lib/CrashTest/Storage/Sql/Schema.pm
lib/CrashTest/Storage/Sql/Schema/Candy.pm
lib/CrashTest/Storage/Sql/Schema/Result/CrashReport.pm
lib/CrashTest/Storage/Sql/Schema/Result/CrashReportData.pm
lib/CrashTest/Storage/Sql/Schema/Result/CrashUser.pm
lib/CrashTest/Storage/Sql/Schema/Result/Module.pm
lib/CrashTest/Storage/Sql/Schema/Result/Product.pm
script/CrashTest
templates/index.atom.ep
templates/index.html.ep
templates/report/backtrace.html.ep
templates/report/backtrace/frames.html.ep
templates/report/backtrace/warning.html.ep
templates/report/client_info.html.ep
templates/report/crash.html.ep
templates/report/crash_info.html.ep
--- a/CrashTest.conf	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,33 +0,0 @@
-{
-  Dumper => {
-      JSONStackwalker => './stackwalker',
-      SymbolsPath => 'breakpad-debug-symbols/*',
-  },
-#  Storage => {
-#      Type => "CrashTest::Storage::FileSystem",
-#      DataDir => 'data/',
-#  },
-  Storage => {
-      Type => "CrashTest::Storage::Sql",
-      DSN => "dbi:Pg:dbname=crashreports",
-      DataDir => 'data/',
-  },
-  DecodeQueue => {
-      Type => "CrashTest::Decode::Queue::NoQueue"
-  },
-#  DecodeQueue => {
-#      Type => "CrashTest::Decode::Queue::Gearman",
-#      GearmanServers => [ 'localhost:4730' ],
-#  },
-  WebInterface => {
-      ScmLinks => {
-          "svn:svn.example.org/testproject" => 'https://redmine.example.org/projects/testproject/repository/entry/<%= $scmpath =%>?rev=<%= $rev =%>#L<%= $line =%>',
-      },
-      ExtraColumns => {
-          Index => [
-              { id => 'os', db_column => "crash_user.os", name => 'Operating System' },
-          ],
-      },
-  },
-};
-# vim:ft=perl:
--- a/CrashTest.pl	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,145 +0,0 @@
-#!/usr/bin/env perl
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-# ABSTRACT: Web interface for breakpad
-
-use Mojolicious::Lite;
-use Mojo::Loader qw/load_class/;
-use UUID;
-use lib 'lib';
-
-use CrashTest::Models::Frame;
-use CrashTest::Models::Thread;
-use CrashTest::StackFilter;
-
-app->attr(storage => sub {
-    my $self = shift;
-
-    my $storage_class = $self->app->config->{Storage}->{Type};
-    if (my $e = load_class($storage_class)) {
-        die ref $e ? "Exception: $e" : 'Not found!';
-    }
-
-    state $storage = $storage_class->new(
-        config => $self->app->config->{Storage},
-        extra_columns => $self->app->config->{WebInterface}->{ExtraColumns},
-    );
-    return $storage;
-});
-
-app->attr(decode_queue => sub {
-    my $self = shift;
-
-    my $decode_class = $self->app->config->{DecodeQueue}->{Type};
-    if (my $e = load_class($decode_class)) {
-        die ref $e ? "Exception: $e" : 'Not found!';
-    }
-
-    state $decode = $decode_class->new(
-        config => $self->app->config->{DecodeQueue},
-        dumper_config => $self->app->config->{Dumper},
-        storage => $self->app->storage
-    );
-    return $decode;
-});
-
-app->attr(stackfilter => sub {
-    my $self = shift;
-
-    state $stackfilter = CrashTest::StackFilter->new(
-        config => $self->app->config,
-        app => $self->app
-    );
-    return $stackfilter;
-});
-
-get '/' => sub {
-    my $self = shift;
-    my $page = 1;
-    my $crashs_per_page = 25;
-
-    if($self->req->url =~ qr{.*\.atom$}) {
-        $crashs_per_page = 100;
-    }
-
-    $self->validation->required('page')->like(qr/^[0-9]+$/);
-    $page = scalar $self->validation->param("page") if $self->validation->is_valid('page');
-
-    my $result = $self->app->storage->index($page, $crashs_per_page, $self->req->param('search'));
-
-    $self->stash(files => $result->{crashs});
-    $self->stash(pager => $result->{pager});
-    $self->stash(extra_columns => $self->app->config->{WebInterface}->{ExtraColumns}->{Index});
-    $self->render('index');
-} => 'index';
-
-get '/report/:uuid' => [ uuid => qr/[0-9a-fA-F-]+/ ] => sub {
-    my $self = shift;
-
-    my $data = $self->app->storage->get_processed_data($self->param('uuid'));
-    $self->stash(processed_data => $data);
-
-    my $crashing_thread = CrashTest::Models::Thread->new($data->{crashing_thread});
-    $crashing_thread = $self->app->stackfilter->apply($crashing_thread);
-    $self->stash(crashing_thread => $crashing_thread);
-
-    my @threads = ();
-    foreach my $raw_thread(@{$data->{threads}}) {
-        my $thread = CrashTest::Models::Thread->new($raw_thread);
-        $thread = $self->app->stackfilter->apply($thread);
-        push @threads, $thread;
-    }
-    $self->stash(threads => \@threads);
-
-    $self->render('report/crash');
-} => 'report';
-
-post '/submit' => sub {
-    my $self = shift;
-
-    #my @valid_params = qw/Add-ons Distributor ProductName ReleaseChannel StartupTime UserID Version BuildID CrashTime Comments/;
-
-    # save the dump in a file
-    my $file = $self->req->upload('upload_file_minidump');
-
-    # TODO check for authorised values ...
-    my %paramshash = map { $_ => $self->req->param($_) } $self->req->param;
-
-    my ($uuid, $uuidstr);
-    UUID::generate($uuid);
-    UUID::unparse($uuid, $uuidstr);
-
-    $self->render_later();
-
-    $self->app->decode_queue->decode($file, \%paramshash, $uuidstr, sub {
-            my $pjson = shift;
-            # reply
-            $self->render(text => $pjson->{status});
-        }
-    );
-} => 'submit';
-
-app->secrets([
-    'My secret passphrase here'
-]);
-
-push @{app->commands->namespaces}, 'CrashTest::Commands';
-push @{app->plugins->namespaces}, 'CrashTest::Helpers';
-
-plugin 'Config';
-plugin 'bootstrap_pagination';
-plugin 'CrashTestHelpers';
-
-app->start;
--- a/bin/fs_to_db.pl	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-#!/usr/bin/env perl
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-use Mojo::Base -strict;
-use lib 'lib';
-
-use Mojo::JSON qw/decode_json/;
-use Mojo::Util qw/decode slurp/;
-use Mojo::Loader qw/load_class/;
-use File::Basename;
-use Mojolicious;
-use Mojo::Home;
-
-if(scalar @ARGV == 0) {
-    say "Usage: $0 <processed json files>";
-    exit 1;
-}
-
-sub load_config {
-    my $app = Mojolicious->new;
-    $app->home(Mojo::Home->new("$FindBin::Bin/../"));
-    $app->moniker("CrashTest");
-    return $app->plugin('Config');
-}
-
-my $config = load_config();
-
-my $storage_class = $config->{Storage}->{Type};
-if (my $e = load_class($storage_class)) {
-    die ref $e ? "Exception: $e" : 'Not found!';
-}
-
-my $storage = $storage_class->new(config => $config->{Storage});
-
-foreach my $arg (@ARGV) {
-    eval {
-        my $pjson = decode_json(slurp $arg);
-
-        my($filename, $dirs, $suffix) = fileparse($arg, qr/\.[^.]*/);
-
-        my $uuid = $filename;
-        $storage->_db_insert_processed_data($uuid, $pjson);
-    };
-    warn $@ if $@;
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/crash_test.conf	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,30 @@
+{
+  Processor => {
+    Common => {
+      JobQueue => {
+        Backend => {
+          Minion => { Pg => "postgresql:///crashtest" },
+        },
+      },
+    },
+    Breakpad => {
+      JSONStackwalker => 'stackwalker',
+      SymbolsPath => 'data/breakpad-debug-symbols/*',
+    },
+  },
+  Storage => [
+    { Type => "Sql",  db => { Pg => "postgresql:///crashtest" } },
+    { Type => "File", DataDir => 'data/crashs/' },
+  ],
+  WebInterface => {
+    ScmLinks => {
+      "svn:svn.example.org/testproject" => 'https://redmine.example.org/projects/testproject/repository/entry/<%= $scmpath =%>?rev=<%= $rev =%>#L<%= $line =%>',
+    },
+    ExtraColumns => {
+      Index => [
+        { id => 'os', db_column => "crash_user.os", name => 'Operating System' },
+      ],
+    },
+  },
+};
+# vim:ft=perl:
--- a/dbicdh/PostgreSQL/deploy/1/001-auto-__VERSION.sql	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
--- 
--- Created by SQL::Translator::Producer::PostgreSQL
--- Created on Sun Feb  1 19:58:35 2015
--- 
-;
---
--- Table: dbix_class_deploymenthandler_versions.
---
-CREATE TABLE "dbix_class_deploymenthandler_versions" (
-  "id" serial NOT NULL,
-  "version" character varying(50) NOT NULL,
-  "ddl" text,
-  "upgrade_sql" text,
-  PRIMARY KEY ("id"),
-  CONSTRAINT "dbix_class_deploymenthandler_versions_version" UNIQUE ("version")
-);
-
-;
--- a/dbicdh/PostgreSQL/deploy/1/001-auto.sql	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,118 +0,0 @@
--- 
--- Created by SQL::Translator::Producer::PostgreSQL
--- Created on Sun Feb  1 19:58:35 2015
--- 
-;
---
--- Table: crash_users.
---
-CREATE TABLE "crash_users" (
-  "id" serial NOT NULL,
-  "user_id" character varying(40) NOT NULL,
-  "os" character varying(40),
-  "cpu_arch" character varying(10),
-  "cpu_count" integer,
-  "extra_info" text,
-  PRIMARY KEY ("id")
-);
-
-;
---
--- Table: modules.
---
-CREATE TABLE "modules" (
-  "id" serial NOT NULL,
-  "debug_id" character varying(33) NOT NULL,
-  "filename" character varying(128) NOT NULL,
-  "version" character varying(64),
-  PRIMARY KEY ("id"),
-  CONSTRAINT "module_id" UNIQUE ("debug_id", "filename")
-);
-
-;
---
--- Table: products.
---
-CREATE TABLE "products" (
-  "id" serial NOT NULL,
-  "distributor" character varying(40),
-  "name" character varying(40),
-  "version" character varying(16),
-  "release_channel" character varying,
-  PRIMARY KEY ("id")
-);
-
-;
---
--- Table: crash_reports.
---
-CREATE TABLE "crash_reports" (
-  "id" serial NOT NULL,
-  "start_time" timestamp,
-  "crash_time" timestamp,
-  "uuid" character varying(36) NOT NULL,
-  "bug_reference" character varying(20),
-  "crash_user_id" integer NOT NULL,
-  "product_id" integer NOT NULL,
-  PRIMARY KEY ("id")
-);
-CREATE INDEX "crash_reports_idx_crash_user_id" on "crash_reports" ("crash_user_id");
-CREATE INDEX "crash_reports_idx_product_id" on "crash_reports" ("product_id");
-
-;
---
--- Table: crash_threads.
---
-CREATE TABLE "crash_threads" (
-  "id" serial NOT NULL,
-  "number" integer NOT NULL,
-  "crashed" bool NOT NULL,
-  "crash_report_id" integer NOT NULL,
-  PRIMARY KEY ("id")
-);
-CREATE INDEX "crash_threads_idx_crash_report_id" on "crash_threads" ("crash_report_id");
-
-;
---
--- Table: crash_frames.
---
-CREATE TABLE "crash_frames" (
-  "id" serial NOT NULL,
-  "number" integer NOT NULL,
-  "function" character varying(128),
-  "source_file" character varying(128),
-  "source_line" integer,
-  "stack_walk_mode" character varying(10),
-  "crash_thread_id" integer NOT NULL,
-  "module_id" integer NOT NULL,
-  PRIMARY KEY ("id")
-);
-CREATE INDEX "crash_frames_idx_crash_thread_id" on "crash_frames" ("crash_thread_id");
-CREATE INDEX "crash_frames_idx_module_id" on "crash_frames" ("module_id");
-
-;
---
--- Foreign Key Definitions
---
-
-;
-ALTER TABLE "crash_reports" ADD CONSTRAINT "crash_reports_fk_crash_user_id" FOREIGN KEY ("crash_user_id")
-  REFERENCES "crash_users" ("id") DEFERRABLE;
-
-;
-ALTER TABLE "crash_reports" ADD CONSTRAINT "crash_reports_fk_product_id" FOREIGN KEY ("product_id")
-  REFERENCES "products" ("id") DEFERRABLE;
-
-;
-ALTER TABLE "crash_threads" ADD CONSTRAINT "crash_threads_fk_crash_report_id" FOREIGN KEY ("crash_report_id")
-  REFERENCES "crash_reports" ("id") DEFERRABLE;
-
-;
-ALTER TABLE "crash_frames" ADD CONSTRAINT "crash_frames_fk_crash_thread_id" FOREIGN KEY ("crash_thread_id")
-  REFERENCES "crash_threads" ("id") DEFERRABLE;
-
-;
-ALTER TABLE "crash_frames" ADD CONSTRAINT "crash_frames_fk_module_id" FOREIGN KEY ("module_id")
-  REFERENCES "modules" ("id") DEFERRABLE;
-
-;
--- a/dbicdh/PostgreSQL/deploy/2/002-extract_crashing_functions.sql	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-
-CREATE OR REPLACE FUNCTION extract_crashing_functions (processed_json jsonb) RETURNS text AS
-$$
-    -- extract threads[crashing_idx]->frames[*]->function
-    SELECT string_agg(functions, E'\n')
-    FROM (
-        SELECT jsonb_array_elements(
-            (($1 #> ARRAY['threads', $1->'crash_info'->>'crashing_thread'])->'frames')
-        )->>'function' AS functions
-    ) AS frames
-$$
-LANGUAGE sql IMMUTABLE;
-
--- This extension is the contrib modules
-CREATE EXTENSION IF NOT EXISTS pg_trgm;
-
--- Used for searching function in the crashing thread backtrace
--- Search will be really slow if this index is not present
-CREATE INDEX crash_report_datas_idx_extract_crashing_functions ON crash_report_datas USING gist (
-    extract_crashing_functions(processed) gist_trgm_ops
-);
-
--- a/dbicdh/SQLite/deploy/1/001-auto-__VERSION.sql	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
--- 
--- Created by SQL::Translator::Producer::SQLite
--- Created on Sun Feb  1 19:58:35 2015
--- 
-
-;
-BEGIN TRANSACTION;
---
--- Table: dbix_class_deploymenthandler_versions
---
-CREATE TABLE dbix_class_deploymenthandler_versions (
-  id INTEGER PRIMARY KEY NOT NULL,
-  version varchar(50) NOT NULL,
-  ddl text,
-  upgrade_sql text
-);
-CREATE UNIQUE INDEX dbix_class_deploymenthandler_versions_version ON dbix_class_deploymenthandler_versions (version);
-COMMIT;
--- a/dbicdh/SQLite/deploy/1/001-auto.sql	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,83 +0,0 @@
--- 
--- Created by SQL::Translator::Producer::SQLite
--- Created on Sun Feb  1 19:58:35 2015
--- 
-
-;
-BEGIN TRANSACTION;
---
--- Table: crash_users
---
-CREATE TABLE crash_users (
-  id INTEGER PRIMARY KEY NOT NULL,
-  user_id varchar(40) NOT NULL,
-  os varchar(40),
-  cpu_arch varchar(10),
-  cpu_count int,
-  extra_info text
-);
---
--- Table: modules
---
-CREATE TABLE modules (
-  id INTEGER PRIMARY KEY NOT NULL,
-  debug_id varchar(33) NOT NULL,
-  filename varchar(128) NOT NULL,
-  version varchar(64)
-);
-CREATE UNIQUE INDEX module_id ON modules (debug_id, filename);
---
--- Table: products
---
-CREATE TABLE products (
-  id INTEGER PRIMARY KEY NOT NULL,
-  distributor varchar(40),
-  name varchar(40),
-  version varchar(16),
-  release_channel varchar
-);
---
--- Table: crash_reports
---
-CREATE TABLE crash_reports (
-  id INTEGER PRIMARY KEY NOT NULL,
-  start_time timestamp,
-  crash_time timestamp,
-  uuid varchar(36) NOT NULL,
-  bug_reference varchar(20),
-  crash_user_id int NOT NULL,
-  product_id int NOT NULL,
-  FOREIGN KEY (crash_user_id) REFERENCES crash_users(id),
-  FOREIGN KEY (product_id) REFERENCES products(id)
-);
-CREATE INDEX crash_reports_idx_crash_user_id ON crash_reports (crash_user_id);
-CREATE INDEX crash_reports_idx_product_id ON crash_reports (product_id);
---
--- Table: crash_threads
---
-CREATE TABLE crash_threads (
-  id INTEGER PRIMARY KEY NOT NULL,
-  number int NOT NULL,
-  crashed bool NOT NULL,
-  crash_report_id int NOT NULL,
-  FOREIGN KEY (crash_report_id) REFERENCES crash_reports(id)
-);
-CREATE INDEX crash_threads_idx_crash_report_id ON crash_threads (crash_report_id);
---
--- Table: crash_frames
---
-CREATE TABLE crash_frames (
-  id INTEGER PRIMARY KEY NOT NULL,
-  number int NOT NULL,
-  function varchar(128),
-  source_file varchar(128),
-  source_line int,
-  stack_walk_mode varchar(10),
-  crash_thread_id int NOT NULL,
-  module_id int NOT NULL,
-  FOREIGN KEY (crash_thread_id) REFERENCES crash_threads(id),
-  FOREIGN KEY (module_id) REFERENCES modules(id)
-);
-CREATE INDEX crash_frames_idx_crash_thread_id ON crash_frames (crash_thread_id);
-CREATE INDEX crash_frames_idx_module_id ON crash_frames (module_id);
-COMMIT;
--- a/dbicdh/_source/deploy/1/001-auto-__VERSION.yml	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,91 +0,0 @@
----
-schema:
-  procedures: {}
-  tables:
-    dbix_class_deploymenthandler_versions:
-      constraints:
-        - deferrable: 1
-          expression: ''
-          fields:
-            - id
-          match_type: ''
-          name: ''
-          on_delete: ''
-          on_update: ''
-          options: []
-          reference_fields: []
-          reference_table: ''
-          type: PRIMARY KEY
-        - deferrable: 1
-          expression: ''
-          fields:
-            - version
-          match_type: ''
-          name: dbix_class_deploymenthandler_versions_version
-          on_delete: ''
-          on_update: ''
-          options: []
-          reference_fields: []
-          reference_table: ''
-          type: UNIQUE
-      fields:
-        ddl:
-          data_type: text
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: ddl
-          order: 3
-          size:
-            - 0
-        id:
-          data_type: int
-          default_value: ~
-          is_auto_increment: 1
-          is_nullable: 0
-          is_primary_key: 1
-          is_unique: 0
-          name: id
-          order: 1
-          size:
-            - 0
-        upgrade_sql:
-          data_type: text
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: upgrade_sql
-          order: 4
-          size:
-            - 0
-        version:
-          data_type: varchar
-          default_value: ~
-          is_nullable: 0
-          is_primary_key: 0
-          is_unique: 1
-          name: version
-          order: 2
-          size:
-            - 50
-      indices: []
-      name: dbix_class_deploymenthandler_versions
-      options: []
-      order: 1
-  triggers: {}
-  views: {}
-translator:
-  add_drop_table: 0
-  filename: ~
-  no_comments: 0
-  parser_args:
-    sources:
-      - __VERSION
-  parser_type: SQL::Translator::Parser::DBIx::Class
-  producer_args: {}
-  producer_type: SQL::Translator::Producer::YAML
-  show_warnings: 0
-  trace: 0
-  version: 0.11020
--- a/dbicdh/_source/deploy/1/001-auto.yml	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,587 +0,0 @@
----
-schema:
-  procedures: {}
-  tables:
-    crash_frames:
-      constraints:
-        - deferrable: 1
-          expression: ''
-          fields:
-            - id
-          match_type: ''
-          name: ''
-          on_delete: ''
-          on_update: ''
-          options: []
-          reference_fields: []
-          reference_table: ''
-          type: PRIMARY KEY
-        - deferrable: 1
-          expression: ''
-          fields:
-            - crash_thread_id
-          match_type: ''
-          name: crash_frames_fk_crash_thread_id
-          on_delete: ''
-          on_update: ''
-          options: []
-          reference_fields:
-            - id
-          reference_table: crash_threads
-          type: FOREIGN KEY
-        - deferrable: 1
-          expression: ''
-          fields:
-            - module_id
-          match_type: ''
-          name: crash_frames_fk_module_id
-          on_delete: ''
-          on_update: ''
-          options: []
-          reference_fields:
-            - id
-          reference_table: modules
-          type: FOREIGN KEY
-      fields:
-        crash_thread_id:
-          data_type: int
-          default_value: ~
-          is_nullable: 0
-          is_primary_key: 0
-          is_unique: 0
-          name: crash_thread_id
-          order: 7
-          size:
-            - 0
-        function:
-          data_type: varchar
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: function
-          order: 3
-          size:
-            - 128
-        id:
-          data_type: int
-          default_value: ~
-          is_auto_increment: 1
-          is_nullable: 0
-          is_primary_key: 1
-          is_unique: 0
-          name: id
-          order: 1
-          size:
-            - 0
-        module_id:
-          data_type: int
-          default_value: ~
-          is_nullable: 0
-          is_primary_key: 0
-          is_unique: 0
-          name: module_id
-          order: 8
-          size:
-            - 0
-        number:
-          data_type: int
-          default_value: ~
-          is_nullable: 0
-          is_primary_key: 0
-          is_unique: 0
-          name: number
-          order: 2
-          size:
-            - 0
-        source_file:
-          data_type: varchar
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: source_file
-          order: 4
-          size:
-            - 128
-        source_line:
-          data_type: int
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: source_line
-          order: 5
-          size:
-            - 0
-        stack_walk_mode:
-          data_type: varchar
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: stack_walk_mode
-          order: 6
-          size:
-            - 10
-      indices:
-        - fields:
-            - crash_thread_id
-          name: crash_frames_idx_crash_thread_id
-          options: []
-          type: NORMAL
-        - fields:
-            - module_id
-          name: crash_frames_idx_module_id
-          options: []
-          type: NORMAL
-      name: crash_frames
-      options: []
-      order: 6
-    crash_reports:
-      constraints:
-        - deferrable: 1
-          expression: ''
-          fields:
-            - id
-          match_type: ''
-          name: ''
-          on_delete: ''
-          on_update: ''
-          options: []
-          reference_fields: []
-          reference_table: ''
-          type: PRIMARY KEY
-        - deferrable: 1
-          expression: ''
-          fields:
-            - crash_user_id
-          match_type: ''
-          name: crash_reports_fk_crash_user_id
-          on_delete: ''
-          on_update: ''
-          options: []
-          reference_fields:
-            - id
-          reference_table: crash_users
-          type: FOREIGN KEY
-        - deferrable: 1
-          expression: ''
-          fields:
-            - product_id
-          match_type: ''
-          name: crash_reports_fk_product_id
-          on_delete: ''
-          on_update: ''
-          options: []
-          reference_fields:
-            - id
-          reference_table: products
-          type: FOREIGN KEY
-      fields:
-        bug_reference:
-          data_type: varchar
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: bug_reference
-          order: 5
-          size:
-            - 20
-        crash_time:
-          data_type: timestamp
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: crash_time
-          order: 3
-          size:
-            - 0
-        crash_user_id:
-          data_type: int
-          default_value: ~
-          is_nullable: 0
-          is_primary_key: 0
-          is_unique: 0
-          name: crash_user_id
-          order: 6
-          size:
-            - 0
-        id:
-          data_type: int
-          default_value: ~
-          is_auto_increment: 1
-          is_nullable: 0
-          is_primary_key: 1
-          is_unique: 0
-          name: id
-          order: 1
-          size:
-            - 0
-        product_id:
-          data_type: int
-          default_value: ~
-          is_nullable: 0
-          is_primary_key: 0
-          is_unique: 0
-          name: product_id
-          order: 7
-          size:
-            - 0
-        start_time:
-          data_type: timestamp
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: start_time
-          order: 2
-          size:
-            - 0
-        uuid:
-          data_type: varchar
-          default_value: ~
-          is_nullable: 0
-          is_primary_key: 0
-          is_unique: 0
-          name: uuid
-          order: 4
-          size:
-            - 36
-      indices:
-        - fields:
-            - crash_user_id
-          name: crash_reports_idx_crash_user_id
-          options: []
-          type: NORMAL
-        - fields:
-            - product_id
-          name: crash_reports_idx_product_id
-          options: []
-          type: NORMAL
-      name: crash_reports
-      options: []
-      order: 4
-    crash_threads:
-      constraints:
-        - deferrable: 1
-          expression: ''
-          fields:
-            - id
-          match_type: ''
-          name: ''
-          on_delete: ''
-          on_update: ''
-          options: []
-          reference_fields: []
-          reference_table: ''
-          type: PRIMARY KEY
-        - deferrable: 1
-          expression: ''
-          fields:
-            - crash_report_id
-          match_type: ''
-          name: crash_threads_fk_crash_report_id
-          on_delete: ''
-          on_update: ''
-          options: []
-          reference_fields:
-            - id
-          reference_table: crash_reports
-          type: FOREIGN KEY
-      fields:
-        crash_report_id:
-          data_type: int
-          default_value: ~
-          is_nullable: 0
-          is_primary_key: 0
-          is_unique: 0
-          name: crash_report_id
-          order: 4
-          size:
-            - 0
-        crashed:
-          data_type: bool
-          default_value: ~
-          is_nullable: 0
-          is_primary_key: 0
-          is_unique: 0
-          name: crashed
-          order: 3
-          size:
-            - 0
-        id:
-          data_type: int
-          default_value: ~
-          is_auto_increment: 1
-          is_nullable: 0
-          is_primary_key: 1
-          is_unique: 0
-          name: id
-          order: 1
-          size:
-            - 0
-        number:
-          data_type: int
-          default_value: ~
-          is_nullable: 0
-          is_primary_key: 0
-          is_unique: 0
-          name: number
-          order: 2
-          size:
-            - 0
-      indices:
-        - fields:
-            - crash_report_id
-          name: crash_threads_idx_crash_report_id
-          options: []
-          type: NORMAL
-      name: crash_threads
-      options: []
-      order: 5
-    crash_users:
-      constraints:
-        - deferrable: 1
-          expression: ''
-          fields:
-            - id
-          match_type: ''
-          name: ''
-          on_delete: ''
-          on_update: ''
-          options: []
-          reference_fields: []
-          reference_table: ''
-          type: PRIMARY KEY
-      fields:
-        cpu_arch:
-          data_type: varchar
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: cpu_arch
-          order: 4
-          size:
-            - 10
-        cpu_count:
-          data_type: int
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: cpu_count
-          order: 5
-          size:
-            - 0
-        extra_info:
-          data_type: text
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: extra_info
-          order: 6
-          size:
-            - 0
-        id:
-          data_type: int
-          default_value: ~
-          is_auto_increment: 1
-          is_nullable: 0
-          is_primary_key: 1
-          is_unique: 0
-          name: id
-          order: 1
-          size:
-            - 0
-        os:
-          data_type: varchar
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: os
-          order: 3
-          size:
-            - 40
-        user_id:
-          data_type: varchar
-          default_value: ~
-          is_nullable: 0
-          is_primary_key: 0
-          is_unique: 0
-          name: user_id
-          order: 2
-          size:
-            - 40
-      indices: []
-      name: crash_users
-      options: []
-      order: 1
-    modules:
-      constraints:
-        - deferrable: 1
-          expression: ''
-          fields:
-            - id
-          match_type: ''
-          name: ''
-          on_delete: ''
-          on_update: ''
-          options: []
-          reference_fields: []
-          reference_table: ''
-          type: PRIMARY KEY
-        - deferrable: 1
-          expression: ''
-          fields:
-            - debug_id
-            - filename
-          match_type: ''
-          name: module_id
-          on_delete: ''
-          on_update: ''
-          options: []
-          reference_fields: []
-          reference_table: ''
-          type: UNIQUE
-      fields:
-        debug_id:
-          data_type: varchar
-          default_value: ~
-          is_nullable: 0
-          is_primary_key: 0
-          is_unique: 1
-          name: debug_id
-          order: 2
-          size:
-            - 33
-        filename:
-          data_type: varchar
-          default_value: ~
-          is_nullable: 0
-          is_primary_key: 0
-          is_unique: 1
-          name: filename
-          order: 3
-          size:
-            - 128
-        id:
-          data_type: int
-          default_value: ~
-          is_auto_increment: 1
-          is_nullable: 0
-          is_primary_key: 1
-          is_unique: 0
-          name: id
-          order: 1
-          size:
-            - 0
-        version:
-          data_type: varchar
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: version
-          order: 4
-          size:
-            - 64
-      indices: []
-      name: modules
-      options: []
-      order: 2
-    products:
-      constraints:
-        - deferrable: 1
-          expression: ''
-          fields:
-            - id
-          match_type: ''
-          name: ''
-          on_delete: ''
-          on_update: ''
-          options: []
-          reference_fields: []
-          reference_table: ''
-          type: PRIMARY KEY
-      fields:
-        distributor:
-          data_type: varchar
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: distributor
-          order: 2
-          size:
-            - 40
-        id:
-          data_type: int
-          default_value: ~
-          is_auto_increment: 1
-          is_nullable: 0
-          is_primary_key: 1
-          is_unique: 0
-          name: id
-          order: 1
-          size:
-            - 0
-        name:
-          data_type: varchar
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: name
-          order: 3
-          size:
-            - 40
-        release_channel:
-          data_type: varchar
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: release_channel
-          order: 5
-          size:
-            - 0
-        version:
-          data_type: varchar
-          default_value: ~
-          is_nullable: 1
-          is_primary_key: 0
-          is_unique: 0
-          name: version
-          order: 4
-          size:
-            - 16
-      indices: []
-      name: products
-      options: []
-      order: 3
-  triggers: {}
-  views: {}
-translator:
-  add_drop_table: 0
-  filename: ~
-  no_comments: 0
-  parser_args:
-    sources:
-      - CrashFrame
-      - CrashReport
-      - CrashThread
-      - CrashUser
-      - Module
-      - Product
-  parser_type: SQL::Translator::Parser::DBIx::Class
-  producer_args: {}
-  producer_type: SQL::Translator::Producer::YAML
-  show_warnings: 0
-  trace: 0
-  version: 0.11020
--- a/dist.ini	Sun Sep 27 22:45:40 2015 +0200
+++ b/dist.ini	Wed Nov 04 17:43:00 2015 +0100
@@ -1,10 +1,10 @@
 name    = CrashTest
-version = 0.1.0
+version = 0.2.0
 author  = Vincent Tondellier <tonton@team1664.org>
 license = GPL_3
 copyright_holder = Vincent Tondellier
 
-main_module = CrashTest.pl
+main_module = script/crash_test.pl
 
 [@Basic]
 [Mercurial::Check]
@@ -25,23 +25,24 @@
 Mojolicious::Plugin::BootstrapPagination = 0.12
 UUID = 0.0.3
 Text::Balanced = 0
+Data::Page = 0
+Minion = 2.00
+
 [Prereqs / RuntimeRecommends]
-; Gearman Queue (also contains the Worker class)
-Gearman::Client = 1.10
-; Database Storage (minimal tested versions, may work with older releases)
-DBIx::Class::DeploymentHandler = 0.002000
-DBIx::Class = 0.08196
-DBIx::Class::Candy = 0.002100
 ; For search field (only for SQL)
-Search::Query = 0.300
-Search::Query::Dialect::DBIxClass = 0.005
-; PostgreSQL
+Search::QueryParser = 0.90
+Search::QueryParser::Sql = 0.10
+; PostgreSQL (recommended)
 DateTime::Format::Pg = 0
 DBD::Pg = 0
-; SQLite
+Mojo::Pg = 2.10
+; SQLite (untested)
 DateTime::Format::SQLite = 0
 DBD::SQLite = 0
-; MySQL
+Mojo::SQLite = 0.010
+Minion::Backend::SQLite = 0
+; MySQL (untested)
 DateTime::Format::MySQL = 0
 DBD::mysql = 0
-
+Mojo::mysql = 0
+Minion::Backend::mysql = 0
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,53 @@
+package CrashTest;
+use Mojo::Base 'Mojolicious';
+
+use CrashTest::Model::Storage;
+use CrashTest::Model::StackFilter;
+use CrashTest::Model::CrashReport;
+use CrashTest::Model::CrashProcessor;
+
+# This method will run once at server start
+sub startup {
+    my $self = shift;
+
+    $self->secrets([
+        'My secret passphrase here'
+    ]);
+
+    # External plugins
+    $self->plugin('Config');
+    $self->plugin('bootstrap_pagination');
+    # Documentation browser under "/perldoc"
+    #$self->plugin('PODRenderer');
+
+    # Commands
+    push @{$self->commands->namespaces}, 'CrashTest::Command';
+
+    # Helpers
+    $self->plugin("CrashTest::Helper::DateTime");
+    $self->plugin("CrashTest::Helper::Backtrace");
+    $self->plugin("CrashTest::Helper::XmlEscape");
+
+
+    $self->helper(crash_reports     => sub { state $crash_reports   = CrashTest::Model::CrashReport->new    (app => $self); });
+
+    $self->helper(crash_processor   => sub { state $crash_processor = CrashTest::Model::CrashProcessor->new (app => $self, config => $self->config); });
+    $self->helper(stackfilter       => sub { state $crash_reports   = CrashTest::Model::StackFilter->new    (app => $self, config => $self->config); });
+    $self->helper(storage           => sub { state $storage         = CrashTest::Model::Storage->new        (app => $self, config => $self->config); });
+
+    $self->plugin('Minion', $self->config->{Processor}->{Common}->{JobQueue}->{Backend}->{Minion});
+
+    $self->storage->load_plugins();
+    $self->crash_processor->load_plugins();
+
+    # Router
+    my $r = $self->routes;
+
+    # Normal route to controller
+    $r->get('/')->to('crash_reports#index')->name('index');
+    $r->get('/report/:uuid' => [ uuid => qr/[0-9a-fA-F-]+/ ])->to('crash_reports#report')->name('report');
+
+    $r->post('/submit')->to('crash_inserter#insert');
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Command/insert.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,55 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Command::insert;
+use Mojo::Base 'Mojolicious::Command';
+use Mojo::JSON qw/decode_json/;
+use Mojo::Util qw/slurp/;
+use File::Spec::Functions qw/catdir catfile/;
+use File::Basename;
+
+# Short description
+has description => 'Insert crash';
+
+# Short usage message
+has usage => <<EOF;
+Usage: APPLICATION insert file ...
+EOF
+
+sub run {
+    my ($self, @args) = @_;
+
+    if(scalar @args < 1) {
+        say $self->usage;
+        exit 0;
+    }
+
+    foreach my $jsonfile(@args) {
+        my ($uuid,$path,$suffix) = fileparse($jsonfile, qw/.json/);
+
+        my $dmp = undef;
+        if(-e catfile($path, "$uuid.dmp")) {
+            $dmp = slurp(catfile($path, "$uuid.dmp"));
+        }
+
+        eval {
+            my $pjson = decode_json(slurp(catfile($path, "$uuid.json")));
+            $self->app->crash_reports->create($uuid, $pjson, $pjson->{client_info}, $dmp);
+        };
+        warn $@ if $@;
+
+    }
+
+}
+
+1;
--- a/lib/CrashTest/Commands/db.pm	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,62 +0,0 @@
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-package CrashTest::Commands::db;
-use Mojo::Base 'Mojolicious::Command';
-use v5.12;
-
-use DBIx::Class::DeploymentHandler;
-use CrashTest::Storage::Sql::Schema;
-
-# Short description
-has description => 'Setup database.';
-
-# Short usage message
-has usage => <<EOF;
-Usage: APPLICATION db [OPTIONS]
-
-  create        Create database
-
-EOF
-
-sub run {
-    my ($self, @args) = @_;
-
-    my $schema = CrashTest::Storage::Sql::Schema->connect($self->app->config->{Storage}->{DSN});
-
-    my $dhargs = {
-        schema              => $schema,
-        script_directory    => "$FindBin::Bin/dbicdh",
-        sql_translator_args => { add_drop_table => 0 },
-        force_overwrite     => 0,
-    };
-
-    if($args[0] eq "prepare") {
-        $dhargs->{force_overwrite} = 1;
-        my $dh = DBIx::Class::DeploymentHandler->new($dhargs);
-        $dh->prepare_install;
-#        $dh->prepare_upgrade;
-    } elsif($args[0] eq "create") {
-        my $dh = DBIx::Class::DeploymentHandler->new($dhargs);
-        $dh->install;
-#    } elsif($args[0] eq "upgrade") {
-#        my $dh = DBIx::Class::DeploymentHandler->new($dhargs);
-#        $dh->upgrade;
-    } else {
-        say "Invalid arguments";
-        exit 1;
-    }
-
-}
-
-1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Controller/CrashInserter.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,28 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Controller::CrashInserter;
+use Mojo::Base 'Mojolicious::Controller';
+
+sub insert {
+    my $self = shift;
+
+    my $processor_task = $self->app->crash_processor->find_processor($self->req);
+    return $self->render(text => "No processor found", status => 421) if !$processor_task;
+
+    $self->app->crash_processor->decode($processor_task, $self->req);
+
+    return $self->render(text => "ok");
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Controller/CrashReports.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,69 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Controller::CrashReports;
+use Mojo::Base 'Mojolicious::Controller';
+use Mojo::Util qw/dumper/;
+
+use CrashTest::Model::Thread;
+
+sub index {
+    my $self = shift;
+
+    my $page = 1;
+    my $crashs_per_page = 25;
+
+    if($self->req->url =~ qr{.*\.atom$}) {
+        $crashs_per_page = 100;
+    }
+
+    $self->validation->required('page')->like(qr/^[0-9]+$/);
+    $page = scalar $self->validation->param("page") if $self->validation->is_valid('page');
+
+    my ($results, $pager) = $self->crash_reports->index($page, $crashs_per_page, $self->req->param('search'));
+
+    #$self->app->log->debug(dumper $results);
+
+    $self->stash(crashs => $results);
+    $self->stash(pager => $pager);
+    $self->stash(extra_columns => []);
+
+    $self->render("index");
+}
+
+sub report {
+    my ($self, $uuid) = @_;
+
+    my $data = $self->app->crash_reports->get_processed_data($self->param('uuid'));
+    $self->stash(processed_data => $data);
+
+    my $crashing_thread = CrashTest::Model::Thread->new($data->{crashing_thread});
+    $crashing_thread = $self->app->stackfilter->apply($crashing_thread);
+    $self->stash(crashing_thread => $crashing_thread);
+
+    my @threads = ();
+    foreach my $raw_thread(@{$data->{threads}}) {
+        my $thread = CrashTest::Model::Thread->new($raw_thread);
+        $thread = $self->app->stackfilter->apply($thread);
+        push @threads, $thread;
+    }
+    $self->stash(threads => \@threads);
+
+    #my $similar = $self->app->storage->get_similar_crashs($self->param('uuid'));
+    #$self->stash(similar => $similar->{crashs});
+    $self->stash(extra_columns => $self->app->config->{WebInterface}->{ExtraColumns}->{Index});
+
+    $self->render("report/crash");
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Helper/Backtrace.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,44 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Helper::Backtrace;
+use Mojo::Base 'Mojolicious::Plugin';
+
+sub register {
+    my ($self, $app, $conf) = @_;
+
+    $app->helper(module_warnings => sub { $self->_bt_warning("module_warnings", @_) } );
+    $app->helper(frame_warnings  => sub { $self->_bt_warning("frame_warnings", @_) } );
+}
+
+sub _bt_warning {
+    my ($self, $type, $c, $frame) = @_;
+
+    my $warnings;
+    if($type eq "frame_warnings") {
+        $warnings = $frame->frame_warnings;
+    } elsif($type eq "module_warnings") {
+        $warnings = $frame->module_warnings;
+    }
+
+    return "" if(!$warnings);
+
+    my @ret;
+    foreach my $warning (@{$warnings}) {
+        push @ret, scalar $c->render_to_string('report/backtrace/warning', tooltip_text => $warning);
+    }
+
+    return Mojo::ByteStream->new(join "", @ret);
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Helper/DateTime.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,46 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Helper::DateTime;
+use Mojo::Base 'Mojolicious::Plugin';
+
+use DateTime::Format::Pg;
+
+sub register {
+    my ($self, $app, $conf) = @_;
+
+    $app->helper(date_from_db_utc => sub {
+        my $d = $self->_date_from_db_utc(@_);
+        return "" unless $d;
+        $d->set_time_zone('local')->strftime("%F %T");
+    });
+
+    $app->helper(date_with_tz_from_db_utc => sub {
+        my $d = $self->_date_from_db_utc(@_);
+        return "" unless $d;
+        $d->strftime("%FT%TZ");
+    });
+}
+
+sub _date_from_db_utc {
+    my ($self, $c, $dbdate) = @_;
+    my $utcdate;
+    eval {
+        $utcdate = DateTime::Format::Pg->parse_datetime($dbdate)->set_time_zone('UTC');
+    };
+    $c->app->log->warn($@) if $@;
+
+    return $utcdate;
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Helper/XmlEscape.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,31 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Helper::XmlEscape;
+use Mojo::Base 'Mojolicious::Plugin';
+use Mojo::ByteStream qw/b/;
+use Mojo::Util qw/xml_escape/;
+
+sub register {
+  my ($self, $mojo, $params) = @_;
+
+  $mojo->helper(xml_escape_block => sub { return $self->xml_escape_block(@_); });
+}
+
+sub xml_escape_block {
+    my ($self, $c, $block) = @_;
+    my $result = $block->();
+    return b(xml_escape($result));
+}
+
+1;
--- a/lib/CrashTest/Helpers/CrashTestHelpers.pm	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,35 +0,0 @@
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-package CrashTest::Helpers::CrashTestHelpers;
-use Mojo::Base 'Mojolicious::Plugin';
-use Mojo::ByteStream qw/b/;
-use Mojo::Util qw/xml_escape/;
-
-sub register {
-  my ($self, $mojo, $params) = @_;
-
-  $mojo->helper(
-      xml_escape_block => sub {
-          return $self->xml_escape_block(@_);
-      }
-  );
-}
-
-sub xml_escape_block {
-    my ($self, $c, $block) = @_;
-    my $result = $block->();
-    return b(xml_escape($result));
-}
-
-1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Model/CrashProcessor.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,78 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Model::CrashProcessor;
+use Mojo::Base -base;
+use File::Temp;
+use UUID;
+
+has [ qw/app config/ ];
+has processors => sub { return [] };
+
+sub _register_instance {
+    my ($self, $processor_instance) = @_;
+
+    push @{$self->processors}, $processor_instance;
+}
+
+sub load_plugins {
+    my ($self) = @_;
+
+    my @conf_processors = grep(!/^Common$/, keys %{$self->config->{Processor}});
+    for my $module (@conf_processors) {
+        $self->app->plugin('CrashTest::Plugin::CrashProcessor::' . $module, {
+                config => $self->config,
+                cb => sub { my $i = shift; $self->_register_instance($i); }
+        });
+    }
+}
+
+sub find_processor {
+    my ($self, $req) = @_;
+
+    # find the first processor that accepts this kind of crash
+    foreach my $processor(@{$self->processors}) {
+        my $validator = $processor->validate($req);
+        if(!$validator->has_error) {
+            return $processor->task_name;
+        }
+    }
+    return undef;
+}
+
+sub decode {
+    my ($self, $task_name, $req) = @_;
+
+    my $tmpdir = $self->config->{Processor}->{Common}->{TmpDir};
+
+    my $files;
+    foreach my $up_name(map { $_->name } @{$req->uploads}) {
+        foreach my $up(@{$req->every_upload($up_name)}) {
+            my $fh = File::Temp->new(DIR => $tmpdir, SUFFIX => '.dat');
+            $fh->unlink_on_destroy(0);
+
+            #print $fh $up->slurp;
+            $up->move_to($fh->filename);
+
+            $files->{$up_name} ||= [];
+            push @{$files->{$up_name}}, $fh->filename;
+        }
+    }
+
+    my ($uuid, $uuidstr);
+    UUID::generate($uuid);
+    UUID::unparse($uuid, $uuidstr);
+    my $id = $self->app->minion->enqueue($task_name => [ $uuidstr, $req->params->to_hash, $files ]);
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Model/CrashReport.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,37 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Model::CrashReport;
+use Mojo::Base -base;
+
+has [ qw/app config/ ];
+
+sub index {
+    my ($self, $page, $nperpage, $search_str) = @_;
+
+    return $self->app->storage->first("index", $page, $nperpage, $search_str);
+}
+
+sub create {
+    my ($self, $uuid, $pjson, $dump) = @_;
+
+    return $self->app->storage->each("create", $uuid, $pjson, $dump);
+}
+
+sub get_processed_data {
+    my ($self, $uuid) = @_;
+
+    return $self->app->storage->first("get_processed_data", $uuid);
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Model/Frame.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,49 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Model::Frame;
+use Mojo::Base -base;
+use Mojo::ByteStream 'b';
+use Mojolicious::Plugin::TagHelpers;
+use File::Basename;
+
+# from json
+has [ qw/frame module function file line trust/ ];
+has [ qw/function_offset module_offset offset/ ];
+has [ qw/missing_symbols corrupt_symbols/ ];
+
+# added
+has frame_number => undef;
+has module_name => "";
+has function_name => "";
+has file_link => "";
+has frame_warnings => sub { return []; };
+has module_warnings => sub { return []; };
+#has "infos";
+
+sub new {
+    my $self = shift->SUPER::new(@_);
+
+    # defaults
+    $self->frame_number($self->frame);
+    $self->function_name($self->function);
+    $self->module_name($self->module);
+    if(defined($self->file)) {
+        my $filename = fileparse($self->file);
+        $self->file_link($filename . ":" . $self->line);
+    }
+
+    return $self;
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Model/StackFilter.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,80 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Model::StackFilter;
+use Mojo::Base -base;
+use Mojo::Loader qw/load_class find_modules/;
+
+has [ qw/config app filters/ ];
+
+sub new {
+    my $self = shift->SUPER::new(@_);
+
+    if(defined($self->config->{StackFilters})) {
+        $self->load_plugins($self->config->{StackFilters});
+    } else {
+        $self->find_stackfilters_plugins();
+    }
+
+    return $self;
+}
+
+sub apply {
+    my ($self, $thread) = @_;
+
+    foreach my $filter(@{$self->filters}) {
+        #say "apply filter $filter";
+        $thread = $filter->apply($thread);
+    }
+
+    return $thread;
+}
+
+sub load_plugins {
+    my ($self, $modules) = @_;
+
+    my @filters = ();
+    for my $module (@$modules) {
+        my $e = load_class($module);
+        die qq{Loading "$module" failed: $e} if ref $e;
+
+        #say "loading $module";
+        push @filters, $module->new(config => $self->config, app => $self->app);
+    }
+
+    $self->filters(\@filters);
+}
+
+sub find_stackfilters_plugins {
+    my ($self) = @_;
+
+    my @modules = ();
+
+    # Find modules in a namespace
+    for my $module (find_modules('CrashTest::Plugin::StackFilter')) {
+        my $e = load_class($module);
+        warn qq{Loading "$module" failed: $e} and next if ref $e;
+
+        push @modules, [ $module, $module->priority ];
+    }
+
+    # sort by prio
+    @modules = sort { $b->[1] <=> $a->[1] } @modules;
+
+    # instanciate
+    my @filters = map { $_->[0]->new(config => $self->config, app => $self->app) } @modules;
+
+    $self->filters(\@filters);
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Model/Storage.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,70 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Model::Storage;
+use Mojo::Base -base;
+
+has [ qw/app config/ ];
+has instances => sub { return [] };
+
+sub register {
+    my ($self, $storage_instance) = @_;
+
+    push @{$self->instances}, $storage_instance;
+
+    #$self->app->log->debug("Loaded $storage_instance");
+}
+
+sub load_plugins {
+    my $self = shift;
+
+    for my $storage(@{$self->config->{Storage}}) {
+        $self->app->plugin("CrashTest::Plugin::Storage::" . $storage->{Type}, $storage);
+    }
+}
+
+
+sub each {
+    my ($self, $proc, @args) = @_;
+
+    my $result = 1;
+    foreach my $storage(@{$self->instances}) {
+        if(defined(my $model = $storage->models->{CrashReport})) {
+            if($model->can($proc)) {
+                $result = $result && $model->$proc(@args);
+            }
+        }
+    }
+    return $result;
+}
+
+sub first {
+    my ($self, $proc, @args) = @_;
+
+    my @result;
+    foreach my $storage(@{$self->instances}) {
+
+        if(defined(my $model = $storage->models->{CrashReport})) {
+            if($model->can($proc)) {
+                @result = $model->$proc(@args);
+                if(@result) {
+                    last;
+                }
+            }
+        }
+    }
+
+    return wantarray ? @result : shift @result;
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Model/Thread.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,40 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Model::Thread;
+use Mojo::Base -base;
+use Mojo::ByteStream 'b';
+use Mojolicious::Plugin::TagHelpers;
+use CrashTest::Model::Frame;
+
+has [ qw/frame_count crashing_thread frames/ ];
+
+sub new {
+    my $self = shift->SUPER::new(@_);
+
+    foreach my $frame(@{$self->frames}) {
+        $frame = CrashTest::Model::Frame->new($frame);
+    }
+
+    return $self;
+}
+
+sub each_frame {
+    my ($self, $block) = @_;
+
+    foreach my $frame(@{$self->frames}) {
+        $block->($frame);
+    }
+}
+
+1;
--- a/lib/CrashTest/Models/Frame.pm	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,53 +0,0 @@
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-package CrashTest::Models::Frame;
-use Mojo::Base -base;
-use Mojo::ByteStream 'b';
-use Mojolicious::Plugin::TagHelpers;
-use File::Basename;
-
-# from json
-has [ qw/frame module function file line trust/ ];
-has [ qw/function_offset module_offset offset/ ];
-has [ qw/missing_symbols corrupt_symbols/ ];
-
-# added
-has [ qw/module_name function_name file_link frame_number/ ];
-has [ qw/warnings infos/ ];
-
-sub new {
-    my $self = shift->SUPER::new(@_);
-
-    # defaults
-    $self->frame_number($self->frame);
-    $self->function_name($self->function);
-    $self->module_name($self->module);
-    if(defined($self->file)) {
-        my $filename = fileparse($self->file);
-        $self->file_link($filename . ":" . $self->line);
-    }
-    #$self->frame_number($self->frame);
-    $self->warnings([]);
-    $self->add_warning("hello world!");
-
-    return $self;
-}
-
-sub add_warning {
-    my ($self, $args) = @_;
-
-    push @{$self->warnings}, $args;
-}
-
-1;
--- a/lib/CrashTest/Models/Thread.pm	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,40 +0,0 @@
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-package CrashTest::Models::Thread;
-use Mojo::Base -base;
-use Mojo::ByteStream 'b';
-use Mojolicious::Plugin::TagHelpers;
-use CrashTest::Models::Frame;
-
-has [ qw/frame_count crashing_thread frames/ ];
-
-sub new {
-    my $self = shift->SUPER::new(@_);
-
-    foreach my $frame(@{$self->frames}) {
-        $frame = CrashTest::Models::Frame->new($frame);
-    }
-
-    return $self;
-}
-
-sub each_frame {
-    my ($self, $block) = @_;
-
-    foreach my $frame(@{$self->frames}) {
-        $block->($frame);
-    }
-}
-
-1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Plugin/CrashProcessor/Breakpad.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,87 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Plugin::CrashProcessor::Breakpad;
+use Mojo::Base 'Mojolicious::Plugin';
+use Mojo::JSON qw/decode_json/;
+use Mojo::Util qw/dumper/;
+use Mojolicious::Validator;
+
+has [ qw/app config dumper_config/ ];
+has task_name => "breakpad_decode_task";
+
+sub register {
+    my ($self, $app, $args) = @_;
+
+    $self->app($app);
+    $self->config($args->{config});
+    $self->dumper_config($self->config->{Processor}->{Breakpad});
+
+    $app->minion->add_task($self->task_name =>
+        sub {
+            my ($job, $uuid, $params, $files) = @_;
+            #$job->app->log->debug(dumper $params);
+
+            $job->on(failed => sub { $self->cleanup($files); });
+            #$job->on(finished => sub { $self->cleanup($files); });
+
+            $self->decode($uuid, $params, $files);
+            $self->cleanup($files);
+        }
+    );
+
+    $app->log->debug("Registered Breakpad");
+    $args->{cb}->($self);
+}
+
+sub validate {
+    my ($self, $req) = @_;
+
+    my $hash = $req->params->to_hash;
+    $hash->{$_} = $req->every_upload($_) for map { $_->name } @{$req->uploads};
+
+    my $validation = Mojolicious::Validator->new->validation->input($hash);
+
+    $validation->required('UserID');
+    #$validation->required('Version');
+    $validation->required('ProductName');
+
+    $validation->required('upload_file_minidump')->upload->size(1024, 4 * 1024 * 1024);
+
+    return $validation;
+}
+
+sub decode {
+    my ($self, $uuidstr, $client_info, $files) = @_;
+
+    my $dmp_file = $files->{upload_file_minidump}->[0];
+
+    my $cmd = $self->dumper_config->{JSONStackwalker} . " $dmp_file " . $self->dumper_config->{SymbolsPath};
+    my $out = qx($cmd 2>/dev/null) or die $!;
+
+    my $pjson = decode_json($out);
+
+    $self->app->crash_reports->create($uuidstr, $pjson, $client_info, $dmp_file);
+}
+
+sub cleanup {
+    my ($self, $files) = @_;
+    #$self->app->log->debug("cleanup " . dumper $files);
+    foreach my $values(values %$files) {
+        foreach my $f(@$values) {
+            unlink $f or $self->app->log->warn("Failed to unlink $f: $!");
+        }
+    }
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Plugin/StackFilter/FileLink.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,104 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Plugin::StackFilter::FileLink;
+use Mojo::Base -base;
+
+sub priority { return 50; }
+
+has [ qw/config app/ ];
+
+sub apply {
+    my ($self, $thread) = @_;
+
+    $thread->each_frame(sub {
+            my $frame = shift;
+
+            $frame->file_link($self->_scm_file_link($frame->file, $frame->line));
+        }
+    );
+
+    return $thread;
+}
+
+sub _link_template {
+    my ($self, $scm, $repo) = @_;
+
+    my $linkconf = "$scm:$repo";
+    if(defined($self->{_template_cache}->{$linkconf})) {
+        return $self->{_template_cache}->{$linkconf};
+    }
+    my $template = $self->config->{WebInterface}->{ScmLinks}->{$linkconf};
+
+    # try the generic repository type
+    if(!defined($template)) {
+        $linkconf = $scm;
+        if(defined($self->{_template_cache}->{$linkconf})) {
+            return $self->{_template_cache}->{$linkconf};
+        }
+        $template = $self->config->{WebInterface}->{ScmLinks}->{$linkconf};
+    }
+
+    return undef if !defined($template);
+
+    $self->app->log->debug("Building template for $linkconf");
+
+    my $mt = Mojo::Template->new
+        ->prepend('my ($repo, $scmpath, $rev, $line) = @_;')
+        ->auto_escape(1)
+        ->escape(sub {
+            my $str = shift;
+            return Mojo::ByteStream::b($str)->url_escape;
+        })
+        ->parse($template)
+        ->build;
+    my $e = $mt->compile;
+    if($e) {
+        $self->app->log->error($e);
+        return undef;
+    }
+
+    $self->{_template_cache}->{$linkconf} = $mt;
+    return $mt;
+}
+
+sub _scm_file_link {
+    my ($self, $file, $line) = @_;
+
+    return "" unless(defined($file));
+
+    # if the file section looks like vcs:vcs_root_url:vcs_path:revision
+    if($file =~ /([^:]+):([^:]+):([^:]+):(.+)/) {
+        my ($scm, $repo, $scmpath, $rev) = ($1, $2, $3, $4);
+        my $filename = File::Spec->splitpath($scmpath);
+
+        # and we have a link template configured for this specific repository or repository type
+        my $template = $self->_link_template($scm, $repo);
+        if(defined($template)) {
+            # build url using the configured template
+            my $url = $template->interpret($repo, $scmpath, $rev, $line);
+            # and create a link to it
+            return $self->app->link_to("$filename:$line" => $url);
+        }
+    }
+
+    # else display only the filename
+    my $filebase = (File::Spec->splitpath($file))[-1];
+    if(defined($line) && $line ne "") {
+        return "$filebase:$line";
+    }
+
+    return $filebase;
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Plugin/StackFilter/FrameTrust.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,61 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Plugin::StackFilter::FrameTrust;
+use Mojo::Base -base;
+
+sub priority { return 10; }
+
+has [ qw/app/ ];
+
+sub new {
+    my $self = shift->SUPER::new(@_);
+
+    return $self;
+}
+
+sub apply {
+    my ($self, $thread) = @_;
+
+    $thread->each_frame(sub {
+            my $frame = shift;
+
+            $self->set_trust($frame);
+        }
+    );
+
+    return $thread;
+}
+
+sub set_trust {
+    my ($self, $frame) = @_;
+
+    if(!defined($frame->module) && $frame->offset ne "0x0") {
+        push @{$frame->module_warnings}, "No module loaded at this address";
+    }
+
+    if($frame->missing_symbols) {
+        push @{$frame->module_warnings}, "Missing symbols";
+    }
+
+    if($frame->corrupt_symbols) {
+        push @{$frame->module_warnings}, "Corrupt symbols";
+    }
+
+    if(defined($frame->trust) && $frame->trust eq "scan") {
+        push @{$frame->frame_warnings}, "Imprecise stackwalking";
+    }
+
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Plugin/StackFilter/HideArgs.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,56 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Plugin::StackFilter::HideArgs;
+use Mojo::Base -base;
+use Text::Balanced qw/extract_bracketed/;
+
+sub priority { return 50; }
+
+has [ qw/app/ ];
+
+sub apply {
+    my ($self, $thread) = @_;
+
+    $thread->each_frame(sub {
+            my $frame = shift;
+
+            $frame->function_name($self->shorten_signature($frame->function_name));
+        }
+    );
+
+    return $thread;
+}
+
+sub shorten_signature {
+    my ($self, $signature) = @_;
+
+    return "" if(!defined($signature) || $signature eq "");
+
+    my $short_signature = "";
+    my $text = $signature;
+    do {
+        my ($str, $next, $prefix) = extract_bracketed($text, '<(', '[^<(]*');
+        if($str) {
+            $short_signature .= $prefix . substr($str, 0, 1) . substr($str, -1, 1);
+            $text = $next;
+        } else {
+            $short_signature .= $next;
+            $text = undef;
+        }
+    } while($text);
+
+    return $self->app->t(code => (title => $signature, class => "shortened-signature prettyprint lang-cpp") => $short_signature);
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Plugin/Storage/Base.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,48 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Plugin::Storage::Base;
+use Mojo::Base -base;
+use Scalar::Util qw/weaken/;
+
+use Mojo::Loader qw/find_packages find_modules load_class/;
+has [ qw/app config/ ];
+
+sub _load_models {
+    my ($self, $model_ns) = @_;
+
+    my %models;
+
+    my @pkgs = find_packages $model_ns;
+    push @pkgs, find_modules $model_ns;
+    foreach my $pkg(@pkgs) {
+        my @pkg_comp = split(/::/, $pkg);
+        my $pkg_name = $pkg_comp[-1];
+
+        my $e = load_class $pkg;
+        warn qq{Loading "$pkg" failed: $e} and next if ref $e;
+
+        my $instance = "$pkg"->new(instance => $self, app => $self->app, config => $self->config);
+        weaken $instance->{instance};
+        weaken $instance->{app};
+        weaken $instance->{config};
+        $models{$pkg_name} = $instance;
+    }
+
+    weaken $self->{app};
+    weaken $self->{config};
+
+    return \%models;
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Plugin/Storage/File.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,98 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Plugin::Storage::File;
+use Mojo::Base 'Mojolicious::Plugin';
+
+sub register {
+    my ($self, $app, $conf) = @_;
+
+    $app->storage->register(CrashTest::Plugin::Storage::File::Instance->new(app => $app, config => $conf));
+}
+
+1;
+
+package CrashTest::Plugin::Storage::File::Instance;
+use Mojo::Base "CrashTest::Plugin::Storage::Base";
+
+has models => sub {
+    my $self = shift;
+
+    state $models = $self->_load_models("CrashTest::Plugin::Storage::File::Model");
+};
+
+1;
+
+package CrashTest::Plugin::Storage::File::Model::CrashReport;
+use Mojo::Base -base;
+use Mojo::Util qw/slurp spurt/;
+use Mojo::JSON qw/j decode_json/;
+use File::Spec;
+
+has [ qw/instance app config extra_columns data_path/ ];
+
+sub new {
+    my $self = shift->SUPER::new(@_);
+
+    $self->data_path($self->config->{DataDir});
+
+    return $self;
+}
+
+sub create {
+    my ($self, $uuid, $pjson, $client_info, $dump) = @_;
+
+    $self->_store_dump($uuid, $dump);
+    $self->_store_processed_data($uuid, $pjson, $client_info);
+}
+
+sub get_processed_data {
+    my ($self, $uuid) = @_;
+
+    my $jsonfilename = File::Spec->catfile($self->data_path, "$uuid.json");
+    my $json_content = slurp($jsonfilename) or die $!;
+
+    my $processed_data = decode_json($json_content);
+
+    return $processed_data;
+}
+
+sub _store_processed_data {
+    my ($self, $uuid, $pjson, $client_info) = @_;
+
+    # Create json for the params
+    $pjson->{client_info} = $client_info;
+
+    my $jsonfilename = File::Spec->catfile($self->data_path, "$uuid.json");
+    my $dmpfilename = File::Spec->catfile($self->data_path, "$uuid.dmp");
+
+    spurt(j($pjson), $jsonfilename) or die $!;
+
+    # Set time of the .dmp to the CrashTime
+    my $crashtime = $pjson->{client_info}->{CrashTime};
+    if(defined($crashtime)) {
+        utime $crashtime, $crashtime, $dmpfilename;
+    }
+}
+
+sub _store_dump {
+    my ($self, $uuid, $dmp_content) = @_;
+
+    my $dmp_file = File::Spec->catfile($self->data_path, "$uuid.dmp");
+    my $fh = IO::File->new($dmp_file, "w") or die($!);
+    $fh->binmode;
+    print $fh $dmp_content;
+    undef $fh;
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Plugin/Storage/Sql.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,50 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Plugin::Storage::Sql;
+use Mojo::Base 'Mojolicious::Plugin';
+
+sub register {
+    my ($self, $app, $conf) = @_;
+
+    push @{$app->commands->namespaces}, 'CrashTest::Plugin::Storage::Sql::Command';
+    $app->storage->register(CrashTest::Plugin::Storage::Sql::Instance->new(app => $app, config => $conf));
+}
+
+1;
+
+package CrashTest::Plugin::Storage::Sql::Instance;
+use Mojo::Base "CrashTest::Plugin::Storage::Base";
+use Mojo::Loader qw/load_class/;
+
+has dbtype => sub {
+    my $self = shift;
+    my @dbtypes = keys %{$self->config->{db}};
+    state $dbtype = $dbtypes[0];
+};
+
+has dbh => sub {
+    my $self = shift;
+
+    my $type = $self->dbtype;
+    load_class "Mojo::$type";
+    state $dbh = "Mojo::$type"->new($self->config->{db}->{$type});
+};
+
+has models => sub {
+    my $self = shift;
+
+    state $models = $self->_load_models("CrashTest::Plugin::Storage::Sql::Model");
+};
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Plugin/Storage/Sql/Command/db.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,65 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Plugin::Storage::Sql::Command::db;
+use Mojo::Base 'Mojolicious::Command';
+use File::Spec::Functions 'catdir';
+use File::Basename;
+
+# Short description
+has description => 'Setup database';
+
+# Short usage message
+has usage => <<EOF;
+Usage: APPLICATION db [OPTIONS]
+
+create        Create database
+reset         Remove and re-create all tables
+upgrade       Upgrade database to latest schema version
+
+EOF
+
+sub run {
+    my ($self, @args) = @_;
+
+    if(scalar @args < 1) {
+        say $self->usage;
+        exit 0;
+    }
+
+    my $path = dirname(__FILE__);
+
+    foreach my $storage_instance(@{$self->app->storage->instances}) {
+        if($storage_instance->isa("CrashTest::Plugin::Storage::Sql::Instance")) {
+
+            my $migrations = $storage_instance->dbh->migrations;
+            $migrations->name("crashtest");
+            $migrations->from_file(catdir($path, '..', 'migrations_pg.sql'));
+
+            if($args[0] eq "create") {
+                $migrations->migrate;
+            } elsif($args[0] eq "reset") {
+                $migrations->migrate(0)->migrate;
+            } elsif($args[0] eq "upgrade") {
+                $migrations->migrate;
+            } elsif($args[0] eq "downgrade") {
+            } else {
+                say "Invalid arguments";
+                exit 1;
+            }
+
+        }
+    }
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Plugin/Storage/Sql/Model/CrashReport.pm	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,213 @@
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+package CrashTest::Plugin::Storage::Sql::Model::CrashReport;
+use Mojo::Base -base;
+use Mojo::JSON qw/j decode_json/;
+use DateTime;
+use Data::Page;
+#use DBI::Log;
+#$DBI::Log::trace = 0;
+use Search::QueryParser::SQL;
+
+has [ qw/instance app config/ ];
+
+has qw/db/;
+
+sub new {
+    my $self = shift->SUPER::new(@_);
+
+    $self->db($self->instance->dbh->db);
+
+    return $self;
+}
+
+sub _build_query_from_search_string {
+    my ($self, $search) = @_;
+
+    my @values;
+
+    # define a callback to collect values safely (without magic markers)
+    my $cb = sub {
+        my ($column, $this_op, $value) = @_;
+        say $column;
+        if($column->type eq "fuzzy") {
+            $this_op = " ILIKE ";
+            $value = "%$value%";
+        }
+        push @values, $value;
+        return join('', $column->stringify, $this_op, '?');
+    };
+
+    my $search_fields = {
+        user_id     => { callback => $cb, name => 'u.user_id' },
+        product     => { callback => $cb, name => 'p.name' },
+        version     => { callback => $cb, name => 'p.version' },
+        channel     => { callback => $cb, name => 'p.release_channel' },
+        function    => { callback => $cb, name => 'extract_crashing_functions(d.processed)', type => "fuzzy" },
+    };
+
+    my $parser = Search::QueryParser::SQL->new(
+        columns         => $search_fields,
+        default_column  => "function",
+        strict          => 1,
+    );
+
+    my $query = $parser->parse($search, 1)
+        or die "Error in query: " . $parser->err;
+
+    # reset before calling dbi
+    @values = ();
+
+    return [ $query->dbi->[0], \@values ];
+}
+
+sub index {
+    my ($self, $pagen, $nperpage, $search_str) = @_;
+
+    my $where = "";
+    my @values = ();
+    if(defined($search_str) && $search_str ne "") {
+        my $q = $self->_build_query_from_search_string($search_str);
+        $where = "WHERE " . $q->[0];
+        @values = @{$q->[1]};
+    }
+
+    my $count = $self->db->query("
+        SELECT count(crash_reports.id) AS total FROM crash_reports
+        JOIN crash_users AS u ON crash_reports.crash_user_id = u.id
+        JOIN products AS p ON crash_reports.product_id = p.id
+        JOIN crash_report_datas AS d ON crash_reports.id = d.crash_report_id
+        $where
+        ", @values)->hash;
+
+    my $pager = Data::Page->new();
+    $pager->total_entries($count->{total});
+    $pager->entries_per_page($nperpage);
+    $pager->current_page($pagen);
+
+    my $results = $self->db->query("
+        SELECT  crash_reports.*,
+                p.distributor AS p_distributor, p.name AS p_name, p.version AS p_version, p.release_channel AS p_release_channel,
+                u.os AS u_os, u.cpu_arch AS u_cpu_arch, u.cpu_count AS u_cpu_count, u.extra_info AS u_extra_info
+        FROM crash_reports
+        JOIN crash_users AS u ON crash_reports.crash_user_id = u.id
+        JOIN products AS p ON crash_reports.product_id = p.id
+        JOIN crash_report_datas AS d ON crash_reports.id = d.crash_report_id
+        $where
+        ORDER BY crash_time DESC
+        OFFSET (?) ROWS
+        FETCH NEXT (?) ROWS ONLY
+        ",
+        @values,
+        $pager->skipped, $pager->entries_per_page
+    )->hashes;
+
+    return ($results, $pager);
+}
+
+sub get_processed_data {
+    my ($self, $uuid) = @_;
+
+    my $dbdata = $self->db->query("SELECT processed FROM crash_report_datas WHERE crash_report_id = (SELECT id FROM crash_reports WHERE uuid = ?)", $uuid)->hash;
+
+    my $processed_data = decode_json($dbdata->{processed});
+
+    return $processed_data;
+}
+
+sub create {
+    my ($self, $uuid, $pjson, $client_info, $dmp_content) = @_;
+
+    if(!defined($client_info->{UserID})) {
+        $self->app->log->info("Invalid crash $uuid: no UserID");
+        return;
+    }
+
+    my $tx = $self->db->begin;
+
+    my ($start_time, $crash_time);
+    if($client_info->{StartupTime}) {
+        $start_time = DateTime->from_epoch(epoch => $client_info->{StartupTime});
+    }
+    if($client_info->{CrashTime}) {
+        $crash_time = DateTime->from_epoch(epoch => $client_info->{CrashTime});
+    }
+
+    my @product_values = (
+        $client_info->{Distributor},
+        $client_info->{ProductName},
+        $client_info->{Version},
+    );
+
+    my $dbproduct = $self->db->query("SELECT * FROM products WHERE distributor = ? AND name = ? AND version = ?", @product_values)->hash;
+    if(!$dbproduct) {
+        push @product_values, $client_info->{ReleaseChannel};
+        $dbproduct = $self->db->query(
+            "INSERT INTO products (distributor, name, version, release_channel) VALUES (?, ?, ?, ?) RETURNING *",
+            @product_values
+        )->hash;
+    }
+
+    my @user_values = (
+        $client_info->{UserID},
+    );
+
+    my $dbuser = $self->db->query("SELECT * FROM crash_users WHERE user_id = ?", @user_values)->hash;
+    if(!$dbuser) {
+        push @user_values, $pjson->{system_info}->{cpu_arch};
+        push @user_values, $pjson->{system_info}->{cpu_count};
+        push @user_values, $pjson->{system_info}->{os};
+        push @user_values, j($client_info);
+
+        $dbuser = $self->db->query(
+            "INSERT INTO crash_users (user_id, cpu_arch, cpu_count, os, extra_info) VALUES (?, ?, ?, ?, ?) RETURNING *",
+            @user_values
+        )->hash;
+    }
+
+    my $main_module;
+    {
+        my $i = $pjson->{main_module};
+        $main_module = $pjson->{modules}->[$i]->{filename};
+    }
+
+    my $dbcrash = $self->db->query(
+        "INSERT INTO crash_reports (start_time, crash_time, uuid, main_module, product_id, crash_user_id) VALUES (?, ?, ?, ?, ?, ?) RETURNING id",
+        $start_time, $crash_time, $uuid, $main_module, $dbproduct->{id}, $dbuser->{id}
+    )->hash;
+
+    $self->db->query(
+        "INSERT INTO crash_report_datas (crash_report_id, processed) VALUES (?, ?) RETURNING id",
+        $dbcrash->{id}, j($pjson)
+    );
+
+    $tx->commit;
+
+    return $dbcrash->{id};
+}
+
+sub update {
+    my ($self, $uuid, $pjson, $dmp_content) = @_;
+
+    my $tx = $self->db->begin;
+
+    my $dbcrash = $self->db->query("SELECT id FROM crash_reports WHERE uuid = ?", $uuid)->hash;
+    $self->db->query("UPDATE crash_report_datas SET processed = ? WHERE crash_report_id = ?", $pjson, $dbcrash->{id});
+
+    $tx->commit;
+
+    return 1;
+}
+
+1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/CrashTest/Plugin/Storage/Sql/migrations_pg.sql	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,79 @@
+-- ###########################################################################
+-- 1 up
+-- ###########################################################################
+CREATE TABLE "crash_users" (
+      "id" serial NOT NULL,
+      "user_id" character varying(40) NOT NULL,
+      "os" character varying(40),
+      "cpu_arch" character varying(10),
+      "cpu_count" integer,
+      "extra_info" jsonb,
+      PRIMARY KEY ("id")
+);
+CREATE TABLE "products" (
+      "id" serial NOT NULL,
+      "distributor" character varying(40),
+      "name" character varying(40),
+      "version" character varying(40),
+      "release_channel" character varying,
+      PRIMARY KEY ("id")
+);
+CREATE TABLE "crash_reports" (
+      "id" serial NOT NULL,
+      "start_time" timestamp,
+      "crash_time" timestamp NOT NULL,
+      "uuid" uuid NOT NULL,
+      "main_module" character varying(40),
+      "crash_user_id" integer NOT NULL,
+      "product_id" integer NOT NULL,
+      CONSTRAINT "crash_reports_uuid_idx" UNIQUE ("uuid"),
+      PRIMARY KEY ("id")
+);
+CREATE INDEX "crash_reports_idx_crash_user_id" on "crash_reports" ("crash_user_id");
+CREATE INDEX "crash_reports_idx_product_id" on "crash_reports" ("product_id");
+CREATE INDEX "crash_reports_crash_time_idx" on "crash_reports" ("crash_time");
+CREATE TABLE "crash_report_datas" (
+      "id" serial NOT NULL,
+      "processed" jsonb NOT NULL,
+      "crash_report_id" integer NOT NULL,
+      CONSTRAINT "crash_report_datas_idx_crash_report_id" UNIQUE ("crash_report_id"),
+      PRIMARY KEY ("id")
+);
+-- CREATE INDEX "crash_report_datas_idx_crash_report_id" on "crash_report_datas" ("crash_report_id");
+ALTER TABLE "crash_reports" ADD CONSTRAINT "crash_reports_fk_crash_user_id" FOREIGN KEY ("crash_user_id")
+  REFERENCES "crash_users" ("id") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE;
+ALTER TABLE "crash_reports" ADD CONSTRAINT "crash_reports_fk_product_id" FOREIGN KEY ("product_id")
+  REFERENCES "products" ("id") ON DELETE CASCADE ON UPDATE CASCADE DEFERRABLE;
+ALTER TABLE "crash_report_datas" ADD CONSTRAINT "crash_report_datas_fk_crash_report_id" FOREIGN KEY ("crash_report_id")
+  REFERENCES "crash_reports" ("id") ON DELETE CASCADE DEFERRABLE;
+
+CREATE OR REPLACE FUNCTION extract_crashing_functions (processed_json jsonb) RETURNS text AS
+$$
+    -- extract threads[crashing_idx]->frames[*]->function
+    SELECT string_agg(functions, E'\n')
+    FROM (
+        SELECT jsonb_array_elements(
+            (($1 #> ARRAY['threads', $1->'crash_info'->>'crashing_thread'])->'frames')
+        )->>'function' AS functions
+    ) AS frames
+$$
+LANGUAGE sql IMMUTABLE;
+
+-- This extension is the contrib modules
+CREATE EXTENSION IF NOT EXISTS pg_trgm;
+
+CREATE INDEX crash_report_datas_idx_extract_crashing_functions ON crash_report_datas USING gist (
+    extract_crashing_functions(processed) gist_trgm_ops
+);
+
+-- ###########################################################################
+-- 1 down
+-- ###########################################################################
+DROP TABLE crash_report_datas CASCADE;
+DROP TABLE crash_reports CASCADE;
+DROP TABLE products CASCADE;
+DROP TABLE crash_users CASCADE;
+DROP FUNCTION extract_crashing_functions (processed_json jsonb);
+
+
+-- vim:ft=pgsql:
--- a/lib/CrashTest/StackFilter.pm	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,80 +0,0 @@
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-package CrashTest::StackFilter;
-use Mojo::Base -base;
-use Mojo::Loader qw/load_class find_modules/;
-
-has [ qw/config app filters/ ];
-
-sub new {
-    my $self = shift->SUPER::new(@_);
-
-    if(defined($self->config->{StackFilters})) {
-        $self->load_plugins($self->config->{StackFilters});
-    } else {
-        $self->find_stackfilters_plugins();
-    }
-
-    return $self;
-}
-
-sub apply {
-    my ($self, $thread) = @_;
-
-    foreach my $filter(@{$self->filters}) {
-        #say "apply filter $filter";
-        $thread = $filter->apply($thread);
-    }
-
-    return $thread;
-}
-
-sub load_plugins {
-    my ($self, $modules) = @_;
-
-    my @filters = ();
-    for my $module (@$modules) {
-        my $e = load_class($module);
-        die qq{Loading "$module" failed: $e} if ref $e;
-
-        #say "loading $module";
-        push @filters, $module->new(config => $self->config, app => $self->app);
-    }
-
-    $self->filters(\@filters);
-}
-
-sub find_stackfilters_plugins {
-    my ($self) = @_;
-
-    my @modules = ();
-
-    # Find modules in a namespace
-    for my $module (find_modules('CrashTest::StackFilters')) {
-        my $e = load_class($module);
-        warn qq{Loading "$module" failed: $e} and next if ref $e;
-
-        push @modules, [ $module, $module->priority ];
-    }
-
-    # sort by prio
-    @modules = sort { $b->[1] <=> $a->[1] } @modules;
-
-    # instanciate
-    my @filters = map { $_->[0]->new(config => $self->config, app => $self->app) } @modules;
-
-    $self->filters(\@filters);
-}
-
-1;
--- a/lib/CrashTest/StackFilters/FileLink.pm	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,105 +0,0 @@
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-package CrashTest::StackFilters::FileLink;
-use Mojo::Base -base;
-
-sub priority { return 50; }
-
-has [ qw/config app/ ];
-
-sub apply {
-    my ($self, $thread) = @_;
-
-    $thread->each_frame(sub {
-            my $frame = shift;
-
-            $frame->file_link($self->_scm_file_link($frame->file, $frame->line));
-        }
-    );
-
-    return $thread;
-}
-
-sub _link_template {
-    my ($self, $scm, $repo) = @_;
-
-    my $linkconf = "$scm:$repo";
-    if(defined($self->{_template_cache}->{$linkconf})) {
-        return $self->{_template_cache}->{$linkconf};
-    }
-    my $template = $self->config->{WebInterface}->{ScmLinks}->{$linkconf};
-
-    # try the generic repository type
-    if(!defined($template)) {
-        $linkconf = $scm;
-        if(defined($self->{_template_cache}->{$linkconf})) {
-            return $self->{_template_cache}->{$linkconf};
-        }
-        $template = $self->config->{WebInterface}->{ScmLinks}->{$linkconf};
-    }
-
-    return undef if !defined($template);
-
-    $self->app->log->debug("Building template for $linkconf");
-
-    my $mt = Mojo::Template->new
-        ->prepend('my ($repo, $scmpath, $rev, $line) = @_;')
-        ->auto_escape(1)
-        ->escape(sub {
-            my $str = shift;
-            return Mojo::ByteStream::b($str)->url_escape;
-        })
-        ->parse($template)
-        ->build;
-    my $e = $mt->compile;
-    if($e) {
-        $self->app->log->error($e);
-        return undef;
-    }
-
-    $self->{_template_cache}->{$linkconf} = $mt;
-    return $mt;
-}
-
-sub _scm_file_link {
-    my ($self, $file, $line) = @_;
-
-    return "" unless(defined($file));
-
-    # if the file section looks like vcs:vcs_root_url:vcs_path:revision
-    if($file =~ /([^:]+):([^:]+):([^:]+):(.+)/) {
-        my ($scm, $repo, $scmpath, $rev) = ($1, $2, $3, $4);
-        my $filename = File::Spec->splitpath($scmpath);
-
-        # and we have a link template configured for this specific repository or repository type
-        my $template = $self->_link_template($scm, $repo);
-        if(defined($template)) {
-            # build url using the configured template
-            my $url = $template->interpret($repo, $scmpath, $rev, $line);
-            # and create a link to it
-            return $self->app->link_to("$filename:$line" => $url);
-        }
-    }
-
-    # else display only the filename
-    my $filebase = (File::Spec->splitpath($file))[-1];
-    if(defined($line) && $line ne "") {
-        return "$filebase:$line";
-    }
-
-    return $filebase;
-}
-
-1;
-
--- a/lib/CrashTest/StackFilters/FrameTrust.pm	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,91 +0,0 @@
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-package CrashTest::StackFilters::FrameTrust;
-use Mojo::Base -base;
-
-sub priority { return 10; }
-
-has [ qw/app/ ];
-
-sub new {
-    my $self = shift->SUPER::new(@_);
-
-    # Search for inline templates in this class too
-    push @{$self->app->renderer->classes}, __PACKAGE__;
-
-    # bug in Mojolicious ?
-    # force refresh of the template cache
-    $self->app->renderer->_warmup;
-
-    return $self;
-}
-
-sub apply {
-    my ($self, $thread) = @_;
-
-    # create a pseudo controller to render templates
-    my $c = $self->app->controller_class->new(app => $self->app);
-
-    $thread->each_frame(sub {
-            my $frame = shift;
-
-            $self->set_trust($c, $frame);
-        }
-    );
-
-    return $thread;
-}
-
-sub set_trust {
-    my ($self, $c, $frame) = @_;
-
-    if(!defined($frame->module) && $frame->offset ne "0x0") {
-        $frame->module_name(
-            $c->render_to_string(
-                template => "stackfilters/frame_trust/warning", partial => 1,
-                content_text => $frame->module_name, popup_text => "No module loaded at this address"
-            )
-        );
-    }
-
-    if($frame->missing_symbols) {
-        $frame->module_name(
-            $c->render_to_string(
-                template => "stackfilters/frame_trust/warning", partial => 1,
-                content_text => $frame->module_name, popup_text => "Missing symbols"
-            )
-        );
-    }
-
-    if(defined($frame->trust) && $frame->trust eq "scan") {
-        $frame->function_name(
-            $c->render_to_string(
-                template => "stackfilters/frame_trust/warning", partial => 1,
-                content_text => $frame->function_name, popup_text => "Imprecise stackwalking"
-            )
-        );
-    }
-
-}
-
-1;
-
-__DATA__
-
-@@ stackfilters/frame_trust/warning.html.ep
-%= tag a => (href => "#", class => "text-danger", "data-toggle" => "tooltip", "data-original-title" => $popup_text) => begin
-  %= tag span => (class => "glyphicon glyphicon-warning-sign") => ""
-% end
-%= $content_text
-
--- a/lib/CrashTest/StackFilters/HideArgs.pm	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,57 +0,0 @@
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-package CrashTest::StackFilters::HideArgs;
-use Mojo::Base -base;
-use Text::Balanced qw/extract_bracketed/;
-
-sub priority { return 50; }
-
-has [ qw/app/ ];
-
-sub apply {
-    my ($self, $thread) = @_;
-
-    $thread->each_frame(sub {
-            my $frame = shift;
-
-            $frame->function_name($self->shorten_signature($frame->function_name));
-        }
-    );
-
-    return $thread;
-}
-
-sub shorten_signature {
-    my ($self, $signature) = @_;
-
-    return "" if(!defined($signature) || $signature eq "");
-
-    my $short_signature = "";
-    my $text = $signature;
-    do {
-        my ($str, $next, $prefix) = extract_bracketed($text, '<(', '[^<(]*');
-        if($str) {
-            $short_signature .= $prefix . substr($str, 0, 1) . substr($str, -1, 1);
-            $text = $next;
-        } else {
-            $short_signature .= $next;
-            $text = undef;
-        }
-    } while($text);
-
-    return $self->app->t(code => (title => $signature, class => "shortened-signature prettyprint lang-cpp") => sub { return $short_signature });
-}
-
-1;
-
--- a/lib/CrashTest/Storage/FileSystem.pm	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,120 +0,0 @@
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-package CrashTest::Storage::FileSystem;
-use Mojo::Base -base;
-use Mojo::Util qw/slurp spurt/;
-use Mojo::JSON qw/j decode_json/;
-use DateTime;
-use Data::Page;
-
-has [ qw/config data_path/ ];
-
-sub new {
-    my $self = shift->SUPER::new(@_);
-
-    $self->data_path($self->config->{DataDir});
-
-    return $self;
-}
-
-sub index {
-    my ($self, $page, $nperpage) = @_;
-
-    my @files;
-    my $dh;
-    opendir($dh, $self->data_path) or die $!;
-    my @allfiles = readdir $dh;
-    foreach(@allfiles) {
-        if($_ =~ /(.*)\.json$/) {
-            my $filename = File::Spec->catfile($self->data_path, $_);
-            if(-f $filename) {
-                push @files, {
-                    file        => $filename,
-                    uuid        => $1,
-                    product     => "",
-                    version     => "",
-                    user        => "",
-                    date        => DateTime->from_epoch(epoch => ((stat $filename)[9])),
-                };
-            }
-        }
-    }
-    closedir $dh;
-
-    my $by_date = sub { $b->{date} <=> $a->{date} };
-    my @sorted_files = sort $by_date @files;
-
-    my $pager = Data::Page->new();
-    $pager->total_entries(scalar @files);
-    $pager->entries_per_page($nperpage);
-    $pager->current_page($page);
-
-    if($page <= $pager->last_page) {
-        @sorted_files = @sorted_files[($pager->first - 1)..($pager->last - 1)];
-    } else {
-        @sorted_files = ();
-    }
-
-    foreach(@sorted_files)
-    {
-        $_->{product} = $self->get_processed_data($_->{uuid})->{client_info}->{ProductName};
-        $_->{version} = $self->get_processed_data($_->{uuid})->{client_info}->{Version};
-        $_->{user} = $self->get_processed_data($_->{uuid})->{client_info}->{UserID};
-    }
-
-    my $results = {
-        pager   => $pager,
-        crashs  => \@sorted_files,
-    };
-
-    return $results;
-}
-
-sub get_processed_data {
-    my ($self, $uuid) = @_;
-
-    my $jsonfilename = File::Spec->catfile($self->data_path, "$uuid.json");
-    my $json_content = slurp($jsonfilename) or die $!;
-
-    my $processed_data = decode_json($json_content);
-
-    return $processed_data;
-}
-
-sub store_processed_data {
-    my ($self, $uuid, $pjson) = @_;
-
-    my $jsonfilename = File::Spec->catfile($self->data_path, "$uuid.json");
-    my $dmpfilename = File::Spec->catfile($self->data_path, "$uuid.dmp");
-
-    spurt(j($pjson), $jsonfilename) or die $!;
-
-    # Set time of the .dmp to the CrashTime
-    my $crashtime = $pjson->{client_info}->{CrashTime};
-    if(defined($crashtime)) {
-        utime $crashtime, $crashtime, $dmpfilename;
-    }
-}
-
-sub store_dump {
-    my ($self, $uuid, $file) = @_;
-
-    my $dmp_file = File::Spec->catfile($self->data_path, "$uuid.dmp");
-    my $fh = IO::File->new($dmp_file, "w") or die($!);
-    $fh->binmode;
-    print $fh $file;
-    undef $fh;
-}
-
-1;
--- a/lib/CrashTest/Storage/Sql.pm	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,175 +0,0 @@
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-package CrashTest::Storage::Sql;
-use Mojo::Base "CrashTest::Storage::FileSystem";
-use Mojo::JSON qw/j decode_json/;
-use DateTime;
-use CrashTest::Storage::Sql::Schema;
-use Search::Query;
-
-has [ qw/extra_columns/ ];
-
-sub new {
-    my $self = shift->SUPER::new(@_);
-
-    $self->{schema} = CrashTest::Storage::Sql::Schema->connect($self->config->{DSN});
-
-    return $self;
-}
-
-sub index {
-    my ($self, $page, $nperpage, $search_str) = @_;
-
-    my $dbic_query = {};
-
-    if(defined($search_str) && $search_str ne "") {
-        my $search_fields = {
-            user_id     => { alias_for => 'crash_user.user_id' },
-            product     => { alias_for => 'product.name' },
-            version     => { alias_for => 'product.version' },
-            channel     => { alias_for => 'product.release_channel' },
-            function    => { alias_for => 'extract_crashing_functions(processed)' },
-        };
-        my $query = Search::Query->parser(
-            dialect => 'DBIxClass',
-            fields => $search_fields,
-            default_field => [ 'extract_crashing_functions(processed)' ],
-        )->parse($search_str);
-
-        if(defined($query)) {
-            $dbic_query = $query->as_dbic_query;
-        }
-    }
-
-    my @select = ();
-    my @sel_as = ();
-    my @extra_ids = ();
-
-    if($self->extra_columns) {
-        foreach my $extra_col(@{$self->extra_columns->{Index}}) {
-            push @select, $extra_col->{db_column};
-            push @sel_as, $extra_col->{id};
-            push @extra_ids, $extra_col->{id};
-        }
-    }
-
-    my $dbcrashs = $self->{schema}->resultset('CrashReport')->search_rs(
-        $dbic_query,
-        {
-            prefetch    => 'product',
-            join        => [ qw/crash_user crash_report_data/ ],
-            '+select'   => \@select,
-            '+as'       => \@sel_as,
-            order_by    => { -desc => 'crash_time' },
-            page        => $page,
-            rows        => $nperpage,
-        },
-    );
-
-    my $results = {
-        pager   => $dbcrashs->pager,
-        crashs  => [],
-    };
-
-    for my $crash($dbcrashs->all) {
-        my $filename = File::Spec->catfile($self->{data_path}, $crash->uuid);
-
-        my $result = {
-            file        => $filename,
-            uuid        => $crash->uuid,
-            product     => $crash->product->name,
-            version     => $crash->product->version,
-            date        => $crash->crash_time,
-        };
-
-        foreach (@extra_ids) {
-            $result->{$_} = $crash->get_column($_);
-        }
-
-        push @{$results->{crashs}}, $result;
-    }
-
-    return $results;
-}
-
-sub _db_insert_processed_data {
-    my ($self, $uuid, $pjson) = @_;
-
-    $self->{schema}->txn_do(sub {
-
-        my $crash = $self->{schema}->resultset('CrashReport')->new({ uuid => $uuid });
-
-        if($pjson->{client_info}->{StartupTime}) {
-            $crash->start_time(DateTime->from_epoch(epoch => $pjson->{client_info}->{StartupTime}));
-        }
-        if($pjson->{client_info}->{CrashTime}) {
-            $crash->crash_time(DateTime->from_epoch(epoch => $pjson->{client_info}->{CrashTime}));
-        }
-
-        my $product = {
-            distributor => $pjson->{client_info}->{Distributor},
-            name        => $pjson->{client_info}->{ProductName},
-            version     => $pjson->{client_info}->{Version},
-        };
-
-        my $dbproduct = $self->{schema}->resultset('Product')->search($product)->first();
-        if($dbproduct) {
-            $crash->product($dbproduct);
-        } else {
-            $product->{release_channel} = $pjson->{client_info}->{ReleaseChannel};
-            $crash->product($self->{schema}->resultset('Product')->new($product));
-        }
-
-        my $user = {
-            user_id => $pjson->{client_info}->{UserID},
-        };
-
-        my $dbuser = $self->{schema}->resultset('CrashUser')->search($user)->first();
-        if($dbuser) {
-            $crash->crash_user($dbuser);
-        } else {
-            $user->{cpu_arch}   = $pjson->{system_info}->{cpu_arch};
-            $user->{cpu_count}  = $pjson->{system_info}->{cpu_count};
-            $user->{os}         = $pjson->{system_info}->{os};
-            $user->{extra_info} = j($pjson->{client_info});
-            $crash->crash_user($self->{schema}->resultset('CrashUser')->new($user));
-        }
-
-        my $txt_json = j($pjson);
-        $crash->create_related('crash_report_data', {
-                processed => $txt_json,
-            });
-
-        $crash->insert;
-    });
-}
-
-sub store_processed_data {
-    my ($self, $uuid, $pjson) = @_;
-
-    $self->_db_insert_processed_data($uuid, $pjson);
-}
-
-sub get_processed_data {
-    my ($self, $uuid) = @_;
-
-    my $crash = $self->{schema}->resultset('CrashReport')->search(
-        { uuid => $uuid },
-        { prefetch => 'crash_report_data' }
-    )->first();
-
-    return decode_json($crash->crash_report_data->processed);
-}
-
-1;
--- a/lib/CrashTest/Storage/Sql/Schema.pm	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-use utf8;
-package CrashTest::Storage::Sql::Schema;
-use base qw/DBIx::Class::Schema/;
-
-our $VERSION = 2;
-
-__PACKAGE__->load_namespaces();
-
-1;
--- a/lib/CrashTest/Storage/Sql/Schema/Candy.pm	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-package CrashTest::Storage::Sql::Schema::Candy;
-
-use Mojo::Base -strict;
-use base 'DBIx::Class::Candy';
-
-sub perl_version { 12 }
-sub autotable { 1 }
-
-1;
--- a/lib/CrashTest/Storage/Sql/Schema/Result/CrashReport.pm	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,67 +0,0 @@
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-package CrashTest::Storage::Sql::Schema::Result::CrashReport;
-
-use CrashTest::Storage::Sql::Schema::Candy;
-__PACKAGE__->load_components(qw/InflateColumn::DateTime Core/);
-
-primary_column id => {
-    data_type => 'int',
-    is_auto_increment => 1,
-};
-
-column start_time => {
-    data_type => 'timestamp',
-    inflate_datetime => 1,
-    is_nullable => 1,
-};
-
-column crash_time => {
-    data_type => 'timestamp',
-    inflate_datetime => 1,
-    is_nullable => 1,
-};
-
-column uuid => {
-    data_type => 'uuid',
-};
-
-column bug_reference => {
-    data_type => 'varchar',
-    size => 20,
-    is_nullable => 1,
-};
-
-has_one crash_report_data => 'CrashTest::Storage::Sql::Schema::Result::CrashReportData', 'crash_report_id';
-
-column crash_user_id => { data_type => 'int' };
-belongs_to crash_user => 'CrashTest::Storage::Sql::Schema::Result::CrashUser', 'crash_user_id';
-
-column product_id => { data_type => 'int' };
-belongs_to product => 'CrashTest::Storage::Sql::Schema::Result::Product', 'product_id';
-
-sub sqlt_deploy_hook {
-    my ($self, $sqlt_table) = @_;
-    $sqlt_table->add_index(
-        name => 'crash_reports_uuid_idx',
-        fields => [ 'uuid' ],
-        type   => 'unique',
-    );
-    $sqlt_table->add_index(
-        name => 'crash_reports_crash_time_idx',
-        fields => [ 'crash_time' ]
-    );
-}
-
-1;
--- a/lib/CrashTest/Storage/Sql/Schema/Result/CrashReportData.pm	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,30 +0,0 @@
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-package CrashTest::Storage::Sql::Schema::Result::CrashReportData;
-
-use CrashTest::Storage::Sql::Schema::Candy;
-
-primary_column id => {
-    data_type => 'int',
-    is_auto_increment => 1,
-};
-
-column processed => {
-    data_type => 'jsonb',
-};
-
-column crash_report_id => { data_type => 'int' };
-belongs_to crash_report => 'CrashTest::Storage::Sql::Schema::Result::CrashReport', 'crash_report_id';
-
-1;
--- a/lib/CrashTest/Storage/Sql/Schema/Result/CrashUser.pm	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,52 +0,0 @@
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-package CrashTest::Storage::Sql::Schema::Result::CrashUser;
-
-use CrashTest::Storage::Sql::Schema::Candy;
-
-primary_column id => {
-    data_type => 'int',
-    is_auto_increment => 1,
-};
-
-column user_id => {
-    data_type => 'varchar',
-    size => 40,
-};
-
-column os => {
-    data_type => 'varchar',
-    size => 40,
-    is_nullable => 1,
-};
-
-column cpu_arch => {
-    data_type => 'varchar',
-    size => 10,
-    is_nullable => 1,
-};
-
-column cpu_count => {
-    data_type => 'int',
-    is_nullable => 1,
-};
-
-column extra_info => {
-    data_type => 'text',
-    is_nullable => 1,
-};
-
-has_many crash_report => 'CrashTest::Storage::Sql::Schema::Result::CrashReport', 'crash_user_id';
-
-1;
--- a/lib/CrashTest/Storage/Sql/Schema/Result/Module.pm	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,41 +0,0 @@
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-package CrashTest::Storage::Sql::Schema::Result::Module;
-
-use CrashTest::Storage::Sql::Schema::Candy;
-
-primary_column id => {
-    data_type => 'int',
-    is_auto_increment => 1,
-};
-
-column debug_id => {
-    data_type => 'varchar',
-    size => 33,
-};
-
-column filename => {
-    data_type => 'varchar',
-    size => 128,
-};
-
-column version => {
-    data_type => 'varchar',
-    size => 64,
-    is_nullable => 1,
-};
-
-unique_constraint module_id => ['debug_id', 'filename'];
-
-1;
--- a/lib/CrashTest/Storage/Sql/Schema/Result/Product.pm	Sun Sep 27 22:45:40 2015 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,48 +0,0 @@
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-package CrashTest::Storage::Sql::Schema::Result::Product;
-
-use CrashTest::Storage::Sql::Schema::Candy;
-
-primary_column id => {
-    data_type => 'int',
-    is_auto_increment => 1,
-};
-
-column distributor => {
-    data_type => 'varchar',
-    size => 40,
-    is_nullable => 1,
-};
-
-column name => {
-    data_type => 'varchar',
-    size => 40,
-    is_nullable => 1,
-};
-
-column version => {
-    data_type => 'varchar',
-    size => 40,
-    is_nullable => 1,
-};
-
-column release_channel => {
-    data_type => 'varchar',
-    is_nullable => 16,
-};
-
-has_many crash_report => 'CrashTest::Storage::Sql::Schema::Result::CrashReport', 'product_id';
-
-1;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/script/CrashTest	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,25 @@
+#!/usr/bin/env perl
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# ABSTRACT: Web interface for breakpad
+
+use strict;
+use warnings;
+
+use lib 'lib';
+
+# Start command line interface for application
+require Mojolicious::Commands;
+Mojolicious::Commands->start_app('CrashTest');
--- a/templates/index.atom.ep	Sun Sep 27 22:45:40 2015 +0200
+++ b/templates/index.atom.ep	Wed Nov 04 17:43:00 2015 +0100
@@ -4,22 +4,22 @@
   <link type="text/html" rel="alternate" href="<%= url_for()->to_abs =%>"/>
   <link type="application/atom+xml" rel="self" href="<%= url_for('current', format => 'atom')->to_abs =%>"/>
   <title>Recent Crashs</title>
-  <updated><%= @$files[0]->{date}->strftime("%FT%TZ") =%></updated>
+  <updated><%= date_with_tz_from_db_utc(@$crashs[0]->{crash_time}) =%></updated>
 
-% foreach my $file(@$files) {
+% foreach my $crash(@$crashs) {
   <entry>
-    <id>tag:crash,2014:uuid::<%= $file->{uuid} =%></id>
-    <link type="text/html" rel="alternate" href="<%= url_for('report', uuid => $file->{uuid})->to_abs =%>"/>
-    <title><%= $file->{product} =%>: <%= $file->{signature} =%></title>
-    <updated><%= $file->{date}->strftime("%FT%TZ") =%></updated>
-    <published><%= $file->{date}->strftime("%FT%TZ") =%></published>
+    <id>tag:crash,2014:uuid::<%= $crash->{uuid} =%></id>
+    <link type="text/html" rel="alternate" href="<%= url_for('report', uuid => $crash->{uuid})->to_abs =%>"/>
+    <title><%= $crash->{p_name} =%>&nbsp;<%= $crash->{p_version} =%>: <%= $crash->{uuid} =%></title>
+    <updated><%= date_with_tz_from_db_utc($crash->{crash_time}) =%></updated>
+    <published><%= date_with_tz_from_db_utc($crash->{crash_time}) =%></published>
     <author>
       <name></name>
     </author>
     <content type="html">
     %= xml_escape_block begin
-      %= t h3 => $file->{date}->strftime("%F %T")
-      %= t h3 => $file->{product} . " version " . $file->{version}
+      %= t h3 => date_from_db_utc($crash->{crash_time})
+      %= t h3 => $crash->{p_name} . " version " . $crash->{p_version}
     % end
     </content>
   </entry>
--- a/templates/index.html.ep	Sun Sep 27 22:45:40 2015 +0200
+++ b/templates/index.html.ep	Wed Nov 04 17:43:00 2015 +0100
@@ -7,25 +7,26 @@
     <th>Version</th>
     <th>UUID</th>
     % foreach my $extra_col(@$extra_columns) {
-      %= t th => $extra_col->{name}
+    %= t th => $extra_col->{name}
     % }
     <th>Date</th>
   </tr>
 </thead>
-% foreach my $crash(@$files) {
+% foreach my $crash(@$crashs) {
   %= t tr => begin
-    %= t td => $crash->{product}
-    %= t td => $crash->{version}
+    %= t td => $crash->{p_name}
+    %= t td => ($crash->{p_version} or "")
     %= t td => (style => "font-family:monospace;") => begin
       %= link_to $crash->{uuid} => url_for('report', uuid => $crash->{uuid})
     % end
     % foreach my $extra_col(@$extra_columns) {
-      %= t td => $crash->{$extra_col->{id}}
+    %= t td => $crash->{$extra_col->{id}}
     % }
-    %= t td => $crash->{date}->set_time_zone('UTC')->set_time_zone('local')->strftime("%F %T")
+    %= t td => date_from_db_utc($crash->{crash_time})
   % end
 % }
 % end
+
 % if($pager->first_page != $pager->last_page) {
   %= bootstrap_pagination($pager->current_page, $pager->last_page);
 % }
--- a/templates/report/backtrace.html.ep	Sun Sep 27 22:45:40 2015 +0200
+++ b/templates/report/backtrace.html.ep	Wed Nov 04 17:43:00 2015 +0100
@@ -15,4 +15,3 @@
     % end
   % }
 </div>
-
--- a/templates/report/backtrace/frames.html.ep	Sun Sep 27 22:45:40 2015 +0200
+++ b/templates/report/backtrace/frames.html.ep	Wed Nov 04 17:43:00 2015 +0100
@@ -13,9 +13,11 @@
       %= $frame->frame_number
     %  end
     %= t td => (class => 'col-md-2') => begin
+      %= module_warnings($frame)
       %= $frame->module_name
     % end
     %= t td => (class => 'col-md-5') => begin
+      %= frame_warnings($frame)
       %= $frame->function_name
     % end
     %= t td => (class => 'col-md-4') => begin
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/templates/report/backtrace/warning.html.ep	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,3 @@
+%= tag a => (href => "#", class => "text-danger", "data-toggle" => "tooltip", "data-original-title" => $tooltip_text) => begin
+  %= tag span => (class => "glyphicon glyphicon-warning-sign") => ""
+% end
--- a/templates/report/client_info.html.ep	Sun Sep 27 22:45:40 2015 +0200
+++ b/templates/report/client_info.html.ep	Wed Nov 04 17:43:00 2015 +0100
@@ -1,3 +1,11 @@
+<tr>
+  %= t td => 'Start Time'
+  %= t td => $client_info->{StartupTime}
+</tr>
+<tr>
+  %= t td => 'Crash Time'
+  %= t td => $client_info->{CrashTime}
+</tr>
 <tr>
   %= t td => 'Product'
   %= t td => $client_info->{ProductName}
@@ -16,9 +24,13 @@
 </tr>
 <tr>
   %= t td => 'Release Channel'
-  %= t td => $client_info->{ReleaseChannel}
+  %= t td => $client_info->{ReleaseChannel} || ""
 </tr>
 <tr>
   %= t td => 'UUID'
   %= t td => $self->param('uuid');
 </tr>
+<tr>
+  %= t td => 'User ID'
+  %= t td => $client_info->{UserID};
+</tr>
--- a/templates/report/crash.html.ep	Sun Sep 27 22:45:40 2015 +0200
+++ b/templates/report/crash.html.ep	Wed Nov 04 17:43:00 2015 +0100
@@ -20,6 +20,7 @@
     % end
     %= t div => (class => 'tab-pane', id => 'details') => begin
      %= t table => (class => 'table table-striped table-hover table-bordered table-condensed') => begin
+      %= include('report/crash_info', crash_info => $processed_data->{crash_info});
       %= include('report/client_info', client_info => $processed_data->{client_info});
       %= include('report/system_info', system_info => $processed_data->{system_info});
      % end
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/templates/report/crash_info.html.ep	Wed Nov 04 17:43:00 2015 +0100
@@ -0,0 +1,4 @@
+<tr>
+  %= t td => 'Crash Type'
+  %= t td => $crash_info->{type}
+</tr>