diff --git a/js/app/controllers/EpnTapModule.js b/js/app/controllers/EpnTapModule.js
index e875f47..e6f98cd 100644
--- a/js/app/controllers/EpnTapModule.js
+++ b/js/app/controllers/EpnTapModule.js
@@ -58,7 +58,7 @@ Ext.define('amdaDesktop.EpnTapModule', {
 		this.targetNamesStore = Ext.data.StoreManager.lookup('targetNamesStore');
 	},
 
-	addUIListeners: function() {
+	addListeners: function() {
 		this.targetNameCB.on('change', function() {
 			this.updateGetBtnStatus();
 		}, this);
@@ -101,7 +101,7 @@ Ext.define('amdaDesktop.EpnTapModule', {
 	loadTarget: function(target) {
 		this.target = target;
 		this.aquireElements();
-		this.addUIListeners();
+		this.addListeners();
 
 		// this.servicesStore.clearFilter();
 		this.granulesStore.removeAll();
@@ -145,6 +145,7 @@ Ext.define('amdaDesktop.EpnTapModule', {
 					'timeMin': timeMin,
 					'timeMax': timeMax
 				},
+				// timeout: 3000,
 				success: this.updateNbResults,
 				failure: this.updateNbResultsFail,
 				scope: this
@@ -191,30 +192,22 @@ Ext.define('amdaDesktop.EpnTapModule', {
 	},
 
 	onPageChanged: function() {
-		var myProxy = this.granulesStore.getProxy();
-		myProxy.params = this.getGranuleParams();
-		myProxy.extraParams = this.getGranuleParams();
-		// //--- Set value to your parameter  ----//
-		// myProxy.setExtraParam('url', this.selectedService.data['access_url']);
-  // myProxy.setExtraParam('MENU_DETAIL', '555555');
+		this.granulesStore.getProxy().extraParams = this.getGranuleParams();
 	},
+
 	/**
 	Trigerred when a row is clicked in `servicesGrid` table (see `EpnTapUI.createServicesGrid()`). Among other things,
 	send a new query and fill `granulesGrid`.
 	*/
 	onServiceSelected: function(record) {
+		// Ext.Ajax.abortAll();
 		this.selectedService = record;
 		var nbRes = this.selectedService.get('nb_results');
 		if(nbRes > 0 && !isNaN(nbRes)) {
-			loadMask.show();
 			this.granulesStore.load({
 				params: this.getGranuleParams(),
 				start: 0,
 				limit: this.granulesStore.pageSize,
-				callback: function(records, operation, success) {
-					console.log(records, operation);
-					loadMask.hide();
-				},
 				scope: this
 			});
 		}
diff --git a/js/app/views/EpnTapUI.js b/js/app/views/EpnTapUI.js
index 337da87..73e219a 100644
--- a/js/app/views/EpnTapUI.js
+++ b/js/app/views/EpnTapUI.js
@@ -113,7 +113,10 @@ Ext.create('Ext.data.Store', {
 	sorters: [
 		{property: 'nb_results', direction: 'DESC'},
 		{property: 'short_name', direction: 'ASC'}
-	]
+	],
+	listeners: {
+		// load: function(record) { console.log(record); }
+	}
 });
 
 /**
@@ -136,21 +139,22 @@ selected.
 - `thumbnail_url`: the thumbnail_url EPN-TAP parameter (ibid).
 */
 Ext.create('Ext.data.Store', {
-	storeId:'granulesStore',
-	pageSize: 25,
+	id: 'granulesStore',
+	model: 'granulesModel',
 	autoload: false,
-	fields:['num', 'dataproduct_type', 'target_name', 'time_min', 'time_max', 'access_format', 'granule_uid', 'access_estsize', 'access_url', 'thumbnail_url'],
+	pageSize: 25,
 	proxy: {
 		type: 'ajax',
 		url: 'php/epntap.php',
 		reader: {
 			type: 'json',
-			root: 'rows'
+			root: 'data'
 		}
-	},
-	sorters: [
-		{property: 'num', direction: 'ASC'}
-	]
+	}, listeners: {
+		'metachange': function(store, meta) {
+			Ext.getCmp('epnTapGranulesGrid').reconfigure(store, meta.columns);
+		}
+	}
 });
 
 /**
@@ -385,7 +389,6 @@ Ext.define('amdaUI.EpnTapUI', {
 		};
 	},
 
-
 	/**
 	Create `epnTapServicesGrid`, an ExtJS grid containing the EPN-TAP services matching with the filter form
 	(`serviceFilterPanel`).
@@ -472,7 +475,6 @@ Ext.define('amdaUI.EpnTapUI', {
 				}
 			}
 		});
-
 		return epnTapServicesGrid;
 	},
 
@@ -498,6 +500,7 @@ Ext.define('amdaUI.EpnTapUI', {
 	- currently only the granule thumbnail, in full size.
 
 	A click on a granule triggers `EpnTapModule.onGranuleSelected()`.
+	TODO: infinite scroll! http://docs.sencha.com/extjs/4.2.3/extjs-build/examples/grid/infinite-scroll-with-filter.html
 	*/
 	createGranulesGrid: function() {
 		var txtRender = function(val) {
@@ -510,9 +513,8 @@ Ext.define('amdaUI.EpnTapUI', {
 			return '<img width="40px height="40px" src="' + val + '">';
 		};
 		var dptRender = function(val) {
-			console.log(Ext.data.StoreManager.lookup('productTypesStore'));
-			return val;
-			// (val in productTypeDict) ? '<p style="white-space: normal;">' + productTypeDict[val] + '</p>' : '<em>' + val + '</em>';
+			var productTypeDict = Ext.data.StoreManager.lookup('productTypesStore').data.map;
+			return productTypeDict[val].data.name;
 		};
 		var formatRender = function(val) {
 			// A dictionnary used to associate the mimetype (i.e "application/fits") to a pretty printed word (i.e "fits").
@@ -585,16 +587,16 @@ Ext.define('amdaUI.EpnTapUI', {
 			store: Ext.data.StoreManager.lookup('granulesStore'),
 			flex: 4,
 			columns: [
-				{ text: 'Num',  dataIndex: 'num', flex: 1, renderer: txtRender },
+				// { text: 'Num',  dataIndex: 'num', flex: 1, renderer: txtRender, hidden: true },
 				// { text: 'Type',  dataIndex: 'dataproduct_type', flex: 2, renderer: dptRender },
-				{ text: 'Target', dataIndex: 'target_name', flex: 2, renderer: txtRender },
-				{ text: 'Time min', dataIndex: 'time_min', flex: 2, renderer: txtRender },
-				{ text: 'Time max', dataIndex: 'time_max', flex: 2, renderer: txtRender },
+				// { text: 'Target', dataIndex: 'target_name', flex: 2, renderer: txtRender, hidden: true },
+				// { text: 'Time min', dataIndex: 'time_min', flex: 2, renderer: txtRender },
+				// { text: 'Time max', dataIndex: 'time_max', flex: 2, renderer: txtRender },
 				// { text: 'Format', dataIndex: 'access_format', flex: 2, renderer: formatRender },
-				{ text: 'uid', dataIndex: 'granule_uid', flex: 2, renderer: txtRender },
-				{ text: 'Size', dataIndex: 'access_estsize', flex: 1, renderer: sizeRender },
-				{ text: 'URL', dataIndex: 'access_url', flex: 1, renderer: linkRender },
-				{ text: 'Thumb.', dataIndex: 'thumbnail_url', flex: 1, renderer: imgRender}
+				// { text: 'uid', dataIndex: 'granule_uid', flex: 2, renderer: txtRender },
+				// { text: 'Size', dataIndex: 'access_estsize', flex: 1, renderer: sizeRender },
+				// { text: 'URL', dataIndex: 'access_url', flex: 1, renderer: linkRender },
+				// { text: 'Thumb.', dataIndex: 'thumbnail_url', flex: 1, renderer: imgRender}
 			],
 			dockedItems: [{
 				xtype: 'pagingtoolbar',
diff --git a/php/epntap.php b/php/epntap.php
index 98fb943..bdb4d93 100644
--- a/php/epntap.php
+++ b/php/epntap.php
@@ -8,20 +8,23 @@ $action = preg_replace("/[^a-zA-Z]+/", "", filter_var($_GET['action'], FILTER_SA
 
 switch ($action) {
 	case 'resolver':
-		echo json_encode(resolver());
+		$response = json_encode(resolver());
 		break;
 	case 'getServices':
-		echo json_encode(getServices());
+		$response = json_encode(getServices());
 		break;
 	case 'getNbResults':
-		echo getNbResults();
+		$response = getNbResults();
 		break;
 	case 'getGranules':
-		echo json_encode(getGranules());
+		$response = json_encode(getGranules());
 		break;
 	default:
-		echo 'unknown action';
+		$response = 'unknown action';
+		break;
 }
+// error_log('epntap response: ' . $response);
+echo $response;
 
 function resolver() {
 	$input = filter_var($_GET['input'], FILTER_SANITIZE_URL);
@@ -41,13 +44,14 @@ function resolver() {
 function getServices() {
 	$registriesURL = ["http://registry.euro-vo.org/regtap/tap", "http://dc.zah.uni-heidelberg.de/tap", "http://gavo.aip.de/tap", "http://reg.g-vo.org/tap"];
 	$columns = ['short_name', 'res_title', 'ivoid', 'access_url', 'table_name', 'content_type', 'creator_seq', 'content_level', 'reference_url', 'created', 'updated'];
-	$getServicesQuery = "SELECT DISTINCT " . implode(', ', $columns) . " FROM rr.resource
+	$query = "SELECT DISTINCT " . implode(', ', $columns) . " FROM rr.resource
 			NATURAL JOIN rr.res_schema NATURAL JOIN rr.res_table NATURAL JOIN rr.interface NATURAL JOIN rr.res_detail NATURAL JOIN rr.capability
 			WHERE standard_id='ivo://ivoa.net/std/tap' AND intf_type='vs:paramhttp' AND detail_xpath='/capability/dataModel/@ivo-id'
 			AND 1=ivo_nocasematch(detail_value, 'ivo://vopdc.obspm/std/EpnCore%') AND table_name LIKE '%.epn_core' ORDER BY short_name, table_name";
+	// error_log('getServices query: ' . $query);
 
 	for($i=0; $i<count($registriesURL); $i++) {
-		$services = request($registriesURL[$i], $getServicesQuery);
+		$services = request($registriesURL[$i], $query);
 		if(! array_key_exists("error", $services)) {
 			for($j=0; $j<count($services); $j++) {
 				$services[$j]['id'] = generateServiceId($services[$j]);
@@ -78,6 +82,7 @@ function getNbResults() {
 	$timeMax = filter_var($_GET['timeMax'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
 
 	$query = "SELECT COUNT(*) AS nb_rows FROM $tableName" . createFilter($targetName, $productTypes, $timeMin, $timeMax);
+	// error_log('getNbResults query: ' . $query);
 	$result = request($url, $query);
 	if(count($result) < 1) {
 		return 'Too few returned raws.';
@@ -97,7 +102,7 @@ function getNbResults() {
 }
 
 function getGranules() {
-	error_log(json_encode($_GET));
+	// error_log('getGranules GET: ' . json_encode($_GET));
 	$url = filter_var($_GET['url'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
 	$tableName = filter_var($_GET['tableName'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
 	$targetName = filter_var($_GET['targetName'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
@@ -108,13 +113,20 @@ function getGranules() {
 	$limit = filter_var($_GET['limit'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
 	$nbRes = filter_var($_GET['nbRes'], FILTER_SANITIZE_STRING, FILTER_FLAG_STRIP_LOW);
 
-	// TODO find a way to handle 'non existing key errors' (and then add access_format)
-	$columns = "dataproduct_type, target_name, time_min, time_max, granule_uid, access_estsize, access_url, thumbnail_url";
 	$filter = createFilter($targetName, $productTypes, $timeMin, $timeMax);
-	$query = "SELECT TOP $limit $columns FROM $tableName $filter OFFSET $start";
+	$query = "SELECT TOP $limit * FROM $tableName $filter OFFSET $start";
+	// error_log('getGranules query: ' . $query);
 	$rows = request($url, $query);
-	return ['success' => true, 'total' => $nbRes, 'rows' => $rows];
-	// return $rows;
+
+	$fields = array();
+	$columns = array();
+	foreach($rows[0] as $key => $value) {
+		$fields[] = ['name' => $key, 'type' => 'string'];
+		$columns[] = ['text' => ucfirst(str_replace('_', ' ', $key)), 'dataIndex' => $key];
+	}
+
+	$metadata = ['fields' => $fields, 'columns' => $columns, 'root' => 'data'];
+	return ['data' => $rows, 'total' => $nbRes, 'metaData' => $metadata];
 }
 
 // ----- utils -----
--
libgit2 0.21.2